ucn 4.2.2 → 4.2.3
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/README.md +23 -11
- package/core/build-worker.js +11 -2
- package/core/cache.js +21 -1
- package/core/callers.js +618 -43
- package/core/deadcode.js +25 -2
- package/core/entrypoints.js +9 -1
- package/core/output/analysis.js +4 -0
- package/core/project.js +13 -3
- package/core/search.js +149 -26
- package/languages/java.js +32 -3
- package/languages/javascript.js +257 -34
- package/languages/python.js +77 -10
- package/languages/rust.js +99 -1
- package/package.json +4 -4
package/core/deadcode.js
CHANGED
|
@@ -759,8 +759,22 @@ function deadcode(index, options = {}) {
|
|
|
759
759
|
} else {
|
|
760
760
|
const binding = (fileEntry.importBindings || [])
|
|
761
761
|
.find(b => b.name === receiver);
|
|
762
|
-
|
|
763
|
-
|
|
762
|
+
let resolved;
|
|
763
|
+
if (binding && fileEntry.moduleResolved) {
|
|
764
|
+
// `from . import types` binds the
|
|
765
|
+
// concrete `.types` submodule even
|
|
766
|
+
// though the import record's module is
|
|
767
|
+
// the package (`.`). Graph building
|
|
768
|
+
// records both resolutions. Prefer the
|
|
769
|
+
// composed submodule so qualified type
|
|
770
|
+
// and value references are attributed
|
|
771
|
+
// to the file that actually owns them.
|
|
772
|
+
const mod = String(binding.module);
|
|
773
|
+
const submodule = mod.endsWith('.')
|
|
774
|
+
? mod + receiver : mod + '.' + receiver;
|
|
775
|
+
resolved = fileEntry.moduleResolved[submodule] ||
|
|
776
|
+
fileEntry.moduleResolved[mod];
|
|
777
|
+
}
|
|
764
778
|
if (resolved) {
|
|
765
779
|
dottedScope = resolved;
|
|
766
780
|
} else if (!isDecoratorRef && !accessorNames.has(name)) {
|
|
@@ -853,6 +867,15 @@ function deadcode(index, options = {}) {
|
|
|
853
867
|
continue;
|
|
854
868
|
}
|
|
855
869
|
|
|
870
|
+
// Named function expressions are consumed by their expression
|
|
871
|
+
// position (argument / assigned value) — the enclosing call
|
|
872
|
+
// executes them, and the name itself creates no deletable
|
|
873
|
+
// surface (ECMA-262 body-only scope). Auditing them claims
|
|
874
|
+
// callback handlers dead (`app.on('ready', function boot(){})`).
|
|
875
|
+
if (symbol.bodyScopedName) {
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
|
|
856
879
|
const fileEntry = index.files.get(symbol.file);
|
|
857
880
|
const lang = fileEntry?.language;
|
|
858
881
|
|
package/core/entrypoints.js
CHANGED
|
@@ -644,7 +644,15 @@ function buildCallbackEntrypointMap(index) {
|
|
|
644
644
|
if (!enc || !enc.name || enc.name === '<anonymous>') continue;
|
|
645
645
|
const route = routeLines.get(enc.startLine);
|
|
646
646
|
if (!route || handledLines.has(enc.startLine)) continue;
|
|
647
|
-
|
|
647
|
+
// Real defs took pass 2 (they arrive as function
|
|
648
|
+
// REFERENCES on the route line). The inline expression's
|
|
649
|
+
// own indexed symbol (bodyScopedName, anchored at this
|
|
650
|
+
// registration line) is not a pass-2 shape — it IS the
|
|
651
|
+
// handler this pass exists to surface.
|
|
652
|
+
const encDefs = index.symbols.get(enc.name) || [];
|
|
653
|
+
const inlineDef = encDefs.find(d => d.bodyScopedName &&
|
|
654
|
+
d.file === filePath && d.startLine === enc.startLine);
|
|
655
|
+
if (encDefs.length > 0 && !inlineDef) continue;
|
|
648
656
|
if (!result.has(enc.name)) {
|
|
649
657
|
result.set(enc.name, {
|
|
650
658
|
framework: route.pattern.framework,
|
package/core/output/analysis.js
CHANGED
|
@@ -193,6 +193,8 @@ function formatContextJson(context) {
|
|
|
193
193
|
line: c.line,
|
|
194
194
|
expression: c.content,
|
|
195
195
|
callerName: c.callerName,
|
|
196
|
+
...(c.calledAs && { calledAs: c.calledAs }),
|
|
197
|
+
...(c.isFunctionReference && { functionReference: true }),
|
|
196
198
|
// Tier parity with the function-path callers list: class
|
|
197
199
|
// usages are the confirmed-tier answer for type symbols.
|
|
198
200
|
...(c.confidence !== undefined && { confidence: c.confidence }),
|
|
@@ -206,6 +208,8 @@ function formatContextJson(context) {
|
|
|
206
208
|
line: c.line,
|
|
207
209
|
expression: c.content,
|
|
208
210
|
callerName: c.callerName ?? null,
|
|
211
|
+
...(c.calledAs && { calledAs: c.calledAs }),
|
|
212
|
+
...(c.isFunctionReference && { functionReference: true }),
|
|
209
213
|
tier: 'unverified',
|
|
210
214
|
...(c.confidence !== undefined && { confidence: c.confidence }),
|
|
211
215
|
...(c.evidenceScore !== undefined && { evidenceScore: c.evidenceScore }),
|
package/core/project.js
CHANGED
|
@@ -505,7 +505,12 @@ class ProjectIndex {
|
|
|
505
505
|
// = require('./validation')` — the record's local alias
|
|
506
506
|
// pins to ITS module, not any module exporting the name.
|
|
507
507
|
const rn = (i.renames || []).find(r => r.original === n);
|
|
508
|
-
return {
|
|
508
|
+
return {
|
|
509
|
+
name: n,
|
|
510
|
+
module: i.module,
|
|
511
|
+
...(rn && { alias: rn.local }),
|
|
512
|
+
...(i.defaultLike && { defaultLike: true }),
|
|
513
|
+
};
|
|
509
514
|
})),
|
|
510
515
|
exports: exports.map(e => e.name),
|
|
511
516
|
exportDetails: exports,
|
|
@@ -548,12 +553,14 @@ class ProjectIndex {
|
|
|
548
553
|
...(item.implements && { implements: item.implements }),
|
|
549
554
|
...(item.indent !== undefined && { indent: item.indent }),
|
|
550
555
|
...(item.isNested && { isNested: item.isNested }),
|
|
556
|
+
...(item.enclosingType && { enclosingType: item.enclosingType }),
|
|
551
557
|
...(item.isMethod && { isMethod: item.isMethod }),
|
|
552
558
|
...(item.receiver && { receiver: item.receiver }),
|
|
553
559
|
...(item.className && { className: item.className }),
|
|
554
560
|
...(item.memberType && { memberType: item.memberType }),
|
|
555
561
|
...(item.fieldType && { fieldType: item.fieldType }),
|
|
556
562
|
...(item.aliasOf && { aliasOf: item.aliasOf }),
|
|
563
|
+
...(item.derefTarget && { derefTarget: item.derefTarget }),
|
|
557
564
|
...(item.decorators && item.decorators.length > 0 && { decorators: item.decorators }),
|
|
558
565
|
// Decorator/annotation/attribute argument capture for endpoints command:
|
|
559
566
|
// these fields hold the parsed first-string-arg of each route-style annotation.
|
|
@@ -570,6 +577,7 @@ class ProjectIndex {
|
|
|
570
577
|
...(item.traitName && { traitName: item.traitName }),
|
|
571
578
|
...(item.isSignature && { isSignature: true }),
|
|
572
579
|
...(item.memberAssigned && { memberAssigned: true }),
|
|
580
|
+
...(item.bodyScopedName && { bodyScopedName: true }),
|
|
573
581
|
...(item.registryMember && { registryMember: true }),
|
|
574
582
|
...(item.registryContainer && { registryContainer: item.registryContainer })
|
|
575
583
|
};
|
|
@@ -577,8 +585,10 @@ class ProjectIndex {
|
|
|
577
585
|
// Property-assignment defs (fix #269: Reply.prototype.serialize
|
|
578
586
|
// = function) declare no lexical name — a bare reference in the
|
|
579
587
|
// file can never bind them, so they never enter the bindings
|
|
580
|
-
// table (the symbol stays indexed and method-reachable).
|
|
581
|
-
|
|
588
|
+
// table (the symbol stays indexed and method-reachable). Named
|
|
589
|
+
// function expressions (bodyScopedName) bind only inside their
|
|
590
|
+
// own body (ECMA-262) — same consequence.
|
|
591
|
+
if (!item.memberAssigned && !item.bodyScopedName) {
|
|
582
592
|
fileEntry.bindings.push({
|
|
583
593
|
id: symbol.bindingId,
|
|
584
594
|
name: symbol.name,
|
package/core/search.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
10
|
const path = require('path');
|
|
11
|
-
const { escapeRegExp, codeUnitCompare, inlineTestRanges, lineInRanges, classDispatchNames } = require('./shared');
|
|
11
|
+
const { escapeRegExp, codeUnitCompare, inlineTestRanges, lineInRanges, classDispatchNames, CALLABLE_SYMBOL_KINDS } = require('./shared');
|
|
12
12
|
const { isTestFile } = require('./discovery');
|
|
13
13
|
const { detectLanguage, getParser, getLanguageModule, langTraits } = require('../languages');
|
|
14
14
|
const { getCachedCalls, _nameBindingReaches } = require('./callers');
|
|
@@ -220,15 +220,16 @@ function usages(index, name, options = {}) {
|
|
|
220
220
|
// Pre-compute: does any imported project file define this name?
|
|
221
221
|
// Used to filter namespace member expressions (e.g., DropdownMenuPrimitive.Separator)
|
|
222
222
|
// while keeping module access patterns (e.g., output.formatExample())
|
|
223
|
-
|
|
224
|
-
const importedFileHasDef = () => {
|
|
225
|
-
|
|
223
|
+
const _importedHasDef = new Map();
|
|
224
|
+
const importedFileHasDef = (receiver) => {
|
|
225
|
+
const cacheKey = receiver || '';
|
|
226
|
+
if (_importedHasDef.has(cacheKey)) return _importedHasDef.get(cacheKey);
|
|
226
227
|
const importedFiles = index.importGraph.get(filePath);
|
|
227
|
-
|
|
228
|
+
let found = false;
|
|
228
229
|
if (importedFiles) for (const imp of importedFiles) {
|
|
229
230
|
const impEntry = index.files.get(imp);
|
|
230
231
|
if (impEntry?.symbols?.some(s => s.name === name)) {
|
|
231
|
-
|
|
232
|
+
found = true;
|
|
232
233
|
break;
|
|
233
234
|
}
|
|
234
235
|
// A module namespace may expose the target through a
|
|
@@ -241,11 +242,72 @@ function usages(index, name, options = {}) {
|
|
|
241
242
|
// no is filtering evidence.
|
|
242
243
|
if (targetFiles.size > 0 &&
|
|
243
244
|
_nameBindingReaches(index, imp, name, targetFiles) !== 'no') {
|
|
244
|
-
|
|
245
|
+
found = true;
|
|
245
246
|
break;
|
|
246
247
|
}
|
|
247
248
|
}
|
|
248
|
-
|
|
249
|
+
|
|
250
|
+
// Named namespace surfaces need one more ownership axis:
|
|
251
|
+
// `import { util } from './core'; util.helper()` where
|
|
252
|
+
// core exports `* as util` from the helper's module. The
|
|
253
|
+
// imported file does not itself define `helper`; entering
|
|
254
|
+
// the namespace is what reaches the owning file.
|
|
255
|
+
if (!found && receiver && targetFiles.size > 0) {
|
|
256
|
+
const queue = [];
|
|
257
|
+
let unknown = false;
|
|
258
|
+
for (const b of (fileEntry.importBindings || [])) {
|
|
259
|
+
if (b.name !== receiver && b.alias !== receiver) continue;
|
|
260
|
+
const rel = fileEntry.moduleResolved?.[b.module];
|
|
261
|
+
if (rel) queue.push({ file: path.join(index.root, rel), attr: b.name, depth: 0 });
|
|
262
|
+
else if (String(b.module).startsWith('.')) unknown = true;
|
|
263
|
+
}
|
|
264
|
+
const seen = new Set();
|
|
265
|
+
while (!found && queue.length > 0) {
|
|
266
|
+
const cur = queue.shift();
|
|
267
|
+
const state = `${cur.file}\0${cur.attr || ''}`;
|
|
268
|
+
if (seen.has(state)) continue;
|
|
269
|
+
seen.add(state);
|
|
270
|
+
if (targetFiles.has(cur.file)) { found = true; break; }
|
|
271
|
+
if (cur.depth >= 4) { unknown = true; continue; }
|
|
272
|
+
const fe = index.files.get(cur.file);
|
|
273
|
+
if (!fe) { unknown = true; continue; }
|
|
274
|
+
for (const e of (fe.exportDetails || [])) {
|
|
275
|
+
if (!e.source) continue;
|
|
276
|
+
let nextAttr;
|
|
277
|
+
if (e.type === 're-export-all' && e.alias === cur.attr) {
|
|
278
|
+
nextAttr = null; // entered the namespace
|
|
279
|
+
} else if (e.type === 're-export-all' && !e.alias && cur.attr) {
|
|
280
|
+
nextAttr = cur.attr; // transparent barrel
|
|
281
|
+
} else if (e.type === 're-export' &&
|
|
282
|
+
(e.alias || e.name) === cur.attr) {
|
|
283
|
+
nextAttr = e.name;
|
|
284
|
+
} else {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const rel = fe.moduleResolved?.[e.source];
|
|
288
|
+
if (rel) {
|
|
289
|
+
queue.push({
|
|
290
|
+
file: path.join(index.root, rel),
|
|
291
|
+
attr: nextAttr,
|
|
292
|
+
depth: cur.depth + 1,
|
|
293
|
+
});
|
|
294
|
+
} else if (String(e.source).startsWith('.')) {
|
|
295
|
+
unknown = true;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// Dynamic/CJS export objects cannot prove that a
|
|
299
|
+
// namespace property is unrelated. Keep the usage
|
|
300
|
+
// visible rather than manufacturing a false miss.
|
|
301
|
+
if ((fe.exportDetails || []).some(e =>
|
|
302
|
+
e.type === 'exports' || e.type === 'module.exports') ||
|
|
303
|
+
(fe.moduleAssignedNames || []).includes(cur.attr)) {
|
|
304
|
+
unknown = true;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (!found && unknown) found = true;
|
|
308
|
+
}
|
|
309
|
+
_importedHasDef.set(cacheKey, found);
|
|
310
|
+
return found;
|
|
249
311
|
};
|
|
250
312
|
|
|
251
313
|
// A qualified usage whose RECEIVER is a symbol defined in this
|
|
@@ -264,6 +326,24 @@ function usages(index, name, options = {}) {
|
|
|
264
326
|
continue;
|
|
265
327
|
}
|
|
266
328
|
|
|
329
|
+
// Rust `Type::method` method values carry their owner from
|
|
330
|
+
// the AST. Keep them only for a matching method owner;
|
|
331
|
+
// class/struct queries must not absorb `Enum::Variant`
|
|
332
|
+
// references that happen to share the target type name.
|
|
333
|
+
// Owners are CALLABLE member defs only — an enum variant
|
|
334
|
+
// member also carries className, and counting it as an
|
|
335
|
+
// owner would let `Boundary::Grid` survive a struct-Grid
|
|
336
|
+
// query via its own enum. Lowercase receivers are module
|
|
337
|
+
// paths (`render::draw` reaches the free fn); `Self` is
|
|
338
|
+
// the enclosing type — both keep escape-hatch visibility.
|
|
339
|
+
if (fileEntry.language === 'rust' && u.scopedReference &&
|
|
340
|
+
u.receiver && /^[A-Z]/.test(u.receiver) && u.receiver !== 'Self') {
|
|
341
|
+
const methodOwners = new Set(definitions
|
|
342
|
+
.filter(d => d.className && CALLABLE_SYMBOL_KINDS.has(d.type))
|
|
343
|
+
.map(d => d.className));
|
|
344
|
+
if (!methodOwners.has(u.receiver)) continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
267
347
|
// Filter member expressions with unrelated receivers in JS/TS/Python.
|
|
268
348
|
// Keeps: standalone usages, self/this/cls/super, method calls on known types,
|
|
269
349
|
// qualified usages whose receiver this file defines,
|
|
@@ -272,7 +352,10 @@ function usages(index, name, options = {}) {
|
|
|
272
352
|
if (u.receiver && !['self', 'this', 'cls', 'super'].includes(u.receiver) &&
|
|
273
353
|
fileEntry.language !== 'go' && fileEntry.language !== 'java' && fileEntry.language !== 'rust') {
|
|
274
354
|
const hasMethodDef = definitions.some(d => d.className);
|
|
275
|
-
|
|
355
|
+
const sameFileMember = definitions.some(d =>
|
|
356
|
+
d.file === filePath && d.memberAssigned);
|
|
357
|
+
if (!hasMethodDef && !sameFileMember && !receiverDefinedHere(u.receiver) &&
|
|
358
|
+
!importedFileHasDef(u.receiver)) {
|
|
276
359
|
continue;
|
|
277
360
|
}
|
|
278
361
|
}
|
|
@@ -738,6 +821,10 @@ function structuralSearch(index, options = {}) {
|
|
|
738
821
|
|
|
739
822
|
// Unused filter (expensive — last check)
|
|
740
823
|
if (unused) {
|
|
824
|
+
// Named function expressions are consumed by their
|
|
825
|
+
// expression position — never "unused" (the deadcode
|
|
826
|
+
// twin of the bodyScopedName audit skip).
|
|
827
|
+
if (def.bodyScopedName) continue;
|
|
741
828
|
index.buildCalleeIndex();
|
|
742
829
|
// A name whose every call site is its own recursion
|
|
743
830
|
// has zero callers (fix #253c — the deadcode
|
|
@@ -873,7 +960,21 @@ function example(index, name, options = {}) {
|
|
|
873
960
|
.map(c => ({ ...c, evidenceTier: 'unverified' })),
|
|
874
961
|
...(rawCallers.unverifiedEntries || [])
|
|
875
962
|
.map(c => ({ ...c, evidenceTier: 'unverified' })),
|
|
876
|
-
].filter(c => !c.functionReference && c.calledAs !== 'bound');
|
|
963
|
+
].filter(c => !c.functionReference && !c.isFunctionReference && c.calledAs !== 'bound');
|
|
964
|
+
|
|
965
|
+
// `about`/`context` expose conserved call lines that no engine candidate
|
|
966
|
+
// claimed as `call-not-resolved`. `example` must preserve the same
|
|
967
|
+
// evidence contract: when those are the only candidate sites, abstain
|
|
968
|
+
// explicitly instead of returning a misleading "no examples" error.
|
|
969
|
+
// Compose the text-ground account only on this empty-candidate path so
|
|
970
|
+
// normal example selection does not pay for a second project-wide scan.
|
|
971
|
+
if (candidates.length === 0) {
|
|
972
|
+
const { composeAccount, callNotResolvedEntries } = require('./analysis');
|
|
973
|
+
const callerAccount = composeAccount(index, name, rawCallers);
|
|
974
|
+
candidates.push(...callNotResolvedEntries(index, callerAccount)
|
|
975
|
+
.filter(c => !c.functionReference && c.calledAs !== 'bound')
|
|
976
|
+
.map(c => ({ ...c, evidenceTier: 'unverified' })));
|
|
977
|
+
}
|
|
877
978
|
|
|
878
979
|
// Dedupe by site, preferring confirmed evidence when a parser shape
|
|
879
980
|
// reaches the same line through more than one resolution path.
|
|
@@ -1203,19 +1304,24 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1203
1304
|
|
|
1204
1305
|
// Fast pre-check: skip if searchTerm doesn't appear in file
|
|
1205
1306
|
if (!content.includes(searchTerm) && !linkedRecords) continue;
|
|
1206
|
-
|
|
1207
|
-
// class nor any non-overriding subclass (fix #246 — a test
|
|
1208
|
-
// holding only a Circle exercises Shape.describe).
|
|
1209
|
-
if (className && ![...dispatchNames].some(dn => content.includes(dn))) continue;
|
|
1210
|
-
|
|
1211
|
-
// --file scoping: only include test files that import from the target source
|
|
1212
|
-
if (sourceFileFilter && !sourceFileFilter.has(testPath)) {
|
|
1213
|
-
continue;
|
|
1214
|
-
}
|
|
1307
|
+
const sourceFileLinked = !sourceFileFilter || sourceFileFilter.has(testPath);
|
|
1215
1308
|
|
|
1216
1309
|
// AST-based usage detection
|
|
1217
1310
|
const astUsages = index._getCachedUsages(testPath, searchTerm) || [];
|
|
1218
1311
|
if (astUsages.length === 0 && !linkedRecords) continue;
|
|
1312
|
+
// Compiler attributes can invoke generated builder methods across
|
|
1313
|
+
// workspace/facade boundaries that the source import graph cannot
|
|
1314
|
+
// represent (`#[arg(value_delimiter = ',')]`). Keep these explicit
|
|
1315
|
+
// AST references as unverified instead of silently dropping a real
|
|
1316
|
+
// test. Ordinary calls/references still require source ownership.
|
|
1317
|
+
const hasAttributeReference = astUsages.some(u => u.inAttribute &&
|
|
1318
|
+
(!testRanges || lineInRanges(u.line, testRanges)));
|
|
1319
|
+
if (!sourceFileLinked && !hasAttributeReference) continue;
|
|
1320
|
+
// className scoping normally requires the class or a dispatching
|
|
1321
|
+
// descendant in the file. Generated attribute references are the
|
|
1322
|
+
// conservative exception: they carry an explicit unverified tier.
|
|
1323
|
+
if (className && ![...dispatchNames].some(dn => content.includes(dn)) &&
|
|
1324
|
+
!hasAttributeReference) continue;
|
|
1219
1325
|
|
|
1220
1326
|
// A test file that DEFINES searchTerm itself owns bare-name
|
|
1221
1327
|
// calls of it (fix #246 — the #244 affectedTests rule, tests()
|
|
@@ -1254,6 +1360,7 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1254
1360
|
|
|
1255
1361
|
for (const usage of astUsages) {
|
|
1256
1362
|
if (usage.usageType === 'definition') continue; // not relevant in test files
|
|
1363
|
+
if (!sourceFileLinked && !usage.inAttribute) continue;
|
|
1257
1364
|
// Inline-test-promoted file: only lines inside the test
|
|
1258
1365
|
// ranges are test code (fix #244).
|
|
1259
1366
|
if (testRanges && !lineInRanges(usage.line, testRanges)) continue;
|
|
@@ -1351,7 +1458,8 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1351
1458
|
// Language-aware test-case detection. Under a local same-name
|
|
1352
1459
|
// shadow the term on a test line is the file's OWN helper —
|
|
1353
1460
|
// only anchor test cases to matches that survived the shadow.
|
|
1354
|
-
if (
|
|
1461
|
+
if (sourceFileLinked &&
|
|
1462
|
+
(!localShadow || matches.some(m => m.matchType !== 'import'))) {
|
|
1355
1463
|
_addTestCaseMatches(index, testPath, entry, searchTerm, className, instanceTypeMap, matches);
|
|
1356
1464
|
}
|
|
1357
1465
|
|
|
@@ -1531,7 +1639,7 @@ function _buildSourceFileImporters(index, defs) {
|
|
|
1531
1639
|
// If so, add it to the queue so its importers are also discovered.
|
|
1532
1640
|
if (!visited.has(imp)) {
|
|
1533
1641
|
const fe = index.files.get(imp);
|
|
1534
|
-
if (fe && _fileReExportsSymbol(fe, symbolName, current)) {
|
|
1642
|
+
if (fe && _fileReExportsSymbol(index, fe, symbolName, current)) {
|
|
1535
1643
|
visited.add(imp);
|
|
1536
1644
|
queue.push(imp);
|
|
1537
1645
|
}
|
|
@@ -1636,13 +1744,28 @@ function _buildSourceFileImporters(index, defs) {
|
|
|
1636
1744
|
* Handles: named re-exports, `module.exports = require(...)` blanket re-exports,
|
|
1637
1745
|
* `export * from ...`, and files that both import from source and export the symbol.
|
|
1638
1746
|
*/
|
|
1639
|
-
function _fileReExportsSymbol(fileEntry, symbolName, sourceAbsPath) {
|
|
1640
|
-
|
|
1641
|
-
//
|
|
1642
|
-
|
|
1747
|
+
function _fileReExportsSymbol(index, fileEntry, symbolName, sourceAbsPath) {
|
|
1748
|
+
// Python module imports become module attributes. Package __init__.py
|
|
1749
|
+
// commonly exposes its public API through `from .core import Context as
|
|
1750
|
+
// Context`, which has no separate export statement in the AST.
|
|
1751
|
+
if (fileEntry.language === 'python' && symbolName) {
|
|
1752
|
+
for (const binding of (fileEntry.importBindings || [])) {
|
|
1753
|
+
if (binding.name !== symbolName && binding.alias !== symbolName) continue;
|
|
1754
|
+
const rel = fileEntry.moduleResolved?.[binding.module];
|
|
1755
|
+
if (rel && path.join(index.root, rel) === sourceAbsPath) return true;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
const exportNames = fileEntry.exports || [];
|
|
1760
|
+
const details = fileEntry.exportDetails || [];
|
|
1761
|
+
if (exportNames.length === 0 && details.length === 0) return false;
|
|
1762
|
+
// Check if any export matches the symbol name. fileEntry.exports is the
|
|
1763
|
+
// persisted string-name list; exportDetails carries source/type metadata.
|
|
1764
|
+
if (symbolName && (exportNames.includes(symbolName) ||
|
|
1765
|
+
details.some(exp => exp.name === symbolName || exp.alias === symbolName))) return true;
|
|
1643
1766
|
// Blanket re-exports: module.exports = require(...), export * from ...
|
|
1644
1767
|
// These have undefined or generic names but re-export everything from the imported module
|
|
1645
|
-
const hasBlanketExport =
|
|
1768
|
+
const hasBlanketExport = details.some(exp =>
|
|
1646
1769
|
!exp.name || exp.type === 'module.exports' || exp.type === 're-export' || exp.type === 'export-all'
|
|
1647
1770
|
);
|
|
1648
1771
|
if (hasBlanketExport) return true;
|
package/languages/java.js
CHANGED
|
@@ -328,6 +328,15 @@ function findFunctions(code, parser) {
|
|
|
328
328
|
* Returns true if node was matched, false otherwise
|
|
329
329
|
*/
|
|
330
330
|
function _processClass(node, classes, processedRanges, lines, code) {
|
|
331
|
+
const enclosingTypeName = current => {
|
|
332
|
+
for (let p = current.parent; p; p = p.parent) {
|
|
333
|
+
if (p.type === 'class_declaration' || p.type === 'interface_declaration' ||
|
|
334
|
+
p.type === 'enum_declaration' || p.type === 'record_declaration') {
|
|
335
|
+
return p.childForFieldName('name')?.text || null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return null;
|
|
339
|
+
};
|
|
331
340
|
// Class declarations
|
|
332
341
|
if (node.type === 'class_declaration') {
|
|
333
342
|
const rangeKey = `${node.startIndex}-${node.endIndex}`;
|
|
@@ -347,8 +356,9 @@ function _processClass(node, classes, processedRanges, lines, code) {
|
|
|
347
356
|
const implementsInfo = extractImplements(node);
|
|
348
357
|
|
|
349
358
|
// Check if this is a nested/inner class
|
|
350
|
-
|
|
359
|
+
const parentNode = node.parent;
|
|
351
360
|
const isNested = parentNode && parentNode.type === 'class_body';
|
|
361
|
+
const enclosingType = enclosingTypeName(node);
|
|
352
362
|
|
|
353
363
|
classes.push({
|
|
354
364
|
name: nameNode.text,
|
|
@@ -358,6 +368,7 @@ function _processClass(node, classes, processedRanges, lines, code) {
|
|
|
358
368
|
members,
|
|
359
369
|
modifiers,
|
|
360
370
|
...(isNested && { isNested: true }),
|
|
371
|
+
...(enclosingType && { enclosingType }),
|
|
361
372
|
...(docstring && { docstring }),
|
|
362
373
|
...(generics && { generics }),
|
|
363
374
|
...(annotations.length > 0 && { annotations }),
|
|
@@ -392,6 +403,7 @@ function _processClass(node, classes, processedRanges, lines, code) {
|
|
|
392
403
|
type: 'interface',
|
|
393
404
|
members: extractClassMembers(node, lines),
|
|
394
405
|
modifiers,
|
|
406
|
+
...(enclosingTypeName(node) && { enclosingType: enclosingTypeName(node) }),
|
|
395
407
|
...(docstring && { docstring }),
|
|
396
408
|
...(generics && { generics }),
|
|
397
409
|
...(annotations.length > 0 && { annotations }),
|
|
@@ -423,6 +435,7 @@ function _processClass(node, classes, processedRanges, lines, code) {
|
|
|
423
435
|
type: 'enum',
|
|
424
436
|
members: extractEnumConstants(node, lines),
|
|
425
437
|
modifiers,
|
|
438
|
+
...(enclosingTypeName(node) && { enclosingType: enclosingTypeName(node) }),
|
|
426
439
|
...(docstring && { docstring }),
|
|
427
440
|
...(annotations.length > 0 && { annotations }),
|
|
428
441
|
...(annotationsWithArgs.length > 0 && { annotationsWithArgs })
|
|
@@ -478,6 +491,7 @@ function _processClass(node, classes, processedRanges, lines, code) {
|
|
|
478
491
|
type: 'record',
|
|
479
492
|
members,
|
|
480
493
|
modifiers,
|
|
494
|
+
...(enclosingTypeName(node) && { enclosingType: enclosingTypeName(node) }),
|
|
481
495
|
...(docstring && { docstring }),
|
|
482
496
|
...(generics && { generics }),
|
|
483
497
|
...(annotations.length > 0 && { annotations }),
|
|
@@ -1150,8 +1164,22 @@ function findCallsInCode(code, parser) {
|
|
|
1150
1164
|
|
|
1151
1165
|
if (nameNode) {
|
|
1152
1166
|
const enclosingFunction = getCurrentEnclosingFunction();
|
|
1153
|
-
|
|
1154
|
-
|
|
1167
|
+
let receiverNode = objNode;
|
|
1168
|
+
while (receiverNode?.type === 'parenthesized_expression' && receiverNode.namedChildCount === 1) {
|
|
1169
|
+
receiverNode = receiverNode.namedChild(0);
|
|
1170
|
+
}
|
|
1171
|
+
let castReceiverType;
|
|
1172
|
+
let castReceiverName;
|
|
1173
|
+
if (receiverNode?.type === 'cast_expression') {
|
|
1174
|
+
castReceiverType = extractTypeName(receiverNode.childForFieldName('type'));
|
|
1175
|
+
const valueNode = receiverNode.childForFieldName('value');
|
|
1176
|
+
if (valueNode?.type === 'identifier') castReceiverName = valueNode.text;
|
|
1177
|
+
}
|
|
1178
|
+
const receiver = castReceiverName ||
|
|
1179
|
+
((receiverNode?.type === 'identifier' || receiverNode?.type === 'this')
|
|
1180
|
+
? receiverNode.text : undefined);
|
|
1181
|
+
const receiverType = castReceiverType ||
|
|
1182
|
+
((receiver && receiver !== 'this') ? getReceiverType(receiver) : undefined);
|
|
1155
1183
|
// fix #202: one-hop declared-field receivers —
|
|
1156
1184
|
// this.service.execute(), svc.client.run(), and bare
|
|
1157
1185
|
// service.execute() where service is a class field (only when
|
|
@@ -1215,6 +1243,7 @@ function findCallsInCode(code, parser) {
|
|
|
1215
1243
|
isMethod: !!objNode,
|
|
1216
1244
|
receiver,
|
|
1217
1245
|
...(receiverType && { receiverType }),
|
|
1246
|
+
...(castReceiverType && { receiverTypeCast: true }),
|
|
1218
1247
|
...(receiverFieldName && { receiverRoot, receiverField: receiverFieldName }),
|
|
1219
1248
|
...(receiverFieldName && receiverRootType && { receiverRootType }),
|
|
1220
1249
|
...(receiverCall && { receiverCall }),
|