ucn 3.8.26 → 4.0.0

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/languages/java.js CHANGED
@@ -10,7 +10,8 @@ const {
10
10
  traverseTreeCached,
11
11
  nodeToLocation,
12
12
  parseStructuredParams,
13
- extractJavaDocstring
13
+ extractJavaDocstring,
14
+ visitNameNodes,
14
15
  } = require('./utils');
15
16
  const { PARSE_OPTIONS, safeParse } = require('./index');
16
17
 
@@ -704,6 +705,29 @@ function extractClassMembers(classNode, codeOrLines) {
704
705
  // (one for class, one for constructor), forcing users to disambiguate.
705
706
  // Constructor signature info (params, line) remains accessible by reading
706
707
  // the class body when needed (e.g. via verify's AST walk).
708
+
709
+ // Field declarations: declared types drive receiver disambiguation
710
+ // (fix #202) — Rust/Go already emit field members with fieldType.
711
+ if (child.type === 'field_declaration') {
712
+ const typeNode = child.childForFieldName('type');
713
+ const fieldTypeText = typeNode ? typeNode.text : null;
714
+ for (let j = 0; j < child.namedChildCount; j++) {
715
+ const decl = child.namedChild(j);
716
+ if (decl.type === 'variable_declarator') {
717
+ const nameNode = decl.childForFieldName('name');
718
+ if (nameNode && fieldTypeText) {
719
+ const { startLine, endLine } = nodeToLocation(child, code);
720
+ members.push({
721
+ name: nameNode.text,
722
+ startLine,
723
+ endLine,
724
+ memberType: 'field',
725
+ fieldType: fieldTypeText
726
+ });
727
+ }
728
+ }
729
+ }
730
+ }
707
731
  }
708
732
 
709
733
  return members;
@@ -899,6 +923,135 @@ function findCallsInCode(code, parser) {
899
923
  return undefined;
900
924
  };
901
925
 
