@kernlang/review 3.2.3 → 3.3.5

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.
Files changed (92) hide show
  1. package/dist/cache.js +140 -3
  2. package/dist/cache.js.map +1 -1
  3. package/dist/call-graph.d.ts +4 -1
  4. package/dist/call-graph.js +290 -25
  5. package/dist/call-graph.js.map +1 -1
  6. package/dist/concept-rules/contract-drift.d.ts +21 -0
  7. package/dist/concept-rules/contract-drift.js +66 -0
  8. package/dist/concept-rules/contract-drift.js.map +1 -0
  9. package/dist/concept-rules/cross-stack-utils.d.ts +50 -0
  10. package/dist/concept-rules/cross-stack-utils.js +98 -0
  11. package/dist/concept-rules/cross-stack-utils.js.map +1 -0
  12. package/dist/concept-rules/index.js +12 -1
  13. package/dist/concept-rules/index.js.map +1 -1
  14. package/dist/concept-rules/tainted-across-wire.d.ts +33 -0
  15. package/dist/concept-rules/tainted-across-wire.js +98 -0
  16. package/dist/concept-rules/tainted-across-wire.js.map +1 -0
  17. package/dist/concept-rules/untyped-api-response.d.ts +30 -0
  18. package/dist/concept-rules/untyped-api-response.js +71 -0
  19. package/dist/concept-rules/untyped-api-response.js.map +1 -0
  20. package/dist/external-tools.d.ts +36 -4
  21. package/dist/external-tools.js +79 -12
  22. package/dist/external-tools.js.map +1 -1
  23. package/dist/graph.js +149 -39
  24. package/dist/graph.js.map +1 -1
  25. package/dist/index.d.ts +29 -4
  26. package/dist/index.js +329 -47
  27. package/dist/index.js.map +1 -1
  28. package/dist/inferrer.d.ts +5 -0
  29. package/dist/inferrer.js +1 -1
  30. package/dist/inferrer.js.map +1 -1
  31. package/dist/llm-bridge.d.ts +26 -1
  32. package/dist/llm-bridge.js +42 -6
  33. package/dist/llm-bridge.js.map +1 -1
  34. package/dist/llm-review.js +29 -11
  35. package/dist/llm-review.js.map +1 -1
  36. package/dist/mappers/ts-concepts.js +278 -7
  37. package/dist/mappers/ts-concepts.js.map +1 -1
  38. package/dist/public-api.d.ts +73 -0
  39. package/dist/public-api.js +351 -0
  40. package/dist/public-api.js.map +1 -0
  41. package/dist/reporter.d.ts +5 -0
  42. package/dist/reporter.js +119 -84
  43. package/dist/reporter.js.map +1 -1
  44. package/dist/review-health.d.ts +38 -0
  45. package/dist/review-health.js +60 -0
  46. package/dist/review-health.js.map +1 -0
  47. package/dist/rules/async.js +4 -16
  48. package/dist/rules/async.js.map +1 -1
  49. package/dist/rules/base.js +112 -87
  50. package/dist/rules/base.js.map +1 -1
  51. package/dist/rules/confidence.d.ts +2 -2
  52. package/dist/rules/confidence.js +32 -15
  53. package/dist/rules/confidence.js.map +1 -1
  54. package/dist/rules/dead-code.d.ts +2 -1
  55. package/dist/rules/dead-code.js +49 -3
  56. package/dist/rules/dead-code.js.map +1 -1
  57. package/dist/rules/index.js +131 -0
  58. package/dist/rules/index.js.map +1 -1
  59. package/dist/rules/kern-source-cross-file.d.ts +2 -0
  60. package/dist/rules/kern-source-cross-file.js +102 -0
  61. package/dist/rules/kern-source-cross-file.js.map +1 -0
  62. package/dist/rules/kern-source.js +86 -9
  63. package/dist/rules/kern-source.js.map +1 -1
  64. package/dist/rules/nextjs-app-router.js +936 -31
  65. package/dist/rules/nextjs-app-router.js.map +1 -1
  66. package/dist/rules/nextjs.js +193 -10
  67. package/dist/rules/nextjs.js.map +1 -1
  68. package/dist/rules/react-composition.js +442 -61
  69. package/dist/rules/react-composition.js.map +1 -1
  70. package/dist/rules/react-hooks.js +51 -2
  71. package/dist/rules/react-hooks.js.map +1 -1
  72. package/dist/rules/react.js +265 -49
  73. package/dist/rules/react.js.map +1 -1
  74. package/dist/rules/utils.d.ts +37 -2
  75. package/dist/rules/utils.js +113 -0
  76. package/dist/rules/utils.js.map +1 -1
  77. package/dist/semantic-diff.js +1 -1
  78. package/dist/semantic-diff.js.map +1 -1
  79. package/dist/taint-ast.js +228 -4
  80. package/dist/taint-ast.js.map +1 -1
  81. package/dist/taint-crossfile.d.ts +30 -2
  82. package/dist/taint-crossfile.js +280 -59
  83. package/dist/taint-crossfile.js.map +1 -1
  84. package/dist/taint-types.d.ts +2 -1
  85. package/dist/taint-types.js +32 -2
  86. package/dist/taint-types.js.map +1 -1
  87. package/dist/taint.d.ts +1 -1
  88. package/dist/taint.js +1 -1
  89. package/dist/taint.js.map +1 -1
  90. package/dist/types.d.ts +80 -0
  91. package/dist/types.js.map +1 -1
  92. 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.0';
