diff --git a/CLAUDE.md b/CLAUDE.md index e17043d..dd461b1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -103,7 +103,7 @@ Rust patterns, error handling, and anti-patterns are defined in `.claude/rules/r - **anyhow::Result** everywhere, always `.context("description")?` - **No unwrap()** in production code -- **lazy_static!** for all regex (never compile inside a function) +- **`LazyLock` statics** for all regex (never compile on every function call) - **Fallback pattern**: if filter fails, execute raw command unchanged - **No async**: single-threaded by design (startup <10ms) - **Exit code propagation**: `std::process::exit(code)` on child failure diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 714ec80..8ae0606 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -60,7 +60,7 @@ Every filter needs a fallback path. Every hook must handle malformed input grace <10ms startup. No async runtime. No config file I/O on the critical path. If developers perceive any delay, they'll disable RTK. Speed is the difference between adoption and abandonment. -`lazy_static!` for all regex. No network calls. No disk reads in the hot path. Benchmark before/after with `hyperfine`. +Use `LazyLock` statics for all regex. No network calls. No disk reads in the hot path. Benchmark before/after with `hyperfine`. ### Extensibility diff --git a/Cargo.lock b/Cargo.lock index 48875b9..5503fca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -672,12 +672,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "leb128fmt" version = "0.1.0" @@ -902,7 +896,6 @@ dependencies = [ "flate2", "getrandom 0.4.2", "ignore", - "lazy_static", "libc", "quick-xml", "regex", diff --git a/Cargo.toml b/Cargo.toml index 7679050..d945b73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,6 @@ anyhow = "1.0" ignore = "0.4" walkdir = "2" regex = "1" -lazy_static = "1.4" serde = { version = "1", features = ["derive"] } serde_json = { version = "1", features = ["preserve_order"] } colored = "3" diff --git a/docs/contributing/CODING_PRACTICES.md b/docs/contributing/CODING_PRACTICES.md index 893f544..63f9ea9 100644 --- a/docs/contributing/CODING_PRACTICES.md +++ b/docs/contributing/CODING_PRACTICES.md @@ -13,7 +13,7 @@ Our goal is to keep the codebase consistent and easy to extend. PRs that deviate New to RTK? The fastest path to a mergeable first PR: 1. **Read the flow once.** Start at [`CONTRIBUTING.md`](../../CONTRIBUTING.md), then skim [`docs/contributing/TECHNICAL.md`](TECHNICAL.md) to see how a command flows from `main.rs` → a `*_cmd.rs` filter → tracking → stdout. -2. **Look at a good example.** [`src/cmds/git/git.rs`](../../src/cmds/git/git.rs) is a representative filter — it shows the `run()` entry point, `lazy_static!` regex setup, filter helpers, and embedded tests all in one file. +2. **Look at a good example.** [`src/cmds/git/git.rs`](../../src/cmds/git/git.rs) is a representative filter — it shows the `run()` entry point, `LazyLock` regex setup, filter helpers, and embedded tests all in one file. 3. **Know the shared helpers before reimplementing.** Two files cover most of what you need: - [`src/core/runner.rs`](../../src/core/runner.rs) — command execution wrappers: `run_filtered()` (run a command, then apply your filter function), `run_passthrough()` (run unfiltered but tracked), `run_streamed()` (streaming filter). - [`src/core/utils.rs`](../../src/core/utils.rs) — shared utilities: `resolved_command()`, `strip_ansi()`, `truncate()`, `count_tokens()`, and more. diff --git a/docs/contributing/TECHNICAL.md b/docs/contributing/TECHNICAL.md index d9c8f99..ec114cb 100644 --- a/docs/contributing/TECHNICAL.md +++ b/docs/contributing/TECHNICAL.md @@ -370,7 +370,7 @@ Declarative filters with an 8-stage pipeline: strip ANSI, regex replace, match o Achieved through: - Zero async overhead (single-threaded, no tokio) -- Lazy regex compilation (`lazy_static!`) +- Lazy regex compilation (`LazyLock`) - Minimal allocations (borrow over clone) - No config file I/O on startup (loaded on-demand) diff --git a/src/cmds/cloud/aws_cmd.rs b/src/cmds/cloud/aws_cmd.rs index fa76d73..a96d2c8 100644 --- a/src/cmds/cloud/aws_cmd.rs +++ b/src/cmds/cloud/aws_cmd.rs @@ -13,9 +13,9 @@ use crate::core::utils::{ }; use crate::json_cmd; use anyhow::{Context, Result}; -use lazy_static::lazy_static; use regex::Regex; use serde_json::Value; +use std::sync::LazyLock; const MAX_ITEMS: usize = CAP_LIST; const JSON_COMPRESS_DEPTH: usize = 4; @@ -1325,9 +1325,8 @@ fn filter_eks_cluster(json_str: &str) -> Option { Some(FilterResult::new(text)) } -lazy_static! { - static ref S3_TRANSFER_RE: Regex = Regex::new(r"^(upload|download|delete|copy|move):").unwrap(); -} +static S3_TRANSFER_RE: LazyLock = + LazyLock::new(|| Regex::new(r"^(upload|download|delete|copy|move):").unwrap()); fn filter_sqs_messages(json_str: &str) -> Option { let v: Value = serde_json::from_str(json_str).ok()?; diff --git a/src/cmds/cloud/psql_cmd.rs b/src/cmds/cloud/psql_cmd.rs index c250538..5355d5b 100644 --- a/src/cmds/cloud/psql_cmd.rs +++ b/src/cmds/cloud/psql_cmd.rs @@ -7,18 +7,18 @@ use crate::core::runner::{self, RunOptions}; use crate::core::truncate::CAP_LIST; use crate::core::utils::resolved_command; use anyhow::Result; -use lazy_static::lazy_static; use regex::Regex; +use std::sync::LazyLock; const MAX_TABLE_ROWS: usize = CAP_LIST; const MAX_EXPANDED_RECORDS: usize = CAP_LIST; -lazy_static! { - static ref EXPANDED_RECORD: Regex = Regex::new(r"-\[ RECORD \d+ \]-").unwrap(); - static ref SEPARATOR: Regex = Regex::new(r"^[-+]+$").unwrap(); - static ref ROW_COUNT: Regex = Regex::new(r"^\(\d+ rows?\)$").unwrap(); - static ref RECORD_HEADER: Regex = Regex::new(r"^-\[ RECORD (\d+) \]-").unwrap(); -} +static EXPANDED_RECORD: LazyLock = + LazyLock::new(|| Regex::new(r"-\[ RECORD \d+ \]-").unwrap()); +static SEPARATOR: LazyLock = LazyLock::new(|| Regex::new(r"^[-+]+$").unwrap()); +static ROW_COUNT: LazyLock = LazyLock::new(|| Regex::new(r"^\(\d+ rows?\)$").unwrap()); +static RECORD_HEADER: LazyLock = + LazyLock::new(|| Regex::new(r"^-\[ RECORD (\d+) \]-").unwrap()); // Edge cases vs previous manual implementation: // - On failure: stderr is no longer eprinted on the success path (only on failure via early_exit) diff --git a/src/cmds/dotnet/binlog.rs b/src/cmds/dotnet/binlog.rs index 09a23e3..d643c41 100644 --- a/src/cmds/dotnet/binlog.rs +++ b/src/cmds/dotnet/binlog.rs @@ -3,11 +3,11 @@ use crate::core::utils::strip_ansi; use anyhow::{Context, Result}; use flate2::read::GzDecoder; -use lazy_static::lazy_static; use regex::Regex; use std::collections::HashSet; use std::io::{Cursor, Read}; use std::path::Path; +use std::sync::LazyLock; #[derive(Debug, Clone, PartialEq, Eq)] pub struct BinlogIssue { @@ -52,63 +52,74 @@ pub struct RestoreSummary { pub duration_text: Option, } -lazy_static! { - static ref ISSUE_RE: Regex = Regex::new( +static ISSUE_RE: LazyLock = LazyLock::new(|| { + Regex::new( r"(?m)^\s*(?P[^\r\n:(]+)\((?P\d+),(?P\d+)\):\s*(?Perror|warning)\s*(?:(?P[A-Za-z]+\d+)\s*:\s*)?(?P.*)$" ) - .expect("valid regex"); - static ref BUILD_SUMMARY_RE: Regex = Regex::new(r"(?mi)^\s*(?P\d+)\s+(?Pwarning|error)\(s\)") - .expect("valid regex"); - static ref ERROR_COUNT_RE: Regex = - Regex::new(r"(?i)\b(?P\d+)\s+error\(s\)").expect("valid regex"); - static ref WARNING_COUNT_RE: Regex = - Regex::new(r"(?i)\b(?P\d+)\s+warning\(s\)").expect("valid regex"); - static ref FALLBACK_ERROR_LINE_RE: Regex = - Regex::new(r"(?mi)^.+\(\d+,\d+\):\s*error(?:\s+[A-Za-z]{2,}\d{3,})?(?:\s*:.*)?$") - .expect("valid regex"); - static ref FALLBACK_WARNING_LINE_RE: Regex = - Regex::new(r"(?mi)^.+\(\d+,\d+\):\s*warning(?:\s+[A-Za-z]{2,}\d{3,})?(?:\s*:.*)?$") - .expect("valid regex"); - static ref DURATION_RE: Regex = - Regex::new(r"(?m)^\s*Time Elapsed\s+(?P[^\r\n]+)$").expect("valid regex"); - static ref TEST_RESULT_RE: Regex = Regex::new( + .expect("valid regex") +}); +static BUILD_SUMMARY_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"(?mi)^\s*(?P\d+)\s+(?Pwarning|error)\(s\)").expect("valid regex") +}); +static ERROR_COUNT_RE: LazyLock = + LazyLock::new(|| Regex::new(r"(?i)\b(?P\d+)\s+error\(s\)").expect("valid regex")); +static WARNING_COUNT_RE: LazyLock = + LazyLock::new(|| Regex::new(r"(?i)\b(?P\d+)\s+warning\(s\)").expect("valid regex")); +static FALLBACK_ERROR_LINE_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"(?mi)^.+\(\d+,\d+\):\s*error(?:\s+[A-Za-z]{2,}\d{3,})?(?:\s*:.*)?$") + .expect("valid regex") +}); +static FALLBACK_WARNING_LINE_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"(?mi)^.+\(\d+,\d+\):\s*warning(?:\s+[A-Za-z]{2,}\d{3,})?(?:\s*:.*)?$") + .expect("valid regex") +}); +static DURATION_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"(?m)^\s*Time Elapsed\s+(?P[^\r\n]+)$").expect("valid regex") +}); +static TEST_RESULT_RE: LazyLock = LazyLock::new(|| { + Regex::new( r"(?m)(?:Passed!|Failed!)\s*-\s*Failed:\s*(?P\d+),\s*Passed:\s*(?P\d+),\s*Skipped:\s*(?P\d+),\s*Total:\s*(?P\d+),\s*Duration:\s*(?P[^\r\n-]+)" ) - .expect("valid regex"); - static ref TEST_SUMMARY_RE: Regex = Regex::new( + .expect("valid regex") +}); +static TEST_SUMMARY_RE: LazyLock = LazyLock::new(|| { + Regex::new( r"(?mi)^\s*Test summary:\s*total:\s*(?P\d+),\s*failed:\s*(?P\d+),\s*(?:succeeded|passed):\s*(?P\d+),\s*skipped:\s*(?P\d+),\s*duration:\s*(?P[^\r\n]+)$" ) - .expect("valid regex"); - static ref FAILED_TEST_HEAD_RE: Regex = Regex::new( - r"(?m)^\s*Failed\s+(?P[^\r\n\[]+)\s+\[[^\]\r\n]+\]\s*$" - ) - .expect("valid regex"); - static ref RESTORE_PROJECT_RE: Regex = - Regex::new(r"(?m)^\s*Restored\s+.+\.csproj\s*\(").expect("valid regex"); - static ref RESTORE_DIAGNOSTIC_RE: Regex = Regex::new( + .expect("valid regex") +}); +static FAILED_TEST_HEAD_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"(?m)^\s*Failed\s+(?P[^\r\n\[]+)\s+\[[^\]\r\n]+\]\s*$").expect("valid regex") +}); +static RESTORE_PROJECT_RE: LazyLock = + LazyLock::new(|| Regex::new(r"(?m)^\s*Restored\s+.+\.csproj\s*\(").expect("valid regex")); +static RESTORE_DIAGNOSTIC_RE: LazyLock = LazyLock::new(|| { + Regex::new( r"(?mi)^\s*(?:(?P.+?)\s+:\s+)?(?Pwarning|error)\s+(?P[A-Za-z]{2,}\d{3,})\s*:\s*(?P.+)$" ) - .expect("valid regex"); - static ref PROJECT_PATH_RE: Regex = - Regex::new(r"(?m)^\s*([A-Za-z]:)?[^\r\n]*\.csproj(?:\s|$)").expect("valid regex"); - static ref PRINTABLE_RUN_RE: Regex = Regex::new(r"[\x20-\x7E]{5,}").expect("valid regex"); - static ref DIAGNOSTIC_CODE_RE: Regex = - Regex::new(r"^[A-Za-z]{2,}\d{3,}$").expect("valid regex"); - static ref SOURCE_FILE_RE: Regex = Regex::new(r"(?i)([A-Za-z]:)?[/\\][^\s]+\.(cs|vb|fs)") - .expect("valid regex"); - static ref SENSITIVE_ENV_RE: Regex = { - let keys = SENSITIVE_ENV_VARS - .iter() - .map(|key| regex::escape(key)) - .collect::>() - .join("|"); - Regex::new(&format!( - r"(?P\b(?:{})\s*(?:=|:)\s*)(?P[^\s;]+)", - keys - )) - .expect("valid regex") - }; -} + .expect("valid regex") +}); +static PROJECT_PATH_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"(?m)^\s*([A-Za-z]:)?[^\r\n]*\.csproj(?:\s|$)").expect("valid regex") +}); +static PRINTABLE_RUN_RE: LazyLock = + LazyLock::new(|| Regex::new(r"[\x20-\x7E]{5,}").expect("valid regex")); +static DIAGNOSTIC_CODE_RE: LazyLock = + LazyLock::new(|| Regex::new(r"^[A-Za-z]{2,}\d{3,}$").expect("valid regex")); +static SOURCE_FILE_RE: LazyLock = + LazyLock::new(|| Regex::new(r"(?i)([A-Za-z]:)?[/\\][^\s]+\.(cs|vb|fs)").expect("valid regex")); +static SENSITIVE_ENV_RE: LazyLock = LazyLock::new(|| { + let keys = SENSITIVE_ENV_VARS + .iter() + .map(|key| regex::escape(key)) + .collect::>() + .join("|"); + Regex::new(&format!( + r"(?P\b(?:{})\s*(?:=|:)\s*)(?P[^\s;]+)", + keys + )) + .expect("valid regex") +}); const SENSITIVE_ENV_VARS: &[&str] = &[ "PATH", diff --git a/src/cmds/git/gh_cmd.rs b/src/cmds/git/gh_cmd.rs index 01cf2ac..aa72b26 100644 --- a/src/cmds/git/gh_cmd.rs +++ b/src/cmds/git/gh_cmd.rs @@ -8,20 +8,19 @@ use crate::core::truncate::CAP_LIST; use crate::core::utils::{ok_confirmation, resolved_command, truncate}; use crate::git; use anyhow::Result; -use lazy_static::lazy_static; use regex::Regex; use serde_json::Value; use std::process::Command; +use std::sync::LazyLock; -lazy_static! { - static ref HTML_COMMENT_RE: Regex = Regex::new(r"(?s)").unwrap(); - static ref BADGE_LINE_RE: Regex = - Regex::new(r"(?m)^\s*\[!\[[^\]]*\]\([^)]*\)\]\([^)]*\)\s*$").unwrap(); - static ref IMAGE_ONLY_LINE_RE: Regex = Regex::new(r"(?m)^\s*!\[[^\]]*\]\([^)]*\)\s*$").unwrap(); - static ref HORIZONTAL_RULE_RE: Regex = - Regex::new(r"(?m)^\s*(?:---+|\*\*\*+|___+)\s*$").unwrap(); - static ref MULTI_BLANK_RE: Regex = Regex::new(r"\n{3,}").unwrap(); -} +static HTML_COMMENT_RE: LazyLock = LazyLock::new(|| Regex::new(r"(?s)").unwrap()); +static BADGE_LINE_RE: LazyLock = + LazyLock::new(|| Regex::new(r"(?m)^\s*\[!\[[^\]]*\]\([^)]*\)\]\([^)]*\)\s*$").unwrap()); +static IMAGE_ONLY_LINE_RE: LazyLock = + LazyLock::new(|| Regex::new(r"(?m)^\s*!\[[^\]]*\]\([^)]*\)\s*$").unwrap()); +static HORIZONTAL_RULE_RE: LazyLock = + LazyLock::new(|| Regex::new(r"(?m)^\s*(?:---+|\*\*\*+|___+)\s*$").unwrap()); +static MULTI_BLANK_RE: LazyLock = LazyLock::new(|| Regex::new(r"\n{3,}").unwrap()); /// Filter markdown body to remove noise while preserving meaningful content. /// Removes HTML comments, badge lines, image-only lines, horizontal rules, diff --git a/src/cmds/git/glab_cmd.rs b/src/cmds/git/glab_cmd.rs index 56885f0..6de8199 100644 --- a/src/cmds/git/glab_cmd.rs +++ b/src/cmds/git/glab_cmd.rs @@ -15,26 +15,27 @@ use crate::core::runner::{self, RunOptions}; use crate::core::truncate::{CAP_LIST, CAP_WARNINGS}; use crate::core::utils::{ok_confirmation, resolved_command, strip_ansi, truncate}; use anyhow::Result; -use lazy_static::lazy_static; use regex::Regex; use serde_json::Value; use std::process::Command; +use std::sync::LazyLock; -lazy_static! { - static ref HTML_COMMENT_RE: Regex = Regex::new(r"(?s)").unwrap(); - static ref BADGE_LINE_RE: Regex = - Regex::new(r"(?m)^\s*\[!\[[^\]]*\]\([^)]*\)\]\([^)]*\)\s*$").unwrap(); - static ref IMAGE_ONLY_LINE_RE: Regex = Regex::new(r"(?m)^\s*!\[[^\]]*\]\([^)]*\)\s*$").unwrap(); - static ref HORIZONTAL_RULE_RE: Regex = - Regex::new(r"(?m)^\s*(?:---+|\*\*\*+|___+)\s*$").unwrap(); - static ref MULTI_BLANK_RE: Regex = Regex::new(r"\n{3,}").unwrap(); - static ref MR_URL_RE: Regex = Regex::new(r"/-/merge_requests/(\d+)").unwrap(); - /// Match GitLab CI section markers: section_start/end:timestamp:name[0K - static ref SECTION_MARKER_RE: Regex = - Regex::new(r"section_(?:start|end):\d+:[a-z0-9_]+(?:\x1b\[0K|\[0K)*").unwrap(); - /// Match bare bracket ANSI-like codes without ESC prefix: [0K, [0;m, [36;1m, etc. - static ref BARE_ANSI_RE: Regex = Regex::new(r"\[[\d;]+[A-Za-z]").unwrap(); -} +static HTML_COMMENT_RE: LazyLock = LazyLock::new(|| Regex::new(r"(?s)").unwrap()); +static BADGE_LINE_RE: LazyLock = + LazyLock::new(|| Regex::new(r"(?m)^\s*\[!\[[^\]]*\]\([^)]*\)\]\([^)]*\)\s*$").unwrap()); +static IMAGE_ONLY_LINE_RE: LazyLock = + LazyLock::new(|| Regex::new(r"(?m)^\s*!\[[^\]]*\]\([^)]*\)\s*$").unwrap()); +static HORIZONTAL_RULE_RE: LazyLock = + LazyLock::new(|| Regex::new(r"(?m)^\s*(?:---+|\*\*\*+|___+)\s*$").unwrap()); +static MULTI_BLANK_RE: LazyLock = LazyLock::new(|| Regex::new(r"\n{3,}").unwrap()); +static MR_URL_RE: LazyLock = + LazyLock::new(|| Regex::new(r"/-/merge_requests/(\d+)").unwrap()); +/// Match GitLab CI section markers: section_start/end:timestamp:name[0K +static SECTION_MARKER_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"section_(?:start|end):\d+:[a-z0-9_]+(?:\x1b\[0K|\[0K)*").unwrap() +}); +/// Match bare bracket ANSI-like codes without ESC prefix: [0K, [0;m, [36;1m, etc. +static BARE_ANSI_RE: LazyLock = LazyLock::new(|| Regex::new(r"\[[\d;]+[A-Za-z]").unwrap()); /// Filter markdown body to remove noise while preserving meaningful content. /// Removes HTML comments, badge lines, image-only lines, horizontal rules, diff --git a/src/cmds/git/gt_cmd.rs b/src/cmds/git/gt_cmd.rs index 266fd63..2967292 100644 --- a/src/cmds/git/gt_cmd.rs +++ b/src/cmds/git/gt_cmd.rs @@ -5,21 +5,22 @@ use crate::core::tracking; use crate::core::truncate::{reduced, CAP_LIST}; use crate::core::utils::{ok_confirmation, resolved_command, strip_ansi, truncate}; use anyhow::{Context, Result}; -use lazy_static::lazy_static; use regex::Regex; use std::ffi::OsString; +use std::sync::LazyLock; -lazy_static! { - static ref EMAIL_RE: Regex = - Regex::new(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b").unwrap(); - static ref BRANCH_NAME_RE: Regex = Regex::new( - r#"(?:Created|Pushed|pushed|Deleted|deleted)\s+branch\s+[`"']?([a-zA-Z0-9/_.\-+@]+)"# +static EMAIL_RE: LazyLock = + LazyLock::new(|| Regex::new(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b").unwrap()); +static BRANCH_NAME_RE: LazyLock = LazyLock::new(|| { + Regex::new( + r#"(?:Created|Pushed|pushed|Deleted|deleted)\s+branch\s+[`"']?([a-zA-Z0-9/_.\-+@]+)"#, ) - .unwrap(); - static ref PR_LINE_RE: Regex = - Regex::new(r"(Created|Updated)\s+pull\s+request\s+#(\d+)\s+for\s+([^\s:]+)(?::\s*(\S+))?") - .unwrap(); -} + .unwrap() +}); +static PR_LINE_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"(Created|Updated)\s+pull\s+request\s+#(\d+)\s+for\s+([^\s:]+)(?::\s*(\S+))?") + .unwrap() +}); fn run_gt_filtered( subcmd: &[&str], diff --git a/src/cmds/js/next_cmd.rs b/src/cmds/js/next_cmd.rs index fd359f0..d87fbdc 100644 --- a/src/cmds/js/next_cmd.rs +++ b/src/cmds/js/next_cmd.rs @@ -5,6 +5,7 @@ use crate::core::truncate::CAP_WARNINGS; use crate::core::utils::{resolved_command, strip_ansi, tool_exists, truncate}; use anyhow::Result; use regex::Regex; +use std::sync::LazyLock; pub fn run(args: &[String], verbose: u8) -> Result { // Try next directly first, fallback to npx if not found @@ -40,17 +41,11 @@ pub fn run(args: &[String], verbose: u8) -> Result { /// Filter Next.js build output - extract routes, bundles, warnings fn filter_next_build(output: &str) -> String { - lazy_static::lazy_static! { - // Route line pattern: ○ /dashboard 1.2 kB 132 kB - static ref ROUTE_PATTERN: Regex = Regex::new( - r"^[○●◐λ✓]\s+(/[^\s]*)\s+(\d+(?:\.\d+)?)\s*(kB|B)" - ).unwrap(); - - // Bundle size pattern - static ref BUNDLE_PATTERN: Regex = Regex::new( - r"^[○●◐λ✓]\s+([\w/\-\.]+)\s+(\d+(?:\.\d+)?)\s*(kB|B)\s+(\d+(?:\.\d+)?)\s*(kB|B)" - ).unwrap(); - } + // Bundle size pattern + static BUNDLE_PATTERN: LazyLock = LazyLock::new(|| { + Regex::new(r"^[○●◐λ✓]\s+([\w/\-\.]+)\s+(\d+(?:\.\d+)?)\s*(kB|B)\s+(\d+(?:\.\d+)?)\s*(kB|B)") + .unwrap() + }); let mut routes_static = 0; let mut routes_dynamic = 0; @@ -173,9 +168,8 @@ fn filter_next_build(output: &str) -> String { /// Extract time from build output (e.g., "Compiled in 34.2s") fn extract_time(line: &str) -> Option { - lazy_static::lazy_static! { - static ref TIME_RE: Regex = Regex::new(r"(\d+(?:\.\d+)?)\s*(s|ms)").unwrap(); - } + static TIME_RE: LazyLock = + LazyLock::new(|| Regex::new(r"(\d+(?:\.\d+)?)\s*(s|ms)").unwrap()); TIME_RE .captures(line) diff --git a/src/cmds/js/playwright_cmd.rs b/src/cmds/js/playwright_cmd.rs index aa930ec..8c763eb 100644 --- a/src/cmds/js/playwright_cmd.rs +++ b/src/cmds/js/playwright_cmd.rs @@ -6,6 +6,7 @@ use crate::core::utils::{detect_package_manager, resolved_command, strip_ansi}; use anyhow::{Context, Result}; use regex::Regex; use serde::Deserialize; +use std::sync::LazyLock; use crate::parser::{ emit_degradation_warning, emit_passthrough_warning, truncate_passthrough, FormatMode, @@ -163,14 +164,10 @@ fn collect_test_results( /// Tier 2: Extract test statistics using regex (degraded mode) fn extract_playwright_regex(output: &str) -> Option { - lazy_static::lazy_static! { - static ref SUMMARY_RE: Regex = Regex::new( - r"(\d+)\s+(passed|failed|flaky|skipped)" - ).unwrap(); - static ref DURATION_RE: Regex = Regex::new( - r"\((\d+(?:\.\d+)?)(ms|s|m)\)" - ).unwrap(); - } + static SUMMARY_RE: LazyLock = + LazyLock::new(|| Regex::new(r"(\d+)\s+(passed|failed|flaky|skipped)").unwrap()); + static DURATION_RE: LazyLock = + LazyLock::new(|| Regex::new(r"\((\d+(?:\.\d+)?)(ms|s|m)\)").unwrap()); let clean_output = strip_ansi(output); @@ -219,11 +216,8 @@ fn extract_playwright_regex(output: &str) -> Option { /// Extract failures using regex fn extract_failures_regex(output: &str) -> Vec { - lazy_static::lazy_static! { - static ref TEST_PATTERN: Regex = Regex::new( - r"[×✗]\s+.*?›\s+([^›]+\.spec\.[tj]sx?)" - ).unwrap(); - } + static TEST_PATTERN: LazyLock = + LazyLock::new(|| Regex::new(r"[×✗]\s+.*?›\s+([^›]+\.spec\.[tj]sx?)").unwrap()); let mut failures = Vec::new(); diff --git a/src/cmds/js/tsc_cmd.rs b/src/cmds/js/tsc_cmd.rs index 96a494c..9ac4f8f 100644 --- a/src/cmds/js/tsc_cmd.rs +++ b/src/cmds/js/tsc_cmd.rs @@ -4,14 +4,13 @@ use crate::core::runner; use crate::core::stream::{BlockHandler, BlockStreamFilter}; use crate::core::utils::{resolved_command, tool_exists, truncate}; use anyhow::Result; -use lazy_static::lazy_static; use regex::Regex; use std::collections::{HashMap, HashSet}; +use std::sync::LazyLock; -lazy_static! { - static ref TSC_ERROR: Regex = - Regex::new(r"^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+(TS\d+):\s+(.+)$").unwrap(); -} +static TSC_ERROR: LazyLock = LazyLock::new(|| { + Regex::new(r"^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+(TS\d+):\s+(.+)$").unwrap() +}); pub fn run(args: &[String], verbose: u8) -> Result { let tsc_exists = tool_exists("tsc"); diff --git a/src/cmds/js/vitest_cmd.rs b/src/cmds/js/vitest_cmd.rs index 2f6cdbb..32dfa0d 100644 --- a/src/cmds/js/vitest_cmd.rs +++ b/src/cmds/js/vitest_cmd.rs @@ -3,6 +3,7 @@ use anyhow::{Context, Result}; use regex::Regex; use serde::Deserialize; +use std::sync::LazyLock; use crate::core::stream::exec_capture; use crate::core::tracking; @@ -116,17 +117,10 @@ fn extract_failures_from_json(json: &VitestJsonOutput) -> Vec { /// Tier 2: Extract test statistics using regex (degraded mode) fn extract_stats_regex(output: &str) -> Option { - lazy_static::lazy_static! { - static ref TEST_FILES_RE: Regex = Regex::new( - r"Test Files\s+(?:(\d+)\s+failed\s+\|\s+)?(\d+)\s+passed" - ).unwrap(); - static ref TESTS_RE: Regex = Regex::new( - r"Tests\s+(?:(\d+)\s+failed\s+\|\s+)?(\d+)\s+passed" - ).unwrap(); - static ref DURATION_RE: Regex = Regex::new( - r"Duration\s+([\d.]+)(ms|s)" - ).unwrap(); - } + static TESTS_RE: LazyLock = + LazyLock::new(|| Regex::new(r"Tests\s+(?:(\d+)\s+failed\s+\|\s+)?(\d+)\s+passed").unwrap()); + static DURATION_RE: LazyLock = + LazyLock::new(|| Regex::new(r"Duration\s+([\d.]+)(ms|s)").unwrap()); let clean_output = strip_ansi(output); diff --git a/src/cmds/jvm/gradlew_cmd.rs b/src/cmds/jvm/gradlew_cmd.rs index 68d03ac..dac4854 100644 --- a/src/cmds/jvm/gradlew_cmd.rs +++ b/src/cmds/jvm/gradlew_cmd.rs @@ -3,20 +3,20 @@ use crate::core::stream::StreamFilter; use crate::core::truncate::CAP_LIST; use crate::core::utils::resolved_command; use anyhow::Result; -use lazy_static::lazy_static; use regex::Regex; use std::ffi::OsString; use std::process::Command; +use std::sync::LazyLock; // ── Shared regex patterns (used across multiple filters) ───────────────────── -lazy_static! { - static ref TASK_LINE: Regex = Regex::new(r"^> Task :").unwrap(); - static ref TRY_SECTION: Regex = - Regex::new(r"^\* Try:|^> Run with --|^> Get more help at").unwrap(); - static ref BUILD_STATUS: Regex = Regex::new(r"^BUILD (SUCCESSFUL|FAILED)").unwrap(); - static ref ACTIONABLE: Regex = Regex::new(r"^\d+ actionable tasks?").unwrap(); -} +static TASK_LINE: LazyLock = LazyLock::new(|| Regex::new(r"^> Task :").unwrap()); +static TRY_SECTION: LazyLock = + LazyLock::new(|| Regex::new(r"^\* Try:|^> Run with --|^> Get more help at").unwrap()); +static BUILD_STATUS: LazyLock = + LazyLock::new(|| Regex::new(r"^BUILD (SUCCESSFUL|FAILED)").unwrap()); +static ACTIONABLE: LazyLock = + LazyLock::new(|| Regex::new(r"^\d+ actionable tasks?").unwrap()); #[derive(Debug, PartialEq)] enum GradlewTask { @@ -177,25 +177,27 @@ pub fn run(args: &[String], verbose: u8) -> Result { // ── Build filter predicate ──────────────────────────────────────────────────── fn filter_build_line(line: &str) -> bool { - lazy_static! { - static ref DAEMON_LINE: Regex = Regex::new( + static DAEMON_LINE: LazyLock = LazyLock::new(|| { + Regex::new( r"^(Starting a Gradle Daemon|Daemon will be stopped|Reusing configuration cache|Calculating task graph|> Configure project|Deprecated Gradle features|You can use|For more on this|Configuration cache entry)" ) - .unwrap(); - static ref PROGRESS: Regex = - Regex::new(r"^\s*\d+%|^Downloading|^Configuring|^Resolving|^\[Incubating\]|^Wrote HTML report|^class \S+ could not|^\[android-") - .unwrap(); - static ref ERROR_LINE: Regex = Regex::new( + .unwrap() + }); + static PROGRESS: LazyLock = LazyLock::new(|| { + Regex::new(r"^\s*\d+%|^Downloading|^Configuring|^Resolving|^\[Incubating\]|^Wrote HTML report|^class \S+ could not|^\[android-") + .unwrap() + }); + static ERROR_LINE: LazyLock = LazyLock::new(|| { + Regex::new( r"(?i)(^FAILURE:|^\* What went wrong:|^\* Where:|> Could not|e: |error:|^Execution failed|Lint found \d+ error)" ) - .unwrap(); - // Compiler + gradle warnings: kotlinc emits "w: ", javac/gradle "warning:" or "Warning:" - static ref WARN_LINE: Regex = Regex::new( - r"^(w: |warning:|Warning:|WARNING:)" - ) - .unwrap(); - static ref BUILD_SCAN: Regex = Regex::new(r"gradle\.com/s/|Publishing build scan").unwrap(); - } + .unwrap() + }); + // Compiler + gradle warnings: kotlinc emits "w: ", javac/gradle "warning:" or "Warning:" + static WARN_LINE: LazyLock = + LazyLock::new(|| Regex::new(r"^(w: |warning:|Warning:|WARNING:)").unwrap()); + static BUILD_SCAN: LazyLock = + LazyLock::new(|| Regex::new(r"gradle\.com/s/|Publishing build scan").unwrap()); // Always strip these if TASK_LINE.is_match(line) @@ -228,14 +230,16 @@ fn is_framework_frame(trimmed: &str) -> bool { } fn filter_test(output: &str) -> String { - lazy_static! { - static ref FAILED_LINE: Regex = Regex::new(r"FAILED$| FAILED ").unwrap(); - static ref PASSED_SKIPPED: Regex = Regex::new(r" PASSED$| SKIPPED$").unwrap(); - static ref SUMMARY_LINE: Regex = Regex::new( - r"\d+ tests? completed|\d+ tests? failed|There were failing tests|See the report at" + static FAILED_LINE: LazyLock = + LazyLock::new(|| Regex::new(r"FAILED$| FAILED ").unwrap()); + static PASSED_SKIPPED: LazyLock = + LazyLock::new(|| Regex::new(r" PASSED$| SKIPPED$").unwrap()); + static SUMMARY_LINE: LazyLock = LazyLock::new(|| { + Regex::new( + r"\d+ tests? completed|\d+ tests? failed|There were failing tests|See the report at", ) - .unwrap(); - } + .unwrap() + }); if output.is_empty() { return String::new(); @@ -304,14 +308,16 @@ fn filter_test(output: &str) -> String { // ── Connected / instrumented test filter ───────────────────────────────────── fn filter_connected(output: &str) -> String { - lazy_static! { - static ref INSTRUMENTATION_STATUS: Regex = - Regex::new(r"^INSTRUMENTATION_STATUS[_CODE]*:").unwrap(); - static ref INSTRUMENTATION_RESULT: Regex = Regex::new(r"^INSTRUMENTATION_RESULT:").unwrap(); - static ref INSTRUMENTATION_CODE: Regex = Regex::new(r"^INSTRUMENTATION_CODE:").unwrap(); - static ref STARTING_TESTS: Regex = Regex::new(r"^Starting \d+ tests? on ").unwrap(); - static ref INSTALLING_APK: Regex = Regex::new(r"^Installing APK").unwrap(); - } + static INSTRUMENTATION_STATUS: LazyLock = + LazyLock::new(|| Regex::new(r"^INSTRUMENTATION_STATUS[_CODE]*:").unwrap()); + static INSTRUMENTATION_RESULT: LazyLock = + LazyLock::new(|| Regex::new(r"^INSTRUMENTATION_RESULT:").unwrap()); + static INSTRUMENTATION_CODE: LazyLock = + LazyLock::new(|| Regex::new(r"^INSTRUMENTATION_CODE:").unwrap()); + static STARTING_TESTS: LazyLock = + LazyLock::new(|| Regex::new(r"^Starting \d+ tests? on ").unwrap()); + static INSTALLING_APK: LazyLock = + LazyLock::new(|| Regex::new(r"^Installing APK").unwrap()); if output.is_empty() { return String::new(); @@ -352,26 +358,25 @@ fn filter_connected(output: &str) -> String { // ── Lint output filter ──────────────────────────────────────────────────────── fn filter_lint(output: &str) -> String { - lazy_static! { - // Android lint errors: src/main/java/Foo.kt:45: Error: message [IssueId] - static ref ANDROID_LINT_ERROR: Regex = - Regex::new(r"[^:]+:\d+:.*[Ee]rror:.*\[").unwrap(); - // Android lint warnings: src/main/java/Foo.kt:89: Warning: message [IssueId] - static ref ANDROID_LINT_WARNING: Regex = - Regex::new(r"[^:]+:\d+:.*[Ww]arning:.*\[").unwrap(); - // ktlint: file:line:col: Lint error > message - static ref KTLINT_VIOLATION: Regex = - Regex::new(r"[^:]+:\d+:\d+:.*[Ll]int").unwrap(); - // detekt: file:line:col: error - message - static ref DETEKT_VIOLATION: Regex = - Regex::new(r"[^:]+:\d+:\d+:.*error").unwrap(); - // Summary lines - static ref SUMMARY_LINE: Regex = - Regex::new(r"\d+ (issues?|errors?|warnings?)").unwrap(); - // Strip report path lines (too long) - static ref REPORT_LINE: Regex = - Regex::new(r"Wrote (HTML|XML|text) report|file://|/build/reports/lint").unwrap(); - } + // Android lint errors: src/main/java/Foo.kt:45: Error: message [IssueId] + static ANDROID_LINT_ERROR: LazyLock = + LazyLock::new(|| Regex::new(r"[^:]+:\d+:.*[Ee]rror:.*\[").unwrap()); + // Android lint warnings: src/main/java/Foo.kt:89: Warning: message [IssueId] + static ANDROID_LINT_WARNING: LazyLock = + LazyLock::new(|| Regex::new(r"[^:]+:\d+:.*[Ww]arning:.*\[").unwrap()); + // ktlint: file:line:col: Lint error > message + static KTLINT_VIOLATION: LazyLock = + LazyLock::new(|| Regex::new(r"[^:]+:\d+:\d+:.*[Ll]int").unwrap()); + // detekt: file:line:col: error - message + static DETEKT_VIOLATION: LazyLock = + LazyLock::new(|| Regex::new(r"[^:]+:\d+:\d+:.*error").unwrap()); + // Summary lines + static SUMMARY_LINE: LazyLock = + LazyLock::new(|| Regex::new(r"\d+ (issues?|errors?|warnings?)").unwrap()); + // Strip report path lines (too long) + static REPORT_LINE: LazyLock = LazyLock::new(|| { + Regex::new(r"Wrote (HTML|XML|text) report|file://|/build/reports/lint").unwrap() + }); if output.is_empty() { return String::new(); diff --git a/src/cmds/jvm/mvn_cmd.rs b/src/cmds/jvm/mvn_cmd.rs index 3ec2c5e..fb27989 100644 --- a/src/cmds/jvm/mvn_cmd.rs +++ b/src/cmds/jvm/mvn_cmd.rs @@ -9,12 +9,12 @@ use crate::core::runner::{self, RunOptions}; use crate::core::truncate::CAP_WARNINGS; use crate::core::utils::{resolved_command, strip_ansi}; use anyhow::Result; -use lazy_static::lazy_static; use regex::Regex; use std::collections::HashSet; use std::ffi::OsString; use std::path::Path; use std::process::Command; +use std::sync::LazyLock; /// Cap on emitted failing test-class blocks and `[ERROR] Failures:` summary /// entries — test-failure cap class, same binding as pytest/rspec/rake/runner. @@ -22,46 +22,52 @@ const MAX_MVN_FAILING_CLASSES: usize = CAP_WARNINGS; // ── Shared regex patterns ──────────────────────────────────────────────────── -lazy_static! { - /// `[INFO] Running com.example.app.FooTest` - static ref RUNNING: Regex = Regex::new(r"^\[INFO\] Running ").unwrap(); +/// `[INFO] Running com.example.app.FooTest` +static RUNNING: LazyLock = LazyLock::new(|| Regex::new(r"^\[INFO\] Running ").unwrap()); - /// Surefire/Failsafe per-class close line. Captures `Failures` and `Errors`. - /// Tolerates the optional `<<< FAILURE!` / `<<< ERROR!` marker (3.5.5 emits - /// `<<< FAILURE!` even for errors-only classes — see - /// `mvn_test_multifail_slice_raw.txt`; `ERROR!` accepted defensively for - /// other Surefire versions; failure detection is via the captured counts, - /// not the marker). Separator is `-` (Surefire 2.x) or `--` (Surefire 3.x). - /// Prefix INFO/ERROR/WARNING (3.x emits WARNING for classes with only - /// skipped tests). - static ref CLOSE: Regex = Regex::new( +/// Surefire/Failsafe per-class close line. Captures `Failures` and `Errors`. +/// Tolerates the optional `<<< FAILURE!` / `<<< ERROR!` marker (3.5.5 emits +/// `<<< FAILURE!` even for errors-only classes — see +/// `mvn_test_multifail_slice_raw.txt`; `ERROR!` accepted defensively for +/// other Surefire versions; failure detection is via the captured counts, +/// not the marker). Separator is `-` (Surefire 2.x) or `--` (Surefire 3.x). +/// Prefix INFO/ERROR/WARNING (3.x emits WARNING for classes with only +/// skipped tests). +static CLOSE: LazyLock = LazyLock::new(|| { + Regex::new( r"^\[(?:INFO|ERROR|WARNING)\] Tests run: \d+, Failures: (\d+), Errors: (\d+), Skipped: \d+, Time elapsed: [^ ]+ s(?:\s+<<<\s*(?:FAILURE|ERROR)!)?\s+--?\s+in (.+)$" - ).unwrap(); + ).unwrap() +}); - /// Final BUILD footer. - static ref BUILD_FOOT: Regex = Regex::new(r"^\[(?:INFO|ERROR)\] BUILD (?:SUCCESS|FAILURE)$").unwrap(); +/// Final BUILD footer. +static BUILD_FOOT: LazyLock = + LazyLock::new(|| Regex::new(r"^\[(?:INFO|ERROR)\] BUILD (?:SUCCESS|FAILURE)$").unwrap()); - /// `[INFO] Results:` separator before the aggregate. - static ref RESULTS: Regex = Regex::new(r"^\[INFO\] Results:\s*$").unwrap(); +/// `[INFO] Results:` separator before the aggregate. +static RESULTS: LazyLock = LazyLock::new(|| Regex::new(r"^\[INFO\] Results:\s*$").unwrap()); - /// Aggregate counts line (no `Time elapsed`, no ` - in `). - static ref AGG: Regex = Regex::new( - r"^\[(?:INFO|ERROR)\] Tests run: \d+, Failures: \d+, Errors: \d+, Skipped: \d+\s*$" - ).unwrap(); +/// Aggregate counts line (no `Time elapsed`, no ` - in `). +static AGG: LazyLock = LazyLock::new(|| { + Regex::new(r"^\[(?:INFO|ERROR)\] Tests run: \d+, Failures: \d+, Errors: \d+, Skipped: \d+\s*$") + .unwrap() +}); - /// Plugin banner line: `[INFO] --- plugin:goal (id) @ module ---`. - static ref PLUGIN_BANNER: Regex = Regex::new(r"^\[INFO\] --- .* @ .* ---$").unwrap(); +/// Plugin banner line: `[INFO] --- plugin:goal (id) @ module ---`. +static PLUGIN_BANNER: LazyLock = + LazyLock::new(|| Regex::new(r"^\[INFO\] --- .* @ .* ---$").unwrap()); - /// Module banner with project name in brackets. - static ref MODULE_BANNER: Regex = Regex::new(r"^\[INFO\] -+< .+ >-+$").unwrap(); +/// Module banner with project name in brackets. +static MODULE_BANNER: LazyLock = + LazyLock::new(|| Regex::new(r"^\[INFO\] -+< .+ >-+$").unwrap()); - /// Reactor summary header that opens the per-module pass/fail block at - /// the end of a multi-module build. - static ref REACTOR_SUMMARY: Regex = Regex::new(r"^\[INFO\] Reactor Summary for ").unwrap(); +/// Reactor summary header that opens the per-module pass/fail block at +/// the end of a multi-module build. +static REACTOR_SUMMARY: LazyLock = + LazyLock::new(|| Regex::new(r"^\[INFO\] Reactor Summary for ").unwrap()); - /// Compile-error coordinate substring to strip when deduping warnings/errors. - static ref FILE_COORD: Regex = Regex::new(r"/[^:]+\.java:\[\d+,\d+\]").unwrap(); -} +/// Compile-error coordinate substring to strip when deduping warnings/errors. +static FILE_COORD: LazyLock = + LazyLock::new(|| Regex::new(r"/[^:]+\.java:\[\d+,\d+\]").unwrap()); // ── Quiet-mode detection ──────────────────────────────────────────────────── diff --git a/src/cmds/php/artisan_cmd.rs b/src/cmds/php/artisan_cmd.rs index 8854351..9fcd696 100644 --- a/src/cmds/php/artisan_cmd.rs +++ b/src/cmds/php/artisan_cmd.rs @@ -2,17 +2,15 @@ use super::test_output::filter_test_runner_output; use super::utils::{strip_ansi_and_controls, PhpTestRunner}; -use lazy_static::lazy_static; use regex::Regex; +use std::sync::LazyLock; -lazy_static! { - static ref BOX_CHARS_RE: Regex = - Regex::new(r"[\u{2500}-\u{257F}\u{2580}-\u{259F}\u{25A0}-\u{25FF}\u{27A0}-\u{27BF}]+") - .unwrap(); - static ref DOTS_RE: Regex = Regex::new(r"\.{3,}").unwrap(); - static ref MULTI_SPACE_RE: Regex = Regex::new(r"[ \t]{2,}").unwrap(); - static ref MULTI_BLANK_RE: Regex = Regex::new(r"\n{3,}").unwrap(); -} +static BOX_CHARS_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"[\u{2500}-\u{257F}\u{2580}-\u{259F}\u{25A0}-\u{25FF}\u{27A0}-\u{27BF}]+").unwrap() +}); +static DOTS_RE: LazyLock = LazyLock::new(|| Regex::new(r"\.{3,}").unwrap()); +static MULTI_SPACE_RE: LazyLock = LazyLock::new(|| Regex::new(r"[ \t]{2,}").unwrap()); +static MULTI_BLANK_RE: LazyLock = LazyLock::new(|| Regex::new(r"\n{3,}").unwrap()); pub fn filter_artisan_output(output: &str) -> String { let mut cleaned = strip_ansi_and_controls(output); diff --git a/src/cmds/php/phpunit_cmd.rs b/src/cmds/php/phpunit_cmd.rs index ff7cf30..f2d4cd0 100644 --- a/src/cmds/php/phpunit_cmd.rs +++ b/src/cmds/php/phpunit_cmd.rs @@ -8,18 +8,16 @@ use super::utils::{php_tool_command, strip_ansi_and_controls}; use crate::core::runner; use anyhow::Result; -use lazy_static::lazy_static; use regex::Regex; +use std::sync::LazyLock; const MAX_FAILURES_SHOWN: usize = 10; const MAX_DETAIL_LINES_PER_FAILURE: usize = 2; -lazy_static! { - // PHPUnit prints each failure heading as "N) Class::method". Anchor to that - // exact shape so detail lines that merely start with a digit and contain ')' - // (e.g. "5 of 10 assertions passed in Foo::bar()") don't split a block. - static ref FAILURE_HEADING_RE: Regex = Regex::new(r"^\d+\) \S").unwrap(); -} +// PHPUnit prints each failure heading as "N) Class::method". Anchor to that +// exact shape so detail lines that merely start with a digit and contain ')' +// (e.g. "5 of 10 assertions passed in Foo::bar()") don't split a block. +static FAILURE_HEADING_RE: LazyLock = LazyLock::new(|| Regex::new(r"^\d+\) \S").unwrap()); pub fn run(args: &[String], verbose: u8) -> Result { let mut cmd = php_tool_command("phpunit"); diff --git a/src/cmds/php/utils.rs b/src/cmds/php/utils.rs index f056cd4..05fb74a 100644 --- a/src/cmds/php/utils.rs +++ b/src/cmds/php/utils.rs @@ -1,13 +1,12 @@ use crate::core::utils::{composer_tool_paths, resolve_binary, resolved_command}; -use lazy_static::lazy_static; use regex::Regex; use std::path::Path; use std::process::Command; +use std::sync::LazyLock; -lazy_static! { - static ref ANSI_RE: Regex = Regex::new(r"\x1b\[[0-9;]*[A-Za-z]").unwrap(); - static ref CONTROL_RE: Regex = Regex::new(r"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]").unwrap(); -} +static ANSI_RE: LazyLock = LazyLock::new(|| Regex::new(r"\x1b\[[0-9;]*[A-Za-z]").unwrap()); +static CONTROL_RE: LazyLock = + LazyLock::new(|| Regex::new(r"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]").unwrap()); pub fn php_tool_command(tool: &str) -> Command { for local_tool in composer_tool_paths(tool) { diff --git a/src/cmds/python/mypy_cmd.rs b/src/cmds/python/mypy_cmd.rs index 96a719a..b94807a 100644 --- a/src/cmds/python/mypy_cmd.rs +++ b/src/cmds/python/mypy_cmd.rs @@ -5,6 +5,7 @@ use crate::core::utils::{resolved_command, strip_ansi, tool_exists, truncate}; use anyhow::Result; use regex::Regex; use std::collections::HashMap; +use std::sync::LazyLock; pub fn run(args: &[String], verbose: u8) -> Result { let mut cmd = if tool_exists("mypy") { @@ -51,13 +52,11 @@ struct MypyError { } pub fn filter_mypy_output(output: &str) -> String { - lazy_static::lazy_static! { - // file.py:12: error: Message [error-code] - // file.py:12:5: error: Message [error-code] - static ref MYPY_DIAG: Regex = Regex::new( - r"^(.+?):(\d+)(?::\d+)?: (error|warning|note): (.+?)(?:\s+\[(.+)\])?$" - ).unwrap(); - } + // file.py:12: error: Message [error-code] + // file.py:12:5: error: Message [error-code] + static MYPY_DIAG: LazyLock = LazyLock::new(|| { + Regex::new(r"^(.+?):(\d+)(?::\d+)?: (error|warning|note): (.+?)(?:\s+\[(.+)\])?$").unwrap() + }); let lines: Vec<&str> = output.lines().collect(); let mut errors: Vec = Vec::new(); diff --git a/src/cmds/python/uv_cmd.rs b/src/cmds/python/uv_cmd.rs index 073eba2..0bbb466 100644 --- a/src/cmds/python/uv_cmd.rs +++ b/src/cmds/python/uv_cmd.rs @@ -13,15 +13,17 @@ use crate::core::tracking; use crate::core::truncate::{CAP_INVENTORY, CAP_WARNINGS}; use crate::core::utils::{exit_code_from_status, resolved_command, strip_ansi, truncate}; use anyhow::{Context, Result}; -use lazy_static::lazy_static; use regex::Regex; +use std::sync::LazyLock; -lazy_static! { - static ref PYTHON_FRAME_RE: Regex = Regex::new(r#"^\s*File ".*", line \d+.*$"#).unwrap(); - static ref PYTHON_EXCEPTION_RE: Regex = - Regex::new(r"^\s*[A-Za-z_][A-Za-z0-9_.]*(?:Error|Exception):").unwrap(); - static ref JS_FRAME_RE: Regex = Regex::new(r"^\s*at .+:\d+:\d+.*$").unwrap(); - static ref ERROR_START_PATTERNS: Vec = vec![ +static PYTHON_FRAME_RE: LazyLock = + LazyLock::new(|| Regex::new(r#"^\s*File ".*", line \d+.*$"#).unwrap()); +static PYTHON_EXCEPTION_RE: LazyLock = + LazyLock::new(|| Regex::new(r"^\s*[A-Za-z_][A-Za-z0-9_.]*(?:Error|Exception):").unwrap()); +static JS_FRAME_RE: LazyLock = + LazyLock::new(|| Regex::new(r"^\s*at .+:\d+:\d+.*$").unwrap()); +static ERROR_START_PATTERNS: LazyLock> = LazyLock::new(|| { + vec![ Regex::new(r"(?i)\berror\b").unwrap(), Regex::new(r"(?i)\bfailed\b").unwrap(), Regex::new(r"(?i)\bfailure\b").unwrap(), @@ -35,8 +37,8 @@ lazy_static! { Regex::new(r"^\s*Caused by:").unwrap(), Regex::new(r"^\s*note:").unwrap(), Regex::new(r"^\s*help:").unwrap(), - ]; -} + ] +}); const MAX_TRACEBACK_FRAMES: usize = CAP_WARNINGS; const MAX_ERROR_CONTINUATION_LINES: usize = CAP_WARNINGS; diff --git a/src/cmds/ruby/rake_cmd.rs b/src/cmds/ruby/rake_cmd.rs index 4336b38..adef345 100644 --- a/src/cmds/ruby/rake_cmd.rs +++ b/src/cmds/ruby/rake_cmd.rs @@ -8,6 +8,7 @@ use crate::core::runner; use crate::core::truncate::CAP_WARNINGS; use crate::core::utils::{ruby_exec, strip_ansi}; use anyhow::Result; +use std::sync::LazyLock; const MAX_RAKE_FAILURES: usize = CAP_WARNINGS; @@ -163,10 +164,9 @@ fn filter_minitest_output(output: &str) -> String { } fn is_failure_header(line: &str) -> bool { - lazy_static::lazy_static! { - static ref RE_FAILURE: regex::Regex = - regex::Regex::new(r"^\d+\)\s+(Failure|Error):$").unwrap(); - } + static RE_FAILURE: LazyLock = + LazyLock::new(|| regex::Regex::new(r"^\d+\)\s+(Failure|Error):$").unwrap()); + RE_FAILURE.is_match(line) } diff --git a/src/cmds/ruby/rspec_cmd.rs b/src/cmds/ruby/rspec_cmd.rs index bf907c3..663f971 100644 --- a/src/cmds/ruby/rspec_cmd.rs +++ b/src/cmds/ruby/rspec_cmd.rs @@ -9,25 +9,27 @@ use crate::core::runner; use crate::core::truncate::{reduced, CAP_WARNINGS}; use crate::core::utils::{fallback_tail, ruby_exec, truncate}; use anyhow::Result; -use lazy_static::lazy_static; use regex::Regex; use serde::Deserialize; +use std::sync::LazyLock; // rspec failures carry full backtraces — show fewer than a generic warning list. const MAX_RSPEC_FAILURES: usize = reduced(CAP_WARNINGS, 5); // ── Noise-stripping regex patterns ────────────────────────────────────────── -lazy_static! { - static ref RE_SPRING: Regex = Regex::new(r"(?i)running via spring preloader").unwrap(); - static ref RE_SIMPLECOV: Regex = - Regex::new(r"(?i)(coverage report|simplecov|coverage/|\.simplecov|All Files.*Lines)") - .unwrap(); - static ref RE_DEPRECATION: Regex = Regex::new(r"^DEPRECATION WARNING:").unwrap(); - static ref RE_FINISHED_IN: Regex = Regex::new(r"^Finished in \d").unwrap(); - static ref RE_SCREENSHOT: Regex = Regex::new(r"saved screenshot to (.+)").unwrap(); - static ref RE_RSPEC_SUMMARY: Regex = Regex::new(r"(\d+) examples?, (\d+) failures?").unwrap(); -} +static RE_SPRING: LazyLock = + LazyLock::new(|| Regex::new(r"(?i)running via spring preloader").unwrap()); +static RE_SIMPLECOV: LazyLock = LazyLock::new(|| { + Regex::new(r"(?i)(coverage report|simplecov|coverage/|\.simplecov|All Files.*Lines)").unwrap() +}); +static RE_DEPRECATION: LazyLock = + LazyLock::new(|| Regex::new(r"^DEPRECATION WARNING:").unwrap()); +static RE_FINISHED_IN: LazyLock = LazyLock::new(|| Regex::new(r"^Finished in \d").unwrap()); +static RE_SCREENSHOT: LazyLock = + LazyLock::new(|| Regex::new(r"saved screenshot to (.+)").unwrap()); +static RE_RSPEC_SUMMARY: LazyLock = + LazyLock::new(|| Regex::new(r"(\d+) examples?, (\d+) failures?").unwrap()); // ── JSON structures matching RSpec's --format json output ─────────────────── diff --git a/src/cmds/rust/runner.rs b/src/cmds/rust/runner.rs index 8a13c1c..044ffcc 100644 --- a/src/cmds/rust/runner.rs +++ b/src/cmds/rust/runner.rs @@ -3,15 +3,15 @@ use crate::core::stream::StreamFilter; use crate::core::truncate::{CAP_LIST, CAP_WARNINGS}; use anyhow::Result; -use lazy_static::lazy_static; use regex::Regex; use std::process::Command; +use std::sync::LazyLock; const MAX_RUNNER_FAILURES: usize = CAP_WARNINGS; const MAX_RUNNER_LINES: usize = CAP_LIST; -lazy_static! { - static ref ERROR_PATTERNS: Vec = vec![ +static ERROR_PATTERNS: LazyLock> = LazyLock::new(|| { + vec![ // Generic errors Regex::new(r"(?i)^.*error[\s:\[].*$").unwrap(), Regex::new(r"(?i)^.*\berr\b.*$").unwrap(), @@ -31,8 +31,8 @@ lazy_static! { Regex::new(r"^\s*at .*:\d+:\d+.*$").unwrap(), // Go Regex::new(r"^.*\.go:\d+:.*$").unwrap(), - ]; -} + ] +}); struct ErrorStreamFilter { in_error_block: bool, diff --git a/src/cmds/scala/sbt_cmd.rs b/src/cmds/scala/sbt_cmd.rs index 9751807..c6adee1 100644 --- a/src/cmds/scala/sbt_cmd.rs +++ b/src/cmds/scala/sbt_cmd.rs @@ -1,55 +1,53 @@ use crate::core::runner::{self, RunOptions}; use crate::core::utils::{resolved_command, truncate}; use anyhow::Result; -use lazy_static::lazy_static; use regex::Regex; use std::ffi::OsString; +use std::sync::LazyLock; -lazy_static! { - /// Matches the ScalaTest summary line: - /// Tests: succeeded N, failed N, canceled N, ignored N, pending N - static ref TEST_SUMMARY_RE: Regex = Regex::new( - r"Tests: succeeded (\d+), failed (\d+), canceled (\d+), ignored (\d+), pending (\d+)" - ).unwrap(); +/// Matches the ScalaTest summary line: +/// Tests: succeeded N, failed N, canceled N, ignored N, pending N +static TEST_SUMMARY_RE: LazyLock = LazyLock::new(|| { + Regex::new( + r"Tests: succeeded (\d+), failed (\d+), canceled (\d+), ignored (\d+), pending (\d+)", + ) + .unwrap() +}); - /// Matches the munit summary line (also used by discipline-munit / ZIO Test): - /// [info] Passed: Total N, Failed N, Errors N, Passed N - /// [info] Failed: Total N, Failed N, Errors N, Passed N - static ref MUNIT_SUMMARY_RE: Regex = Regex::new( - r"^\[info\] (?:Passed|Failed): Total \d+, Failed (\d+), Errors (\d+), Passed (\d+)" - ).unwrap(); +/// Matches the munit summary line (also used by discipline-munit / ZIO Test): +/// [info] Passed: Total N, Failed N, Errors N, Passed N +/// [info] Failed: Total N, Failed N, Errors N, Passed N +static MUNIT_SUMMARY_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"^\[info\] (?:Passed|Failed): Total \d+, Failed (\d+), Errors (\d+), Passed (\d+)") + .unwrap() +}); - /// Matches suite count line: - /// Suites: completed N, aborted N - static ref SUITE_SUMMARY_RE: Regex = Regex::new( - r"Suites: completed (\d+), aborted (\d+)" - ).unwrap(); +/// Matches suite count line: +/// Suites: completed N, aborted N +static SUITE_SUMMARY_RE: LazyLock = + LazyLock::new(|| Regex::new(r"Suites: completed (\d+), aborted (\d+)").unwrap()); - /// Matches the "Run completed in" timing line - static ref RUN_TIME_RE: Regex = Regex::new( - r"Run completed in (\d+) seconds?" - ).unwrap(); +/// Matches the "Run completed in" timing line +static RUN_TIME_RE: LazyLock = + LazyLock::new(|| Regex::new(r"Run completed in (\d+) seconds?").unwrap()); - /// Matches [info] Compiling N Scala source(s) - static ref COMPILE_COUNT_RE: Regex = Regex::new( - r"\[info\] Compiling (\d+) Scala source" - ).unwrap(); +/// Matches [info] Compiling N Scala source(s) +static COMPILE_COUNT_RE: LazyLock = + LazyLock::new(|| Regex::new(r"\[info\] Compiling (\d+) Scala source").unwrap()); - /// Matches [success] Total time: Ns - static ref SUCCESS_TIME_RE: Regex = Regex::new( - r"\[success\] Total time: (\d+) s" - ).unwrap(); +/// Matches [success] Total time: Ns +static SUCCESS_TIME_RE: LazyLock = + LazyLock::new(|| Regex::new(r"\[success\] Total time: (\d+) s").unwrap()); - /// Matches [error] lines - static ref ERROR_RE: Regex = Regex::new( - r"^\[error\]" - ).unwrap(); +/// Matches [error] lines +static ERROR_RE: LazyLock = LazyLock::new(|| Regex::new(r"^\[error\]").unwrap()); - /// Lines that are SBT noise (loading, resolving, downloading, etc.) - static ref NOISE_RE: Regex = Regex::new( +/// Lines that are SBT noise (loading, resolving, downloading, etc.) +static NOISE_RE: LazyLock = LazyLock::new(|| { + Regex::new( r"^\[info\] (welcome to sbt|loading |set current project|Updating |Resolved |Fetching |downloading |Done )" - ).unwrap(); -} + ).unwrap() +}); /// Integration test subcommand patterns (sbt configuration/task notation). /// These produce ScalaTest output and should use the same filtering as `sbt test`. diff --git a/src/cmds/system/log_cmd.rs b/src/cmds/system/log_cmd.rs index 3dca374..47ed179 100644 --- a/src/cmds/system/log_cmd.rs +++ b/src/cmds/system/log_cmd.rs @@ -4,23 +4,23 @@ use crate::core::guard::never_worse; use crate::core::tracking; use crate::core::truncate::{reduced, CAP_WARNINGS}; use anyhow::Result; -use lazy_static::lazy_static; use regex::Regex; use std::collections::HashMap; use std::fs; use std::io::{self, BufRead}; use std::path::Path; +use std::sync::LazyLock; -lazy_static! { - static ref TIMESTAMP_RE: Regex = - Regex::new(r"^\d{4}[-/]\d{2}[-/]\d{2}[T ]\d{2}:\d{2}:\d{2}[.,]?\d*\s*").unwrap(); - static ref UUID_RE: Regex = - Regex::new(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}") - .unwrap(); - static ref HEX_RE: Regex = Regex::new(r"0x[0-9a-fA-F]+").unwrap(); - static ref NUM_RE: Regex = Regex::new(r"\b\d{4,}\b").unwrap(); - static ref PATH_RE: Regex = Regex::new(r"/[\w./\-]+").unwrap(); -} +static TIMESTAMP_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"^\d{4}[-/]\d{2}[-/]\d{2}[T ]\d{2}:\d{2}:\d{2}[.,]?\d*\s*").unwrap() +}); +static UUID_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}") + .unwrap() +}); +static HEX_RE: LazyLock = LazyLock::new(|| Regex::new(r"0x[0-9a-fA-F]+").unwrap()); +static NUM_RE: LazyLock = LazyLock::new(|| Regex::new(r"\b\d{4,}\b").unwrap()); +static PATH_RE: LazyLock = LazyLock::new(|| Regex::new(r"/[\w./\-]+").unwrap()); /// Filter and deduplicate log output pub fn run_file(file: &Path, verbose: u8) -> Result<()> { @@ -76,7 +76,7 @@ fn analyze_logs(content: &str) -> String { let mut unique_errors: Vec = Vec::new(); let mut unique_warnings: Vec = Vec::new(); - // Use module-level lazy_static regexes for normalization + // Use module-level LazyLock regexes for normalization for line in content.lines() { let line_lower = line.to_lowercase(); diff --git a/src/cmds/system/ls.rs b/src/cmds/system/ls.rs index 98eb364..767470f 100644 --- a/src/cmds/system/ls.rs +++ b/src/cmds/system/ls.rs @@ -5,19 +5,19 @@ use crate::core::runner::{self, RunOptions}; use crate::core::truncate::{reduced, CAP_WARNINGS}; use crate::core::utils::resolved_command; use anyhow::Result; -use lazy_static::lazy_static; use regex::Regex; use std::io::IsTerminal; +use std::sync::LazyLock; -lazy_static! { - /// Matches the date+time portion in `ls -la` output, which serves as a - /// stable anchor regardless of owner/group column width. - /// E.g.: " Mar 31 16:18 " or " Dec 25 2024 " - static ref LS_DATE_RE: Regex = Regex::new( +/// Matches the date+time portion in `ls -la` output, which serves as a +/// stable anchor regardless of owner/group column width. +/// E.g.: " Mar 31 16:18 " or " Dec 25 2024 " +static LS_DATE_RE: LazyLock = LazyLock::new(|| { + Regex::new( r"\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2}\s+(?:\d{4}|\d{2}:\d{2})\s+" ) - .unwrap(); -} + .unwrap() +}); pub fn run(args: &[String], verbose: u8) -> Result { let show_all = args diff --git a/src/cmds/system/search.rs b/src/cmds/system/search.rs index 9950722..146a5b4 100644 --- a/src/cmds/system/search.rs +++ b/src/cmds/system/search.rs @@ -17,6 +17,7 @@ use regex::Regex; use std::collections::HashMap; use std::io::IsTerminal; use std::process::Command; +use std::sync::LazyLock; /// Short single-char flags that consume one following token (or inline remainder) /// as their value. `-e` is handled separately — its value goes to `patterns`. @@ -734,9 +735,9 @@ pub fn run( /// The `bool` in the tuple is `true` for match lines (`:` separator) and /// `false` for context lines (`-` separator, emitted by -A/-B/-C). fn parse_match_line(line: &str) -> Option<(String, usize, bool, &str)> { - lazy_static::lazy_static! { - static ref MATCH_LINE_RE: Regex = Regex::new(r"^([^\x00]+)\x00(\d+)([:-])(.*)$").unwrap(); - } + static MATCH_LINE_RE: LazyLock = + LazyLock::new(|| Regex::new(r"^([^\x00]+)\x00(\d+)([:-])(.*)$").unwrap()); + MATCH_LINE_RE.captures(line).and_then(|caps| { let file = caps.get(1)?.as_str().to_string(); let line_num: usize = caps.get(2)?.as_str().parse().ok()?; diff --git a/src/core/README.md b/src/core/README.md index 1861703..ab3529e 100644 --- a/src/core/README.md +++ b/src/core/README.md @@ -132,4 +132,4 @@ When the truncated output is a **flat list** and the hidden items start at a pre **Deviating from a cap.** A filter whose items are unusually verbose (multi-line entries, backtraces) may show fewer than its class cap. Use `truncate::reduced(cap, by)` rather than a bare `cap - by`: `reduced` returns `cap - by`, except when the reduction would empty the list (`by >= cap`), in which case it drops the deviation and uses the full `cap`. This guarantees a deviation can never hide every item, and — crucially — stays a `usize`-underflow-safe `const fn` once caps become runtime-configurable (a bare `CAP_WARNINGS - 5` would panic or wrap to "no truncation" if a user set `CAP_WARNINGS` below `5`). Never deviate with a bare literal or with `*`/`/` (those scale unboundedly). Each deviation needs a one-line comment stating why. ## Adding New Functionality -Place new infrastructure code here if it meets **all** of these criteria: (1) it has no dependencies on command modules or hooks, (2) it is used by two or more other modules, and (3) it provides a general-purpose utility rather than command-specific logic. Follow the existing pattern of lazy-initialized resources (`lazy_static!` for regex, on-demand config loading) to preserve the <10ms startup target. Add `#[cfg(test)] mod tests` with unit tests in the same file. +Place new infrastructure code here if it meets **all** of these criteria: (1) it has no dependencies on command modules or hooks, (2) it is used by two or more other modules, and (3) it provides a general-purpose utility rather than command-specific logic. Follow the existing pattern of lazy-initialized resources (`LazyLock` for regex, on-demand config loading) to preserve the <10ms startup target. Add `#[cfg(test)] mod tests` with unit tests in the same file. diff --git a/src/core/filter.rs b/src/core/filter.rs index 90f89ad..4c71281 100644 --- a/src/core/filter.rs +++ b/src/core/filter.rs @@ -1,8 +1,8 @@ //! Strips comments and boilerplate from source code to save tokens. -use lazy_static::lazy_static; use regex::Regex; use std::str::FromStr; +use std::sync::LazyLock; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FilterLevel { @@ -155,10 +155,7 @@ impl FilterStrategy for NoFilter { pub struct MinimalFilter; -lazy_static! { - static ref MULTIPLE_BLANK_LINES: Regex = Regex::new(r"\n{3,}").unwrap(); - static ref TRAILING_WHITESPACE: Regex = Regex::new(r"[ \t]+$").unwrap(); -} +static MULTIPLE_BLANK_LINES: LazyLock = LazyLock::new(|| Regex::new(r"\n{3,}").unwrap()); impl FilterStrategy for MinimalFilter { fn filter(&self, content: &str, lang: &Language) -> String { @@ -232,14 +229,14 @@ impl FilterStrategy for MinimalFilter { pub struct AggressiveFilter; -lazy_static! { - static ref IMPORT_PATTERN: Regex = - Regex::new(r"^(use |import |from |require\(|#include)").unwrap(); - static ref FUNC_SIGNATURE: Regex = Regex::new( - r"^(pub\s+)?(async\s+)?(fn|def|function|func|class|struct|enum|trait|interface|type)\s+\w+" +static IMPORT_PATTERN: LazyLock = + LazyLock::new(|| Regex::new(r"^(use |import |from |require\(|#include)").unwrap()); +static FUNC_SIGNATURE: LazyLock = LazyLock::new(|| { + Regex::new( + r"^(pub\s+)?(async\s+)?(fn|def|function|func|class|struct|enum|trait|interface|type)\s+\w+", ) - .unwrap(); -} + .unwrap() +}); impl FilterStrategy for AggressiveFilter { fn filter(&self, content: &str, lang: &Language) -> String { diff --git a/src/core/toml_filter.rs b/src/core/toml_filter.rs index b5a9be6..977e8d9 100644 --- a/src/core/toml_filter.rs +++ b/src/core/toml_filter.rs @@ -23,10 +23,10 @@ /// 7. max_lines — absolute line cap /// 8. on_empty — message if result is empty use super::constants::RTK_META_COMMANDS; -use lazy_static::lazy_static; use regex::{Regex, RegexSet}; use serde::Deserialize; use std::collections::BTreeMap; +use std::sync::LazyLock; // Built-in filters: concatenated from src/filters/*.toml by build.rs at compile time. const BUILTIN_TOML: &str = include_str!(concat!(env!("OUT_DIR"), "/builtin_filters.toml")); @@ -395,9 +395,7 @@ fn compile_filter(name: String, def: TomlFilterDef) -> Result = LazyLock::new(TomlFilterRegistry::load); pub fn toml_disabled() -> bool { std::env::var("RTK_NO_TOML").ok().as_deref() == Some("1") @@ -420,9 +418,7 @@ pub fn filter_parse_error(content: &str) -> Option { .map(|e| e.to_string()) } -lazy_static! { - static ref MATCH_SET: RegexSet = build_match_set(); -} +static MATCH_SET: LazyLock = LazyLock::new(build_match_set); pub fn command_matches_filter(command: &str) -> bool { MATCH_SET.is_match(command) @@ -810,7 +806,7 @@ mod tests { use super::*; // Helper: build a CompiledFilter from inline TOML for tests. - // Never touches the lazy_static registry. + // Never touches the lazy registry. fn make_filters(toml: &str) -> Vec { TomlFilterRegistry::parse_and_compile(toml, "test").expect("test TOML should be valid") } diff --git a/src/core/utils.rs b/src/core/utils.rs index 956d012..8374ed1 100644 --- a/src/core/utils.rs +++ b/src/core/utils.rs @@ -11,6 +11,7 @@ use serde_json::Value; use std::fs; use std::path::PathBuf; use std::process::Command; +use std::sync::LazyLock; use std::sync::OnceLock; /// Truncates a string to `max_len` characters, appending `...` if needed. @@ -49,9 +50,9 @@ pub fn truncate(s: &str, max_len: usize) -> String { /// assert_eq!(strip_ansi(colored), "Error"); /// ``` pub fn strip_ansi(text: &str) -> String { - lazy_static::lazy_static! { - static ref ANSI_RE: Regex = Regex::new(r"\x1b\[[0-9;]*[a-zA-Z]").unwrap(); - } + static ANSI_RE: LazyLock = + LazyLock::new(|| Regex::new(r"\x1b\[[0-9;]*[a-zA-Z]").unwrap()); + ANSI_RE.replace_all(text, "").to_string() } diff --git a/src/discover/README.md b/src/discover/README.md index bb6bc38..e6121a0 100644 --- a/src/discover/README.md +++ b/src/discover/README.md @@ -70,4 +70,4 @@ Add an entry to `rules.rs`. Each rule has: - `category`, `savings_pct` — metadata for discover reports - `subcmd_savings`, `subcmd_status` — per-subcommand overrides -No other files need to change. The registry compiles the patterns at first use via `lazy_static`. +No other files need to change. The registry compiles the patterns at first use via `LazyLock`. diff --git a/src/discover/registry.rs b/src/discover/registry.rs index 7f39899..81b1707 100644 --- a/src/discover/registry.rs +++ b/src/discover/registry.rs @@ -1,9 +1,9 @@ //! Matches shell commands against known RTK rewrite rules to decide how to handle them. use crate::core::utils::composer_bin_dirs; -use lazy_static::lazy_static; use regex::{Regex, RegexSet}; use std::path::Path; +use std::sync::LazyLock; use super::lexer::{shell_split, split_on_operators, tokenize, ParsedToken, PipeKind, TokenKind}; use super::rules::{IGNORED_EXACT, IGNORED_PREFIXES, RULES}; @@ -48,36 +48,42 @@ pub fn category_avg_tokens(category: &str, subcmd: &str) -> usize { } } -lazy_static! { - static ref REGEX_SET: RegexSet = - RegexSet::new(RULES.iter().map(|r| r.pattern)).expect("invalid regex patterns"); - static ref COMPILED: Vec = RULES +static REGEX_SET: LazyLock = LazyLock::new(|| { + RegexSet::new(RULES.iter().map(|r| r.pattern)).expect("invalid regex patterns") +}); +static COMPILED: LazyLock> = LazyLock::new(|| { + RULES .iter() .map(|r| Regex::new(r.pattern).expect("invalid regex")) - .collect(); - static ref ENV_PREFIX: Regex = { - let double_quoted = r#""(?:[^"\\]|\\.)*""#; - let single_quoted = r#"'(?:[^'\\]|\\.)*'"#; - let unquoted = r#"[^\s]*"#; - let env_value = format!("(?:{}|{}|{})", double_quoted, single_quoted, unquoted); - let env_assign = format!(r#"[A-Z_][A-Z0-9_]*={}"#, env_value); - Regex::new(&format!(r#"^(?:sudo\s+|env\s+|{}\s+)+"#, env_assign)).unwrap() - }; - // Git global options that appear before the subcommand: -C , -c , - // --git-dir , --work-tree , and flag-only options (#163) - static ref GIT_GLOBAL_OPT: Regex = - Regex::new(r"^(?:(?:-C\s+\S+|-c\s+\S+|--git-dir(?:=\S+|\s+\S+)|--work-tree(?:=\S+|\s+\S+)|--no-pager|--no-optional-locks|--bare|--literal-pathspecs)\s+)+").unwrap(); - // Issue #1362: each capture expects a SINGLE file argument (`\S+$`). Multi-file - // invocations like `head -3 a b c` fail to match so the segment is passed through - // to the native `head`/`tail` binary — which already handles multi-file with - // `==> name <==` banners that `rtk read --max-lines` cannot reproduce. - static ref HEAD_N: Regex = Regex::new(r"^head\s+-(\d+)\s+(\S+)$").unwrap(); - static ref HEAD_LINES: Regex = Regex::new(r"^head\s+--lines=(\d+)\s+(\S+)$").unwrap(); - static ref TAIL_N: Regex = Regex::new(r"^tail\s+-(\d+)\s+(\S+)$").unwrap(); - static ref TAIL_N_SPACE: Regex = Regex::new(r"^tail\s+-n\s+(\d+)\s+(\S+)$").unwrap(); - static ref TAIL_LINES_EQ: Regex = Regex::new(r"^tail\s+--lines=(\d+)\s+(\S+)$").unwrap(); - static ref TAIL_LINES_SPACE: Regex = Regex::new(r"^tail\s+--lines\s+(\d+)\s+(\S+)$").unwrap(); -} + .collect() +}); +static ENV_PREFIX: LazyLock = LazyLock::new(|| { + let double_quoted = r#""(?:[^"\\]|\\.)*""#; + let single_quoted = r#"'(?:[^'\\]|\\.)*'"#; + let unquoted = r#"[^\s]*"#; + let env_value = format!("(?:{}|{}|{})", double_quoted, single_quoted, unquoted); + let env_assign = format!(r#"[A-Z_][A-Z0-9_]*={}"#, env_value); + Regex::new(&format!(r#"^(?:sudo\s+|env\s+|{}\s+)+"#, env_assign)).unwrap() +}); +// Git global options that appear before the subcommand: -C , -c , +// --git-dir , --work-tree , and flag-only options (#163) +static GIT_GLOBAL_OPT: LazyLock = LazyLock::new(|| { + Regex::new(r"^(?:(?:-C\s+\S+|-c\s+\S+|--git-dir(?:=\S+|\s+\S+)|--work-tree(?:=\S+|\s+\S+)|--no-pager|--no-optional-locks|--bare|--literal-pathspecs)\s+)+").unwrap() +}); +// Issue #1362: each capture expects a SINGLE file argument (`\S+$`). Multi-file +// invocations like `head -3 a b c` fail to match so the segment is passed through +// to the native `head`/`tail` binary — which already handles multi-file with +// `==> name <==` banners that `rtk read --max-lines` cannot reproduce. +static HEAD_N: LazyLock = LazyLock::new(|| Regex::new(r"^head\s+-(\d+)\s+(\S+)$").unwrap()); +static HEAD_LINES: LazyLock = + LazyLock::new(|| Regex::new(r"^head\s+--lines=(\d+)\s+(\S+)$").unwrap()); +static TAIL_N: LazyLock = LazyLock::new(|| Regex::new(r"^tail\s+-(\d+)\s+(\S+)$").unwrap()); +static TAIL_N_SPACE: LazyLock = + LazyLock::new(|| Regex::new(r"^tail\s+-n\s+(\d+)\s+(\S+)$").unwrap()); +static TAIL_LINES_EQ: LazyLock = + LazyLock::new(|| Regex::new(r"^tail\s+--lines=(\d+)\s+(\S+)$").unwrap()); +static TAIL_LINES_SPACE: LazyLock = + LazyLock::new(|| Regex::new(r"^tail\s+--lines\s+(\d+)\s+(\S+)$").unwrap()); const GOLANGCI_GLOBAL_OPT_WITH_VALUE: &[&str] = &[ "-c", @@ -519,17 +525,15 @@ fn strip_trailing_redirects(cmd: &str) -> (&str, &str) { (cmd_part, redir_part) } -lazy_static! { - /// Matches a bash line-continuation: a backslash immediately followed by - /// `\n` or `\r\n`, *plus* any horizontal whitespace on the line before AND - /// after the break. This is what bash already collapses to a single space - /// before executing the command — rtk's hook matcher needs to do the same - /// so commands authored across multiple lines still hit the rewrite rules. - /// Consuming the trailing whitespace prevents double spaces in cases like - /// `git diff \HEAD~1`. - static ref LINE_CONTINUATION_RE: Regex = - Regex::new(r"(?m)[ \t\x0B\x0C]*\\\r?\n[ \t\x0B\x0C]*").unwrap(); -} +/// Matches a bash line-continuation: a backslash immediately followed by +/// `\n` or `\r\n`, *plus* any horizontal whitespace on the line before AND +/// after the break. This is what bash already collapses to a single space +/// before executing the command — rtk's hook matcher needs to do the same +/// so commands authored across multiple lines still hit the rewrite rules. +/// Consuming the trailing whitespace prevents double spaces in cases like +/// `git diff \HEAD~1`. +static LINE_CONTINUATION_RE: LazyLock = + LazyLock::new(|| Regex::new(r"(?m)[ \t\x0B\x0C]*\\\r?\n[ \t\x0B\x0C]*").unwrap()); /// Replace every bash line continuation with a single space, mirroring what /// bash does before dispatching the command. Returns a borrowed `&str` when the diff --git a/src/learn/README.md b/src/learn/README.md index 233dc4d..261c82a 100644 --- a/src/learn/README.md +++ b/src/learn/README.md @@ -14,7 +14,7 @@ Analyzes Claude Code session history to detect recurring CLI mistakes — comman ## Dependencies -- **Uses**: `discover::provider::ClaudeProvider` (session file discovery and command extraction), `lazy_static`/`regex` (error pattern matching), `serde_json` (JSON output) +- **Uses**: `discover::provider::ClaudeProvider` (session file discovery and command extraction), `LazyLock`/`regex` (error pattern matching), `serde_json` (JSON output) - **Used by**: `src/main.rs` (routes `rtk learn` command) ## Detection Algorithm diff --git a/src/learn/detector.rs b/src/learn/detector.rs index 867e035..020bce1 100644 --- a/src/learn/detector.rs +++ b/src/learn/detector.rs @@ -1,7 +1,7 @@ //! Pattern-matches CLI errors against known correction rules. -use lazy_static::lazy_static; use regex::Regex; +use std::sync::LazyLock; #[derive(Debug, Clone, PartialEq)] pub enum ErrorType { @@ -48,32 +48,38 @@ pub struct CorrectionRule { pub example_error: String, } -lazy_static! { - static ref UNKNOWN_FLAG_RE: Regex = Regex::new( +static UNKNOWN_FLAG_RE: LazyLock = LazyLock::new(|| { + Regex::new( r"(?i)(unexpected argument|unknown (option|flag)|unrecognized (option|flag)|invalid (option|flag))" - ).unwrap(); + ).unwrap() +}); - static ref CMD_NOT_FOUND_RE: Regex = Regex::new( - r"(?i)(command not found|not recognized as an internal|no such file or directory.*command)" - ).unwrap(); +static CMD_NOT_FOUND_RE: LazyLock = LazyLock::new(|| { + Regex::new( + r"(?i)(command not found|not recognized as an internal|no such file or directory.*command)", + ) + .unwrap() +}); - static ref WRONG_PATH_RE: Regex = Regex::new( - r"(?i)(no such file or directory|cannot find the path|file not found)" - ).unwrap(); +static WRONG_PATH_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"(?i)(no such file or directory|cannot find the path|file not found)").unwrap() +}); - static ref MISSING_ARG_RE: Regex = Regex::new( +static MISSING_ARG_RE: LazyLock = LazyLock::new(|| { + Regex::new( r"(?i)(requires a value|requires an argument|missing (required )?argument|expected.*argument)" - ).unwrap(); + ).unwrap() +}); - static ref PERMISSION_DENIED_RE: Regex = Regex::new( - r"(?i)(permission denied|access denied|not permitted)" - ).unwrap(); +static PERMISSION_DENIED_RE: LazyLock = + LazyLock::new(|| Regex::new(r"(?i)(permission denied|access denied|not permitted)").unwrap()); - // User rejection patterns - NOT actual errors - static ref USER_REJECTION_RE: Regex = Regex::new( +// User rejection patterns - NOT actual errors +static USER_REJECTION_RE: LazyLock = LazyLock::new(|| { + Regex::new( r"(?i)(user (doesn't want|declined|rejected|cancelled)|operation (cancelled|aborted) by user)" - ).unwrap(); -} + ).unwrap() +}); /// Filters out user rejections - requires actual error-indicating content pub fn is_command_error(is_error: bool, output: &str) -> bool {