926
+ // Variable receiving this call's result (fix #207 return-type flow):
927
+ // `var x = find();` / `x = find();` → 'x'. Declared-type locals are
928
+ // already typed directly above — this covers `var` and reassignment,
929
+ // letting findCallers type x from the producer's declared return type.
930
+ const assignmentTargetOf = (callNode) => {
931
+ const p = callNode.parent;
932
+ if (p?.type === 'variable_declarator') {
933
+ const value = p.childForFieldName('value');
934
+ const nameNode = p.childForFieldName('name');
935
+ if (value && value.id === callNode.id && nameNode?.type === 'identifier') return nameNode.text;
936
+ }
937
+ if (p?.type === 'assignment_expression') {
938
+ const right = p.childForFieldName('right');
939
+ const left = p.childForFieldName('left');
940
+ if (right && right.id === callNode.id && left?.type === 'identifier') return left.text;
941
+ }
942
+ return undefined;
943
+ };
944
+
945
+ // All names declared anywhere in a function body (locals, for/catch/lambda
946
+ // params). Guard for fix #202: a bare identifier receiver is only treated
947
+ // as an implicit-this field when NO local of that name is declared —
948
+ // mistyping a shadowed local could wrongly exclude a true caller.
949
+ const scopeDeclared = new Map();
950
+ const collectDeclaredNames = (fnNode) => {
951
+ const declared = new Set();
952
+ const walk = (n) => {
953
+ for (let i = 0; i < n.namedChildCount; i++) {
954
+ const c = n.namedChild(i);
955
+ if (c.type === 'variable_declarator' ||
956
+ c.type === 'enhanced_for_statement' ||
957
+ c.type === 'catch_formal_parameter') {
958
+ const nn = c.childForFieldName('name');
959
+ if (nn) declared.add(nn.text);
960
+ } else if (c.type === 'lambda_expression') {
961
+ const params = c.childForFieldName('parameters');
962
+ if (params?.type === 'identifier') declared.add(params.text);
963
+ else if (params) {
964
+ for (let j = 0; j < params.namedChildCount; j++) {
965
+ const pc = params.namedChild(j);
966
+ if (pc.type === 'identifier') declared.add(pc.text);
967
+ else {
968
+ const pn = pc.childForFieldName('name');
969
+ if (pn) declared.add(pn.text);
970
+ }
971
+ }
972
+ }
973
+ }
974
+ walk(c);
975
+ }
976
+ };
977
+ walk(fnNode);
978
+ return declared;
979
+ };
980
+ const isDeclaredLocal = (varName) => {
981
+ for (let i = functionStack.length - 1; i >= 0; i--) {
982
+ const declared = scopeDeclared.get(functionStack[i].startLine);
983
+ if (declared?.has(varName)) return true;
984
+ }
985
+ return false;
986
+ };
987
+
988
+ // Nearest enclosing class/interface/enum/record name (for implicit-this fields)
989
+ const findEnclosingClassName = (n) => {
990
+ for (let p = n.parent; p; p = p.parent) {
991
+ if (p.type === 'class_declaration' || p.type === 'interface_declaration' ||
992
+ p.type === 'enum_declaration' || p.type === 'record_declaration') {
993
+ return p.childForFieldName('name')?.text;
994
+ }
995
+ }
996
+ return undefined;
997
+ };
998
+
999
+ // Call-site argument shape: count + per-arg static kind. Kinds feed the
1000
+ // overload discipline in findCallers (Java is the only supported language
1001
+ // with arity/type overloading): literal kinds can prove a call binds a
1002
+ // DIFFERENT same-class overload than the pinned one. Unknown args are
1003
+ // 'expr' — never evidence.
1004
+ const bareTypeName = (text) => {
1005
+ let t = text;
1006
+ const g = t.indexOf('<');
1007
+ if (g > 0) t = t.substring(0, g);
1008
+ const d = t.lastIndexOf('.');
1009
+ if (d >= 0) t = t.substring(d + 1);
1010
+ return t.trim();
1011
+ };
1012
+ const argKindOf = (arg) => {
1013
+ switch (arg.type) {
1014
+ case 'string_literal': return 'string';
1015
+ case 'character_literal': return 'char';
1016
+ case 'decimal_integer_literal':
1017
+ case 'hex_integer_literal':
1018
+ case 'octal_integer_literal':
1019
+ case 'binary_integer_literal':
1020
+ return /[lL]$/.test(arg.text) ? 'long' : 'int';
1021
+ case 'decimal_floating_point_literal':
1022
+ case 'hex_floating_point_literal':
1023
+ return /[fF]$/.test(arg.text) ? 'float' : 'double';
1024
+ case 'true':
1025
+ case 'false': return 'boolean';
1026
+ case 'null_literal': return 'null';
1027
+ case 'object_creation_expression': {
1028
+ const tn = arg.childForFieldName('type');
1029
+ return tn ? `new:${bareTypeName(tn.text)}` : 'expr';
1030
+ }
1031
+ case 'cast_expression': {
1032
+ const tn = arg.childForFieldName('type');
1033
+ return tn ? `cast:${bareTypeName(tn.text)}` : 'expr';
1034
+ }
1035
+ case 'lambda_expression':
1036
+ case 'method_reference': return 'lambda';
1037
+ case 'unary_expression':
1038
+ // -1, -2.5 — numeric literal kinds survive negation
1039
+ return arg.namedChildCount === 1 ? argKindOf(arg.namedChild(0)) : 'expr';
1040
+ default: return 'expr';
1041
+ }
1042
+ };
1043
+ const getCallArgs = (callNode) => {
1044
+ const argsNode = callNode.childForFieldName('arguments');
1045
+ if (!argsNode) return { argCount: 0, argKinds: null };
1046
+ const kinds = [];
1047
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
1048
+ const arg = argsNode.namedChild(i);
1049
+ if (arg.type === 'comment') continue;
1050
+ kinds.push(argKindOf(arg));
1051
+ }
1052
+ return { argCount: kinds.length, argKinds: kinds.some(k => k !== 'expr') ? kinds : null };
1053
+ };
1054
+
902
1055
  traverseTree(tree.rootNode, (node) => {
903
1056
  // Track function entry
904
1057
  if (isFunctionNode(node)) {
@@ -909,6 +1062,7 @@ function findCallsInCode(code, parser) {
909
1062
  };
910
1063
  functionStack.push(entry);
911
1064
  scopeTypes.set(entry.startLine, buildScopeTypeMap(node));
1065
+ scopeDeclared.set(entry.startLine, collectDeclaredNames(node));
912
1066
  }
913
1067
 
914
1068
  // Handle method invocations: foo(), obj.foo(), this.foo()
@@ -920,13 +1074,68 @@ function findCallsInCode(code, parser) {
920
1074
  const enclosingFunction = getCurrentEnclosingFunction();
921
1075
  const receiver = (objNode?.type === 'identifier' || objNode?.type === 'this') ? objNode.text : undefined;
922
1076
  const receiverType = (receiver && receiver !== 'this') ? getReceiverType(receiver) : undefined;
1077
+ // fix #202: one-hop declared-field receivers —
1078
+ // this.service.execute(), svc.client.run(), and bare
1079
+ // service.execute() where service is a class field (only when
1080
+ // no same-named local is declared anywhere in the method).
1081
+ let receiverRoot, receiverFieldName, receiverRootType;
1082
+ if (objNode && !receiverType) {
1083
+ if (objNode.type === 'field_access') {
1084
+ const rootNode = objNode.childForFieldName('object');
1085
+ const fldNode = objNode.childForFieldName('field');
1086
+ if (fldNode?.type === 'identifier' && rootNode) {
1087
+ if (rootNode.type === 'this') {
1088
+ receiverRoot = 'this';
1089
+ receiverFieldName = fldNode.text;
1090
+ receiverRootType = findEnclosingClassName(node);
1091
+ } else if (rootNode.type === 'identifier') {
1092
+ const rootType = getReceiverType(rootNode.text);
1093
+ if (rootType) {
1094
+ receiverRoot = rootNode.text;
1095
+ receiverFieldName = fldNode.text;
1096
+ receiverRootType = rootType;
1097
+ }
1098
+ }
1099
+ }
1100
+ } else if (objNode.type === 'identifier' && receiver &&
1101
+ !isDeclaredLocal(receiver)) {
1102
+ // Implicit-this field (or a class name — the field-type
1103
+ // hop in findCallers simply finds no field and no-ops).
1104
+ receiverRoot = 'this';
1105
+ receiverFieldName = receiver;
1106
+ receiverRootType = findEnclosingClassName(node);
1107
+ }
1108
+ }
1109
+ // Chained receiver (fix #220): the receiver IS a call —
1110
+ // getConfig().validate() — record the producer so findCallers
1111
+ // can type it from the declared return annotation.
1112
+ let receiverCall, receiverCallIsMethod;
1113
+ if (!receiver && !receiverFieldName && objNode?.type === 'method_invocation') {
1114
+ const prodName = objNode.childForFieldName('name');
1115
+ if (prodName) {
1116
+ receiverCall = prodName.text;
1117
+ if (objNode.childForFieldName('object')) receiverCallIsMethod = true;
1118
+ }
1119
+ }
923
1120
  const firstArg = getFirstStringArg(node);
1121
+ const callArgs = getCallArgs(node);
1122
+ const assignedTo = assignmentTargetOf(node);
924
1123
  calls.push({
925
1124
  name: nameNode.text,
926
- line: node.startPosition.row + 1,
1125
+ // Multi-line chains (builder.x()\n.y()) must report each
1126
+ // method's OWN name line, not the chain-start line — the
1127
+ // account's ground set is keyed by the name's line
1128
+ line: nameNode.startPosition.row + 1,
927
1129
  isMethod: !!objNode,
928
1130
  receiver,
929
1131
  ...(receiverType && { receiverType }),
1132
+ ...(receiverFieldName && { receiverRoot, receiverField: receiverFieldName }),
1133
+ ...(receiverFieldName && receiverRootType && { receiverRootType }),
1134
+ ...(receiverCall && { receiverCall }),
1135
+ ...(receiverCallIsMethod && { receiverCallIsMethod: true }),
1136
+ argCount: callArgs.argCount,
1137
+ ...(callArgs.argKinds && { argKinds: callArgs.argKinds }),
1138
+ ...(assignedTo && { assignedTo }),
930
1139
  enclosingFunction,
931
1140
  ...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
932
1141
  });
@@ -944,18 +1153,27 @@ function findCallsInCode(code, parser) {
944
1153
  if (genericIdx > 0) {
945
1154
  typeName = typeName.substring(0, genericIdx);
946
1155
  }
947
- // Handle qualified names like pkg.Class
1156
+ // Handle qualified names like pkg.Class — keep the qualifier
1157
+ // as receiver (fix #206): a qualified type must not resolve to
1158
+ // a same-file binding of an unrelated same-name symbol.
1159
+ let typeQualifier = null;
948
1160
  const dotIdx = typeName.lastIndexOf('.');
949
1161
  if (dotIdx > 0) {
1162
+ const qualParts = typeName.substring(0, dotIdx).split('.');
1163
+ typeQualifier = qualParts[qualParts.length - 1] || null;
950
1164
  typeName = typeName.substring(dotIdx + 1);
951
1165
  }
952
1166
 
953
1167
  const enclosingFunction = getCurrentEnclosingFunction();
1168
+ const ctorArgs = getCallArgs(node);
954
1169
  calls.push({
955
1170
  name: typeName,
956
1171
  line: node.startPosition.row + 1,
957
1172
  isMethod: false,
958
1173
  isConstructor: true,
1174
+ ...(typeQualifier && { receiver: typeQualifier }),
1175
+ argCount: ctorArgs.argCount,
1176
+ ...(ctorArgs.argKinds && { argKinds: ctorArgs.argKinds }),
959
1177
  enclosingFunction
960
1178
  });
961
1179
  }
@@ -984,22 +1202,29 @@ function findCallsInCode(code, parser) {
984
1202
  return true;
985
1203
  }
986
1204
 
987
- // Track local variable types from new Type() assignments
988
- // e.g., Foo f = new Foo(); or var f = new Foo();
1205
+ // Track local variable types from declarations (fix #207 extends #202-era
1206
+ // new-Type() inference): the DECLARED type is compiler-checked evidence —
1207
+ // `Service s = lookup();` types s as Service regardless of the value
1208
+ // expression. `var` declarations fall back to new Type() value inference.
989
1209
  if (node.type === 'local_variable_declaration' && functionStack.length > 0) {
1210
+ const declTypeNode = node.childForFieldName('type');
1211
+ const declaredType = declTypeNode && declTypeNode.text !== 'var'
1212
+ ? extractTypeName(declTypeNode) : null;
990
1213
  for (let i = 0; i < node.namedChildCount; i++) {
991
1214
  const child = node.namedChild(i);
992
1215
  if (child.type === 'variable_declarator') {
993
1216
  const nameNode = child.childForFieldName('name');
994
1217
  const valueNode = child.childForFieldName('value');
995
- if (nameNode && valueNode && valueNode.type === 'object_creation_expression') {
996
- const typeNode = valueNode.childForFieldName('type');
997
- const typeName = extractTypeName(typeNode);
998
- if (typeName) {
999
- const scopeKey = functionStack[functionStack.length - 1].startLine;
1000
- const typeMap = scopeTypes.get(scopeKey);
1001
- if (typeMap) typeMap.set(nameNode.text, typeName);
1002
- }
1218
+ // new Type() is the DYNAMIC type more precise than the
1219
+ // declared static type (Foo f = new Bar() dispatches to Bar)
1220
+ let typeName = valueNode?.type === 'object_creation_expression'
1221
+ ? extractTypeName(valueNode.childForFieldName('type'))
1222
+ : null;
1223
+ if (!typeName) typeName = declaredType;
1224
+ if (nameNode && typeName) {
1225
+ const scopeKey = functionStack[functionStack.length - 1].startLine;
1226
+ const typeMap = scopeTypes.get(scopeKey);
1227
+ if (typeMap) typeMap.set(nameNode.text, typeName);
1003
1228
  }
1004
1229
  }
1005
1230
  }
@@ -1156,7 +1381,7 @@ function findUsagesInCode(code, name, parser) {
1156
1381
  const tree = parseTree(parser, code);
1157
1382
  const usages = [];
1158
1383
 
1159
- traverseTreeCached(tree.rootNode, (node) => {
1384
+ visitNameNodes(tree, code, name, (node) => {
1160
1385
  // Look for identifiers and type_identifiers with the matching name
1161
1386
  // type_identifier is used in Java for type references: new ClassName(), extends ClassName, field types
1162
1387
  if ((node.type !== 'identifier' && node.type !== 'type_identifier') || node.text !== name) {
@@ -1243,10 +1468,12 @@ function findUsagesInCode(code, name, parser) {
1243
1468
  usageType = 'call';
1244
1469
  }
1245
1470
  }
1246
- // Static method call: ClassName.staticMethod() — ClassName is the object
1471
+ // Object position of a method call: x.method() — x is a receiver
1472
+ // (variable or ClassName), referenced, not called. The call belongs
1473
+ // to the name field, handled above.
1247
1474
  else if (parent.type === 'method_invocation' &&
1248
1475
  parent.childForFieldName('object') === node) {
1249
- usageType = 'call';
1476
+ usageType = 'reference';
1250
1477
  }
1251
1478
  // Field access: obj.field
1252
1479
  else if (parent.type === 'field_access' &&