Skip to content

Commit b107af4

Browse files
committed
Introduce graph explorer
1 parent b210b18 commit b107af4

35 files changed

+1235
-710
lines changed

packages/knip/src/DependencyDeputy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ export class DependencyDeputy {
259259
const devDependencyIssues: Issue[] = [];
260260
const optionalPeerDependencyIssues: Issue[] = [];
261261

262-
for (const [workspace, { manifestPath: filePath, manifestStr }] of this._manifests.entries()) {
262+
for (const [workspace, { manifestPath: filePath, manifestStr }] of this._manifests) {
263263
const referencedDependencies = this.referencedDependencies.get(workspace);
264264
const hasTypesIncluded = this.getHasTypesIncluded(workspace);
265265
const peeker = new PackagePeeker(manifestStr);
@@ -449,7 +449,7 @@ export class DependencyDeputy {
449449
public getConfigurationHints() {
450450
const configurationHints = new Set<ConfigurationHint>();
451451

452-
for (const [workspaceName, manifest] of this._manifests.entries()) {
452+
for (const [workspaceName, manifest] of this._manifests) {
453453
for (const identifier of manifest.unusedIgnoreDependencies) {
454454
configurationHints.add({ workspaceName, identifier, type: 'ignoreDependencies' });
455455
}

packages/knip/src/ProjectPrincipal.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ export class ProjectPrincipal {
313313
}
314314

315315
reconcileCache(graph: ModuleGraph) {
316-
for (const [filePath, file] of graph.entries()) {
316+
for (const [filePath, file] of graph) {
317317
const fd = this.cache.getFileDescriptor(filePath);
318318
if (!fd?.meta) continue;
319319
// biome-ignore lint: correctness/noUnusedVariables

packages/knip/src/WorkspaceWorker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ export class WorkspaceWorker {
420420
const configFiles = this.configFilesMap.get(wsName);
421421
if (configFiles) {
422422
do {
423-
for (const [pluginName, dependencies] of configFiles.entries()) {
423+
for (const [pluginName, dependencies] of configFiles) {
424424
configFiles.delete(pluginName);
425425
if (this.enabledPlugins.includes(pluginName)) {
426426
for (const input of await runPlugin(pluginName, Array.from(dependencies))) inputs.push(input);

packages/knip/src/compilers/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export const getIncludedCompilers = (
5454
): [SyncCompilers, AsyncCompilers] => {
5555
const hasDependency = (packageName: string) => dependencies.has(packageName);
5656

57-
for (const [extension, { condition, compiler }] of compilers.entries()) {
57+
for (const [extension, { condition, compiler }] of compilers) {
5858
// For MDX, try Astro compiler first if available
5959
if (extension === '.mdx' && AstroMDX.condition(hasDependency)) {
6060
syncCompilers.set(extension, AstroMDX.compiler);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const CONTINUE = 'continue';
2+
3+
export const STOP = 'stop';
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { ModuleGraph } from '../types/module-graph.js';
2+
import { buildExportsTree } from './operations/build-trace-tree.js';
3+
import { hasStrictlyNsReferences } from './operations/has-strictly-ns-references.js';
4+
import { isReferenced } from './operations/is-referenced.js';
5+
6+
export const createGraphExplorer = (graph: ModuleGraph, entryPaths: Set<string>) => {
7+
return {
8+
isReferenced: (filePath: string, identifier: string, options: { includeEntryExports: boolean }) =>
9+
isReferenced(graph, entryPaths, filePath, identifier, options),
10+
hasStrictlyNsReferences: (filePath: string, identifier: string) =>
11+
hasStrictlyNsReferences(graph, graph.get(filePath)?.imported, identifier),
12+
buildExportsTree: (options: { filePath?: string; identifier?: string }) =>
13+
buildExportsTree(graph, entryPaths, options),
14+
};
15+
};
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import type { FileNode, Identifier, ModuleGraph } from '../../types/module-graph.js';
2+
import { CONTINUE } from '../constants.js';
3+
import { walkDown } from '../walk-down.js';
4+
5+
/** @public */
6+
export interface TreeNode {
7+
filePath: string;
8+
identifier: string;
9+
hasRef: boolean;
10+
isEntry: boolean;
11+
children: TreeNode[];
12+
}
13+
14+
export const buildExportsTree = (
15+
graph: ModuleGraph,
16+
entryPaths: Set<string>,
17+
options: { filePath?: string; identifier?: Identifier }
18+
) => {
19+
const traces: TreeNode[] = [];
20+
21+
const processFile = (filePath: string, file: FileNode) => {
22+
for (const exportId of options.identifier ? [options.identifier] : file.exports.keys()) {
23+
if (!options.identifier || file.exports.has(exportId)) {
24+
const trace = buildExportTree(graph, entryPaths, filePath, exportId);
25+
if (trace) traces.push(trace);
26+
}
27+
}
28+
};
29+
30+
if (options.filePath) {
31+
const file = graph.get(options.filePath);
32+
if (file) processFile(options.filePath, file);
33+
} else {
34+
for (const [filePath, file] of graph) processFile(filePath, file);
35+
}
36+
37+
return traces;
38+
};
39+
40+
const buildExportTree = (
41+
graph: ModuleGraph,
42+
entryPaths: Set<string>,
43+
filePath: string,
44+
identifier: string
45+
): TreeNode => {
46+
const rootNode: TreeNode = {
47+
filePath,
48+
identifier,
49+
hasRef: false,
50+
isEntry: entryPaths.has(filePath),
51+
children: [],
52+
};
53+
54+
const nodeMap = new Map<string, TreeNode>();
55+
nodeMap.set(`${filePath}:${identifier}`, rootNode);
56+
57+
walkDown(
58+
graph,
59+
filePath,
60+
identifier,
61+
(sourceFile, sourceId, importingFile, id, isEntry, isReExport) => {
62+
const key = `${importingFile}:${id}`;
63+
const childNode = nodeMap.get(key) ?? {
64+
filePath: importingFile,
65+
identifier: id,
66+
hasRef: !isReExport && Boolean(graph.get(importingFile)?.traceRefs?.has(id)),
67+
isEntry,
68+
children: [],
69+
};
70+
nodeMap.set(key, childNode);
71+
72+
const parentKey = `${sourceFile}:${sourceId}`;
73+
const parentNode = nodeMap.get(parentKey) ?? rootNode;
74+
parentNode.children.push(childNode);
75+
76+
return CONTINUE;
77+
},
78+
entryPaths
79+
);
80+
81+
return rootNode;
82+
};
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import type { ImportMaps, ModuleGraph } from '../../types/module-graph.js';
2+
import {
3+
forEachAliasReExport,
4+
forEachNamespaceReExport,
5+
forEachPassThroughReExport,
6+
getStarReExportSources,
7+
} from '../visitors.js';
8+
9+
export const hasStrictlyNsReferences = (
10+
graph: ModuleGraph,
11+
importsForExport: ImportMaps | undefined,
12+
identifier: string
13+
): [boolean, string?] => {
14+
if (!importsForExport) return [false];
15+
16+
let foundNamespace: string | undefined;
17+
18+
const aliasByIdentifier = new Map<string, Array<{ alias: string; sources: Set<string> }>>();
19+
const namespaceReExports = new Map<string, Array<Set<string>>>();
20+
const namespaceEdges: Array<{ namespace: string; sources: Set<string> }> = [];
21+
const directById = new Map<string, Set<string>>();
22+
23+
forEachPassThroughReExport(importsForExport, (id, sources) => {
24+
directById.set(id, sources);
25+
});
26+
27+
forEachAliasReExport(importsForExport, (id, alias, sources) => {
28+
let arr = aliasByIdentifier.get(id);
29+
if (!arr) {
30+
arr = [];
31+
aliasByIdentifier.set(id, arr);
32+
}
33+
arr.push({ alias, sources });
34+
});
35+
36+
forEachNamespaceReExport(importsForExport, (namespace, sources) => {
37+
namespaceEdges.push({ namespace, sources });
38+
let arr = namespaceReExports.get(namespace);
39+
if (!arr) {
40+
arr = [];
41+
namespaceReExports.set(namespace, arr);
42+
}
43+
arr.push(sources);
44+
});
45+
46+
const starSources = getStarReExportSources(importsForExport);
47+
48+
const followReExports = (
49+
sources: Set<string>,
50+
nextId: string,
51+
propagateNamespace = true
52+
): [boolean, string?] | undefined => {
53+
for (const filePath of sources) {
54+
const file = graph.get(filePath);
55+
if (!file?.imported) continue;
56+
const result = hasStrictlyNsReferences(graph, file.imported, nextId);
57+
if (result[0] === false) return result;
58+
if (propagateNamespace && result[1]) foundNamespace = result[1];
59+
}
60+
return undefined;
61+
};
62+
63+
for (const ns of importsForExport.importedNs.keys()) {
64+
const hasNsRef = importsForExport.refs.has(ns);
65+
if (!hasNsRef) return [false, ns];
66+
67+
for (const ref of importsForExport.refs) {
68+
if (ref.startsWith(`${ns}.`)) return [false, ns];
69+
}
70+
71+
const nsReExports = namespaceReExports.get(ns);
72+
if (nsReExports) {
73+
for (const sources of nsReExports) {
74+
const result = followReExports(sources, identifier, false);
75+
if (result) return result;
76+
}
77+
}
78+
79+
const nsAliases = aliasByIdentifier.get(ns);
80+
if (nsAliases) {
81+
for (const { sources } of nsAliases) {
82+
const result = followReExports(sources, identifier, false);
83+
if (result) return result;
84+
}
85+
}
86+
87+
foundNamespace = ns;
88+
}
89+
90+
const directSources = directById.get(identifier);
91+
if (directSources) {
92+
const result = followReExports(directSources, identifier, true);
93+
if (result) return result;
94+
}
95+
96+
if (starSources) {
97+
const result = followReExports(starSources, identifier, true);
98+
if (result) return result;
99+
}
100+
101+
const [id, ...rest] = identifier.split('.');
102+
const aliasEntries = aliasByIdentifier.get(id);
103+
if (aliasEntries) {
104+
for (const { alias, sources } of aliasEntries) {
105+
const result = followReExports(sources, [alias, ...rest].join('.'), true);
106+
if (result) return result;
107+
}
108+
}
109+
110+
for (const { namespace: ns, sources } of namespaceEdges) {
111+
const result = followReExports(sources, `${ns}.${identifier}`, true);
112+
if (result) return result;
113+
}
114+
115+
if (foundNamespace) return [true, foundNamespace];
116+
return [false];
117+
};
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { OPAQUE } from '../../constants.js';
2+
import type { Identifier, ModuleGraph } from '../../types/module-graph.js';
3+
import {
4+
getAliasReExportMap,
5+
getNamespaceReExportSources,
6+
getPassThroughReExportSources,
7+
getStarReExportSources,
8+
} from '../visitors.js';
9+
10+
export const isReferenced = (
11+
graph: ModuleGraph,
12+
entryPaths: Set<string>,
13+
filePath: string,
14+
id: Identifier,
15+
options: { includeEntryExports: boolean }
16+
) => {
17+
const seen = new Set<string>();
18+
19+
const check = (currentPath: string, currentId: string): [boolean, string | undefined] => {
20+
const isEntryFile = entryPaths.has(currentPath);
21+
let reExportingEntryFile: string | undefined = isEntryFile ? currentPath : undefined;
22+
23+
if (isEntryFile && !options.includeEntryExports) return [false, reExportingEntryFile];
24+
25+
if (seen.has(currentPath)) return [false, reExportingEntryFile];
26+
seen.add(currentPath);
27+
28+
const restIds = currentId.split('.');
29+
const identifier = restIds.shift();
30+
const file = graph.get(currentPath)?.imported;
31+
32+
if (!identifier || !file) {
33+
return [false, reExportingEntryFile];
34+
}
35+
36+
const directSources = getPassThroughReExportSources(file, identifier);
37+
const starSources = getStarReExportSources(file);
38+
39+
const followSources = (sources: Set<string>, nextId: string): boolean => {
40+
for (const byFilePath of sources) {
41+
if (seen.has(byFilePath)) continue;
42+
const result = check(byFilePath, nextId);
43+
if (result[1]) reExportingEntryFile = result[1];
44+
if (result[0]) return true;
45+
}
46+
return false;
47+
};
48+
49+
if (
50+
file.imported.get(OPAQUE) ||
51+
((identifier === currentId || (identifier !== currentId && file.refs.has(currentId))) &&
52+
(file.imported.has(identifier) || file.importedAs.has(identifier)))
53+
) {
54+
return [true, reExportingEntryFile];
55+
}
56+
57+
for (const [exportId, aliases] of file.importedAs) {
58+
if (identifier === exportId) {
59+
for (const alias of aliases.keys()) {
60+
const aliasedRef = [alias, ...restIds].join('.');
61+
if (file.refs.has(aliasedRef)) {
62+
return [true, reExportingEntryFile];
63+
}
64+
}
65+
}
66+
}
67+
68+
for (const namespace of file.importedNs.keys()) {
69+
if (file.refs.has(`${namespace}.${currentId}`)) {
70+
return [true, reExportingEntryFile];
71+
}
72+
73+
const nsAliasMap = getAliasReExportMap(file, namespace);
74+
if (nsAliasMap) {
75+
for (const [alias, sources] of nsAliasMap) {
76+
if (followSources(sources, `${alias}.${currentId}`)) return [true, reExportingEntryFile];
77+
}
78+
}
79+
80+
const nsReExportSources = getNamespaceReExportSources(file, namespace);
81+
if (nsReExportSources) {
82+
if (followSources(nsReExportSources, `${namespace}.${currentId}`)) return [true, reExportingEntryFile];
83+
}
84+
}
85+
86+
const aliasMap = getAliasReExportMap(file, identifier);
87+
if (aliasMap) {
88+
for (const [alias, sources] of aliasMap) {
89+
const ref = [alias, ...restIds].join('.');
90+
if (followSources(sources, ref)) return [true, reExportingEntryFile];
91+
}
92+
}
93+
94+
if (directSources) {
95+
if (followSources(directSources, currentId)) return [true, reExportingEntryFile];
96+
} else if (starSources) {
97+
if (followSources(starSources, currentId)) return [true, reExportingEntryFile];
98+
}
99+
100+
for (const [namespace, sources] of file.reExportedNs) {
101+
if (followSources(sources, `${namespace}.${currentId}`)) {
102+
return [true, reExportingEntryFile];
103+
}
104+
}
105+
106+
return [false, reExportingEntryFile];
107+
};
108+
109+
return check(filePath, id);
110+
};

0 commit comments

Comments
 (0)