diff --git a/__tests__/mybatis-extractor-robustness.test.ts b/__tests__/mybatis-extractor-robustness.test.ts index 9598075e2..ab1f6bbc0 100644 --- a/__tests__/mybatis-extractor-robustness.test.ts +++ b/__tests__/mybatis-extractor-robustness.test.ts @@ -216,3 +216,56 @@ describe('iBatis 2 coverage (#1182 gap 3)', () => { expect(methodNodes(xml, 'SqlMapConfig.xml')).toHaveLength(0); }); }); + +describe('MyBatis extractor — whitespace in close tags', () => { + it('does not swallow the next statement when a close tag has trailing space', () => { + const xml = + '' + + '' + + '' + + ''; + expect(methodNames(xml).sort()).toEqual([ + 'com.example.FooMapper::a', + 'com.example.FooMapper::b', + ]); + }); + + it('still resolves the include after a spaced close tag', () => { + const xml = + '' + + 'id' + + '' + + ''; + const refs = extractFromSource('FooMapper.xml', xml).unresolvedReferences.map( + (r) => r.referenceName + ); + expect(refs).toContain('com.example.FooMapper::cols'); + }); +}); + +describe('MyBatis extractor — qualified include refid', () => { + it('resolves a cross-mapper refid whose namespace has multiple dots', () => { + const xml = + '' + + 'id, name' + + '' + + ''; + const res = extractFromSource('M.xml', xml); + const fragment = res.nodes.find((n) => n.qualifiedName.endsWith('::base')); + const refs = res.unresolvedReferences.map((r) => r.referenceName); + expect(fragment?.qualifiedName).toBe('com.example.M::base'); + expect(refs).toContain('com.example.M::base'); + }); + + it('still resolves a same-namespace bare refid (regression guard)', () => { + const xml = + '' + + 'id' + + '' + + ''; + const refs = extractFromSource('M.xml', xml).unresolvedReferences.map( + (r) => r.referenceName + ); + expect(refs).toContain('com.example.M::base'); + }); +}); diff --git a/src/extraction/mybatis-extractor.ts b/src/extraction/mybatis-extractor.ts index 0d68d0055..ae2a9e566 100644 --- a/src/extraction/mybatis-extractor.ts +++ b/src/extraction/mybatis-extractor.ts @@ -185,7 +185,7 @@ export class MyBatisExtractor { dialect === 'ibatis' ? 'select|insert|update|delete|sql|statement|procedure' : 'select|insert|update|delete|sql'; - const stmtRegex = new RegExp(`<(${verbs})\\b([^>]*)>([\\s\\S]*?)`, 'g'); + const stmtRegex = new RegExp(`<(${verbs})\\b([^>]*)>([\\s\\S]*?)`, 'g'); let m: RegExpExecArray | null; while ((m = stmtRegex.exec(body)) !== null) { const elemType = m[1]!; @@ -199,6 +199,7 @@ export class MyBatisExtractor { if (!idMatch) continue; const id = idMatch[2]!; const absoluteIndex = bodyStart + m.index; + const openTagLen = elemType.length + attrs.length + 2; const startLine = this.getLineNumber(absoluteIndex); const endLine = this.getLineNumber(absoluteIndex + m[0].length); const { qualifiedName: qualified, name } = this.qualifyStatement(namespace, id); @@ -236,12 +237,14 @@ export class MyBatisExtractor { let inc: RegExpExecArray | null; while ((inc = includeRegex.exec(elemBody)) !== null) { const refid = inc[2]!; - const refQualified = refid.includes('.') - ? refid.replace(/\./g, '::') - : namespace - ? `${namespace}::${refid}` - : refid; - const includeOffset = absoluteIndex + (m[0].length - m[3]!.length - ``.length) + inc.index; + const dot = refid.lastIndexOf('.'); + const refQualified = + dot >= 0 + ? `${refid.slice(0, dot)}::${refid.slice(dot + 1)}` + : namespace + ? `${namespace}::${refid}` + : refid; + const includeOffset = absoluteIndex + openTagLen + inc.index; const line = this.getLineNumber(includeOffset); this.unresolvedReferences.push({ fromNodeId: nodeId,