diff --git a/packages/knip/fixtures/import-star-iteration/fruit.ts b/packages/knip/fixtures/import-star-iteration/fruit.ts new file mode 100644 index 000000000..5b5dbf12f --- /dev/null +++ b/packages/knip/fixtures/import-star-iteration/fruit.ts @@ -0,0 +1,7 @@ +export class Orange { + public message = "I am an orange"; +} + +export class Apple { + public message = "I am an apple"; +} diff --git a/packages/knip/fixtures/import-star-iteration/index.ts b/packages/knip/fixtures/import-star-iteration/index.ts new file mode 100644 index 000000000..ac7dcf575 --- /dev/null +++ b/packages/knip/fixtures/import-star-iteration/index.ts @@ -0,0 +1,18 @@ +import * as fruitClasses from "./fruit"; +import * as veggieClasses from "./vegetables"; + +// Outputs: +// I am an orange +// I am an apple +// I am broccoli +// I am spinach + +for (const className in fruitClasses) { + const classInstance = new fruitClasses[className](); + console.log(`${classInstance.message}`); +} + +for (const myClass of veggieClasses) { + const classInstance = new myClass(); + console.log(classInstance.message); +} diff --git a/packages/knip/fixtures/import-star-iteration/package.json b/packages/knip/fixtures/import-star-iteration/package.json new file mode 100644 index 000000000..c951d7840 --- /dev/null +++ b/packages/knip/fixtures/import-star-iteration/package.json @@ -0,0 +1,17 @@ +{ + "name": "@fixtures/import-star-iteration", + "devDependencies": { + "ts-node": "^10.8.2" + }, + "scripts": { + "execute-test-code": "ts-node index.ts" + }, + "knip": { + "ignoreBinaries": [ + "ts-node" + ], + "ignoreDependencies": [ + "ts-node" + ] + } +} diff --git a/packages/knip/fixtures/import-star-iteration/tsconfig.json b/packages/knip/fixtures/import-star-iteration/tsconfig.json new file mode 100644 index 000000000..875cb6001 --- /dev/null +++ b/packages/knip/fixtures/import-star-iteration/tsconfig.json @@ -0,0 +1,3 @@ +{ + "compilerOptions": {} +} diff --git a/packages/knip/fixtures/import-star-iteration/vegetables.ts b/packages/knip/fixtures/import-star-iteration/vegetables.ts new file mode 100644 index 000000000..23cba9ae0 --- /dev/null +++ b/packages/knip/fixtures/import-star-iteration/vegetables.ts @@ -0,0 +1,11 @@ +class Broccoli { + public message = "I am broccoli"; +} + +class Spinach { + public message = "I am spinach"; +} + +// This is contrived, but this leads to us being able to use for (...of) in index.ts +const veggieClasses = [Broccoli, Spinach]; +export = veggieClasses; // Makes the file a module diff --git a/packages/knip/src/typescript/ast-helpers.ts b/packages/knip/src/typescript/ast-helpers.ts index 0ca284c81..909a783ee 100644 --- a/packages/knip/src/typescript/ast-helpers.ts +++ b/packages/knip/src/typescript/ast-helpers.ts @@ -165,6 +165,11 @@ export const isDestructuring = (node: ts.Node) => ts.isVariableDeclarationList(node.parent.parent) && ts.isObjectBindingPattern(node.parent.name); +// Pattern: for (const x in NS) { } +// Pattern: for (const x of NS) { } +export const isIteratingObject = (node: ts.Node) => + node.parent && (ts.isForInStatement(node.parent) || ts.isForOfStatement(node.parent)); + export const getDestructuredIds = (name: ts.ObjectBindingPattern) => name.elements.map(element => element.name.getText()); diff --git a/packages/knip/src/typescript/get-imports-and-exports.ts b/packages/knip/src/typescript/get-imports-and-exports.ts index 9ddd0e854..2d072fcb8 100644 --- a/packages/knip/src/typescript/get-imports-and-exports.ts +++ b/packages/knip/src/typescript/get-imports-and-exports.ts @@ -22,6 +22,7 @@ import { isConsiderReferencedNS, isDestructuring, isImportSpecifier, + isIteratingObject, isObjectEnumerationCallExpressionArgument, isReferencedInExport, } from './ast-helpers.js'; @@ -353,6 +354,10 @@ const getImportsAndExports = ( } else if (isObjectEnumerationCallExpressionArgument(node)) { // Pattern: Object.keys(NS) imports.refs.add(id); + } else if (isIteratingObject(node)) { + // Pattern: for (const x in NS) { } + // Pattern: for (const x of NS) { } + imports.refs.add(id); } } } diff --git a/packages/knip/test/import-star-iteration.ts b/packages/knip/test/import-star-iteration.ts new file mode 100644 index 000000000..1a7214f98 --- /dev/null +++ b/packages/knip/test/import-star-iteration.ts @@ -0,0 +1,23 @@ +import { test } from 'bun:test'; +import assert from 'node:assert/strict'; +import { main as knip } from '../src/index.js'; +import { resolve } from '../src/util/path.js'; +import baseArguments from './helpers/baseArguments.js'; +import baseCounters from './helpers/baseCounters.js'; + +const cwd = resolve('fixtures/import-star-iteration'); + +test('Handle usage of members of a namespace when imported using * and iterating', async () => { + const { counters } = await knip({ + ...baseArguments, + cwd, + }); + + // Classes Orange and Apple are both used using a for (...in) loop + // Classes Broccoli and Spinach are both used using a for (...of) loop + assert.deepEqual(counters, { + ...baseCounters, + processed: 3, + total: 3, + }); +});