Skip to content
Merged
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
6 changes: 6 additions & 0 deletions analysis/dead-code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ fallow dead-code --unused-exports --unused-types
fallow dead-code --unresolved-imports --unlisted-deps
```

## Optional props that callers never supply

[Absent component props](/explanations/dead-code#absent-component-props) are optional props that a component reads but no inspected reachable caller supplies. Enable `rules.absent-component-props` with `warn` or `error`. The rule starts off because a default or a stable API can make an omitted prop intentional.

Fallow reports candidates only when it knows at least one reachable caller and can account for the component's callers statically. Unknown spreads, forwarding, dynamic use, and external package users prevent that conclusion. A finding includes the declaration, inspected callers, and whether a default exists. Review that evidence before changing the component. Fallow does not remove the prop or its conditional UI automatically.

## Output formats

<Tabs>
Expand Down
5 changes: 5 additions & 0 deletions cli/dead-code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ fallow check # Hidden alias for fallow dead-code
| `--unprovided-injects` | Report only unprovided injects |
| `--unrendered-components` | Report only unrendered components |
| `--unused-component-props` | Report only unused component props |
| `--absent-component-props` | Opt in to [absent component props](/explanations/dead-code#absent-component-props) and report only that issue type |
| `--unused-component-emits` | Report only unused component emits |
| `--unused-component-inputs` | Report only unused component inputs |
| `--unused-component-outputs` | Report only unused component outputs |
Expand Down Expand Up @@ -402,6 +403,10 @@ The JSON output always includes a `summary` counts object, with a count for each

`deprecated_exports_in_use` works the same way: the array and the `summary.deprecated_exports_in_use` count are always in the JSON output. Fallow fills the array only when you enable the rule with `--deprecated-exports-in-use` or in `rules`. An `unused_exports` entry for an export with a `@deprecated` tag has `deprecated: true` and, when the tag has a message, `deprecated_reason`. Both keys are absent when the export is not deprecated.

`absent_component_props` contains optional props that their component reads but no inspected reachable caller supplies. Fallow omits this array when it is empty; `summary.absent_component_props` still reports its count. The rule is off by default. Enable it with `--absent-component-props`, or set `absent-component-props` to `warn` or `error` in `rules`. These are manual review candidates. See [Absent component props](/explanations/dead-code#absent-component-props) for supported syntax and exclusions.

Each entry has `component_name`, `prop_name`, and `framework` to identify the prop. `has_default` records whether the declaration has a default. `path`, `line`, and `col` point to the declaration. Each entry in `inspected_call_sites` has `path`, `line`, and `col` for a reachable caller that fallow inspected. Review those callers and the default before proposing a change.

Each finding has an optional `effective_severity` field (`error` or `warn`) with its rule severity, after `overrides[].rules`. The CI formats read it. See [Levels in CI formats](/configuration/rules#levels-in-ci-formats).

### `used_in_workspaces`
Expand Down
3 changes: 2 additions & 1 deletion configuration/rules.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Start with all rules as `warn`. Promote one type at a time to `error` when you h

When you do not configure a rule, it uses its default severity. The **Default** column in [Available rule names](#available-rule-names) shows the default of each rule. The defaults by group:

- Default `off` (opt-in): `private-type-leaks`, `deprecated-exports-in-use`, `prop-drilling`, `thin-wrapper`, `duplicate-prop-shape`, `coverage-gaps`, `feature-flags`, `require-suppression-reason`, `security-client-server-leak`, and `security-sink`.
- Default `off` (opt-in): `private-type-leaks`, `deprecated-exports-in-use`, `absent-component-props`, `prop-drilling`, `thin-wrapper`, `duplicate-prop-shape`, `coverage-gaps`, `feature-flags`, `require-suppression-reason`, `security-client-server-leak`, and `security-sink`.
- Default `warn`:
- Dependency hygiene: `unused-dev-dependencies`, `unused-optional-dependencies`, `type-only-dependencies`, `test-only-dependencies`, and `dev-dependencies-in-production`.
- Framework components: `unused-store-members`, `unprovided-injects`, `unrendered-components`, `unused-component-props`, `unused-component-emits`, `unused-component-inputs`, `unused-component-outputs`, `unused-svelte-events`, `unused-server-actions`, and `unused-load-data-keys`.
Expand Down Expand Up @@ -108,6 +108,7 @@ stale-suppressions = "warn"
| `unprovided-injects` | `warn` | `inject()` or `getContext()` reads a key that no `provide()` or `setContext()` supplies |
| `unrendered-components` | `warn` | A Vue or Svelte component that a barrel exports but that no code renders |
| `unused-component-props` | `warn` | A Vue, Svelte, or React component prop that its own component never references. See [`unusedComponentProps`](/configuration/overview#unusedcomponentprops) |
| `absent-component-props` | `off` | An optional prop that its component reads but no inspected reachable caller supplies. Requires known callers and a complete static caller set. Review defaults and API intent before changing it. See [Absent component props](/explanations/dead-code#absent-component-props) |
| `unused-component-emits` | `warn` | A Vue `<script setup>` `defineEmits` event that its own component never emits |
| `unused-component-inputs` | `warn` | An Angular `@Input()`, signal `input()`, or `model()` input that its own component never reads |
| `unused-component-outputs` | `warn` | An Angular `@Output()` or signal `output()` output that its own component never emits |
Expand Down
61 changes: 60 additions & 1 deletion explanations/dead-code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Use this page to decide which `fallow dead-code` findings to fix, in which order

## Issue types

Each dead-code issue type has a default severity. An `error` finding makes the run exit with code 1. A `warn` finding shows in the output, but does not fail the run unless you pass `--fail-on-issues`. An `off` rule does not run. To change a severity, use [`rules`](/configuration/rules).
Each dead-code issue type has a default severity. An `error` finding makes the run exit with code 1. A `warn` finding shows in the output, but does not fail the run unless you pass `--fail-on-issues` or `--ci`. An `off` rule does not run. To change a severity, use [`rules`](/configuration/rules).

### Unused files

Expand Down Expand Up @@ -627,6 +627,65 @@ For **Astro**, the same rule key reports a field of a `.astro` `interface Props`

**When to act:** Use the prop where it belongs, or remove it from `defineProps` (Vue) or from the destructure (React). There is no auto-fix. Suppress the finding with `// fallow-ignore-next-line unused-component-prop`, or set the rule to `off`.

