Skip to content

Commit 41c2017

Browse files
committedFeb 10, 2024
Reorganize exports/types and nsExports/nsTypes issue types (resolves #475)
1 parent 87917df commit 41c2017

33 files changed

+353
-132
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export * as ReExported from './re-exported-module';
2+
import * as NS from './namespace';
3+
import * as NS2 from './namespace2';
4+
import * as NS3 from './namespace3';
5+
import * as NS4 from './namespace4';
6+
import * as NS5 from './namespace5';
7+
import * as NS6 from './namespace6';
8+
import fn from 'external';
9+
10+
NS.identifier15;
11+
NS['identifier16'];
12+
NS.identifier17();
13+
14+
const { identifier18, identifier19, identifier20 } = NS2;
15+
16+
NS2.identifier21.method();
17+
18+
function usage() {
19+
const hello = { NS3 };
20+
}
21+
22+
fn(NS4);
23+
24+
const spread = { ...NS5 };
25+
26+
const assign = NS6;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const identifier15 = 1;
2+
export const identifier16 = 1;
3+
export const identifier17 = () => 1;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const identifier18 = 1;
2+
export const identifier19 = 1;
3+
export const identifier20 = () => 1;
4+
export const identifier21 = { method: () => 1 };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const identifier31 = 31;
2+
export const identifier32 = 32;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const identifier33 = 33;
2+
export const identifier34 = 34;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const identifier35 = 35;
2+
export const identifier36 = 36;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const identifier37 = 37;
2+
export const identifier38 = 38;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "@fixtures/imports-namespace-with-nsexports",
3+
"knip": {
4+
"include": [
5+
"nsExports"
6+
]
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const myFunction = () => void 0;
2+
3+
export default myFunction;

‎packages/knip/fixtures/imports-namespace/index.ts

+15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
export * as ReExported from './re-exported-module';
22
import * as NS from './namespace';
33
import * as NS2 from './namespace2';
4+
import * as NS3 from './namespace3';
5+
import * as NS4 from './namespace4';
6+
import * as NS5 from './namespace5';
7+
import * as NS6 from './namespace6';
8+
import fn from 'external';
49

510
NS.identifier15;
611
NS['identifier16'];
@@ -9,3 +14,13 @@ NS.identifier17();
914
const { identifier18, identifier19, identifier20 } = NS2;
1015

1116
NS2.identifier21.method();
17+
18+
function usage() {
19+
const hello = { NS3 };
20+
}
21+
22+
fn(NS4);
23+
24+
const spread = { ...NS5 };
25+
26+
const assign = NS6;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const identifier31 = 31;
2+
export const identifier32 = 32;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const identifier33 = 33;
2+
export const identifier34 = 34;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const identifier35 = 35;
2+
export const identifier36 = 36;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const identifier37 = 37;
2+
export const identifier38 = 38;
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
11
{
2-
"name": "@fixtures/namespace",
3-
"knip": {
4-
"entry": [
5-
"index.ts"
6-
],
7-
"project": [
8-
"*.ts"
9-
]
10-
}
2+
"name": "@fixtures/imports-namespace"
113
}

‎packages/knip/src/constants.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,9 @@ export const ISSUE_TYPE_TITLE: Record<IssueType, string> = {
113113
binaries: 'Unlisted binaries',
114114
unresolved: 'Unresolved imports',
115115
exports: 'Unused exports',
116-
nsExports: 'Unused exports in namespaces',
116+
nsExports: 'Exports in used namespace',
117117
types: 'Unused exported types',
118-
nsTypes: 'Unused exported types in namespaces',
118+
nsTypes: 'Exported types in used namespace',
119119
enumMembers: 'Unused exported enum members',
120120
classMembers: 'Unused exported class members',
121121
duplicates: 'Duplicate exports',

‎packages/knip/src/index.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { fromBinary, isBinary } from './util/protocols.js';
2323
import { _resolveSpecifier } from './util/require.js';
2424
import { shouldIgnore } from './util/tag.js';
2525
import { loadTSConfig } from './util/tsconfig-loader.js';
26+
import { getType, getHasStrictlyNsReferences } from './util/type.js';
2627
import { WorkspaceWorker } from './WorkspaceWorker.js';
2728
import type { Workspace } from './ConfigurationChief.js';
2829
import type { CommandLineOptions } from './types/cli.js';
@@ -577,19 +578,20 @@ export const main = async (unresolvedConfiguration: CommandLineOptions) => {
577578
}
578579
}
579580

580-
const hasNsImport = Boolean(importsForExport?.hasStar);
581+
const [hasStrictlyNsReferences, namespace] = getHasStrictlyNsReferences(importsForExport);
581582

582583
const isType = ['enum', 'type', 'interface'].includes(exportedItem.type);
583584

584-
if (hasNsImport && ((!report.nsTypes && isType) || (!report.nsExports && !isType))) continue;
585+
if (hasStrictlyNsReferences && ((!report.nsTypes && isType) || (!report.nsExports && !isType))) continue;
585586

586587
if (!isExportedItemReferenced(exportedItem)) {
587-
const type = isType ? (hasNsImport ? 'nsTypes' : 'types') : hasNsImport ? 'nsExports' : 'exports';
588+
const type = getType(hasStrictlyNsReferences, isType);
588589
collector.addIssue({
589590
type,
590591
filePath,
591592
symbol: identifier,
592593
symbolType: exportedItem.type,
594+
parentSymbol: namespace,
593595
pos: exportedItem.pos,
594596
line: exportedItem.line,
595597
col: exportedItem.col,

‎packages/knip/src/reporters/json.ts

+11-11
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { OwnershipEngine } from '@snyk/github-codeowners/dist/lib/ownership/inde
22
import { isFile } from '../util/fs.js';
33
import { relative, resolve } from '../util/path.js';
44
import { convert } from './util.js';
5-
import type { Report, ReporterOptions, IssueRecords, SymbolIssueType, Issue } from '../types/issues.js';
5+
import type { Report, ReporterOptions, IssueRecords, Issue } from '../types/issues.js';
66
import type { Entries } from 'type-fest';
77

88
type ExtraReporterOptions = {
@@ -22,14 +22,13 @@ type Row = {
2222
unresolved?: Array<{ name: string }>;
2323
exports?: Array<Item>;
2424
types?: Array<Item>;
25+
nsExports?: Array<Item>;
26+
nsTypes?: Array<Item>;
2527
duplicates?: Array<Item[]>;
2628
enumMembers?: Record<string, Array<Item>>;
2729
classMembers?: Record<string, Array<Item>>;
2830
};
2931

30-
const mergeTypes = (type: SymbolIssueType) =>
31-
type === 'exports' || type === 'nsExports' ? 'exports' : type === 'types' || type === 'nsTypes' ? 'types' : type;
32-
3332
export default async ({ report, issues, options }: ReporterOptions) => {
3433
let opts: ExtraReporterOptions = {};
3534
try {
@@ -55,22 +54,23 @@ export default async ({ report, issues, options }: ReporterOptions) => {
5554
...(report.unlisted && { unlisted: [] }),
5655
...(report.binaries && { binaries: [] }),
5756
...(report.unresolved && { unresolved: [] }),
58-
...((report.exports || report.nsExports) && { exports: [] }),
59-
...((report.types || report.nsTypes) && { types: [] }),
57+
...(report.exports && { exports: [] }),
58+
...(report.nsExports && { nsExports: [] }),
59+
...(report.types && { types: [] }),
60+
...(report.nsTypes && { nsTypes: [] }),
6061
...(report.enumMembers && { enumMembers: {} }),
6162
...(report.classMembers && { classMembers: {} }),
6263
...(report.duplicates && { duplicates: [] }),
6364
};
6465
return row;
6566
};
6667

67-
for (const [reportType, isReportType] of Object.entries(report) as Entries<Report>) {
68+
for (const [type, isReportType] of Object.entries(report) as Entries<Report>) {
6869
if (isReportType) {
69-
if (reportType === 'files') {
70+
if (type === 'files') {
7071
// Ignore
7172
} else {
72-
const type = mergeTypes(reportType);
73-
flatten(issues[reportType] as IssueRecords).forEach(issue => {
73+
flatten(issues[type] as IssueRecords).forEach(issue => {
7474
const { filePath, symbol, symbols, parentSymbol } = issue;
7575
json[filePath] = json[filePath] ?? initRow(filePath);
7676
if (type === 'duplicates') {
@@ -82,7 +82,7 @@ export default async ({ report, issues, options }: ReporterOptions) => {
8282
item[parentSymbol].push(convert(issue));
8383
}
8484
} else {
85-
if (type === 'exports' || type === 'types' || type === 'unresolved') {
85+
if (['exports', 'nsExports', 'types', 'nsTypes', 'unresolved'].includes(type)) {
8686
json[filePath][type]?.push(convert(issue));
8787
} else {
8888
json[filePath][type]?.push({ name: symbol });

‎packages/knip/src/typescript/getImportsAndExports.ts

+49-12
Original file line numberDiff line numberDiff line change
@@ -285,19 +285,56 @@ const getImportsAndExports = (
285285
result && (Array.isArray(result) ? result.forEach(addScript) : addScript(result));
286286
}
287287

288-
if (ts.isIdentifier(node) && isAccessExpression(node.parent)) {
289-
const symbol = sourceFile.locals?.get(String(node.escapedText));
290-
if (symbol) {
291-
if (importedInternalSymbols.has(symbol)) {
292-
let members: string[] = [];
293-
let current: ts.Node = node.parent;
294-
while (current) {
295-
const ms = getMemberStringLiterals(typeChecker, current);
296-
if (!ms) break;
297-
members = members.concat(ms.flatMap(id => (members.length === 0 ? id : members.map(ns => `${ns}.${id}`))));
298-
current = current.parent;
288+
if (ts.isIdentifier(node)) {
289+
if (isAccessExpression(node.parent)) {
290+
const symbol = sourceFile.locals?.get(String(node.escapedText));
291+
if (symbol) {
292+
if (importedInternalSymbols.has(symbol)) {
293+
let members: string[] = [];
294+
let current: ts.Node = node.parent;
295+
while (current) {
296+
const ms = getMemberStringLiterals(typeChecker, current);
297+
if (!ms) break;
298+
members = members.concat(
299+
ms.flatMap(id => (members.length === 0 ? id : members.map(ns => `${ns}.${id}`)))
300+
);
301+
current = current.parent;
302+
}
303+
maybeAddAccessExpressionAsNsImport(String(node.escapedText), members);
304+
}
305+
}
306+
} else if (
307+
// TODO Ideally we store NamespaceImport symbols and check directly against those, but can't get symbols to match
308+
ts.isShorthandPropertyAssignment(node.parent) ||
309+
(ts.isCallExpression(node.parent) && node.parent.arguments.includes(node)) ||
310+
ts.isSpreadAssignment(node.parent) ||
311+
ts.isExportAssignment(node.parent)
312+
) {
313+
const symbol = sourceFile.locals?.get(String(node.escapedText));
314+
if (symbol) {
315+
const importedSymbolFilePath = importedInternalSymbols.get(symbol);
316+
if (importedSymbolFilePath) {
317+
internalImports[importedSymbolFilePath].identifiers.add(String(node.escapedText));
318+
}
319+
}
320+
} else if (ts.isVariableDeclaration(node.parent)) {
321+
if (ts.isVariableDeclarationList(node.parent.parent) && ts.isObjectBindingPattern(node.parent.name)) {
322+
const symbol = sourceFile.locals?.get(String(node.escapedText));
323+
if (symbol) {
324+
const importedSymbolFilePath = importedInternalSymbols.get(symbol);
325+
if (importedSymbolFilePath) {
326+
const members = node.parent.name.elements.flatMap(decl => decl.name.getText());
327+
maybeAddAccessExpressionAsNsImport(String(node.escapedText), members);
328+
}
329+
}
330+
} else if (node.parent.initializer === node) {
331+
const symbol = sourceFile.locals?.get(String(node.escapedText));
332+
if (symbol) {
333+
const importedSymbolFilePath = importedInternalSymbols.get(symbol);
334+
if (importedSymbolFilePath) {
335+
internalImports[importedSymbolFilePath].identifiers.add(String(node.escapedText));
336+
}
299337
}
300-
maybeAddAccessExpressionAsNsImport(String(node.escapedText), members);
301338
}
302339
}
303340
}

‎packages/knip/src/util/get-included-issue-types.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ type Options = {
1717
exports?: boolean;
1818
};
1919

20-
const defaultExcludedIssueTypes = ['classMembers'];
20+
/** @internal */
21+
export const defaultExcludedIssueTypes = ['classMembers', 'nsExports', 'nsTypes'];
2122
const defaultIssueTypes = ISSUE_TYPES.filter(type => !defaultExcludedIssueTypes.includes(type));
2223

2324
const normalize = (values: string[]) => values.map(value => value.split(',')).flat();

‎packages/knip/src/util/type.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { SerializableImports } from '../types/imports.js';
2+
3+
export const getHasStrictlyNsReferences = (importsForExport: SerializableImports): [boolean, string?] => {
4+
if (!importsForExport || !importsForExport.hasStar || importsForExport.importedNs.size === 0) return [false];
5+
let namespace;
6+
for (const ns of importsForExport.importedNs) {
7+
const hasNs = importsForExport.identifiers.has(ns);
8+
if (!hasNs) return [false, ns];
9+
for (const id of importsForExport.identifiers) if (id.startsWith(ns + '.')) return [false, ns];
10+
namespace = ns;
11+
}
12+
return [true, namespace];
13+
};
14+
15+
export const getType = (hasOnlyNsReference: boolean, isType: boolean) =>
16+
hasOnlyNsReference ? (isType ? 'nsTypes' : 'nsExports') : isType ? 'types' : 'exports';

‎packages/knip/test/cli-reporter-json.test.ts

+9-9
Original file line numberDiff line numberDiff line change
@@ -74,31 +74,31 @@ test('knip --reporter json (exports & types)', () => {
7474
],
7575
},
7676
{
77-
file: 'named-exports.ts',
77+
file: 'my-namespace.ts',
7878
dependencies: [],
7979
devDependencies: [],
8080
optionalPeerDependencies: [],
8181
unlisted: [],
8282
binaries: [],
8383
unresolved: [],
84-
exports: [
85-
{ name: 'renamedExport', line: 6, col: 30, pos: 179 },
86-
{ name: 'namedExport', line: 7, col: 15, pos: 215 },
87-
],
88-
types: [],
84+
exports: [{ name: 'nsUnusedKey', line: 3, col: 14, pos: 84 }],
85+
types: [{ name: 'MyNamespace', line: 5, col: 18, pos: 119 }],
8986
enumMembers: {},
9087
duplicates: [],
9188
},
9289
{
93-
file: 'my-namespace.ts',
90+
file: 'named-exports.ts',
9491
dependencies: [],
9592
devDependencies: [],
9693
optionalPeerDependencies: [],
9794
unlisted: [],
9895
binaries: [],
9996
unresolved: [],
100-
exports: [{ name: 'nsUnusedKey', line: 3, col: 14, pos: 84 }],
101-
types: [{ name: 'MyNamespace', line: 5, col: 18, pos: 119 }],
97+
exports: [
98+
{ name: 'renamedExport', line: 6, col: 30, pos: 179 },
99+
{ name: 'namedExport', line: 7, col: 15, pos: 215 },
100+
],
101+
types: [],
102102
enumMembers: {},
103103
duplicates: [],
104104
},

‎packages/knip/test/entry-js.test.ts

+6-12
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,14 @@ test('Find unused files and exports with JS entry file', async () => {
1616
assert.equal(issues.files.size, 1);
1717
assert(issues.files.has(join(cwd, 'dangling.js')));
1818

19-
assert.equal(Object.values(issues.exports).length, 1);
19+
assert.equal(Object.values(issues.exports).length, 2);
2020
assert.equal(issues.exports['my-module.ts']['unused'].symbol, 'unused');
2121
assert.equal(issues.exports['my-module.ts']['default'].symbol, 'default');
22+
assert.equal(issues.exports['my-namespace.ts']['key'].symbol, 'key');
2223

23-
assert.equal(Object.values(issues.types).length, 1);
24+
assert.equal(Object.values(issues.types).length, 2);
2425
assert.equal(issues.types['my-module.ts']['AnyType'].symbolType, 'type');
25-
26-
assert.equal(Object.values(issues.nsExports).length, 1);
27-
assert.equal(issues.nsExports['my-namespace.ts']['key'].symbol, 'key');
28-
29-
assert.equal(Object.values(issues.nsTypes).length, 1);
30-
assert.equal(issues.nsTypes['my-namespace.ts']['MyNamespace'].symbol, 'MyNamespace');
26+
assert.equal(issues.types['my-namespace.ts']['MyNamespace'].symbol, 'MyNamespace');
3127

3228
assert.equal(Object.values(issues.duplicates).length, 1);
3329
assert.equal(issues.duplicates['my-module.ts']['myExport|default'].symbols?.length, 2);
@@ -36,10 +32,8 @@ test('Find unused files and exports with JS entry file', async () => {
3632
...baseCounters,
3733
files: 1,
3834
unlisted: 0,
39-
exports: 2,
40-
nsExports: 1,
41-
nsTypes: 1,
42-
types: 1,
35+
exports: 3,
36+
types: 2,
4337
duplicates: 1,
4438
processed: 4,
4539
total: 4,

‎packages/knip/test/exports.test.ts

+10-22
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ test('Find unused exports', async () => {
1313
cwd,
1414
});
1515

16-
assert.equal(Object.values(issues.exports).length, 5);
16+
assert.equal(Object.values(issues.exports).length, 6);
1717
assert.equal(issues.exports['default.ts']['NamedExport'].symbol, 'NamedExport');
1818
assert.equal(issues.exports['my-module.ts']['default'].symbol, 'default');
1919
assert.equal(issues.exports['my-module.ts']['unusedNumber'].symbol, 'unusedNumber');
@@ -22,45 +22,33 @@ test('Find unused exports', async () => {
2222
assert.equal(issues.exports['named-exports.ts']['renamedExport'].symbol, 'renamedExport');
2323
assert.equal(issues.exports['named-exports.ts']['namedExport'].symbol, 'namedExport');
2424
assert.equal(issues.exports['dynamic-import.ts']['unusedZero'].symbol, 'unusedZero');
25+
assert.equal(issues.exports['my-namespace.ts']['nsUnusedKey'].line, 3);
26+
assert.equal(issues.exports['my-namespace.ts']['nsUnusedKey'].col, 14);
27+
assert.equal(issues.exports['my-namespace.ts']['nsUnusedKey'].symbol, 'nsUnusedKey');
2528
assert(!issues.exports['index.ts']);
2629

27-
assert.equal(Object.values(issues.types).length, 2);
30+
assert.equal(Object.values(issues.types).length, 3);
2831
assert.equal(issues.types['my-module.ts']['MyAnyType'].symbolType, 'type');
2932
assert.equal(issues.types['types.ts']['MyEnum'].symbolType, 'enum');
3033
assert.equal(issues.types['types.ts']['MyType'].symbolType, 'type');
34+
assert.equal(issues.types['my-namespace.ts']['MyNamespace'].symbol, 'MyNamespace');
3135
assert(!issues.types['index.ts']);
3236

33-
assert.equal(Object.values(issues.nsExports).length, 1);
34-
assert.equal(issues.nsExports['my-namespace.ts']['nsUnusedKey'].symbol, 'nsUnusedKey');
35-
36-
assert.equal(Object.values(issues.nsTypes).length, 1);
37-
assert.equal(issues.nsTypes['my-namespace.ts']['MyNamespace'].symbol, 'MyNamespace');
38-
3937
assert.equal(Object.values(issues.duplicates).length, 1);
4038
assert.equal(issues.duplicates['my-module.ts']['exportedResult|default'].symbols?.length, 2);
4139

4240
assert.equal(issues.exports['default.ts']['NamedExport'].line, 1);
4341
assert.equal(issues.exports['default.ts']['NamedExport'].col, 14);
44-
// assert.equal(issues.exports['default.ts']['NamedExport'].pos, 13);
4542

4643
assert.equal(issues.types['my-module.ts']['MyAnyType'].line, 19);
4744
assert.equal(issues.types['my-module.ts']['MyAnyType'].col, 13);
48-
// assert.equal(issues.types['my-module.ts']['MyAnyType'].pos, 702);
49-
50-
assert.equal(issues.nsExports['my-namespace.ts']['nsUnusedKey'].line, 3);
51-
assert.equal(issues.nsExports['my-namespace.ts']['nsUnusedKey'].col, 14);
52-
// assert.equal(issues.nsExports['my-namespace.ts']['nsUnusedKey'].pos, 84);
53-
54-
assert.equal(issues.nsTypes['my-namespace.ts']['MyNamespace'].line, 5);
55-
assert.equal(issues.nsTypes['my-namespace.ts']['MyNamespace'].col, 18);
56-
// assert.equal(issues.nsTypes['my-namespace.ts']['MyNamespace'].pos, 119);
45+
assert.equal(issues.types['my-namespace.ts']['MyNamespace'].line, 5);
46+
assert.equal(issues.types['my-namespace.ts']['MyNamespace'].col, 18);
5747

5848
assert.deepEqual(counters, {
5949
...baseCounters,
60-
exports: 8,
61-
nsExports: 1,
62-
types: 3,
63-
nsTypes: 1,
50+
exports: 9,
51+
types: 4,
6452
duplicates: 1,
6553
processed: 17,
6654
total: 17,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import assert from 'node:assert/strict';
2+
import test from 'node:test';
3+
import { main } from '../src/index.js';
4+
import { resolve } from '../src/util/path.js';
5+
import baseArguments from './helpers/baseArguments.js';
6+
import baseCounters from './helpers/baseCounters.js';
7+
8+
const cwd = resolve('fixtures/imports-namespace-with-nsexports');
9+
10+
test('Ignore namespace re-export by entry file', async () => {
11+
const { counters } = await main({
12+
...baseArguments,
13+
cwd,
14+
});
15+
16+
assert.deepEqual(counters, {
17+
...baseCounters,
18+
nsExports: 8,
19+
unlisted: 1,
20+
processed: 8,
21+
total: 8,
22+
});
23+
});

‎packages/knip/test/imports-namespace.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ test('Ignore namespace re-export by entry file', async () => {
1515

1616
assert.deepEqual(counters, {
1717
...baseCounters,
18-
nsExports: 3, // TODO should be 0
19-
processed: 4,
20-
total: 4,
18+
unlisted: 1,
19+
processed: 8,
20+
total: 8,
2121
});
2222
});

‎packages/knip/test/include-entry-reexports.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ test('Report unused nsExports in entry source files', async () => {
2828
isIncludeEntryExports: true,
2929
});
3030

31-
assert(issues.nsExports['packages/shared/bar.mjs']['bar']);
31+
assert(issues.exports['packages/shared/bar.mjs']['bar']);
3232

3333
assert.deepEqual(counters, {
3434
...baseCounters,
35-
nsExports: 1,
35+
exports: 1,
3636
processed: 4,
3737
total: 4,
3838
});

‎packages/knip/test/js-only.test.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,15 @@ test('Find unused files and exports with only JS files', async () => {
1616
assert.equal(issues.files.size, 1);
1717
assert(issues.files.has(join(cwd, 'dangling.js')));
1818

19-
assert.equal(Object.values(issues.exports).length, 0);
20-
21-
assert.equal(Object.values(issues.nsExports['my-namespace.js']).length, 2);
22-
assert.equal(issues.nsExports['my-namespace.js']['x'].symbol, 'x');
23-
assert.equal(issues.nsExports['my-namespace.js']['z'].symbol, 'z');
19+
assert.equal(Object.values(issues.exports).length, 1);
20+
assert.equal(Object.values(issues.exports['my-namespace.js']).length, 2);
21+
assert.equal(issues.exports['my-namespace.js']['x'].symbol, 'x');
22+
assert.equal(issues.exports['my-namespace.js']['z'].symbol, 'z');
2423

2524
assert.deepEqual(counters, {
2625
...baseCounters,
2726
files: 1,
28-
nsExports: 2,
27+
exports: 2,
2928
processed: 3,
3029
total: 3,
3130
});

‎packages/knip/test/re-exports-export-ns.test.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ test('Find re-exports through namespaces (1)', async () => {
1313
cwd,
1414
});
1515

16-
assert(issues.nsExports['4-leaf-C.ts']['fnC']);
16+
assert(issues.exports['4-leaf-C.ts']['fnC']);
1717
assert(issues.enumMembers['4-leaf-A.ts']['UnusedProp']);
1818

1919
assert.deepEqual(counters, {
2020
...baseCounters,
2121
enumMembers: 1,
22-
nsExports: 1,
22+
exports: 1,
2323
processed: 7,
2424
total: 7,
2525
});
@@ -33,15 +33,14 @@ test('Find re-exports through namespaces (1) including entry files', async () =>
3333
});
3434

3535
assert(issues.exports['index.ts']['default']);
36-
assert(issues.nsExports['1-root.ts']['exportedFnOnNs']);
37-
assert(issues.nsExports['4-leaf-C.ts']['fnC']);
36+
assert(issues.exports['4-leaf-C.ts']['fnC']);
37+
// assert(issues.nsExports['1-root.ts']['exportedFnOnNs']); // only when `nsExports` is included
3838
assert(issues.enumMembers['4-leaf-A.ts']['UnusedProp']);
3939

4040
assert.deepEqual(counters, {
4141
...baseCounters,
42-
exports: 1,
42+
exports: 2,
4343
enumMembers: 1,
44-
nsExports: 2,
4544
processed: 7,
4645
total: 7,
4746
});

‎packages/knip/test/tags.test.ts

+17-17
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ test('Include or exclude tagged exports (default)', async () => {
1313
cwd,
1414
});
1515

16-
assert(issues.nsExports['tags.ts']['UnusedUntagged']);
17-
assert(issues.nsExports['tags.ts']['UnusedCustom']);
18-
assert(issues.nsExports['tags.ts']['UnusedInternal']);
19-
assert(issues.nsExports['tags.ts']['UnusedCustomAndInternal']);
16+
assert(issues.exports['tags.ts']['UnusedUntagged']);
17+
assert(issues.exports['tags.ts']['UnusedCustom']);
18+
assert(issues.exports['tags.ts']['UnusedInternal']);
19+
assert(issues.exports['tags.ts']['UnusedCustomAndInternal']);
20+
assert(issues.exports['tags.ts']['MyCustomClass']);
2021
assert(issues.classMembers['tags.ts']['UnusedUntagged']);
2122
assert(issues.classMembers['tags.ts']['UnusedCustom']);
2223
assert(issues.classMembers['tags.ts']['UnusedInternal']);
@@ -25,13 +26,12 @@ test('Include or exclude tagged exports (default)', async () => {
2526
assert(issues.enumMembers['tags.ts']['UnusedCustom']);
2627
assert(issues.enumMembers['tags.ts']['UnusedInternal']);
2728
assert(issues.enumMembers['tags.ts']['UnusedCustomAndInternal']);
28-
assert(issues.nsExports['tags.ts']['MyCustomClass']);
29-
assert(issues.nsTypes['tags.ts']['MyCustomEnum']);
29+
assert(issues.types['tags.ts']['MyCustomEnum']);
3030

3131
assert.deepEqual(counters, {
3232
...baseCounters,
33-
nsExports: 5,
34-
nsTypes: 1,
33+
exports: 5,
34+
types: 1,
3535
classMembers: 4,
3636
enumMembers: 4,
3737
processed: 2,
@@ -46,15 +46,15 @@ test('Include or exclude tagged exports (include)', async () => {
4646
tags: [['custom'], []],
4747
});
4848

49-
assert(issues.nsExports['tags.ts']['UnusedCustom']);
50-
assert(issues.nsExports['tags.ts']['UnusedCustomAndInternal']);
51-
assert(issues.nsExports['tags.ts']['MyCustomClass']);
52-
assert(issues.nsTypes['tags.ts']['MyCustomEnum']);
49+
assert(issues.exports['tags.ts']['UnusedCustom']);
50+
assert(issues.exports['tags.ts']['UnusedCustomAndInternal']);
51+
assert(issues.exports['tags.ts']['MyCustomClass']);
52+
assert(issues.types['tags.ts']['MyCustomEnum']);
5353

5454
assert.deepEqual(counters, {
5555
...baseCounters,
56-
nsExports: 3,
57-
nsTypes: 1,
56+
exports: 3,
57+
types: 1,
5858
processed: 2,
5959
total: 2,
6060
});
@@ -67,16 +67,16 @@ test('Include or exclude tagged exports (exclude)', async () => {
6767
tags: [[], ['custom']],
6868
});
6969

70-
assert(issues.nsExports['tags.ts']['UnusedUntagged']);
71-
assert(issues.nsExports['tags.ts']['UnusedInternal']);
70+
assert(issues.exports['tags.ts']['UnusedUntagged']);
71+
assert(issues.exports['tags.ts']['UnusedInternal']);
7272
assert(issues.classMembers['tags.ts']['UnusedUntagged']);
7373
assert(issues.classMembers['tags.ts']['UnusedInternal']);
7474
assert(issues.enumMembers['tags.ts']['UnusedUntagged']);
7575
assert(issues.enumMembers['tags.ts']['UnusedInternal']);
7676

7777
assert.deepEqual(counters, {
7878
...baseCounters,
79-
nsExports: 2,
79+
exports: 2,
8080
classMembers: 2,
8181
enumMembers: 2,
8282
processed: 2,

‎packages/knip/test/util/get-included-issue-types.test.ts

+15-5
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import assert from 'node:assert/strict';
22
import test from 'node:test';
33
import { ISSUE_TYPES } from '../../src/constants.js';
4-
import { getIncludedIssueTypes } from '../../src/util/get-included-issue-types.js';
4+
import { defaultExcludedIssueTypes, getIncludedIssueTypes } from '../../src/util/get-included-issue-types.js';
55

66
const included = (type: string) => [type, true];
77
const excluded = (type: string) => [type, false];
88
const all = Object.fromEntries(ISSUE_TYPES.map(included));
99
const none = Object.fromEntries(ISSUE_TYPES.map(excluded));
1010
const defaults = Object.fromEntries([
11-
...ISSUE_TYPES.filter(type => type !== 'classMembers').map(included),
12-
...['classMembers'].map(excluded),
11+
...ISSUE_TYPES.filter(type => !defaultExcludedIssueTypes.includes(type)).map(included),
12+
...defaultExcludedIssueTypes.map(excluded),
1313
]);
1414

1515
test('Resolve included issue types (default)', async () => {
@@ -19,7 +19,12 @@ test('Resolve included issue types (default)', async () => {
1919
});
2020

2121
test('Resolve included issue types (all)', async () => {
22-
const cliArgs = { include: ['classMembers'], exclude: [], dependencies: false, exports: false };
22+
const cliArgs = {
23+
include: ['classMembers', 'nsExports', 'nsTypes'],
24+
exclude: [],
25+
dependencies: false,
26+
exports: false,
27+
};
2328
const config = getIncludedIssueTypes(cliArgs);
2429
assert.deepEqual(config, { ...all });
2530
});
@@ -100,7 +105,12 @@ test('Resolve included issue types (--exports)', async () => {
100105
});
101106

102107
test('Resolve included issue types (all)', async () => {
103-
const cliArgs = { include: ['files', 'classMembers'], exclude: [], dependencies: true, exports: true };
108+
const cliArgs = {
109+
include: ['files', 'classMembers', 'nsExports', 'nsTypes'],
110+
exclude: [],
111+
dependencies: true,
112+
exports: true,
113+
};
104114
const config = getIncludedIssueTypes(cliArgs);
105115
assert.deepEqual(config, all);
106116
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import assert from 'node:assert/strict';
2+
import test from 'node:test';
3+
import { getHasStrictlyNsReferences } from '../../src/util/type.js';
4+
import type { SerializableImports } from '../../src/types/imports.js';
5+
6+
const base: SerializableImports = {
7+
specifier: '',
8+
isReExport: false,
9+
isReExportedBy: new Set(),
10+
isReExportedAs: new Set(),
11+
isReExportedNs: new Set(),
12+
hasStar: false,
13+
importedNs: new Set(),
14+
identifiers: new Set(),
15+
};
16+
17+
test('Strictly namespace refs (no namespaces)', () => {
18+
assert.deepStrictEqual(getHasStrictlyNsReferences(base), [false]);
19+
});
20+
21+
test('Strictly namespace refs (single ns)', () => {
22+
assert.deepStrictEqual(
23+
getHasStrictlyNsReferences({
24+
...base,
25+
hasStar: true,
26+
importedNs: new Set(['ns']),
27+
identifiers: new Set(['ns']),
28+
}),
29+
[true, 'ns']
30+
);
31+
});
32+
33+
test('Strictly namespace refs (no id)', () => {
34+
assert.deepStrictEqual(
35+
getHasStrictlyNsReferences({
36+
...base,
37+
hasStar: true,
38+
importedNs: new Set(['ns']),
39+
identifiers: new Set([]),
40+
}),
41+
[false, 'ns']
42+
);
43+
});
44+
45+
test('Strictly namespace refs (single ns, no id)', () => {
46+
assert.deepStrictEqual(
47+
getHasStrictlyNsReferences({
48+
...base,
49+
hasStar: true,
50+
importedNs: new Set([]),
51+
identifiers: new Set(['ns']),
52+
}),
53+
[false]
54+
);
55+
});
56+
57+
test('Strictly namespace refs (multiple ns, no id)', () => {
58+
assert.deepStrictEqual(
59+
getHasStrictlyNsReferences({
60+
...base,
61+
hasStar: true,
62+
importedNs: new Set(['ns', 'ns2']),
63+
identifiers: new Set(['ns']),
64+
}),
65+
[false, 'ns2']
66+
);
67+
});
68+
69+
test('Strictly namespace refs (member access)', () => {
70+
assert.deepStrictEqual(
71+
getHasStrictlyNsReferences({
72+
...base,
73+
hasStar: true,
74+
importedNs: new Set(['ns']),
75+
identifiers: new Set(['ns', 'ns.prop']),
76+
}),
77+
[false, 'ns']
78+
);
79+
});
80+
81+
test('Strictly namespace refs (no star)', () => {
82+
assert.deepStrictEqual(
83+
getHasStrictlyNsReferences({
84+
...base,
85+
hasStar: false,
86+
importedNs: new Set(['ns']),
87+
identifiers: new Set(['ns']),
88+
}),
89+
[false]
90+
);
91+
});

‎packages/knip/test/zero-config.test.ts

+6-12
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,24 @@ test('Find unused exports in zero-config mode', async () => {
1515

1616
assert.equal(issues.files.size, 1);
1717

18-
assert.equal(Object.values(issues.exports).length, 1);
18+
assert.equal(Object.values(issues.exports).length, 2);
1919
assert.equal(issues.exports['my-module.ts']['unused'].symbol, 'unused');
2020
assert.equal(issues.exports['my-module.ts']['default'].symbol, 'default');
21+
assert.equal(issues.exports['my-namespace.ts']['z'].symbol, 'z');
2122
assert(!issues.exports['index.ts']);
2223

23-
assert.equal(Object.values(issues.types).length, 1);
24+
assert.equal(Object.values(issues.types).length, 2);
2425
assert.equal(issues.types['my-module.ts']['AnyType'].symbolType, 'type');
25-
26-
assert.equal(Object.values(issues.nsExports).length, 1);
27-
assert.equal(issues.nsExports['my-namespace.ts']['z'].symbol, 'z');
28-
29-
assert.equal(Object.values(issues.nsTypes).length, 1);
30-
assert.equal(issues.nsTypes['my-namespace.ts']['NS'].symbol, 'NS');
26+
assert.equal(issues.types['my-namespace.ts']['NS'].symbol, 'NS');
3127

3228
assert.equal(Object.values(issues.duplicates).length, 1);
3329
assert.equal(issues.duplicates['my-module.ts']['myExport|default'].symbols?.length, 2);
3430

3531
assert.deepEqual(counters, {
3632
...baseCounters,
3733
files: 1,
38-
exports: 2,
39-
nsExports: 1,
40-
types: 1,
41-
nsTypes: 1,
34+
exports: 3,
35+
types: 2,
4236
duplicates: 1,
4337
processed: 4,
4438
total: 4,

0 commit comments

Comments
 (0)
Please sign in to comment.