knip 6.33.0 → 6.34.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.
@@ -1,6 +1,6 @@
1
1
  import type { ParseResult } from 'oxc-parser';
2
2
  import { CacheConsultant } from './CacheConsultant.ts';
3
- import type { AsyncCompilers, SyncCompilers } from './compilers/types.ts';
3
+ import type { AsyncCompilers, Compilers, SyncCompilers } from './compilers/types.ts';
4
4
  import type { GetImportsAndExportsOptions, IgnoreExportsUsedInFile, PluginVisitorContext, PluginVisitorObject } from './types/config.ts';
5
5
  import type { FileNode, ModuleGraph } from './types/module-graph.ts';
6
6
  import type { Paths } from './types/project.ts';
@@ -19,6 +19,8 @@ export declare class ProjectPrincipal {
19
19
  private _localRefsVisitor;
20
20
  syncCompilers: SyncCompilers;
21
21
  asyncCompilers: AsyncCompilers;
22
+ private scopedSyncCompilers;
23
+ private scopedAsyncCompilers;
22
24
  private paths;
23
25
  private rootDirs;
24
26
  private tsConfigFile;
@@ -26,14 +28,15 @@ export declare class ProjectPrincipal {
26
28
  cache: CacheConsultant<FileNode>;
27
29
  toSourceFilePath: ToSourceFilePath;
28
30
  private findWorkspacePackageTarget;
31
+ private findWorkspaceNameByFilePath;
29
32
  fileManager: SourceFileManager;
30
33
  private resolveModule;
31
34
  resolveGlobPattern: ResolveGlobPattern;
32
35
  resolvedFiles: Set<string>;
33
36
  deletedFiles: Set<string>;
34
37
  private onPathAdded;
35
- constructor(options: MainOptions, toSourceFilePath: ToSourceFilePath, findWorkspacePackageTarget?: WorkspacePackageTargetHandler);
36
- addCompilers(compilers: [SyncCompilers, AsyncCompilers]): void;
38
+ constructor(options: MainOptions, toSourceFilePath: ToSourceFilePath, findWorkspacePackageTarget: WorkspacePackageTargetHandler | undefined, findWorkspaceNameByFilePath: (filePath: string) => string | undefined);
39
+ addCompilers(workspaceName: string, compilers: Compilers): void;
37
40
  addPaths(paths: Paths, basePath: string, scope: string): void;
38
41
  addRootDirs(rootDirs: string[] | undefined, scope: string): void;
39
42
  init(): void;
@@ -30,6 +30,8 @@ export class ProjectPrincipal {
30
30
  _localRefsVisitor;
31
31
  syncCompilers = new Map();
32
32
  asyncCompilers = new Map();
33
+ scopedSyncCompilers = new Map();
34
+ scopedAsyncCompilers = new Map();
33
35
  paths = new Map();
34
36
  rootDirs = new Map();
35
37
  tsConfigFile;
@@ -37,16 +39,18 @@ export class ProjectPrincipal {
37
39
  cache;
38
40
  toSourceFilePath;
39
41
  findWorkspacePackageTarget;
42
+ findWorkspaceNameByFilePath;
40
43
  fileManager;
41
44
  resolveModule = () => undefined;
42
45
  resolveGlobPattern = pattern => [pattern];
43
46
  resolvedFiles = new Set();
44
47
  deletedFiles = new Set();
45
48
  onPathAdded;
46
- constructor(options, toSourceFilePath, findWorkspacePackageTarget) {
49
+ constructor(options, toSourceFilePath, findWorkspacePackageTarget, findWorkspaceNameByFilePath) {
47
50
  this.cache = new CacheConsultant('root', options);
48
51
  this.toSourceFilePath = toSourceFilePath;
49
52
  this.findWorkspacePackageTarget = findWorkspacePackageTarget;
53
+ this.findWorkspaceNameByFilePath = findWorkspaceNameByFilePath;
50
54
  this.tsConfigFile = options.tsConfigFile ? toAbsolute(options.tsConfigFile, options.cwd) : undefined;
51
55
  this.pluginVisitorObjects.push(createBunShellVisitor(this.pluginCtx));
52
56
  this.fileManager = new SourceFileManager({
@@ -54,16 +58,34 @@ export class ProjectPrincipal {
54
58
  });
55
59
  this.walkAndAnalyze = timerify(this.walkAndAnalyze.bind(this), 'walkAndAnalyze');
56
60
  }
57
- addCompilers(compilers) {
61
+ addCompilers(workspaceName, compilers) {
58
62
  for (const [ext, compiler] of compilers[0]) {
59
- if (!this.syncCompilers.has(ext)) {
60
- this.syncCompilers.set(ext, compiler);
63
+ const workspaceCompilers = this.scopedSyncCompilers.get(ext);
64
+ if (workspaceCompilers) {
65
+ workspaceCompilers.set(workspaceName, compiler);
66
+ }
67
+ else {
68
+ const workspaceCompilers = new Map([[workspaceName, compiler]]);
69
+ this.scopedSyncCompilers.set(ext, workspaceCompilers);
70
+ this.syncCompilers.set(ext, (source, filePath) => {
71
+ const owner = this.findWorkspaceNameByFilePath(filePath);
72
+ return ((owner ? workspaceCompilers.get(owner) : undefined) ?? compiler)(source, filePath);
73
+ });
61
74
  this.extensions.add(ext);
62
75
  }
63
76
  }
64
77
  for (const [ext, compiler] of compilers[1]) {
65
- if (!this.asyncCompilers.has(ext)) {
66
- this.asyncCompilers.set(ext, compiler);
78
+ const workspaceCompilers = this.scopedAsyncCompilers.get(ext);
79
+ if (workspaceCompilers) {
80
+ workspaceCompilers.set(workspaceName, compiler);
81
+ }
82
+ else {
83
+ const workspaceCompilers = new Map([[workspaceName, compiler]]);
84
+ this.scopedAsyncCompilers.set(ext, workspaceCompilers);
85
+ this.asyncCompilers.set(ext, (source, filePath) => {
86
+ const owner = this.findWorkspaceNameByFilePath(filePath);
87
+ return ((owner ? workspaceCompilers.get(owner) : undefined) ?? compiler)(source, filePath);
88
+ });
67
89
  this.extensions.add(ext);
68
90
  }
69
91
  }
package/dist/cli.js CHANGED
@@ -51,7 +51,7 @@ const main = async () => {
51
51
  isShowProgress: options.isShowProgress,
52
52
  isTreatConfigHintsAsErrors: options.isTreatConfigHintsAsErrors,
53
53
  isTreatTagHintsAsErrors: options.isTreatTagHintsAsErrors,
54
- maxShowIssues: args['max-show-issues'] ? Number(args['max-show-issues']) : undefined,
54
+ maxShowIssues: options.maxShowIssues,
55
55
  options: args['reporter-options'] ?? '',
56
56
  preprocessorOptions: args['preprocessor-options'] ?? '',
57
57
  selectedWorkspaces,
@@ -75,7 +75,7 @@ const main = async () => {
75
75
  perfObserver.reset();
76
76
  }
77
77
  if (!args['no-exit-code'] &&
78
- (totalErrorCount > Number(args['max-issues'] ?? 0) ||
78
+ (totalErrorCount > options.maxIssues ||
79
79
  (!options.isDisableConfigHints && options.isTreatConfigHintsAsErrors && configurationHints.length > 0) ||
80
80
  (!options.isDisableTagHints && options.isTreatTagHintsAsErrors && tagHints.size > 0))) {
81
81
  process.exitCode = 1;
@@ -191,7 +191,7 @@ export const analyze = async ({ analyzedFiles, counselor, chief, collector, depu
191
191
  }
192
192
  }
193
193
  }
194
- if (file.imports?.external) {
194
+ if (file.imports.external) {
195
195
  for (const extImport of file.imports.external) {
196
196
  const packageName = getPackageNameFromModuleSpecifier(extImport.specifier);
197
197
  const isHandled = packageName &&
@@ -200,7 +200,7 @@ export const analyze = async ({ analyzedFiles, counselor, chief, collector, depu
200
200
  isTypeOnly: extImport.isTypeOnly,
201
201
  isResolved: extImport.filePath !== undefined,
202
202
  });
203
- if (!isHandled)
203
+ if (!isHandled && !(extImport.jsDocTags?.size && shouldIgnoreTags(extImport.jsDocTags)))
204
204
  collector.addIssue({
205
205
  type: 'unlisted',
206
206
  filePath,
@@ -214,7 +214,7 @@ export const analyze = async ({ analyzedFiles, counselor, chief, collector, depu
214
214
  });
215
215
  }
216
216
  }
217
- if (file.imports?.unresolved) {
217
+ if (file.imports.unresolved) {
218
218
  for (const unresolvedImport of file.imports.unresolved) {
219
219
  const { specifier, pos, line, col } = unresolvedImport;
220
220
  collector.addIssue({
@@ -90,7 +90,7 @@ export async function build({ chief, collector, counselor, deputy, principal, is
90
90
  compilers[0].set(ext, compiler);
91
91
  };
92
92
  await worker.registerCompilers(registerCompiler);
93
- principal.addCompilers(compilers);
93
+ principal.addCompilers(name, compilers);
94
94
  const extensions = getCompilerExtensions(compilers);
95
95
  const extensionGlobStr = `.{${[...DEFAULT_EXTENSIONS, ...extensions].map(ext => ext.slice(1)).join(',')}}`;
96
96
  const config = chief.getConfigForWorkspace(name, extensions);
@@ -1,5 +1,6 @@
1
1
  import { toEntry, toProductionEntry } from '../../util/input.js';
2
2
  import { getScriptCommands } from '../../util/scripts.js';
3
+ import { createFsPromisesGlobVisitor } from './visitors/fsPromisesGlob.js';
3
4
  const title = 'Node.js';
4
5
  const isEnabled = () => true;
5
6
  const patterns = [
@@ -21,6 +22,9 @@ const hasNodeTest = (scripts) => {
21
22
  return result;
22
23
  };
23
24
  const entry = ['server.js'];
25
+ const registerVisitors = ({ ctx, registerVisitor }) => {
26
+ registerVisitor(createFsPromisesGlobVisitor(ctx));
27
+ };
24
28
  const resolve = options => {
25
29
  const entries = entry.map(id => toProductionEntry(id));
26
30
  if (hasNodeTest(options.manifest.scripts) || hasNodeTest(options.rootManifest?.scripts)) {
@@ -53,5 +57,6 @@ const plugin = {
53
57
  entry,
54
58
  resolve,
55
59
  args,
60
+ registerVisitors,
56
61
  };
57
62
  export default plugin;
@@ -0,0 +1,2 @@
1
+ import type { PluginVisitorContext, PluginVisitorObject } from '../../../types/config.ts';
2
+ export declare function createFsPromisesGlobVisitor(ctx: PluginVisitorContext): PluginVisitorObject;
@@ -0,0 +1,89 @@
1
+ import { findProperty, getPropertyKey } from '../../../typescript/ast-helpers.js';
2
+ import { getStringValue, isStringLiteral } from '../../../typescript/ast-nodes.js';
3
+ const FS_PROMISES = new Set(['node:fs/promises', 'fs/promises']);
4
+ export function createFsPromisesGlobVisitor(ctx) {
5
+ const globNames = new Set();
6
+ const namespaceNames = new Set();
7
+ return {
8
+ Program() {
9
+ globNames.clear();
10
+ namespaceNames.clear();
11
+ },
12
+ ImportDeclaration(node) {
13
+ if (!FS_PROMISES.has(node.source.value))
14
+ return;
15
+ for (const specifier of node.specifiers ?? []) {
16
+ if (specifier.type === 'ImportNamespaceSpecifier') {
17
+ namespaceNames.add(specifier.local.name);
18
+ }
19
+ else if (specifier.type === 'ImportSpecifier' &&
20
+ specifier.imported.type === 'Identifier' &&
21
+ specifier.imported.name === 'glob') {
22
+ globNames.add(specifier.local.name);
23
+ }
24
+ }
25
+ },
26
+ VariableDeclarator(node) {
27
+ if (node.init?.type !== 'CallExpression' ||
28
+ node.init.callee.type !== 'Identifier' ||
29
+ node.init.callee.name !== 'require' ||
30
+ !isStringLiteral(node.init.arguments[0]) ||
31
+ !FS_PROMISES.has(getStringValue(node.init.arguments[0])))
32
+ return;
33
+ if (node.id.type === 'Identifier') {
34
+ namespaceNames.add(node.id.name);
35
+ }
36
+ else if (node.id.type === 'ObjectPattern') {
37
+ for (const property of node.id.properties) {
38
+ if (property.type === 'Property' &&
39
+ getPropertyKey(property) === 'glob' &&
40
+ property.value.type === 'Identifier') {
41
+ globNames.add(property.value.name);
42
+ }
43
+ }
44
+ }
45
+ },
46
+ CallExpression(node) {
47
+ let isGlobCall = node.callee.type === 'Identifier' && globNames.has(node.callee.name);
48
+ if (!isGlobCall &&
49
+ node.callee.type === 'MemberExpression' &&
50
+ !node.callee.computed &&
51
+ node.callee.object.type === 'Identifier' &&
52
+ namespaceNames.has(node.callee.object.name) &&
53
+ node.callee.property.type === 'Identifier' &&
54
+ node.callee.property.name === 'glob') {
55
+ isGlobCall = true;
56
+ }
57
+ if (!isGlobCall)
58
+ return;
59
+ const arg = node.arguments[0];
60
+ if (!arg)
61
+ return;
62
+ let patterns;
63
+ if (isStringLiteral(arg)) {
64
+ patterns = [getStringValue(arg)];
65
+ }
66
+ else if (arg.type === 'ArrayExpression') {
67
+ patterns = [];
68
+ for (const element of arg.elements) {
69
+ if (!element || !isStringLiteral(element))
70
+ return;
71
+ patterns.push(getStringValue(element));
72
+ }
73
+ }
74
+ if (!patterns?.length || patterns.some(pattern => pattern.startsWith('!')))
75
+ return;
76
+ const options = node.arguments[1];
77
+ if (options && options.type !== 'ObjectExpression')
78
+ return;
79
+ if (options &&
80
+ (options.properties.length > 1 ||
81
+ options.properties.some(property => property.type !== 'Property' || property.computed || getPropertyKey(property) !== 'cwd')))
82
+ return;
83
+ const cwdNode = findProperty(options, 'cwd');
84
+ if (cwdNode && !isStringLiteral(cwdNode))
85
+ return;
86
+ ctx.addImportGlob(patterns, { cwd: cwdNode ? getStringValue(cwdNode) : '.' });
87
+ },
88
+ };
89
+ }
@@ -87,18 +87,18 @@ const addAppEntries = (inputs, srcDir, serverDir, config, dir) => {
87
87
  inputs.push(toDeferResolveProductionEntry(resolveAlias(id, srcDir, dir)));
88
88
  };
89
89
  const findLayerConfigs = (cwd) => _syncGlob({ cwd, patterns: [`layers/*/${config.at(0)}`] });
90
+ const definitionFiles = [
91
+ '.nuxt/imports.d.ts',
92
+ '.nuxt/components.d.ts',
93
+ '.nuxt/types/nitro-routes.d.ts',
94
+ '.nuxt/types/nitro-imports.d.ts',
95
+ ];
90
96
  const registerCompilers = async ({ cwd, hasDependency, registerCompiler }) => {
91
- if (hasDependency('nuxt') || hasDependency('nuxt-nightly')) {
97
+ if (hasDependency('nuxt') || hasDependency('nuxt-nightly') || isDirectory(cwd, '.nuxt')) {
98
+ const paths = definitionFiles.map(file => join(cwd, file));
92
99
  const maps = createAutoImportMaps();
93
- const definitionFiles = [
94
- '.nuxt/imports.d.ts',
95
- '.nuxt/components.d.ts',
96
- '.nuxt/types/nitro-routes.d.ts',
97
- '.nuxt/types/nitro-imports.d.ts',
98
- ];
99
- for (const file of definitionFiles) {
100
- const path = join(cwd, file);
101
- buildAutoImportMap(path, readAndParseFile(path), maps, file.endsWith('components.d.ts'));
100
+ for (const path of paths) {
101
+ buildAutoImportMap(path, readAndParseFile(path), maps, path.endsWith('components.d.ts'));
102
102
  }
103
103
  registerCompiler({ extension: '.vue', compiler: createVueCompiler(maps, cwd) });
104
104
  registerCompiler({ extension: '.ts', compiler: createTsCompiler(maps) });
@@ -121,6 +121,9 @@ const resolveConfig = async (localConfig, options) => {
121
121
  addModule(id);
122
122
  }
123
123
  addAppEntries(inputs, srcDir, serverDir, localConfig, cwd);
124
+ const sharedDir = toAbsolute(localConfig.dir?.shared ?? 'shared', cwd);
125
+ inputs.push(toAlias('#shared', sharedDir));
126
+ inputs.push(toAlias('#shared/*', join(sharedDir, '*'), { dir: cwd }));
124
127
  const aliases = localConfig.alias;
125
128
  if (aliases) {
126
129
  for (const key in aliases) {
package/dist/run.js CHANGED
@@ -60,7 +60,7 @@ export const run = async (options) => {
60
60
  const isGitIgnored = await getGitIgnoredHandler(options, new Set(workspaces.map(w => w.dir)));
61
61
  const toSourceFilePath = getModuleSourcePathHandler(chief);
62
62
  const findWorkspacePackageTarget = getWorkspacePackageTargetHandler(chief);
63
- const principal = new ProjectPrincipal(options, toSourceFilePath, findWorkspacePackageTarget);
63
+ const principal = new ProjectPrincipal(options, toSourceFilePath, findWorkspacePackageTarget, filePath => chief.findWorkspaceByFilePath(filePath)?.name);
64
64
  collector.setWorkspaceFilter(chief.workspaceFilePathFilter);
65
65
  collector.setSelectedWorkspaces(chief.selectedWorkspaces);
66
66
  collector.setIgnoreIssues(chief.getIgnoreIssues());
@@ -141,6 +141,7 @@ export type PluginVisitorContext = {
141
141
  markImportExpressionHandled: (pos: number) => void;
142
142
  addImportGlob: (patterns: string[], options?: {
143
143
  base?: string;
144
+ cwd?: string;
144
145
  filter?: RegExp;
145
146
  }) => void;
146
147
  markExportRegistered: (name: string) => void;
@@ -30,6 +30,7 @@ export interface Import extends Position {
30
30
  readonly identifier: string | undefined;
31
31
  readonly isTypeOnly: boolean;
32
32
  readonly modifiers: number;
33
+ readonly jsDocTags: Tags | undefined;
33
34
  }
34
35
  export interface ExternalRef {
35
36
  readonly specifier: string;
@@ -76,6 +77,7 @@ export type FileNode = {
76
77
  export interface ImportGlob {
77
78
  patterns: string[];
78
79
  base?: string;
80
+ cwd?: string;
79
81
  filter?: RegExp;
80
82
  }
81
83
  export type ModuleGraph = Map<FilePath, FileNode>;
@@ -62,6 +62,7 @@ const getImportsAndExports = (filePath, sourceText, resolveModule, options, igno
62
62
  col: opts.col,
63
63
  isTypeOnly: isDts || !!(modifiers & IMPORT_FLAGS.TYPE_ONLY),
64
64
  modifiers,
65
+ jsDocTags: undefined,
65
66
  });
66
67
  const file = internal.get(importFilePath);
67
68
  const importMaps = file ?? createImports();
@@ -146,6 +147,7 @@ const getImportsAndExports = (filePath, sourceText, resolveModule, options, igno
146
147
  col,
147
148
  isTypeOnly: isDts || !!(modifiers & IMPORT_FLAGS.TYPE_ONLY),
148
149
  modifiers,
150
+ jsDocTags,
149
151
  });
150
152
  }
151
153
  }
@@ -171,6 +173,7 @@ const getImportsAndExports = (filePath, sourceText, resolveModule, options, igno
171
173
  col,
172
174
  isTypeOnly: isDts || !!(modifiers & IMPORT_FLAGS.TYPE_ONLY),
173
175
  modifiers,
176
+ jsDocTags: undefined,
174
177
  });
175
178
  }
176
179
  }
@@ -280,7 +283,7 @@ const getImportsAndExports = (filePath, sourceText, resolveModule, options, igno
280
283
  pluginCtx.addScript = (s) => scripts.add(s);
281
284
  pluginCtx.addImport = (spec, pos, mod) => addImport(spec, undefined, undefined, undefined, pos, mod);
282
285
  pluginCtx.markImportExpressionHandled = (pos) => handledImportExpressions.add(pos);
283
- pluginCtx.addImportGlob = (patterns, opts) => importGlobs.push({ patterns, base: opts?.base, filter: opts?.filter });
286
+ pluginCtx.addImportGlob = (patterns, opts) => importGlobs.push({ patterns, base: opts?.base, cwd: opts?.cwd, filter: opts?.filter });
284
287
  pluginCtx.markExportRegistered = (name) => registeredCustomElements.add(name);
285
288
  }
286
289
  const localRefs = _walkAST(result.program, sourceText, filePath, result.module.hasModuleSyntax, {
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { _syncGlob } from '../util/glob.js';
3
- import { dirname, isAbsolute, join, toRelative } from '../util/path.js';
3
+ import { dirname, isAbsolute, join, toAbsolute, toRelative } from '../util/path.js';
4
4
  function resolveBaseDir(base, dir, resolveGlobPattern) {
5
5
  if (base.startsWith('.'))
6
6
  return join(dir, base);
@@ -15,8 +15,13 @@ function resolveBaseDir(base, dir, resolveGlobPattern) {
15
15
  export function resolveImportGlobs(items, containingFile, resolveGlobPattern, workspaceRoot) {
16
16
  const filePaths = [];
17
17
  const dir = dirname(containingFile);
18
- for (const { patterns, base, filter } of items) {
19
- if (base !== undefined) {
18
+ for (const { patterns, base, cwd, filter } of items) {
19
+ if (cwd !== undefined) {
20
+ for (const filePath of _syncGlob({ patterns, cwd: toAbsolute(cwd, workspaceRoot) })) {
21
+ filePaths.push(filePath);
22
+ }
23
+ }
24
+ else if (base !== undefined) {
20
25
  const cwd = resolveBaseDir(base, dir, resolveGlobPattern);
21
26
  if (!cwd)
22
27
  continue;
@@ -256,7 +256,7 @@ export function handleNewExpression(node, s) {
256
256
  node.arguments[1].property.name === 'url') {
257
257
  const specifier = getStringValue(node.arguments[0]);
258
258
  if (specifier)
259
- s.addImport(specifier, undefined, undefined, undefined, node.arguments[0].start, IMPORT_FLAGS.ENTRY | IMPORT_FLAGS.OPTIONAL);
259
+ s.addImport(specifier, undefined, undefined, undefined, node.arguments[0].start, IMPORT_FLAGS.ENTRY | IMPORT_FLAGS.OPTIONAL, undefined, s.getJSDocTags(node.start));
260
260
  return;
261
261
  }
262
262
  if (s.hasWorkerThreadsImport &&
@@ -1,4 +1,4 @@
1
- import { Visitor, type Class, type Function as FunctionNode, type Program, type Span, type VariableDeclarator } from 'oxc-parser';
1
+ import { Visitor, type Class, type Function as FunctionNode, type Program, type Span, type TSInterfaceDeclaration, type VariableDeclarator } from 'oxc-parser';
2
2
  import type { PluginVisitorObject } from '../../types/config.ts';
3
3
  import type { GetImportsAndExportsOptions } from '../../types/config.ts';
4
4
  import type { Fix } from '../../types/exports.ts';
@@ -67,10 +67,12 @@ export interface WalkState extends WalkContext {
67
67
  scopeEnds: number[];
68
68
  shadowScopes: Map<string, [number, number][]>;
69
69
  localDeclarations: Map<string, FunctionNode | Class | VariableDeclarator>;
70
+ localInterfaces: Map<string, TSInterfaceDeclaration | null>;
70
71
  pendingCallRefs: Array<{
71
72
  name: string;
72
73
  exportName: string;
73
74
  seen: Set<string>;
75
+ kind: 'callee' | 'argument';
74
76
  }>;
75
77
  pendingMemberCallRefs: Array<{
76
78
  objectName: string;
@@ -49,7 +49,7 @@ const _addExport = (identifier, type, pos, members, fix, isReExport, jsDocTags)
49
49
  });
50
50
  }
51
51
  };
52
- const _collectRefsInType = (node, exportName, signatureOnly, seen = new Set(), inBody = false) => {
52
+ const _collectRefsInType = (node, exportName, signatureOnly, seen = new Set(), inBody = false, thisType) => {
53
53
  if (!node)
54
54
  return;
55
55
  const type = node.type;
@@ -64,10 +64,14 @@ const _collectRefsInType = (node, exportName, signatureOnly, seen = new Set(), i
64
64
  if (node.typeName?.type === 'Identifier')
65
65
  _addRefInExport(node.typeName.name, exportName);
66
66
  break;
67
+ case 'TSThisType':
68
+ if (thisType)
69
+ _addRefInExport(thisType, exportName);
70
+ return;
67
71
  case 'CallExpression': {
68
72
  const callee = node.callee;
69
73
  if (callee?.type === 'Identifier') {
70
- state.pendingCallRefs.push({ name: callee.name, exportName, seen });
74
+ state.pendingCallRefs.push({ name: callee.name, exportName, seen, kind: 'callee' });
71
75
  }
72
76
  else if (callee?.type === 'MemberExpression' &&
73
77
  !callee.computed &&
@@ -84,8 +88,9 @@ const _collectRefsInType = (node, exportName, signatureOnly, seen = new Set(), i
84
88
  const args = node.arguments;
85
89
  if (args) {
86
90
  for (const arg of args) {
87
- if (arg?.type === 'Identifier')
88
- state.pendingCallRefs.push({ name: arg.name, exportName, seen });
91
+ if (arg?.type === 'Identifier') {
92
+ state.pendingCallRefs.push({ name: arg.name, exportName, seen, kind: 'argument' });
93
+ }
89
94
  }
90
95
  }
91
96
  }
@@ -101,14 +106,14 @@ const _collectRefsInType = (node, exportName, signatureOnly, seen = new Set(), i
101
106
  case 'TSSatisfiesExpression':
102
107
  if (inBody) {
103
108
  if (node.expression)
104
- _collectRefsInType(node.expression, exportName, signatureOnly, seen, inBody);
109
+ _collectRefsInType(node.expression, exportName, signatureOnly, seen, inBody, thisType);
105
110
  return;
106
111
  }
107
112
  break;
108
113
  case 'VariableDeclarator':
109
114
  if (inBody) {
110
115
  if (node.init)
111
- _collectRefsInType(node.init, exportName, signatureOnly, seen, inBody);
116
+ _collectRefsInType(node.init, exportName, signatureOnly, seen, inBody, thisType);
112
117
  return;
113
118
  }
114
119
  break;
@@ -124,14 +129,63 @@ const _collectRefsInType = (node, exportName, signatureOnly, seen = new Set(), i
124
129
  if (Array.isArray(val)) {
125
130
  for (const item of val) {
126
131
  if (item)
127
- _collectRefsInType(item, exportName, signatureOnly, seen, childInBody);
132
+ _collectRefsInType(item, exportName, signatureOnly, seen, childInBody, thisType);
128
133
  }
129
134
  }
130
135
  else {
131
- _collectRefsInType(val, exportName, signatureOnly, seen, childInBody);
136
+ _collectRefsInType(val, exportName, signatureOnly, seen, childInBody, thisType);
132
137
  }
133
138
  }
134
139
  };
140
+ const _collectRefsInDirectMemberResult = (fn, exportName, seen) => {
141
+ const body = fn.body;
142
+ if (body?.type !== 'MemberExpression' ||
143
+ body.computed ||
144
+ body.object?.type !== 'Identifier' ||
145
+ body.property?.type !== 'Identifier') {
146
+ return false;
147
+ }
148
+ const params = Array.isArray(fn.params) ? fn.params : (fn.params?.items ?? []);
149
+ const param = params.find((param) => param.type === 'Identifier' && param.name === body.object.name);
150
+ const paramType = param?.typeAnnotation?.typeAnnotation;
151
+ if (paramType?.type !== 'TSTypeReference' || paramType.typeName?.type !== 'Identifier' || paramType.typeArguments) {
152
+ return false;
153
+ }
154
+ const declaration = state.localInterfaces.get(paramType.typeName.name);
155
+ if (!declaration || declaration.typeParameters)
156
+ return false;
157
+ const members = declaration.body.body.filter((member) => member.type === 'TSPropertySignature' &&
158
+ !member.computed &&
159
+ member.key?.type === 'Identifier' &&
160
+ member.key.name === body.property.name &&
161
+ Boolean(member.typeAnnotation));
162
+ if (members.length !== 1)
163
+ return false;
164
+ _collectRefsInType(members[0].typeAnnotation, exportName, true, seen, false, paramType.typeName.name);
165
+ return true;
166
+ };
167
+ const _collectRefsInCallResult = (node, exportName, seen) => {
168
+ const annotatedType = node.type === 'VariableDeclarator' ? node.id.typeAnnotation?.typeAnnotation : undefined;
169
+ if (annotatedType?.type === 'TSFunctionType') {
170
+ _collectRefsInType(annotatedType.typeParameters, exportName, true, seen);
171
+ return _collectRefsInType(annotatedType.returnType, exportName, true, seen);
172
+ }
173
+ if (annotatedType)
174
+ return _collectRefsInType(annotatedType, exportName, true, seen);
175
+ const fn = node.type === 'VariableDeclarator' ? node.init : node;
176
+ if (fn?.type !== 'ArrowFunctionExpression' &&
177
+ fn?.type !== 'FunctionDeclaration' &&
178
+ fn?.type !== 'FunctionExpression') {
179
+ return _collectRefsInType(node, exportName, true, seen);
180
+ }
181
+ if (fn.returnType) {
182
+ _collectRefsInType(fn.typeParameters, exportName, true, seen);
183
+ return _collectRefsInType(fn.returnType, exportName, true, seen);
184
+ }
185
+ if (_collectRefsInDirectMemberResult(fn, exportName, seen))
186
+ return;
187
+ _collectRefsInType(fn, exportName, true, seen);
188
+ };
135
189
  const _addRefInExport = (name, exportName) => {
136
190
  const refs = state.referencedInExport.get(name);
137
191
  if (refs)
@@ -221,6 +275,12 @@ const coreVisitorObject = {
221
275
  state.addImport(specifier, name, undefined, undefined, node.id.start, IMPORT_FLAGS.TYPE_ONLY | IMPORT_FLAGS.AUGMENT);
222
276
  }
223
277
  },
278
+ TSInterfaceDeclaration(node) {
279
+ if (state.scopeDepth > 0 || state.isInNamespace(node))
280
+ return;
281
+ const name = node.id.name;
282
+ state.localInterfaces.set(name, state.localInterfaces.has(name) ? null : node);
283
+ },
224
284
  ClassDeclaration(node) {
225
285
  state.classNameStack.push(node.id?.name ?? '');
226
286
  if (node.id?.name) {
@@ -740,6 +800,7 @@ function walkAST(program, sourceText, filePath, hasModuleSyntax, ctx) {
740
800
  scopeEnds: [],
741
801
  shadowScopes: new Map(),
742
802
  localDeclarations: new Map(),
803
+ localInterfaces: new Map(),
743
804
  pendingCallRefs: [],
744
805
  pendingMemberCallRefs: [],
745
806
  localToExports: new Map(),
@@ -756,14 +817,18 @@ function walkAST(program, sourceText, filePath, hasModuleSyntax, ctx) {
756
817
  ctx.visitor.visit(program);
757
818
  while (state.pendingCallRefs.length > 0 || state.pendingMemberCallRefs.length > 0) {
758
819
  while (state.pendingCallRefs.length > 0) {
759
- const { name, exportName, seen } = state.pendingCallRefs.pop();
760
- if (seen.has(name))
820
+ const { name, exportName, seen, kind } = state.pendingCallRefs.pop();
821
+ const key = `${kind}:${name}`;
822
+ if (seen.has(key))
761
823
  continue;
762
824
  const decl = state.localDeclarations.get(name);
763
825
  if (!decl)
764
826
  continue;
765
- seen.add(name);
766
- _collectRefsInType(decl, exportName, true, seen);
827
+ seen.add(key);
828
+ if (kind === 'callee')
829
+ _collectRefsInCallResult(decl, exportName, seen);
830
+ else
831
+ _collectRefsInType(decl, exportName, true, seen);
767
832
  }
768
833
  while (state.pendingMemberCallRefs.length > 0) {
769
834
  const { objectName, propertyName, exportName, seen } = state.pendingMemberCallRefs.pop();
@@ -780,7 +845,7 @@ function walkAST(program, sourceText, filePath, hasModuleSyntax, ctx) {
780
845
  if (fn.type !== 'ArrowFunctionExpression' && fn.type !== 'FunctionExpression')
781
846
  continue;
782
847
  seen.add(key);
783
- _collectRefsInType(fn, exportName, true, seen);
848
+ _collectRefsInCallResult(fn, exportName, seen);
784
849
  }
785
850
  }
786
851
  for (let i = 0; i < state.memberRefsInFile.length; i += 2) {
@@ -1,5 +1,6 @@
1
1
  export declare const helpText = "\u2702\uFE0F Find unused dependencies, exports and files in your JavaScript and TypeScript projects\n\nUsage: knip [options]\n\nOptions:\n -h, --help Print this help text\n -V, --version Print version\n -n, --no-progress Don't show dynamic progress updates (automatically enabled in CI environments)\n -c, --config [file] Configuration file path\n (default: [.]knip.json[c], knip.(js|ts), knip.config.(js|ts) or package.json#knip)\n --use-tsconfig-files Use tsconfig.json to define project files (override `project` patterns)\n -t, --tsConfig [file] TypeScript configuration path (default: tsconfig.json)\n\nMode\n --cache Enable caching\n --cache-location Change cache location (default: node_modules/.cache/knip)\n --include-entry-exports Include entry files when reporting unused exports\n --no-gitignore Don't respect .gitignore\n -p, --production Analyze only production source files (e.g. no test files, devDependencies)\n -s, --strict Consider only direct dependencies of workspace (not devDependencies, not other workspaces)\n -w, --watch Watch mode\n\nScope\n -W, --workspace [filter] Filter workspaces by name, directory, or glob (can be repeated)\n -D, --directory [dir] Run process from a different directory (default: cwd)\n --include Include 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,catalog,catalogReferences\n --exports Shortcut for --include exports,nsExports,types,nsTypes,enumMembers,namespaceMembers,duplicates\n --files Shortcut for --include files\n --cycles Shortcut for --include cycles (circular dependencies)\n --tags Include or exclude tagged exports\n\nFix\n -f, --fix Fix issues (modifies files in your repo)\n --fix-type Fix only issues of type, can be comma-separated or repeated (2)\n --allow-remove-files Allow Knip to remove files (with --fix)\n -F, --format Format modified files after --fix using the local formatter\n\nOutput\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 (default: symbols), can be repeated (3)\n --reporter-options Pass extra options to the reporter (as JSON string, see example)\n --no-config-hints Suppress configuration hints\n --no-tag-hints Suppress tag hints\n --treat-config-hints-as-errors Exit with non-zero code (1) if there are any configuration hints\n --treat-tag-hints-as-errors Exit with non-zero code (1) if there are any tag hints\n --max-issues Maximum number of total issues before non-zero exit code (default: 0)\n --max-show-issues Maximum number of issues to display per type\n --no-exit-code Always exit with code zero (0)\n\nTroubleshooting\n -d, --debug Show debug output\n --memory Measure memory usage and display data table\n --memory-realtime Log memory usage in realtime\n --performance Measure count and running time of key functions and display stats table\n --performance-fn [name] Measure only function [name]\n -u, --duration Print total running time (zero overhead, no instrumentation)\n --trace Show trace output\n --trace-dependency [name] Show files that import the named dependency\n --trace-export [name] Show trace output for named export(s)\n --trace-file [file] Show trace output for exports in file\n\n(1) Issue types: files, dependencies, unlisted, unresolved, exports, nsExports, types, nsTypes, enumMembers, namespaceMembers, duplicates, catalog, catalogReferences, cycles\n(2) Fixable issue types: dependencies, exports, types, files, catalog\n(3) Built-in reporters: symbols (default), compact, codeowners, cycles, json, codeclimate, markdown, disclosure, github-actions, sarif\n\nExamples:\n\n$ knip\n$ knip --production\n$ knip --workspace packages/client --include files,dependencies\n$ knip --workspace @myorg/* --workspace '!@myorg/legacy'\n$ knip --workspace './apps/*' --workspace '@shared/utils'\n$ knip -c ./config/knip.json --reporter compact\n$ knip --reporter codeowners --reporter-options '{\"path\":\".github/CODEOWNERS\"}'\n$ knip --tags=-lintignore\n\nWebsite: https://knip.dev";
2
2
  export type ParsedCLIArgs = ReturnType<typeof parseCLIArgs>;
3
+ export declare const parseNumericOption: (value: string | undefined, option: string) => number | undefined;
3
4
  export default function parseCLIArgs(): {
4
5
  cache?: boolean | undefined;
5
6
  'cache-location'?: string | undefined;
@@ -1,4 +1,5 @@
1
1
  import { parseArgs } from 'node:util';
2
+ import { ConfigurationError } from './errors.js';
2
3
  export const helpText = `✂️ Find unused dependencies, exports and files in your JavaScript and TypeScript projects
3
4
 
4
5
  Usage: knip [options]
@@ -79,6 +80,14 @@ $ knip --reporter codeowners --reporter-options '{"path":".github/CODEOWNERS"}'
79
80
  $ knip --tags=-lintignore
80
81
 
81
82
  Website: https://knip.dev`;
83
+ export const parseNumericOption = (value, option) => {
84
+ if (value === undefined)
85
+ return undefined;
86
+ if (!/^\d+$/.test(value)) {
87
+ throw new ConfigurationError(`Option --${option} expects a non-negative integer, got: ${value}`);
88
+ }
89
+ return Number(value);
90
+ };
82
91
  export default function parseCLIArgs() {
83
92
  return parseArgs({
84
93
  options: {
@@ -1,5 +1,5 @@
1
1
  import type { Options } from '../types/options.ts';
2
- import type { ParsedCLIArgs } from './cli-arguments.ts';
2
+ import { type ParsedCLIArgs } from './cli-arguments.ts';
3
3
  interface CreateOptions extends Partial<Options> {
4
4
  args?: ParsedCLIArgs;
5
5
  }
@@ -43,6 +43,7 @@ export declare const createOptions: (options: CreateOptions) => Promise<{
43
43
  isTreatTagHintsAsErrors: boolean;
44
44
  isUseTscFiles: boolean | undefined;
45
45
  isWatch: boolean;
46
+ maxIssues: number;
46
47
  maxShowIssues: number | undefined;
47
48
  parsedConfig: {
48
49
  angular?: string | boolean | string[] | {
@@ -2,6 +2,7 @@ import { partitionCompilers } from '../compilers/index.js';
2
2
  import { ISSUE_TYPES, KNIP_CONFIG_LOCATIONS } from '../constants.js';
3
3
  import { knipConfigurationSchema } from '../schema/configuration.js';
4
4
  import { getCatalogContainer } from './catalog.js';
5
+ import { parseNumericOption } from './cli-arguments.js';
5
6
  import { ConfigurationError } from './errors.js';
6
7
  import { findFile, loadJSON } from './fs.js';
7
8
  import { getIncludedIssueTypes, shorthandCycles, shorthandDeps, shorthandExports, shorthandFiles, } from './get-included-issue-types.js';
@@ -150,7 +151,8 @@ export const createOptions = async (options) => {
150
151
  isTreatTagHintsAsErrors: args['treat-tag-hints-as-errors'] ?? parsedConfig.treatTagHintsAsErrors ?? false,
151
152
  isUseTscFiles: options.isUseTscFiles ?? args['use-tsconfig-files'] ?? (options.isSession && !configFilePath),
152
153
  isWatch: args.watch ?? options.isWatch ?? false,
153
- maxShowIssues: args['max-show-issues'] ? Number(args['max-show-issues']) : undefined,
154
+ maxIssues: parseNumericOption(args['max-issues'], 'max-issues') ?? 0,
155
+ maxShowIssues: parseNumericOption(args['max-show-issues'], 'max-show-issues'),
154
156
  parsedConfig,
155
157
  rules,
156
158
  tags,
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "6.33.0";
1
+ export declare const version = "6.34.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = '6.33.0';
1
+ export const version = '6.34.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knip",
3
- "version": "6.33.0",
3
+ "version": "6.34.0",
4
4
  "description": "Find and fix unused dependencies, exports and files in your TypeScript and JavaScript projects",
5
5
  "keywords": [
6
6
  "analysis",
@@ -96,14 +96,14 @@
96
96
  "zod": "^4.4.3"
97
97
  },
98
98
  "devDependencies": {
99
- "@jest/types": "^30.4.1",
99
+ "@jest/types": "^30.5.0",
100
100
  "@types/bun": "^1.4.0",
101
101
  "@types/picomatch": "^4.0.3",
102
- "@types/webpack": "^5.28.5",
103
102
  "codeclimate-types": "^0.3.1",
104
103
  "prettier": "^3.9.6",
105
104
  "tsx": "^4.23.12",
106
- "typescript": "7.0.2"
105
+ "typescript": "7.0.2",
106
+ "webpack": "^5.110.1"
107
107
  },
108
108
  "engines": {
109
109
  "node": "^20.19.0 || >=22.12.0"