### Absent component props {#absent-component-props}

An optional prop that its component reads, but no inspected reachable caller supplies. Fallow requires at least one known caller and a complete set of statically identified callers in the analyzed project. A component exported for external package users has unknown callers, so fallow skips it. An export used only inside an application can still qualify.

The rule is off by default. To enable it, set `absent-component-props` to `warn` or `error` in [rules](/configuration/rules):

```json .fallowrc.json
{
"rules": {
"absent-component-props": "warn"
}
}
```

With `warn`, these findings do not fail the run unless you pass `--fail-on-issues` or `--ci`. With `error`, they count as error-severity findings. A required prop omitted by a caller remains the responsibility of TypeScript or the framework's type checker. A prop that the component never reads belongs to [unused component props](#unused-component-props).

For example, an application component reads an optional `compact` prop:

```tsx Card.tsx
interface CardProps {
title: string;
compact?: boolean;
}

export const Card = ({ title, compact = false }: CardProps) => (
<article className={compact ? "card-compact" : "card"}>{title}</article>
);
```

If every known caller renders `<Card title="Overview" />`, `compact` can be a review candidate. The default may be intentional. Keep a stable API when callers are expected to opt in later.

Supplying `compact={false}` or `compact={undefined}` still counts as supplying the prop. Fallow can use a spread only when it can prove which keys the object supplies. An unknown spread can hide a supplied prop, so fallow skips absence findings for that component. Dynamic component selection, unsupported wrappers, and prop forwarding also prevent a complete static caller set.

#### Supported syntax

Fallow uses these declarations and caller forms when it can resolve the component and prove a complete static caller set. It does not infer optional props from imported or unsupported type shapes. A required typed prop remains required even when a destructure or `withDefaults` gives it a default.

