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 .claude/rules/detection.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ Non-obvious implementation details for each detection feature. These are NOT dis
- **Entry-point public API class members**: class methods/properties on classes exposed through non-private package entry points are not reported as `unused-class-member` by default. The detector requires public-API provenance from an actual non-private package entry point re-export (`main`, root `exports`, subpath `exports`), from a barrel inside the transitive `export *` closure rooted at one, or from `src/**/index.*` source subpath indexes when the package has no `exports` map. Do not key this on `ReferenceKind::ReExport` alone: internal barrels also create re-export references and must keep member-level detection. Named, renamed, default-as-named, package subpath exports, source-first root `index.js` fallbacks, exportless source subpath indexes, and multi-hop star barrels are covered. The skip is class-member-only; enum member behavior is unchanged, private app packages do not get this skip, and reachable internal classes imported by an entry point but not re-exported still report unused public members. See issue #643.
- **Configurable class member allowlist**: the built-in Angular/React lifecycle check (see `predicates::lifecycle`) is extended at analysis time with two user-controlled sources: (1) top-level `FallowConfig.used_class_members`, (2) `Plugin::used_class_members()` / `ExternalPluginDef.used_class_members` on active plugins. Both sources feed `AggregatedPluginResult.used_class_members`, which `analyze/mod.rs` merges with the config list into a single `FxHashSet<&str>` passed to `find_unused_members`. Intended for third-party interface patterns where a library calls consumer methods reflectively (ag-Grid's `agInit`/`refresh`, Web Components' `connectedCallback`). The allowlist is scoped to class members — matching enum member names are still flagged. Member strings containing `*` or `?` compile as `globset::Glob` matchers, supporting `"*"` (every member on a heritage-matching class), `"enter*"` / `"*Handler"` (prefix / suffix), and `"on*Event"` (combined). Globs run alongside exact-name matches and respect any `extends` / `implements` heritage clause on a scoped rule. Glob patterns matching zero members emit a single `WARN` per pattern at the end of the run via `ClassMemberAllowlist::warn_unmatched_patterns`, surfacing dead allowlist entries; exact-name entries do not warn. See issues #98, #254.
- **Lexical custom-node lifecycle members**: the `lexical` plugin (enabler: `lexical` or any `@lexical/`-scoped package) credits the methods Lexical calls reflectively on custom nodes, so classes that `extends DecoratorNode` / `ElementNode` / `TextNode` do not surface their framework-invoked methods as `unused-class-member`. Implemented via heritage-scoped `UsedClassMemberRule::Scoped` rules in `crates/core/src/plugins/lexical.rs`, mirroring the `lit` / `ember` plugins. The shared lifecycle / serialization / DOM-reconciliation set (`getType`, `clone`, `importJSON`, `importDOM`, `exportJSON`, `exportDOM`, `createDOM`, `updateDOM`, `updateFromJSON`, `getTextContent`) is credited on all three bases; `isInline` is additionally credited on `ElementNode` and `DecoratorNode` only (TextNode is inherently inline and has no `isInline()` hook); `decorate` is credited on `DecoratorNode` only. The per-base layering means a genuinely-dead `isInline` on a TextNode subclass or `decorate` on an ElementNode subclass still surfaces. Method names verified against the Lexical custom-node docs (lexical.dev/docs/concepts/nodes). Heritage matching is on the direct superclass name, so an intermediate base between the user class and the Lexical base is not covered; custom nodes extend a Lexical base directly in practice. Non-lifecycle methods on a node subclass are still reported. No `tooling_dependencies()` is declared because Lexical packages are directly-imported runtime deps. Replaces the manual scoped `usedClassMembers` allowlist users hand-write today. See issue #628.
- **NestJS framework-dispatched lifecycle / handler members**: the `nestjs` plugin (enabler: `@nestjs/core`) credits the methods Nest invokes reflectively on consumer classes, so they are not reported as `unused-class-member`. Implemented via heritage-scoped `UsedClassMemberRule::Scoped` rules in `crates/core/src/plugins/nestjs.rs` (one rule per Nest interface, all `implements`-scoped, never `extends`), mirroring the `lit` / `lexical` / `ember` plugins. Handler dispatch methods are scoped to their single interface: `configure` (`NestModule`), `canActivate` (`CanActivate`), `intercept` (`NestInterceptor`), `transform` (`PipeTransform`), `catch` (`ExceptionFilter`), `use` (`NestMiddleware`). The five lifecycle interfaces (`OnModuleInit`, `OnModuleDestroy`, `OnApplicationBootstrap`, `BeforeApplicationShutdown`, `OnApplicationShutdown`) each credit ALL five lifecycle method names (`onModuleInit`, `onModuleDestroy`, `onApplicationBootstrap`, `beforeApplicationShutdown`, `onApplicationShutdown`) rather than only the one method the implemented interface declares. This is intentional and more correct: Nest's lifecycle manager dispatches by duck-typed method presence, so a class that implements one lifecycle interface but also defines a sibling hook method has that sibling invoked at runtime too. Heritage matching is on `implements`, so an ordinary class that implements none of these interfaces still surfaces a genuinely-unused `transform` / `configure` / lifecycle method, and non-lifecycle methods on a Nest class are still reported. No `CACHE_VERSION` bump: the rules are interpreted at analyze time against existing `implements` heritage data and never enter the extraction cache. See issue #843.
- **Native Error subclass `name` members**: a class member named `name` declared on a class whose heritage reaches a native ECMAScript error constructor (`Error`, `TypeError`, `RangeError`, `SyntaxError`, `ReferenceError`, `EvalError`, `URIError`, `AggregateError`) is treated as runtime-used and never reported as `unused-class-member`. Custom error classes set `name` so logs, serializers, and `err.name === "..."` discrimination expose the error type, none of which is a static member access fallow can see. Implemented as a built-in heritage-scoped suppression in `crates/core/src/analyze/unused_members.rs` (`is_error_subclass_runtime_member`), mirroring the existing native-Custom-Elements built-in (`is_native_custom_element_lifecycle_method`, `super_class == Some("HTMLElement")`); no plugin or dependency gate because the bases are JavaScript globals. `build_error_subclass_export_keys` seeds on classes that directly extend a native error name, then walks `extends` chains downward via `build_parent_to_children`, so a transitive `class ApiError extends DomainError` where `DomainError extends Error` (resolved through imports, local exports, and re-export origins) is also covered. The rule is heritage-scoped: an ordinary `class Person { name = "x" }` still reports an unused `name`, and members other than `name` on error subclasses (`message` / `stack` / `cause` are inherited and rarely re-declared; arbitrary helper methods) still report. Seeding is by direct super-class name, matching the HTMLElement built-in, so a local class shadowing a native error name is treated as a native error base (pathological in practice). Same-file NON-exported intermediate base classes are not resolved, the same limitation `build_parent_to_children` carries for all heritage-aware member crediting. No `CACHE_VERSION` bump: only analyze-time interpretation of the existing `class_heritage` / `super_class` extraction data changes. See issue #620.
- **Cross-package namespace re-export through object literal**: `import * as foo from './bar'; export const API = { foo }` in a barrel followed by `import { API } from '@scope/lib'; API.foo.bar` in another package credits `bar` on `./bar.ts`. The visitor records per-module `NamespaceObjectAlias` entries `{ via_export_name, suffix, namespace_local }` at finalize time (covering shorthand `{ foo }` and nested forms `{ motionNet: { adEngine } }`); both `into_module_info` and the SFC `merge_into` paths populate them. The graph layer threads them onto `ResolvedModule` and a Phase 2b pass (after reference population, before reachability) walks each consumer's `member_accesses` for `object == "<consumer_local>.<suffix>"`, looks up the namespace import's target file via the module's resolved imports filtered to `ImportedName::Namespace`, and runs the `mark_member_exports_referenced` + `create_synthetic_exports_for_star_re_exports` pair from `narrowing.rs` so star-barrel namespace targets (`./foo/index.ts` doing `export * from './bar'`) get a synthetic stub export the Phase 4 chain resolver can carry to the real defining file. Default-export aliases match against `ImportedName::Default`. **Consumer-side multi-hop** (issue #310): the alias-defining file may sit behind 1-3 named-re-export barrels (`pkg/index.ts` -> `pkg/methods/index.ts` -> `pkg/methods/methods.ts`); the matcher does a forward BFS over re-export edges starting from `(alias_file, via_export_name)` to enumerate every `(barrel_file, exported_name_at_barrel)` pair through which the alias is reachable, then matches consumer imports against the full set. Plain `export *` propagates names unchanged; `export { A as B }` propagates the renamed `B`; `export * as ns` is skipped at non-seed barrels (its source identifiers are hidden behind `ns.<name>`, not exposed directly). **Target-side multi-hop** (issue #328): the namespace target itself may chain through `export * as N from './source'` namespace re-exports (`barrel.ts` does `export * as inner from './leaf'`; consumer accesses `API.foo.inner.X`). After the initial Phase 2b credit lands on a name namespace-re-exported on the target, the matcher walks chained `export * as <name> from './source'` edges on the alias-target side and propagates the consumer's deeper accesses (`<expected_object>.<member>.<X>`) onto the underlying source recursively via `ChainWalkCtx`. Cycle-protected by a per-access visited set keyed on module index. Bumps the extract `CACHE_VERSION` because the new `namespace_object_aliases` field changes serialized module state. See issue #303 (cross-package case) building on issue #269 (same-file case which `resolve_bound_member_accesses` already handled by re-emitting the access against the namespace local); #310 (consumer-side multi-hop); #328 (target-side namespace-re-export chain).
- **Cross-package member access through barrels**: `import { Foo } from '@scope/lib'` followed by `Foo.bar` correctly credits `bar` on the originating file even when `@scope/lib`'s entry resolves to a barrel that re-exports `Foo` from a sibling module. Phase 4 chain resolution synthesizes a stub `ExportSymbol` on the barrel for reference tracking, so without further propagation the access map keys at the barrel and the detection loop's origin-keyed lookup misses where `members` are populated. `find_unused_members` walks each access key through `ReExportEdge` chains (named re-exports, renamed re-exports, and `export *` fan-out when no named edge matches) to every defining-site export and copies the access set. Cycle-protected. `export * as ns from './mod'` edges are intentionally skipped because two-level `ns.Foo.member` access isn't tracked at extract time. Same propagation runs for `whole_object_used_exports` (`Object.values(Foo)` on a re-exported enum). Covers both `unused_enum_members` and `unused_class_members`. See issue #178.
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

