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:
1 parent
55839edd8f
commit
2543ae565a
10 files changed
+1013
-10
No files matched your search
@@ -775,11 +775,77 @@ function fabricNativeImplEdges(ctx: ResolutionContext): Edge[] {
|
||||
return edges;
|
||||
}
|
||||
|
||||
/**
|
||||
* MyBatis: link a Java mapper interface method to the XML statement that holds
|
||||
* its SQL. The XML extractor (`src/extraction/mybatis-extractor.ts`) qualifies
|
||||
* each `<select|insert|update|delete|sql id="X">` as `<namespace>::<id>` where
|
||||
* `<namespace>` is the Java FQN of the mapper interface. A Java method's
|
||||
* qualifiedName ends with `<ClassName>::<methodName>`, so we suffix-match the
|
||||
* last two segments of the XML qualified name to find a unique Java method by
|
||||
* `<ClassName>::<methodName>` (`ClassName` = last dotted segment of the XML
|
||||
* namespace). Cross-mapper `<include refid="other.X">` references go through
|
||||
* the normal qualified-name resolver — only the Java↔XML bridge is synthetic.
|
||||
*
|
||||
* Precision over recall: ambiguous mappers (multiple Java classes with the
|
||||
* same simple name) are dropped. We need-not bridge by package because Java
|
||||
* mapper interfaces are typically uniquely named within a project.
|
||||
*/
|
||||
function mybatisJavaXmlEdges(queries: QueryBuilder): Edge[] {
|
||||
const edges: Edge[] = [];
|
||||
const seen = new Set<string>();
|
||||
// Index Java methods by `<ClassName>::<methodName>` for O(1) lookup.
|
||||
const javaIndex = new Map<string, Node[]>();
|
||||
for (const m of queries.getNodesByKind('method')) {
|
||||
if (m.language !== 'java' && m.language !== 'kotlin') continue;
|
||||
const parts = m.qualifiedName.split('::');
|
||||
const last = parts[parts.length - 1];
|
||||
const cls = parts[parts.length - 2];
|
||||
if (!last || !cls) continue;
|
||||
const key = `${cls}::${last}`;
|
||||
const arr = javaIndex.get(key);
|
||||
if (arr) arr.push(m); else javaIndex.set(key, [m]);
|
||||
}
|
||||
|
||||
for (const xml of queries.getNodesByKind('method')) {
|
||||
if (xml.language !== 'xml') continue;
|
||||
// Qualified name: `<namespace>::<id>`. Extract the simple class name.
|
||||
const colonIdx = xml.qualifiedName.lastIndexOf('::');
|
||||
if (colonIdx < 0) continue;
|
||||
const namespace = xml.qualifiedName.slice(0, colonIdx);
|
||||
const id = xml.qualifiedName.slice(colonIdx + 2);
|
||||
if (!namespace || !id) continue;
|
||||
const dotIdx = namespace.lastIndexOf('.');
|
||||
const className = dotIdx >= 0 ? namespace.slice(dotIdx + 1) : namespace;
|
||||
const candidates = javaIndex.get(`${className}::${id}`);
|
||||
if (!candidates || candidates.length === 0) continue;
|
||||
// Drop ambiguous matches (multiple same-name classes); the user can
|
||||
// disambiguate by adding the package-suffix match in a future enhancement.
|
||||
if (candidates.length > 1) continue;
|
||||
const java = candidates[0]!;
|
||||
const key = `${java.id}>${xml.id}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
edges.push({
|
||||
source: java.id,
|
||||
target: xml.id,
|
||||
kind: 'calls',
|
||||
line: java.startLine,
|
||||
provenance: 'heuristic',
|
||||
metadata: {
|
||||
synthesizedBy: 'mybatis-java-xml',
|
||||
via: `${className}.${id}`,
|
||||
registeredAt: `${xml.filePath}:${xml.startLine}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
return edges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synthesize dispatcher→callback edges (field observers + EventEmitters +
|
||||
* React re-render + JSX children + Vue templates + RN event channel +
|
||||
* Fabric native-impl). Returns the count added. Never throws into
|
||||
* indexing — callers wrap in try/catch.
|
||||
* Fabric native-impl + MyBatis Java↔XML). Returns the count added. Never
|
||||
* throws into indexing — callers wrap in try/catch.
|
||||
*/
|
||||
export function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionContext): number {
|
||||
const fieldEdges = fieldChannelEdges(queries, ctx);
|
||||
@@ -792,6 +858,7 @@ export function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionCo
|
||||
const ifaceEdges = interfaceOverrideEdges(queries);
|
||||
const rnEventEdgesList = rnEventEdges(ctx);
|
||||
const fabricNativeEdges = fabricNativeImplEdges(ctx);
|
||||
const mybatisEdges = mybatisJavaXmlEdges(queries);
|
||||
|
||||
const merged: Edge[] = [];
|
||||
const seen = new Set<string>();
|
||||
@@ -806,6 +873,7 @@ export function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionCo
|
||||
...ifaceEdges,
|
||||
...rnEventEdgesList,
|
||||
...fabricNativeEdges,
|
||||
...mybatisEdges,
|
||||
]) {
|
||||
const key = `${e.source}>${e.target}`;
|
||||
if (seen.has(key)) continue;
|
||||
|
||||
Reference in new issue
Block a user