Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Ruby **lifecycle-hook symbols** now register: `before_action :authenticate`, `after_save :reindex`, `around_create`, `validate :check`, `rescue_from(…, with: :handler)` and friends link the symbol to the method it names — on the class itself or **inherited from a parent** (`before_action :authenticate` in a controller resolves to `ApplicationController`'s method). `validates` (plural) is excluded since its symbols name attributes, not methods. Validated on rails/rails (+385 edges, every sampled edge genuine). (Ruby)
- Method references to a type that needed **no import** now resolve: Java/Kotlin same-package references (`.concatMapMaybe(Maybe::just, …)`), **Kotlin companion-object members** (`KtHandlers::handle`), and cross-file C++ member pointers (`&TestSuite::RunSetUpTestSuite`). Resolution stays anchored to the named type, so a same-named member on a different class never matches. (Java, Kotlin, C++)
- CodeGraph now sees where a function is **registered as a callback**, not just where it's called. A function name passed as an argument (`signal(SIGINT, handler)`, `qsort(…, compare)`, `addEventListener(…, onBlur)`), assigned to a function pointer or field (`ops->recv_cb = my_cb`, `OnClick := Handler`), or placed in a struct initializer or handler table (`{ .recv_cb = my_cb }`, `{ "get", getCommand }`) now produces a reference edge from the registration site to the function — so `codegraph_callers` and `codegraph_impact` surface callback wiring that previously looked like dead code. Works across all supported languages, including the language-specific forms: C/C++ `&fn`, Java `Class::method`, Kotlin `::fn`, Swift `#selector`, Objective-C `@selector`, Ruby `method(:fn)`, Scala eta-expansion, and Delphi/Pascal `@Handler` and `OnClick := Handler` event wiring. Callers output labels these "via callback registration". Resolution is deliberately conservative: an ambiguous name produces no edge rather than a wrong one. Re-index a project to benefit. Thanks @zmcrazy. (#756)
- MyBatis `<resultMap>` elements are now indexed, and statements that declare `resultMap="..."` link to them — so you can ask which statements use a given result map (callers / impact), not just which use a `<sql>` fragment. (#592)
- The `codegraph_node` MCP tool can now **read a whole source file like the built-in Read tool — only faster, served from the index**. Pass a file path with no symbol and it returns that file's current source with line numbers (the same `<n>⇥<line>` shape Read produces, so an assistant can edit straight from it), narrowable with `offset`/`limit` exactly like Read, plus a one-line note of which files depend on it (the file's blast radius). Use it anywhere you'd reach for Read on an indexed source file. Pass `symbolsOnly: true` for just the file's structure. Configuration/data files (`.yml` / `.properties`) are summarized by key only, never dumped, so secrets in them are never surfaced. The agent-facing guidance was also retuned so assistants reach for codegraph while *implementing* a change (not only when answering questions), since one codegraph call returns the same bytes plus the blast radius, faster than re-reading the file.
- New `codegraph upgrade` command updates CodeGraph to the latest release in place — it detects how you installed (the standalone `install.sh` / `install.ps1` bundle, npm, or npx) and does the right thing for each, on macOS, Linux, and Windows. Use `codegraph upgrade --check` to see whether an update is available without installing, or `codegraph upgrade <version>` to move to a specific version. After upgrading it reminds you to re-index your projects so they pick up the newer engine's improvements. (#679)
- `codegraph status` now flags when a project's index was built by an older engine than the one you're running and recommends re-indexing (also surfaced in `codegraph status --json`), so you know when a `codegraph index -f` or `codegraph sync` will add coverage a newer release introduced.
Expand Down
71 changes: 71 additions & 0 deletions __tests__/frameworks-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,77 @@ describe('Java end-to-end — field-injected bean trace (issue #389)', () => {
cg.close();
});

it('indexes MyBatis <resultMap> and links statements that reference it (#592)', async () => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-mybatis-rm-'));
const javaDir = path.join(tmpDir, 'src/main/java/com/example/dao');
const xmlDir = path.join(tmpDir, 'src/main/resources/mappers');
fs.mkdirSync(javaDir, { recursive: true });
fs.mkdirSync(xmlDir, { recursive: true });
fs.writeFileSync(
path.join(tmpDir, 'pom.xml'),
'<project><dependencies><dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId></dependency></dependencies></project>\n'
);
// Java method `userResult` deliberately collides with the resultMap id,
// to prove the Java↔XML bridge does NOT wrongly link to the result map.
fs.writeFileSync(
path.join(javaDir, 'UserMapper.java'),
'package com.example.dao;\n' +
'public interface UserMapper {\n' +
' Object findById(int id);\n' +
' Object userResult();\n' +
'}\n'
);
fs.writeFileSync(
path.join(xmlDir, 'UserMapper.xml'),
'<?xml version="1.0" encoding="UTF-8"?>\n' +
'<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">\n' +
'<mapper namespace="com.example.dao.UserMapper">\n' +
' <resultMap id="userResult" type="User">\n' +
' <id column="id" property="id"/>\n' +
' <result column="email" property="email"/>\n' +
' </resultMap>\n' +
' <select id="findById" resultMap="userResult">\n' +
' SELECT id, email FROM users WHERE id = #{id}\n' +
' </select>\n' +
'</mapper>\n'
);

const cg = CodeGraph.initSync(tmpDir);
await cg.indexAll();

const methods = cg.getNodesByKind('method');
const resultMap = methods.find((n) => n.name === 'userResult' && n.language === 'xml');
const findById = methods.find((n) => n.name === 'findById' && n.language === 'xml');
expect(resultMap, '<resultMap id="userResult"> should be indexed').toBeDefined();
expect(resultMap!.qualifiedName).toBe('com.example.dao.UserMapper::userResult');
expect(resultMap!.signature).toBe('<resultMap> type=User');
expect(findById).toBeDefined();

// <select resultMap="userResult"> -> <resultMap id="userResult"> edge.
const rmEdge = cg.getOutgoingEdges(findById!.id).find((e) => e.target === resultMap!.id);
expect(rmEdge, 'statement should reference the result map it uses').toBeDefined();

// The guard must not over-match: the real <select id="findById"> still
// bridges to its Java mapper method.
const javaFindById = methods.find((n) => n.name === 'findById' && n.language === 'java');
expect(javaFindById).toBeDefined();
const realBridge = cg
.getOutgoingEdges(javaFindById!.id)
.find((e) => e.target === findById!.id);
expect(realBridge, 'Java findById should still bridge to its XML <select>').toBeDefined();

// The Java↔XML bridge must NOT link the same-named Java method to the
// result map (result maps are not mapper methods).
const javaUserResult = methods.find((n) => n.name === 'userResult' && n.language === 'java');
expect(javaUserResult).toBeDefined();
const bogusBridge = cg
.getOutgoingEdges(javaUserResult!.id)
.find((e) => e.target === resultMap!.id);
expect(bogusBridge, 'Java method must not bridge to a result map').toBeUndefined();

cg.close();
});