| Framework | Declarations | Caller syntax | Cases that prevent a finding |
|:----------|:-------------|:--------------|:-----------------------------|
| React and Preact, including Next.js | JSX function and arrow components with same-file inline types, interfaces, or type aliases; untyped destructure defaults; recognized `memo` and `forwardRef` wrappers | JSX attributes and children, including module scope, imported aliases, namespace members, and proven object spreads | Unresolved prop types, unsupported wrappers, component values that escape, or unknown spreads |
| Solid | JSX function and arrow components with recognized Solid bindings and supported local prop contracts | JSX attributes, children, and proven object spreads | Unsupported wrappers, unresolved types, or opaque component use |
| Qwik | Recognized `component$` wrappers, including supported same-file generic prop types | JSX attributes, children, and proven object spreads | Unrecognized wrappers, unresolved types, or opaque component use |
| Vue and Nuxt | Typed or runtime `defineProps`, local named types, `withDefaults`, and supported Options API props | Static component tags, camel-case and kebab-case bindings, `v-model`, and literal or proven local constant `v-bind` | Dynamic or scoped component use, exposed or forwarded props, `defineModel`, rest props, or unresolved declarations |
| Svelte and SvelteKit | Typed or destructured Svelte 5 `$props`, untyped defaults, and supported legacy `export let` props | Static tags, shorthand attributes, `bind`, paired-tag children, and proven spreads | Dynamic components, scoped component bindings, `$bindable`, rest props, or forwarding |
| Astro | Same-file `Props` interfaces or type aliases, with `Astro.props` reads and destructure defaults | Static tags, explicit attributes, and proven spreads; an Astro caller can target another supported component framework | Dynamic tags, unresolved types, scoped component expressions, or opaque props use |
| Angular | Recognized `Component` or `Directive` classes with aliased `@Input` or signal `input` declarations | Static selectors and attribute, property, or two-way bindings in supported inline or external HTML templates | Inheritance, unsupported metadata or host directives, model behavior, dynamic outlets, or imperative component use |
| Lit | Direct recognized `LitElement` or `ReactiveElement` subclasses with supported property decorators and attribute aliases | Imported `html` templates, static tags, normalized HTML attributes, and case-sensitive property bindings | Unsupported inheritance or registration, imperative DOM use, injected HTML, dynamic static-HTML templates, or callers through unsupported framework syntax |
| Ember and Glimmer | Direct component subclasses with optional fields in a same-file typed `Signature.Args`; reads in script or the owning class template | Static Glimmer component tags and named `@arguments` | Classic registry lookup, unsupported signatures, whole-args forwarding, computed argument access, or component values forwarded through template arguments |

Angular reads in an external `templateUrl` belong to the class that declares that template. A read in another class's template does not make the prop eligible. Glimmer template reads through `@arg` or `this.args.arg` count when the template belongs to the declaring class. Passing a component value to a child through a template argument makes its callers uncertain.

Lit attribute names follow HTML case rules and the property's configured attribute alias. Direct `.property` bindings use the exact property name, including when `attribute: false` disables attribute supply. A custom-element caller in Astro, Vue, Svelte, or JSX remains evidence of a caller, but prevents a Lit absence finding when fallow cannot model that syntax bridge.

A proven spread has a known set of own keys and comes from a literal or an immutable local binding. Mutated or escaped objects, imported objects, computed keys, and methods or accessors do not establish complete prop supply. A `.tsx` filename alone does not establish which framework's component rules apply.

Each finding points to the declaration and the inspected callers. It records whether the declaration has a default. Review these locations and the component's API before changing code. Static caller inspection does not prove that conditional UI is unreachable at runtime.

