diff --git a/packages/docs/src/content/docs/reference/jsdoc-tsdoc-tags.md b/packages/docs/src/content/docs/reference/jsdoc-tsdoc-tags.md index 6429eb8b8..fb19273b5 100644 --- a/packages/docs/src/content/docs/reference/jsdoc-tsdoc-tags.md +++ b/packages/docs/src/content/docs/reference/jsdoc-tsdoc-tags.md @@ -110,6 +110,32 @@ Works identical to [`@public`][9]. Knip ignores other tags like `@alpha` and [TSDoc: @beta][10] +## `@knip-ignore-until` + +Temporarily ignore an unused export until a specified date. After the date +passes, Knip will report the export as unused again. + +Example: + +```ts +/** + * @knip-ignore-until 2025-04-01 + */ +export const temporarilyIgnored = () => {}; +``` + +Use ISO 8601 date format (`YYYY-MM-DD`). This is useful for: + +- Exports that will be used in upcoming features +- Temporary workarounds with a planned removal date +- Staged rollouts where code will be enabled later + +| Date value | Behavior | +| ---------- | -------- | +| Future date | Ignored (not reported) | +| Past date | Reported as unused | +| Invalid date | Reported as unused | + [1]: ../reference/cli.md#--tags [2]: ./configuration.md#tags [3]: ./cli.md#--include-entry-exports diff --git a/packages/knip/fixtures/ignore-until/index.ts b/packages/knip/fixtures/ignore-until/index.ts new file mode 100644 index 000000000..d3eb2fba6 --- /dev/null +++ b/packages/knip/fixtures/ignore-until/index.ts @@ -0,0 +1 @@ +import './module.js'; diff --git a/packages/knip/fixtures/ignore-until/module.ts b/packages/knip/fixtures/ignore-until/module.ts new file mode 100644 index 000000000..bf584f24c --- /dev/null +++ b/packages/knip/fixtures/ignore-until/module.ts @@ -0,0 +1,16 @@ +/** + * @knip-ignore-until 2099-12-31 + */ +export const ignoredUntilFuture = 1; + +/** + * @knip-ignore-until 2020-01-01 + */ +export const ignoredUntilPast = 1; + +/** + * @knip-ignore-until invalid-date + */ +export const ignoredUntilInvalid = 1; + +export const regularUnused = 1; diff --git a/packages/knip/fixtures/ignore-until/package.json b/packages/knip/fixtures/ignore-until/package.json new file mode 100644 index 000000000..67f62929d --- /dev/null +++ b/packages/knip/fixtures/ignore-until/package.json @@ -0,0 +1,3 @@ +{ + "name": "@fixtures/ignore-until" +} diff --git a/packages/knip/src/typescript/ast-helpers.ts b/packages/knip/src/typescript/ast-helpers.ts index 970804aee..783e633e9 100644 --- a/packages/knip/src/typescript/ast-helpers.ts +++ b/packages/knip/src/typescript/ast-helpers.ts @@ -187,7 +187,8 @@ export const getJSDocTags = (node: ts.Node) => { tagNodes = [...tagNodes, ...ts.getJSDocTags(node.parent)]; } for (const tagNode of tagNodes) { - const match = tagNode.getText()?.match(/@\S+/); + const text = tagNode.getText(); + const match = text?.match(/@knip-ignore-until\s+\S+/) ?? text?.match(/@\S+/); if (match) tags.add(match[0]); } return tags; diff --git a/packages/knip/src/util/tag.ts b/packages/knip/src/util/tag.ts index ff6a72c53..683ac532b 100644 --- a/packages/knip/src/util/tag.ts +++ b/packages/knip/src/util/tag.ts @@ -22,10 +22,21 @@ export const shouldIgnore = (jsDocTags: Set, tags: Tags) => { return false; }; +const isIgnoredUntilValid = (jsDocTags: Set) => { + for (const tag of jsDocTags) { + const match = tag.match(/@knip-ignore-until\s+(\S+)/); + if (match && new Date(match[1]) > new Date()) { + return true; + } + } + return false; +}; + export const getShouldIgnoreHandler = (isProduction: boolean) => (jsDocTags: Set) => jsDocTags.has(PUBLIC_TAG) || jsDocTags.has(BETA_TAG) || jsDocTags.has(ALIAS_TAG) || - (isProduction && jsDocTags.has(INTERNAL_TAG)); + (isProduction && jsDocTags.has(INTERNAL_TAG)) || + isIgnoredUntilValid(jsDocTags); export const getShouldIgnoreTagHandler = (tags: Tags) => (jsDocTags: Set) => shouldIgnore(jsDocTags, tags); diff --git a/packages/knip/test/ignore-until.test.ts b/packages/knip/test/ignore-until.test.ts new file mode 100644 index 000000000..309f196e0 --- /dev/null +++ b/packages/knip/test/ignore-until.test.ts @@ -0,0 +1,28 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { main } from '../src/index.js'; +import baseCounters from './helpers/baseCounters.js'; +import { createOptions } from './helpers/create-options.js'; +import { resolve } from './helpers/resolve.js'; + +const cwd = resolve('fixtures/ignore-until'); + +test('Ignore exports until date', async () => { + const options = await createOptions({ cwd }); + const { issues, counters } = await main(options); + + assert(!issues.exports['module.ts']?.['ignoredUntilFuture']); + + assert(issues.exports['module.ts']?.['ignoredUntilPast']); + + assert(issues.exports['module.ts']?.['ignoredUntilInvalid']); + + assert(issues.exports['module.ts']?.['regularUnused']); + + assert.deepEqual(counters, { + ...baseCounters, + exports: 3, + processed: 2, + total: 2, + }); +});