knip 3.6.1 → 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;
@@ -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}`);
@@ -124,6 +124,8 @@ export class ProjectPrincipal {
124
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)
@@ -351,6 +357,10 @@ export const main = async (unresolvedConfiguration) => {
351
357
  symbolType: exportedItem.type,
352
358
  ...principal.getPos(exportedItem.node, exportedItem.pos),
353
359
  });
360
+ if (isType)
361
+ fixer.addUnusedTypeNode(filePath, exportedItem.fix);
362
+ else
363
+ fixer.addUnusedExportNode(filePath, exportedItem.fix);
354
364
  }
355
365
  }
356
366
  }
@@ -374,6 +384,9 @@ export const main = async (unresolvedConfiguration) => {
374
384
  const unusedIgnoredWorkspaces = chief.getUnusedIgnoredWorkspaces();
375
385
  unusedIgnoredWorkspaces.forEach(identifier => collector.addConfigurationHint({ type: 'ignoreWorkspaces', identifier }));
376
386
  const { issues, counters, configurationHints } = collector.getIssues();
387
+ if (isFix) {
388
+ await fixer.fixIssues(issues);
389
+ }
377
390
  streamer.clear();
378
391
  return { report, issues, counters, rules, configurationHints };
379
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,6 +18,7 @@ export type AddImportOptions = {
16
18
  };
17
19
  export type AddExportOptions = ExportItem & {
18
20
  identifier: string;
21
+ fix: ExportPos;
19
22
  };
20
23
  export declare const _getImportsAndExports: (sourceFile: BoundSourceFile, getResolvedModule: GetResolvedModule, options: GetImportsAndExportsOptions) => {
21
24
  imports: {
@@ -95,7 +95,7 @@ const getImportsAndExports = (sourceFile, getResolvedModule, options) => {
95
95
  }
96
96
  }
97
97
  };
98
- const addExport = ({ node, identifier, type, pos, posDecl, members = [] }) => {
98
+ const addExport = ({ node, identifier, type, pos, posDecl, members = [], fix }) => {
99
99
  if (options.skipExports)
100
100
  return;
101
101
  const jsDocTags = getJSDocTags(node);
@@ -106,7 +106,7 @@ const getImportsAndExports = (sourceFile, getResolvedModule, options) => {
106
106
  exports.set(identifier, { ...item, members: crew, jsDocTags: tags });
107
107
  }
108
108
  else {
109
- exports.set(identifier, { node, type, members, jsDocTags, pos, posDecl: posDecl ?? pos });
109
+ exports.set(identifier, { node, type, members, jsDocTags, pos, posDecl: posDecl ?? pos, fix });
110
110
  }
111
111
  if (!jsDocTags.has('@alias')) {
112
112
  if (ts.isExportAssignment(node))
@@ -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.1";
1
+ export declare const version = "3.7.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = '3.6.1';
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.1",
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": {
@@ -55,6 +55,7 @@
55
55
  "dependencies": {
56
56
  "@ericcornelissen/bash-parser": "0.5.2",
57
57
  "@npmcli/map-workspaces": "3.0.4",
58
+ "@npmcli/package-json": "5.0.0",
58
59
  "@pkgjs/parseargs": "0.11.0",
59
60
  "@pnpm/logger": "5.0.0",
60
61
  "@pnpm/workspace.pkgs-graph": "^2.0.11",
@@ -80,7 +81,6 @@
80
81
  "devDependencies": {
81
82
  "@jest/types": "29.6.3",
82
83
  "@knip/eslint-config": "0.0.0",
83
- "@npmcli/package-json": "5.0.0",
84
84
  "@release-it/bumper": "^6.0.1",
85
85
  "@swc/cli": "^0.1.63",
86
86
  "@swc/core": "^1.3.100",