Skip to content

Commit deeb2ac

Browse files
committedJul 10, 2024·
Move & refactor setRefs → findInternalReferences
1 parent 09fc536 commit deeb2ac

File tree

2 files changed

+71
-53
lines changed

2 files changed

+71
-53
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import ts from 'typescript';
2+
import type { Export, ExportMember } from '../types/dependency-graph.js';
3+
import { isIdChar } from '../util/regex.js';
4+
5+
const isType = (item: Export | ExportMember) =>
6+
item.type === 'type' || item.type === 'interface' || item.type === 'member';
7+
8+
// Find internal references to export item for `ignoreExportsUsedInFile`
9+
// Also detect usage of non-types within types (e.g. class or typeof within interface), as those should be exported as well
10+
export const findInternalReferences = (
11+
item: Export | ExportMember,
12+
sourceFile: ts.SourceFile,
13+
typeChecker: ts.TypeChecker,
14+
referencedSymbolsInExportedTypes: Set<ts.Symbol>
15+
): [number, boolean] => {
16+
if (!item.symbol) return [0, false];
17+
18+
if (item.symbol.flags & ts.SymbolFlags.AliasExcludes) return [1, false];
19+
20+
const text = sourceFile.text;
21+
const id = item.identifier;
22+
const symbols = new Set<ts.Symbol>();
23+
24+
let refCount = 0;
25+
let isSymbolInExportedType = false;
26+
let index = 0;
27+
28+
// biome-ignore lint/suspicious/noAssignInExpressions: deal with it
29+
while (index < text.length && (index = text.indexOf(id, index)) !== -1) {
30+
if (!isIdChar(text.charAt(index - 1)) && !isIdChar(text.charAt(index + id.length))) {
31+
const isExportDeclaration = index === item.pos || index === item.pos + 1; // off-by-one from `stripQuotes`
32+
if (!isExportDeclaration) {
33+
// @ts-expect-error ts.getTokenAtPosition is internal fn
34+
const symbol = typeChecker.getSymbolAtLocation(ts.getTokenAtPosition(sourceFile, index));
35+
if (symbol) {
36+
const isInExportedType = referencedSymbolsInExportedTypes.has(symbol);
37+
38+
if (isInExportedType) isSymbolInExportedType = true;
39+
40+
if (item.symbol === symbol) {
41+
refCount++;
42+
if (isInExportedType || isType(item)) return [refCount, isSymbolInExportedType];
43+
}
44+
45+
// @ts-expect-error Keep it cheap
46+
const declaration = symbol.declarations?.[0];
47+
if (declaration) {
48+
// Pattern: export { identifier }
49+
if (item.symbol === declaration.name?.flowNode?.node?.symbol) {
50+
return [++refCount, isSymbolInExportedType];
51+
}
52+
53+
if (ts.isImportSpecifier(declaration) && symbols.has(symbol)) {
54+
// Consider re-exports referenced
55+
return [++refCount, isSymbolInExportedType];
56+
}
57+
}
58+
59+
symbols.add(symbol);
60+
}
61+
}
62+
}
63+
index += id.length;
64+
}
65+
66+
return [refCount, isSymbolInExportedType];
67+
};

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

