diff --git a/packages/knip/src/IssueCollector.ts b/packages/knip/src/IssueCollector.ts index 1dcc861a8..25f3ada17 100644 --- a/packages/knip/src/IssueCollector.ts +++ b/packages/knip/src/IssueCollector.ts @@ -75,9 +75,10 @@ export class IssueCollector { if (this.isMatch(issue.filePath)) return; const key = relative(this.cwd, issue.filePath); issue.severity = this.rules[issue.type]; - this.issues[issue.type][key] = this.issues[issue.type][key] ?? {}; - if (!this.issues[issue.type][key][issue.symbol]) { - this.issues[issue.type][key][issue.symbol] = issue; + const issues = this.issues[issue.type]; + issues[key] = issues[key] ?? {}; + if (!issues[key][issue.symbol]) { + issues[key][issue.symbol] = issue; this.counters[issue.type]++; } return issue; diff --git a/packages/knip/src/index.ts b/packages/knip/src/index.ts index e5dda3a36..8378492ee 100644 --- a/packages/knip/src/index.ts +++ b/packages/knip/src/index.ts @@ -582,7 +582,14 @@ export const main = async (unresolvedConfiguration: CommandLineOptions) => { for (const specifier of file.imports.external) { const packageName = getPackageNameFromModuleSpecifier(specifier); const isHandled = packageName && deputy.maybeAddReferencedExternalDependency(ws, packageName); - if (!isHandled) collector.addIssue({ type: 'unlisted', filePath, workspace: ws.name, symbol: specifier }); + if (!isHandled) + collector.addIssue({ + type: 'unlisted', + filePath, + workspace: ws.name, + symbol: packageName ?? specifier, + specifier, + }); } } diff --git a/packages/knip/src/reporters/symbols.ts b/packages/knip/src/reporters/symbols.ts index ec47cd062..9eeaa9aba 100644 --- a/packages/knip/src/reporters/symbols.ts +++ b/packages/knip/src/reporters/symbols.ts @@ -6,14 +6,26 @@ import type { Issue, ReporterOptions } from '../types/issues.js'; import { relative, toRelative } from '../util/path.js'; import { getTitle, identity, logTitle } from './util.js'; +const dim = picocolors.gray; +const bright = picocolors.whiteBright; + const TRUNCATE_WIDTH = 40; const truncate = (text: string) => (text.length > TRUNCATE_WIDTH ? `${text.slice(0, TRUNCATE_WIDTH - 3)}...` : text); +const hl = (issue: Issue) => { + if (issue.specifier && issue.specifier !== issue.symbol && issue.specifier.includes(issue.symbol)) { + const parts = issue.specifier.split(issue.symbol); + const rest = parts.slice(1).join(''); + return [dim(parts[0]), bright(issue.symbol), dim(rest)].join(''); + } + return issue.symbol; +}; + const logIssueRecord = (issues: Issue[]) => { const table = new EasyTable(); for (const issue of issues) { - const print = issue.isFixed || issue.severity === 'warn' ? picocolors.gray : identity; - table.cell('symbol', print(issue.symbols ? truncate(issue.symbols.map(s => s.symbol).join(', ')) : issue.symbol)); + const print = issue.isFixed || issue.severity === 'warn' ? dim : identity; + table.cell('symbol', print(issue.symbols ? truncate(issue.symbols.map(s => s.symbol).join(', ')) : hl(issue))); issue.parentSymbol && table.cell('parentSymbol', print(issue.parentSymbol)); issue.symbolType && table.cell('symbolType', print(issue.symbolType)); const pos = issue.line === undefined ? '' : `:${issue.line}${issue.col === undefined ? '' : `:${issue.col}`}`; @@ -41,8 +53,8 @@ export default ({ report, issues, tagHints, configurationHints, noConfigHints, i title && logTitle(title, issuesForType.length); for (const issue of issuesForType) { const relPath = toRelative(issue.filePath); - if (issue.isFixed) console.log(picocolors.gray(`${relPath} (removed)`)); - else if (issue.severity === 'warn') console.log(picocolors.gray(relPath)); + if (issue.isFixed) console.log(dim(`${relPath} (removed)`)); + else if (issue.severity === 'warn') console.log(dim(relPath)); else console.log(relPath); } totalIssues = totalIssues + issuesForType.length; @@ -66,7 +78,7 @@ export default ({ report, issues, tagHints, configurationHints, noConfigHints, i const message = `Unused item in ${type}`; const workspace = workspaceName && workspaceName !== ROOT_WORKSPACE_NAME ? ` (workspace: ${workspaceName})` : ''; - console.warn(picocolors.gray(`${message}${workspace}:`), identifier); + console.warn(dim(`${message}${workspace}:`), identifier); } } if (tagHints.size > 0) { @@ -74,7 +86,7 @@ export default ({ report, issues, tagHints, configurationHints, noConfigHints, i for (const hint of tagHints) { const { filePath, identifier, tagName } = hint; const message = `Unused tag in ${toRelative(filePath)}:`; - console.warn(picocolors.gray(message), `${identifier} → ${tagName}`); + console.warn(dim(message), `${identifier} → ${tagName}`); } } } diff --git a/packages/knip/src/types/issues.ts b/packages/knip/src/types/issues.ts index 215035995..ef1d42cc6 100644 --- a/packages/knip/src/types/issues.ts +++ b/packages/knip/src/types/issues.ts @@ -18,6 +18,7 @@ export type Issue = { symbols?: IssueSymbol[]; symbolType?: SymbolType; parentSymbol?: string; + specifier?: string; severity?: IssueSeverity; pos?: number; line?: number; diff --git a/packages/knip/src/util/get-referenced-inputs.ts b/packages/knip/src/util/get-referenced-inputs.ts index 116a5dbdc..abbf3e8af 100644 --- a/packages/knip/src/util/get-referenced-inputs.ts +++ b/packages/knip/src/util/get-referenced-inputs.ts @@ -37,6 +37,7 @@ export const getReferencedInputsHandler = filePath: containingFilePath, workspace: workspace.name, symbol: binaryName, + specifier, }); return; } @@ -59,7 +60,8 @@ export const getReferencedInputsHandler = type: 'unlisted', filePath: containingFilePath, workspace: specifierWorkspace.name, - symbol: specifier, + symbol: packageName ?? specifier, + specifier, }); } return; @@ -92,6 +94,7 @@ export const getReferencedInputsHandler = filePath: containingFilePath, workspace: workspace.name, symbol: packageName ?? specifier, + specifier, }); } else if (!isGitIgnored(filePath)) { // Let's start out conservatively diff --git a/packages/knip/test/module-resolution-tsconfig-paths.test.ts b/packages/knip/test/module-resolution-tsconfig-paths.test.ts index 7bc112660..ea49e42aa 100644 --- a/packages/knip/test/module-resolution-tsconfig-paths.test.ts +++ b/packages/knip/test/module-resolution-tsconfig-paths.test.ts @@ -16,7 +16,7 @@ test('Resolve modules properly using tsconfig paths and globs', async () => { assert.equal(issues.dependencies['package.json']['internal'].symbol, 'internal'); assert.equal(issues.unlisted['index.ts']['@unknown'].symbol, '@unknown'); - assert.equal(issues.unlisted['index.ts']['unresolved/dir'].symbol, 'unresolved/dir'); + assert.equal(issues.unlisted['index.ts']['unresolved'].symbol, 'unresolved'); assert.equal(issues.exports['internal-package/index.ts']['unused'].symbol, 'unused'); assert.equal(issues.exports['unprefixed/module.ts']['unused'].symbol, 'unused'); diff --git a/packages/knip/test/plugins/cypress-multi-reporter.test.ts b/packages/knip/test/plugins/cypress-multi-reporter.test.ts index 7ee18af48..a73265dd5 100644 --- a/packages/knip/test/plugins/cypress-multi-reporter.test.ts +++ b/packages/knip/test/plugins/cypress-multi-reporter.test.ts @@ -13,9 +13,9 @@ test('Find dependencies with the cypress-multi-reporter plugin', async () => { cwd, }); - assert(issues.unlisted['cypress.config.ts']['@nrwl/cypress/plugins/cypress-preset']); + assert(issues.unlisted['cypress.config.ts']['@nrwl/cypress']); assert(issues.unlisted['cypress/support/commands.ts']['@faker-js/faker']); - assert(issues.unlisted['cypress/support/e2e.ts']['@testing-library/cypress/add-commands']); + assert(issues.unlisted['cypress/support/e2e.ts']['@testing-library/cypress']); assert(issues.unresolved['cypress.config.ts']['@testing-library/my-fake-reporter']); assert.deepEqual(counters, { diff --git a/packages/knip/test/plugins/cypress.test.ts b/packages/knip/test/plugins/cypress.test.ts index 9739e0e14..9e056983c 100644 --- a/packages/knip/test/plugins/cypress.test.ts +++ b/packages/knip/test/plugins/cypress.test.ts @@ -13,9 +13,9 @@ test('Find dependencies with the Cypress plugin', async () => { cwd, }); - assert(issues.unlisted['cypress.config.ts']['@nrwl/cypress/plugins/cypress-preset']); + assert(issues.unlisted['cypress.config.ts']['@nrwl/cypress']); assert(issues.unlisted['cypress/support/commands.ts']['@faker-js/faker']); - assert(issues.unlisted['cypress/support/e2e.ts']['@testing-library/cypress/add-commands']); + assert(issues.unlisted['cypress/support/e2e.ts']['@testing-library/cypress']); assert.deepEqual(counters, { ...baseCounters, diff --git a/packages/knip/test/plugins/jest.test.ts b/packages/knip/test/plugins/jest.test.ts index cd79cc0ac..5cadb4917 100644 --- a/packages/knip/test/plugins/jest.test.ts +++ b/packages/knip/test/plugins/jest.test.ts @@ -14,7 +14,7 @@ test('Find dependencies with the Jest plugin', async () => { }); assert(issues.unlisted['jest.config.shared.js']['@jest/types']); - assert(issues.unlisted['jest.setup.js']['@testing-library/jest-dom/extend-expect']); + assert(issues.unlisted['jest.setup.js']['@testing-library/jest-dom']); assert(issues.unlisted['jest.config.js']['@jest/types']); assert(issues.unresolved['jest.config.js']['@nrwl/react/plugins/jest']); assert(issues.unresolved['jest.config.js']['babel-jest']); diff --git a/packages/knip/test/plugins/nx-crystal.test.ts b/packages/knip/test/plugins/nx-crystal.test.ts index 2399e283c..6a4939e1a 100644 --- a/packages/knip/test/plugins/nx-crystal.test.ts +++ b/packages/knip/test/plugins/nx-crystal.test.ts @@ -15,7 +15,7 @@ test('Find dependencies with the Nx plugin', async () => { assert(issues.devDependencies['package.json']['@nx/cypress']); assert(issues.devDependencies['package.json']['@nrwl/workspace']); - assert(issues.unlisted['nx.json']['@nx/nuxt/plugin']); + assert(issues.unlisted['nx.json']['@nx/nuxt']); assert(issues.binaries['package.json']['nx']); assert.deepEqual(counters, { diff --git a/packages/knip/test/plugins/remix.test.ts b/packages/knip/test/plugins/remix.test.ts index 325e78065..e24f63ef2 100644 --- a/packages/knip/test/plugins/remix.test.ts +++ b/packages/knip/test/plugins/remix.test.ts @@ -26,11 +26,11 @@ test('Find dependencies with the Remix plugin', async () => { assert(issues.unlisted['app/entry.client.tsx']['@remix-run/react']); assert(issues.unlisted['app/entry.client.tsx']['react']); - assert(issues.unlisted['app/entry.client.tsx']['react-dom/client']); + assert(issues.unlisted['app/entry.client.tsx']['react-dom']); assert(issues.unlisted['app/entry.server.tsx']['@remix-run/node']); assert(issues.unlisted['app/entry.server.tsx']['@remix-run/react']); - assert(issues.unlisted['app/entry.server.tsx']['react-dom/server']); + assert(issues.unlisted['app/entry.server.tsx']['react-dom']); assert(issues.unlisted['app/root.tsx']['@remix-run/node']); assert(issues.unlisted['app/root.tsx']['@remix-run/react']); diff --git a/packages/knip/test/plugins/storybook.test.ts b/packages/knip/test/plugins/storybook.test.ts index 1db3c71cd..36c77e1bb 100644 --- a/packages/knip/test/plugins/storybook.test.ts +++ b/packages/knip/test/plugins/storybook.test.ts @@ -17,7 +17,7 @@ test('Find dependencies with Storybook plugin', async () => { assert(issues.unlisted['main.js']['@storybook/builder-webpack5']); assert(issues.unlisted['main.js']['@storybook/manager-webpack5']); assert(issues.unlisted['main.js']['@storybook/react-webpack5']); - assert(issues.unlisted['preview.js']['cypress-storybook/react']); + assert(issues.unlisted['preview.js']['cypress-storybook']); assert(issues.unresolved['main.js']['@storybook/addon-knobs/preset']); assert(issues.unresolved['main.js']['storybook-addon-export-to-codesandbox']); assert(issues.binaries['package.json']['storybook']); diff --git a/packages/knip/test/plugins/typescript.test.ts b/packages/knip/test/plugins/typescript.test.ts index d19a1f5a3..a14749844 100644 --- a/packages/knip/test/plugins/typescript.test.ts +++ b/packages/knip/test/plugins/typescript.test.ts @@ -19,7 +19,7 @@ test('Find dependencies with the TypeScript plugin', async () => { assert(issues.unlisted['tsconfig.jsx-import-source-preact.json']['preact']); assert(issues.unresolved['tsconfig.jsx-import-source-preact.json']['preact']); assert(issues.unresolved['tsconfig.jsx-import-source-react.json']['vitest/globals']); - assert(issues.unlisted['tsconfig.jsx-import-source-react.json']['hastscript/svg']); + assert(issues.unlisted['tsconfig.jsx-import-source-react.json']['hastscript']); assert.deepEqual(counters, { ...baseCounters, @@ -39,7 +39,7 @@ test('Find dependencies with the TypeScript plugin (production)', async () => { }); assert(issues.unlisted['tsconfig.jsx-import-source-preact.json']['preact']); - assert(issues.unlisted['tsconfig.jsx-import-source-react.json']['hastscript/svg']); + assert(issues.unlisted['tsconfig.jsx-import-source-react.json']['hastscript']); assert.deepEqual(counters, { ...baseCounters, diff --git a/packages/knip/test/workspaces-paths.test.ts b/packages/knip/test/workspaces-paths.test.ts index 424807554..d7f1c1b69 100644 --- a/packages/knip/test/workspaces-paths.test.ts +++ b/packages/knip/test/workspaces-paths.test.ts @@ -13,7 +13,7 @@ test('Find unused files, dependencies and exports in workspaces (w/ paths)', asy }); assert.equal(Object.keys(issues.unlisted).length, 1); - assert(issues.unlisted['packages/lib-e/src/index.ts']['not/found']); + assert(issues.unlisted['packages/lib-e/src/index.ts']['not']); assert.deepEqual(counters, { ...baseCounters, diff --git a/packages/knip/test/workspaces.test.ts b/packages/knip/test/workspaces.test.ts index 890261de9..1ae9d2f71 100644 --- a/packages/knip/test/workspaces.test.ts +++ b/packages/knip/test/workspaces.test.ts @@ -27,7 +27,7 @@ test('Find unused files, dependencies and exports in workspaces (default)', asyn assert(issues.unlisted['apps/frontend/index.ts']['vanilla-js']); assert(issues.unlisted['apps/backend/index.ts']['globby']); assert(issues.unlisted['apps/backend/index.ts']['js-yaml']); - assert(issues.unlisted['packages/tools/tsconfig.json']['@workspaces/tsconfig/tsconfig.base.json']); + assert(issues.unlisted['packages/tools/tsconfig.json']['@workspaces/tsconfig']); assert(issues.types['packages/shared/types.ts']['UnusedEnum']);