ucn 4.2.1 → 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/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
- const resolved = binding && fileEntry.moduleResolved &&
763
- fileEntry.moduleResolved[binding.module];
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
 
@@ -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
- if (index.symbols.has(enc.name)) continue; // real defs took pass 2
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,
@@ -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 { name: n, module: i.module, ...(rn && { alias: rn.local }) };
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
- if (!item.memberAssigned) {
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
- let _importedHasDef = null;
224
- const importedFileHasDef = () => {
225
- if (_importedHasDef !== null) return _importedHasDef;
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
- _importedHasDef = false;
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
- _importedHasDef = true;
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
- _importedHasDef = true;
245
+ found = true;
245
246
  break;
246
247
  }
247
248
  }
248
- return _importedHasDef;
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
- if (!hasMethodDef && !receiverDefinedHere(u.receiver) && !importedFileHasDef()) {
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
- // className scoping: skip test files that reference neither the
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 (!localShadow || matches.some(m => m.matchType !== 'import')) {
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
- if (!fileEntry.exports || fileEntry.exports.length === 0) return false;
1641
- // Check if any export matches the symbol name
1642
- if (symbolName && fileEntry.exports.some(exp => exp.name === symbolName)) return true;
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 = fileEntry.exports.some(exp =>
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
- let parentNode = node.parent;
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
- const receiver = (objNode?.type === 'identifier' || objNode?.type === 'this') ? objNode.text : undefined;
1154
- const receiverType = (receiver && receiver !== 'this') ? getReceiverType(receiver) : undefined;
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 }),