skills: fallow
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,644 @@
|
||||
# Fallow: Critical Gotchas
|
||||
|
||||
Common pitfalls and their correct solutions when working with fallow.
|
||||
|
||||
---
|
||||
|
||||
## `fix` Requires `--yes` in Non-TTY Environments
|
||||
|
||||
The `fix` command prompts for confirmation in interactive terminals. In agent subprocesses, CI pipelines, or piped input (non-TTY), the `--yes` flag is mandatory. Without it, `fix` exits with code 2 and an error.
|
||||
|
||||
```bash
|
||||
# WRONG: fix exits with code 2 in non-TTY
|
||||
fallow fix --format json --quiet
|
||||
|
||||
# CORRECT: always use --dry-run first, then --yes
|
||||
fallow fix --dry-run --format json --quiet # preview
|
||||
fallow fix --yes --format json --quiet # apply
|
||||
```
|
||||
|
||||
Always preview with `--dry-run` before applying. This is a destructive operation that modifies source files.
|
||||
|
||||
---
|
||||
|
||||
## Don't Create Config Unless Needed
|
||||
|
||||
Fallow works with zero configuration for most projects thanks to 121 auto-detecting framework plugins. Creating an unnecessary config file can mask issues or override detection behavior.
|
||||
|
||||
```bash
|
||||
# WRONG: creating config for a standard Next.js project
|
||||
fallow init
|
||||
# This may override auto-detected settings
|
||||
|
||||
# CORRECT: run analysis first with zero config
|
||||
fallow dead-code --format json --quiet
|
||||
# Only create config if you need to customize rules, ignore patterns, or entry points
|
||||
```
|
||||
|
||||
Only create a config when you need to:
|
||||
- Change rule severity levels for incremental adoption
|
||||
- Add custom ignore patterns or ignore dependencies
|
||||
- Specify additional entry points not auto-detected
|
||||
- Configure duplication detection settings
|
||||
|
||||
---
|
||||
|
||||
## Use `--format json` for Agent Consumption
|
||||
|
||||
Human-formatted output contains ANSI colors, progress bars, and timing info. Never parse it programmatically.
|
||||
|
||||
```bash
|
||||
# WRONG: parsing human output
|
||||
fallow dead-code | grep "unused"
|
||||
|
||||
# CORRECT: use structured JSON
|
||||
fallow dead-code --format json --quiet
|
||||
```
|
||||
|
||||
The `--quiet` flag suppresses progress bars on stderr. Without it, stderr output may interfere with stdout parsing.
|
||||
|
||||
---
|
||||
|
||||
## `--changed-since` Shows Only New Issues
|
||||
|
||||
The `--changed-since` flag limits analysis to files modified since a git ref. It only reports issues in those files, not all issues in the project. Works with both `dead-code` and `dupes`.
|
||||
|
||||
```bash
|
||||
# This only shows issues in files changed since main
|
||||
fallow dead-code --format json --quiet --changed-since main
|
||||
|
||||
# Same for duplication — only clone groups involving changed files
|
||||
fallow dupes --format json --quiet --changed-since main
|
||||
|
||||
# This shows ALL issues in the project
|
||||
fallow dead-code --format json --quiet
|
||||
```
|
||||
|
||||
Don't use `--changed-since` when auditing the full project. Use it for PR checks and incremental CI.
|
||||
|
||||
---
|
||||
|
||||
## Filter Flags Are Additive
|
||||
|
||||
Issue type filter flags (`--unused-exports`, `--unused-files`, etc.) are inclusive. They select which issue types to show. Using multiple flags shows the union.
|
||||
|
||||
```bash
|
||||
# Shows only unused exports
|
||||
fallow dead-code --format json --quiet --unused-exports
|
||||
|
||||
# Shows unused exports AND unused files
|
||||
fallow dead-code --format json --quiet --unused-exports --unused-files
|
||||
|
||||
# Shows ALL issue types (default when no filter is specified)
|
||||
fallow dead-code --format json --quiet
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Syntactic Analysis: No TypeScript Compiler
|
||||
|
||||
Fallow uses Oxc for pure syntactic analysis. It does not run the TypeScript compiler. This means:
|
||||
|
||||
- **Fully dynamic imports** (`import(variable)`) are not resolved. Only static strings, template literals with static prefixes, `import.meta.glob`, and `require.context` patterns
|
||||
- **Value-level type narrowing** is not performed. Fallow can't know that `if (x instanceof Foo)` means `Foo` is "used"
|
||||
- **Conditional exports** based on runtime values are not analyzed
|
||||
- **Function overload signatures are deduplicated**: TypeScript function overloads (multiple signatures for the same function name) are merged into a single export. They are not reported as separate unused exports
|
||||
|
||||
```typescript
|
||||
// RESOLVED: static pattern with prefix
|
||||
import(`./locales/${lang}.json`);
|
||||
|
||||
// RESOLVED: import.meta.glob
|
||||
const modules = import.meta.glob('./modules/*.ts');
|
||||
|
||||
// NOT RESOLVED: fully dynamic
|
||||
const mod = import(someVariable);
|
||||
```
|
||||
|
||||
If fallow falsely flags something due to dynamic patterns, use inline suppression:
|
||||
|
||||
```typescript
|
||||
// fallow-ignore-next-line unused-export
|
||||
export const dynamicallyUsed = createHandler();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Re-Export Chains Are Resolved
|
||||
|
||||
Fallow fully resolves `export *` and named re-export chains through barrel files. An export consumed through a chain of barrel files is NOT falsely flagged.
|
||||
|
||||
```typescript
|
||||
// src/utils.ts
|
||||
export const helper = () => {}; // NOT flagged, used via barrel chain
|
||||
|
||||
// src/index.ts (barrel)
|
||||
export * from './utils';
|
||||
|
||||
// src/app.ts
|
||||
import { helper } from './index'; // Resolves through the chain
|
||||
```
|
||||
|
||||
If an export IS flagged as unused despite being in a barrel file, it means no downstream consumer actually imports it. The barrel file re-exports it, but nobody uses it from there.
|
||||
|
||||
---
|
||||
|
||||
## Exit Code 1 vs 2
|
||||
|
||||
| Code | Meaning | Action |
|
||||
|------|---------|--------|
|
||||
| 0 | No error-severity issues | Success |
|
||||
| 1 | Error-severity issues found | Review findings |
|
||||
| 2 | Runtime error (`fix` without `--yes` in non-TTY, invalid config) | Fix config or add `--yes` |
|
||||
|
||||
Exit code 1 is triggered by issues with `"error"` severity in the rules config. Without a rules section, all issue types default to `"error"`. Use the rules system to control which issues fail CI:
|
||||
|
||||
```jsonc
|
||||
// Only fail on unused files and deps, warn on everything else
|
||||
{
|
||||
"rules": {
|
||||
"unused-files": "error",
|
||||
"unused-dependencies": "error",
|
||||
"unused-exports": "warn",
|
||||
"unused-types": "warn"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `--fail-on-issues` Promotes Warn to Error
|
||||
|
||||
The `--fail-on-issues` flag promotes all `warn`-severity rules to `error` for that run. This means exit code 1 for ANY reported issue.
|
||||
|
||||
```bash
|
||||
# With rules: { "unused-exports": "warn" }
|
||||
|
||||
# This exits 0 even with warn-level findings
|
||||
fallow dead-code --format json --quiet
|
||||
|
||||
# This exits 1 if ANY issue is found (warn promoted to error)
|
||||
fallow dead-code --format json --quiet --fail-on-issues
|
||||
```
|
||||
|
||||
Use `--fail-on-issues` for strict CI gates. Use the rules system for gradual adoption.
|
||||
|
||||
---
|
||||
|
||||
## Baseline Comparison Tracks Issue Identity
|
||||
|
||||
Baselines track issues by identity (file + issue type + name), not by count. Adding a new unused export while fixing an old one doesn't cancel out.
|
||||
|
||||
```bash
|
||||
# Save current state as baseline
|
||||
fallow dead-code --format json --quiet --save-baseline fallow-baselines/dead-code.json
|
||||
|
||||
# Later: only fail on NEW issues not in the baseline
|
||||
fallow dead-code --format json --quiet --baseline fallow-baselines/dead-code.json --fail-on-issues
|
||||
```
|
||||
|
||||
Commit the baseline file to your repo. Update it periodically as you fix existing issues.
|
||||
|
||||
---
|
||||
|
||||
## Duplication Modes Affect What's Detected
|
||||
|
||||
The detection mode significantly affects results. Choose based on your needs:
|
||||
|
||||
```bash
|
||||
# strict: exact token match only
|
||||
fallow dupes --format json --quiet --mode strict
|
||||
# Catches: copy-pasted code with zero changes
|
||||
|
||||
# mild (default): syntax normalized
|
||||
fallow dupes --format json --quiet --mode mild
|
||||
# Catches: whitespace and semicolon differences
|
||||
|
||||
# weak: literal values normalized
|
||||
fallow dupes --format json --quiet --mode weak
|
||||
# Catches: same structure with different strings/numbers
|
||||
|
||||
# semantic: identifier names normalized
|
||||
fallow dupes --format json --quiet --mode semantic
|
||||
# Catches: same logic with renamed variables
|
||||
```
|
||||
|
||||
`semantic` mode produces the most findings but may include false positives where similar structure is coincidental.
|
||||
|
||||
---
|
||||
|
||||
## Workspace Flag Scopes Output, Not Analysis
|
||||
|
||||
The `--workspace` flag scopes **output** to a single package, but the full cross-workspace module graph is still built. This means:
|
||||
|
||||
- Imports from other workspace packages are still resolved
|
||||
- Re-export chains crossing package boundaries are still tracked
|
||||
- Only issues IN the specified package are reported
|
||||
|
||||
```bash
|
||||
# Analyze everything, show only issues in "my-package"
|
||||
fallow dead-code --format json --quiet --workspace my-package
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Production Mode Excludes Test Files
|
||||
|
||||
`--production` excludes test/dev files and only analyzes production scripts. This changes what's reported:
|
||||
|
||||
- Test files (`*.test.*`, `*.spec.*`, `*.stories.*`, `__tests__/**`) are excluded
|
||||
- Only `start`, `build`, `serve`, `preview`, `prepare` scripts are analyzed
|
||||
- Unused devDependencies are NOT reported (forced to `off`)
|
||||
- Type-only production dependencies ARE reported (should be devDependencies)
|
||||
|
||||
```bash
|
||||
# WRONG: using --production for a full audit
|
||||
fallow dead-code --format json --quiet --production
|
||||
# Misses test-file dead code and devDependency issues
|
||||
|
||||
# CORRECT: use --production only for production-focused CI
|
||||
fallow dead-code --format json --quiet --production --fail-on-issues
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Watch Mode Is Not for Agents
|
||||
|
||||
The `watch` command starts an interactive file watcher that never exits. Never use it in agent workflows.
|
||||
|
||||
```bash
|
||||
# WRONG: this will hang forever
|
||||
fallow watch
|
||||
|
||||
# CORRECT: run one-shot analysis
|
||||
fallow dead-code --format json --quiet
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Suppressing Duplication False Positives
|
||||
|
||||
Code duplication has its own suppression token: `code-duplication`. Use it for intentionally similar code (e.g., test helpers, generated patterns).
|
||||
|
||||
```typescript
|
||||
// WRONG: using the wrong token
|
||||
// fallow-ignore-file unused-export
|
||||
// This suppresses dead code, not duplication
|
||||
|
||||
// CORRECT: suppress duplication for a specific line
|
||||
// fallow-ignore-next-line code-duplication
|
||||
const handler = createStandardHandler(config);
|
||||
|
||||
// CORRECT: suppress all duplication in a file
|
||||
// fallow-ignore-file code-duplication
|
||||
```
|
||||
|
||||
This is separate from the dead code suppression tokens. See the full list of valid tokens in the [CLI Reference](cli-reference.md#inline-suppression-comments).
|
||||
|
||||
---
|
||||
|
||||
## Decorated Members Are Skipped By Default
|
||||
|
||||
Class members with decorators (NestJS `@Get()`, Angular `@Input()`, TypeORM `@Column()`, etc.) are excluded from unused member detection by default. Decorator-driven frameworks consume these via reflection at runtime, so reporting them as unused would be a false positive.
|
||||
|
||||
```typescript
|
||||
class UserController {
|
||||
@Get('/users')
|
||||
getUsers() { ... } // NOT flagged, has decorator
|
||||
}
|
||||
```
|
||||
|
||||
### Opt specific decorators out via `ignoreDecorators`
|
||||
|
||||
If you use utility decorators that DO NOT imply reflective use (Playwright's `@step("label")`, internal labeling decorators like `@measure`, `@log`, `@retry`), list their names in the `ignoreDecorators` config option so the methods carrying them are checked for usage like undecorated methods.
|
||||
|
||||
```jsonc
|
||||
// .fallowrc.json
|
||||
{
|
||||
"ignoreDecorators": ["@step"]
|
||||
}
|
||||
```
|
||||
|
||||
Conservative semantics: a method carrying any decorator NOT in the list still gets skipped. So `@step` + `@Inject` on the same method stays treated as framework-managed. Matching rule: entries containing `.` (`"decorators.log"`) match the full dotted path; bare entries (`"step"` or `"decorators"`) match the leftmost segment, so a single bare `"decorators"` entry collapses an entire `@decorators.*` namespace. Both `"@step"` and `"step"` round-trip equivalently. Unmatched entries (a decorator name in the config that never appears in your codebase) surface as a one-time warning at end of run.
|
||||
|
||||
The default empty list preserves today's skip-all behavior, so existing NestJS / Angular / TypeORM projects see no change.
|
||||
|
||||
---
|
||||
|
||||
## JSDoc Visibility Tags Keep Exports Alive
|
||||
|
||||
Exports annotated with `/** @public */`, `/** @internal */`, `/** @beta */`, `/** @alpha */`, or `/** @api public */` are never reported as unused. This is designed for library authors whose exports are consumed by external projects not visible to fallow.
|
||||
|
||||
```typescript
|
||||
// NOT flagged: @public annotation
|
||||
/** @public */
|
||||
export const createWidget = () => {};
|
||||
|
||||
// NOT flagged: @internal annotation
|
||||
/** @internal */
|
||||
export const resetState = () => {};
|
||||
|
||||
// NOT flagged: @beta annotation
|
||||
/** @beta */
|
||||
export const experimentalFeature = () => {};
|
||||
|
||||
// NOT flagged: @alpha annotation
|
||||
/** @alpha */
|
||||
export const unstableApi = () => {};
|
||||
|
||||
// NOT flagged: @api public variant
|
||||
/** @api public */
|
||||
export interface WidgetConfig {}
|
||||
|
||||
// STILL flagged: line comments don't count
|
||||
// @public
|
||||
export const notProtected = () => {};
|
||||
```
|
||||
|
||||
Only `/** */` JSDoc block comments are recognized. Line comments (`// @public`) are ignored.
|
||||
|
||||
---
|
||||
|
||||
## `@expected-unused` JSDoc Tag for Intentional Dead Code
|
||||
|
||||
Exports annotated with `/** @expected-unused */` are treated as intentionally unused. They are excluded from unused export detection AND tracked for staleness. If the export later becomes used (imported by another module), fallow reports the `@expected-unused` tag as stale via the `stale-suppressions` rule.
|
||||
|
||||
```typescript
|
||||
// NOT flagged as unused: @expected-unused annotation
|
||||
/** @expected-unused */
|
||||
export const deprecatedHelper = () => {};
|
||||
|
||||
// If something starts importing deprecatedHelper,
|
||||
// fallow reports the @expected-unused tag as stale
|
||||
```
|
||||
|
||||
Use `@expected-unused` instead of `// fallow-ignore-next-line` when you want fallow to notify you if the export becomes referenced again. The `stale-suppressions` rule (default: `warn`) controls severity.
|
||||
|
||||
Only `/** */` JSDoc block comments are recognized. The tag works on all export types.
|
||||
|
||||
---
|
||||
|
||||
## Stale Suppression Detection
|
||||
|
||||
Fallow detects `// fallow-ignore` comments and `@expected-unused` JSDoc tags that no longer match any issue. This prevents suppression comments from silently hiding issues that have been resolved or moved.
|
||||
|
||||
```typescript
|
||||
// STALE: the export below is actually used now
|
||||
// fallow-ignore-next-line unused-export
|
||||
export const helper = () => {}; // imported in app.ts
|
||||
```
|
||||
|
||||
Use `--stale-suppressions` to filter for only stale suppression findings. The `stale-suppressions` rule defaults to `warn`. Set to `error` in CI to enforce suppression hygiene:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"rules": {
|
||||
"stale-suppressions": "error"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## JSDoc `import()` Types Count as References
|
||||
|
||||
Types referenced only from JSDoc `import()` annotations are tracked as type-only imports, so the referenced exports are not flagged as unused. This works for plain JavaScript files that want TypeScript types without converting to `.ts`.
|
||||
|
||||
```js
|
||||
// src/app.js
|
||||
|
||||
/**
|
||||
* @param cfg {import('./types.ts').Config}
|
||||
* @returns {import('./types.ts').Result}
|
||||
*/
|
||||
function boot(cfg) {
|
||||
return { ok: true };
|
||||
}
|
||||
```
|
||||
|
||||
Fallow treats `Config` and `Result` in `./types.ts` as used. Works with `@param`, `@returns`, `@type`, `@typedef`, `@callback`, union annotations (`{import('./a').A | import('./b').B}`), nested member access, bare package specifiers, and parent-relative paths. Only `/** */` blocks are scanned.
|
||||
|
||||
---
|
||||
|
||||
## JSX `<script src>` and `<link href>` Are Asset References
|
||||
|
||||
Inside JSX/TSX files, lowercase intrinsic `<script src="...">` and `<link rel="stylesheet|modulepreload" href="...">` are tracked as asset references, same as in plain HTML files. This is needed for SSR frameworks like Hono where layout components emit HTML via JSX.
|
||||
|
||||
```tsx
|
||||
// src/layout.tsx
|
||||
|
||||
export const Layout = () => (
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="/static/style.css" />
|
||||
<script src="/static/app.js"></script>
|
||||
</head>
|
||||
<body><h1>Hello</h1></body>
|
||||
</html>
|
||||
);
|
||||
```
|
||||
|
||||
Fallow marks `static/style.css` and `static/app.js` as reachable. Root-relative paths (starting with `/`) resolve from the source file's parent directory first, then the project root, matching how Vite/Parcel/Hono serve static assets. Only `StringLiteral` attribute values are captured: expression containers (`href={someVar}`) and capitalized React-style components (`<Script>`, `<Link>`) are intentionally ignored because they have component-specific semantics.
|
||||
|
||||
---
|
||||
|
||||
## GraphQL `#import` Documents Are Tracked
|
||||
|
||||
GraphQL `.graphql` and `.gql` files can keep nearby fragment documents reachable with relative `#import` comments. Fallow tracks `./` and `../` specifiers, including extensionless imports that resolve through `.graphql` and `.gql`; package-style specifiers are ignored.
|
||||
|
||||
```graphql
|
||||
# src/query.graphql
|
||||
|
||||
#import "./fragments/user-fields"
|
||||
|
||||
query CurrentUser {
|
||||
currentUser {
|
||||
...UserFields
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Fallow marks `src/fragments/user-fields.graphql` or `src/fragments/user-fields.gql` as reachable when either file exists. A typo in the relative path is reported as an unresolved import instead of silently dropping the edge.
|
||||
|
||||
---
|
||||
|
||||
## Library Packages: Use `publicPackages` Instead of Visibility Tags
|
||||
|
||||
In monorepos, shared library packages have exported APIs consumed by external consumers not visible to fallow. Instead of annotating every export with `/** @public */` (or `@internal`, `@beta`, `@alpha`), use the `publicPackages` config to mark entire workspace packages as public libraries. Exports and exported enum/class members from these packages are excluded from unused API detection.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"publicPackages": ["@myorg/shared-lib", "@myorg/ui-kit"]
|
||||
}
|
||||
```
|
||||
|
||||
This is the correct solution for library false positives in monorepos. Only use JSDoc visibility tags (`/** @public */`, `/** @internal */`, etc.) for individual exports in application packages.
|
||||
|
||||
---
|
||||
|
||||
## Dynamically Loaded Files: Use `dynamicallyLoaded`
|
||||
|
||||
Files loaded at runtime via plugin systems, locale directories, or lazy module patterns are not statically reachable from entry points. Use `dynamicallyLoaded` to mark these files as always-used.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"dynamicallyLoaded": ["plugins/**/*.ts", "locales/**/*.json"]
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
# WRONG: suppressing individual files
|
||||
# fallow-ignore-file unused-file (in each plugin file)
|
||||
|
||||
# CORRECT: declare the pattern in config
|
||||
# { "dynamicallyLoaded": ["plugins/**/*.ts"] }
|
||||
```
|
||||
|
||||
This is preferable to adding inline suppression comments to every dynamically loaded file.
|
||||
|
||||
---
|
||||
|
||||
## Class Instance Members Are Tracked
|
||||
|
||||
Fallow tracks class member usage through instance variables. If you instantiate a class and call methods on the instance, those members are correctly marked as used.
|
||||
|
||||
```typescript
|
||||
class MyService {
|
||||
greet() { return 'hello'; } // NOT flagged: used via instance
|
||||
unused() { return 'bye'; } // Flagged: never called
|
||||
}
|
||||
|
||||
const svc = new MyService();
|
||||
svc.greet();
|
||||
```
|
||||
|
||||
This also handles whole-object instance patterns (`Object.values(svc)`, `{ ...svc }`, `for..in`) conservatively (all members marked as used). The tracking is scope-unaware, so same-named variables in different scopes may produce false negatives (not false positives).
|
||||
|
||||
---
|
||||
|
||||
## Type-Only Dependencies Should Be devDependencies
|
||||
|
||||
In `--production` mode, fallow detects production dependencies that are only imported via `import type`. Since TypeScript types are erased at runtime, these packages should be in `devDependencies` instead.
|
||||
|
||||
```typescript
|
||||
// If "zod" is in dependencies (not devDependencies):
|
||||
import type { ZodSchema } from 'zod'; // Flagged as type-only dependency
|
||||
|
||||
// This is a real import, not type-only:
|
||||
import { z } from 'zod'; // NOT flagged
|
||||
```
|
||||
|
||||
```bash
|
||||
# Detect type-only dependencies (reported automatically with --production)
|
||||
fallow dead-code --format json --quiet --production
|
||||
|
||||
# Suppress for a specific dependency
|
||||
# fallow-ignore-next-line type-only-dependency
|
||||
```
|
||||
|
||||
The `type-only-dependencies` rule defaults to `warn`. Suppress with `"type-only-dependencies": "off"` in your rules config if you intentionally keep type-only packages in production dependencies.
|
||||
|
||||
---
|
||||
|
||||
## Test-Only Dependencies Should Be devDependencies
|
||||
|
||||
Fallow detects production dependencies that are only imported from test files (`*.test.*`, `*.spec.*`, `__tests__/**`). Since these packages are never used in production code, they should be in `devDependencies` instead.
|
||||
|
||||
```typescript
|
||||
// If "msw" is in dependencies (not devDependencies):
|
||||
// src/handlers.test.ts
|
||||
import { setupServer } from 'msw/node'; // Flagged as test-only dependency
|
||||
|
||||
// src/app.ts — no imports of "msw" here
|
||||
```
|
||||
|
||||
```bash
|
||||
# Detect test-only dependencies (reported automatically)
|
||||
fallow dead-code --format json --quiet
|
||||
|
||||
# Suppress for a specific dependency
|
||||
# fallow-ignore-next-line test-only-dependency
|
||||
```
|
||||
|
||||
The `test-only-dependencies` rule defaults to `warn`. Suppress with `"test-only-dependencies": "off"` in your rules config if you intentionally keep test-only packages in production dependencies.
|
||||
|
||||
---
|
||||
|
||||
## GitLab CI: `FALLOW_COMMENT` vs `FALLOW_REVIEW`
|
||||
|
||||
These are separate features and can be used independently or together:
|
||||
|
||||
- **`FALLOW_COMMENT: "true"`** — posts a single summary comment on the MR with issue counts and a findings table
|
||||
- **`FALLOW_REVIEW: "true"`** — posts inline code review comments on the exact MR diff lines where issues were found
|
||||
|
||||
```yaml
|
||||
# WRONG: expecting inline review comments from FALLOW_COMMENT
|
||||
variables:
|
||||
FALLOW_COMMENT: "true"
|
||||
# This only posts a summary comment, not inline annotations
|
||||
|
||||
# CORRECT: use FALLOW_REVIEW for inline diff comments
|
||||
variables:
|
||||
FALLOW_REVIEW: "true"
|
||||
|
||||
# CORRECT: use both for summary + inline
|
||||
variables:
|
||||
FALLOW_COMMENT: "true"
|
||||
FALLOW_REVIEW: "true"
|
||||
```
|
||||
|
||||
Both require a `GITLAB_TOKEN` CI/CD variable (project access token with `api` scope). `CI_JOB_TOKEN` is read-only for MR notes in the official GitLab API, so it is not enough for summary comments or inline discussions.
|
||||
|
||||
---
|
||||
|
||||
## License Errors Include a Machine-Readable Code Suffix
|
||||
|
||||
`fallow license refresh` and `fallow license activate --trial` can fail with a backend error. The CLI always appends the raw HTTP status and the backend error code after the human hint, so scripts can grep for the code without parsing prose:
|
||||
|
||||
```
|
||||
fallow license refresh: your stored license is too stale to refresh. Reactivate with: fallow license activate --trial --email <addr> (HTTP 401, code token_stale)
|
||||
```
|
||||
|
||||
Stable codes the CLI surfaces today:
|
||||
|
||||
| Code | Operation | Meaning |
|
||||
|------|-----------|---------|
|
||||
| `token_stale` | `refresh` | Stored JWT is more than 45 days past its `exp`. Reactivate. |
|
||||
| `invalid_token` | `refresh` | Stored JWT is missing required claims (e.g. `sub`). Reactivate. |
|
||||
| `unauthorized` | `refresh` or `trial` | Auth failed. Reactivate. |
|
||||
| `rate_limit_exceeded` | `trial` | Trial endpoint is capped at 5 per hour per IP. Wait or use a different network. |
|
||||
|
||||
To detect a rate-limited trial signup in CI:
|
||||
|
||||
```bash
|
||||
if fallow license activate --trial --email "$EMAIL" 2>&1 | grep -q "code rate_limit_exceeded"; then
|
||||
echo "Trial rate-limited; fallback to cached FALLOW_LICENSE" >&2
|
||||
fi
|
||||
```
|
||||
|
||||
Unknown codes fall back to the backend `message` field when present, otherwise the raw body, so existing scripts that match on HTTP status alone still work.
|
||||
|
||||
---
|
||||
|
||||
## GitLab CI: Auto `--changed-since` in MR Pipelines
|
||||
|
||||
The official GitLab CI template automatically sets `--changed-since origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME` in merge request pipelines. You do not need to set `FALLOW_CHANGED_SINCE` manually unless you want a different ref.
|
||||
|
||||
```yaml
|
||||
# UNNECESSARY: changed-since is auto-detected in MR pipelines
|
||||
variables:
|
||||
FALLOW_CHANGED_SINCE: "origin/main"
|
||||
|
||||
# CORRECT: let the template auto-detect
|
||||
# (no FALLOW_CHANGED_SINCE needed — it reads the MR target branch)
|
||||
```
|
||||
|
||||
Override `FALLOW_CHANGED_SINCE` only when you need a specific ref (e.g., a release branch) or want to disable auto-detection by setting it to an empty string.
|
||||
|
||||
---
|
||||
|
||||
## GitLab CI: Package Manager Detection
|
||||
|
||||
The GitLab CI template auto-detects the project's package manager from lockfiles (`package-lock.json` for npm, `pnpm-lock.yaml` for pnpm, `yarn.lock` for yarn). MR comments and review comments use the correct commands for the detected manager.
|
||||
|
||||
This means review comments will show `pnpm remove lodash` instead of `npm uninstall lodash` in a pnpm project. No configuration is needed — detection is automatic.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Node.js Bindings
|
||||
|
||||
When embedding fallow inside a Node.js process (editor extensions, long-running servers, custom tooling), prefer the NAPI bindings over spawning the CLI. Same analysis engine, same JSON envelopes, no subprocess or JSON parsing overhead.
|
||||
|
||||
```bash
|
||||
npm install @fallow-cli/fallow-node
|
||||
```
|
||||
|
||||
```ts
|
||||
import { detectDeadCode, detectDuplication, computeHealth } from '@fallow-cli/fallow-node';
|
||||
|
||||
const deadCode = await detectDeadCode({ root: process.cwd(), explain: true });
|
||||
const dupes = await detectDuplication({ root: process.cwd(), mode: 'mild', minTokens: 30 });
|
||||
const health = await computeHealth({ root: process.cwd(), score: true, ownershipEmails: 'handle' });
|
||||
```
|
||||
|
||||
Six async functions: `detectDeadCode`, `detectCircularDependencies`, `detectBoundaryViolations`, `detectDuplication`, `computeComplexity`, `computeHealth`. Each returns the same JSON envelope the CLI emits for `--format json`. Rejected promises throw a `FallowNodeError` with `message`, `exitCode`, and optional `code`, `help`, `context` fields that mirror the CLI's structured error surface.
|
||||
|
||||
Enum-like fields take lowercase CLI-style literals (`"mild"`, `"cyclomatic"`, `"handle"`, `"low"`). Write-path commands (`fix`, `init`, `hooks install`, `hooks uninstall`, `license activate`, `coverage setup`) are not exposed; use the CLI for those.
|
||||
|
||||
See <https://docs.fallow.tools/integrations/node-bindings> for the full field reference.
|
||||
@@ -0,0 +1,804 @@
|
||||
# Fallow: Common Workflow Patterns & Recipes
|
||||
|
||||
Step-by-step workflows for common fallow usage scenarios.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Full Project Audit](#full-project-audit)
|
||||
- [PR Dead Code Check](#pr-dead-code-check)
|
||||
- [CI Pipeline Setup](#ci-pipeline-setup)
|
||||
- [Incremental Adoption with Baselines](#incremental-adoption-with-baselines)
|
||||
- [Monorepo Analysis](#monorepo-analysis)
|
||||
- [Duplication Threshold CI Gate](#duplication-threshold-ci-gate)
|
||||
- [Migration from knip](#migration-from-knip)
|
||||
- [Migration from jscpd](#migration-from-jscpd)
|
||||
- [Safe Auto-Fix Workflow](#safe-auto-fix-workflow)
|
||||
- [Production vs Full Audit](#production-vs-full-audit)
|
||||
- [Debugging False Positives](#debugging-false-positives)
|
||||
- [Combined Dead Code + Duplication](#combined-dead-code--duplication)
|
||||
- [Custom Plugin Setup](#custom-plugin-setup)
|
||||
- [GitHub Code Scanning Integration](#github-code-scanning-integration)
|
||||
- [Guard `git push` with a Claude Code PreToolUse hook](#guard-git-push-with-a-claude-code-pretooluse-hook)
|
||||
|
||||
---
|
||||
|
||||
## Full Project Audit
|
||||
|
||||
Complete codebase hygiene audit.
|
||||
|
||||
### Step 1: Run full analysis
|
||||
|
||||
```bash
|
||||
fallow dead-code --format json --quiet
|
||||
```
|
||||
|
||||
### Step 2: Review issue counts
|
||||
|
||||
Parse `total_issues` and individual arrays (`unused_files`, `unused_exports`, etc.) to understand the scope.
|
||||
|
||||
### Step 3: Find duplication
|
||||
|
||||
```bash
|
||||
fallow dupes --format json --quiet
|
||||
```
|
||||
|
||||
### Step 4: Preview auto-fix
|
||||
|
||||
```bash
|
||||
fallow fix --dry-run --format json --quiet
|
||||
```
|
||||
|
||||
### Step 5: Apply fixes (after user confirmation)
|
||||
|
||||
```bash
|
||||
fallow fix --yes --format json --quiet
|
||||
```
|
||||
|
||||
### Step 6: Verify
|
||||
|
||||
```bash
|
||||
fallow dead-code --format json --quiet
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PR Dead Code Check
|
||||
|
||||
Check if a pull request introduces new dead code.
|
||||
|
||||
### Step 1: Analyze changed files
|
||||
|
||||
```bash
|
||||
fallow dead-code --format json --quiet --changed-since main --fail-on-issues
|
||||
```
|
||||
|
||||
Exit code 1 if the PR introduces new dead code. Exit code 0 if clean.
|
||||
|
||||
### Step 2: If issues found, show specifics
|
||||
|
||||
```bash
|
||||
fallow dead-code --format json --quiet --changed-since main
|
||||
```
|
||||
|
||||
Parse the JSON to list specific files and exports that became unused.
|
||||
|
||||
---
|
||||
|
||||
## CI Pipeline Setup
|
||||
|
||||
### GitHub Actions: Basic
|
||||
|
||||
```yaml
|
||||
- name: Dead code check
|
||||
run: npx fallow dead-code --fail-on-issues --quiet
|
||||
```
|
||||
|
||||
### GitHub Actions: With SARIF Upload
|
||||
|
||||
```yaml
|
||||
- name: Fallow analysis
|
||||
run: npx fallow dead-code --ci > fallow.sarif
|
||||
continue-on-error: true # --ci sets --fail-on-issues; continue to upload SARIF even if issues found
|
||||
|
||||
- name: Upload SARIF
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: fallow.sarif
|
||||
```
|
||||
|
||||
### GitHub Actions: Using the Official Action
|
||||
|
||||
```yaml
|
||||
- uses: fallow-rs/fallow@v2
|
||||
with:
|
||||
command: dead-code
|
||||
fail-on-issues: true
|
||||
changed-since: main
|
||||
```
|
||||
|
||||
### GitHub Actions: Security Delta Gate
|
||||
|
||||
Fail a PR only when it introduces new security candidates (or makes existing ones newly reachable). Gated failures exit with code 8; the `issues` output counts only matching gate candidates. PR comment and review renderers skip security envelopes.
|
||||
|
||||
```yaml
|
||||
- uses: fallow-rs/fallow@v2
|
||||
with:
|
||||
command: security
|
||||
security-gate: new # or newly-reachable (needs a base ref via changed-since or PR auto-scoping)
|
||||
```
|
||||
|
||||
GitLab equivalent: `FALLOW_COMMAND: "security"` with `FALLOW_SECURITY_GATE: "new"`.
|
||||
|
||||
### GitHub Actions: With Health Score
|
||||
|
||||
```yaml
|
||||
- uses: fallow-rs/fallow@v2
|
||||
with:
|
||||
score: true
|
||||
changed-since: main
|
||||
```
|
||||
|
||||
Computes a health score (0-100 with letter grade) in combined mode and enables the health delta header in PR comments.
|
||||
|
||||
### GitHub Actions: Severity-Aware PR Quality Gate (Audit)
|
||||
|
||||
```yaml
|
||||
- uses: fallow-rs/fallow@v2
|
||||
with:
|
||||
command: audit
|
||||
gate: new-only # default; fails only on findings introduced by this PR
|
||||
fail-on-issues: true
|
||||
```
|
||||
|
||||
Runs `fallow audit` to combine dead-code + complexity + duplication scoped to changed files. The gate respects rule severity from `.fallowrc.json`, so `unused-exports: warn` projects do not fail when a PR touches a file with pre-existing warn-tier findings. Use `gate: all` to fail on every finding in changed files.
|
||||
|
||||
The action exposes `outputs.verdict` (`pass`/`warn`/`fail`) and `outputs.gate` for downstream conditionals; `outputs.issues` holds the introduced count under `gate: new-only` and the total count under `gate: all`.
|
||||
|
||||
```yaml
|
||||
- uses: fallow-rs/fallow@v2
|
||||
id: fallow
|
||||
with:
|
||||
command: audit
|
||||
|
||||
- name: Block release on regression
|
||||
if: steps.fallow.outputs.verdict == 'fail'
|
||||
run: exit 1
|
||||
```
|
||||
|
||||
Three additional outputs surface silent failures in the action's PR comment / review steps. `outputs.changed-files-unavailable` (`true`/`false`, default `false`) signals that the analyze step could not enumerate PR-changed files (transient GitHub API failure, expired token, missing permissions), so analysis ran against the full codebase. `outputs.post-skipped-reason` (`none`/`pagination_failure`) signals the Post review comments step aborted to avoid duplicate threads. `outputs.dedup-lookup-failed` (`true`/`false`) signals a dedup lookup failed on either the Post PR comment or Post review comments step. All three are always emitted regardless of which failure path was taken, so downstream `if:` gates can match positively without absent-vs-false ambiguity. Gate on these to detect degraded posting state and re-run the action.
|
||||
|
||||
### GitHub Actions: Inline PR Annotations (No Advanced Security)
|
||||
|
||||
The official action supports inline PR annotations via GitHub workflow commands. This does not require Advanced Security (unlike SARIF upload) and works on any GitHub plan.
|
||||
|
||||
```yaml
|
||||
- uses: fallow-rs/fallow@v2
|
||||
with:
|
||||
command: dead-code
|
||||
changed-since: main
|
||||
annotations: true
|
||||
max-annotations: 50 # default: 50, limits annotation count
|
||||
```
|
||||
|
||||
Annotations appear as inline warnings on the PR diff. They work with all commands (`dead-code`, `dupes`, `health`, and the default combined mode). The `max-annotations` input prevents annotation flooding on large projects.
|
||||
|
||||
### GitHub Actions: PR-Scoped Check
|
||||
|
||||
```yaml
|
||||
- name: Check for new dead code
|
||||
run: npx fallow dead-code --format json --quiet --changed-since ${{ github.event.pull_request.base.sha }} --fail-on-issues
|
||||
```
|
||||
|
||||
### GitHub Actions: Duplication Gate
|
||||
|
||||
```yaml
|
||||
- name: Duplication check
|
||||
run: npx fallow dupes --format json --quiet --threshold 5 --mode mild
|
||||
```
|
||||
|
||||
Fails if overall duplication exceeds 5%.
|
||||
|
||||
### GitHub Actions: PR-Scoped Duplication Check
|
||||
|
||||
```yaml
|
||||
- name: Check duplication in changed files
|
||||
run: npx fallow dupes --format json --quiet --changed-since ${{ github.event.pull_request.base.sha }}
|
||||
```
|
||||
|
||||
Only reports duplication in files modified by the PR.
|
||||
|
||||
### GitLab CI: Using the Official Template
|
||||
|
||||
```yaml
|
||||
include:
|
||||
- remote: 'https://raw.githubusercontent.com/fallow-rs/fallow/main/ci/gitlab-ci.yml'
|
||||
|
||||
fallow:
|
||||
extends: .fallow
|
||||
variables:
|
||||
FALLOW_COMMAND: "dead-code"
|
||||
FALLOW_FAIL_ON_ISSUES: "true"
|
||||
```
|
||||
|
||||
Generates Code Quality reports (inline MR annotations) automatically. In MR pipelines, `--changed-since` is automatically set to the target branch — no manual configuration needed.
|
||||
|
||||
If runners cannot reach `raw.githubusercontent.com`, run `fallow ci-template gitlab --vendor`, commit the generated `ci/` and `action/` files, and use GitLab's local include syntax:
|
||||
|
||||
```yaml
|
||||
include:
|
||||
- local: 'ci/gitlab-ci.yml'
|
||||
```
|
||||
|
||||
### GitLab CI: With MR Summary Comments
|
||||
|
||||
```yaml
|
||||
include:
|
||||
- remote: 'https://raw.githubusercontent.com/fallow-rs/fallow/main/ci/gitlab-ci.yml'
|
||||
|
||||
fallow:
|
||||
extends: .fallow
|
||||
variables:
|
||||
FALLOW_COMMENT: "true"
|
||||
FALLOW_SUMMARY_SCOPE: "diff"
|
||||
```
|
||||
|
||||
Posts a summary comment on the MR with issue counts and findings. In MR pipelines, `--changed-since` is auto-detected from `$CI_MERGE_REQUEST_TARGET_BRANCH_NAME`, so only issues from changed files are reported. `FALLOW_SUMMARY_SCOPE: "diff"` also hides project-level dependency/catalog/override findings whose anchor line is outside the diff. Requires `GITLAB_TOKEN` CI/CD variable (project access token with `api` scope); `CI_JOB_TOKEN` is read-only for MR notes in the official GitLab API.
|
||||
|
||||
### GitLab CI: With Inline Code Review Comments
|
||||
|
||||
```yaml
|
||||
include:
|
||||
- remote: 'https://raw.githubusercontent.com/fallow-rs/fallow/main/ci/gitlab-ci.yml'
|
||||
|
||||
fallow:
|
||||
extends: .fallow
|
||||
variables:
|
||||
FALLOW_REVIEW: "true"
|
||||
FALLOW_REVIEW_GUIDANCE: "true"
|
||||
```
|
||||
|
||||
Posts inline review comments directly on the MR diff lines where issues were found. `FALLOW_REVIEW_GUIDANCE: "true"` adds collapsed "What to do" guidance blocks to each inline finding. This gives developers precise feedback without leaving the code review flow. Can be combined with `FALLOW_COMMENT: "true"` for both a summary and inline comments. Requires `GITLAB_TOKEN`.
|
||||
|
||||
### GitLab CI: Combined MR Comments + Review
|
||||
|
||||
```yaml
|
||||
include:
|
||||
- remote: 'https://raw.githubusercontent.com/fallow-rs/fallow/main/ci/gitlab-ci.yml'
|
||||
|
||||
fallow:
|
||||
extends: .fallow
|
||||
variables:
|
||||
FALLOW_COMMENT: "true"
|
||||
FALLOW_SUMMARY_SCOPE: "diff"
|
||||
FALLOW_REVIEW: "true"
|
||||
FALLOW_REVIEW_GUIDANCE: "true"
|
||||
FALLOW_FAIL_ON_ISSUES: "true"
|
||||
```
|
||||
|
||||
Posts both a summary comment and inline review comments on the MR. `FALLOW_SUMMARY_SCOPE: "diff"` only affects the sticky summary; inline review comments remain anchored to diff lines. The template auto-detects the package manager (npm/pnpm/yarn) from lockfiles, so review comments show the correct commands for the project (e.g., `pnpm remove` instead of `npm uninstall`).
|
||||
|
||||
### GitLab CI: With Health Score and Trend
|
||||
|
||||
```yaml
|
||||
include:
|
||||
- remote: 'https://raw.githubusercontent.com/fallow-rs/fallow/main/ci/gitlab-ci.yml'
|
||||
|
||||
fallow:
|
||||
extends: .fallow
|
||||
variables:
|
||||
FALLOW_SCORE: "true"
|
||||
FALLOW_TREND: "true"
|
||||
FALLOW_COMMENT: "true"
|
||||
```
|
||||
|
||||
Computes the health score and compares against saved snapshots. The MR comment includes a health delta header showing score changes. `FALLOW_TREND` implies `FALLOW_SCORE`.
|
||||
|
||||
### GitLab CI: Manual (Without Template)
|
||||
|
||||
```yaml
|
||||
fallow:
|
||||
image: node:20-slim
|
||||
script:
|
||||
- npx fallow dead-code --fail-on-issues --quiet --format json > fallow-results.json
|
||||
artifacts:
|
||||
paths:
|
||||
- fallow-results.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Incremental Adoption with Baselines
|
||||
|
||||
For large projects with existing dead code. Adopt gradually without fixing everything at once.
|
||||
|
||||
### Step 1: Save current state as baseline
|
||||
|
||||
```bash
|
||||
fallow dead-code --format json --quiet --save-baseline fallow-baselines/dead-code.json
|
||||
```
|
||||
|
||||
### Step 2: Commit the baseline
|
||||
|
||||
```bash
|
||||
git add fallow-baselines/dead-code.json
|
||||
git commit -m "chore: add fallow baseline"
|
||||
```
|
||||
|
||||
### Step 3: CI only fails on NEW issues
|
||||
|
||||
```bash
|
||||
fallow dead-code --format json --quiet --baseline fallow-baselines/dead-code.json --fail-on-issues
|
||||
```
|
||||
|
||||
### Step 4: Gradually fix and update baseline
|
||||
|
||||
As you fix existing issues, regenerate the baseline:
|
||||
|
||||
```bash
|
||||
fallow dead-code --format json --quiet --save-baseline fallow-baselines/dead-code.json
|
||||
```
|
||||
|
||||
### Duplication baseline
|
||||
|
||||
Same pattern works for duplication:
|
||||
|
||||
```bash
|
||||
fallow dupes --format json --quiet --save-baseline fallow-baselines/dupes.json
|
||||
fallow dupes --format json --quiet --baseline fallow-baselines/dupes.json --threshold 5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Monorepo Analysis
|
||||
|
||||
### Analyze the full monorepo
|
||||
|
||||
```bash
|
||||
fallow dead-code --format json --quiet
|
||||
```
|
||||
|
||||
Fallow auto-detects workspaces from `package.json` workspaces or `pnpm-workspace.yaml`.
|
||||
|
||||
### Analyze a single package
|
||||
|
||||
```bash
|
||||
fallow dead-code --format json --quiet --workspace my-package
|
||||
```
|
||||
|
||||
Full cross-workspace graph is built (so imports between packages are resolved), but only issues in `my-package` are reported.
|
||||
|
||||
### Per-package CI
|
||||
|
||||
Run analysis for each workspace package separately:
|
||||
|
||||
```bash
|
||||
fallow dead-code --format json --quiet --workspace package-a --fail-on-issues
|
||||
fallow dead-code --format json --quiet --workspace package-b --fail-on-issues
|
||||
```
|
||||
|
||||
### List all discovered files across workspaces
|
||||
|
||||
```bash
|
||||
fallow list --files --format json --quiet
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Duplication Threshold CI Gate
|
||||
|
||||
Enforce a maximum duplication percentage.
|
||||
|
||||
### Step 1: Measure current duplication
|
||||
|
||||
```bash
|
||||
fallow dupes --format json --quiet
|
||||
```
|
||||
|
||||
Check `duplication_percentage` in the JSON output.
|
||||
|
||||
### Step 2: Set threshold slightly above current
|
||||
|
||||
If current duplication is 3.8%, set threshold to 5%:
|
||||
|
||||
```bash
|
||||
fallow dupes --format json --quiet --threshold 5
|
||||
```
|
||||
|
||||
Exits with code 1 if duplication exceeds 5%.
|
||||
|
||||
### Step 3: Tighten over time
|
||||
|
||||
As you reduce duplication, lower the threshold.
|
||||
|
||||
### Cross-directory only
|
||||
|
||||
To ignore duplication within the same directory (local helpers, similar test files):
|
||||
|
||||
```bash
|
||||
fallow dupes --format json --quiet --threshold 5 --skip-local
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration from knip
|
||||
|
||||
### Step 1: Preview migration
|
||||
|
||||
```bash
|
||||
fallow migrate --dry-run
|
||||
```
|
||||
|
||||
Shows what config would be generated. Auto-detects `knip.json`, `.knip.json`, `knip.jsonc`, `.knip.jsonc`, or `package.json#knip`.
|
||||
|
||||
### Step 2: Apply migration
|
||||
|
||||
```bash
|
||||
fallow migrate
|
||||
```
|
||||
|
||||
Creates `.fallowrc.json` with mapped settings:
|
||||
- knip `rules`/`exclude`/`include` → fallow `rules` (error/warn/off)
|
||||
- knip `ignore` → fallow `ignorePatterns`
|
||||
- knip `ignoreDependencies` → fallow `ignoreDependencies`
|
||||
- knip `ignoreExportsUsedInFile` → fallow `ignoreExportsUsedInFile` (boolean and `{ type, interface }` object form both supported; fallow groups type aliases and interfaces under one issue, so the two type-kind fields behave identically)
|
||||
- Unmappable fields generate warnings with suggestions
|
||||
|
||||
### Step 3: Compare results
|
||||
|
||||
```bash
|
||||
# Run fallow
|
||||
fallow dead-code --format json --quiet
|
||||
|
||||
# Compare with knip output
|
||||
npx knip --reporter json
|
||||
```
|
||||
|
||||
### Step 4: Remove knip config
|
||||
|
||||
Once satisfied, remove the old `knip.json` and uninstall knip.
|
||||
|
||||
---
|
||||
|
||||
## Migration from jscpd
|
||||
|
||||
### Step 1: Preview migration
|
||||
|
||||
```bash
|
||||
fallow migrate --dry-run
|
||||
```
|
||||
|
||||
Auto-detects `.jscpd.json` or `package.json#jscpd`.
|
||||
|
||||
### Step 2: Apply migration
|
||||
|
||||
```bash
|
||||
fallow migrate
|
||||
```
|
||||
|
||||
Maps jscpd settings:
|
||||
- `minTokens` → `duplicates.minTokens`
|
||||
- `minLines` → `duplicates.minLines`
|
||||
- `threshold` → `duplicates.threshold`
|
||||
- `mode` → `duplicates.mode`
|
||||
|
||||
### Step 3: Compare results
|
||||
|
||||
```bash
|
||||
fallow dupes --format json --quiet
|
||||
```
|
||||
|
||||
### Detection mode mapping
|
||||
|
||||
| jscpd | fallow |
|
||||
|-------|--------|
|
||||
| Default (exact tokens) | `strict` |
|
||||
| — | `mild` (fallow default, syntax normalized) |
|
||||
| — | `weak` (literal normalization) |
|
||||
| — | `semantic` (variable rename detection) |
|
||||
|
||||
---
|
||||
|
||||
## Safe Auto-Fix Workflow
|
||||
|
||||
### Step 1: Dry-run first
|
||||
|
||||
```bash
|
||||
fallow fix --dry-run --format json --quiet
|
||||
```
|
||||
|
||||
### Step 2: Review each proposed change
|
||||
|
||||
Parse the JSON `changes` array. Each entry shows:
|
||||
- `path`: file to be modified
|
||||
- `action`: what will happen (`remove_export`, `remove_dependency`)
|
||||
- `name`: the symbol or dependency being removed
|
||||
- `line`: the line number
|
||||
|
||||
### Step 3: Confirm with user before applying
|
||||
|
||||
Show the proposed changes. Wait for user confirmation.
|
||||
|
||||
### Step 4: Apply
|
||||
|
||||
```bash
|
||||
fallow fix --yes --format json --quiet
|
||||
```
|
||||
|
||||
### Step 5: Verify
|
||||
|
||||
```bash
|
||||
fallow dead-code --format json --quiet
|
||||
```
|
||||
|
||||
### Step 6: Run project tests
|
||||
|
||||
After auto-fix, always run the project's test suite to verify nothing broke.
|
||||
|
||||
---
|
||||
|
||||
## Production vs Full Audit
|
||||
|
||||
### Full audit (default)
|
||||
|
||||
```bash
|
||||
fallow dead-code --format json --quiet
|
||||
```
|
||||
|
||||
Includes all files, all scripts, all dependencies (including devDependencies).
|
||||
|
||||
### Production audit
|
||||
|
||||
```bash
|
||||
fallow dead-code --format json --quiet --production
|
||||
```
|
||||
|
||||
Differences:
|
||||
- Excludes: `*.test.*`, `*.spec.*`, `*.stories.*`, `__tests__/**`, `__mocks__/**`
|
||||
- Only analyzes: `start`, `build`, `serve`, `preview`, `prepare` scripts
|
||||
- Skips: unused devDependency detection
|
||||
- Adds: type-only production dependency detection
|
||||
|
||||
Use production mode for:
|
||||
- Checking what ships to users
|
||||
- Finding dependencies that should be devDependencies
|
||||
- CI pipelines focused on production bundle
|
||||
|
||||
Use full mode for:
|
||||
- Complete codebase hygiene
|
||||
- Finding unused test utilities
|
||||
- Auditing devDependency usage
|
||||
|
||||
---
|
||||
|
||||
## Debugging False Positives
|
||||
|
||||
### Trace an export's usage chain
|
||||
|
||||
```bash
|
||||
fallow dead-code --format json --quiet --trace src/utils.ts:myFunction
|
||||
```
|
||||
|
||||
Shows where `myFunction` is imported (or not imported) and why it's flagged.
|
||||
|
||||
### Trace all edges for a file
|
||||
|
||||
```bash
|
||||
fallow dead-code --format json --quiet --trace-file src/utils.ts
|
||||
```
|
||||
|
||||
Shows all imports/exports for the file and their resolution status.
|
||||
|
||||
### Trace a dependency
|
||||
|
||||
```bash
|
||||
fallow dead-code --format json --quiet --trace-dependency lodash
|
||||
```
|
||||
|
||||
Shows all files that import lodash.
|
||||
|
||||
### If the trace shows it IS used
|
||||
|
||||
The export might be consumed through a pattern fallow can't resolve (fully dynamic import, reflection). Add a suppression:
|
||||
|
||||
```typescript
|
||||
// fallow-ignore-next-line unused-export
|
||||
export const dynamicallyUsed = createHandler();
|
||||
```
|
||||
|
||||
### If the trace shows it's NOT used
|
||||
|
||||
The export is genuinely unused. Consider removing it or marking it as intentionally kept:
|
||||
|
||||
```typescript
|
||||
// fallow-ignore-next-line unused-export
|
||||
export const publicApi = createWidget(); // Used by external consumers
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Combined Dead Code + Duplication
|
||||
|
||||
Cross-reference dead code with duplication findings to find high-priority cleanup targets.
|
||||
|
||||
### Step 1: Run combined analysis
|
||||
|
||||
```bash
|
||||
fallow dead-code --format json --quiet --include-dupes
|
||||
```
|
||||
|
||||
This adds duplication context to dead code findings, identifying clone instances that exist in unused files or overlap with unused exports.
|
||||
|
||||
### Step 2: Prioritize cleanup
|
||||
|
||||
Focus on findings that are BOTH dead code and duplicated:
|
||||
- Unused files containing duplicate code → delete the file entirely
|
||||
- Unused exports that are clones of other exports → remove the duplicate
|
||||
|
||||
---
|
||||
|
||||
## Custom Plugin Setup
|
||||
|
||||
For frameworks not covered by the 122 built-in plugins.
|
||||
|
||||
### Option 1: Inline framework config
|
||||
|
||||
```jsonc
|
||||
// .fallowrc.json
|
||||
{
|
||||
"framework": [
|
||||
{
|
||||
"name": "my-framework",
|
||||
"enablers": ["my-framework"],
|
||||
"entryPoints": ["src/routes/**/*.ts", "src/middleware/**/*.ts"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Option 2: External plugin file
|
||||
|
||||
Create `.fallow/plugins/my-framework.jsonc`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "my-framework",
|
||||
"detection": { "dependency": "my-framework" },
|
||||
"entryPoints": ["src/routes/**/*.ts"],
|
||||
"alwaysUsedFiles": ["src/bootstrap.ts"],
|
||||
"usedExports": {
|
||||
"src/config.ts": ["default"]
|
||||
},
|
||||
"toolingDependencies": ["my-framework-cli"]
|
||||
}
|
||||
```
|
||||
|
||||
### Option 3: Plugin directory
|
||||
|
||||
```jsonc
|
||||
// .fallowrc.json
|
||||
{
|
||||
"plugins": ["tools/plugins/"]
|
||||
}
|
||||
```
|
||||
|
||||
Place `.jsonc`, `.json`, or `.toml` plugin files in that directory.
|
||||
|
||||
---
|
||||
|
||||
## GitHub Code Scanning Integration
|
||||
|
||||
Upload fallow results to GitHub's Code Scanning dashboard.
|
||||
|
||||
### Step 1: Generate SARIF output
|
||||
|
||||
```bash
|
||||
fallow dead-code --format sarif --quiet > fallow.sarif
|
||||
```
|
||||
|
||||
### Step 2: Upload via GitHub Action
|
||||
|
||||
```yaml
|
||||
- name: Upload SARIF
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: fallow.sarif
|
||||
```
|
||||
|
||||
### All-in-one with `--ci`
|
||||
|
||||
```bash
|
||||
fallow dead-code --ci > fallow.sarif
|
||||
```
|
||||
|
||||
The `--ci` flag is equivalent to `--format sarif --fail-on-issues --quiet`. Note: `--fail-on-issues` means exit code 1 if issues exist, in CI scripts use `continue-on-error: true` or `|| true` to ensure the SARIF upload step still runs.
|
||||
|
||||
---
|
||||
|
||||
## Guard `git push` with a Claude Code PreToolUse hook
|
||||
|
||||
Use this when Claude Code is allowed to run Git commands in a repository that already uses fallow.
|
||||
|
||||
The pattern is a local agent gate, not a Git hook. Claude Code intercepts its own `Bash` tool calls before execution. When Claude tries `git commit` or `git push`, the hook runs:
|
||||
|
||||
```bash
|
||||
fallow audit --format json --quiet --explain --gate-marker agent
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
- `pass`: allow the command
|
||||
- `warn`: allow the command
|
||||
- `fail`: exit 2 and write the raw audit JSON to stderr
|
||||
- runtime error JSON like `{ "error": true, ... }`: fail open, do not block
|
||||
|
||||
Because Claude receives stderr as tool feedback on a blocked `PreToolUse` call, it can read the structured findings (including `_meta.docs` links and `actions`), fix the code, and retry the Git command.
|
||||
|
||||
Install it automatically:
|
||||
|
||||
```bash
|
||||
fallow hooks install --target agent
|
||||
```
|
||||
|
||||
Remove it later with:
|
||||
|
||||
```bash
|
||||
fallow hooks uninstall --target agent
|
||||
```
|
||||
|
||||
Manual files:
|
||||
|
||||
### `.claude/settings.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/fallow-gate.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `.claude/hooks/fallow-gate.sh`
|
||||
|
||||
Prefer `fallow hooks install --target agent` to install this file. The script is written and maintained by fallow itself; the canonical source is [`crates/cli/src/setup_hooks/fallow-gate.sh`](https://github.com/fallow-rs/fallow/blob/main/crates/cli/src/setup_hooks/fallow-gate.sh).
|
||||
|
||||
Behavior you can rely on:
|
||||
- Runs only when the intercepted command matches `git commit` or `git push`; otherwise exits 0.
|
||||
- Resolves `fallow` from PATH first, then `npx --no-install fallow` as a fallback. Skips with a stderr notice if neither is available or if `jq` is missing.
|
||||
- Enforces a version floor via `FALLOW_GATE_MIN_VERSION` (default `2.85.0`). Binaries below the floor are blocked with an upgrade hint. Set the env var to the empty string to disable the check.
|
||||
- Runs `fallow audit --format json --quiet --explain --gate-marker agent` and, on verdict=`fail`, writes the full JSON envelope to stderr preceded by `fallow-gate: blocked by fallow <version> at <binary>` so the responsible binary is always identifiable. The gate marker lets local Impact record blocked-then-cleared agent gate events when Impact is enabled.
|
||||
- On runtime error (`{"error": true, ...}`) or unexpected non-zero exit, fails open with a one-line stderr notice; warn verdicts pass through silently.
|
||||
|
||||
Codex fallback (add to repo root `AGENTS.md`):
|
||||
|
||||
```md
|
||||
Before any `git commit` or `git push`, run `fallow audit --format json --quiet --explain --gate-marker agent`. If the verdict is `fail`, fix the reported findings before retrying. Treat JSON runtime errors like `{ "error": true, ... }` as non-blocking.
|
||||
```
|
||||
|
||||
Keep `fallow audit` in CI alongside this local gate. The hook only runs for Claude Code, not for human pushes or other agents, so it is a reinforcement layer rather than a replacement for server-side enforcement.
|
||||
|
||||
### Remove the hook
|
||||
|
||||
```bash
|
||||
fallow hooks uninstall --target agent
|
||||
```
|
||||
|
||||
Removes the fallow-gate handler from `.claude/settings.json` (preserving any other handlers in the same matcher group), deletes `.claude/hooks/fallow-gate.sh` if it still carries the `# Generated by fallow setup-hooks.` marker, and strips the managed block from `AGENTS.md`. Idempotent: a second run reports `unchanged` / `not present` and exits 0.
|
||||
|
||||
Use `--force` to remove a hook script that the user has edited (the marker is no longer present). Use `--dry-run` to preview without touching files.
|
||||
|
||||
### Distinguish from `fallow hooks install --target git`
|
||||
|
||||
`fallow hooks install --target git` is a different target: it scaffolds a shell-level Git pre-commit hook under `.git/hooks/` that runs `fallow` on changed files. That is the *human* enforcement path. `fallow hooks install --target agent` is the *agent* enforcement path, targeting `.claude/` and `AGENTS.md`. Both can live in the same repo: git hooks catch human commits, the agent gate catches agent commits.
|
||||
Reference in New Issue
Block a user