@kernlang/review 3.2.3 → 3.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cache.js +140 -3
- package/dist/cache.js.map +1 -1
- package/dist/call-graph.d.ts +4 -1
- package/dist/call-graph.js +290 -25
- package/dist/call-graph.js.map +1 -1
- package/dist/external-tools.d.ts +23 -4
- package/dist/external-tools.js +68 -12
- package/dist/external-tools.js.map +1 -1
- package/dist/graph.js +149 -39
- package/dist/graph.js.map +1 -1
- package/dist/index.d.ts +27 -3
- package/dist/index.js +254 -41
- package/dist/index.js.map +1 -1
- package/dist/inferrer.d.ts +5 -0
- package/dist/inferrer.js +1 -1
- package/dist/inferrer.js.map +1 -1
- package/dist/mappers/ts-concepts.js +31 -6
- package/dist/mappers/ts-concepts.js.map +1 -1
- package/dist/public-api.d.ts +73 -0
- package/dist/public-api.js +351 -0
- package/dist/public-api.js.map +1 -0
- package/dist/reporter.d.ts +5 -0
- package/dist/reporter.js +119 -84
- package/dist/reporter.js.map +1 -1
- package/dist/review-health.d.ts +38 -0
- package/dist/review-health.js +60 -0
- package/dist/review-health.js.map +1 -0
- package/dist/rules/async.js +4 -16
- package/dist/rules/async.js.map +1 -1
- package/dist/rules/base.js +112 -87
- package/dist/rules/base.js.map +1 -1
- package/dist/rules/confidence.d.ts +2 -2
- package/dist/rules/confidence.js +32 -15
- package/dist/rules/confidence.js.map +1 -1
- package/dist/rules/dead-code.d.ts +2 -1
- package/dist/rules/dead-code.js +49 -3
- package/dist/rules/dead-code.js.map +1 -1
- package/dist/rules/index.js +131 -0
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/kern-source-cross-file.d.ts +2 -0
- package/dist/rules/kern-source-cross-file.js +102 -0
- package/dist/rules/kern-source-cross-file.js.map +1 -0
- package/dist/rules/kern-source.js +51 -4
- package/dist/rules/kern-source.js.map +1 -1
- package/dist/rules/nextjs-app-router.js +936 -31
- package/dist/rules/nextjs-app-router.js.map +1 -1
- package/dist/rules/nextjs.js +193 -10
- package/dist/rules/nextjs.js.map +1 -1
- package/dist/rules/react-composition.js +442 -61
- package/dist/rules/react-composition.js.map +1 -1
- package/dist/rules/react-hooks.js +51 -2
- package/dist/rules/react-hooks.js.map +1 -1
- package/dist/rules/react.js +265 -49
- package/dist/rules/react.js.map +1 -1
- package/dist/rules/utils.d.ts +37 -2
- package/dist/rules/utils.js +113 -0
- package/dist/rules/utils.js.map +1 -1
- package/dist/semantic-diff.js +1 -1
- package/dist/semantic-diff.js.map +1 -1
- package/dist/taint-ast.js +228 -4
- package/dist/taint-ast.js.map +1 -1
- package/dist/taint-crossfile.d.ts +30 -2
- package/dist/taint-crossfile.js +280 -59
- package/dist/taint-crossfile.js.map +1 -1
- package/dist/taint-types.d.ts +2 -1
- package/dist/taint-types.js +32 -2
- package/dist/taint-types.js.map +1 -1
- package/dist/taint.d.ts +1 -1
- package/dist/taint.js +1 -1
- package/dist/taint.js.map +1 -1
- package/dist/types.d.ts +78 -0
- package/dist/types.js.map +1 -1
- package/package.json +3 -3
package/dist/cache.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
-
import { join } from 'path';
|
|
4
|
+
import { dirname, join, resolve } from 'path';
|
|
5
5
|
// Version stamp for cache invalidation — changes when rules/analyzers change
|
|
6
|
-
const REVIEW_CACHE_VERSION = '3.2.
|
|
6
|
+
const REVIEW_CACHE_VERSION = '3.2.3-review-cache-2';
|
|
7
|
+
const IMPORT_SPECIFIER_RE = /(?:import|export)\s+(?:[^'"`]*?\s+from\s+)?['"]([^'"]+)['"]|import\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
8
|
+
const EXTENSION_FALLBACK = {
|
|
9
|
+
'.js': ['.ts', '.tsx', '.mts', '.cts'],
|
|
10
|
+
'.jsx': ['.tsx'],
|
|
11
|
+
'.mjs': ['.mts'],
|
|
12
|
+
'.cjs': ['.cts'],
|
|
13
|
+
};
|
|
7
14
|
export class ReviewCache {
|
|
8
15
|
l1 = new Map();
|
|
9
16
|
cacheDir;
|
|
@@ -71,8 +78,16 @@ export function computeCacheKey(fileContent, config, filePath) {
|
|
|
71
78
|
// Include version so cache auto-invalidates when kern-lang is upgraded
|
|
72
79
|
hash.update(REVIEW_CACHE_VERSION);
|
|
73
80
|
hash.update(fileContent);
|
|
74
|
-
hash.update(JSON.stringify(config));
|
|
81
|
+
hash.update(JSON.stringify(serializeConfigForCache(config, filePath)));
|
|
75
82
|
hash.update(filePath);
|
|
83
|
+
hashRelativeImportTree(hash, filePath, fileContent, new Set([filePath]), 0);
|
|
84
|
+
// Include the host tsconfig's path + content in the cache key so findings are invalidated when
|
|
85
|
+
// the user edits (or adds) a tsconfig that changes compilerOptions like `jsx`, `paths`, `strict`.
|
|
86
|
+
// Monorepos typically put shared options in a tsconfig.base.json that the per-package configs
|
|
87
|
+
// `extends`, so walk the extends chain and hash every file along the way.
|
|
88
|
+
if (config.tsConfigFilePath) {
|
|
89
|
+
hashTsConfigChain(hash, config.tsConfigFilePath);
|
|
90
|
+
}
|
|
76
91
|
// Include custom rule file contents in cache key to avoid stale hits when rules change
|
|
77
92
|
if (config.rulesDirs) {
|
|
78
93
|
for (const dir of config.rulesDirs) {
|
|
@@ -92,6 +107,128 @@ export function computeCacheKey(fileContent, config, filePath) {
|
|
|
92
107
|
}
|
|
93
108
|
return hash.digest('hex');
|
|
94
109
|
}
|
|
110
|
+
function serializeConfigForCache(config, filePath) {
|
|
111
|
+
const serialized = {
|
|
112
|
+
...config,
|
|
113
|
+
fileContextMap: undefined,
|
|
114
|
+
graphFileMap: undefined,
|
|
115
|
+
};
|
|
116
|
+
const fileContext = config.fileContextMap?.get(filePath);
|
|
117
|
+
if (fileContext) {
|
|
118
|
+
serialized.fileContext = fileContext;
|
|
119
|
+
if (fileContext.importedBy.length > 0 && config.fileContextMap) {
|
|
120
|
+
serialized.importerContexts = fileContext.importedBy
|
|
121
|
+
.map((importer) => config.fileContextMap?.get(importer))
|
|
122
|
+
.filter(Boolean);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const graphFile = config.graphFileMap?.get(filePath);
|
|
126
|
+
if (graphFile) {
|
|
127
|
+
serialized.graphFile = graphFile;
|
|
128
|
+
}
|
|
129
|
+
if (config.fileContextMap) {
|
|
130
|
+
serialized.graphContextEnabled = true;
|
|
131
|
+
}
|
|
132
|
+
if (config.graphFileMap) {
|
|
133
|
+
serialized.graphEdgesEnabled = true;
|
|
134
|
+
}
|
|
135
|
+
return serialized;
|
|
136
|
+
}
|
|
137
|
+
function hashTsConfigChain(hash, tsconfigPath, seen = new Set(), depth = 0) {
|
|
138
|
+
if (depth > 10)
|
|
139
|
+
return; // Cycle / pathological chain guard.
|
|
140
|
+
const absPath = resolve(tsconfigPath);
|
|
141
|
+
if (seen.has(absPath))
|
|
142
|
+
return;
|
|
143
|
+
seen.add(absPath);
|
|
144
|
+
if (!existsSync(absPath))
|
|
145
|
+
return;
|
|
146
|
+
hash.update(absPath);
|
|
147
|
+
let content = '';
|
|
148
|
+
try {
|
|
149
|
+
content = readFileSync(absPath, 'utf-8');
|
|
150
|
+
hash.update(content);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// Follow the `extends` field so shared base configs participate in invalidation. Stripping
|
|
156
|
+
// comments with a naive regex is good enough for the extends field — tsconfigs rarely hide a
|
|
157
|
+
// string like `"extends"` inside a comment, and JSONC edge cases are acceptable loss here.
|
|
158
|
+
try {
|
|
159
|
+
const stripped = content.replace(/\/\*[\s\S]*?\*\/|\/\/.*$/gm, '');
|
|
160
|
+
const parsed = JSON.parse(stripped);
|
|
161
|
+
const extended = parsed?.extends;
|
|
162
|
+
const extendsList = Array.isArray(extended) ? extended : typeof extended === 'string' ? [extended] : [];
|
|
163
|
+
for (const raw of extendsList) {
|
|
164
|
+
if (typeof raw !== 'string')
|
|
165
|
+
continue;
|
|
166
|
+
// Resolve relative to the current tsconfig; non-relative specifiers (package refs) aren't walked —
|
|
167
|
+
// they live in node_modules and would require full resolution, which is overkill for cache invalidation.
|
|
168
|
+
const candidate = raw.startsWith('.') ? resolve(dirname(absPath), raw) : undefined;
|
|
169
|
+
if (!candidate)
|
|
170
|
+
continue;
|
|
171
|
+
const withExt = candidate.endsWith('.json') ? candidate : `${candidate}.json`;
|
|
172
|
+
hashTsConfigChain(hash, withExt, seen, depth + 1);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// Malformed JSONC — skip extends walk; already hashed the raw content above.
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function hashRelativeImportTree(hash, filePath, fileContent, seen, depth, maxDepth = 3) {
|
|
180
|
+
if (depth >= maxDepth)
|
|
181
|
+
return;
|
|
182
|
+
for (const specifier of collectRelativeImportSpecifiers(fileContent)) {
|
|
183
|
+
for (const candidate of resolveImportCandidates(filePath, specifier)) {
|
|
184
|
+
if (!existsSync(candidate) || seen.has(candidate))
|
|
185
|
+
continue;
|
|
186
|
+
seen.add(candidate);
|
|
187
|
+
try {
|
|
188
|
+
const importedContent = readFileSync(candidate, 'utf-8');
|
|
189
|
+
hash.update(candidate);
|
|
190
|
+
hash.update(importedContent);
|
|
191
|
+
hashRelativeImportTree(hash, candidate, importedContent, seen, depth + 1, maxDepth);
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
/* skip unreadable imports */
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function collectRelativeImportSpecifiers(fileContent) {
|
|
201
|
+
const specs = new Set();
|
|
202
|
+
for (const match of fileContent.matchAll(IMPORT_SPECIFIER_RE)) {
|
|
203
|
+
const spec = match[1] ?? match[2];
|
|
204
|
+
if (spec?.startsWith('.'))
|
|
205
|
+
specs.add(spec);
|
|
206
|
+
}
|
|
207
|
+
return [...specs];
|
|
208
|
+
}
|
|
209
|
+
function resolveImportCandidates(filePath, specifier) {
|
|
210
|
+
const baseDir = dirname(filePath);
|
|
211
|
+
const candidates = [];
|
|
212
|
+
const pushResolved = (relativePath) => {
|
|
213
|
+
candidates.push(resolve(baseDir, relativePath));
|
|
214
|
+
};
|
|
215
|
+
const ext = Object.keys(EXTENSION_FALLBACK).find((suffix) => specifier.endsWith(suffix));
|
|
216
|
+
if (ext) {
|
|
217
|
+
pushResolved(specifier);
|
|
218
|
+
for (const fallback of EXTENSION_FALLBACK[ext]) {
|
|
219
|
+
pushResolved(`${specifier.slice(0, -ext.length)}${fallback}`);
|
|
220
|
+
}
|
|
221
|
+
return candidates;
|
|
222
|
+
}
|
|
223
|
+
if (/\.[cm]?[jt]sx?$/.test(specifier)) {
|
|
224
|
+
pushResolved(specifier);
|
|
225
|
+
return candidates;
|
|
226
|
+
}
|
|
227
|
+
for (const suffix of ['.ts', '.tsx', '.mts', '.cts', '/index.ts', '/index.tsx', '/index.mts', '/index.cts']) {
|
|
228
|
+
pushResolved(`${specifier}${suffix}`);
|
|
229
|
+
}
|
|
230
|
+
return candidates;
|
|
231
|
+
}
|
|
95
232
|
export const reviewCache = new ReviewCache();
|
|
96
233
|
export function clearReviewCache() {
|
|
97
234
|
reviewCache.clear();
|
package/dist/cache.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AAC7F,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
|
package/dist/call-graph.d.ts
CHANGED
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
* - Higher-order functions: arr.map(callback)
|
|
16
16
|
* - Computed property access: obj[key]()
|
|
17
17
|
* - Framework magic: decorators, DI containers
|
|
18
|
-
*
|
|
18
|
+
*
|
|
19
|
+
* Resolved via local alias tracking:
|
|
20
|
+
* - const f = foo; f() (identifier alias to local / imported fn)
|
|
21
|
+
* - const g = f; g() (transitive alias, one extra hop)
|
|
19
22
|
*
|
|
20
23
|
* Dead export findings from unresolved edges get lower confidence (0.70 vs 0.90).
|
|
21
24
|
*/
|
package/dist/call-graph.js
CHANGED
|
@@ -15,17 +15,19 @@
|
|
|
15
15
|
* - Higher-order functions: arr.map(callback)
|
|
16
16
|
* - Computed property access: obj[key]()
|
|
17
17
|
* - Framework magic: decorators, DI containers
|
|
18
|
-
*
|
|
18
|
+
*
|
|
19
|
+
* Resolved via local alias tracking:
|
|
20
|
+
* - const f = foo; f() (identifier alias to local / imported fn)
|
|
21
|
+
* - const g = f; g() (transitive alias, one extra hop)
|
|
19
22
|
*
|
|
20
23
|
* Dead export findings from unresolved edges get lower confidence (0.70 vs 0.90).
|
|
21
24
|
*/
|
|
22
25
|
import { SyntaxKind } from 'ts-morph';
|
|
23
|
-
|
|
24
|
-
/** Build a map of importName → resolvedFilePath#exportName for a source file */
|
|
26
|
+
/** Build a map of local import binding → resolved export target for a source file */
|
|
25
27
|
function buildImportBindings(sourceFile, graphFiles) {
|
|
26
28
|
const bindings = new Map();
|
|
27
29
|
for (const decl of sourceFile.getImportDeclarations()) {
|
|
28
|
-
const resolvedSf = decl
|
|
30
|
+
const resolvedSf = resolveImportSourceFile(sourceFile, decl, graphFiles);
|
|
29
31
|
if (!resolvedSf)
|
|
30
32
|
continue;
|
|
31
33
|
const resolvedPath = resolvedSf.getFilePath();
|
|
@@ -33,25 +35,174 @@ function buildImportBindings(sourceFile, graphFiles) {
|
|
|
33
35
|
continue;
|
|
34
36
|
// Named imports: import { foo, bar as baz } from './mod'
|
|
35
37
|
for (const named of decl.getNamedImports()) {
|
|
36
|
-
const localName = named.getName();
|
|
37
|
-
const _originalName = named.getAliasNode()?.getText() || localName;
|
|
38
|
-
// The imported name is the original export name
|
|
39
38
|
const importedName = named.getName();
|
|
40
|
-
|
|
39
|
+
const localName = named.getAliasNode()?.getText() ?? importedName;
|
|
40
|
+
const target = resolveExportBinding(resolvedSf, importedName, graphFiles) ?? {
|
|
41
|
+
targetFile: resolvedPath,
|
|
42
|
+
targetName: importedName,
|
|
43
|
+
};
|
|
44
|
+
bindings.set(localName, { kind: 'named', ...target });
|
|
41
45
|
}
|
|
42
46
|
// Default import: import Foo from './mod'
|
|
43
47
|
const defaultImport = decl.getDefaultImport();
|
|
44
48
|
if (defaultImport) {
|
|
45
|
-
|
|
49
|
+
const target = resolveDefaultExportBinding(resolvedSf, graphFiles) ?? {
|
|
50
|
+
targetFile: resolvedPath,
|
|
51
|
+
targetName: 'default',
|
|
52
|
+
};
|
|
53
|
+
bindings.set(defaultImport.getText(), { kind: 'default', ...target });
|
|
46
54
|
}
|
|
47
55
|
// Namespace import: import * as mod from './mod'
|
|
48
56
|
const namespaceImport = decl.getNamespaceImport();
|
|
49
57
|
if (namespaceImport) {
|
|
50
|
-
bindings.set(namespaceImport.getText(),
|
|
58
|
+
bindings.set(namespaceImport.getText(), {
|
|
59
|
+
kind: 'namespace',
|
|
60
|
+
targetFile: resolvedPath,
|
|
61
|
+
targetName: '*',
|
|
62
|
+
members: buildNamespaceMembers(resolvedSf, graphFiles),
|
|
63
|
+
});
|
|
51
64
|
}
|
|
52
65
|
}
|
|
53
66
|
return bindings;
|
|
54
67
|
}
|
|
68
|
+
function resolveImportSourceFile(sourceFile, decl, graphFiles) {
|
|
69
|
+
try {
|
|
70
|
+
const resolved = decl.getModuleSpecifierSourceFile() ?? undefined;
|
|
71
|
+
if (resolved && graphFiles.has(resolved.getFilePath()))
|
|
72
|
+
return resolved;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
/* fall back to the import graph edge below */
|
|
76
|
+
}
|
|
77
|
+
let specifier;
|
|
78
|
+
try {
|
|
79
|
+
specifier = decl.getModuleSpecifierValue();
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
const graphFile = graphFiles.get(sourceFile.getFilePath());
|
|
85
|
+
const edge = graphFile?.importEdges.find((candidate) => candidate.specifier === specifier);
|
|
86
|
+
return edge ? sourceFile.getProject().getSourceFile(edge.to) : undefined;
|
|
87
|
+
}
|
|
88
|
+
function resolveExportBinding(sourceFile, exportName, graphFiles) {
|
|
89
|
+
const decls = sourceFile.getExportedDeclarations().get(exportName);
|
|
90
|
+
if (!decls || decls.length === 0)
|
|
91
|
+
return undefined;
|
|
92
|
+
return resolveBindingFromDeclarations(decls, exportName, graphFiles);
|
|
93
|
+
}
|
|
94
|
+
function resolveDefaultExportBinding(sourceFile, graphFiles) {
|
|
95
|
+
const symbol = sourceFile.getDefaultExportSymbol();
|
|
96
|
+
if (!symbol)
|
|
97
|
+
return undefined;
|
|
98
|
+
return resolveBindingFromDeclarations(symbol.getDeclarations(), 'default', graphFiles);
|
|
99
|
+
}
|
|
100
|
+
function buildNamespaceMembers(sourceFile, graphFiles) {
|
|
101
|
+
const members = new Map();
|
|
102
|
+
for (const [exportName, decls] of sourceFile.getExportedDeclarations()) {
|
|
103
|
+
const resolved = resolveBindingFromDeclarations(decls, exportName, graphFiles);
|
|
104
|
+
if (resolved)
|
|
105
|
+
members.set(exportName, resolved);
|
|
106
|
+
}
|
|
107
|
+
return members;
|
|
108
|
+
}
|
|
109
|
+
function addImportedExportKeys(importedExportKeys, importBindings) {
|
|
110
|
+
for (const binding of importBindings.values()) {
|
|
111
|
+
if (binding.kind === 'namespace') {
|
|
112
|
+
for (const member of binding.members?.values() ?? []) {
|
|
113
|
+
importedExportKeys.add(`${member.targetFile}#${member.targetName}`);
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
importedExportKeys.add(`${binding.targetFile}#${binding.targetName}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function resolveBindingFromDeclarations(declarations, fallbackName, graphFiles) {
|
|
121
|
+
for (const decl of declarations) {
|
|
122
|
+
const declFile = decl.getSourceFile().getFilePath();
|
|
123
|
+
if (!graphFiles.has(declFile))
|
|
124
|
+
continue;
|
|
125
|
+
return {
|
|
126
|
+
targetFile: declFile,
|
|
127
|
+
targetName: getDeclarationBindingName(decl, fallbackName),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
function getDeclarationBindingName(decl, fallbackName) {
|
|
133
|
+
const maybeNamed = decl;
|
|
134
|
+
if (typeof maybeNamed.getName === 'function') {
|
|
135
|
+
const name = maybeNamed.getName();
|
|
136
|
+
if (name)
|
|
137
|
+
return name;
|
|
138
|
+
}
|
|
139
|
+
if (decl.getKindName() === 'ExportAssignment') {
|
|
140
|
+
const expr = decl.getExpression?.();
|
|
141
|
+
if (expr?.getKind() === SyntaxKind.Identifier)
|
|
142
|
+
return expr.getText();
|
|
143
|
+
}
|
|
144
|
+
return fallbackName;
|
|
145
|
+
}
|
|
146
|
+
// ── Local Alias Resolution ──────────────────────────────────────────────
|
|
147
|
+
/**
|
|
148
|
+
* Build a map of local variable aliases pointing to functions.
|
|
149
|
+
*
|
|
150
|
+
* const f = importedFn → { targetFile: '<import target>', targetName: '<export name>' }
|
|
151
|
+
* const g = localFn → { targetFile: '<this file>', targetName: 'localFn' }
|
|
152
|
+
* const h = g → resolved transitively on a second pass
|
|
153
|
+
*
|
|
154
|
+
* Only covers top-level `const/let/var name = <identifier>` patterns (no destructuring,
|
|
155
|
+
* no reassignment tracking, no function-returned aliases). Namespace imports are
|
|
156
|
+
* deliberately skipped here — they are handled by the property-access branch of
|
|
157
|
+
* extractCallSites (e.g. `ns.foo()`).
|
|
158
|
+
*/
|
|
159
|
+
function buildLocalAliases(sourceFile, localFnNames, importBindings) {
|
|
160
|
+
const aliases = new Map();
|
|
161
|
+
const filePath = sourceFile.getFilePath();
|
|
162
|
+
// First pass: direct aliases (imported fn or local fn as RHS)
|
|
163
|
+
for (const stmt of sourceFile.getVariableStatements()) {
|
|
164
|
+
for (const decl of stmt.getDeclarations()) {
|
|
165
|
+
const init = decl.getInitializer();
|
|
166
|
+
if (!init || init.getKind() !== SyntaxKind.Identifier)
|
|
167
|
+
continue;
|
|
168
|
+
const aliasName = decl.getName();
|
|
169
|
+
const sourceName = init.getText();
|
|
170
|
+
if (aliasName === sourceName)
|
|
171
|
+
continue;
|
|
172
|
+
const importBinding = importBindings.get(sourceName);
|
|
173
|
+
if (importBinding && importBinding.kind !== 'namespace') {
|
|
174
|
+
aliases.set(aliasName, {
|
|
175
|
+
targetFile: importBinding.targetFile,
|
|
176
|
+
targetName: importBinding.targetName,
|
|
177
|
+
});
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (localFnNames.has(sourceName)) {
|
|
181
|
+
aliases.set(aliasName, {
|
|
182
|
+
targetFile: filePath,
|
|
183
|
+
targetName: sourceName,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Second pass: transitive aliases (RHS is itself an alias created in pass 1).
|
|
189
|
+
// One extra hop catches `const g = f` where `f` was `const f = imp`.
|
|
190
|
+
for (const stmt of sourceFile.getVariableStatements()) {
|
|
191
|
+
for (const decl of stmt.getDeclarations()) {
|
|
192
|
+
const init = decl.getInitializer();
|
|
193
|
+
if (!init || init.getKind() !== SyntaxKind.Identifier)
|
|
194
|
+
continue;
|
|
195
|
+
const aliasName = decl.getName();
|
|
196
|
+
const sourceName = init.getText();
|
|
197
|
+
if (aliases.has(aliasName))
|
|
198
|
+
continue;
|
|
199
|
+
const transitive = aliases.get(sourceName);
|
|
200
|
+
if (transitive)
|
|
201
|
+
aliases.set(aliasName, transitive);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return aliases;
|
|
205
|
+
}
|
|
55
206
|
// ── Function Collection ─────────────────────────────────────────────────
|
|
56
207
|
/** Collect all function declarations from a source file */
|
|
57
208
|
function collectFunctions(sourceFile, filePath) {
|
|
@@ -122,12 +273,19 @@ function collectFunctions(sourceFile, filePath) {
|
|
|
122
273
|
}
|
|
123
274
|
// ── Call Site Extraction ────────────────────────────────────────────────
|
|
124
275
|
/** Extract all call sites from a function body */
|
|
125
|
-
function extractCallSites(fnNode, sourceFile, localFnNames, importBindings) {
|
|
276
|
+
function extractCallSites(fnNode, sourceFile, localFnNames, importBindings, localAliases) {
|
|
126
277
|
const callSites = [];
|
|
127
278
|
// Find the AST node for this function
|
|
128
279
|
const body = findFunctionBody(fnNode, sourceFile);
|
|
129
280
|
if (!body)
|
|
130
281
|
return callSites;
|
|
282
|
+
// Names that are re-bound anywhere inside this function (parameters or
|
|
283
|
+
// local var/let/const). If a file-global alias name is rebound here, we
|
|
284
|
+
// must not apply the alias — the inner binding shadows it. Conservative
|
|
285
|
+
// by design: we drop the alias even if the rebinding is in a sibling
|
|
286
|
+
// branch, falling back to an unresolved call site rather than resolving
|
|
287
|
+
// to the wrong target.
|
|
288
|
+
const shadowedNames = collectShadowedNames(body);
|
|
131
289
|
// Walk all call expressions in the body
|
|
132
290
|
body.forEachDescendant((node) => {
|
|
133
291
|
if (node.getKind() !== SyntaxKind.CallExpression)
|
|
@@ -161,13 +319,28 @@ function extractCallSites(fnNode, sourceFile, localFnNames, importBindings) {
|
|
|
161
319
|
}
|
|
162
320
|
// Imported function?
|
|
163
321
|
const binding = importBindings.get(targetName);
|
|
164
|
-
if (binding) {
|
|
165
|
-
const [targetFile, exportName] = splitBinding(binding);
|
|
322
|
+
if (binding && binding.kind !== 'namespace') {
|
|
166
323
|
callSites.push({
|
|
167
324
|
callerName: fnNode.name,
|
|
168
325
|
callerFile: fnNode.filePath,
|
|
169
|
-
targetName:
|
|
170
|
-
targetFile,
|
|
326
|
+
targetName: binding.targetName,
|
|
327
|
+
targetFile: binding.targetFile,
|
|
328
|
+
line: call.getStartLineNumber(),
|
|
329
|
+
argumentCount: args.length,
|
|
330
|
+
resolved: true,
|
|
331
|
+
hasAwait,
|
|
332
|
+
});
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
// Local alias — `const f = foo; f()` resolves to foo's target.
|
|
336
|
+
// Skip if the name is rebound within this function body (shadowed).
|
|
337
|
+
const alias = localAliases.get(targetName);
|
|
338
|
+
if (alias && !shadowedNames.has(targetName)) {
|
|
339
|
+
callSites.push({
|
|
340
|
+
callerName: fnNode.name,
|
|
341
|
+
callerFile: fnNode.filePath,
|
|
342
|
+
targetName: alias.targetName,
|
|
343
|
+
targetFile: alias.targetFile,
|
|
171
344
|
line: call.getStartLineNumber(),
|
|
172
345
|
argumentCount: args.length,
|
|
173
346
|
resolved: true,
|
|
@@ -210,13 +383,26 @@ function extractCallSites(fnNode, sourceFile, localFnNames, importBindings) {
|
|
|
210
383
|
}
|
|
211
384
|
// Namespace import: mod.foo()
|
|
212
385
|
const nsBinding = importBindings.get(objName);
|
|
213
|
-
if (nsBinding?.
|
|
214
|
-
const
|
|
386
|
+
if (nsBinding?.kind === 'namespace') {
|
|
387
|
+
const memberBinding = nsBinding.members?.get(methodName);
|
|
388
|
+
if (!memberBinding) {
|
|
389
|
+
callSites.push({
|
|
390
|
+
callerName: fnNode.name,
|
|
391
|
+
callerFile: fnNode.filePath,
|
|
392
|
+
targetName: `${objName}.${methodName}`,
|
|
393
|
+
targetFile: '',
|
|
394
|
+
line: call.getStartLineNumber(),
|
|
395
|
+
argumentCount: args.length,
|
|
396
|
+
resolved: false,
|
|
397
|
+
hasAwait,
|
|
398
|
+
});
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
215
401
|
callSites.push({
|
|
216
402
|
callerName: fnNode.name,
|
|
217
403
|
callerFile: fnNode.filePath,
|
|
218
|
-
targetName:
|
|
219
|
-
targetFile,
|
|
404
|
+
targetName: memberBinding.targetName,
|
|
405
|
+
targetFile: memberBinding.targetFile,
|
|
220
406
|
line: call.getStartLineNumber(),
|
|
221
407
|
argumentCount: args.length,
|
|
222
408
|
resolved: true,
|
|
@@ -251,6 +437,51 @@ function extractCallSites(fnNode, sourceFile, localFnNames, importBindings) {
|
|
|
251
437
|
});
|
|
252
438
|
return callSites;
|
|
253
439
|
}
|
|
440
|
+
/**
|
|
441
|
+
* Collect every identifier name that is rebound inside this function body —
|
|
442
|
+
* function parameters plus every variable declaration anywhere in the body.
|
|
443
|
+
* Used to suppress file-global aliases when the function shadows them.
|
|
444
|
+
*/
|
|
445
|
+
function collectShadowedNames(body) {
|
|
446
|
+
const names = new Set();
|
|
447
|
+
// Parameters of the enclosing function.
|
|
448
|
+
const enclosingFn = body.getParent();
|
|
449
|
+
if (enclosingFn && 'getParameters' in enclosingFn && typeof enclosingFn.getParameters === 'function') {
|
|
450
|
+
for (const p of enclosingFn.getParameters()) {
|
|
451
|
+
const nameNode = p.getNameNode();
|
|
452
|
+
const k = nameNode.getKindName();
|
|
453
|
+
if (k === 'Identifier') {
|
|
454
|
+
names.add(nameNode.getText());
|
|
455
|
+
}
|
|
456
|
+
else if (k === 'ObjectBindingPattern' || k === 'ArrayBindingPattern') {
|
|
457
|
+
for (const el of nameNode.getElements()) {
|
|
458
|
+
const nm = el.getName?.();
|
|
459
|
+
if (typeof nm === 'string')
|
|
460
|
+
names.add(nm);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
// All variable declarations anywhere inside the body (any nested scope).
|
|
466
|
+
body.forEachDescendant((node) => {
|
|
467
|
+
if (node.getKind() !== SyntaxKind.VariableDeclaration)
|
|
468
|
+
return;
|
|
469
|
+
const decl = node;
|
|
470
|
+
const nameNode = decl.getNameNode();
|
|
471
|
+
const k = nameNode.getKindName();
|
|
472
|
+
if (k === 'Identifier') {
|
|
473
|
+
names.add(nameNode.getText());
|
|
474
|
+
}
|
|
475
|
+
else if (k === 'ObjectBindingPattern' || k === 'ArrayBindingPattern') {
|
|
476
|
+
for (const el of nameNode.getElements()) {
|
|
477
|
+
const nm = el.getName?.();
|
|
478
|
+
if (typeof nm === 'string')
|
|
479
|
+
names.add(nm);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
return names;
|
|
484
|
+
}
|
|
254
485
|
/** Find the AST body node for a FunctionNode */
|
|
255
486
|
function findFunctionBody(fnNode, sourceFile) {
|
|
256
487
|
// Try named function
|
|
@@ -272,10 +503,6 @@ function findFunctionBody(fnNode, sourceFile) {
|
|
|
272
503
|
}
|
|
273
504
|
return undefined;
|
|
274
505
|
}
|
|
275
|
-
function splitBinding(binding) {
|
|
276
|
-
const idx = binding.lastIndexOf('#');
|
|
277
|
-
return [binding.slice(0, idx), binding.slice(idx + 1)];
|
|
278
|
-
}
|
|
279
506
|
const BUILTINS = new Set([
|
|
280
507
|
'console',
|
|
281
508
|
'setTimeout',
|
|
@@ -317,6 +544,7 @@ export function buildCallGraph(graph, project) {
|
|
|
317
544
|
}
|
|
318
545
|
const allFunctions = new Map();
|
|
319
546
|
const fileFunctions = new Map();
|
|
547
|
+
const importedExportKeys = new Set();
|
|
320
548
|
// Phase 1: Collect all functions from all files
|
|
321
549
|
for (const gf of graph.files) {
|
|
322
550
|
const sf = project.getSourceFile(gf.path);
|
|
@@ -337,8 +565,10 @@ export function buildCallGraph(graph, project) {
|
|
|
337
565
|
const fns = fileFunctions.get(gf.path) || [];
|
|
338
566
|
const localFnNames = new Set(fns.map((f) => f.name));
|
|
339
567
|
const importBindings = buildImportBindings(sf, graphFiles);
|
|
568
|
+
addImportedExportKeys(importedExportKeys, importBindings);
|
|
569
|
+
const localAliases = buildLocalAliases(sf, localFnNames, importBindings);
|
|
340
570
|
for (const fn of fns) {
|
|
341
|
-
fn.calls = extractCallSites(fn, sf, localFnNames, importBindings);
|
|
571
|
+
fn.calls = extractCallSites(fn, sf, localFnNames, importBindings, localAliases);
|
|
342
572
|
unresolvedCount += fn.calls.filter((c) => !c.resolved).length;
|
|
343
573
|
}
|
|
344
574
|
}
|
|
@@ -354,11 +584,46 @@ export function buildCallGraph(graph, project) {
|
|
|
354
584
|
}
|
|
355
585
|
}
|
|
356
586
|
}
|
|
587
|
+
// Collect every class name instantiated somewhere in the graph. A file
|
|
588
|
+
// can export a singleton (`export const foo = new Foo()`) while no file
|
|
589
|
+
// imports the class by name — all consumers use the instance. Without
|
|
590
|
+
// this, every method of a singleton-exported class reads as dead.
|
|
591
|
+
const instantiatedClasses = new Set();
|
|
592
|
+
for (const gf of graph.files) {
|
|
593
|
+
const sf = project.getSourceFile(gf.path);
|
|
594
|
+
if (!sf)
|
|
595
|
+
continue;
|
|
596
|
+
sf.forEachDescendant((node) => {
|
|
597
|
+
if (node.getKind() !== SyntaxKind.NewExpression)
|
|
598
|
+
return;
|
|
599
|
+
const newExpr = node;
|
|
600
|
+
const callee = newExpr.getExpression();
|
|
601
|
+
if (callee.getKindName() === 'Identifier') {
|
|
602
|
+
instantiatedClasses.add(callee.getText());
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
}
|
|
357
606
|
// Phase 4: Identify dead exports and orphan functions
|
|
358
607
|
const deadExports = [];
|
|
359
608
|
const orphanFunctions = [];
|
|
360
609
|
for (const [key, fn] of allFunctions) {
|
|
361
|
-
if (fn.isExported && fn.calledBy.length === 0) {
|
|
610
|
+
if (fn.isExported && fn.calledBy.length === 0 && !importedExportKeys.has(key)) {
|
|
611
|
+
// Class methods inherit liveness from the class. `FileStateCache.get`
|
|
612
|
+
// is never imported by name — consumers import `FileStateCache` and
|
|
613
|
+
// invoke `.get()` via property access (unresolvable statically). If
|
|
614
|
+
// the class itself is imported anywhere, flagging each method as
|
|
615
|
+
// dead is a false positive on every class-shipping package.
|
|
616
|
+
if (fn.name.includes('.')) {
|
|
617
|
+
const className = fn.name.split('.')[0];
|
|
618
|
+
if (importedExportKeys.has(`${fn.filePath}#${className}`))
|
|
619
|
+
continue;
|
|
620
|
+
// Class is never imported by name but is instantiated somewhere in
|
|
621
|
+
// the graph (singleton / factory pattern). Methods reachable via
|
|
622
|
+
// the instance aren't visible to static analysis, so trust the
|
|
623
|
+
// `new`-site as proof of liveness.
|
|
624
|
+
if (instantiatedClasses.has(className))
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
362
627
|
deadExports.push(key);
|
|
363
628
|
}
|
|
364
629
|
if (!fn.isExported && fn.calledBy.length === 0) {
|