There is no automatic prop removal or conditional-UI deletion. To keep an intentional prop, suppress the finding at its declaration with `fallow-ignore-next-line absent-component-prop`, using the comment syntax of that source file. You can also set the rule to `off` for matching paths in [overrides](/configuration/overview#overrides).

### Unused component emits {#unused-component-emits}

An event declared with `defineEmits` in Vue `<script setup>` that no code in its own single-file component emits: no `emit('name')` in `<script>`, and no `emit('name')` or `$emit('name')` in `<template>`. This is the emit-side version of unused component props. `vue-tsc` and Volar do not find it. They check that callers use correct listeners, not that the component emits each declared event.
Expand Down
16 changes: 16 additions & 0 deletions integrations/mcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,22 @@ When the response `capabilities` array has `function_identity_v2`, cloud runtime

The `save_baseline`, `save_regression_baseline`, and `save_snapshot` parameters write only inside the project root, its Git work tree, the CI workspace (`GITHUB_WORKSPACE`, `CI_PROJECT_DIR`), `RUNNER_TEMP`, or the system temp directory. Another path returns a tool error. See [Save destinations](/cli/global-flags#save-destinations).

### Review optional props absent from callers

To request [absent component props](/explanations/dead-code#absent-component-props), pass `issue_types: ["absent-component-props"]` to `analyze`:

```json
{
"name": "analyze",
"arguments": {
"root": "/path/to/project",
"issue_types": ["absent-component-props"]
}
}
```

The rule is off by default. Selecting it opts in to findings about optional props that the component reads but no inspected reachable caller supplies. A finding includes its declaration and inspected callers. Review defaults and API intent before proposing an edit. Every action requires manual review; no action automatically removes a prop or conditional UI.

### Notable tool parameters

Some tools take more parameters than the common `root`, `config`, `no_cache`, and `threads`:
Expand Down
22 changes: 11 additions & 11 deletions public-content-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"visibility": "public-only"
},
"content": {
"sha256": "a264cabeaba9e5e81e4845e8fd4ffa7c0387d4280931b5bc1e79ec2d596ce825",
"sha256": "46913310d60a6d81018d51ef04c38770d17b19b82c8160aac6c36d8bb5bf47b6",
"files": [
{
"path": "adoption.mdx",
Expand All @@ -31,8 +31,8 @@
},
{
"path": "analysis/dead-code.mdx",
"bytes": 32350,
"sha256": "037f642cf52efe477ecb2118268cbd3207a7ff75c3375f1643e133452117bef3"
"bytes": 33141,
"sha256": "3f38d9dcfb873ecae2261001395e520e3492fa70435aa1ab358d2fe3e9150c36"
},
{
"path": "analysis/debugging.mdx",
Expand Down Expand Up @@ -101,8 +101,8 @@
},
{
"path": "cli/dead-code.mdx",
"bytes": 32364,
"sha256": "91fd30b6e3584002d4a53f2de28a4f771893397e296973b4c5a20b11f7263f90"
"bytes": 33394,
"sha256": "babe550f36419c70cece19fd6d1061327a4f9bd31fba0f19b584a771b6b0548f"
},
{
"path": "cli/decision-surface.mdx",
Expand Down Expand Up @@ -261,8 +261,8 @@
},
{
"path": "configuration/rules.mdx",
"bytes": 18720,
"sha256": "e0b910e7f6c7eefedcc5aedb45f007daa56877c76639feaf55f78b9e09371360"
"bytes": 19056,
"sha256": "013d0147211e0448a577fc575b45c3a2800c219c06854c16d953bc28814916f9"
},
{
"path": "configuration/suppression.mdx",
Expand Down Expand Up @@ -291,8 +291,8 @@
},
{
"path": "explanations/dead-code.mdx",
"bytes": 86001,
"sha256": "599442af48c03b82dc17df3667b7c00e20f55a0fc5ec3664d2a598f08bd5cbd1"
"bytes": 92938,
"sha256": "127902f64461e6aade063d744dec7e7f119ee9f6b51dc3a4642e668d438e23b4"
},
{
"path": "explanations/duplication.mdx",
Expand Down Expand Up @@ -406,8 +406,8 @@
},
{
"path": "integrations/mcp.mdx",
"bytes": 56264,
"sha256": "6fdfbb8bbad25598aef75a4582f79845ae0a78bab2d24140925b2c1f19389b95"
"bytes": 56957,
"sha256": "a52d643d2a2bbab2dcef418165226f9dc36a3cad83568175285683faf77f93a3"
},
{
"path": "integrations/neovim.mdx",
Expand Down