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.
@@ -671,7 +671,7 @@ function constructorTypeInfo(funcNode) {
671
671
  const attr = funcNode.childForFieldName('attribute');
672
672
  const object = funcNode.childForFieldName('object');
673
673
  return attr && /^[A-Z]/.test(attr.text)
674
- ? { type: attr.text, qualifier: object?.type === 'identifier' ? object.text : undefined }
674
+ ? { type: attr.text, qualifier: object?.text || undefined }
675
675
  : undefined;
676
676
  }
677
677
  return undefined;
@@ -685,6 +685,8 @@ function findCallsInCode(code, parser) {
685
685
  const nonCallableNames = new Set(); // Track names assigned non-callable values
686
686
  const localVarTypes = new Map(); // Track local variable types: varName -> typeName (for receiverType inference)
687
687
  const localVarTypeQualifiers = new Map(); // varName -> imported module alias that owns the inferred type
688
+ const constructedReceiverVars = new Set(); // exact constructor-result bindings
689
+ const withBindingVars = new Set(); // names produced by a context-manager as-target
688
690
  // Member-access aliases (fix #218): `append = output.append` makes a later
689
691
  // bare `append(part)` a METHOD call on `output` — it must carry the
690
692
  // receiver's evidence, never bind by bare name to a same-file def
@@ -697,6 +699,8 @@ function findCallsInCode(code, parser) {
697
699
  const moduleAliases = new Set(); // Names bound to MODULES (import httpx / import numpy as np)
698
700
  const localVarTypesStack = []; // Stack for function-scoped save/restore of localVarTypes
699
701
  const localVarTypeQualifiersStack = [];
702
+ const constructedReceiverVarsStack = [];
703
+ const withBindingVarsStack = [];
700
704
 
701
705
  // Helper: extract first string-arg literal from a call node.
702
706
  // Used by route extraction to capture path arg of requests.get('/users'), httpx.get('/users') etc.
@@ -848,6 +852,16 @@ function findCallsInCode(code, parser) {
848
852
  }
849
853
  }
850
854
  if (p.type === 'function_definition' || p.type === 'async_function_definition') {
855
+ const params = p.childForFieldName('parameters');
856
+ if (params) {
857
+ for (let i = 0; i < params.namedChildCount; i++) {
858
+ const prm = params.namedChild(i);
859
+ const prmName = prm.type === 'identifier'
860
+ ? prm
861
+ : (prm.childForFieldName('name') || prm.namedChild(0));
862
+ if (prmName?.type === 'identifier' && prmName.text === name) return true;
863
+ }
864
+ }
851
865
  const body = p.childForFieldName('body');
852
866
  return body ? _bindsNameInScope(body, name) : false;
853
867
  }
@@ -888,6 +902,8 @@ function findCallsInCode(code, parser) {
888
902
  // Save localVarTypes so inner declarations don't leak to sibling functions
889
903
  localVarTypesStack.push(new Map(localVarTypes));
890
904
  localVarTypeQualifiersStack.push(new Map(localVarTypeQualifiers));
905
+ constructedReceiverVarsStack.push(new Set(constructedReceiverVars));
906
+ withBindingVarsStack.push(new Set(withBindingVars));
891
907
  memberAliasesStack.push(new Map(memberAliases));
892
908
  }
893
909
 
@@ -912,11 +928,18 @@ function findCallsInCode(code, parser) {
912
928
  const ctx = value.namedChild(0);
913
929
  const target = value.namedChildCount > 1 ? value.namedChild(value.namedChildCount - 1) : null;
914
930
  const targetId = target?.type === 'as_pattern_target' ? target.namedChild(0) : null;
931
+ const recordWithTargets = n => {
932
+ if (!n) return;
933
+ if (n.type === 'identifier') withBindingVars.add(n.text);
934
+ for (let i = 0; i < n.namedChildCount; i++) recordWithTargets(n.namedChild(i));
935
+ };
936
+ recordWithTargets(targetId || target);
915
937
  if (ctx?.type === 'call' && targetId?.type === 'identifier') {
916
938
  const ctor = constructorTypeInfo(ctx.childForFieldName('function'));
917
939
  if (ctor) {
918
940
  localVarTypes.set(targetId.text, ctor.type);
919
- if (ctor.qualifier && moduleAliases.has(ctor.qualifier)) {
941
+ constructedReceiverVars.add(targetId.text);
942
+ if (ctor.qualifier && moduleAliases.has(ctor.qualifier.split('.')[0])) {
920
943
  localVarTypeQualifiers.set(targetId.text, ctor.qualifier);
921
944
  } else {
922
945
  localVarTypeQualifiers.delete(targetId.text);
@@ -940,6 +963,8 @@ function findCallsInCode(code, parser) {
940
963
  }
941
964
  }
942
965
  memberAliases.delete(left.text); // any assignment rebinds the name
966
+ constructedReceiverVars.delete(left.text);
967
+ withBindingVars.delete(left.text);
943
968
  // Rebinding without a known type makes any previously inferred
944
969
  // type stale — nearest-preceding-assignment semantics (#199's
945
970
  // documented rule). Without this, `x = ""; x = render(); x.m()`
@@ -1017,7 +1042,8 @@ function findCallsInCode(code, parser) {
1017
1042
  const ctor = constructorTypeInfo(right.childForFieldName('function'));
1018
1043
  if (ctor) {
1019
1044
  localVarTypes.set(left.text, ctor.type);
1020
- if (ctor.qualifier && moduleAliases.has(ctor.qualifier)) {
1045
+ constructedReceiverVars.add(left.text);
1046
+ if (ctor.qualifier && moduleAliases.has(ctor.qualifier.split('.')[0])) {
1021
1047
  localVarTypeQualifiers.set(left.text, ctor.qualifier);
1022
1048
  } else {
1023
1049
  localVarTypeQualifiers.delete(left.text);
@@ -1101,6 +1127,7 @@ function findCallsInCode(code, parser) {
1101
1127
  ...(argSpread && { argSpread: true }),
1102
1128
  enclosingFunction,
1103
1129
  uncertain,
1130
+ ...(isShadowedByLocal(funcNode, funcNode.text) && { localShadow: true }),
1104
1131
  ...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
1105
1132
  });
1106
1133
  }
@@ -1190,7 +1217,11 @@ function findCallsInCode(code, parser) {
1190
1217
  receiver,
1191
1218
  ...(receiverType && { receiverType }),
1192
1219
  ...(receiverTypeQualifier && { receiverTypeQualifier }),
1220
+ ...(receiver && constructedReceiverVars.has(receiver) && { receiverConstructed: true }),
1221
+ ...(receiver && withBindingVars.has(receiver) && { receiverWithBinding: true }),
1193
1222
  ...(receiverIsModule && { receiverIsModule: true }),
1223
+ ...(receiver && objNode?.type === 'identifier' &&
1224
+ isShadowedByLocal(objNode, receiver) && { receiverLocalBinding: true }),
1194
1225
  ...(selfAttribute && { selfAttribute }),
1195
1226
  ...(receiverCall && { receiverCall }),
1196
1227
  ...(receiverCallIsMethod && { receiverCallIsMethod: true }),
@@ -1273,6 +1304,12 @@ function findCallsInCode(code, parser) {
1273
1304
  localVarTypeQualifiers.clear();
1274
1305
  for (const [k, v] of savedQualifiers) localVarTypeQualifiers.set(k, v);
1275
1306
  }
1307
+ const savedConstructed = constructedReceiverVarsStack.pop();
1308
+ constructedReceiverVars.clear();
1309
+ if (savedConstructed) for (const name of savedConstructed) constructedReceiverVars.add(name);
1310
+ const savedWithBindings = withBindingVarsStack.pop();
1311
+ withBindingVars.clear();
1312
+ if (savedWithBindings) for (const name of savedWithBindings) withBindingVars.add(name);
1276
1313
  const savedAliases = memberAliasesStack.pop();
1277
1314
  if (savedAliases) {
1278
1315
  memberAliases.clear();
@@ -1304,13 +1341,33 @@ function findImportsInCode(code, parser) {
1304
1341
  for (let i = 0; i < node.namedChildCount; i++) {
1305
1342
  const child = node.namedChild(i);
1306
1343
  if (child.type === 'dotted_name') {
1307
- // import os
1308
- imports.push({
1309
- module: child.text,
1310
- names: [child.text.split('.').pop()],
1311
- type: 'import',
1312
- line
1313
- });
1344
+ // `import pkg.submodule` binds `pkg`, while also loading
1345
+ // the complete submodule. Record both ownership edges so
1346
+ // `pkg.public_api()` can resolve through pkg/__init__.py
1347
+ // and `pkg.submodule.member` can still resolve to the
1348
+ // imported child module.
1349
+ const parts = child.text.split('.');
1350
+ if (parts.length > 1) {
1351
+ imports.push({
1352
+ module: parts[0],
1353
+ names: [parts[0]],
1354
+ type: 'import',
1355
+ line
1356
+ });
1357
+ imports.push({
1358
+ module: child.text,
1359
+ names: [],
1360
+ type: 'import-submodule',
1361
+ line
1362
+ });
1363
+ } else {
1364
+ imports.push({
1365
+ module: child.text,
1366
+ names: [child.text],
1367
+ type: 'import',
1368
+ line
1369
+ });
1370
+ }
1314
1371
  } else if (child.type === 'aliased_import') {
1315
1372
  // import sys as system
1316
1373
  const nameNode = child.namedChild(0);
@@ -1537,6 +1594,16 @@ function findUsagesInCode(code, name, parser, tree) {
1537
1594
  usages.push({ line, column, usageType, receiver: object.text });
1538
1595
  return true;
1539
1596
  }
1597
+ // Constructed receiver: ColorTriplet(...).normalized is a
1598
+ // class-associated property reference, not an unowned bare
1599
+ // name. This matters to class-scoped `tests` queries.
1600
+ if (object && object.type === 'call') {
1601
+ const ctor = object.childForFieldName('function');
1602
+ if (ctor?.type === 'identifier') {
1603
+ usages.push({ line, column, usageType, receiver: ctor.text });
1604
+ return true;
1605
+ }
1606
+ }
1540
1607
  // self.attr receiver (unittest setUp idiom: self.w = Widget(3);
1541
1608
  // self.w.render()) — record the ATTR name so the instance-type
1542
1609
  // map built from the assignment line ('w' → Widget) matches
package/languages/rust.js CHANGED
@@ -432,6 +432,7 @@ function _processClass(node, types, processedRanges, lines, code) {
432
432
  const { startLine, endLine } = nodeToLocation(node, lines);
433
433
  const implInfo = extractImplInfo(node);
434
434
  const docstring = extractRustDocstring(lines, startLine);
435
+ const derefTarget = extractDerefTarget(node, implInfo.traitName);
435
436
 
436
437
  types.push({
437
438
  name: implInfo.name,
@@ -442,6 +443,7 @@ function _processClass(node, types, processedRanges, lines, code) {
442
443
  typeName: implInfo.typeName,
443
444
  members: extractImplMembers(node, lines, implInfo.typeName),
444
445
  modifiers: [],
446
+ ...(derefTarget && { derefTarget }),
445
447
  ...(docstring && { docstring })
446
448
  });
447
449
  return true; // matched
@@ -548,19 +550,41 @@ function _processClass(node, types, processedRanges, lines, code) {
548
550
  */
549
551
  function _postProcessTraitImpls(types) {
550
552
  const implTraits = new Map(); // typeName → [traitName, ...]
553
+ const derefTargets = new Map(); // typeName -> Set<Target>
551
554
  for (const t of types) {
552
555
  if (t.type === 'impl' && t.traitName && t.typeName) {
553
556
  if (!implTraits.has(t.typeName)) implTraits.set(t.typeName, []);
554
557
  implTraits.get(t.typeName).push(t.traitName);
558
+ if (t.derefTarget) {
559
+ if (!derefTargets.has(t.typeName)) derefTargets.set(t.typeName, new Set());
560
+ derefTargets.get(t.typeName).add(t.derefTarget);
561
+ }
555
562
  }
556
563
  }
557
564
  for (const t of types) {
558
565
  if ((t.type === 'struct' || t.type === 'enum') && implTraits.has(t.name)) {
559
566
  t.implements = implTraits.get(t.name);
567
+ const targets = derefTargets.get(t.name);
568
+ if (targets?.size === 1) t.derefTarget = [...targets][0];
560
569
  }
561
570
  }
562
571
  }
563
572
 
573
+ function extractDerefTarget(implNode, traitName) {
574
+ if (!traitName || !/(^|::)Deref(?:Mut)?$/.test(traitName)) return null;
575
+ let found = null;
576
+ const walk = node => {
577
+ if (found) return;
578
+ if (node.type === 'type_item' && node.childForFieldName('name')?.text === 'Target') {
579
+ found = aliasBaseTypeName(node.childForFieldName('type'));
580
+ return;
581
+ }
582
+ for (let i = 0; i < node.namedChildCount; i++) walk(node.namedChild(i));
583
+ };
584
+ walk(implNode);
585
+ return found;
586
+ }
587
+
564
588
  /**
565
589
  * Process a node for state object extraction (single-pass helper)
566
590
  * Returns true if node was matched, false otherwise
@@ -654,6 +678,28 @@ function extractStructFields(structNode, codeOrLines) {
654
678
  const bodyNode = structNode.childForFieldName('body');
655
679
  if (!bodyNode) return fields;
656
680
 
681
+ if (bodyNode.type === 'ordered_field_declaration_list') {
682
+ let position = 0;
683
+ for (let i = 0; i < bodyNode.namedChildCount; i++) {
684
+ const field = bodyNode.namedChild(i);
685
+ // Visibility modifiers and field attributes (`#[serde(..)] u32`)
686
+ // are separate children; the type node owns the tuple position.
687
+ // Numeric member names let the shared declared-field hop resolve
688
+ // `self.0.method()` exactly.
689
+ if (field.type === 'visibility_modifier' ||
690
+ field.type === 'attribute_item') continue;
691
+ const { startLine, endLine } = nodeToLocation(field, code);
692
+ fields.push({
693
+ name: String(position++),
694
+ startLine,
695
+ endLine,
696
+ memberType: 'field',
697
+ fieldType: field.text,
698
+ });
699
+ }
700
+ return fields;
701
+ }
702
+
657
703
  for (let i = 0; i < bodyNode.namedChildCount; i++) {
658
704
  const field = bodyNode.namedChild(i);
659
705
  if (field.type === 'field_declaration') {
@@ -1474,7 +1520,7 @@ function findCallsInCode(code, parser) {
1474
1520
  if (obj?.type === 'field_expression') {
1475
1521
  const rootNode = obj.childForFieldName('value');
1476
1522
  const fldNode = obj.childForFieldName('field');
1477
- if (fldNode?.type === 'field_identifier' && rootNode &&
1523
+ if ((fldNode?.type === 'field_identifier' || fldNode?.type === 'integer_literal') && rootNode &&
1478
1524
  (rootNode.type === 'identifier' || rootNode.type === 'self')) {
1479
1525
  receiverRoot = rootNode.text;
1480
1526
  receiverField = fldNode.text;
@@ -2139,6 +2185,30 @@ function _indexInParent(node, parent) {
2139
2185
  function findUsagesInCode(code, name, parser, tree) {
2140
2186
  tree = tree || parseTree(parser, code);
2141
2187
  const usages = [];
2188
+ // Lazy same-file enum→variants map: built only when a paren-less
2189
+ // `Type::name` reference needs the enum-variant check.
2190
+ let _enumVariants = null;
2191
+ const sameFileEnumVariant = (enumName, variantName) => {
2192
+ if (_enumVariants === null) {
2193
+ _enumVariants = new Map();
2194
+ traverseTreeCached(tree.rootNode, (n) => {
2195
+ if (n.type !== 'enum_item') return;
2196
+ const enName = n.childForFieldName('name')?.text;
2197
+ const body = n.childForFieldName('body');
2198
+ if (!enName || !body) return;
2199
+ let set = _enumVariants.get(enName);
2200
+ if (!set) { set = new Set(); _enumVariants.set(enName, set); }
2201
+ for (let i = 0; i < body.namedChildCount; i++) {
2202
+ const child = body.namedChild(i);
2203
+ if (child.type === 'enum_variant') {
2204
+ const vn = child.childForFieldName('name')?.text;
2205
+ if (vn) set.add(vn);
2206
+ }
2207
+ }
2208
+ });
2209
+ }
2210
+ return _enumVariants.get(enumName)?.has(variantName) || false;
2211
+ };
2142
2212
 
2143
2213
  visitNameNodes(tree, code, name, (node) => {
2144
2214
  // Look for identifier, field_identifier (method names in obj.method() calls),
@@ -2201,6 +2271,34 @@ function findUsagesInCode(code, name, parser, tree) {
2201
2271
  return true;
2202
2272
  }
2203
2273
  }
2274
+ } else if (sameNode(parent.childForFieldName('name'), node)) {
2275
+ // Associated method value: `Cursive::quit` is a reference
2276
+ // to the method even though no call_expression wraps it.
2277
+ // Preserve its type receiver so the project-aware usage
2278
+ // layer can distinguish it from `Enum::Variant`.
2279
+ const pathNode = parent.childForFieldName('path');
2280
+ if (pathNode) {
2281
+ const segs = pathNode.text.split('::');
2282
+ const receiver = segs[segs.length - 1];
2283
+ // A same-file `enum Receiver { Name }` proves this is
2284
+ // the variant, not an associated item of the queried
2285
+ // symbol — provable without the index, so filtered
2286
+ // here; cross-file receivers stay for the project
2287
+ // layer's owner check.
2288
+ if (receiver && sameFileEnumVariant(receiver, name)) {
2289
+ return true;
2290
+ }
2291
+ if (receiver) {
2292
+ usages.push({
2293
+ line,
2294
+ column,
2295
+ usageType: 'reference',
2296
+ receiver,
2297
+ scopedReference: true,
2298
+ });
2299
+ return true;
2300
+ }
2301
+ }
2204
2302
  }
2205
2303
  }
2206
2304
  // Turbofish call on a bare name: f::<T>() — the identifier's parent
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "4.2.2",
3
+ "version": "4.2.3",
4
4
  "mcpName": "io.github.mleoca/ucn",
5
5
  "description": "Code intelligence toolkit for AI agents: extract functions, trace call chains, find callers, and detect dead code without reading entire files. Works as MCP server, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java.",
6
6
  "main": "index.js",
@@ -18,9 +18,9 @@
18
18
  "eval:performance": "node --expose-gc eval/run-performance-gate.js",
19
19
  "lint": "eslint core/ cli/ mcp/ languages/ eval/run-*.js eval/*-gate-policy.js",
20
20
  "verify": "npm run lint && npm test",
21
- "trust:gate:semantic": "node eval/run-oracle-eval.js --repo preact-signals,httpx,cobra,clap,javapoet --min-precision 0.98 --max-unscored-ratio 0.10",
22
- "trust:gate:deadcode": "node eval/run-deadcode-eval.js --repo preact-signals,httpx,cobra,clap,javapoet --sample 100 --arm default",
23
- "trust:gate:performance": "node --expose-gc eval/run-performance-gate.js --repo preact-signals,httpx,cobra,clap,javapoet --queries 40",
21
+ "trust:gate:semantic": "node eval/run-oracle-eval.js --release --min-precision 0.98 --max-unscored-ratio 0.10",
22
+ "trust:gate:deadcode": "node eval/run-deadcode-eval.js --release --sample 100 --arm default",
23
+ "trust:gate:performance": "node --expose-gc eval/run-performance-gate.js --release --queries 40",
24
24
  "trust:gate:fast": "node eval/run-oracle-eval.js --repo preact-signals,httpx --min-precision 0.98 && node --expose-gc eval/run-performance-gate.js --repo preact-signals,httpx --queries 20",
25
25
  "trust:gate": "npm run trust:gate:semantic && npm run trust:gate:deadcode && npm run trust:gate:performance"
26
26
  },