knip 3.6.0 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ import type { ExportPos } from './types/exports.js';
2
+ import type { Issues } from './types/issues.js';
3
+ export declare class IssueFixer {
4
+ isEnabled: boolean;
5
+ cwd: string;
6
+ isFixDependencies: boolean;
7
+ isFixUnusedTypes: boolean;
8
+ isFixUnusedExports: boolean;
9
+ unusedTypeNodes: Map<string, Set<[number, number]>>;
10
+ unusedExportNodes: Map<string, Set<[number, number]>>;
11
+ constructor({ isEnabled, cwd, fixTypes }: {
12
+ isEnabled: boolean;
13
+ cwd: string;
14
+ fixTypes: string[];
15
+ });
16
+ addUnusedTypeNode(filePath: string, fix: ExportPos): void;
17
+ addUnusedExportNode(filePath: string, fix: ExportPos): void;
18
+ fixIssues(issues: Issues): Promise<void>;
19
+ private removeUnusedExportKeywords;
20
+ private removeUnusedDependencies;
21
+ }
@@ -0,0 +1,75 @@
1
+ import { readFile, writeFile } from 'fs/promises';
2
+ import NPMCliPackageJson from '@npmcli/package-json';
3
+ import { dirname, join } from './util/path.js';
4
+ export class IssueFixer {
5
+ isEnabled = false;
6
+ cwd = process.cwd();
7
+ isFixDependencies = true;
8
+ isFixUnusedTypes = true;
9
+ isFixUnusedExports = true;
10
+ unusedTypeNodes = new Map();
11
+ unusedExportNodes = new Map();
12
+ constructor({ isEnabled, cwd, fixTypes = [] }) {
13
+ this.isEnabled = isEnabled;
14
+ this.cwd = cwd;
15
+ this.isFixDependencies = fixTypes.length === 0 || fixTypes.includes('dependencies');
16
+ this.isFixUnusedTypes = fixTypes.length === 0 || fixTypes.includes('types');
17
+ this.isFixUnusedExports = fixTypes.length === 0 || fixTypes.includes('exports');
18
+ }
19
+ addUnusedTypeNode(filePath, fix) {
20
+ if (fix.length === 0)
21
+ return;
22
+ if (this.unusedTypeNodes.has(filePath))
23
+ this.unusedTypeNodes.get(filePath).add(fix);
24
+ else
25
+ this.unusedTypeNodes.set(filePath, new Set([fix]));
26
+ }
27
+ addUnusedExportNode(filePath, fix) {
28
+ if (fix.length === 0)
29
+ return;
30
+ if (this.unusedExportNodes.has(filePath))
31
+ this.unusedExportNodes.get(filePath).add(fix);
32
+ else
33
+ this.unusedExportNodes.set(filePath, new Set([fix]));
34
+ }
35
+ async fixIssues(issues) {
36
+ await this.removeUnusedExportKeywords();
37
+ await this.removeUnusedDependencies(issues);
38
+ }
39
+ async removeUnusedExportKeywords() {
40
+ const filePaths = new Set([...this.unusedTypeNodes.keys(), ...this.unusedExportNodes.keys()]);
41
+ for (const filePath of filePaths) {
42
+ const exportPositions = [
43
+ ...(this.isFixUnusedTypes ? this.unusedTypeNodes.get(filePath) ?? [] : []),
44
+ ...(this.isFixUnusedExports ? this.unusedExportNodes.get(filePath) ?? [] : []),
45
+ ].sort((a, b) => b[0] - a[0]);
46
+ if (exportPositions.length > 0) {
47
+ const text = await readFile(filePath, 'utf-8');
48
+ const sourceFileText = exportPositions.reduce((text, [start, end]) => text.substring(0, start) + text.substring(end), text);
49
+ await writeFile(filePath, sourceFileText);
50
+ }
51
+ }
52
+ }
53
+ async removeUnusedDependencies(issues) {
54
+ if (!this.isFixDependencies)
55
+ return;
56
+ const filePaths = new Set([...Object.keys(issues.dependencies), ...Object.keys(issues.devDependencies)]);
57
+ for (const filePath of filePaths) {
58
+ const manifest = await NPMCliPackageJson.load(dirname(join(this.cwd, filePath)));
59
+ const pkg = manifest.content;
60
+ if (filePath in issues.dependencies) {
61
+ Object.keys(issues.dependencies[filePath]).forEach(dependency => {
62
+ if (pkg.dependencies)
63
+ delete pkg.dependencies[dependency];
64
+ });
65
+ }
66
+ if (filePath in issues.devDependencies) {
67
+ Object.keys(issues.devDependencies[filePath]).forEach(dependency => {
68
+ if (pkg.devDependencies)
69
+ delete pkg.devDependencies[dependency];
70
+ });
71
+ }
72
+ await manifest.save();
73
+ }
74
+ }
75
+ }
@@ -38,8 +38,10 @@ export declare class ProjectPrincipal {
38
38
  getUsedResolvedFiles(): string[];
39
39
  private getProgramSourceFiles;
40
40
  getUnreferencedFiles(): string[];
41
- analyzeSourceFile(filePath: string, { skipTypeOnly }: {
41
+ analyzeSourceFile(filePath: string, { skipTypeOnly, isFixExports, isFixTypes }: {
42
42
  skipTypeOnly: boolean;
43
+ isFixExports: boolean;
44
+ isFixTypes: boolean;
43
45
  }): {
44
46
  imports: {
45
47
  internal: import("./types/imports.js").Imports;
@@ -3,7 +3,7 @@ import { DEFAULT_EXTENSIONS } from './constants.js';
3
3
  import { IGNORED_FILE_EXTENSIONS } from './constants.js';
4
4
  import { getJSDocTags, getLineAndCharacterOfPosition, isInModuleBlock } from './typescript/ast-helpers.js';
5
5
  import { createHosts } from './typescript/createHosts.js';
6
- import { getImportsAndExports } from './typescript/getImportsAndExports.js';
6
+ import { _getImportsAndExports } from './typescript/getImportsAndExports.js';
7
7
  import { createCustomModuleResolver } from './typescript/resolveModuleNames.js';
8
8
  import { SourceFileManager } from './typescript/SourceFileManager.js';
9
9
  import { compact } from './util/array.js';
@@ -113,7 +113,7 @@ export class ProjectPrincipal {
113
113
  const sourceFiles = this.getProgramSourceFiles();
114
114
  return Array.from(this.projectPaths).filter(filePath => !sourceFiles.has(filePath));
115
115
  }
116
- analyzeSourceFile(filePath, { skipTypeOnly }) {
116
+ analyzeSourceFile(filePath, { skipTypeOnly, isFixExports, isFixTypes }) {
117
117
  const sourceFile = this.backend.fileManager.getSourceFile(filePath);
118
118
  if (!sourceFile)
119
119
  throw new Error(`Unable to find ${filePath}`);
@@ -121,9 +121,11 @@ export class ProjectPrincipal {
121
121
  const getResolvedModule = specifier => this.backend.program?.getResolvedModule
122
122
  ? this.backend.program.getResolvedModule(sourceFile, specifier, undefined)
123
123
  : sourceFile.resolvedModules?.get(specifier, undefined);
124
- const { imports, exports, scripts } = getImportsAndExports(sourceFile, getResolvedModule, {
124
+ const { imports, exports, scripts } = _getImportsAndExports(sourceFile, getResolvedModule, {
125
125
  skipTypeOnly,
126
126
  skipExports,
127
+ isFixExports,
128
+ isFixTypes,
127
129
  });
128
130
  const { internal, unresolved, external } = imports;
129
131
  const unresolvedImports = new Set();
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import { Performance } from './util/Performance.js';
7
7
  import { runPreprocessors, runReporters } from './util/reporter.js';
8
8
  import { version } from './version.js';
9
9
  import { main } from './index.js';
10
- const { debug: isDebug = false, help: isHelp, 'max-issues': maxIssues = '0', 'no-config-hints': noConfigHints = false, 'no-exit-code': noExitCode = false, 'no-gitignore': isNoGitIgnore = false, 'no-progress': isNoProgress = false, 'include-entry-exports': isIncludeEntryExports = false, 'isolate-workspaces': isIsolateWorkspaces = false, performance: isObservePerf = false, production: isProduction = false, 'reporter-options': reporterOptions = '', 'preprocessor-options': preprocessorOptions = '', strict: isStrict = false, tsConfig, version: isVersion, } = parsedArgValues;
10
+ const { debug: isDebug = false, help: isHelp, 'max-issues': maxIssues = '0', 'no-config-hints': noConfigHints = false, 'no-exit-code': noExitCode = false, 'no-gitignore': isNoGitIgnore = false, 'no-progress': isNoProgress = false, 'include-entry-exports': isIncludeEntryExports = false, 'isolate-workspaces': isIsolateWorkspaces = false, performance: isObservePerf = false, production: isProduction = false, 'reporter-options': reporterOptions = '', 'preprocessor-options': preprocessorOptions = '', strict: isStrict = false, fix: isFix = false, 'fix-type': fixTypes = [], tsConfig, version: isVersion, } = parsedArgValues;
11
11
  if (isHelp) {
12
12
  console.log(helpText);
13
13
  process.exit(0);
@@ -29,6 +29,8 @@ const run = async () => {
29
29
  isShowProgress,
30
30
  isIncludeEntryExports,
31
31
  isIsolateWorkspaces,
32
+ isFix,
33
+ fixTypes: fixTypes.flatMap(type => type.split(',')),
32
34
  });
33
35
  const initialData = {
34
36
  report,
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { ConfigurationChief } from './ConfigurationChief.js';
5
5
  import { ConsoleStreamer } from './ConsoleStreamer.js';
6
6
  import { DependencyDeputy } from './DependencyDeputy.js';
7
7
  import { IssueCollector } from './IssueCollector.js';
8
+ import { IssueFixer } from './IssueFixer.js';
8
9
  import { PrincipalFactory } from './PrincipalFactory.js';
9
10
  import { ProjectPrincipal } from './ProjectPrincipal.js';
10
11
  import { debugLogObject, debugLogArray, debugLog } from './util/debug.js';
@@ -16,7 +17,7 @@ import { _resolveSpecifier } from './util/require.js';
16
17
  import { loadTSConfig } from './util/tsconfig-loader.js';
17
18
  import { WorkspaceWorker } from './WorkspaceWorker.js';
18
19
  export const main = async (unresolvedConfiguration) => {
19
- const { cwd, tsConfigFile, gitignore, isStrict, isProduction, isShowProgress, isIncludeEntryExports, isIsolateWorkspaces, } = unresolvedConfiguration;
20
+ const { cwd, tsConfigFile, gitignore, isStrict, isProduction, isShowProgress, isIncludeEntryExports, isIsolateWorkspaces, isFix, fixTypes, } = unresolvedConfiguration;
20
21
  debugLogObject('*', 'Unresolved configuration (from CLI arguments)', unresolvedConfiguration);
21
22
  const chief = new ConfigurationChief({ cwd, isProduction, isStrict, isIncludeEntryExports });
22
23
  const deputy = new DependencyDeputy({ isProduction, isStrict });
@@ -30,6 +31,7 @@ export const main = async (unresolvedConfiguration) => {
30
31
  const report = chief.getIssueTypesToReport();
31
32
  const rules = chief.getRules();
32
33
  const filters = chief.getFilters();
34
+ const fixer = new IssueFixer({ isEnabled: isFix || fixTypes.length > 0, cwd, fixTypes });
33
35
  const isReportDependencies = report.dependencies || report.unlisted || report.unresolved;
34
36
  const isReportValues = report.exports || report.nsExports || report.classMembers;
35
37
  const isReportTypes = report.types || report.nsTypes || report.enumMembers;
@@ -190,7 +192,11 @@ export const main = async (unresolvedConfiguration) => {
190
192
  const analyzeSourceFile = (filePath, _principal = principal) => {
191
193
  const workspace = chief.findWorkspaceByFilePath(filePath);
192
194
  if (workspace) {
193
- const { imports, exports, scripts } = _principal.analyzeSourceFile(filePath, { skipTypeOnly: isStrict });
195
+ const { imports, exports, scripts } = _principal.analyzeSourceFile(filePath, {
196
+ skipTypeOnly: isStrict,
197
+ isFixExports: fixer.isFixUnusedExports,
198
+ isFixTypes: fixer.isFixUnusedTypes,
199
+ });
194
200
  const { internal, external, unresolved } = imports;
195
201
  const { exported, duplicate } = exports;
196
202
  if (exported.size > 0)
@@ -308,8 +314,11 @@ export const main = async (unresolvedConfiguration) => {
308
314
  if (isProduction && exportedItem.jsDocTags.has('@internal'))
309
315
  continue;
310
316
  if (importsForExport && isSymbolImported(symbol, importsForExport)) {
311
- if (importsForExport.isReExport && isExportedInEntryFile(principal, importsForExport))
317
+ if (!isIncludeEntryExports &&
318
+ importsForExport.isReExport &&
319
+ isExportedInEntryFile(principal, importsForExport)) {
312
320
  continue;
321
+ }
313
322
  if (report.enumMembers && exportedItem.type === 'enum' && exportedItem.members) {
314
323
  principal.findUnusedMembers(filePath, exportedItem.members).forEach(member => {
315
324
  collector.addIssue({
@@ -348,6 +357,10 @@ export const main = async (unresolvedConfiguration) => {
348
357
  symbolType: exportedItem.type,
349
358
  ...principal.getPos(exportedItem.node, exportedItem.pos),
350
359
  });
360
+ if (isType)
361
+ fixer.addUnusedTypeNode(filePath, exportedItem.fix);
362
+ else
363
+ fixer.addUnusedExportNode(filePath, exportedItem.fix);
351
364
  }
352
365
  }
353
366
  }
@@ -371,6 +384,9 @@ export const main = async (unresolvedConfiguration) => {
371
384
  const unusedIgnoredWorkspaces = chief.getUnusedIgnoredWorkspaces();
372
385
  unusedIgnoredWorkspaces.forEach(identifier => collector.addConfigurationHint({ type: 'ignoreWorkspaces', identifier }));
373
386
  const { issues, counters, configurationHints } = collector.getIssues();
387
+ if (isFix) {
388
+ await fixer.fixIssues(issues);
389
+ }
374
390
  streamer.clear();
375
391
  return { report, issues, counters, rules, configurationHints };
376
392
  };
@@ -7,4 +7,6 @@ export interface CommandLineOptions {
7
7
  isShowProgress: boolean;
8
8
  isIncludeEntryExports: boolean;
9
9
  isIsolateWorkspaces: boolean;
10
+ isFix: boolean;
11
+ fixTypes: string[];
10
12
  }
@@ -2,6 +2,7 @@ import ts from 'typescript';
2
2
  import type { SymbolType } from './issues.js';
3
3
  type FilePath = string;
4
4
  type Identifier = string;
5
+ export type ExportPos = [number, number] | [];
5
6
  export type ExportItem = {
6
7
  node: ts.Node;
7
8
  pos: number;
@@ -9,12 +10,14 @@ export type ExportItem = {
9
10
  type: SymbolType;
10
11
  members?: ExportItemMember[];
11
12
  jsDocTags?: Set<string>;
13
+ fix: ExportPos;
12
14
  };
13
15
  export type ExportItemMember = {
14
16
  node: ts.Node;
15
17
  identifier: Identifier;
16
18
  pos: number;
17
19
  type: SymbolType;
20
+ fix: ExportPos;
18
21
  };
19
22
  export type ExportItems = Map<string, Required<ExportItem>>;
20
23
  export type Exports = Map<FilePath, ExportItems>;
@@ -1,11 +1,13 @@
1
1
  import ts from 'typescript';
2
2
  import type { BoundSourceFile, GetResolvedModule } from './SourceFile.js';
3
- import type { ExportItems as Exports, ExportItem } from '../types/exports.js';
3
+ import type { ExportItems as Exports, ExportItem, ExportPos } from '../types/exports.js';
4
4
  import type { Imports, UnresolvedImport } from '../types/imports.js';
5
5
  import type { IssueSymbol } from '../types/issues.js';
6
6
  export type GetImportsAndExportsOptions = {
7
7
  skipTypeOnly: boolean;
8
8
  skipExports: boolean;
9
+ isFixExports: boolean;
10
+ isFixTypes: boolean;
9
11
  };
10
12
  export type AddImportOptions = {
11
13
  specifier: string;
@@ -16,8 +18,9 @@ export type AddImportOptions = {
16
18
  };
17
19
  export type AddExportOptions = ExportItem & {
18
20
  identifier: string;
21
+ fix: ExportPos;
19
22
  };
20
- export declare const getImportsAndExports: (sourceFile: BoundSourceFile, getResolvedModule: GetResolvedModule, options: GetImportsAndExportsOptions) => {
23
+ export declare const _getImportsAndExports: (sourceFile: BoundSourceFile, getResolvedModule: GetResolvedModule, options: GetImportsAndExportsOptions) => {
21
24
  imports: {
22
25
  internal: Imports;
23
26
  external: Set<string>;
@@ -3,6 +3,7 @@ import ts from 'typescript';
3
3
  import { getOrSet } from '../util/map.js';
4
4
  import { isStartsLikePackageName, sanitizeSpecifier } from '../util/modules.js';
5
5
  import { isInNodeModules } from '../util/path.js';
6
+ import { timerify } from '../util/Performance.js';
6
7
  import { isDeclarationFileExtension, isAccessExpression, getAccessExpressionName, getJSDocTags, getLineAndCharacterOfPosition, } from './ast-helpers.js';
7
8
  import getExportVisitors from './visitors/exports/index.js';
8
9
  import { getJSXImplicitImportBase } from './visitors/helpers.js';
@@ -13,7 +14,7 @@ const getVisitors = (sourceFile) => ({
13
14
  import: getImportVisitors(sourceFile),
14
15
  script: getScriptVisitors(sourceFile),
15
16
  });
16
- export const getImportsAndExports = (sourceFile, getResolvedModule, options) => {
17
+ const getImportsAndExports = (sourceFile, getResolvedModule, options) => {
17
18
  const internalImports = new Map();
18
19
  const externalImports = new Set();
19
20
  const unresolvedImports = new Set();
@@ -94,7 +95,7 @@ export const getImportsAndExports = (sourceFile, getResolvedModule, options) =>
94
95
  }
95
96
  }
96
97
  };
97
- const addExport = ({ node, identifier, type, pos, posDecl, members = [] }) => {
98
+ const addExport = ({ node, identifier, type, pos, posDecl, members = [], fix }) => {
98
99
  if (options.skipExports)
99
100
  return;
100
101
  const jsDocTags = getJSDocTags(node);
@@ -105,7 +106,7 @@ export const getImportsAndExports = (sourceFile, getResolvedModule, options) =>
105
106
  exports.set(identifier, { ...item, members: crew, jsDocTags: tags });
106
107
  }
107
108
  else {
108
- exports.set(identifier, { node, type, members, jsDocTags, pos, posDecl: posDecl ?? pos });
109
+ exports.set(identifier, { node, type, members, jsDocTags, pos, posDecl: posDecl ?? pos, fix });
109
110
  }
110
111
  if (!jsDocTags.has('@alias')) {
111
112
  if (ts.isExportAssignment(node))
@@ -176,3 +177,4 @@ export const getImportsAndExports = (sourceFile, getResolvedModule, options) =>
176
177
  scripts,
177
178
  };
178
179
  };
180
+ export const _getImportsAndExports = timerify(getImportsAndExports);
@@ -1,9 +1,10 @@
1
1
  import ts from 'typescript';
2
2
  import { SymbolType } from '../../../types/issues.js';
3
3
  import { exportVisitor as visit } from '../index.js';
4
- export default visit(() => true, node => {
4
+ export default visit(() => true, (node, { isFixExports }) => {
5
5
  if (ts.isExportAssignment(node)) {
6
6
  const pos = node.getChildAt(1).getStart();
7
- return { node, identifier: 'default', type: SymbolType.UNKNOWN, pos };
7
+ const fix = isFixExports ? [node.getStart(), node.getEnd() + 1] : [];
8
+ return { node, identifier: 'default', type: SymbolType.UNKNOWN, pos, fix };
8
9
  }
9
10
  });
@@ -1,7 +1,7 @@
1
1
  import ts from 'typescript';
2
2
  import { SymbolType } from '../../../types/issues.js';
3
3
  import { exportVisitor as visit } from '../index.js';
4
- export default visit(() => true, node => {
4
+ export default visit(() => true, (node, { isFixExports, isFixTypes }) => {
5
5
  if (ts.isExportDeclaration(node)) {
6
6
  if (node.exportClause && ts.isNamedExports(node.exportClause)) {
7
7
  const type = node.isTypeOnly ? SymbolType.TYPE : SymbolType.UNKNOWN;
@@ -13,13 +13,8 @@ export default visit(() => true, node => {
13
13
  const pos = element.name.pos;
14
14
  const name = ts.getNameOfDeclaration(declaration);
15
15
  const posDecl = name?.pos ?? declaration?.pos ?? pos;
16
- return {
17
- node: element,
18
- identifier,
19
- type,
20
- pos,
21
- posDecl,
22
- };
16
+ const fix = isFixExports || isFixTypes ? [element.getStart(), element.getEnd()] : [];
17
+ return { node: element, identifier, type, pos, posDecl, fix };
23
18
  });
24
19
  }
25
20
  }
@@ -3,19 +3,21 @@ import { SymbolType } from '../../../types/issues.js';
3
3
  import { compact } from '../../../util/array.js';
4
4
  import { isGetOrSetAccessorDeclaration, isPrivateMember, stripQuotes } from '../../ast-helpers.js';
5
5
  import { exportVisitor as visit } from '../index.js';
6
- export default visit(() => true, node => {
7
- const modifierKinds = node.modifiers?.map(modifier => modifier.kind) ?? [];
8
- if (modifierKinds.includes(ts.SyntaxKind.ExportKeyword)) {
6
+ export default visit(() => true, (node, { isFixExports, isFixTypes }) => {
7
+ const exportKeyword = node.modifiers?.find(mod => mod.kind === ts.SyntaxKind.ExportKeyword);
8
+ if (exportKeyword) {
9
9
  if (ts.isVariableStatement(node)) {
10
10
  return node.declarationList.declarations.flatMap(declaration => {
11
11
  if (ts.isObjectBindingPattern(declaration.name)) {
12
12
  return compact(declaration.name.elements.map(element => {
13
13
  if (ts.isIdentifier(element.name)) {
14
+ const fix = isFixExports ? [element.getStart(), element.getEnd()] : [];
14
15
  return {
15
16
  node: element,
16
17
  identifier: element.name.escapedText.toString(),
17
18
  type: SymbolType.UNKNOWN,
18
19
  pos: element.name.getStart(),
20
+ fix,
19
21
  };
20
22
  }
21
23
  }));
@@ -23,28 +25,47 @@ export default visit(() => true, node => {
23
25
  else if (ts.isArrayBindingPattern(declaration.name)) {
24
26
  return compact(declaration.name.elements.map(element => {
25
27
  if (ts.isBindingElement(element)) {
28
+ const fix = isFixExports ? [element.getStart(), element.getEnd()] : [];
26
29
  return {
27
30
  node: element,
28
31
  identifier: element.getText(),
29
32
  type: SymbolType.UNKNOWN,
30
33
  pos: element.getStart(),
34
+ fix,
31
35
  };
32
36
  }
33
37
  }));
34
38
  }
35
39
  else {
36
40
  const identifier = declaration.name.getText();
37
- return { node: declaration, identifier, type: SymbolType.UNKNOWN, pos: declaration.name.getStart() };
41
+ const fix = isFixExports ? [exportKeyword.getStart(), exportKeyword.getEnd() + 1] : [];
42
+ return {
43
+ node: declaration,
44
+ identifier,
45
+ type: SymbolType.UNKNOWN,
46
+ pos: declaration.name.getStart(),
47
+ fix,
48
+ };
38
49
  }
39
50
  });
40
51
  }
52
+ const defaultKeyword = node.modifiers?.find(mod => mod.kind === ts.SyntaxKind.DefaultKeyword);
41
53
  if (ts.isFunctionDeclaration(node) && node.name) {
42
- const identifier = modifierKinds.includes(ts.SyntaxKind.DefaultKeyword) ? 'default' : node.name.getText();
54
+ const identifier = defaultKeyword ? 'default' : node.name.getText();
43
55
  const pos = (node.name ?? node.body ?? node).getStart();
44
- return { node, identifier, pos, type: SymbolType.FUNCTION };
56
+ const fix = isFixExports
57
+ ? [exportKeyword.getStart(), (defaultKeyword ?? exportKeyword).getEnd() + 1]
58
+ : [];
59
+ return {
60
+ node,
61
+ identifier,
62
+ pos,
63
+ type: SymbolType.FUNCTION,
64
+ fix,
65
+ };
45
66
  }
46
67
  if (ts.isClassDeclaration(node) && node.name) {
47
- const identifier = modifierKinds.includes(ts.SyntaxKind.DefaultKeyword) ? 'default' : node.name.getText();
68
+ const identifier = defaultKeyword ? 'default' : node.name.getText();
48
69
  const pos = (node.name ?? node).getStart();
49
70
  const members = node.members
50
71
  .filter((member) => (ts.isPropertyDeclaration(member) ||
@@ -56,25 +77,59 @@ export default visit(() => true, node => {
56
77
  identifier: member.name.getText(),
57
78
  pos: member.name.getStart(),
58
79
  type: SymbolType.MEMBER,
80
+ fix: [],
59
81
  }));
60
- return { node, identifier, type: SymbolType.CLASS, pos, members };
82
+ const fix = isFixExports
83
+ ? [exportKeyword.getStart(), (defaultKeyword ?? exportKeyword).getEnd() + 1]
84
+ : [];
85
+ return {
86
+ node,
87
+ identifier,
88
+ type: SymbolType.CLASS,
89
+ pos,
90
+ members,
91
+ fix,
92
+ };
61
93
  }
62
94
  if (ts.isTypeAliasDeclaration(node)) {
63
- return { node, identifier: node.name.getText(), type: SymbolType.TYPE, pos: node.name.getStart() };
95
+ const fix = isFixTypes ? [exportKeyword.getStart(), exportKeyword.getEnd() + 1] : [];
96
+ return {
97
+ node,
98
+ identifier: node.name.getText(),
99
+ type: SymbolType.TYPE,
100
+ pos: node.name.getStart(),
101
+ fix,
102
+ };
64
103
  }
65
104
  if (ts.isInterfaceDeclaration(node)) {
66
- return { node, identifier: node.name.getText(), type: SymbolType.INTERFACE, pos: node.name.getStart() };
105
+ const fix = isFixTypes ? [exportKeyword.getStart(), exportKeyword.getEnd() + 1] : [];
106
+ return {
107
+ node,
108
+ identifier: node.name.getText(),
109
+ type: SymbolType.INTERFACE,
110
+ pos: node.name.getStart(),
111
+ fix,
112
+ };
67
113
  }
68
114
  if (ts.isEnumDeclaration(node)) {
69
- const identifier = modifierKinds.includes(ts.SyntaxKind.DefaultKeyword) ? 'default' : node.name.getText();
115
+ const identifier = defaultKeyword ? 'default' : node.name.getText();
70
116
  const pos = node.name.getStart();
71
117
  const members = node.members.map(member => ({
72
118
  node: member,
73
119
  identifier: stripQuotes(member.name.getText()),
74
120
  pos: member.name.getStart(),
75
121
  type: SymbolType.MEMBER,
122
+ fix: [],
76
123
  }));
77
- return { node, identifier, type: SymbolType.ENUM, pos, members };
124
+ const fix = isFixTypes ? [exportKeyword.getStart(), exportKeyword.getEnd() + 1] : [];
125
+ return {
126
+ node,
127
+ identifier,
128
+ type: SymbolType.ENUM,
129
+ pos,
130
+ members,
131
+ fix,
132
+ };
78
133
  }
79
134
  }
80
135
  });
@@ -4,7 +4,7 @@ import { stripQuotes } from '../../ast-helpers.js';
4
4
  import { isJS } from '../helpers.js';
5
5
  import { exportVisitor as visit } from '../index.js';
6
6
  const isModuleExportsAccess = (node) => ts.isIdentifier(node.expression) && node.expression.escapedText === 'module' && node.name.escapedText === 'exports';
7
- export default visit(isJS, node => {
7
+ export default visit(isJS, (node, { isFixExports }) => {
8
8
  if (ts.isExpressionStatement(node)) {
9
9
  if (ts.isBinaryExpression(node.expression)) {
10
10
  if (ts.isPropertyAccessExpression(node.expression.left)) {
@@ -12,17 +12,25 @@ export default visit(isJS, node => {
12
12
  isModuleExportsAccess(node.expression.left.expression)) {
13
13
  const identifier = node.expression.left.name.getText();
14
14
  const pos = node.expression.left.name.pos;
15
- return { node, identifier, type: SymbolType.UNKNOWN, pos };
15
+ const fix = isFixExports ? [node.getStart(), node.getEnd()] : [];
16
+ return {
17
+ node: node.expression.left.name,
18
+ identifier,
19
+ type: SymbolType.UNKNOWN,
20
+ pos,
21
+ fix,
22
+ };
16
23
  }
17
24
  else if (isModuleExportsAccess(node.expression.left)) {
18
25
  const expr = node.expression.right;
19
26
  if (ts.isObjectLiteralExpression(expr) && expr.properties.every(ts.isShorthandPropertyAssignment)) {
20
27
  return expr.properties.map(node => {
21
- return { node, identifier: node.getText(), type: SymbolType.UNKNOWN, pos: node.pos };
28
+ const fix = isFixExports ? [node.getStart(), node.getEnd()] : [];
29
+ return { node, identifier: node.getText(), type: SymbolType.UNKNOWN, pos: node.pos, fix };
22
30
  });
23
31
  }
24
32
  else {
25
- return { node, identifier: 'default', type: SymbolType.UNKNOWN, pos: expr.pos };
33
+ return { node, identifier: 'default', type: SymbolType.UNKNOWN, pos: expr.pos, fix: [] };
26
34
  }
27
35
  }
28
36
  }
@@ -32,7 +40,14 @@ export default visit(isJS, node => {
32
40
  isModuleExportsAccess(node.expression.left.expression)) {
33
41
  const identifier = stripQuotes(node.expression.left.argumentExpression.getText());
34
42
  const pos = node.expression.left.argumentExpression.pos;
35
- return { node, identifier, type: SymbolType.UNKNOWN, pos };
43
+ const fix = isFixExports ? [node.getStart(), node.getEnd()] : [];
44
+ return {
45
+ node: node.expression.left.argumentExpression,
46
+ identifier,
47
+ type: SymbolType.UNKNOWN,
48
+ pos,
49
+ fix,
50
+ };
36
51
  }
37
52
  }
38
53
  }
@@ -1,4 +1,4 @@
1
- export declare const helpText = "\u2702\uFE0F Find unused files, dependencies and exports in your JavaScript and TypeScript projects\n\nUsage: knip [options]\n\nOptions:\n -c, --config [file] Configuration file path (default: [.]knip.json[c], knip.js, knip.ts or package.json#knip)\n -t, --tsConfig [file] TypeScript configuration path (default: tsconfig.json)\n --production Analyze only production source files (e.g. no tests, devDependencies, exported types)\n --strict Consider only direct dependencies of workspace (not devDependencies, not other workspaces)\n -W, --workspace [dir] Analyze a single workspace (default: analyze all configured workspaces)\n --directory [dir] Run process from a different directory (default: cwd)\n --no-gitignore Don't use .gitignore\n --include Report only provided issue type(s), can be comma-separated or repeated (1)\n --exclude Exclude provided issue type(s) from report, can be comma-separated or repeated (1)\n --dependencies Shortcut for --include dependencies,unlisted,binaries,unresolved\n --exports Shortcut for --include exports,nsExports,classMembers,types,nsTypes,enumMembers,duplicates\n --include-entry-exports Include entry files when reporting unused exports\n --isolate-workspaces Isolated workspaces in monorepo\n -n, --no-progress Don't show dynamic progress updates (automatically enabled in CI environments)\n --preprocessor Preprocess the results before providing it to the reporter(s), can be repeated\n --preprocessor-options Pass extra options to the preprocessor (as JSON string, see --reporter-options example)\n --reporter Select reporter: symbols, compact, codeowners, json, can be repeated (default: symbols)\n --reporter-options Pass extra options to the reporter (as JSON string, see example)\n --no-config-hints Suppress configuration hints\n --no-exit-code Always exit with code zero (0)\n --max-issues Maximum number of issues before non-zero exit code (default: 0)\n -d, --debug Show debug output\n --performance Measure count and running time of expensive functions and display stats table\n -h, --help Print this help text\n -V, --version Print version\n\n(1) Issue types: files, dependencies, unlisted, unresolved, exports, nsExports, classMembers, types, nsTypes, enumMembers, duplicates\n\nExamples:\n\n$ knip\n$ knip --production\n$ knip --workspace packages/client --include files,dependencies\n$ knip -c ./config/knip.json --reporter compact\n$ knip --reporter codeowners --reporter-options '{\"path\":\".github/CODEOWNERS\"}'\n\nWebsite: https://knip.dev";
1
+ export declare const helpText = "\u2702\uFE0F Find unused files, dependencies and exports in your JavaScript and TypeScript projects\n\nUsage: knip [options]\n\nOptions:\n -c, --config [file] Configuration file path (default: [.]knip.json[c], knip.js, knip.ts or package.json#knip)\n -t, --tsConfig [file] TypeScript configuration path (default: tsconfig.json)\n --production Analyze only production source files (e.g. no tests, devDependencies, exported types)\n --strict Consider only direct dependencies of workspace (not devDependencies, not other workspaces)\n -W, --workspace [dir] Analyze a single workspace (default: analyze all configured workspaces)\n --directory [dir] Run process from a different directory (default: cwd)\n --no-gitignore Don't use .gitignore\n --include Report only provided issue type(s), can be comma-separated or repeated (1)\n --exclude Exclude provided issue type(s) from report, can be comma-separated or repeated (1)\n --dependencies Shortcut for --include dependencies,unlisted,binaries,unresolved\n --exports Shortcut for --include exports,nsExports,classMembers,types,nsTypes,enumMembers,duplicates\n --fix Fix issues\n --fix-type Fix only issues of type, can be comma-separated or repeated (2)\n --include-entry-exports Include entry files when reporting unused exports\n --isolate-workspaces Isolated workspaces in monorepo\n -n, --no-progress Don't show dynamic progress updates (automatically enabled in CI environments)\n --preprocessor Preprocess the results before providing it to the reporter(s), can be repeated\n --preprocessor-options Pass extra options to the preprocessor (as JSON string, see --reporter-options example)\n --reporter Select reporter: symbols, compact, codeowners, json, can be repeated (default: symbols)\n --reporter-options Pass extra options to the reporter (as JSON string, see example)\n --no-config-hints Suppress configuration hints\n --no-exit-code Always exit with code zero (0)\n --max-issues Maximum number of issues before non-zero exit code (default: 0)\n -d, --debug Show debug output\n --performance Measure count and running time of expensive functions and display stats table\n -h, --help Print this help text\n -V, --version Print version\n\n(1) Issue types: files, dependencies, unlisted, unresolved, exports, nsExports, classMembers, types, nsTypes, enumMembers, duplicates\n(2) Fixable issue types: dependencies, exports, types\n\nExamples:\n\n$ knip\n$ knip --production\n$ knip --workspace packages/client --include files,dependencies\n$ knip -c ./config/knip.json --reporter compact\n$ knip --reporter codeowners --reporter-options '{\"path\":\".github/CODEOWNERS\"}'\n\nWebsite: https://knip.dev";
2
2
  declare const _default: {
3
3
  config: string | undefined;
4
4
  debug: boolean | undefined;
@@ -6,6 +6,8 @@ declare const _default: {
6
6
  directory: string | undefined;
7
7
  exclude: string[] | undefined;
8
8
  exports: boolean | undefined;
9
+ fix: boolean | undefined;
10
+ 'fix-type': string[] | undefined;
9
11
  help: boolean | undefined;
10
12
  'ignore-internal': boolean | undefined;
11
13
  include: string[] | undefined;
@@ -15,6 +15,8 @@ Options:
15
15
  --exclude Exclude provided issue type(s) from report, can be comma-separated or repeated (1)
16
16
  --dependencies Shortcut for --include dependencies,unlisted,binaries,unresolved
17
17
  --exports Shortcut for --include exports,nsExports,classMembers,types,nsTypes,enumMembers,duplicates
18
+ --fix Fix issues
19
+ --fix-type Fix only issues of type, can be comma-separated or repeated (2)
18
20
  --include-entry-exports Include entry files when reporting unused exports
19
21
  --isolate-workspaces Isolated workspaces in monorepo
20
22
  -n, --no-progress Don't show dynamic progress updates (automatically enabled in CI environments)
@@ -31,6 +33,7 @@ Options:
31
33
  -V, --version Print version
32
34
 
33
35
  (1) Issue types: files, dependencies, unlisted, unresolved, exports, nsExports, classMembers, types, nsTypes, enumMembers, duplicates
36
+ (2) Fixable issue types: dependencies, exports, types
34
37
 
35
38
  Examples:
36
39
 
@@ -51,6 +54,8 @@ try {
51
54
  directory: { type: 'string' },
52
55
  exclude: { type: 'string', multiple: true },
53
56
  exports: { type: 'boolean' },
57
+ fix: { type: 'boolean' },
58
+ 'fix-type': { type: 'string', multiple: true },
54
59
  help: { type: 'boolean', short: 'h' },
55
60
  'ignore-internal': { type: 'boolean' },
56
61
  include: { type: 'string', multiple: true },
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "3.6.0";
1
+ export declare const version = "3.7.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = '3.6.0';
1
+ export const version = '3.7.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knip",
3
- "version": "3.6.0",
3
+ "version": "3.7.0",
4
4
  "description": "Find unused files, dependencies and exports in your TypeScript and JavaScript projects",
5
5
  "homepage": "https://knip.dev",
6
6
  "repository": {
@@ -14,6 +14,16 @@
14
14
  "name": "Lars Kappert",
15
15
  "email": "lars@webpro.nl"
16
16
  },
17
+ "funding": [
18
+ {
19
+ "type": "github",
20
+ "url": "https://github.com/sponsors/webpro"
21
+ },
22
+ {
23
+ "type": "opencollective",
24
+ "url": "https://opencollective.com/webpro"
25
+ }
26
+ ],
17
27
  "main": "./dist/index.js",
18
28
  "bin": {
19
29
  "knip": "./bin/knip.js"
@@ -45,6 +55,7 @@
45
55
  "dependencies": {
46
56
  "@ericcornelissen/bash-parser": "0.5.2",
47
57
  "@npmcli/map-workspaces": "3.0.4",
58
+ "@npmcli/package-json": "5.0.0",
48
59
  "@pkgjs/parseargs": "0.11.0",
49
60
  "@pnpm/logger": "5.0.0",
50
61
  "@pnpm/workspace.pkgs-graph": "^2.0.11",
@@ -70,7 +81,6 @@
70
81
  "devDependencies": {
71
82
  "@jest/types": "29.6.3",
72
83
  "@knip/eslint-config": "0.0.0",
73
- "@npmcli/package-json": "5.0.0",
74
84
  "@release-it/bumper": "^6.0.1",
75
85
  "@swc/cli": "^0.1.63",
76
86
  "@swc/core": "^1.3.100",
package/schema.json CHANGED
@@ -96,17 +96,17 @@
96
96
  "rules": {
97
97
  "type": "object",
98
98
  "properties": {
99
- "files": { "$ref": "#/definitions/ruleValue" },
99
+ "classMembers": { "$ref": "#/definitions/ruleValue" },
100
100
  "dependencies": { "$ref": "#/definitions/ruleValue" },
101
- "unlisted": { "$ref": "#/definitions/ruleValue" },
102
- "unresolved": { "$ref": "#/definitions/ruleValue" },
101
+ "duplicates": { "$ref": "#/definitions/ruleValue" },
102
+ "enumMembers": { "$ref": "#/definitions/ruleValue" },
103
103
  "exports": { "$ref": "#/definitions/ruleValue" },
104
+ "files": { "$ref": "#/definitions/ruleValue" },
104
105
  "nsExports": { "$ref": "#/definitions/ruleValue" },
105
- "classMembers": { "$ref": "#/definitions/ruleValue" },
106
- "types": { "$ref": "#/definitions/ruleValue" },
107
106
  "nsTypes": { "$ref": "#/definitions/ruleValue" },
108
- "enumMembers": { "$ref": "#/definitions/ruleValue" },
109
- "duplicates": { "$ref": "#/definitions/ruleValue" }
107
+ "types": { "$ref": "#/definitions/ruleValue" },
108
+ "unlisted": { "$ref": "#/definitions/ruleValue" },
109
+ "unresolved": { "$ref": "#/definitions/ruleValue" }
110
110
  }
111
111
  }
112
112
  },
@@ -123,16 +123,18 @@
123
123
  "items": {
124
124
  "type": "string",
125
125
  "enum": [
126
- "files",
126
+ "binaries",
127
+ "classMembers",
127
128
  "dependencies",
128
- "unlisted",
129
+ "duplicates",
130
+ "enumMembers",
129
131
  "exports",
132
+ "files",
130
133
  "nsExports",
131
- "classMembers",
132
- "types",
133
134
  "nsTypes",
134
- "enumMembers",
135
- "duplicates"
135
+ "types",
136
+ "unlisted",
137
+ "unresolved"
136
138
  ]
137
139
  }
138
140
  },