Skip to content
Open
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
140 changes: 140 additions & 0 deletions packages/docs/src/content/docs/features/bulk-suppressions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
---
title: Bulk Suppressions
---

Knip supports a suppression system to ignore reported issues. This is useful
when introducing Knip to a large, existing codebase, or when you want to
temporarily ignore specific issues.

Suppressions are not yet supported in [production][1]/[strict][2] mode.

## Generating suppressions

To suppress all currently reported issues, run:

```sh
knip --suppress-all
```

This creates a `.knip-suppressions.json` file in the project root. This file
acts as a baseline: it snapshots usage issues so you can focus on new issues, or
burn down the existing ones at your own pace.

Flags:

- Use `--suppress-until <date>` to add an expiry date (`YYYY-MM-DD`) to new
suppressions.
- Use `--suppressions-location <path>` for a custom file path.
- Use existing [scope flags][3] like `--include`, `--exports` and `--workspace`
to filter suppressions.

## Managing suppressions

Stale suppressions are pruned automatically on every `knip` run. When you fix an
issue (or delete the code), the corresponding entry will be automatically
removed from the file.

### Tackling suppressed issues

To reveal a subset of suppressed issues, combine `--no-suppressions` with one or
more [scope flags][3] like `--include`, `--exports` or `--workspace`:

```sh
knip --no-suppressions --exports
```

This shows all suppressed export issues so you can fix them incrementally. After
fixing, just run `knip` to update the suppressions file automatically.

### Expiry

Use the `--suppress-until` argument, or manually add an `until` field to any
suppression in the JSON file:

```json
"src/feature-flagged.ts": {
"exports": {
"deprecatedHelper": {"until":"2026-02-16"}
}
}
```

After this date, the suppression is ignored, and Knip will report the issue
again. This might help with planning, temporary workarounds and migration
processes.

## CI

To ensure that new issues are caught (not suppressed) and the suppressions file
is up-to-date (no unused entries):

```sh
knip --check-suppressions
```

This exits non-zero if the suppression file has changed (i.e. if suppressions
were auto-pruned or added). This enforces suppression file updates committed
along with fixed issues.

## Suppressions vs. JSDoc tags

The suppressions file is intended for bulk-ignoring existing issues when
introducing Knip to a codebase. For individual cases where you want to document
_why_ something is kept, prefer JSDoc tags like `@lintignore`, `@internal` or
`@public` directly in the code:

```ts
/** @lintignore Exported but unused for reasons */
export function formatDate() {}
```

Tags live next to the code, carry context naturally, and don't rely on an
external file. See [JSDoc Tags][4] for details.

That said, additional fields in the JSON file are preserved.

## Suppressions vs. ignore patterns

Use `ignore*` items for false positives (i.e. when Knip is wrong), use
suppressions for actual issues you want to fix later.

Remember that [ignore][5] patterns are nearly always a bad idea. They might hurt
performance and hide issues that you do want to know about. [Exclude the file
from analysis][6], use a more specific `ignore*` pattern to get rid of a false
positive, or suppress a specific issue temporarily.

## Suppressions file

The `.knip-suppressions.json` file is human-readable and git-friendly. Sorted
keys and one line per item:

```json title=".knip-suppressions.json"
{
"version": 1,
"suppressions": {
"packages/ui/package.json": {
"dependencies": {
"lodash": {}
}
},
"src/old-module.ts": {
"files": {
"src/old-module.ts": {"until":"2026-02-16"}
}
},
"src/utils/helpers.ts": {
"exports": {
"formatDate": {},
"parseQuery": {"until":"2026-02-16"}
}
}
}
}
```

[1]: ./production-mode.md
[2]: ./production-mode.md#strict-mode
[3]: ../reference/cli#scope
[4]: ../reference/jsdoc-tsdoc-tags.md
[5]: ../reference/configuration.md#ignore
[6]: ../guides/configuring-project-files.md
35 changes: 35 additions & 0 deletions packages/docs/src/content/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,41 @@ The default exit codes:
| `1` | Knip ran successfully, but there is at least one lint issue |
| `2` | Knip did not run successfully due to bad input or internal error |

## Suppressions

### `--suppress-all`

Generate a `.knip-suppressions.json` file to suppress all currently reported
issues.

### `--suppress-type [type]`

Suppress only a specific issue type (e.g. `exports` or `dependencies`). Can be
combined with `--suppress-all` to refresh only one type.

### `--suppress-until [date]`

Snapshot this date into the suppressions file. Format: `YYYY-MM-DD`. Knip
ignores the suppression after this date.

### `--suppressions-location`

Path to the suppressions file. Default: `.knip-suppressions.json`.

### `--check-suppressions`

Exit with an error code if the suppressions file is modified (e.g. by
auto-pruning). Useful in CI to ensure the file is up-to-date.

### `--prune-suppressions`

Explicitly remove unused entries from the suppressions file. (Note: Knip
automatically prunes unused suppressions when running with `--fix`).

### `--no-suppressions`

Run Knip ignoring any existing suppressions file.

## Troubleshooting

