Repository navigation
v2.73.0: pnpm overrides detection, catalog auto-fix, custom-location ESLint configs
Highlights
Two new detectors and one walker fix:
- pnpm
overrides:detection: flag entries whose target package no longer appears in any workspacepackage.json, plus a separate rule for malformed override grammar thatpnpm installwould refuse to honor. fallow fixauto-removes unused pnpmcatalog:entries: close the round-trip on the v2.70.0 catalog detector with a line-aware YAML deleter that preserves comments and rewrites emptied parents to{}sopnpm installkeeps working.- Hidden directories referenced by
package.json#scriptsare now walked:.config/eslint.config.js(and similar custom-location configs) get parsed instead of silently invisible, so their imports are credited as used.
Upgrade note
The new misconfigured-dependency-overrides rule defaults to error. A workspace with a malformed override key or empty value will flip from a green fallow check on v2.72 to a red one on v2.73. To absorb the change without action, set rules.misconfigured-dependency-overrides: "warn" in your fallow config before upgrading.
Added
Detects unused and misconfigured pnpm overrides entries
Two new rules read both pnpm-workspace.yaml's overrides: top-level (canonical, pnpm 9+) and the root package.json's pnpm.overrides (legacy form).
unused-dependency-overrides (default warn) flags entries whose target package is not declared in any workspace package.json. The conservative static algorithm uses the parent-chain rule: react>react-dom is considered USED when EITHER react OR react-dom is declared, covering the CVE-fix pattern where the parent is declared and the override forces a transitive version. Findings carry the raw key, structured decomposition (target_package, parent_package, version_constraint, version_range), the source file (pnpm-workspace.yaml or package.json), a 1-based line number, and an optional hint flagging entries that may target a purely transitive dependency (CVE-fix or canary-alias pattern).
misconfigured-dependency-overrides (default error) catches entries whose key cannot be parsed (empty key, dangling separators) or whose value is missing; pnpm install refuses to honor these. Special pnpm values (- removal, $ref self-reference, npm:alias@^1) are explicitly allowlisted and never flagged as misconfigured.
Suppression is config-only via ignoreDependencyOverrides: [{ package, source? }]. Inline YAML / JSON comments are not feasible since pnpm-workspace.yaml uses YAML and package.json has no comment syntax; the optional source field scopes a suppression to "pnpm-workspace.yaml" or "package.json".
All six report formats render the findings: human two-tier, JSON with discriminated remove-dependency-override / fix-dependency-override primary actions + ignoreDependencyOverrides add-to-config suppress, SARIF rules fallow/unused-dependency-override and fallow/misconfigured-dependency-override, compact, markdown, CodeClimate. The GitHub Action and GitLab CI jq scripts surface both in summary tables and emit ::warning for unused and ::error for misconfigured annotations. The LSP emits matching diagnostics anchored on the source file line. The MCP analyze tool accepts issue_types: ["unused-dependency-overrides", "misconfigured-dependency-overrides"], and the VS Code "Unused Code" tree shows two new categories.
fallow explain unused-dependency-override and fallow explain misconfigured-dependency-override open with the CVE-pin caveat and pnpm-grammar examples respectively.
Closes #336.
fallow fix auto-removes unused pnpm catalog entries
The unused-catalog-entries detector shipped in v2.70.0, but until now the only available action was # fallow-ignore-next-line unused-catalog-entry; users had to hand-edit the YAML to drop the entry.
The fix is line-aware (preserves comments and stylistic choices) and detects object-form entries such as
react:
specifier: ^18.2.0
publishConfig: {}by consuming subsequent lines whose indent is strictly greater than the entry's own.
When removing the last entry of a catalog group (default catalog: or a named catalogs.<name>:) leaves the header with no children, the fix rewrites the header to catalog: {} / <name>: {} so the file stays installable; bare key: in YAML parses as null which pnpm rejects with Cannot convert undefined or null to object at install time.
Entries whose hardcoded_consumers is non-empty are skipped: removing the catalog entry while a workspace package still pins a hardcoded version of the same package would break the user's next pnpm install. The skip is surfaced in the human stderr summary and in the JSON output ({"type": "remove_catalog_entry", "applied": false, "skipped": true, "skip_reason": "hardcoded_consumers", "consumers": [...]}), and the per-instance auto_fixable bool on the check-command action correctly flips to false for findings with hardcoded consumers so agents that filter on the bool skip those automatically.
After a successful run the CLI emits a one-line Run \pnpm install` to refresh pnpm-lock.yamlreminder so the workspace stays internally consistent. The fix output's top-level envelope adds a"skipped"count alongside the existing"total_fixed"` so consumers can gate on partial-fix runs.
The LSP unused-catalog-entry diagnostic now exposes a matching Remove unused catalog entry quick-fix code action with the same hardcoded-consumer guard, the same empty-parent rewrite, and an anchored key-prefix sanity check so sibling entries with shared prefixes (react vs react-native, lodash vs lodash-es) cannot be deleted by mistake.
Closes #335.
Fixed
Dependencies imported only from a config file in a hidden directory are no longer reported as unused
A script like "lint": "eslint -c .config/eslint.config.js" puts the ESLint config in .config/, a hidden directory the file walker normally skips. Fallow detected the file as an entry point (the script-parser found it) but never parsed it, so the imports inside (@eslint/js, anything else the config pulls in) became phantom unused dependencies.
The walker now traverses hidden directories referenced from package.json#scripts config arguments (--config / -c) and positional file arguments. The same scope mechanism React Router uses for .client / .server is reused; the names are extracted per-package and scoped to the package root, so a workspace's .config/ stays isolated from a sibling's.
Three guardrails:
- A denylist of known build-cache / VCS / IDE directories (
.git,.next,.nuxt,.output,.svelte-kit,.turbo,.nx,.cache,.parcel-cache,.vercel,.netlify,.yarn,.pnpm-store,.docusaurus,.vscode,.idea,.fallow,.husky) is never auto-scoped even when a script reads or writes into one. - Only the structured
config_args/file_argsoutputs of the script parser are inspected, so a logging path like.nx/cache/result.jsonmentioned in a script body cannot pull.nx/into scope. - Paths containing
..segments are skipped so a workspace script referencing../../.config/...cannot generate a scope rooted at a sibling.
Projects without hidden-dir paths in their scripts see no behavior change. Surfaces a tracing::debug! line per inferred scope (RUST_LOG=fallow_core=debug) for support diagnostics.
Closes #358. Thanks @FunctionDJ for the report.
Changed
- Internal refactor: drop redundant per-call
FileId/ path indices in detectors (find_unused_exports,find_private_type_leaks,find_duplicate_exports). Findings counts unchanged acrossnext.js(24986),typescript,vite,vue-core,svelte. No behavior change. Closes #333. - CI dependency bumps:
pnpm/action-setup6.0.3 -> 6.0.5,crate-ci/typos1.45.2 -> 1.46.0.
Full Changelog: v2.72.0...v2.73.0