Skip to content

Commit 49b06b9

Browse files
committed
Refactor import props into modifiers
1 parent bb923c2 commit 49b06b9

File tree

15 files changed

+140
-58
lines changed

15 files changed

+140
-58
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
new URL('./file.css', import.meta.url);
22
new URL('./file.js', import.meta.url);
3+
new URL('./404.js', import.meta.url);

packages/knip/src/constants.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,3 +229,11 @@ export const FIX_FLAGS = {
229229
EMPTY_DECLARATION: 1 << 1, // remove declaration if empty
230230
WITH_NEWLINE: 1 << 2, // remove with newline
231231
} as const;
232+
233+
export const IMPORT_MODIFIERS = {
234+
NONE: 0,
235+
RE_EXPORT: 1 << 0,
236+
TYPE_ONLY: 1 << 1,
237+
ENTRY: 1 << 2, // entry path, ignore exports
238+
OPTIONAL: 1 << 3, // no error if not resolved
239+
} as const;

packages/knip/src/types/imports.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,5 @@ export interface ImportNode {
77
namespace?: string | undefined;
88
pos: number;
99
symbol?: ts.Symbol;
10-
isTypeOnly?: boolean;
11-
isReExport?: boolean;
12-
resolve?: boolean;
10+
modifiers: number;
1311
}

