From 774f623e911e9913b183eba391cc675fcf2e195d Mon Sep 17 00:00:00 2001 From: Kevin Brightwell Date: Wed, 18 Mar 2026 17:00:57 -0400 Subject: [PATCH 1/3] feat(gradle): global noise filters and path normalization Add filter infrastructure for gradle output: - global.rs: 37 noise regex patterns, Try-block removal, ANSI stripping (70-90% token savings) - paths.rs: absolute path normalization to repo-relative - Minimal mod.rs stub for module declarations New dev-dependency: insta 1.46 for snapshot testing. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Kevin Brightwell --- Cargo.toml | 1 + src/gradle/global.rs | 275 ++++++++++++++++++ src/gradle/mod.rs | 2 + src/gradle/paths.rs | 110 +++++++ ...obal__tests__compile_success_snapshot.snap | 6 + ...global__tests__generic_noise_snapshot.snap | 6 + src/main.rs | 1 + tests/fixtures/gradle/compile_success_raw.txt | 79 +++++ tests/fixtures/gradle/generic_noise_raw.txt | 72 +++++ 9 files changed, 552 insertions(+) create mode 100644 src/gradle/global.rs create mode 100644 src/gradle/mod.rs create mode 100644 src/gradle/paths.rs create mode 100644 src/gradle/snapshots/rtk__gradle__global__tests__compile_success_snapshot.snap create mode 100644 src/gradle/snapshots/rtk__gradle__global__tests__generic_noise_snapshot.snap create mode 100644 tests/fixtures/gradle/compile_success_raw.txt create mode 100644 tests/fixtures/gradle/generic_noise_raw.txt diff --git a/Cargo.toml b/Cargo.toml index 488d962d..68a3637b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ which = "8" toml = "0.8" [dev-dependencies] +insta = "1.46" [profile.release] opt-level = 3 diff --git a/src/gradle/global.rs b/src/gradle/global.rs new file mode 100644 index 00000000..a84158d8 --- /dev/null +++ b/src/gradle/global.rs @@ -0,0 +1,275 @@ +use crate::utils::strip_ansi; +use lazy_static::lazy_static; +use regex::Regex; + +lazy_static! { + static ref NOISE_PATTERNS: Vec = vec![ + // Task status lines (UP-TO-DATE, SKIPPED, NO-SOURCE, FROM-CACHE) + Regex::new(r"^> Task \S+ (UP-TO-DATE|SKIPPED|NO-SOURCE|FROM-CACHE)$").unwrap(), + // Bare executed task lines (no suffix) — replaced by ✓ summary + Regex::new(r"^> Task \S+\s*$").unwrap(), + // Configure lines + Regex::new(r"^> Configure project ").unwrap(), + // Daemon startup + Regex::new(r"^(Starting a? ?Gradle Daemon|Gradle Daemon started|Daemon initialized|Worker lease)").unwrap(), + // JVM warnings + Regex::new(r"^(OpenJDK 64-Bit Server VM warning:|Initialized native services|Initialized jansi)").unwrap(), + // Incubating (including Problems report) + Regex::new(r"\[Incubating\]|Configuration on demand is an incubating feature|Parallel Configuration Cache is an incubating feature").unwrap(), + // Config cache + Regex::new(r"^(Reusing configuration cache|Calculating task graph|Configuration cache entry|Storing configuration cache|Loading configuration cache)").unwrap(), + // Deprecation + Regex::new(r"^(Deprecated Gradle features were used|For more on this, please refer to|You can use '--warning-mode all')").unwrap(), + // Downloads + progress bars + Regex::new(r"^(Download |Downloading )").unwrap(), + Regex::new(r"^\s*\[[\s<=\->]+\]\s+\d+%").unwrap(), + // Build scan + develocity URLs (both private develocity.* and public scans.gradle.com) + Regex::new(r"^(Publishing build scan|https://(develocity\.|scans\.gradle\.com)|Upload .* build scan|Waiting for build scan)").unwrap(), + // VFS (all VFS> lines and Virtual file system lines) + Regex::new(r"^(VFS>|Virtual file system )").unwrap(), + // Evaluation + Regex::new(r"^(Evaluating root project|All projects evaluated|Settings evaluated)").unwrap(), + // Classpath + Regex::new(r"^(Classpath snapshot |Snapshotting classpath)").unwrap(), + // Kotlin daemon + Regex::new(r"^(Kotlin compile daemon|Connected to the daemon)").unwrap(), + // Reflection warnings + Regex::new(r"(?i)^WARNING:.*illegal reflective|(?i)^WARNING:.*reflect").unwrap(), + // File system events + Regex::new(r"^Received \d+ file system events").unwrap(), + // Javac/kapt notes (not actionable) + Regex::new(r"^Note: ").unwrap(), + ]; +} + +/// Apply global noise filters to gradle output. +/// +/// Drops noise lines, removes `* Try:` blocks, and trims blank lines. +pub fn apply_global_filters(input: &str) -> String { + let config = load_extra_patterns(); + apply_global_filters_with_extras(input, &config) +} + +/// Load extra drop patterns from config.toml [gradle] section. +fn load_extra_patterns() -> Vec { + match crate::config::Config::load() { + Ok(config) => compile_extra_patterns(&config.gradle.extra_drop_patterns), + Err(_) => Vec::new(), + } +} + +/// Compile user-supplied regex patterns, skipping invalid ones with stderr warning. +pub fn compile_extra_patterns(patterns: &[String]) -> Vec { + let mut compiled = Vec::new(); + for p in patterns { + match Regex::new(p) { + Ok(re) => compiled.push(re), + Err(e) => { + eprintln!("rtk: invalid extra_drop_pattern '{}': {}", p, e); + } + } + } + compiled +} + +/// Core filter logic, testable with explicit extra patterns. +pub fn apply_global_filters_with_extras(input: &str, extra_patterns: &[Regex]) -> String { + let mut result = Vec::new(); + let mut in_try_block = false; + + for line in input.lines() { + let trimmed = line.trim(); + // Strip ANSI escape codes for pattern matching (but keep original line for output) + let clean = strip_ansi(trimmed); + let clean_trimmed = clean.trim(); + + // Try block removal: "* Try:" through next "* " header or end of block + // Must be checked before blank line handling so blank lines inside Try blocks are consumed + if clean_trimmed.starts_with("* Try:") { + in_try_block = true; + continue; + } + if in_try_block { + if clean_trimmed.is_empty() { + // Blank lines inside Try block — consume + continue; + } else if clean_trimmed.starts_with("* ") { + // Next * header ends the Try block + in_try_block = false; + // Fall through to process this line normally + } else if clean_trimmed.starts_with("> ") + || clean_trimmed.starts_with("Get more help at") + { + // Indented content within Try block + continue; + } else { + // Non-Try-block content — end the block + in_try_block = false; + // Fall through to process this line normally + } + } + + // Skip empty lines (blank line collapsing) + if clean_trimmed.is_empty() { + // Only add blank if last line wasn't blank + if result + .last() + .map_or(true, |l: &String| !l.trim().is_empty()) + { + result.push(String::new()); + } + continue; + } + + // Check against built-in noise patterns (use ANSI-stripped text) + if NOISE_PATTERNS.iter().any(|re| re.is_match(clean_trimmed)) { + continue; + } + + // Drop lines that are only ANSI escape codes (no visible content after stripping) + if clean_trimmed.is_empty() && !trimmed.is_empty() { + continue; + } + + // Check against extra user-supplied patterns + if extra_patterns.iter().any(|re| re.is_match(clean_trimmed)) { + continue; + } + + // Lines always kept (BUILD SUCCESSFUL/FAILED, FAILURE header, What went wrong) + // These pass through naturally since they don't match noise patterns + + result.push(line.to_string()); + } + + // Trim leading/trailing blank lines + while result.first().map_or(false, |l| l.trim().is_empty()) { + result.remove(0); + } + while result.last().map_or(false, |l| l.trim().is_empty()) { + result.pop(); + } + + result.join("\n") +} + +#[cfg(test)] +mod tests { + use super::*; + use insta::assert_snapshot; + + fn count_tokens(text: &str) -> usize { + text.split_whitespace().count() + } + + #[test] + fn test_compile_success_snapshot() { + let input = include_str!("../../tests/fixtures/gradle/compile_success_raw.txt"); + let output = apply_global_filters(input); + assert_snapshot!(output); + } + + #[test] + fn test_compile_success_token_savings() { + let input = include_str!("../../tests/fixtures/gradle/compile_success_raw.txt"); + let output = apply_global_filters(input); + let input_tokens = count_tokens(input); + let output_tokens = count_tokens(&output); + let savings = 100.0 - (output_tokens as f64 / input_tokens as f64 * 100.0); + assert!( + savings >= 90.0, + "Expected >=90% savings on compile success, got {:.1}% (input={}, output={})", + savings, + input_tokens, + output_tokens + ); + } + + #[test] + fn test_generic_noise_snapshot() { + let input = include_str!("../../tests/fixtures/gradle/generic_noise_raw.txt"); + let output = apply_global_filters(input); + assert_snapshot!(output); + } + + #[test] + fn test_generic_noise_token_savings() { + let input = include_str!("../../tests/fixtures/gradle/generic_noise_raw.txt"); + let output = apply_global_filters(input); + let input_tokens = count_tokens(input); + let output_tokens = count_tokens(&output); + let savings = 100.0 - (output_tokens as f64 / input_tokens as f64 * 100.0); + assert!( + savings >= 90.0, + "Expected >=90% savings on generic noise, got {:.1}% (input={}, output={})", + savings, + input_tokens, + output_tokens + ); + } + + #[test] + fn test_try_block_removal() { + let input = "Some content\n\n* Try:\n> Run with --stacktrace option.\n> Run with --info option.\n> Run with --scan.\n> Get more help at https://help.gradle.org.\n\n* What went wrong:\nSomething failed"; + let output = apply_global_filters_with_extras(input, &[]); + assert!(!output.contains("* Try:"), "Try block should be removed"); + assert!( + !output.contains("--stacktrace"), + "Try block content should be removed" + ); + assert!( + output.contains("* What went wrong:"), + "What went wrong should be kept" + ); + } + + #[test] + fn test_note_lines_dropped() { + let input = "Note: Some input files use unchecked or unsafe operations.\nNote: Recompile with -Xlint:unchecked for details.\nBUILD SUCCESSFUL in 1s"; + let output = apply_global_filters_with_extras(input, &[]); + assert!(!output.contains("Note:"), "Note: lines should be dropped"); + assert!(output.contains("BUILD SUCCESSFUL")); + } + + #[test] + fn test_build_result_always_kept() { + let input = "Starting Gradle Daemon...\nBUILD SUCCESSFUL in 12s\n8 actionable tasks: 1 executed, 7 up-to-date"; + let output = apply_global_filters_with_extras(input, &[]); + assert!(output.contains("BUILD SUCCESSFUL")); + } + + #[test] + fn test_failure_header_kept() { + let input = "FAILURE: Build failed with an exception\n\n* What went wrong:\nCompilation failed\n\nBUILD FAILED in 5s"; + let output = apply_global_filters_with_extras(input, &[]); + assert!(output.contains("FAILURE: Build failed with an exception")); + assert!(output.contains("* What went wrong:")); + assert!(output.contains("BUILD FAILED")); + } + + #[test] + fn test_extra_drop_patterns() { + let input = "Normal line\nCustomOrgBuildPlugin: initializing\nAnother normal line"; + let extras = compile_extra_patterns(&["^CustomOrgBuildPlugin:".to_string()]); + let output = apply_global_filters_with_extras(input, &extras); + assert!(!output.contains("CustomOrgBuildPlugin")); + assert!(output.contains("Normal line")); + assert!(output.contains("Another normal line")); + } + + #[test] + fn test_invalid_extra_pattern_skipped() { + let patterns = vec!["[invalid".to_string(), "^valid$".to_string()]; + let compiled = compile_extra_patterns(&patterns); + assert_eq!(compiled.len(), 1, "Invalid pattern should be skipped"); + } + + #[test] + fn test_blank_line_trimming() { + let input = "\n\n\nBUILD SUCCESSFUL in 1s\n\n\n"; + let output = apply_global_filters_with_extras(input, &[]); + assert!(!output.starts_with('\n')); + assert!(!output.ends_with('\n')); + assert!(output.contains("BUILD SUCCESSFUL")); + } +} diff --git a/src/gradle/mod.rs b/src/gradle/mod.rs new file mode 100644 index 00000000..3ccc90a6 --- /dev/null +++ b/src/gradle/mod.rs @@ -0,0 +1,2 @@ +pub mod global; +pub mod paths; diff --git a/src/gradle/paths.rs b/src/gradle/paths.rs new file mode 100644 index 00000000..dfb76539 --- /dev/null +++ b/src/gradle/paths.rs @@ -0,0 +1,110 @@ +use lazy_static::lazy_static; +use regex::Regex; + +lazy_static! { + /// Matches absolute paths that look like a repo checkout path. + /// Captures: /Users/*/backend/, /home/*/backend/, /opt/*/backend/, etc. + /// Also handles generic project roots: /Users/*/project-name/ + static ref ABSOLUTE_PATH_PREFIX: Regex = + Regex::new(r"(?:file://)?/(?:Users|home|opt|var|tmp)/[^/]+/(?:[^/]+/)*").unwrap(); +} + +/// Strip absolute path prefixes to make paths repo-relative. +/// +/// Converts paths like `/Users/developer/backend/app-payments/src/...` +/// to `app-payments/src/...`. Works for paths in error messages, file +/// references, and gradle task output. +pub fn normalize_paths(input: &str) -> String { + // Find the repo root from the first absolute path in the output. + // Heuristic: look for a path containing /src/ and extract everything before the module dir. + let repo_root = detect_repo_root(input); + + match repo_root { + Some(root) => input.replace(&root, ""), + None => { + // Fallback: just strip common prefixes with regex + ABSOLUTE_PATH_PREFIX.replace_all(input, "").to_string() + } + } +} + +/// Detect the repo root from paths in the output. +/// Looks for patterns like `/Users/dev/backend/app-foo/src/` and extracts +/// `/Users/dev/backend/` as the repo root. +fn detect_repo_root(input: &str) -> Option { + // Look for error lines with paths like: + // e: /Users/developer/backend/app-payments/src/main/kotlin/... + // w: /Users/developer/backend/app-payments/src/main/kotlin/... + lazy_static! { + // Non-greedy: capture up to but not including the module directory + // e.g., /Users/dev/backend/ from /Users/dev/backend/app-payments/src/... + static ref PATH_IN_ERROR: Regex = + Regex::new(r"(?:e|w): (/(?:Users|home|opt|var|tmp)/[^/]+/(?:[^/]+/)*?)[^/]+/(?:src/|build/)") + .unwrap(); + static ref PATH_ANYWHERE: Regex = + Regex::new(r"(/(?:Users|home|opt|var|tmp)/[^/]+/(?:[^/]+/)*?)[^/]+/(?:src/|build/)").unwrap(); + } + + // Try error lines first (most reliable) + if let Some(caps) = PATH_IN_ERROR.captures(input) { + return Some(caps[1].to_string()); + } + + // Fallback to any path + if let Some(caps) = PATH_ANYWHERE.captures(input) { + return Some(caps[1].to_string()); + } + + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_normalize_error_path() { + let input = + "e: /Users/developer/backend/app-payments/src/main/kotlin/com/example/Foo.kt:42:5 Unresolved reference"; + let output = normalize_paths(input); + assert_eq!( + output, + "e: app-payments/src/main/kotlin/com/example/Foo.kt:42:5 Unresolved reference" + ); + } + + #[test] + fn test_normalize_warning_path() { + let input = + "w: /Users/developer/backend/app-payments/src/main/kotlin/com/example/Foo.kt:8:1 Unused param"; + let output = normalize_paths(input); + assert_eq!( + output, + "w: app-payments/src/main/kotlin/com/example/Foo.kt:8:1 Unused param" + ); + } + + #[test] + fn test_normalize_multiple_paths_same_root() { + let input = "e: /Users/dev/backend/app-payments/src/Foo.kt:1 Error\ne: /Users/dev/backend/app-orders/src/Bar.kt:2 Error"; + let output = normalize_paths(input); + assert!(output.contains("app-payments/src/Foo.kt:1")); + assert!(output.contains("app-orders/src/Bar.kt:2")); + assert!(!output.contains("/Users/dev/backend/")); + } + + #[test] + fn test_normalize_no_paths() { + let input = "BUILD SUCCESSFUL in 12s"; + let output = normalize_paths(input); + assert_eq!(output, input); + } + + #[test] + fn test_normalize_file_protocol() { + let input = "file:///Users/dev/backend/app-payments/src/Foo.kt:1 Error"; + // The regex-based fallback should handle this + let output = normalize_paths(input); + assert!(!output.contains("/Users/dev/")); + } +} diff --git a/src/gradle/snapshots/rtk__gradle__global__tests__compile_success_snapshot.snap b/src/gradle/snapshots/rtk__gradle__global__tests__compile_success_snapshot.snap new file mode 100644 index 00000000..6d5629f3 --- /dev/null +++ b/src/gradle/snapshots/rtk__gradle__global__tests__compile_success_snapshot.snap @@ -0,0 +1,6 @@ +--- +source: src/gradle/global.rs +expression: output +--- +BUILD SUCCESSFUL in 1m 38s +1558 actionable tasks: 210 executed, 583 from cache, 765 up-to-date diff --git a/src/gradle/snapshots/rtk__gradle__global__tests__generic_noise_snapshot.snap b/src/gradle/snapshots/rtk__gradle__global__tests__generic_noise_snapshot.snap new file mode 100644 index 00000000..ee0d6381 --- /dev/null +++ b/src/gradle/snapshots/rtk__gradle__global__tests__generic_noise_snapshot.snap @@ -0,0 +1,6 @@ +--- +source: src/gradle/global.rs +expression: output +--- +BUILD SUCCESSFUL in 25s +11 actionable tasks: 0 executed, 11 up-to-date diff --git a/src/main.rs b/src/main.rs index 2bbc4bb2..cbf72d63 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,7 @@ mod gh_cmd; mod git; mod go_cmd; mod golangci_cmd; +mod gradle; mod grep_cmd; mod gt_cmd; mod hook_audit_cmd; diff --git a/tests/fixtures/gradle/compile_success_raw.txt b/tests/fixtures/gradle/compile_success_raw.txt new file mode 100644 index 00000000..8e1d46da --- /dev/null +++ b/tests/fixtures/gradle/compile_success_raw.txt @@ -0,0 +1,79 @@ +Starting a Gradle Daemon (subsequent builds will be faster) +Configuration on demand is an incubating feature. +Parallel Configuration Cache is an incubating feature. +Calculating task graph as no cached configuration is available for tasks: :backend:app-payments:compileKotlin +VFS> Statistics since last build: +VFS> > Stat: Executed stat() x 0. getUnixMode() x 0 +VFS> > FileHasher: Hashed 0 files (0 bytes) +VFS> > DirectorySnapshotter: Snapshot 0 directory hierarchies (visited 0 directories, 0 files and 0 failed files) +> Task :protos:protos-core-versioning:checkKotlinGradlePluginConfigurationErrors SKIPPED +> Task :protos:protos-core-currency:checkKotlinGradlePluginConfigurationErrors SKIPPED +> Task :protos:protos-core-geography:checkKotlinGradlePluginConfigurationErrors SKIPPED +> Task :protos:protos-core-datetime:checkKotlinGradlePluginConfigurationErrors SKIPPED +> Task :events:checkKotlinGradlePluginConfigurationErrors SKIPPED +> Task :core:core-extensions:checkKotlinGradlePluginConfigurationErrors SKIPPED +> Task :protos:protos-core-versioning:generateMainProtos UP-TO-DATE +> Task :protos:protos-core-currency:generateMainProtos UP-TO-DATE +> Task :protos:protos-core-geography:generateMainProtos UP-TO-DATE +> Task :protos:protos-core-datetime:generateMainProtos UP-TO-DATE +> Task :protos:protos-core-versioning:generateProtos UP-TO-DATE +> Task :protos:protos-core-currency:generateProtos UP-TO-DATE +> Task :protos:protos-core-geography:generateProtos UP-TO-DATE +> Task :protos:protos-core-datetime:generateProtos UP-TO-DATE +> Task :protos:protos-core-versioning:processResources UP-TO-DATE +> Task :protos:protos-core-currency:processResources UP-TO-DATE +> Task :protos:protos-core-geography:processResources UP-TO-DATE +> Task :protos:protos-core-datetime:processResources UP-TO-DATE +> Task :core:core-extensions:processResources NO-SOURCE +> Task :protos:protos-core-versioning:compileKotlin UP-TO-DATE +> Task :protos:protos-core-currency:compileKotlin UP-TO-DATE +> Task :protos:protos-core-geography:compileKotlin UP-TO-DATE +> Task :protos:protos-core-datetime:compileKotlin UP-TO-DATE +> Task :protos:protos-core-versioning:postGenerationCleanup UP-TO-DATE +> Task :protos:protos-core-currency:postGenerationCleanup UP-TO-DATE +> Task :protos:protos-core-versioning:compileJava NO-SOURCE +> Task :protos:protos-core-currency:compileJava NO-SOURCE +> Task :protos:protos-core-geography:compileJava NO-SOURCE +> Task :protos:protos-core-datetime:compileJava NO-SOURCE +> Task :protos:protos-core-versioning:classes UP-TO-DATE +> Task :protos:protos-core-currency:classes UP-TO-DATE +> Task :protos:protos-core-geography:classes UP-TO-DATE +> Task :protos:protos-core-datetime:classes UP-TO-DATE +> Task :protos:protos-core-versioning:jar UP-TO-DATE +> Task :protos:protos-core-currency:jar UP-TO-DATE +> Task :protos:protos-core-geography:jar UP-TO-DATE +> Task :protos:protos-core-datetime:jar UP-TO-DATE +> Task :core:core-extensions:compileKotlin FROM-CACHE +> Task :core:core-extensions:compileJava NO-SOURCE +> Task :core:core-extensions:classes UP-TO-DATE +> Task :core:core-extensions:jar UP-TO-DATE +> Task :backend:app-common:kspKotlin FROM-CACHE +> Task :backend:app-common:processResources NO-SOURCE +> Task :backend:app-common:compileKotlin FROM-CACHE +> Task :backend:app-common:compileJava NO-SOURCE +> Task :backend:app-common:classes UP-TO-DATE +> Task :backend:app-common:jar +> Task :backend:app-payments:kspKotlin FROM-CACHE +> Task :backend:app-payments:compileKotlin FROM-CACHE + +[Incubating] Problems report is available at: file:///Users/developer/example-backend/build/reports/problems/problems-report.html + +Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0. + +You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins. + +For more on this, please refer to https://docs.gradle.org/8.14/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation. + +BUILD SUCCESSFUL in 1m 38s +1558 actionable tasks: 210 executed, 583 from cache, 765 up-to-date +Received 216736 file system events during the current build while watching 1 locations +Virtual file system retains information about 125611 files, 17699 directories and 769 missing files until next build +VFS> Statistics during current build: +VFS> > Stat: Executed stat() x 193. getUnixMode() x 2274 +VFS> > FileHasher: Hashed 28944 files (390031618 bytes) +VFS> > DirectorySnapshotter: Snapshot 7069 directory hierarchies (visited 27458 directories, 175521 files and 0 failed files) + +Publishing build scan... +https://scans.gradle.com/s/fagmceoq32cxc + +Configuration cache entry stored. diff --git a/tests/fixtures/gradle/generic_noise_raw.txt b/tests/fixtures/gradle/generic_noise_raw.txt new file mode 100644 index 00000000..4f4d1b60 --- /dev/null +++ b/tests/fixtures/gradle/generic_noise_raw.txt @@ -0,0 +1,72 @@ +Starting a Gradle Daemon (subsequent builds will be faster) +Configuration on demand is an incubating feature. +Parallel Configuration Cache is an incubating feature. +Calculating task graph as no cached configuration is available for tasks: :backend:app-payments:assemble +VFS> Statistics since last build: +VFS> > Stat: Executed stat() x 0. getUnixMode() x 0 +VFS> > FileHasher: Hashed 0 files (0 bytes) +VFS> > DirectorySnapshotter: Snapshot 0 directory hierarchies (visited 0 directories, 0 files and 0 failed files) +Received 17 file system events since last build while watching 1 locations +Virtual file system retained information about 158297 files, 24428 directories and 1255 missing files since last build +> Task :protos:protos-core-versioning:checkKotlinGradlePluginConfigurationErrors SKIPPED +> Task :protos:protos-core-currency:checkKotlinGradlePluginConfigurationErrors SKIPPED +> Task :events:checkKotlinGradlePluginConfigurationErrors SKIPPED +> Task :core:core-extensions:checkKotlinGradlePluginConfigurationErrors SKIPPED +> Task :protos:protos-core-versioning:generateMainProtos UP-TO-DATE +> Task :protos:protos-core-versioning:generateProtos UP-TO-DATE +> Task :protos:protos-core-versioning:processResources UP-TO-DATE +> Task :protos:protos-core-versioning:compileKotlin UP-TO-DATE +> Task :protos:protos-core-versioning:compileJava NO-SOURCE +> Task :protos:protos-core-versioning:classes UP-TO-DATE +> Task :protos:protos-core-versioning:jar UP-TO-DATE +> Task :protos:protos-core-versioning:postGenerationCleanup UP-TO-DATE +> Task :core:core-extensions:processResources NO-SOURCE +> Task :core:core-extensions:compileKotlin FROM-CACHE +> Task :core:core-extensions:compileJava NO-SOURCE +> Task :core:core-extensions:classes UP-TO-DATE +> Task :core:core-extensions:jar UP-TO-DATE +> Task :backend:app-common:kspKotlin FROM-CACHE +> Task :backend:app-common:processResources NO-SOURCE +> Task :backend:app-common:compileKotlin FROM-CACHE +> Task :backend:app-common:compileJava NO-SOURCE +> Task :backend:app-common:classes UP-TO-DATE +> Task :backend:app-common:jar +> Task :backend:app-payments:compileKotlin UP-TO-DATE +> Task :backend:app-payments:compileJava NO-SOURCE +> Task :backend:app-payments:processResources UP-TO-DATE +> Task :backend:app-payments:classes UP-TO-DATE +> Task :backend:app-payments:jar UP-TO-DATE +> Task :backend:app-payments:assemble UP-TO-DATE + +Note: Some input files use unchecked or unsafe operations. +Note: Recompile with -Xlint:unchecked for details. +Note: Some input files use unchecked or unsafe operations. +Note: Recompile with -Xlint:unchecked for details. + +* Try: +> Run with --stacktrace option to get the stack trace. +> Run with --info or --debug option to get more log output. +> Run with --scan to get full insights. +> Get more help at https://help.gradle.org. + +[Incubating] Problems report is available at: file:///Users/developer/example-backend/build/reports/problems/problems-report.html + +Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0. + +You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins. + +For more on this, please refer to https://docs.gradle.org/8.14/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation. + +BUILD SUCCESSFUL in 25s +11 actionable tasks: 0 executed, 11 up-to-date +Received 747 file system events during the current build while watching 1 locations +Virtual file system retains information about 160976 files, 24516 directories and 3917 missing files until next build +VFS> Statistics during current build: +VFS> > Stat: Executed stat() x 36. getUnixMode() x 2 +VFS> > FileHasher: Hashed 1021 files (14411852 bytes) +VFS> > DirectorySnapshotter: Snapshot 21 directory hierarchies (visited 92 directories, 1320 files and 0 failed files) + +Publishing build scan... +https://scans.gradle.com/s/iyn5qieotkino + +Configuration cache entry stored. From 7e14c11dfa77eb3aa109ef0da90b314bf2e80f2a Mon Sep 17 00:00:00 2001 From: Kevin Brightwell Date: Wed, 18 Mar 2026 18:13:22 -0400 Subject: [PATCH 2/3] perf(gradle): use RegexSet for single-pass noise matching Convert NOISE_PATTERNS Vec to RegexSet for single-pass matching instead of iterating 37 patterns. Also convert extra_drop_patterns to RegexSet at load time. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Kevin Brightwell --- src/gradle/global.rs | 103 ++++++++++++++++++++++++++----------------- 1 file changed, 62 insertions(+), 41 deletions(-) diff --git a/src/gradle/global.rs b/src/gradle/global.rs index a84158d8..99d9b135 100644 --- a/src/gradle/global.rs +++ b/src/gradle/global.rs @@ -1,45 +1,46 @@ use crate::utils::strip_ansi; use lazy_static::lazy_static; -use regex::Regex; +use regex::{Regex, RegexSet}; lazy_static! { - static ref NOISE_PATTERNS: Vec = vec![ + /// Built-in noise patterns compiled into a single RegexSet for single-pass matching. + static ref NOISE_SET: RegexSet = RegexSet::new([ // Task status lines (UP-TO-DATE, SKIPPED, NO-SOURCE, FROM-CACHE) - Regex::new(r"^> Task \S+ (UP-TO-DATE|SKIPPED|NO-SOURCE|FROM-CACHE)$").unwrap(), + r"^> Task \S+ (UP-TO-DATE|SKIPPED|NO-SOURCE|FROM-CACHE)$", // Bare executed task lines (no suffix) — replaced by ✓ summary - Regex::new(r"^> Task \S+\s*$").unwrap(), + r"^> Task \S+\s*$", // Configure lines - Regex::new(r"^> Configure project ").unwrap(), + r"^> Configure project ", // Daemon startup - Regex::new(r"^(Starting a? ?Gradle Daemon|Gradle Daemon started|Daemon initialized|Worker lease)").unwrap(), + r"^(Starting a? ?Gradle Daemon|Gradle Daemon started|Daemon initialized|Worker lease)", // JVM warnings - Regex::new(r"^(OpenJDK 64-Bit Server VM warning:|Initialized native services|Initialized jansi)").unwrap(), + r"^(OpenJDK 64-Bit Server VM warning:|Initialized native services|Initialized jansi)", // Incubating (including Problems report) - Regex::new(r"\[Incubating\]|Configuration on demand is an incubating feature|Parallel Configuration Cache is an incubating feature").unwrap(), + r"\[Incubating\]|Configuration on demand is an incubating feature|Parallel Configuration Cache is an incubating feature", // Config cache - Regex::new(r"^(Reusing configuration cache|Calculating task graph|Configuration cache entry|Storing configuration cache|Loading configuration cache)").unwrap(), + r"^(Reusing configuration cache|Calculating task graph|Configuration cache entry|Storing configuration cache|Loading configuration cache)", // Deprecation - Regex::new(r"^(Deprecated Gradle features were used|For more on this, please refer to|You can use '--warning-mode all')").unwrap(), + r"^(Deprecated Gradle features were used|For more on this, please refer to|You can use '--warning-mode all')", // Downloads + progress bars - Regex::new(r"^(Download |Downloading )").unwrap(), - Regex::new(r"^\s*\[[\s<=\->]+\]\s+\d+%").unwrap(), + r"^(Download |Downloading )", + r"^\s*\[[\s<=\->]+\]\s+\d+%", // Build scan + develocity URLs (both private develocity.* and public scans.gradle.com) - Regex::new(r"^(Publishing build scan|https://(develocity\.|scans\.gradle\.com)|Upload .* build scan|Waiting for build scan)").unwrap(), + r"^(Publishing build scan|https://(develocity\.|scans\.gradle\.com)|Upload .* build scan|Waiting for build scan)", // VFS (all VFS> lines and Virtual file system lines) - Regex::new(r"^(VFS>|Virtual file system )").unwrap(), + r"^(VFS>|Virtual file system )", // Evaluation - Regex::new(r"^(Evaluating root project|All projects evaluated|Settings evaluated)").unwrap(), + r"^(Evaluating root project|All projects evaluated|Settings evaluated)", // Classpath - Regex::new(r"^(Classpath snapshot |Snapshotting classpath)").unwrap(), + r"^(Classpath snapshot |Snapshotting classpath)", // Kotlin daemon - Regex::new(r"^(Kotlin compile daemon|Connected to the daemon)").unwrap(), + r"^(Kotlin compile daemon|Connected to the daemon)", // Reflection warnings - Regex::new(r"(?i)^WARNING:.*illegal reflective|(?i)^WARNING:.*reflect").unwrap(), + r"(?i)^WARNING:.*illegal reflective|(?i)^WARNING:.*reflect", // File system events - Regex::new(r"^Received \d+ file system events").unwrap(), + r"^Received \d+ file system events", // Javac/kapt notes (not actionable) - Regex::new(r"^Note: ").unwrap(), - ]; + r"^Note: ", + ]).unwrap(); } /// Apply global noise filters to gradle output. @@ -51,29 +52,35 @@ pub fn apply_global_filters(input: &str) -> String { } /// Load extra drop patterns from config.toml [gradle] section. -fn load_extra_patterns() -> Vec { +fn load_extra_patterns() -> Option { match crate::config::Config::load() { Ok(config) => compile_extra_patterns(&config.gradle.extra_drop_patterns), - Err(_) => Vec::new(), + Err(_) => None, } } -/// Compile user-supplied regex patterns, skipping invalid ones with stderr warning. -pub fn compile_extra_patterns(patterns: &[String]) -> Vec { - let mut compiled = Vec::new(); - for p in patterns { - match Regex::new(p) { - Ok(re) => compiled.push(re), +/// Compile user-supplied regex patterns into a RegexSet, skipping invalid ones with stderr warning. +pub fn compile_extra_patterns(patterns: &[String]) -> Option { + let valid: Vec<&str> = patterns + .iter() + .filter(|p| match Regex::new(p) { + Ok(_) => true, Err(e) => { eprintln!("rtk: invalid extra_drop_pattern '{}': {}", p, e); + false } - } + }) + .map(|s| s.as_str()) + .collect(); + if valid.is_empty() { + None + } else { + RegexSet::new(&valid).ok() } - compiled } /// Core filter logic, testable with explicit extra patterns. -pub fn apply_global_filters_with_extras(input: &str, extra_patterns: &[Regex]) -> String { +pub fn apply_global_filters_with_extras(input: &str, extra_patterns: &Option) -> String { let mut result = Vec::new(); let mut in_try_block = false; @@ -121,8 +128,8 @@ pub fn apply_global_filters_with_extras(input: &str, extra_patterns: &[Regex]) - continue; } - // Check against built-in noise patterns (use ANSI-stripped text) - if NOISE_PATTERNS.iter().any(|re| re.is_match(clean_trimmed)) { + // Check against built-in noise patterns (single-pass RegexSet match) + if NOISE_SET.is_match(clean_trimmed) { continue; } @@ -132,7 +139,10 @@ pub fn apply_global_filters_with_extras(input: &str, extra_patterns: &[Regex]) - } // Check against extra user-supplied patterns - if extra_patterns.iter().any(|re| re.is_match(clean_trimmed)) { + if extra_patterns + .as_ref() + .map_or(false, |set| set.is_match(clean_trimmed)) + { continue; } @@ -211,7 +221,7 @@ mod tests { #[test] fn test_try_block_removal() { let input = "Some content\n\n* Try:\n> Run with --stacktrace option.\n> Run with --info option.\n> Run with --scan.\n> Get more help at https://help.gradle.org.\n\n* What went wrong:\nSomething failed"; - let output = apply_global_filters_with_extras(input, &[]); + let output = apply_global_filters_with_extras(input, &None); assert!(!output.contains("* Try:"), "Try block should be removed"); assert!( !output.contains("--stacktrace"), @@ -226,7 +236,7 @@ mod tests { #[test] fn test_note_lines_dropped() { let input = "Note: Some input files use unchecked or unsafe operations.\nNote: Recompile with -Xlint:unchecked for details.\nBUILD SUCCESSFUL in 1s"; - let output = apply_global_filters_with_extras(input, &[]); + let output = apply_global_filters_with_extras(input, &None); assert!(!output.contains("Note:"), "Note: lines should be dropped"); assert!(output.contains("BUILD SUCCESSFUL")); } @@ -234,14 +244,14 @@ mod tests { #[test] fn test_build_result_always_kept() { let input = "Starting Gradle Daemon...\nBUILD SUCCESSFUL in 12s\n8 actionable tasks: 1 executed, 7 up-to-date"; - let output = apply_global_filters_with_extras(input, &[]); + let output = apply_global_filters_with_extras(input, &None); assert!(output.contains("BUILD SUCCESSFUL")); } #[test] fn test_failure_header_kept() { let input = "FAILURE: Build failed with an exception\n\n* What went wrong:\nCompilation failed\n\nBUILD FAILED in 5s"; - let output = apply_global_filters_with_extras(input, &[]); + let output = apply_global_filters_with_extras(input, &None); assert!(output.contains("FAILURE: Build failed with an exception")); assert!(output.contains("* What went wrong:")); assert!(output.contains("BUILD FAILED")); @@ -261,13 +271,24 @@ mod tests { fn test_invalid_extra_pattern_skipped() { let patterns = vec!["[invalid".to_string(), "^valid$".to_string()]; let compiled = compile_extra_patterns(&patterns); - assert_eq!(compiled.len(), 1, "Invalid pattern should be skipped"); + assert!( + compiled.is_some(), + "Should produce a RegexSet with the valid pattern" + ); + assert!( + compiled.as_ref().unwrap().is_match("valid"), + "Valid pattern should match" + ); + assert!( + !compiled.as_ref().unwrap().is_match("no match"), + "Should not match arbitrary text" + ); } #[test] fn test_blank_line_trimming() { let input = "\n\n\nBUILD SUCCESSFUL in 1s\n\n\n"; - let output = apply_global_filters_with_extras(input, &[]); + let output = apply_global_filters_with_extras(input, &None); assert!(!output.starts_with('\n')); assert!(!output.ends_with('\n')); assert!(output.contains("BUILD SUCCESSFUL")); From 6b215a39a6152f09b81e7734fbea8b4cedc29518 Mon Sep 17 00:00:00 2001 From: Kevin Brightwell Date: Wed, 18 Mar 2026 21:07:54 -0400 Subject: [PATCH 3/3] chore: regenerate Cargo.lock with insta dev-dependency Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Kevin Brightwell --- Cargo.lock | 107 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 65 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fd00d755..1ae8cd42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,9 +40,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -55,15 +55,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -139,9 +139,9 @@ checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "cc" -version = "1.2.56" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "shlex", @@ -168,9 +168,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -178,9 +178,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -190,9 +190,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -202,15 +202,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "colored" @@ -222,6 +222,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -324,10 +336,10 @@ dependencies = [ ] [[package]] -name = "env_home" -version = "0.1.0" +name = "encode_unicode" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "equivalent" @@ -651,6 +663,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "insta" +version = "1.46.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e82db8c87c7f1ccecb34ce0c24399b8a73081427f3c7c50a5d597925356115e4" +dependencies = [ + "console", + "once_cell", + "similar", + "tempfile", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -687,9 +711,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libredox" @@ -756,9 +780,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -902,6 +926,7 @@ dependencies = [ "flate2", "hostname", "ignore", + "insta", "lazy_static", "quick-xml", "regex", @@ -1076,6 +1101,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "smallvec" version = "1.15.1" @@ -1124,9 +1155,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.26.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.4.2", @@ -1415,13 +1446,11 @@ dependencies = [ [[package]] name = "which" -version = "8.0.1" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a824aeba0fbb27264f815ada4cff43d65b1741b7a4ed7629ff9089148c4a4e0" +checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" dependencies = [ - "env_home", - "rustix", - "winsafe", + "libc", ] [[package]] @@ -1658,12 +1687,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winsafe" -version = "0.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" - [[package]] name = "wit-bindgen" version = "0.51.0" @@ -1783,18 +1806,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.40" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.40" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" dependencies = [ "proc-macro2", "quote",