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
2 changes: 1 addition & 1 deletion .claude/rules/detection.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ Non-obvious implementation details for each detection feature. These are NOT dis
- **`export *` chain propagation**: multi-level barrel file chains fully resolved for transitive usage tracking. The fixpoint loop in `crates/graph/src/graph/re_exports/mod.rs::resolve_re_export_chains` terminates naturally via the monotone dedup-by-`from_file` check in `propagate.rs`; a defensive safety backstop bounded by `re_export_info.len() + 1` iterations guards against future regressions. Chain depth is unbounded in practice (the old `max_iterations = 20` cap silently truncated barrels with deeper chains; removed for #442). Re-export cycles are detected up front via iterative Tarjan SCC over the (barrel, source) edge subgraph; cycles surface BOTH as one `tracing::warn!` per cycle naming the member paths (RUST_LOG=warn discoverability, unchanged from #442) AND as a user-visible `re-export-cycle` finding on `AnalysisResults.re_export_cycles` (#515). Self-re-exports (a barrel re-exporting from itself, often a rename leftover) carry `kind: "self-loop"`; multi-node SCCs carry `kind: "multi-node"`. The finding is file-scoped (no per-edge line/col anchor); suppression via `// fallow-ignore-file re-export-cycle` (or aliases `re-export-cycles` / `reexport-cycle` / `reexport-cycles`) on any member file drops the cycle from results, and per-file `overrides.rules.re-export-cycle` is a no-op because the cycle spans multiple files (resolver emits a load-time warn pointing at the working escape hatch). Synthetic bridge stubs created during multi-hop star propagation inherit `is_type_only` from the triggering re-export edge so multi-hop `export type *` chains tag the stub correctly; when both a type-only and a value star edge reach the same source for the same name, a `synthetic_stubs` side set tracks the stub identity so the value edge can downgrade `is_type_only: false`. Real `export type Foo` declarations on the source are never in the side set and stay type-only. Type-only re-export cycles (`export type * from './b'` paired symmetrically) still surface as findings, because chain propagation through the loop is the same no-op as for value cycles. See `decisions/005-re-export-chain-resolution.md`.
- **`export * as Foo from './bar'` namespace re-export propagation**: a barrel that namespace-re-exports a module records a `ReExportEdge { imported_name: "*", exported_name: "Foo" }` plus a synthesised stub `ExportSymbol` named `Foo` on the barrel. The Phase 2 narrowing path only handles `import * as ns` (direct namespace imports), and Phase 4's named-re-export propagation looks for a source export literally named `"*"` (never matches), so a consumer using the named import form (`import { Foo } from './barrel'; Foo.X`) would otherwise leave every export of `./bar` unreferenced. Phase 2c (`graph::namespace_re_exports::propagate_namespace_re_exports`) closes the gap: it iterates re-export edges with `imported_name == "*" && exported_name != "*"`, enumerates the reachable `(barrel_file, exported_name_at_barrel)` pairs via a forward BFS over named re-export edges (so multi-hop chains like `outer_barrel.Foo -> inner_barrel.Foo -> source` match), and for every consumer that imports a reachable name records each `<consumer_local>.<member>` access. Credits are applied with `mark_member_exports_referenced` plus `create_synthetic_exports_for_star_re_exports` on the namespace target file, mirroring `narrow_namespace_references`'s two-step shape so star barrels on the target (`./source/index.ts` doing `export * from './impl'`) get a synthetic stub the Phase 4 chain resolver carries through. Whole-object uses (`Object.values(Foo)`, spread, destructure-with-rest) credit every target export; entry-point barrels exposing the namespace externally also credit every target export, mirroring `propagate_entry_point_star`. Consumer bindings reported as unused (`unused_import_bindings`) are skipped. The walker intentionally does NOT follow `export * as ns from` edges forward at non-seed barrels because the original identifier is hidden behind `ns.<name>` rather than exposed directly. See issue #324.
- **CSS Modules**: default imports (`import styles from '...'`) resolve `styles.className` to named exports via graph-level narrowing. Spread/`Object.values` handled conservatively.
- **Convention auto-import resolution (Nuxt components and scripts)**: frameworks that expose components by filesystem convention (`<Card001 />` resolving to `components/Card001.vue` with no `import`) are modeled by a first-class `Plugin::auto_imports(&self, root) -> Vec<AutoImportRule>` trait method. The architecture splits across two layers so the per-file extraction cache stays correct: (1) **capture** at extraction. The Vue SFC template scanner (`crates/extract/src/sfc_template/shared.rs::mark_binding_used`) records every PascalCase tag that matches no import and no local binding into `ModuleInfo.auto_import_candidates` (content-local, so it round-trips through the bitcode cache; native lowercase HTML elements and locals are skipped, kebab tags are normalized to their Pascal form). JS/TS and SFC script parsing also records unresolved value-reference identifiers such as `useCounter()` and `formatPrice()`, including files with zero explicit imports; locally declared, explicitly imported, and type-only names are not captured. (2) **resolve** at graph-build, never cached: `crates/graph/src/resolve/mod.rs::synthesize_auto_import_edges` runs after `apply_specifier_upgrades`, builds a `name -> FileId` table from the active plugins' `auto_imports` rules (mapped via the live file index), skips known JS/Web/Vue/Nuxt built-ins, and pushes a synthetic `ResolvedImport` for each captured name that matches. Name collisions over-credit every match (so `Comments.client.vue` + `Comments.server.vue`, both `<Comments>`, or same-name composables in two convention dirs, stay reachable). The Nuxt plugin (`crates/core/src/plugins/nuxt.rs`) scans `components/` and `app/components/`, deriving the directory-prefixed PascalCase name with Nuxt's prefix-overlap dedup (`base/foo/Button.vue` -> `BaseFooButton`, `foo/Foo.vue` -> `Foo`, `base/BaseButton.vue` -> `BaseButton`), pre-expanding the implicit `Lazy`-prefixed variant, and stripping `.client` / `.server` / `.global` suffixes so paired files share one name. It scans top-level `composables/`, `app/composables/`, `utils/`, and `app/utils/`, plus recursive `shared/utils/` and `shared/types/`, emitting named-export rules and default-export rules keyed by the derived basename. Synthesis is unconditional and additive. The opt-in `autoImports: true` config field gates ONLY entry-pattern removal: component fallbacks are dropped unless a root/workspace `nuxt.config` declares `components:`, and composable/util fallbacks are dropped unless it declares `imports:`. Pinia stores (#740) remain separate; bumps the extract `CACHE_VERSION` because `auto_import_candidates` content changes. See issues #704 and #739.
- **Convention auto-import resolution (Nuxt components, scripts, and Pinia stores)**: frameworks that expose components or script bindings by filesystem convention (`<Card001 />` resolving to `components/Card001.vue`, or `useUserStore()` resolving to `stores/user.ts`, with no `import`) are modeled by a first-class `Plugin::auto_imports(&self, root) -> Vec<AutoImportRule>` trait method. The architecture splits across two layers so the per-file extraction cache stays correct: (1) **capture** at extraction. The Vue SFC template scanner (`crates/extract/src/sfc_template/shared.rs::mark_binding_used`) records every PascalCase tag that matches no import and no local binding into `ModuleInfo.auto_import_candidates` (content-local, so it round-trips through the bitcode cache; native lowercase HTML elements and locals are skipped, kebab tags are normalized to their Pascal form). JS/TS and SFC script parsing also records unresolved value-reference identifiers such as `useCounter()`, `formatPrice()`, and `useUserStore()`, including files with zero explicit imports; locally declared, explicitly imported, and type-only names are not captured. (2) **resolve** at graph-build, never cached: `crates/graph/src/resolve/mod.rs::synthesize_auto_import_edges` runs after `apply_specifier_upgrades`, builds a `name -> FileId` table from the active plugins' `auto_imports` rules (mapped via the live file index), skips known JS/Web/Vue/Nuxt built-ins, and pushes a synthetic `ResolvedImport` for each captured name that matches. Name collisions over-credit every match (so `Comments.client.vue` + `Comments.server.vue`, both `<Comments>`, same-name composables in two convention dirs, or same-name stores in two supported dirs, stay reachable). The Nuxt plugin (`crates/core/src/plugins/nuxt.rs`) scans `components/` and `app/components/`, deriving the directory-prefixed PascalCase name with Nuxt's prefix-overlap dedup (`base/foo/Button.vue` -> `BaseFooButton`, `foo/Foo.vue` -> `Foo`, `base/BaseButton.vue` -> `BaseButton`), pre-expanding the implicit `Lazy`-prefixed variant, and stripping `.client` / `.server` / `.global` suffixes so paired files share one name. It scans top-level `composables/`, `app/composables/`, `utils/`, and `app/utils/`, plus recursive `shared/utils/` and `shared/types/`, emitting named-export rules and default-export rules keyed by the derived basename. The Pinia plugin (`crates/core/src/plugins/pinia.rs`) activates on `@pinia/nuxt` and scans direct files in `stores/` and `app/stores/`, emitting named-export rules only for value exports that match `use<Name>Store`; nested stores and custom `storesDirs` remain out of scope. Synthesis is unconditional and additive. The opt-in `autoImports: true` config field gates ONLY Nuxt entry-pattern removal: component fallbacks are dropped unless a root/workspace `nuxt.config` declares `components:`, and composable/util fallbacks are dropped unless it declares `imports:`. Pinia adds no entry-pattern fallback and does not require an extract `CACHE_VERSION` bump because it reuses existing script-level candidate capture. See issues #704, #739, and #740.
- **Nuxt `@nuxt/content` config entry**: `@nuxt/content` v3 reads a root `content.config.ts` (`export default defineContentConfig({...})`) at build time; nothing in app source imports it, so it surfaced as `unused-file` plus `unused-export`. When `@nuxt/content` appears in the nuxt.config `modules:` array, `crates/core/src/plugins/nuxt.rs::resolve_config` credits `content.config.{ts,js,mts,mjs,cts,cjs}` as a default-export entry (entry pattern + `default` used-export rule), resolved relative to the nuxt.config directory via `normalize_config_path` so nested/monorepo configs credit the sibling `content.config` rather than a root one. Gated on module registration, not mere package.json presence: an installed-but-unregistered `@nuxt/content` leaves a genuinely-orphan `content.config` correctly flagged. Scoped to the first-party `@nuxt/content` only; third-party Nuxt module config files are deliberately not hardcoded (declined in #138). No `CACHE_VERSION` bump (`resolve_config` is analysis-time, not extraction). See issue #792.

## Analysis-level
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **Pinia stores auto-imported by `@pinia/nuxt` are now tracked in the module graph.** Fallow now activates a Pinia plugin on `@pinia/nuxt`, scans direct store files in `stores/` and Nuxt 4 `app/stores/`, and maps value exports named `use<Name>Store` to the file that provides them. Pages that call `useUserStore()` without an import now keep `stores/user.ts` reachable and credit the matching export, while sibling helper exports can still report as unused. Nested stores and custom `storesDirs` remain out of scope for the default detector. (Closes [#740](https://github.com/fallow-rs/fallow/issues/740).)
- **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).)
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -751,11 +751,11 @@ See the [full configuration reference](https://docs.fallow.tools/configuration/o

## Framework plugins

117 built-in plugins detect entry points, convention exports, config-defined aliases, and template-visible usage for your framework automatically.
118 built-in plugins detect entry points, convention exports, config-defined aliases, and template-visible usage for your framework automatically.

| Category | Plugins |
|---|---|
| **Frameworks** | Next.js, Nuxt, Remix, Qwik, SvelteKit, Gatsby, Astro, Angular, NestJS, AdonisJS, Contentlayer, Fumadocs, Lit, Obsidian, Ember, Expo, Expo Router, Electron, and more |
| **Frameworks** | Next.js, Nuxt, Pinia, Remix, Qwik, SvelteKit, Gatsby, Astro, Angular, NestJS, AdonisJS, Contentlayer, Fumadocs, Lit, Obsidian, Ember, Expo, Expo Router, Electron, and more |
| **Bundlers** | Vite, Webpack, Rspack, Rsbuild, Rollup, Rolldown, Tsup, Tsdown, pkg-utils, Parcel |
| **Testing** | Vitest, Jest, Playwright, Cypress, Storybook, Stryker, Mocha, Ava, tap, tsd |
| **CI/CD & Release** | Danger, Commitlint, Commitizen, Semantic Release |
Expand Down
1 change: 1 addition & 0 deletions crates/core/src/plugins/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,7 @@ mod opennext_cloudflare;
mod oxlint;
mod pandacss;
mod parcel;
mod pinia;
mod pkg_utils;
mod playwright;
mod plop;
Expand Down
Loading
Loading