- **NestJS lifecycle and handler methods are no longer reported as unused class members.** The `nestjs` plugin now credits the methods Nest invokes reflectively on consumer classes: `configure` on `NestModule`, the guard/interceptor/pipe/filter/middleware dispatch methods (`canActivate`, `intercept`, `transform`, `catch`, `use`) on their respective interfaces, and the five module lifecycle hooks (`onModuleInit`, `onModuleDestroy`, `onApplicationBootstrap`, `beforeApplicationShutdown`, `onApplicationShutdown`). Each rule is heritage-scoped to its Nest interface via `implements`, so an ordinary class that implements none of these interfaces still reports a genuinely unused method, and non-lifecycle methods on a Nest class are still flagged. A class that implements one lifecycle interface also has its sibling lifecycle hooks credited, matching Nest's duck-typed lifecycle dispatch. (Closes [#843](https://github.com/fallow-rs/fallow/issues/843).)
- **Svelte markup `<script src>` tags no longer surface as unresolved imports.** Svelte treats tags such as `<svelte:head><script src="/some-lib.min.js" async></script></svelte:head>` as runtime HTML, not bundled SFC script modules. Fallow now keeps parsing the `src` metadata for span fidelity but no longer emits synthetic imports for Svelte `script src` references, so root-relative browser assets do not become false `unresolved-imports`. Vue external scripts keep their existing graph edges. Thanks [@codingthat](https://github.com/codingthat) for the report. (Closes [#835](https://github.com/fallow-rs/fallow/issues/835).)
- **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).)
Expand Down
Loading
Loading