(
fileNodeId: string,
namespace: string,
dialect: 'mybatis' | 'ibatis',
bodyStart: number,
bodyEnd: number
)
| 168 | } |
| 169 | |
| 170 | private extractMapper( |
| 171 | fileNodeId: string, |
| 172 | namespace: string, |
| 173 | dialect: 'mybatis' | 'ibatis', |
| 174 | bodyStart: number, |
| 175 | bodyEnd: number |
| 176 | ): void { |
| 177 | const body = this.source.slice(bodyStart, bodyEnd); |
| 178 | // Match each top-level statement-shaped element. The body may have nested |
| 179 | // tags (`<if>`, `<foreach>`, `<include>`), so we scan with a regex that |
| 180 | // pairs an opening tag to its matching close — the simple form below works |
| 181 | // because MyBatis/iBatis statement elements are not themselves nested. |
| 182 | // iBatis 2 adds the generic `<statement>` and `<procedure>` on top of the |
| 183 | // MyBatis 3 verbs; gating by dialect keeps MyBatis extraction unchanged. |
| 184 | const verbs = |
| 185 | dialect === 'ibatis' |
| 186 | ? 'select|insert|update|delete|sql|statement|procedure' |
| 187 | : 'select|insert|update|delete|sql'; |
| 188 | const stmtRegex = new RegExp(`<(${verbs})\\b([^>]*)>([\\s\\S]*?)</\\1>`, 'g'); |
| 189 | let m: RegExpExecArray | null; |
| 190 | while ((m = stmtRegex.exec(body)) !== null) { |
| 191 | const elemType = m[1]!; |
| 192 | const attrs = m[2] ?? ''; |
| 193 | const elemBody = m[3] ?? ''; |
| 194 | // Accept either quote style (`(["'])…\1`). The identifier-shaped MyBatis |
| 195 | // attributes matched here and below (namespace/id/refid/resultType/ |
| 196 | // parameterType) are Java FQNs, method names, or type aliases and never |
| 197 | // contain a quote character, so excluding both quotes from the value is safe. |
| 198 | const idMatch = /\bid\s*=\s*(["'])([^"']+)\1/.exec(attrs); |
| 199 | if (!idMatch) continue; |
| 200 | const id = idMatch[2]!; |
| 201 | const absoluteIndex = bodyStart + m.index; |
| 202 | const startLine = this.getLineNumber(absoluteIndex); |
| 203 | const endLine = this.getLineNumber(absoluteIndex + m[0].length); |
| 204 | const { qualifiedName: qualified, name } = this.qualifyStatement(namespace, id); |
| 205 | const isSqlFragment = elemType === 'sql'; |
| 206 | // The id-hash folds in the statement's byte offset (unique per statement |
| 207 | // in the file), not just the start line: two statements sharing a |
| 208 | // qualifiedName AND a start line — e.g. a vendor-split `databaseId` pair |
| 209 | // (`<select id="x" databaseId="oracle">…</select><select id="x" |
| 210 | // databaseId="mysql">…`) written on one line — would otherwise hash to |
| 211 | // the same node id, and `INSERT OR REPLACE INTO nodes` (id is the PRIMARY |
| 212 | // KEY) would silently drop one. qualifiedName and startLine are stored |
| 213 | // unchanged, so the Java↔XML suffix-match bridge is untouched. |
| 214 | const nodeId = generateNodeId(this.filePath, 'method', qualified, absoluteIndex); |
| 215 | const node: Node = { |
| 216 | id: nodeId, |
| 217 | kind: 'method', |
| 218 | name, |
| 219 | qualifiedName: qualified, |
| 220 | filePath: this.filePath, |
| 221 | language: 'xml', |
| 222 | signature: this.buildSignature(elemType, attrs, isSqlFragment), |
| 223 | startLine, |
| 224 | endLine, |
| 225 | startColumn: 0, |
| 226 | endColumn: 0, |
| 227 | docstring: this.previewSql(elemBody), |
no test coverage detected