Skip to content

v2.73.0: pnpm overrides detection, catalog auto-fix, custom-location ESLint configs

Choose a tag to compare

@BartWaardenburg BartWaardenburg released this 12 May 22:57
· 3860 commits to main since this release
v2.73.0
b163749

Highlights

Two new detectors and one walker fix:

  • pnpm overrides: detection: flag entries whose target package no longer appears in any workspace package.json, plus a separate rule for malformed override grammar that pnpm install would refuse to honor.
  • fallow fix auto-removes unused pnpm catalog: 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 {} so pnpm install keeps working.
  • Hidden directories referenced by package.json#scripts are 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_args outputs of the script parser are inspected, so a logging path like .nx/cache/result.json mentioned 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 across next.js (24986), typescript, vite, vue-core, svelte. No behavior change. Closes #333.
  • CI dependency bumps: pnpm/action-setup 6.0.3 -> 6.0.5, crate-ci/typos 1.45.2 -> 1.46.0.

Full Changelog: v2.72.0...v2.73.0