it('binds @Value / @ConfigurationProperties to YAML + .properties keys (incl. relaxed binding)', async () => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-spring-config-'));
const javaDir = path.join(tmpDir, 'src/main/java/com/example');
Expand Down
28 changes: 27 additions & 1 deletion src/extraction/mybatis-extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export class MyBatisExtractor {
// tags (`<if>`, `<foreach>`, `<include>`), so we scan with a regex that
// pairs an opening tag to its matching close — the simple form below works
// because MyBatis statement elements are not themselves nested.
const stmtRegex = /<(select|insert|update|delete|sql)\b([^>]*)>([\s\S]*?)<\/\1>/g;
const stmtRegex = /<(select|insert|update|delete|sql|resultMap)\b([^>]*)>([\s\S]*?)<\/\1>/g;
let m: RegExpExecArray | null;
while ((m = stmtRegex.exec(body)) !== null) {
const elemType = m[1]!;
Expand All @@ -123,6 +123,7 @@ export class MyBatisExtractor {
const endLine = this.getLineNumber(absoluteIndex + m[0].length);
const qualified = `${namespace}::${id}`;
const isSqlFragment = elemType === 'sql';
const isResultMap = elemType === 'resultMap';
const nodeId = generateNodeId(this.filePath, 'method', qualified, startLine);
const node: Node = {
id: nodeId,
Expand Down Expand Up @@ -159,11 +160,36 @@ export class MyBatisExtractor {
column: 0,
});
}

// resultMap="X" on a statement → reference to the <resultMap id="X">
// node, so callers/impact can answer "which statements use this result
// map" (mirrors the <include refid> link above). A qualified `ns.X`
// points at another mapper's result map.
if (!isResultMap && !isSqlFragment) {
const rmId = /\bresultMap\s*=\s*"([^"]+)"/.exec(attrs)?.[1]?.trim();
if (rmId) {
const rmQualified = rmId.includes('.') ? rmId.replace(/\./g, '::') : `${namespace}::${rmId}`;
this.unresolvedReferences.push({
fromNodeId: nodeId,
referenceName: rmQualified,
referenceKind: 'references',
line: startLine,
column: 0,
});
}
}
}
}

// NOTE: the `<sql>` and `<resultMap>` signature prefixes are load-bearing —
// the Java↔XML bridge (`mybatisJavaXmlEdges`) uses them to skip non-statement
// nodes. Keep them in sync if this changes.
private buildSignature(elemType: string, attrs: string, isSqlFragment: boolean): string {
if (isSqlFragment) return '<sql>';
if (elemType === 'resultMap') {
const type = /\btype\s*=\s*"([^"]+)"/.exec(attrs)?.[1];
return type ? `<resultMap> type=${type}` : '<resultMap>';
}
const verb = elemType.toUpperCase();
const result = /\bresultType\s*=\s*"([^"]+)"/.exec(attrs)?.[1];
const param = /\bparameterType\s*=\s*"([^"]+)"/.exec(attrs)?.[1];
Expand Down
5 changes: 5 additions & 0 deletions src/resolution/callback-synthesizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1415,6 +1415,11 @@ function mybatisJavaXmlEdges(queries: QueryBuilder): Edge[] {

for (const xml of queries.iterateNodesByKind('method')) {
if (xml.language !== 'xml') continue;
// Only real mapper statements (<select|insert|update|delete>) map to a Java
// interface method. `<sql>` fragments and `<resultMap>`s share the method-node
// shape but are not mapper methods, so an `id` that happens to match a Java
// method name must NOT be bridged to them.
if (xml.signature === '<sql>' || xml.signature?.startsWith('<resultMap>')) continue;
// Qualified name: `<namespace>::<id>`. Extract the simple class name.
const colonIdx = xml.qualifiedName.lastIndexOf('::');
if (colonIdx < 0) continue;
Expand Down