Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <coverage-final.json>`. 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).)
Expand Down
76 changes: 60 additions & 16 deletions crates/core/src/plugins/registry/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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: &regex::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: &regex::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).
Expand Down
63 changes: 62 additions & 1 deletion crates/core/src/plugins/registry/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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");
Expand Down
Loading