knip 1.0.0-alpha.0 → 1.0.0-alpha.2

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.
package/README.md CHANGED
@@ -20,8 +20,8 @@ The dots don't connect themselves. This is where Knip comes in:
20
20
  - [x] Finds used dependencies not listed in `package.json`.
21
21
  - [x] Finds duplicate exports.
22
22
  - [x] Finds unused members of classes and enums
23
- - [x] Supports workspaces/monorepos
24
- - [x] Growing list of plugins (= less to configure and custom dependency resolvers)
23
+ - [x] Built-in support for monorepos (workspaces)
24
+ - [x] Growing list of built-in plugins (= less to configure and custom dependency resolvers)
25
25
  - [x] Supports JavaScript (without `tsconfig.json`, or TypeScript `allowJs: true`).
26
26
  - [x] Features multiple [reporters][1] and supports [custom reporters][2] (think JSON and `CODEOWNERS`)
27
27
  - [x] Run Knip as part of your CI environment to detect issues and prevent regressions.
@@ -18,8 +18,8 @@ const defaultConfig = {
18
18
  ignoreWorkspaces: [],
19
19
  workspaces: {
20
20
  [ROOT_WORKSPACE_NAME]: {
21
- entryFiles: ['index.{js,ts}', 'src/index.{js,ts}'],
22
- projectFiles: ['**/*.ts'],
21
+ entryFiles: ['index.{js,ts,tsx}', 'src/index.{js,ts,tsx}'],
22
+ projectFiles: ['**/*.{js,ts,tsx}'],
23
23
  ignore: [],
24
24
  paths: {},
25
25
  },
@@ -103,10 +103,12 @@ export default class ConfigurationChief {
103
103
  const config = isObject ? arrayify(pluginConfig.config) : arrayify(pluginConfig);
104
104
  const entryFiles = isObject && 'entryFiles' in pluginConfig ? arrayify(pluginConfig.entryFiles) : [];
105
105
  const projectFiles = isObject && 'projectFiles' in pluginConfig ? arrayify(pluginConfig.projectFiles) : entryFiles;
106
+ const sampleFiles = isObject && 'sampleFiles' in pluginConfig ? arrayify(pluginConfig.sampleFiles) : [];
106
107
  workspaces[workspaceName][pluginName] = {
107
108
  config,
108
109
  entryFiles,
109
110
  projectFiles,
111
+ sampleFiles,
110
112
  };
111
113
  }
112
114
  }
@@ -1,4 +1,5 @@
1
1
  import { WorkspaceConfiguration } from './types/config.js';
2
+ import type { Issue } from './types/issues.js';
2
3
  import type { WorkspaceManifests } from './types/workspace.js';
3
4
  import type { PackageJson } from 'type-fest';
4
5
  export default class DependencyDeputy {
@@ -8,14 +9,12 @@ export default class DependencyDeputy {
8
9
  referencedDependencies: Map<string, Set<string>>;
9
10
  peerDependencies: Map<string, Map<string, Set<string>>>;
10
11
  tsConfigPathGlobs: Map<string, string[]>;
11
- isProduction: boolean;
12
12
  constructor();
13
- addWorkspace({ name, dir, manifestPath, manifest, isProduction, }: {
13
+ addWorkspace({ name, dir, manifestPath, manifest, }: {
14
14
  name: string;
15
15
  dir: string;
16
16
  manifestPath: string;
17
17
  manifest: PackageJson;
18
- isProduction: boolean;
19
18
  }): void;
20
19
  cancelWorkspace(workspaceName: string): void;
21
20
  getManifest(workspaceName: string): PackageJson | undefined;
@@ -47,13 +46,7 @@ export default class DependencyDeputy {
47
46
  resolvePackageName(moduleSpecifier: string): string;
48
47
  private isInDependencies;
49
48
  settleDependencyIssues(): {
50
- dependencyIssues: {
51
- filePath: string;
52
- symbol: string;
53
- }[];
54
- devDependencyIssues: {
55
- filePath: string;
56
- symbol: string;
57
- }[];
49
+ dependencyIssues: Issue[];
50
+ devDependencyIssues: Issue[];
58
51
  };
59
52
  }
@@ -1,5 +1,6 @@
1
1
  import { isBuiltin } from 'node:module';
2
2
  import micromatch from 'micromatch';
3
+ import { isDefinitelyTyped, getDefinitelyTypedPackage } from './util/modules.js';
3
4
  const IGNORE_DEFINITELY_TYPED = ['node'];
4
5
  export default class DependencyDeputy {
5
6
  _manifests = new Map();
@@ -8,13 +9,12 @@ export default class DependencyDeputy {
8
9
  referencedDependencies;
9
10
  peerDependencies;
10
11
  tsConfigPathGlobs = new Map();
11
- isProduction = false;
12
12
  constructor() {
13
13
  this.referencedDependencies = new Map();
14
14
  this.peerDependencies = new Map();
15
15
  this.canceledWorkspaces = new Set();
16
16
  }
17
- addWorkspace({ name, dir, manifestPath, manifest, isProduction, }) {
17
+ addWorkspace({ name, dir, manifestPath, manifest, }) {
18
18
  const scripts = Object.values(manifest.scripts ?? {});
19
19
  const dependencies = Object.keys(manifest.dependencies ?? {});
20
20
  const peerDependencies = Object.keys(manifest.peerDependencies ?? {});
@@ -33,7 +33,6 @@ export default class DependencyDeputy {
33
33
  productionDependencies,
34
34
  allDependencies: [...productionDependencies, ...devDependencies],
35
35
  });
36
- this.isProduction = isProduction;
37
36
  }
38
37
  cancelWorkspace(workspaceName) {
39
38
  this.canceledWorkspaces.add(workspaceName);
@@ -71,12 +70,14 @@ export default class DependencyDeputy {
71
70
  const packageName = this.resolvePackageName(moduleSpecifier);
72
71
  const workspaceNames = isStrict ? [workspace.name] : [workspace.name, ...[...workspace.ancestors].reverse()];
73
72
  const closestWorkspaceName = workspaceNames.find(name => this.isInDependencies(name, packageName));
74
- if (closestWorkspaceName) {
75
- this.addReferencedDependency(closestWorkspaceName, packageName);
76
- }
77
- else {
78
- return moduleSpecifier;
73
+ const typesPackageName = !isDefinitelyTyped(packageName) && getDefinitelyTypedPackage(packageName);
74
+ const closestWorkspaceNameForTypes = typesPackageName && workspaceNames.find(name => this.isInDependencies(name, typesPackageName));
75
+ if (closestWorkspaceName || closestWorkspaceNameForTypes) {
76
+ closestWorkspaceName && this.addReferencedDependency(closestWorkspaceName, packageName);
77
+ closestWorkspaceNameForTypes && this.addReferencedDependency(closestWorkspaceNameForTypes, typesPackageName);
78
+ return;
79
79
  }
80
+ return moduleSpecifier;
80
81
  }
81
82
  isInternalDependency(workspaceName, moduleSpecifier) {
82
83
  if (moduleSpecifier.startsWith('/') || moduleSpecifier.startsWith('.'))
@@ -106,15 +107,13 @@ export default class DependencyDeputy {
106
107
  continue;
107
108
  const referencedDependencies = this.referencedDependencies.get(workspaceName);
108
109
  const isUnreferencedDependency = (dependency) => {
110
+ if (referencedDependencies?.has(dependency))
111
+ return false;
109
112
  const [scope, typedDependency] = dependency.split('/');
110
113
  if (scope === '@types') {
111
114
  if (IGNORE_DEFINITELY_TYPED.includes(typedDependency))
112
115
  return false;
113
- if (referencedDependencies?.has(dependency))
114
- return false;
115
- const [scope, name] = typedDependency.split('__');
116
- const typedPackageName = scope && name ? `@${scope}/${name}` : typedDependency;
117
- return referencedDependencies ? !referencedDependencies.has(typedPackageName) : false;
116
+ return !referencedDependencies?.has(typedDependency);
118
117
  }
119
118
  if (!referencedDependencies?.has(dependency)) {
120
119
  const peerDependencies = Array.from(this.peerDependencies.get(workspaceName)?.get(dependency) ?? []);
@@ -124,10 +123,10 @@ export default class DependencyDeputy {
124
123
  };
125
124
  this.getProductionDependencies(workspaceName)
126
125
  .filter(isUnreferencedDependency)
127
- .forEach(symbol => dependencyIssues.push({ filePath: manifestPath, symbol }));
126
+ .forEach(symbol => dependencyIssues.push({ type: 'dependencies', filePath: manifestPath, symbol }));
128
127
  this.getDevDependencies(workspaceName)
129
128
  .filter(isUnreferencedDependency)
130
- .forEach(symbol => devDependencyIssues.push({ filePath: manifestPath, symbol }));
129
+ .forEach(symbol => devDependencyIssues.push({ type: 'devDependencies', filePath: manifestPath, symbol }));
131
130
  }
132
131
  return { dependencyIssues, devDependencyIssues };
133
132
  }
package/dist/index.js CHANGED
@@ -7,8 +7,10 @@ import SourceLab from './source-lab.js';
7
7
  import { compact } from './util/array.js';
8
8
  import { ROOT_WORKSPACE_NAME } from './util/constants.js';
9
9
  import { debugLogObject, debugLogFiles } from './util/debug.js';
10
+ import { _findExternalImportModuleSpecifiers } from './util/externalImports.js';
10
11
  import { findFile, loadJSON } from './util/fs.js';
11
12
  import { _glob } from './util/glob.js';
13
+ import { _findDuplicateExportedNames } from './util/project.js';
12
14
  import { loadTSConfig } from './util/tsconfig-loader.js';
13
15
  import WorkspaceWorker from './workspace-worker.js';
14
16
  export const main = async (unresolvedConfiguration) => {
@@ -16,7 +18,7 @@ export const main = async (unresolvedConfiguration) => {
16
18
  const chief = new ConfigurationChief({ cwd });
17
19
  const deputy = new DependencyDeputy();
18
20
  debugLogObject(1, 'Unresolved configuration', unresolvedConfiguration);
19
- const collector = new IssueCollector({ cwd, isShowProgress, isProduction });
21
+ const collector = new IssueCollector({ cwd, isShowProgress });
20
22
  collector.updateMessage('Reading configuration and manifest files...');
21
23
  await chief.loadLocalConfig();
22
24
  const workspaces = await chief.getActiveWorkspaces();
@@ -30,7 +32,7 @@ export const main = async (unresolvedConfiguration) => {
30
32
  const negatedWorkspacePatterns = Object.values(workspaces)
31
33
  .filter(workspace => workspace.name !== ROOT_WORKSPACE_NAME)
32
34
  .map(workspace => `!${workspace.name}`);
33
- const lab = new SourceLab({ issueManager: collector, report, workspaceDirs });
35
+ const lab = new SourceLab({ report, workspaceDirs });
34
36
  const principal = new ProjectPrincipal();
35
37
  for (const { name, dir, config, ancestors } of workspaces) {
36
38
  const isRoot = name === ROOT_WORKSPACE_NAME;
@@ -39,7 +41,7 @@ export const main = async (unresolvedConfiguration) => {
39
41
  const manifest = isRoot ? chief.manifest : manifestPath && (await loadJSON(manifestPath));
40
42
  if (!manifestPath || !manifest)
41
43
  continue;
42
- deputy.addWorkspace({ name, dir, manifestPath, manifest, isProduction });
44
+ deputy.addWorkspace({ name, dir, manifestPath, manifest });
43
45
  const tsConfigFilePath = path.join(dir, 'tsconfig.json');
44
46
  const tsConfig = await loadTSConfig(tsConfigFilePath);
45
47
  if (isRoot && tsConfig) {
@@ -189,10 +191,10 @@ export const main = async (unresolvedConfiguration) => {
189
191
  if (!deputy.isInternalDependency(name, issue.symbol)) {
190
192
  if (!workspaceDependencies.includes(issue.symbol)) {
191
193
  if (isStrict) {
192
- collector.addIssue('unlisted', issue);
194
+ collector.addIssue(issue);
193
195
  }
194
196
  else if (!rootDependencies.includes(issue.symbol)) {
195
- collector.addIssue('unlisted', issue);
197
+ collector.addIssue(issue);
196
198
  }
197
199
  }
198
200
  }
@@ -207,23 +209,34 @@ export const main = async (unresolvedConfiguration) => {
207
209
  collector.addFilesIssues(unreferencedProductionFiles);
208
210
  collector.updateMessage('Connecting the dots...');
209
211
  usedProductionFiles.forEach(sourceFile => {
212
+ collector.counters.processed++;
210
213
  const filePath = sourceFile.getFilePath();
211
214
  const workspaceDir = workspaceDirs.find(workspaceDir => filePath.startsWith(workspaceDir));
212
215
  const workspace = workspaces.find(workspace => workspace.dir === workspaceDir);
213
- const externalModuleSpecifiers = lab.analyzeSourceFile(sourceFile);
214
- if (workspace) {
216
+ if (workspace && (report.dependencies || report.unlisted)) {
217
+ const externalModuleSpecifiers = _findExternalImportModuleSpecifiers(sourceFile);
215
218
  externalModuleSpecifiers.forEach(moduleSpecifier => {
216
219
  const unlistedDependency = deputy.maybeAddListedReferencedDependency(workspace, moduleSpecifier, isStrict);
217
220
  if (unlistedDependency)
218
- collector.addIssue('unlisted', { filePath, symbol: unlistedDependency });
221
+ collector.addIssue({ type: 'unlisted', filePath, symbol: unlistedDependency });
219
222
  });
220
223
  }
224
+ if (report.duplicates) {
225
+ const duplicateExports = _findDuplicateExportedNames(sourceFile);
226
+ duplicateExports.forEach(symbols => {
227
+ const symbol = symbols.join('|');
228
+ collector.addIssue({ type: 'duplicates', filePath, symbol, symbols });
229
+ });
230
+ }
231
+ const issues = lab.analyzeSourceFile(sourceFile);
232
+ issues.forEach(issue => issue.type && collector.addIssue(issue));
221
233
  });
222
234
  collector.removeProgress();
223
- const { dependencyIssues, devDependencyIssues } = deputy.settleDependencyIssues();
224
- dependencyIssues.forEach(issue => collector.addIssue('dependencies', issue));
225
- if (!isProduction) {
226
- devDependencyIssues.forEach(issue => collector.addIssue('devDependencies', issue));
235
+ if (report.dependencies) {
236
+ const { dependencyIssues, devDependencyIssues } = deputy.settleDependencyIssues();
237
+ dependencyIssues.forEach(issue => collector.addIssue(issue));
238
+ if (!isProduction)
239
+ devDependencyIssues.forEach(issue => collector.addIssue(issue));
227
240
  }
228
241
  const { issues, counters } = collector.getIssues();
229
242
  debugLogObject(3, 'Issues', issues);
@@ -1,9 +1,8 @@
1
1
  import { LineRewriter } from './util/log.js';
2
- import type { Issue, SymbolIssueType, Report } from './types/issues.js';
2
+ import type { Issue, Report } from './types/issues.js';
3
3
  type IssueCollectorOptions = {
4
4
  cwd: string;
5
5
  isShowProgress?: boolean;
6
- isProduction?: boolean;
7
6
  report?: Report;
8
7
  };
9
8
  export default class IssueCollector {
@@ -15,12 +14,11 @@ export default class IssueCollector {
15
14
  referencedFiles: Set<string>;
16
15
  cwd: string;
17
16
  isShowProgress: boolean;
18
- isProduction: boolean;
19
- constructor({ cwd, isShowProgress, isProduction, report }: IssueCollectorOptions);
17
+ constructor({ cwd, isShowProgress, report }: IssueCollectorOptions);
20
18
  setIsShowProgress(isShowProgress: boolean): void;
21
19
  setProjectFilesCount(count: number): void;
22
20
  addFilesIssues(filePaths: Set<string>): void;
23
- addIssue(issueType: SymbolIssueType, issue: Issue): void;
21
+ addIssue(issue: Issue): void;
24
22
  setReport(report: Report): void;
25
23
  updateMessage(message: string): void;
26
24
  updateProgress(issue?: Issue): void;
@@ -11,10 +11,8 @@ export default class IssueCollector {
11
11
  referencedFiles;
12
12
  cwd;
13
13
  isShowProgress = false;
14
- isProduction = false;
15
- constructor({ cwd, isShowProgress = false, isProduction = false, report }) {
14
+ constructor({ cwd, isShowProgress = false, report }) {
16
15
  this.lineRewriter = new LineRewriter();
17
- this.isProduction = isProduction;
18
16
  this.cwd = cwd;
19
17
  this.setIsShowProgress(isShowProgress);
20
18
  this.report = report ?? initReport();
@@ -38,13 +36,13 @@ export default class IssueCollector {
38
36
  }
39
37
  });
40
38
  }
41
- addIssue(issueType, issue) {
39
+ addIssue(issue) {
42
40
  issue.filePath = path.relative(this.cwd, issue.filePath);
43
41
  const key = relative(issue.filePath);
44
- this.issues[issueType][key] = this.issues[issueType][key] ?? {};
45
- if (!this.issues[issueType][key][issue.symbol]) {
46
- this.issues[issueType][key][issue.symbol] = issue;
47
- this.counters[issueType]++;
42
+ this.issues[issue.type][key] = this.issues[issue.type][key] ?? {};
43
+ if (!this.issues[issue.type][key][issue.symbol]) {
44
+ this.issues[issue.type][key][issue.symbol] = issue;
45
+ this.counters[issue.type]++;
48
46
  this.updateProgress(issue);
49
47
  }
50
48
  }
@@ -8,6 +8,7 @@ export const isEnabled = ({ dependencies }) => {
8
8
  return dependencies.has('eslint');
9
9
  };
10
10
  export const CONFIG_FILE_PATTERNS = ['eslint.config.js', '.eslintrc', '.eslintrc.json', '.eslintrc.js'];
11
+ const SAMPLE_FILE_PATHS = ['__placeholder__.js', '__placeholder__.ts'];
11
12
  const resolvePluginPackageName = (pluginName) => {
12
13
  return pluginName.startsWith('@')
13
14
  ? pluginName.includes('/')
@@ -15,16 +16,15 @@ const resolvePluginPackageName = (pluginName) => {
15
16
  : `${pluginName}/eslint-plugin`
16
17
  : `eslint-plugin-${pluginName}`;
17
18
  };
18
- const findESLintDependencies = async (configFilePath, { cwd }) => {
19
+ const findESLintDependencies = async (configFilePath, { cwd, config }) => {
19
20
  if (path.basename(configFilePath) === 'eslint.config.js') {
20
21
  return [];
21
22
  }
22
23
  else {
23
24
  const engine = new ESLint({ cwd, overrideConfigFile: configFilePath, useEslintrc: false });
24
- const jsConfig = await engine.calculateConfigForFile('__placeholder__.js');
25
- const tsConfig = await engine.calculateConfigForFile('__placeholder__.ts');
26
- const tsxConfig = await engine.calculateConfigForFile('__placeholder__.spec.tsx');
27
- const dependencies = [jsConfig, tsConfig, tsxConfig].map(config => {
25
+ const calculateConfigForFile = async (sampleFile) => await engine.calculateConfigForFile(sampleFile);
26
+ const sampleFiles = config?.sampleFiles.length > 0 ? config.sampleFiles : SAMPLE_FILE_PATHS;
27
+ const dependencies = await Promise.all(sampleFiles.map(calculateConfigForFile)).then(configs => configs.flatMap(config => {
28
28
  if (!config)
29
29
  return [];
30
30
  const plugins = config.plugins?.map(resolvePluginPackageName) ?? [];
@@ -32,7 +32,7 @@ const findESLintDependencies = async (configFilePath, { cwd }) => {
32
32
  const extraParsers = config.parserOptions?.babelOptions?.presets ?? [];
33
33
  const settings = config.settings ? getDependenciesFromSettings(config.settings) : [];
34
34
  return [...parsers, ...extraParsers, ...plugins, ...settings].map(getPackageName);
35
- });
35
+ }));
36
36
  return compact(dependencies.flat());
37
37
  }
38
38
  };
@@ -1,3 +1,3 @@
1
1
  export const isEnabled = ({ dependencies }) => dependencies.has('rollup');
2
2
  export const CONFIG_FILE_PATTERNS = [];
3
- export const ENTRY_FILE_PATTERNS = ['rollup.config.{js,mjs}'];
3
+ export const ENTRY_FILE_PATTERNS = ['rollup.config.{js,mjs,ts}'];
@@ -43,20 +43,20 @@ export default ({ report, issues, options }) => {
43
43
  const fallbackOwner = dependenciesOwner;
44
44
  const calcFileOwnership = (filePath) => codeownersEngine.calcFileOwnership(relative(filePath))[0] ?? fallbackOwner;
45
45
  const addOwner = (issue) => ({ ...issue, owner: calcFileOwnership(issue.filePath) });
46
- for (const [reportType, isReportType] of Object.entries(report)) {
46
+ for (const [type, isReportType] of Object.entries(report)) {
47
47
  if (isReportType) {
48
- const title = reportMultipleGroups && ISSUE_TYPE_TITLE[reportType];
49
- if (issues[reportType] instanceof Set) {
50
- const toIssue = (filePath) => ({ filePath, symbol: filePath });
51
- const issuesForType = Array.from(issues[reportType]).map(toIssue);
48
+ const title = reportMultipleGroups && ISSUE_TYPE_TITLE[type];
49
+ if (issues[type] instanceof Set) {
50
+ const toIssue = (filePath) => ({ type, filePath, symbol: filePath });
51
+ const issuesForType = Array.from(issues[type]).map(toIssue);
52
52
  logIssueSet(issuesForType.map(addOwner), title);
53
53
  }
54
- else if (reportType === 'duplicates') {
55
- const issuesForType = Object.values(issues[reportType]).map(Object.values).flat().map(addOwner);
54
+ else if (type === 'duplicates') {
55
+ const issuesForType = Object.values(issues[type]).map(Object.values).flat().map(addOwner);
56
56
  logIssueRecord(issuesForType, title);
57
57
  }
58
58
  else {
59
- const issuesForType = Object.values(issues[reportType]).map(issues => {
59
+ const issuesForType = Object.values(issues[type]).map(issues => {
60
60
  const items = Object.values(issues);
61
61
  return addOwner({ ...items[0], symbols: items.map(issue => issue.symbol) });
62
62
  });
@@ -1,22 +1,19 @@
1
1
  import { SourceFile } from 'ts-morph';
2
- import IssueCollector from './issue-collector.js';
3
- import type { Report } from './types/issues.js';
2
+ import type { Report, Issue } from './types/issues.js';
4
3
  type FileLabOptions = {
5
- issueManager: IssueCollector;
6
4
  report: Report;
7
5
  workspaceDirs: string[];
8
6
  };
9
7
  export default class SourceLab {
10
- issueManager: IssueCollector;
11
8
  report: Report;
12
9
  workspaceDirs: string[];
13
10
  skipExportsAnalysis: Set<unknown>;
14
11
  isReportExports: boolean;
15
12
  isReportValues: boolean;
16
13
  isReportTypes: boolean;
17
- constructor({ issueManager, report, workspaceDirs }: FileLabOptions);
14
+ constructor({ report, workspaceDirs }: FileLabOptions);
18
15
  skipExportsAnalysisFor(filePath: string | string[]): void;
19
- analyzeSourceFile(sourceFile: SourceFile): string[];
16
+ analyzeSourceFile(sourceFile: SourceFile): Set<Issue>;
20
17
  private analyzeExports;
21
18
  }
22
19
  export {};
@@ -1,18 +1,15 @@
1
1
  import { ts, Node } from 'ts-morph';
2
- import { _findExternalImportModuleSpecifiers } from './util/externalImports.js';
3
2
  import { findUnusedClassMembers, findUnusedEnumMembers } from './util/members.js';
4
- import { _findDuplicateExportedNames, _hasReferencingDefaultImport, _findReferencingNamespaceNodes, _getExportedDeclarations, _findReferences, hasExternalReferences, } from './util/project.js';
3
+ import { _hasReferencingDefaultImport, _findReferencingNamespaceNodes, _getExportedDeclarations, _findReferences, hasExternalReferences, } from './util/project.js';
5
4
  import { getType } from './util/type.js';
6
5
  export default class SourceLab {
7
- issueManager;
8
6
  report;
9
7
  workspaceDirs;
10
8
  skipExportsAnalysis;
11
9
  isReportExports;
12
10
  isReportValues;
13
11
  isReportTypes;
14
- constructor({ issueManager, report, workspaceDirs }) {
15
- this.issueManager = issueManager;
12
+ constructor({ report, workspaceDirs }) {
16
13
  this.report = report;
17
14
  this.workspaceDirs = workspaceDirs;
18
15
  this.skipExportsAnalysis = new Set();
@@ -27,30 +24,18 @@ export default class SourceLab {
27
24
  filePath.forEach(filePath => this.skipExportsAnalysis.add(filePath));
28
25
  }
29
26
  analyzeSourceFile(sourceFile) {
30
- const im = this.issueManager;
31
- const report = this.report;
32
- let externalModuleSpecifiers = [];
33
- im.counters.processed++;
27
+ const issues = new Set();
34
28
  const filePath = sourceFile.getFilePath();
35
- if (report.dependencies || report.unlisted) {
36
- externalModuleSpecifiers = _findExternalImportModuleSpecifiers(sourceFile);
37
- }
38
- if (report.duplicates) {
39
- const duplicateExports = _findDuplicateExportedNames(sourceFile);
40
- duplicateExports.forEach(symbols => {
41
- const symbol = symbols.join('|');
42
- im.addIssue('duplicates', { filePath, symbol, symbols });
43
- });
44
- }
45
29
  if (this.skipExportsAnalysis.has(filePath))
46
- return externalModuleSpecifiers;
30
+ return issues;
47
31
  if (this.isReportExports) {
48
- this.analyzeExports(sourceFile);
32
+ const exportsIssues = this.analyzeExports(sourceFile);
33
+ exportsIssues.forEach(issue => issues.add(issue));
49
34
  }
50
- return externalModuleSpecifiers;
35
+ return issues;
51
36
  }
52
37
  analyzeExports(sourceFile) {
53
- const im = this.issueManager;
38
+ const issues = new Set();
54
39
  const report = this.report;
55
40
  const filePath = sourceFile.getFilePath();
56
41
  const exportDeclarations = _getExportedDeclarations(sourceFile);
@@ -70,7 +55,8 @@ export default class SourceLab {
70
55
  if (declaration.isKind(ts.SyntaxKind.EnumDeclaration)) {
71
56
  identifier = declaration.getFirstChildByKind(ts.SyntaxKind.Identifier);
72
57
  if (report.enumMembers) {
73
- findUnusedEnumMembers(declaration, filePath).forEach(member => im.addIssue('enumMembers', {
58
+ findUnusedEnumMembers(declaration, filePath).forEach(member => issues.add({
59
+ type: 'enumMembers',
74
60
  filePath,
75
61
  symbol: member.getName(),
76
62
  parentSymbol: identifier?.getText(),
@@ -80,7 +66,8 @@ export default class SourceLab {
80
66
  else if (declaration.isKind(ts.SyntaxKind.ClassDeclaration)) {
81
67
  identifier = declaration.getFirstChildByKind(ts.SyntaxKind.Identifier);
82
68
  if (report.classMembers) {
83
- findUnusedClassMembers(declaration, filePath).forEach(member => im.addIssue('classMembers', {
69
+ findUnusedClassMembers(declaration, filePath).forEach(member => issues.add({
70
+ type: 'classMembers',
84
71
  filePath,
85
72
  symbol: member.getName(),
86
73
  parentSymbol: identifier?.getText(),
@@ -119,38 +106,31 @@ export default class SourceLab {
119
106
  }
120
107
  if (identifier || fakeIdentifier) {
121
108
  const identifierText = fakeIdentifier ?? identifier?.getText() ?? '*';
122
- if (report.exports && im.issues.exports[filePath]?.[identifierText])
123
- return;
124
- if (report.types && im.issues.types[filePath]?.[identifierText])
125
- return;
126
- if (report.nsExports && im.issues.nsExports[filePath]?.[identifierText])
127
- return;
128
- if (report.nsTypes && im.issues.nsTypes[filePath]?.[identifierText])
129
- return;
130
109
  const refs = _findReferences(identifier);
131
110
  if (refs.length === 0) {
132
- im.addIssue('exports', { filePath, symbol: identifierText });
111
+ issues.add({ type: 'exports', filePath, symbol: identifierText });
133
112
  }
134
113
  else {
135
114
  if (hasExternalReferences(refs, filePath))
136
115
  return;
137
116
  if (_findReferencingNamespaceNodes(sourceFile).length > 0) {
138
117
  if (type) {
139
- im.addIssue('nsTypes', { filePath, symbol: identifierText, symbolType: type });
118
+ issues.add({ type: 'nsTypes', filePath, symbol: identifierText, symbolType: type });
140
119
  }
141
120
  else {
142
- im.addIssue('nsExports', { filePath, symbol: identifierText });
121
+ issues.add({ type: 'nsExports', filePath, symbol: identifierText });
143
122
  }
144
123
  }
145
124
  else if (type) {
146
- im.addIssue('types', { filePath, symbol: identifierText, symbolType: type });
125
+ issues.add({ type: 'types', filePath, symbol: identifierText, symbolType: type });
147
126
  }
148
127
  else {
149
- im.addIssue('exports', { filePath, symbol: identifierText });
128
+ issues.add({ type: 'exports', filePath, symbol: identifierText });
150
129
  }
151
130
  }
152
131
  }
153
132
  });
154
133
  });
134
+ return issues;
155
135
  }
156
136
  }
@@ -3,6 +3,7 @@ export interface PluginConfiguration {
3
3
  config: NormalizedGlob;
4
4
  entryFiles: NormalizedGlob;
5
5
  projectFiles: NormalizedGlob;
6
+ sampleFiles: NormalizedGlob;
6
7
  }
7
8
  interface PluginsConfiguration {
8
9
  babel: PluginConfiguration;
@@ -1,5 +1,6 @@
1
1
  type SymbolType = 'type' | 'interface' | 'enum';
2
2
  export type Issue = {
3
+ type: SymbolIssueType;
3
4
  filePath: string;
4
5
  symbol: string;
5
6
  symbols?: string[];
@@ -1,4 +1,5 @@
1
1
  import { PackageJson } from 'type-fest';
2
+ import { PluginConfiguration } from './config.js';
2
3
  type IsPluginEnabledCallbackOptions = {
3
4
  manifest: PackageJson;
4
5
  dependencies: Set<string>;
@@ -7,5 +8,6 @@ export type IsPluginEnabledCallback = (options: IsPluginEnabledCallbackOptions)
7
8
  export type GenericPluginCallback = (configFilePath: string, { cwd, manifest }: {
8
9
  cwd: string;
9
10
  manifest: PackageJson;
11
+ config: PluginConfiguration;
10
12
  }) => Promise<string[]> | string[];
11
13
  export {};
@@ -85,16 +85,19 @@ export declare const LocalConfiguration: z.ZodObject<z.extendShape<z.extendShape
85
85
  entryFiles: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
86
86
  productionEntryFiles: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
87
87
  projectFiles: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
88
+ sampleFiles: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
88
89
  }, "strip", z.ZodTypeAny, {
89
90
  config?: string | string[] | undefined;
90
91
  entryFiles?: string | string[] | undefined;
91
92
  projectFiles?: string | string[] | undefined;
92
93
  productionEntryFiles?: string | string[] | undefined;
94
+ sampleFiles?: string | string[] | undefined;
93
95
  }, {
94
96
  config?: string | string[] | undefined;
95
97
  entryFiles?: string | string[] | undefined;
96
98
  projectFiles?: string | string[] | undefined;
97
99
  productionEntryFiles?: string | string[] | undefined;
100
+ sampleFiles?: string | string[] | undefined;
98
101
  }>]>>;
99
102
  gatsby: z.ZodOptional<z.ZodUnion<[z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>, z.ZodObject<{
100
103
  config: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
@@ -290,6 +293,7 @@ export declare const LocalConfiguration: z.ZodObject<z.extendShape<z.extendShape
290
293
  entryFiles?: string | string[] | undefined;
291
294
  projectFiles?: string | string[] | undefined;
292
295
  productionEntryFiles?: string | string[] | undefined;
296
+ sampleFiles?: string | string[] | undefined;
293
297
  } | undefined;
294
298
  gatsby?: string | string[] | {
295
299
  config?: string | string[] | undefined;
@@ -385,6 +389,7 @@ export declare const LocalConfiguration: z.ZodObject<z.extendShape<z.extendShape
385
389
  entryFiles?: string | string[] | undefined;
386
390
  projectFiles?: string | string[] | undefined;
387
391
  productionEntryFiles?: string | string[] | undefined;
392
+ sampleFiles?: string | string[] | undefined;
388
393
  } | undefined;
389
394
  gatsby?: string | string[] | {
390
395
  config?: string | string[] | undefined;
@@ -517,16 +522,19 @@ export declare const LocalConfiguration: z.ZodObject<z.extendShape<z.extendShape
517
522
  entryFiles: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
518
523
  productionEntryFiles: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
519
524
  projectFiles: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
525
+ sampleFiles: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
520
526
  }, "strip", z.ZodTypeAny, {
521
527
  config?: string | string[] | undefined;
522
528
  entryFiles?: string | string[] | undefined;
523
529
  projectFiles?: string | string[] | undefined;
524
530
  productionEntryFiles?: string | string[] | undefined;
531
+ sampleFiles?: string | string[] | undefined;
525
532
  }, {
526
533
  config?: string | string[] | undefined;
527
534
  entryFiles?: string | string[] | undefined;
528
535
  projectFiles?: string | string[] | undefined;
529
536
  productionEntryFiles?: string | string[] | undefined;
537
+ sampleFiles?: string | string[] | undefined;
530
538
  }>]>>;
531
539
  gatsby: z.ZodOptional<z.ZodUnion<[z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>, z.ZodObject<{
532
540
  config: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
@@ -726,6 +734,7 @@ export declare const LocalConfiguration: z.ZodObject<z.extendShape<z.extendShape
726
734
  entryFiles?: string | string[] | undefined;
727
735
  projectFiles?: string | string[] | undefined;
728
736
  productionEntryFiles?: string | string[] | undefined;
737
+ sampleFiles?: string | string[] | undefined;
729
738
  } | undefined;
730
739
  gatsby?: string | string[] | {
731
740
  config?: string | string[] | undefined;
@@ -821,6 +830,7 @@ export declare const LocalConfiguration: z.ZodObject<z.extendShape<z.extendShape
821
830
  entryFiles?: string | string[] | undefined;
822
831
  projectFiles?: string | string[] | undefined;
823
832
  productionEntryFiles?: string | string[] | undefined;
833
+ sampleFiles?: string | string[] | undefined;
824
834
  } | undefined;
825
835
  gatsby?: string | string[] | {
826
836
  config?: string | string[] | undefined;
@@ -921,6 +931,7 @@ export declare const LocalConfiguration: z.ZodObject<z.extendShape<z.extendShape
921
931
  entryFiles?: string | string[] | undefined;
922
932
  projectFiles?: string | string[] | undefined;
923
933
  productionEntryFiles?: string | string[] | undefined;
934
+ sampleFiles?: string | string[] | undefined;
924
935
  } | undefined;
925
936
  gatsby?: string | string[] | {
926
937
  config?: string | string[] | undefined;
@@ -1016,6 +1027,7 @@ export declare const LocalConfiguration: z.ZodObject<z.extendShape<z.extendShape
1016
1027
  entryFiles?: string | string[] | undefined;
1017
1028
  projectFiles?: string | string[] | undefined;
1018
1029
  productionEntryFiles?: string | string[] | undefined;
1030
+ sampleFiles?: string | string[] | undefined;
1019
1031
  } | undefined;
1020
1032
  gatsby?: string | string[] | {
1021
1033
  config?: string | string[] | undefined;
@@ -21,12 +21,22 @@ const pluginWithEntryFilesSchema = z.union([
21
21
  projectFiles: globSchema.optional(),
22
22
  }),
23
23
  ]);
24
+ const pluginWithSampleFilesSchema = z.union([
25
+ globSchema,
26
+ z.object({
27
+ config: globSchema.optional(),
28
+ entryFiles: globSchema.optional(),
29
+ productionEntryFiles: globSchema.optional(),
30
+ projectFiles: globSchema.optional(),
31
+ sampleFiles: globSchema.optional(),
32
+ }),
33
+ ]);
24
34
  const pluginsConfigurationSchema = z.object({
25
35
  babel: pluginWithEntryFilesSchema,
26
36
  capacitor: pluginWithEntryFilesSchema,
27
37
  changesets: pluginWithEntryFilesSchema,
28
38
  cypress: pluginWithEntryFilesSchema,
29
- eslint: pluginWithEntryFilesSchema,
39
+ eslint: pluginWithSampleFilesSchema,
30
40
  gatsby: pluginWithEntryFilesSchema,
31
41
  jest: pluginWithEntryFilesSchema,
32
42
  next: pluginWithEntryFilesSchema,
@@ -1,4 +1,5 @@
1
1
  export declare const negate: (pattern: string) => string;
2
+ export declare const removeProductionSuffix: (pattern: string) => string;
2
3
  export declare const _glob: ({ cwd, workingDir, patterns, ignore, gitignore, }: {
3
4
  cwd: string;
4
5
  workingDir?: string | undefined;
package/dist/util/glob.js CHANGED
@@ -10,6 +10,7 @@ const prependDirToPattern = (workingDir, pattern) => {
10
10
  return path.posix.join(workingDir, pattern);
11
11
  };
12
12
  export const negate = (pattern) => `!${pattern}`;
13
+ export const removeProductionSuffix = (pattern) => pattern.replace(/!$/, '');
13
14
  const sortNegatedLast = (a, b) => (a.startsWith('!') ? 1 : b.startsWith('!') ? -1 : 0);
14
15
  const glob = async ({ cwd, workingDir = cwd, patterns, ignore = [], gitignore = true, }) => {
15
16
  const cwdPosix = ensurePosixPath(cwd);
@@ -1,4 +1,4 @@
1
1
  import { MethodDeclaration, PropertyDeclaration } from 'ts-morph';
2
2
  import type { ClassDeclaration, EnumDeclaration } from 'ts-morph';
3
- export declare const findUnusedClassMembers: (declaration: ClassDeclaration, filePath: string) => (PropertyDeclaration | MethodDeclaration)[];
3
+ export declare const findUnusedClassMembers: (declaration: ClassDeclaration, filePath: string) => (MethodDeclaration | PropertyDeclaration)[];
4
4
  export declare const findUnusedEnumMembers: (declaration: EnumDeclaration, filePath: string) => import("ts-morph").EnumMember[];
@@ -1 +1,3 @@
1
1
  export declare const getPackageName: (value: string) => string;
2
+ export declare const isDefinitelyTyped: (packageName: string) => boolean;
3
+ export declare const getDefinitelyTypedPackage: (packageName: string) => string;
@@ -8,3 +8,9 @@ export const getPackageName = (value) => {
8
8
  }
9
9
  return value.startsWith('/') ? value : value.split('/')[0];
10
10
  };
11
+ export const isDefinitelyTyped = (packageName) => packageName.startsWith('@types/');
12
+ export const getDefinitelyTypedPackage = (packageName) => {
13
+ if (isDefinitelyTyped(packageName))
14
+ return packageName;
15
+ return '@types/' + packageName.replace('@', '__');
16
+ };
@@ -3,7 +3,7 @@ import * as npm from './npm-scripts/index.js';
3
3
  import * as plugins from './plugins/index.js';
4
4
  import { ROOT_WORKSPACE_NAME, TEST_FILE_PATTERNS } from './util/constants.js';
5
5
  import { debugLogFiles, debugLogIssues } from './util/debug.js';
6
- import { _pureGlob, negate } from './util/glob.js';
6
+ import { _pureGlob, negate, removeProductionSuffix } from './util/glob.js';
7
7
  const negatedTestFilePatterns = TEST_FILE_PATTERNS.map(negate);
8
8
  export default class WorkspaceWorker {
9
9
  name;
@@ -51,7 +51,7 @@ export default class WorkspaceWorker {
51
51
  }
52
52
  getConfigForPlugin(pluginName) {
53
53
  return (this.config[pluginName] ??
54
- this.rootWorkspaceConfig[pluginName] ?? { config: [], entryFiles: [], projectFiles: [] });
54
+ this.rootWorkspaceConfig[pluginName] ?? { config: [], entryFiles: [], projectFiles: [], sampleFiles: [] });
55
55
  }
56
56
  async init() {
57
57
  this.setEnabledPlugins();
@@ -69,7 +69,7 @@ export default class WorkspaceWorker {
69
69
  async initReferencedDependencies() {
70
70
  const { dependencies, peerDependencies } = await npm.findDependencies(this.rootConfig.ignoreBinaries, this.manifest, this.isRoot, this.dir, this.rootWorkspaceDir);
71
71
  const filePath = path.join(this.dir, 'package.json');
72
- dependencies.forEach(dependency => this.referencedDependencyIssues.add({ filePath, symbol: dependency }));
72
+ dependencies.forEach(dependency => this.referencedDependencyIssues.add({ type: 'unlisted', filePath, symbol: dependency }));
73
73
  dependencies.forEach(dependency => this.referencedDependencies.add(dependency));
74
74
  this.peerDependencies = peerDependencies;
75
75
  }
@@ -79,7 +79,7 @@ export default class WorkspaceWorker {
79
79
  return [];
80
80
  return [entryFiles, TEST_FILE_PATTERNS, this.isRoot ? this.negatedWorkspacePatterns : []]
81
81
  .flat()
82
- .map(p => p.replace(/!$/, ''));
82
+ .map(removeProductionSuffix);
83
83
  }
84
84
  getProjectFilePatterns() {
85
85
  const { projectFiles } = this.config;
@@ -87,7 +87,7 @@ export default class WorkspaceWorker {
87
87
  return [];
88
88
  return [projectFiles, TEST_FILE_PATTERNS, this.isRoot ? this.negatedWorkspacePatterns : []]
89
89
  .flat()
90
- .map(p => p.replace(/!$/, ''));
90
+ .map(removeProductionSuffix);
91
91
  }
92
92
  getPluginEntryFilePatterns(isProduction = false) {
93
93
  const patterns = [];
@@ -103,7 +103,7 @@ export default class WorkspaceWorker {
103
103
  }
104
104
  }
105
105
  }
106
- return patterns.flat().map(p => p.replace(/!$/, ''));
106
+ return patterns.flat().map(removeProductionSuffix);
107
107
  }
108
108
  getPluginProjectFilePatterns() {
109
109
  const patterns = [];
@@ -118,7 +118,7 @@ export default class WorkspaceWorker {
118
118
  patterns.push(projectFilePatterns.length > 0 ? projectFilePatterns : entryFilesPatterns);
119
119
  }
120
120
  }
121
- return patterns.flat().map(p => p.replace(/!$/, ''));
121
+ return patterns.flat().map(removeProductionSuffix);
122
122
  }
123
123
  getPluginConfigPatterns() {
124
124
  const patterns = [];
@@ -130,7 +130,7 @@ export default class WorkspaceWorker {
130
130
  patterns.push(...pluginConfig.config);
131
131
  }
132
132
  }
133
- return patterns.flat().map(p => p.replace(/!$/, ''));
133
+ return patterns.flat().map(removeProductionSuffix);
134
134
  }
135
135
  getProductionEntryFilePatterns() {
136
136
  const entryFiles = this.config.entryFiles.filter(p => p.endsWith('!'));
@@ -139,18 +139,18 @@ export default class WorkspaceWorker {
139
139
  const negatedEntryFiles = this.config.entryFiles.filter(p => !p.endsWith('!')).map(negate);
140
140
  return [entryFiles, negatedEntryFiles, negatedTestFilePatterns, this.isRoot ? this.negatedWorkspacePatterns : []]
141
141
  .flat()
142
- .map(p => p.replace(/!$/, ''));
142
+ .map(removeProductionSuffix);
143
143
  }
144
144
  getProductionProjectFilePatterns() {
145
145
  const projectFiles = this.config.projectFiles;
146
146
  if (projectFiles.length === 0)
147
147
  return this.getProductionEntryFilePatterns();
148
- const _projectFiles = this.config.projectFiles.map(p => {
149
- if (!p.endsWith('!') && !p.startsWith('!'))
150
- return negate(p);
151
- return p;
148
+ const _projectFiles = this.config.projectFiles.map(pattern => {
149
+ if (!pattern.endsWith('!') && !pattern.startsWith('!'))
150
+ return negate(pattern);
151
+ return pattern;
152
152
  });
153
- const negatedEntryFiles = this.config.entryFiles.filter(p => !p.endsWith('!')).map(negate);
153
+ const negatedEntryFiles = this.config.entryFiles.filter(pattern => !pattern.endsWith('!')).map(negate);
154
154
  const negatedPluginConfigPatterns = this.getPluginConfigPatterns().map(negate);
155
155
  const negatedPluginEntryFilePatterns = this.getPluginEntryFilePatterns(true).map(negate);
156
156
  const negatedPluginProjectFilePatterns = this.getPluginProjectFilePatterns().map(negate);
@@ -164,7 +164,7 @@ export default class WorkspaceWorker {
164
164
  this.isRoot ? this.negatedWorkspacePatterns : [],
165
165
  ]
166
166
  .flat()
167
- .map(p => p.replace(/!$/, ''));
167
+ .map(removeProductionSuffix);
168
168
  }
169
169
  getProductionPluginEntryFilePatterns() {
170
170
  const patterns = [];
@@ -178,10 +178,9 @@ export default class WorkspaceWorker {
178
178
  }
179
179
  }
180
180
  }
181
- const p = patterns.flat().map(p => p.replace(/!$/, ''));
182
- if (p.length === 0)
181
+ if (patterns.length === 0)
183
182
  return [];
184
- return [p, negatedTestFilePatterns].flat();
183
+ return [patterns.flat().map(removeProductionSuffix), negatedTestFilePatterns].flat();
185
184
  }
186
185
  getConfigurationEntryFilePattern(pluginName) {
187
186
  const pluginConfig = this.getConfigForPlugin(pluginName);
@@ -213,14 +212,19 @@ export default class WorkspaceWorker {
213
212
  const cwd = this.dir;
214
213
  const ignore = this.getWorkspaceIgnorePatterns();
215
214
  const configFilePaths = await _pureGlob({ patterns, cwd, ignore });
215
+ const pluginConfig = this.getConfigForPlugin(pluginName);
216
216
  debugLogFiles(1, `Globbed ${pluginName} config file paths`, configFilePaths);
217
217
  if (configFilePaths.length === 0)
218
218
  return [];
219
- const referencedDependencies = (await Promise.all(configFilePaths.map(async (configFilePath) => {
220
- const dependencies = await pluginCallback(configFilePath, { cwd, manifest: this.manifest });
221
- return dependencies.map(symbol => ({ filePath: configFilePath, symbol }));
219
+ const referencedDependencyIssues = (await Promise.all(configFilePaths.map(async (configFilePath) => {
220
+ const dependencies = await pluginCallback(configFilePath, {
221
+ cwd,
222
+ manifest: this.manifest,
223
+ config: pluginConfig,
224
+ });
225
+ return dependencies.map(symbol => ({ type: 'unlisted', filePath: configFilePath, symbol }));
222
226
  }))).flat();
223
- debugLogIssues(1, `Dependencies used by ${pluginName} configuration`, referencedDependencies);
224
- return referencedDependencies;
227
+ debugLogIssues(1, `Dependencies used by ${pluginName} configuration`, referencedDependencyIssues);
228
+ return referencedDependencyIssues;
225
229
  }
226
230
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knip",
3
- "version": "1.0.0-alpha.0",
3
+ "version": "1.0.0-alpha.2",
4
4
  "description": "Find unused files, dependencies and exports in your TypeScript and JavaScript project",
5
5
  "keywords": [
6
6
  "find",