+4-53
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@ import { isBuiltin } from 'node:module';
22
import ts from 'typescript';
33
import { ALIAS_TAG, ANONYMOUS, DEFAULT_EXTENSIONS, IMPORT_STAR } from '../constants.js';
44
import type { Tags } from '../types/cli.js';
5-
import type { Export, ExportMap, ExportMember, ImportMap, UnresolvedImport } from '../types/dependency-graph.js';
5+
import type { ExportMap, ExportMember, ImportMap, UnresolvedImport } from '../types/dependency-graph.js';
66
import type { ExportNode, ExportNodeMember } from '../types/exports.js';
77
import type { ImportNode } from '../types/imports.js';
88
import type { IssueSymbol } from '../types/issues.js';
99
import { timerify } from '../util/Performance.js';
1010
import { addNsValue, addValue, createImports } from '../util/dependency-graph.js';
1111
import { isStartsLikePackageName, sanitizeSpecifier } from '../util/modules.js';
1212
import { extname, isInNodeModules } from '../util/path.js';
13-
import { isIdChar } from '../util/regex.js';
1413
import { shouldIgnore } from '../util/tag.js';
1514
import type { BoundSourceFile } from './SourceFile.js';
1615
import {
@@ -25,6 +24,7 @@ import {
2524
isImportSpecifier,
2625
isReferencedInExportedType,
2726
} from './ast-helpers.js';
27+
import { findInternalReferences } from './find-internal-references.js';
2828
import getDynamicImportVisitors from './visitors/dynamic-imports/index.js';
2929
import getExportVisitors from './visitors/exports/index.js';
3030
import { getImportsFromPragmas } from './visitors/helpers.js';
@@ -54,9 +54,6 @@ const createMember = (node: ts.Node, member: ExportNodeMember, pos: number): Exp
5454
};
5555
};
5656

57-
const isType = (item: Export | ExportMember) =>
58-
item.type === 'type' || item.type === 'interface' || item.type === 'member';
59-
6057
export type GetImportsAndExportsOptions = {
6158
skipTypeOnly: boolean;
6259
skipExports: boolean;
@@ -397,59 +394,13 @@ const getImportsAndExports = (
397394
const pragmaImports = getImportsFromPragmas(sourceFile);
398395
if (pragmaImports) for (const node of pragmaImports) addImport(node, sourceFile);
399396

400-
const setRefs = (item: Export | ExportMember) => {
401-
if (!item.symbol) return;
402-
403-
if (item.symbol.flags & ts.SymbolFlags.AliasExcludes) {
404-
item.refs = [1, false];
405-
return;
406-
}
407-
408-
const symbols = new Set<ts.Symbol>();
409-
let index = 0;
410-
const text = sourceFile.text;
411-
const id = item.identifier;
412-
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
413-
while (index < text.length && (index = text.indexOf(id, index)) !== -1) {
414-
if (!isIdChar(text.charAt(index - 1)) && !isIdChar(text.charAt(index + id.length))) {
415-
const isExportDeclaration = index === item.pos || index === item.pos + 1; // off-by-one from `stripQuotes`
416-
if (!isExportDeclaration) {
417-
// @ts-expect-error ts.getTokenAtPosition is internal fn
418-
const symbol = typeChecker.getSymbolAtLocation(ts.getTokenAtPosition(sourceFile, index));
419-
if (symbol) {
420-
const isInExportedType = referencedSymbolsInExportedTypes.has(symbol);
421-
if (item.symbol === symbol) {
422-
item.refs = [1, isInExportedType];
423-
if (isInExportedType || isType(item)) break;
424-
}
425-
// @ts-expect-error Keep it cheap
426-
const declaration = symbol.declarations?.[0];
427-
if (declaration) {
428-
if (item.symbol === declaration.name?.flowNode?.node?.symbol) {
429-
item.refs = [1, isInExportedType];
430-
break;
431-
}
432-
if (ts.isImportSpecifier(declaration) && symbols.has(symbol)) {
433-
// re-exported symbol is referenced
434-
item.refs = [1, isInExportedType];
435-
break;
436-
}
437-
}
438-
symbols.add(symbol);
439-
}
440-
}
441-
}
442-
index += id.length;
443-
}
444-
};
445-
446397
const isSetRefs = ignoreExportsUsedInFile;
447398
for (const item of exports.values()) {
448399
if (isSetRefs === true || (typeof isSetRefs === 'object' && item.type !== 'unknown' && !!isSetRefs[item.type])) {
449-
setRefs(item);
400+
item.refs = findInternalReferences(item, sourceFile, typeChecker, referencedSymbolsInExportedTypes);
450401
}
451402
for (const member of item.members) {
452-
setRefs(member);
403+
member.refs = findInternalReferences(member, sourceFile, typeChecker, referencedSymbolsInExportedTypes);
453404
member.symbol = undefined;
454405
}
455406
item.symbol = undefined;

0 commit comments

Comments
 (0)
Please sign in to comment.