From 2800be961f975f250e16b6affc86503c0298d531 Mon Sep 17 00:00:00 2001 From: Bart Waardenburg Date: Mon, 1 Jun 2026 15:18:53 +0200 Subject: [PATCH 1/2] chore: coordinate issue 513 tanstack regex warning From 60d63c8c15bae516117a471869f1baef3655c138 Mon Sep 17 00:00:00 2001 From: Bart Waardenburg Date: Mon, 1 Jun 2026 15:26:59 +0200 Subject: [PATCH 2/2] fix: clarify tanstack route ignore regex warnings --- CHANGELOG.md | 1 + crates/core/src/plugins/registry/helpers.rs | 76 ++++++++++++++++----- crates/core/src/plugins/registry/tests.rs | 63 ++++++++++++++++- 3 files changed, 123 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa570e759..d24421736 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **TanStack Router `routeFileIgnorePattern` warnings now call out JavaScript regex compatibility instead of treating every unsupported pattern as a user typo.** TanStack accepts JavaScript regular expressions, while fallow validates route ignore patterns with Rust's regex engine before matching route file segments. Patterns that use JavaScript-only syntax such as lookahead are still ignored during analysis so the run can continue, but the warning now names `routeFileIgnorePattern`, points at the source config file when known, and explains that the syntax is unsupported by fallow's Rust regex engine. (Refs [#513](https://github.com/fallow-rs/fallow/issues/513).) - **Nuxt composables and utils referenced only through script auto-imports are now tracked in the module graph.** Fallow now records unresolved value identifiers in JS/TS and Vue/Svelte script blocks, then resolves Nuxt convention exports from top-level `composables/`, `app/composables/`, `utils/`, and `app/utils/`, plus recursive `shared/utils/` and `shared/types/`, during graph build. This keeps files like `composables/useCounter.ts`, `utils/format-price.ts`, and named exports from `composables/index.ts` reachable when a page calls `useCounter()` or `formatPrice()` without an import. Local declarations, explicit imports, type-only references, and known JS/Web/Vue/Nuxt built-ins do not synthesize edges. With `autoImports: true`, component entry-pattern removal remains guarded by `components:` config, while composable/util pattern removal is separately guarded by `imports:` config. (Closes [#739](https://github.com/fallow-rs/fallow/issues/739).) - **`fallow health` now surfaces CRAP coverage-source consistency in JSON and lets teams tune the secondary CRAP refactor band.** CRAP source precedence is explicit (`template` inheritance first, then Istanbul data including unmatched files, then static estimates), health JSON emits `summary.coverage_source_consistency` and grouped health emits `groups[].coverage_source_consistency` whenever CRAP findings carry source data, and `health.crapRefactorBand` configures the old fixed 5-point window that adds a secondary `refactor-function` action to near-threshold CRAP-only findings. This closes the remaining JSON/config contract work from issue #474 after the human-output clarification below. (Closes [#474](https://github.com/fallow-rs/fallow/issues/474).) - **`fallow health` now explains whether CRAP scores are estimated or sourced from Istanbul coverage in the high-complexity output.** Previously the main human section showed values like `650.0 CRAP` without saying whether they came from real coverage or the static export-reference estimate, and the file-score hint told users to pass a bare `--coverage` flag. The complexity section now includes a short coverage-source note, mixed Istanbul runs call out that unmatched functions fall back to estimates, and the hint spells out `fallow health --coverage `. A top-level `fallow --coverage` typo now gets a targeted health-command hint instead of clap suggesting unrelated flags such as `--tolerance`. (Refs [#474](https://github.com/fallow-rs/fallow/issues/474).) diff --git a/crates/core/src/plugins/registry/helpers.rs b/crates/core/src/plugins/registry/helpers.rs index 7a0962690..19d18e32e 100644 --- a/crates/core/src/plugins/registry/helpers.rs +++ b/crates/core/src/plugins/registry/helpers.rs @@ -333,15 +333,15 @@ fn validate_path_rule_regexes( .retain(|pattern| match regex::Regex::new(pattern) { Ok(_) => true, Err(err) => { - let loc = config_path - .map(|p| format!(" in {}", p.display())) - .unwrap_or_default(); tracing::warn!( - "plugin '{plugin_name}'{loc}: invalid excluded regex \ - '{pattern}' for entry pattern '{rule_pattern}': {err}; \ - the pattern will be ignored. A future release may reject \ - invalid regex patterns at config load.", - rule_pattern = rule.pattern, + "{}", + invalid_excluded_regex_warning( + plugin_name, + config_path, + pattern, + &rule.pattern, + &err, + ) ); false } @@ -350,21 +350,65 @@ fn validate_path_rule_regexes( .retain(|pattern| match regex::Regex::new(pattern) { Ok(_) => true, Err(err) => { - let loc = config_path - .map(|p| format!(" in {}", p.display())) - .unwrap_or_default(); tracing::warn!( - "plugin '{plugin_name}'{loc}: invalid excluded segment \ - regex '{pattern}' for entry pattern '{rule_pattern}': \ - {err}; the pattern will be ignored. A future release \ - may reject invalid regex patterns at config load.", - rule_pattern = rule.pattern, + "{}", + invalid_excluded_segment_regex_warning( + plugin_name, + config_path, + pattern, + &rule.pattern, + &err, + ) ); false } }); } +fn config_location(config_path: Option<&Path>) -> String { + config_path + .map(|p| format!(" in {}", p.display())) + .unwrap_or_default() +} + +fn invalid_excluded_regex_warning( + plugin_name: &str, + config_path: Option<&Path>, + pattern: &str, + rule_pattern: &str, + err: ®ex::Error, +) -> String { + let loc = config_location(config_path); + format!( + "plugin '{plugin_name}'{loc}: invalid excluded regex '{pattern}' for entry pattern \ + '{rule_pattern}': {err}; the pattern will be ignored. A future release may reject \ + invalid regex patterns at config load." + ) +} + +pub(super) fn invalid_excluded_segment_regex_warning( + plugin_name: &str, + config_path: Option<&Path>, + pattern: &str, + rule_pattern: &str, + err: ®ex::Error, +) -> String { + let loc = config_location(config_path); + if plugin_name == "tanstack-router" { + return format!( + "plugin '{plugin_name}'{loc}: routeFileIgnorePattern '{pattern}' for entry pattern \ + '{rule_pattern}' uses JavaScript regex syntax unsupported by fallow's Rust regex \ + engine: {err}; fallow will ignore this routeFileIgnorePattern during analysis." + ); + } + + format!( + "plugin '{plugin_name}'{loc}: invalid excluded segment regex '{pattern}' for entry pattern \ + '{rule_pattern}': {err}; the pattern will be ignored. A future release may reject \ + invalid regex patterns at config load." + ) +} + /// Merge a `PluginResult` from config parsing into the aggregated result. /// /// `config_path` is the source config file the plugin parsed (when known). diff --git a/crates/core/src/plugins/registry/tests.rs b/crates/core/src/plugins/registry/tests.rs index 43d6f1e87..b1d642c70 100644 --- a/crates/core/src/plugins/registry/tests.rs +++ b/crates/core/src/plugins/registry/tests.rs @@ -4,7 +4,10 @@ use fallow_config::{ ExternalPluginDef, ExternalUsedExport, PluginDetection, ScopedUsedClassMemberRule, UsedClassMemberRule, }; -use helpers::{check_plugin_detection, discover_config_files, process_config_result}; +use helpers::{ + check_plugin_detection, discover_config_files, invalid_excluded_segment_regex_warning, + process_config_result, +}; use rustc_hash::FxHashSet; fn make_external(name: &str, enablers: &[&str], config_patterns: &[&str]) -> ExternalPluginDef { @@ -3191,6 +3194,64 @@ fn process_config_result_strips_invalid_regex_in_used_exports() { ); } +#[test] +fn tanstack_route_file_ignore_pattern_warning_names_js_regex_compatibility() { + let pattern = ["^(", "?!layout\\.tsx$|__root\\.tsx$).+\\.tsx$"].concat(); + let err = regex::Regex::new(&pattern).unwrap_err(); + let warning = invalid_excluded_segment_regex_warning( + "tanstack-router", + Some(Path::new("/proj/vite.config.ts")), + &pattern, + "src/routes/**/*.{ts,tsx,js,jsx}", + &err, + ); + + assert!(warning.contains("plugin 'tanstack-router' in /proj/vite.config.ts")); + assert!(warning.contains("routeFileIgnorePattern")); + assert!(warning.contains("syntax unsupported by fallow's Rust regex engine")); + assert!(warning.contains("src/routes/**/*.{ts,tsx,js,jsx}")); + assert!(warning.contains(&pattern)); + assert!(warning.contains(&err.to_string())); + assert!( + !warning.contains("future release"), + "TanStack JavaScript regex compatibility warnings should not threaten hard failure" + ); +} + +#[test] +fn tanstack_route_file_ignore_pattern_unsupported_patterns_are_warn_and_drop() { + let unsupported_patterns = [ + "^(?!layout\\.tsx$|__root\\.tsx$).+\\.tsx$", + "^_(?!_)", + "/*.{js,jsx}", + ]; + + for pattern in unsupported_patterns { + let mut aggregated = AggregatedPluginResult::default(); + let rule = PathRule::new("src/routes/**/*.{ts,tsx,js,jsx}") + .with_excluded_segment_regexes(["valid_segment", pattern]); + let config_result = PluginResult { + entry_patterns: vec![rule], + ..Default::default() + }; + + process_config_result( + "tanstack-router", + config_result, + &mut aggregated, + Some(Path::new("/proj/vite.config.ts")), + ); + + assert_eq!(aggregated.entry_patterns.len(), 1); + let (kept, _name) = &aggregated.entry_patterns[0]; + assert_eq!( + kept.exclude_segment_regexes, + vec!["valid_segment".to_string()], + "unsupported TanStack pattern should be stripped without failing: {pattern}" + ); + } +} + #[test] fn missing_meta_framework_prerequisites_flags_astro_without_dot_astro() { let dir = tempfile::tempdir().expect("create temp dir");