Files
baiyue-firstleg/scripts/mapper-scan-contract.mjs
2026-08-28 23:01:52 +08:00

123 lines
5.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readFileSync, readdirSync, statSync } from 'node:fs'
import { dirname, join, relative, resolve, sep } from 'node:path'
import { fileURLToPath } from 'node:url'
const scriptDir = dirname(fileURLToPath(import.meta.url))
export const repositoryRoot = resolve(scriptDir, '..')
function filesBelow(root, predicate) {
if (!statSafe(root)?.isDirectory()) return []
const result = []
for (const entry of readdirSync(root, { withFileTypes: true })) {
if (entry.name === 'target' || entry.name === '.git') continue
const path = join(root, entry.name)
if (entry.isDirectory()) result.push(...filesBelow(path, predicate))
else if (predicate(path)) result.push(path)
}
return result
}
function statSafe(path) {
try { return statSync(path) } catch { return null }
}
function packageName(source) {
return source.match(/\bpackage\s+([\w.]+)\s*;/)?.[1] || ''
}
function mapperInterfaces(javaFiles) {
return javaFiles.flatMap((path) => {
const source = readFileSync(path, 'utf8')
const names = [...source.matchAll(/\b(?:public\s+)?interface\s+(\w*Mapper)\b/g)].map((match) => match[1])
return names.map((name) => ({ path, source, packageName: packageName(source), name }))
})
}
function mapperScanAnnotation(source) {
const start = source.indexOf('@MapperScan')
if (start < 0) return ''
const end = source.indexOf('\n', start)
if (source[start + '@MapperScan'.length] !== '(') return source.slice(start, end < 0 ? undefined : end)
let depth = 0
for (let index = start + '@MapperScan'.length; index < source.length; index += 1) {
if (source[index] === '(') depth += 1
else if (source[index] === ')' && --depth === 0) return source.slice(start, index + 1)
}
return source.slice(start)
}
function moduleNames(root) {
const pom = readFileSync(join(root, 'pom.xml'), 'utf8')
return [...pom.matchAll(/<module>([^<]+)<\/module>/g)].map((match) => match[1].trim())
}
export function auditMapperScanning(root = repositoryRoot) {
const failures = []
let mapperCount = 0
let persistenceModuleCount = 0
for (const moduleName of moduleNames(root)) {
const moduleRoot = join(root, moduleName)
const javaRoot = join(moduleRoot, 'src', 'main', 'java')
const javaFiles = filesBelow(javaRoot, (path) => path.endsWith('.java'))
const mappers = mapperInterfaces(javaFiles)
if (!mappers.length) continue
persistenceModuleCount += 1
mapperCount += mappers.length
const mapperNames = new Map()
for (const mapper of mappers) {
const previous = mapperNames.get(mapper.name)
if (previous) {
failures.push(`${moduleName}: 根包扫描下 Mapper Bean 名称重复 ${mapper.name}(${relative(root, previous)}、${relative(root, mapper.path)})`)
} else mapperNames.set(mapper.name, mapper.path)
if (!/@Mapper\b/.test(mapper.source)) {
failures.push(`${moduleName}: ${mapper.packageName}.${mapper.name} 缺少 @Mapper(${relative(root, mapper.path)})`)
}
}
const applications = javaFiles.filter((path) => /@SpringBootApplication\b/.test(readFileSync(path, 'utf8')))
if (applications.length !== 1) {
failures.push(`${moduleName}: 期望且只能有一个 @SpringBootApplication,实际 ${applications.length} 个`)
continue
}
const applicationPath = applications[0]
const applicationSource = readFileSync(applicationPath, 'utf8')
const rootPackage = packageName(applicationSource)
const scan = mapperScanAnnotation(applicationSource)
if (!scan) failures.push(`${moduleName}: 启动类缺少统一 @MapperScan`)
else {
if (!scan.includes(`"${rootPackage}"`)) failures.push(`${moduleName}: @MapperScan 必须覆盖服务根包 ${rootPackage}`)
if (!/annotationClass\s*=\s*Mapper\.class/.test(scan)) failures.push(`${moduleName}: @MapperScan 必须使用 annotationClass = Mapper.class`)
}
for (const path of javaFiles) {
if (path === applicationPath) continue
if (/@MapperScan\b/.test(readFileSync(path, 'utf8'))) {
failures.push(`${moduleName}: 禁止分散维护 MapperScan(${relative(root, path)})`)
}
}
for (const mapper of mappers) {
if (!(mapper.packageName === rootPackage || mapper.packageName.startsWith(`${rootPackage}.`))) {
failures.push(`${moduleName}: ${mapper.packageName}.${mapper.name} 不在服务扫描根包 ${rootPackage} 内`)
}
}
}
return { failures, mapperCount, persistenceModuleCount }
}
function main() {
const result = auditMapperScanning(repositoryRoot)
if (result.failures.length) {
console.error(`Mapper 扫描合同失败,共 ${result.failures.length} 项:`)
result.failures.forEach((failure) => console.error(`- ${failure}`))
process.exitCode = 1
return
}
console.log(`Mapper 扫描合同通过:${result.persistenceModuleCount} 个持久化模块、${result.mapperCount} 个 Mapper 均使用 @Mapper,并由服务根包注解过滤扫描。`)
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main()