### `--debug`
Expand Down
7 changes: 7 additions & 0 deletions packages/knip/fixtures/suppressions-workspaces/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@fixtures/suppressions-workspaces",
"workspaces": [
"workspace-a",
"workspace-b"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { used } from './module';
used();
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const used = () => 'used';
export const unusedA = 'unused in workspace-a';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "@fixtures/suppressions-workspaces__a"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { used } from './module';
used();
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const used = () => 'used';
export const unusedB = 'unused in workspace-b';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "@fixtures/suppressions-workspaces__b"
}
22 changes: 22 additions & 0 deletions packages/knip/fixtures/suppressions/.knip-suppressions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"version": 1,
"suppressions": {
"module.ts": {
"exports": {
"anotherUnused": {},
"unusedExport": {}
}
},
"package.json": {
"dependencies": {
"unused-pkg": {},
"used-pkg": {}
}
},
"unused.ts": {
"files": {
"unused.ts": {}
}
}
}
}
2 changes: 2 additions & 0 deletions packages/knip/fixtures/suppressions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { used } from './module';
used();
3 changes: 3 additions & 0 deletions packages/knip/fixtures/suppressions/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const used = () => 'used';
export const unusedExport = () => 'unused';
export const anotherUnused = 'also unused';
7 changes: 7 additions & 0 deletions packages/knip/fixtures/suppressions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@fixtures/suppressions",
"dependencies": {
"used-pkg": "*",
"unused-pkg": "*"
}
}
1 change: 1 addition & 0 deletions packages/knip/fixtures/suppressions/unused.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const orphan = 'this file is unused';
4 changes: 3 additions & 1 deletion packages/knip/src/IssueCollector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ export class IssueCollector {
this.issues.files.add(filePath);
const symbol = relative(this.cwd, filePath);
// @ts-expect-error TODO Fix up in next major
this.issues._files[symbol] = [{ type: 'files', filePath, symbol, severity: this.rules.files }];
this.issues._files[symbol] = {
[symbol]: { type: 'files', filePath, symbol, severity: this.rules.files, fixes: [] },
};

this.counters.files++;
this.counters.processed++;
Expand Down
24 changes: 24 additions & 0 deletions packages/knip/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { logError, logWarning } from './util/log.js';
import { perfObserver } from './util/Performance.js';
import { runPreprocessors, runReporters } from './util/reporter.js';
import { prettyMilliseconds } from './util/string.js';
import { _handleSuppressions } from './util/suppressions.js';
import { version } from './version.js';

let args: ReturnType<typeof parseArgs> = {};
Expand Down Expand Up @@ -59,6 +60,27 @@ const main = async () => {
// These modes have their own reporting mechanism
if (options.isWatch || options.isTrace) return;

let suppressedCount = 0;
let expiredCount = 0;

if (!options.isProduction) {
const suppressionResult = await _handleSuppressions(issues, counters, options);

if (suppressionResult.action === 'generated') {
console.log(suppressionResult.message);
process.exit(0);
}

if (suppressionResult.action === 'applied') {
suppressedCount = suppressionResult.suppressedCount;
expiredCount = suppressionResult.expiredCount;
if (suppressionResult.isChanged && options.checkSuppressions) {
console.log('Suppressions file has been updated. Please commit the changes.');
process.exit(1);
}
}
}

const initialData: ReporterOptions = {
report: options.includedIssueTypes,
issues,
Expand All @@ -77,6 +99,8 @@ const main = async () => {
options: args['reporter-options'] ?? '',
preprocessorOptions: args['preprocessor-options'] ?? '',
selectedWorkspaces,
suppressedCount,
expiredCount,
};

const finalData = await runPreprocessors(args.preprocessor ?? [], initialData);
Expand Down
2 changes: 2 additions & 0 deletions packages/knip/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export const KNIP_CONFIG_LOCATIONS = [
'knip.config.js',
];

export const DEFAULT_SUPPRESSIONS_FILE = '.knip-suppressions.json';

export const DEFAULT_EXTENSIONS = ['.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx', '.mts', '.cts'];

export const IS_DTS = /\.d\.(c|m)?ts$/;
Expand Down
6 changes: 6 additions & 0 deletions packages/knip/src/reporters/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,10 @@ export default (options: ReporterOptions) => {
) {
console.log('✂️ Excellent, Knip found no issues.');
}

if (isShowProgress && options.suppressedCount && options.suppressedCount > 0) {
const parts = [`${options.suppressedCount} suppressed`];
if (options.expiredCount && options.expiredCount > 0) parts.push(`${options.expiredCount} expired`);
console.log(dim(`(${parts.join(', ')})`));
}
};
2 changes: 2 additions & 0 deletions packages/knip/src/types/issues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export type ReporterOptions = {
selectedWorkspaces: string[] | undefined;
configFilePath: string | undefined;
maxShowIssues?: number;
suppressedCount?: number;
expiredCount?: number;
};

export type Reporter = (options: ReporterOptions) => void;
Expand Down
19 changes: 19 additions & 0 deletions packages/knip/src/types/suppressions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { IssueType } from './issues.js';

export interface SuppressionMeta {
until?: string;
}

export type SuppressionEntry = Record<string, SuppressionMeta>;

export type SuppressionsByType = Partial<Record<IssueType, SuppressionEntry>>;

export interface Suppressions {
version: 1;
suppressions: Record<string, SuppressionsByType>;
}

export interface ApplyResult {
suppressedCount: number;
expiredCount: number;
}
Loading
Loading