6
+ const REVIEW_CACHE_VERSION = '3.2.3-review-cache-3';
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":"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"}
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,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAG9C,6EAA6E;AAC7E,MAAM,oBAAoB,GAAG,sBAAsB,CAAC;AACpD,MAAM,mBAAmB,GACvB,+FAA+F,CAAC;AAClG,MAAM,kBAAkB,GAA6B;IACnD,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;IACtC,MAAM,EAAE,CAAC,MAAM,CAAC;IAChB,MAAM,EAAE,CAAC,MAAM,CAAC;IAChB,MAAM,EAAE,CAAC,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,OAAO,WAAW;IACd,EAAE,GAAG,IAAI,GAAG,EAAwB,CAAC;IACrC,QAAQ,CAAS;IAEzB;QACE,wGAAwG;QACxG,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;QAC/D,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,IAAI,EAAE,qBAAqB,CAAC,CAAC;QAClD,IAAI,CAAC,cAAc,EAAE,CAAC;IACxB,CAAC;IAEO,cAAc;QACpB,IAAI,CAAC;YACH,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC/B,SAAS,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAChD,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,yEAAyE;QAC3E,CAAC;IACH,CAAC;IAEM,GAAG,CAAC,GAAW;QACpB,WAAW;QACX,IAAI,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACrB,OAAO,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC1B,CAAC;QAED,WAAW;QACX,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,GAAG,OAAO,CAAC,CAAC;QACrD,IAAI,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC1B,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;gBAC1D,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;gBACvB,OAAO,IAAI,CAAC;YACd,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,SAAS,CAAC;YACnB,CAAC;QACH,CAAC;QAED,OAAO,SAAS,CAAC;IACnB,CAAC;IAEM,GAAG,CAAC,GAAW,EAAE,MAAoB;QAC1C,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACzB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,GAAG,OAAO,CAAC,CAAC;QACrD,IAAI,CAAC;YACH,IAAI,CAAC,cAAc,EAAE,CAAC;YACtB,aAAa,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC;QAC5D,CAAC;QAAC,MAAM,CAAC;YACP,sBAAsB;QACxB,CAAC;IACH,CAAC;IAEM,KAAK;QACV,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;QAChB,IAAI,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC9B,IAAI,CAAC;gBACH,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;gBACxD,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,CAAC;YAAC,MAAM,CAAC;gBACP,sBAAsB;YACxB,CAAC;QACH,CAAC;IACH,CAAC;CACF;AAED,MAAM,UAAU,eAAe,CAAC,WAAmB,EAAE,MAAoB,EAAE,QAAgB;IACzF,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IAClC,uEAAuE;IACvE,IAAI,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAClC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IACzB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,uBAAuB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;IACvE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACtB,sBAAsB,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,IAAI,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5E,+FAA+F;IAC/F,kGAAkG;IAClG,8FAA8F;IAC9F,0EAA0E;IAC1E,IAAI,MAAM,CAAC,gBAAgB,EAAE,CAAC;QAC5B,iBAAiB,CAAC,IAAI,EAAE,MAAM,CAAC,gBAAgB,CAAC,CAAC;IACnD,CAAC;IAED,uFAAuF;IACvF,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QACrB,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;YACnC,IAAI,CAAC;gBACH,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;oBACpB,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;wBACrC,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;4BAC5B,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;wBACvD,CAAC;oBACH,CAAC;gBACH,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,0BAA0B;YAC5B,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC5B,CAAC;AAED,SAAS,uBAAuB,CAAC,MAAoB,EAAE,QAAgB;IACrE,MAAM,UAAU,GAA4B;QAC1C,GAAG,MAAM;QACT,cAAc,EAAE,SAAS;QACzB,YAAY,EAAE,SAAS;KACxB,CAAC;IAEF,MAAM,WAAW,GAAG,MAAM,CAAC,cAAc,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC;IACzD,IAAI,WAAW,EAAE,CAAC;QAChB,UAAU,CAAC,WAAW,GAAG,WAAW,CAAC;QACrC,IAAI,WAAW,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC;YAC/D,UAAU,CAAC,gBAAgB,GAAG,WAAW,CAAC,UAAU;iBACjD,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,MAAM,CAAC,cAAc,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC;iBACvD,MAAM,CAAC,OAAO,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,CAAC,YAAY,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC;IACrD,IAAI,SAAS,EAAE,CAAC;QACd,UAAU,CAAC,SAAS,GAAG,SAAS,CAAC;IACnC,CAAC;IAED,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC;QAC1B,UAAU,CAAC,mBAAmB,GAAG,IAAI,CAAC;IACxC,CAAC;IACD,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;QACxB,UAAU,CAAC,iBAAiB,GAAG,IAAI,CAAC;IACtC,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,SAAS,iBAAiB,CACxB,IAAmC,EACnC,YAAoB,EACpB,OAAoB,IAAI,GAAG,EAAE,EAC7B,KAAK,GAAG,CAAC;IAET,IAAI,KAAK,GAAG,EAAE;QAAE,OAAO,CAAC,oCAAoC;IAC5D,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACtC,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;QAAE,OAAO;IAC9B,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAClB,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO;IACjC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACrB,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,IAAI,CAAC;QACH,OAAO,GAAG,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACzC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACvB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;IACT,CAAC;IACD,2FAA2F;IAC3F,6FAA6F;IAC7F,2FAA2F;IAC3F,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,4BAA4B,EAAE,EAAE,CAAC,CAAC;QACnE,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACpC,MAAM,QAAQ,GAAG,MAAM,EAAE,OAAO,CAAC;QACjC,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACxG,KAAK,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC;YAC9B,IAAI,OAAO,GAAG,KAAK,QAAQ;gBAAE,SAAS;YACtC,mGAAmG;YACnG,yGAAyG;YACzG,MAAM,SAAS,GAAG,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YACnF,IAAI,CAAC,SAAS;gBAAE,SAAS;YACzB,MAAM,OAAO,GAAG,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,SAAS,OAAO,CAAC;YAC9E,iBAAiB,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;QACpD,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,6EAA6E;IAC/E,CAAC;AACH,CAAC;AAED,SAAS,sBAAsB,CAC7B,IAAmC,EACnC,QAAgB,EAChB,WAAmB,EACnB,IAAiB,EACjB,KAAa,EACb,QAAQ,GAAG,CAAC;IAEZ,IAAI,KAAK,IAAI,QAAQ;QAAE,OAAO;IAE9B,KAAK,MAAM,SAAS,IAAI,+BAA+B,CAAC,WAAW,CAAC,EAAE,CAAC;QACrE,KAAK,MAAM,SAAS,IAAI,uBAAuB,CAAC,QAAQ,EAAE,SAAS,CAAC,EAAE,CAAC;YACrE,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC;gBAAE,SAAS;YAC5D,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YAEpB,IAAI,CAAC;gBACH,MAAM,eAAe,GAAG,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;gBACzD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;gBACvB,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;gBAC7B,sBAAsB,CAAC,IAAI,EAAE,SAAS,EAAE,eAAe,EAAE,IAAI,EAAE,KAAK,GAAG,CAAC,EAAE,QAAQ,CAAC,CAAC;gBACpF,MAAM;YACR,CAAC;YAAC,MAAM,CAAC;gBACP,6BAA6B;YAC/B,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,+BAA+B,CAAC,WAAmB;IAC1D,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,CAAC;QAC9D,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC;QAClC,IAAI,IAAI,EAAE,UAAU,CAAC,GAAG,CAAC;YAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC7C,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC;AACpB,CAAC;AAED,SAAS,uBAAuB,CAAC,QAAgB,EAAE,SAAiB;IAClE,MAAM,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAClC,MAAM,UAAU,GAAa,EAAE,CAAC;IAEhC,MAAM,YAAY,GAAG,CAAC,YAAoB,EAAE,EAAE;QAC5C,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC;IAClD,CAAC,CAAC;IAEF,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IACzF,IAAI,GAAG,EAAE,CAAC;QACR,YAAY,CAAC,SAAS,CAAC,CAAC;QACxB,KAAK,MAAM,QAAQ,IAAI,kBAAkB,CAAC,GAAG,CAAC,EAAE,CAAC;YAC/C,YAAY,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,QAAQ,EAAE,CAAC,CAAC;QAChE,CAAC;QACD,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,IAAI,iBAAiB,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QACtC,YAAY,CAAC,SAAS,CAAC,CAAC;QACxB,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,KAAK,MAAM,MAAM,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,CAAC,EAAE,CAAC;QAC5G,YAAY,CAAC,GAAG,SAAS,GAAG,MAAM,EAAE,CAAC,CAAC;IACxC,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,WAAW,EAAE,CAAC;AAE7C,MAAM,UAAU,gBAAgB;IAC9B,WAAW,CAAC,KAAK,EAAE,CAAC;AACtB,CAAC"}
@@ -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
- * - Aliased function variables: const f = foo; f()
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
  */
@@ -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
- * - Aliased function variables: const f = foo; f()
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
- // ── Import Resolution ───────────────────────────────────────────────────
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.getModuleSpecifierSourceFile();
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
- bindings.set(localName, `${resolvedPath}#${importedName}`);
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
- bindings.set(defaultImport.getText(), `${resolvedPath}#default`);
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(), `${resolvedPath}#*`);
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: exportName,
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?.endsWith('#*')) {
214
- const targetFile = nsBinding.slice(0, -2);
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: methodName,
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) {