Four gaps in the MyBatis mapper extractor, all reported and reproduced by @ESPINS in #1182 and verified against main: 1. Single-quoted attribute values (namespace/id/refid/resultType/parameterType) were dropped — the regexes hardcoded double quotes. Now accept either quote via a backreference. 2. Tags inside <!-- ... --> produced phantom statement/include symbols. A length-preserving, CDATA-aware pre-pass blanks comments before scanning, keeping offsets/line numbers intact. 3. Legacy iBatis 2 <sqlMap> files had zero statement coverage (the root finder gated on a <mapper namespace> root). It now also recognizes <sqlMap> (namespaced and namespace-less DAO.method ids) and iBatis's extra <statement>/<procedure> verbs — closing the gap with no new dependency (option (c) from the issue; the batis-xml parser route is declined). 4. Two statements sharing a qualifiedName AND a start line (a vendor-split databaseId pair on one line) collided on the node id, so INSERT OR REPLACE silently dropped one. The id-hash now folds in the statement's byte offset; the stored qualifiedName/startLine are unchanged so the Java<->XML bridge is untouched. Gaps 1 and 2 follow @ESPINS's fix-mybatis-quotes-comments branch. Tests add extractor-level coverage for all four gaps plus a DB-level e2e that proves iBatis statements land and both vendor-split nodes survive a real indexAll. Co-authored-by: Jimin Lee <dlwlalsggg@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Jimin Lee
Claude Opus 4.8
parent
356f5f7659
commit
f5edf8cf49
@@ -464,6 +464,70 @@ describe('Java end-to-end — field-injected bean trace (issue #389)', () => {
|
||||
cg.close();
|
||||
});
|
||||
|
||||
it('covers legacy iBatis <sqlMap> statements and keeps same-line vendor-split pairs (#1182)', async () => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-ibatis-'));
|
||||
const xmlDir = path.join(tmpDir, 'src/main/resources/sqlmaps');
|
||||
fs.mkdirSync(xmlDir, { recursive: true });
|
||||
|
||||
// iBatis 2 sqlMap with an explicit namespace.
|
||||
fs.writeFileSync(
|
||||
path.join(xmlDir, 'Account.xml'),
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n' +
|
||||
'<!DOCTYPE sqlMap PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN" "http://ibatis.apache.org/dtd/sql-map-2.dtd">\n' +
|
||||
"<sqlMap namespace='Account'>\n" +
|
||||
" <sql id='cols'>id, name, email</sql>\n" +
|
||||
" <select id='getById' resultClass='Account'>SELECT <include refid='cols'/> FROM account WHERE id = #id#</select>\n" +
|
||||
" <insert id='insert' parameterClass='Account'>INSERT INTO account (id) VALUES (#id#)</insert>\n" +
|
||||
' <!-- <select id="disabled">SELECT 0</select> -->\n' +
|
||||
'</sqlMap>\n'
|
||||
);
|
||||
// Namespace-less sqlMap whose ids carry the qualifier as `Map.statement`.
|
||||
fs.writeFileSync(
|
||||
path.join(xmlDir, 'LegacyDao.xml'),
|
||||
'<sqlMap>\n' +
|
||||
' <select id="LegacyDao.findAll" resultClass="Row">SELECT * FROM t</select>\n' +
|
||||
'</sqlMap>\n'
|
||||
);
|
||||
// MyBatis mapper with a vendor-split databaseId pair written on ONE line —
|
||||
// same qualifiedName + same start line. Before the id-hash fold both nodes
|
||||
// hashed identically and INSERT OR REPLACE dropped one.
|
||||
fs.writeFileSync(
|
||||
path.join(xmlDir, 'VendorMapper.xml'),
|
||||
'<mapper namespace="com.example.VendorMapper">\n' +
|
||||
'<select id="findUser" databaseId="oracle">SELECT 1 FROM dual</select><select id="findUser" databaseId="mysql">SELECT 1</select>\n' +
|
||||
'</mapper>\n'
|
||||
);
|
||||
|
||||
const cg = CodeGraph.initSync(tmpDir);
|
||||
await cg.indexAll();
|
||||
|
||||
const xmlMethods = cg.getNodesByKind('method').filter((n) => n.language === 'xml');
|
||||
const qnames = xmlMethods.map((n) => n.qualifiedName);
|
||||
|
||||
// iBatis statements now land in the graph (was zero coverage before #1182).
|
||||
expect(qnames).toContain('Account::getById');
|
||||
expect(qnames).toContain('Account::insert');
|
||||
expect(qnames).toContain('Account::cols');
|
||||
expect(qnames).toContain('LegacyDao::findAll');
|
||||
// The commented-out statement produced no node.
|
||||
expect(qnames).not.toContain('Account::disabled');
|
||||
|
||||
// <include refid='cols'/> resolves to the <sql> fragment in the same map.
|
||||
const getById = xmlMethods.find((n) => n.qualifiedName === 'Account::getById');
|
||||
const cols = xmlMethods.find((n) => n.qualifiedName === 'Account::cols');
|
||||
expect(getById).toBeDefined();
|
||||
expect(cols).toBeDefined();
|
||||
const incEdge = cg.getOutgoingEdges(getById!.id).find((e) => e.target === cols!.id);
|
||||
expect(incEdge, "iBatis <include refid='cols'/> should reach the <sql> fragment").toBeDefined();
|
||||
|
||||
// Both vendor-split statements survive the DB write (the collision fix).
|
||||
const findUser = xmlMethods.filter((n) => n.name === 'findUser');
|
||||
expect(findUser, 'both databaseId variants of findUser should survive').toHaveLength(2);
|
||||
expect(new Set(findUser.map((n) => n.id)).size).toBe(2);
|
||||
|
||||
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');
|
||||
|
||||
Reference in New Issue
Block a user