packages/knip/src/typescript/get-imports-and-exports.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { isBuiltin } from 'node:module';
22
import ts from 'typescript';
3-
import { ALIAS_TAG, ANONYMOUS, IMPORT_STAR, PROTOCOL_VIRTUAL } from '../constants.js';
3+
import { ALIAS_TAG, ANONYMOUS, IMPORT_MODIFIERS, IMPORT_STAR, PROTOCOL_VIRTUAL } from '../constants.js';
44
import type { GetImportsAndExportsOptions, IgnoreExportsUsedInFile } from '../types/config.js';
55
import type { ExportNode, ExportNodeMember } from '../types/exports.js';
66
import type { ImportNode } from '../types/imports.js';
@@ -17,7 +17,7 @@ import type {
1717
import { addNsValue, addValue, createImports } from '../util/module-graph.js';
1818
import { getPackageNameFromFilePath, isStartsLikePackageName, sanitizeSpecifier } from '../util/modules.js';
1919
import { timerify } from '../util/Performance.js';
20-
import { isInNodeModules } from '../util/path.js';
20+
import { dirname, isInNodeModules, resolve } from '../util/path.js';
2121
import { shouldIgnore } from '../util/tag.js';
2222
import {
2323
getAccessMembers,
@@ -68,7 +68,6 @@ interface AddInternalImportOptions extends ImportNode {
6868
namespace?: string;
6969
identifier: string;
7070
filePath: string;
71-
isReExport: boolean;
7271
line: number;
7372
col: number;
7473
}
@@ -128,7 +127,7 @@ const getImportsAndExports = (
128127
};
129128

130129
const addInternalImport = (options: AddInternalImportOptions) => {
131-
const { identifier, symbol, filePath, namespace, alias, specifier, isReExport } = options;
130+
const { identifier, symbol, filePath, namespace, alias, specifier } = options;
132131

133132
const isStar = identifier === IMPORT_STAR;
134133

@@ -142,7 +141,7 @@ const getImportsAndExports = (
142141

143142
const nsOrAlias = symbol ? String(symbol.escapedName) : alias;
144143

145-
if (isReExport) {
144+
if (options.modifiers & IMPORT_MODIFIERS.RE_EXPORT) {
146145
if (isStar && namespace) {
147146
// Pattern: export * as NS from 'specifier';
148147
addValue(imports.reExportedNs, namespace, sourceFile.fileName);
@@ -179,7 +178,7 @@ const getImportsAndExports = (
179178
if (module) {
180179
const filePath = module.resolvedFileName;
181180
if (filePath) {
182-
if (opts.resolve && !isInNodeModules(filePath)) {
181+
if (opts.modifiers && opts.modifiers & IMPORT_MODIFIERS.ENTRY && !isInNodeModules(filePath)) {
183182
resolved.add(filePath);
184183
return;
185184
}
@@ -190,14 +189,13 @@ const getImportsAndExports = (
190189
...opts,
191190
identifier: opts.identifier ?? ANONYMOUS,
192191
filePath,
193-
isReExport: opts.isReExport ?? false,
194192
line: line + 1,
195193
col: character + 1,
196194
});
197195
}
198196

199197
if (module.isExternalLibraryImport) {
200-
if (options.skipTypeOnly && opts.isTypeOnly) return;
198+
if (options.skipTypeOnly && opts.modifiers & IMPORT_MODIFIERS.TYPE_ONLY) return;
201199

202200
const isInNM = isInNodeModules(opts.specifier);
203201

@@ -221,10 +219,16 @@ const getImportsAndExports = (
221219
}
222220
}
223221
} else {
224-
if (options.skipTypeOnly && opts.isTypeOnly) return;
222+
if (options.skipTypeOnly && opts.modifiers & IMPORT_MODIFIERS.TYPE_ONLY) return;
225223
if (shouldIgnore(getJSDocTags(node), options.tags)) return;
226224
if (opts.specifier.startsWith(PROTOCOL_VIRTUAL)) return;
227225

226+
const r = resolve(dirname(sourceFile.fileName), opts.specifier);
227+
if (opts.modifiers && opts.modifiers & IMPORT_MODIFIERS.OPTIONAL) {
228+
resolved.add(r);
229+
return;
230+
}
231+
228232
// @ts-expect-error TODO
229233
const pos = 'moduleSpecifier' in node ? node.moduleSpecifier.pos : node.pos;
230234
const { line, character } = sourceFile.getLineAndCharacterOfPosition(pos);

packages/knip/src/typescript/visitors/dynamic-imports/importCall.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import ts from 'typescript';
2-
import { ANONYMOUS } from '../../../constants.js';
2+
import { ANONYMOUS, IMPORT_MODIFIERS } from '../../../constants.js';
33
import {
44
findAncestor,
55
findDescendants,
@@ -20,6 +20,7 @@ export default visit(
2020
if (isImportCall(node)) {
2121
if (node.arguments[0] && ts.isStringLiteralLike(node.arguments[0])) {
2222
const specifier = node.arguments[0].text;
23+
const modifiers = IMPORT_MODIFIERS.NONE;
2324

2425
if (specifier) {
2526
const accessExpression = findAncestor<ts.AccessExpression>(node, _node => {
@@ -45,24 +46,24 @@ export default visit(
4546
// Pattern: import('specifier').then(module => module.identifier);
4647
return accessExpressions.map(binding => {
4748
const identifier = String(binding.name.escapedText);
48-
return { identifier, specifier, pos };
49+
return { identifier, specifier, pos, modifiers };
4950
});
5051
}
5152
} else if (arg && ts.isObjectBindingPattern(arg.name)) {
5253
// Pattern: import('specifier').then({ identifier } => identifier);
5354
return arg.name.elements.map(element => {
5455
const identifier = (element.propertyName ?? element.name).getText();
5556
const alias = element.propertyName ? element.name.getText() : undefined;
56-
return { identifier, alias, specifier, pos: element.pos };
57+
return { identifier, alias, specifier, pos: element.pos, modifiers };
5758
});
5859
}
5960
}
6061
// Pattern: import('specifier').then(id => id)
61-
return { identifier: 'default', specifier, pos };
62+
return { identifier: 'default', specifier, pos, modifiers };
6263
}
6364

6465
// Pattern: (await import('./prop-access')).propAccess;
65-
if (identifier !== 'catch') return { identifier, specifier, pos };
66+
if (identifier !== 'catch') return { identifier, specifier, pos, modifiers };
6667
}
6768

6869
if (
@@ -73,7 +74,7 @@ export default visit(
7374
const pos = accessExpression.argumentExpression.pos;
7475
const identifier = name;
7576
// Pattern: (await import('specifier'))['identifier']
76-
return { identifier, specifier, pos };
77+
return { identifier, specifier, pos, modifiers };
7778
}
7879
}
7980

@@ -97,9 +98,16 @@ export default visit(
9798
const scope: ts.Node = findAncestor(variableDeclaration, ts.isFunctionBody) || node.getSourceFile();
9899
const accessed = getAccessedIdentifiers(alias, scope);
99100
if (accessed.length > 0) {
100-
return accessed.map(acc => ({ identifier: acc.identifier, alias, symbol, specifier, pos: acc.pos }));
101+
return accessed.map(acc => ({
102+
identifier: acc.identifier,
103+
alias,
104+
symbol,
105+
specifier,
106+
pos: acc.pos,
107+
modifiers,
108+
}));
101109
}
102-
return { identifier: 'default', alias, symbol, specifier, pos: node.arguments[0].pos };
110+
return { identifier: 'default', alias, symbol, specifier, pos: node.arguments[0].pos, modifiers };
103111
}
104112
const bindings = findDescendants<ts.BindingElement>(variableDeclaration, ts.isBindingElement);
105113
if (bindings.length > 0) {
@@ -108,11 +116,11 @@ export default visit(
108116
const identifier = (element.propertyName ?? element.name).getText();
109117
const alias = element.propertyName ? element.name.getText() : undefined;
110118
const symbol = getSymbol(element, isTLA);
111-
return { identifier, alias, symbol, specifier, pos: element.pos };
119+
return { identifier, alias, symbol, specifier, pos: element.pos, modifiers };
112120
});
113121
}
114122
// Pattern: import('specifier')
115-
return { identifier: ANONYMOUS, specifier, pos: node.arguments[0].pos };
123+
return { identifier: ANONYMOUS, specifier, pos: node.arguments[0].pos, modifiers };
116124
}
117125
const arrayLiteralExpression = node.parent;
118126
const variableDeclarationParent = node.parent.parent?.parent?.parent;
@@ -133,27 +141,27 @@ export default visit(
133141
const identifier = (element.propertyName ?? element.name).getText();
134142
const alias = element.propertyName ? element.name.getText() : undefined;
135143
const symbol = getSymbol(element, isTL);
136-
return { identifier, alias, symbol, specifier, pos: element.pos };
144+
return { identifier, alias, symbol, specifier, pos: element.pos, modifiers };
137145
});
138146
}
139147

140148
if (!ts.isOmittedExpression(element) && ts.isIdentifier(element.name)) {
141149
// Pattern: const [a, b] = await Promise.all([import('A'), import('B')]);
142150
const alias = String(element.name.escapedText);
143151
const symbol = getSymbol(element, isTL);
144-
return { identifier: 'default', symbol, alias, specifier, pos: element.pos };
152+
return { identifier: 'default', symbol, alias, specifier, pos: element.pos, modifiers };
145153
}
146154

147-
return { identifier: 'default', specifier, pos: element.pos };
155+
return { identifier: 'default', specifier, pos: element.pos, modifiers };
148156
}
149157
}
150158

151159
// Pattern: return import('side-effects')
152-
return { identifier: 'default', specifier, pos: node.arguments[0].pos };
160+
return { identifier: 'default', specifier, pos: node.arguments[0].pos, modifiers };
153161
}
154162

155163
// Fallback, seems to never happen though
156-
return { specifier, identifier: 'default', pos: node.arguments[0].pos };
164+
return { specifier, identifier: 'default', pos: node.arguments[0].pos, modifiers };
157165
}
158166
}
159167
}

packages/knip/src/typescript/visitors/dynamic-imports/importType.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import ts from 'typescript';
2+
import { IMPORT_MODIFIERS } from '../../../constants.js';
23
import { importVisitor as visit } from '../index.js';
34

45
export default visit(
56
() => true,
67
node => {
78
if (ts.isImportTypeNode(node)) {
89
if (ts.isLiteralTypeNode(node.argument) && ts.isStringLiteral(node.argument.literal)) {
9-
return { specifier: node.argument.literal.text, identifier: undefined, pos: 0, isTypeOnly: true };
10+
return {
11+
specifier: node.argument.literal.text,
12+
identifier: undefined,
13+
pos: 0,
14+
modifiers: IMPORT_MODIFIERS.TYPE_ONLY,
15+
};
1016
}
1117
}
1218
}

packages/knip/src/typescript/visitors/dynamic-imports/jsDocType.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import ts from 'typescript';
2+
import { IMPORT_MODIFIERS } from '../../../constants.js';
23
import type { ImportNode } from '../../../types/imports.js';
34
import { importVisitor as visit } from '../index.js';
45

@@ -13,7 +14,12 @@ const getImportSpecifiers = (node: ts.JSDocTag) => {
1314
if (ts.isLiteralTypeNode(importClause) && ts.isStringLiteral(importClause.literal)) {
1415
const identifier =
1516
node.qualifier && ts.isIdentifier(node.qualifier) ? String(node.qualifier.escapedText) : 'default';
16-
imports.push({ specifier: importClause.literal.text, identifier, pos: importClause.literal.pos });
17+
imports.push({
18+
specifier: importClause.literal.text,
19+
identifier,
20+
pos: importClause.literal.pos,
21+
modifiers: IMPORT_MODIFIERS.NONE,
22+
});
1723
}
1824
}
1925

@@ -23,7 +29,12 @@ const getImportSpecifiers = (node: ts.JSDocTag) => {
2329
// biome-ignore lint: suspicious/noTsIgnore
2430
// @ts-ignore node.moduleSpecifier added in TS v5.5.0
2531
const moduleSpecifier = node.moduleSpecifier;
26-
imports.push({ specifier: moduleSpecifier.text, identifier: undefined, pos: moduleSpecifier.pos });
32+
imports.push({
33+
specifier: moduleSpecifier.text,
34+
identifier: undefined,
35+
pos: moduleSpecifier.pos,
36+
modifiers: IMPORT_MODIFIERS.NONE,
37+
});
2738
}
2839

2940
ts.forEachChild(node, visit);

packages/knip/src/typescript/visitors/dynamic-imports/requireCall.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import ts from 'typescript';
2-
import { IMPORT_STAR } from '../../../constants.js';
2+
import { IMPORT_MODIFIERS, IMPORT_STAR } from '../../../constants.js';
33
import { findAncestor, findDescendants, isModuleExportsAccess, isRequireCall, isTopLevel } from '../../ast-helpers.js';
4-
import { isNotJS } from '../helpers.js';
54
import { importVisitor as visit } from '../index.js';
65

76
export default visit(
@@ -10,19 +9,18 @@ export default visit(
109
if (isRequireCall(node)) {
1110
if (ts.isStringLiteralLike(node.arguments[0])) {
1211
const specifier = node.arguments[0].text;
12+
const modifiers = IMPORT_MODIFIERS.NONE;
1313

1414
if (specifier) {
1515
const propertyAccessExpression = findAncestor<ts.PropertyAccessExpression>(node, _node => {
1616
if (ts.isExpressionStatement(_node) || ts.isCallExpression(_node)) return 'STOP';
1717
return ts.isPropertyAccessExpression(_node);
1818
});
1919

20-
const resolve = isNotJS(node.getSourceFile());
21-
2220
if (propertyAccessExpression) {
2321
// Pattern: require('side-effects').identifier
2422
const identifier = String(propertyAccessExpression.name.escapedText);
25-
return { identifier, specifier, pos: propertyAccessExpression.name.pos, resolve };
23+
return { identifier, specifier, pos: propertyAccessExpression.name.pos, modifiers };
2624
}
2725
const variableDeclaration = node.parent;
2826
if (
@@ -40,7 +38,7 @@ export default visit(
4038
symbol: isTLA ? variableDeclaration.symbol : undefined,
4139
specifier,
4240
pos: node.arguments[0].pos,
43-
resolve,
41+
modifiers,
4442
};
4543
}
4644
const bindings = findDescendants<ts.BindingElement>(variableDeclaration, ts.isBindingElement);
@@ -51,11 +49,11 @@ export default visit(
5149
const alias = element.propertyName ? element.name.getText() : undefined;
5250
// @ts-expect-error TODO FIXME Property 'symbol' does not exist on type 'BindingElement'.
5351
const symbol = isTLA ? element.symbol : undefined;
54-
return { identifier, specifier, alias, symbol, pos: element.pos, resolve };
52+
return { identifier, specifier, alias, symbol, pos: element.pos, modifiers };
5553
});
5654
}
5755
// Pattern: require('specifier')
58-
return { identifier: 'default', specifier, pos: node.arguments[0].pos, resolve };
56+
return { identifier: 'default', specifier, pos: node.arguments[0].pos, modifiers };
5957
}
6058

6159
if (
@@ -64,11 +62,16 @@ export default visit(
6462
isModuleExportsAccess(node.parent.left)
6563
) {
6664
// Pattern: module.exports = require('specifier')
67-
return { identifier: IMPORT_STAR, specifier, isReExport: true, pos: node.arguments[0].pos };
65+
return {
66+
identifier: IMPORT_STAR,
67+
specifier,
68+
pos: node.arguments[0].pos,
69+
modifiers: IMPORT_MODIFIERS.RE_EXPORT,
70+
};
6871
}
6972

7073
// Pattern: require('side-effects')
71-
return { identifier: 'default', specifier, pos: node.arguments[0].pos, resolve };
74+
return { identifier: 'default', specifier, pos: node.arguments[0].pos, modifiers };
7275
}
7376
}
7477
}

packages/knip/src/typescript/visitors/dynamic-imports/resolveCall.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import ts from 'typescript';
2+
import { IMPORT_MODIFIERS } from '../../../constants.js';
23
import { isPropertyAccessCall } from '../../ast-helpers.js';
34
import { importVisitor as visit } from '../index.js';
45

@@ -7,9 +8,16 @@ export default visit(
78
node => {
89
if (isPropertyAccessCall(node, 'require.resolve') || isPropertyAccessCall(node, 'import.meta.resolve')) {
910
// Pattern: require.resolve('specifier')
11+
// Pattern: import.meta.resolve('specifier')
1012
if (node.arguments[0] && ts.isStringLiteralLike(node.arguments[0])) {
1113
const specifier = node.arguments[0].text;
12-
if (specifier) return { specifier, identifier: undefined, pos: node.arguments[0].pos, resolve: true };
14+
if (specifier)
15+
return {
16+
specifier,
17+
identifier: undefined,
18+
pos: node.arguments[0].pos,
19+
modifiers: IMPORT_MODIFIERS.ENTRY,
20+
};
1321
}
1422
}
1523
}

0 commit comments

Comments
 (0)