Closes three gaps that broke `trace(controller, mapper-xml)` on real Spring +
MyBatis projects:
1. **Field-injected concrete-bean trace.** Java `this.<field>.method()` is
unwrapped at extraction (was surfaced as `this.<field>.method` and dropped
through every name-matcher strategy). The receiver name is then looked up
in the enclosing class's field declarations to get the declared type and
resolve the method on it. Closes the controller→bean hop when the field
name doesn't capitalize to the type (`userbo` → `UserBO`). General Java
fix, not Spring-specific.
2. **MyBatis XML mapper as a first-class language.** New extractor parses
`<mapper namespace="..."><select|insert|update|delete|sql id="X">` and
emits method-shaped nodes qualified as `<namespace>::<id>`, plus
`<include refid="X"/>` references to `<sql>` fragments. Non-mapper XML
(pom, log4j, web.xml) → file node only. A new synthesizer
(`mybatisJavaXmlEdges`) joins Java mapper methods to XML statements by
suffix-matching qualified names. Ambiguous simple-name collisions dropped
for precision.
3. **Spring `@Value`/`@ConfigurationProperties` → application config.**
`application.{yml,yaml,properties}` + profile variants parse on the
framework path; each leaf key becomes a `constant` node qualified by its
dotted path. `@Value("${k}")` / `@Value("${k:default}")` and
`@ConfigurationProperties(prefix="X")` emit binding nodes that resolve
with Spring's relaxed binding (kebab↔camel↔snake).
Validated on macrozheng/mall-tiny: full chain
`UmsRoleController.listResource → UmsRoleService.listResource → impl →
UmsResourceMapper.getResourceListByRoleId → XML <select>` connects across 5
hops via static + synthesized edges. 11/11 @Value annotations resolved
(incl. `@ConfigurationProperties(prefix="secure.ignored")`); 6/6 custom-SQL
mapper methods bridge to XML.
Tests: 4 new integration tests in frameworks-integration.test.ts. Full
suite: 1005 passed.
Docs: CHANGELOG `[Unreleased]` entry + dynamic-dispatch-coverage-playbook
narrative + matrix row.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
55839edd8f
commit
2543ae565a
@@ -262,6 +262,66 @@ function inferCppReceiverType(
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Java/Kotlin: infer a receiver's declared type by walking field declarations
|
||||
* in the class enclosing the call site. The field's `signature` is already in
|
||||
* the form "<TypeName> <fieldName>" (set by tree-sitter.ts extractField), so we
|
||||
* pull the type from there. Handles Spring `@Resource UserBO userbo;` /
|
||||
* `@Autowired private UserService userService;` where the receiver field name
|
||||
* doesn't match the class name by Java naming convention.
|
||||
*
|
||||
* Returns the bare type name (generics stripped, dotted package stripped) or
|
||||
* null when no matching field is in the enclosing class.
|
||||
*/
|
||||
function inferJavaFieldReceiverType(
|
||||
receiverName: string,
|
||||
ref: UnresolvedRef,
|
||||
context: ResolutionContext,
|
||||
): string | null {
|
||||
const inFile = context.getNodesInFile(ref.filePath);
|
||||
if (inFile.length === 0) return null;
|
||||
|
||||
// Find the class enclosing the call line (tightest match by latest start).
|
||||
let enclosing: Node | null = null;
|
||||
for (const n of inFile) {
|
||||
if (n.kind !== 'class' && n.kind !== 'interface') continue;
|
||||
if (n.language !== ref.language) continue;
|
||||
const end = n.endLine ?? n.startLine;
|
||||
if (n.startLine <= ref.line && end >= ref.line) {
|
||||
if (!enclosing || n.startLine >= enclosing.startLine) enclosing = n;
|
||||
}
|
||||
}
|
||||
if (!enclosing) return null;
|
||||
|
||||
const enclosingEnd = enclosing.endLine ?? enclosing.startLine;
|
||||
const field = inFile.find(
|
||||
(n) =>
|
||||
n.kind === 'field' &&
|
||||
n.name === receiverName &&
|
||||
n.language === ref.language &&
|
||||
n.startLine >= enclosing.startLine &&
|
||||
(n.endLine ?? n.startLine) <= enclosingEnd,
|
||||
);
|
||||
if (!field || !field.signature) return null;
|
||||
|
||||
// Signature shape: "<TypeName> <fieldName>" (extractField). Pull the type,
|
||||
// strip generics + dotted package, drop array/varargs markers.
|
||||
const beforeName = field.signature.slice(
|
||||
0,
|
||||
field.signature.lastIndexOf(field.name),
|
||||
);
|
||||
const typeRaw = beforeName.trim();
|
||||
if (!typeRaw) return null;
|
||||
|
||||
const typeNoGenerics = typeRaw.replace(/<[^>]*>/g, '').trim();
|
||||
const typeNoArray = typeNoGenerics.replace(/\[\s*\]/g, '').replace(/\.\.\.$/, '').trim();
|
||||
const parts = typeNoArray.split(/[.\s]+/).filter(Boolean);
|
||||
const lastPart = parts[parts.length - 1];
|
||||
if (!lastPart) return null;
|
||||
if (!/^[A-Z]/.test(lastPart)) return null; // primitives / lowercase → skip
|
||||
return lastPart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to resolve by method name on a class/object
|
||||
*/
|
||||
@@ -297,6 +357,28 @@ export function matchMethodCall(
|
||||
}
|
||||
}
|
||||
|
||||
// Java/Kotlin: receiver may be a field whose name doesn't match the type by
|
||||
// Java naming convention (`userbo` → class `UserBO`, abbreviated). Look up
|
||||
// the field in the enclosing class to get its declared type, then resolve
|
||||
// the method on that type. Covers Spring `@Resource`/`@Autowired` field
|
||||
// injection where the field type is the concrete bean class.
|
||||
if ((ref.language === 'java' || ref.language === 'kotlin') && dotMatch) {
|
||||
const inferredType = inferJavaFieldReceiverType(objectOrClass!, ref, context);
|
||||
if (inferredType) {
|
||||
const typedMatch = resolveMethodOnType(
|
||||
inferredType,
|
||||
methodName!,
|
||||
ref,
|
||||
context,
|
||||
0.9,
|
||||
'instance-method',
|
||||
);
|
||||
if (typedMatch) {
|
||||
return typedMatch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 1: Direct class name match (existing logic)
|
||||
const classCandidates = context.getNodesByName(objectOrClass!);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user