Files
t-admin/scripts/quality-check.js
T
12914 0a7725f6d8 feat(脏改动归属收尾): 门禁报告去时间戳/pay-order退款空吞补提示/门禁62-0-0
G1: scripts/data-contract-report.js + quality-check.js 删除 Generated 时间戳行
    (时间戳保留在 gitignored 的 *.json generatedAt 备查;reports/*.md
    此后跑门禁不再变脏,归属:门禁产物噪音)
G2: pay-order/list.vue confirmRefund 云端拒绝 errCode 分支 + 校验/网络
    catch 分支均补 toast 用户可见提示(此前静默失败)
门禁: npm test 62 schemas/0 漂移/0 错误全绿;diff --check 按
    core.whitespace=cr-at-eol 通过(仓库既有 CRLF 惯例,autocrlf=false)
2026-09-14 03:24:15 +08:00

233 lines
8.1 KiB
JavaScript
Raw 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.
const fs = require('fs')
const path = require('path')
const root = path.resolve(__dirname, '..')
const reportDir = path.join(root, 'reports')
const sourceRoots = ['pages', 'components', 'windows', 'js_sdk', 'App.vue', 'main.js']
const excludedDirs = new Set(['.git', 'node_modules', 'unpackage', '.zcode'])
const sourceExtensions = new Set(['.vue', '.js', '.json'])
function walk(target) {
const absolute = path.join(root, target)
if (!fs.existsSync(absolute)) return []
const stat = fs.statSync(absolute)
if (stat.isFile()) return [absolute]
const files = []
for (const entry of fs.readdirSync(absolute, { withFileTypes: true })) {
if (entry.isDirectory() && excludedDirs.has(entry.name)) continue
const child = path.join(absolute, entry.name)
if (entry.isDirectory()) files.push(...walk(path.relative(root, child)))
else if (sourceExtensions.has(path.extname(entry.name))) files.push(child)
}
return files
}
function relative(file) {
return path.relative(root, file).replace(/\\/g, '/')
}
function lineOf(text, index) {
return text.slice(0, index).split(/\r?\n/).length
}
function addFinding(list, severity, file, line, message, evidence = '') {
list.push({ severity, file: relative(file), line, message, evidence })
}
function readJson(file) {
try {
return JSON.parse(stripJsonComments(fs.readFileSync(file, 'utf8')))
} catch (error) {
return { __parseError: error.message }
}
}
// B1-2: schema 允许 uniCloud JSONC 注释头,比较前剥离
function stripJsonComments(text) {
let out = ''
let i = 0
const n = text.length
let inStr = false
while (i < n) {
const c = text[i]
if (inStr) {
out += c
if (c === '\\') { out += text[i + 1] || ''; i += 2; continue }
if (c === '"') inStr = false
i++
continue
}
if (c === '"') { inStr = true; out += c; i++; continue }
if (c === '/' && text[i + 1] === '/') {
const end = text.indexOf('\n', i)
const stop = end === -1 ? n : end
out += ' '.repeat(stop - i)
i = stop
continue
}
if (c === '/' && text[i + 1] === '*') {
const end = text.indexOf('*/', i + 2)
const stop = end === -1 ? n : end + 2
const skipped = text.slice(i, stop)
out += skipped.replace(/[^\n]/g, ' ')
i = stop
continue
}
out += c
i++
}
return out
}
function collectSchemas() {
const schemaFiles = []
for (const base of ['uniCloud-alipay/database', 'uni_modules']) {
schemaFiles.push(...walk(base).filter(file => file.endsWith('.schema.json')))
}
const schemas = new Map()
for (const file of schemaFiles) {
const name = path.basename(file, '.schema.json')
schemas.set(name, { file: relative(file), schema: readJson(file) })
}
return schemas
}
function collectFiles() {
return sourceRoots.flatMap(walk).filter(file => !file.endsWith('.schema.json'))
}
function checkVue3(files, findings) {
for (const file of files.filter(file => file.endsWith('.vue'))) {
const text = fs.readFileSync(file, 'utf8')
for (const pattern of [
{ re: /\s(slot|show-iconn)=/g, message: '发现 Vue3 兼容性标记或拼写错误' },
{ re: /<[^>]+\sslot="[^" está]+"/g, message: '发现旧版具名插槽语法' }
]) {
let match
while ((match = pattern.re.exec(text))) {
addFinding(findings, 'error', file, lineOf(text, match.index), pattern.message, match[0].trim())
}
}
}
}
function checkSensitiveFields(files, findings) {
const sensitive = /\b(password|token|password_secret|secret_key|private_key)\b/i
for (const file of files.filter(file => file.endsWith('.vue'))) {
const text = fs.readFileSync(file, 'utf8')
const isList = /<unicloud-db|download-excel/.test(text) && /list\.vue$/.test(file)
if (!isList) continue
const lines = text.split(/\r?\n/)
lines.forEach((line, index) => {
if (sensitive.test(line)) {
addFinding(findings, 'error', file, index + 1, '列表页面包含敏感字段,禁止直接查询、展示或导出', line.trim())
}
})
}
}
function checkCollectionContracts(files, schemas, findings, warnings) {
// B1-2: 只认 <unicloud-db> 主标签的字面量 collection;辅助集合(uni-data-select 等)
// 只登记不比对,避免噪音 warning。:collection="变量" 绑定由 script 区首个定义解析。
const udbRe = /<unicloud-db\b[^>]*>/g
const udbCollectionRe = /collectionList\s*:\s*["']([^"']+)["']|collection\s*:\s*["']([^"']+)["']/
const scriptRe = /collectionList\s*:\s*["']([^"']+)["']/
for (const file of files.filter(file => file.endsWith('.vue') || file.endsWith('.js'))) {
const text = fs.readFileSync(file, 'utf8')
const mains = new Set()
let um
while ((um = udbRe.exec(text))) {
const cm = udbCollectionRe.exec(um[0])
if (cm) mains.add(cm[1] || cm[2])
}
if (!mains.size) {
const sm = scriptRe.exec(text)
if (sm) mains.add(sm[1])
}
for (const name of mains) {
if (name.startsWith('uni-') || name.startsWith('opendb-') || name.includes('comment') || name === 'cms-temp') {
if (!schemas.has(name)) {
addFinding(warnings, 'warning', file, 1, '页面引用的主集合未在本地 schema 中发现', name)
}
}
}
}
}
function checkKnownPatterns(files, findings) {
const patterns = [
{ re: /if\s*\(this\.where\._id\)/, message: '使用不存在的 where._id 判断加载条件' },
{ re: /const\s+queryRe\s*=\s*new\s+RegExp\(query,\s*['"]i['"]\)/, message: '用户搜索输入未转义正则特殊字符' },
{ re: /field\.hasOwnProperty\(['"]value['"]\)/, message: '疑似使用未定义变量 field' },
{ re: /uploadSuccessTaskNames\.push\(name\)/, message: '疑似使用未定义变量 name' },
{ re: /:emptyText="error\.message\s*\|\|\s*loading\s*\?/, message: 'emptyText 条件优先级可能错误' }
]
for (const file of files) {
const text = fs.readFileSync(file, 'utf8')
for (const { re, message } of patterns) {
const match = re.exec(text)
if (match) addFinding(findings, 'error', file, lineOf(text, match.index), message, match[0])
}
}
}
function checkJsonParse(files, findings) {
for (const file of files.filter(file => file.endsWith('.json'))) {
const parsed = readJson(file)
if (parsed.__parseError) addFinding(findings, 'error', file, 1, 'JSON 解析失败', parsed.__parseError)
}
}
function makeReport(findings, warnings, schemas) {
const report = {
generatedAt: new Date().toISOString(),
project: 'uni-admin 2.5.1',
schemaCount: schemas.size,
errors: findings,
warnings,
summary: {
errors: findings.length,
warnings: warnings.length,
status: findings.length ? 'failed' : 'passed'
}
}
fs.mkdirSync(reportDir, { recursive: true })
fs.writeFileSync(path.join(reportDir, 'quality-report.json'), JSON.stringify(report, null, 2) + '\n')
const lines = [
'# Quality report',
'',
// G1收尾:去掉 Generated 时间戳行 —— 每次跑门禁必变导致 reports/*.md 常脏;
// 时间戳仍保留在 gitignored 的 quality-report.json(generatedAt)中备查
`- Status: **${report.summary.status}**`,
`- Errors: ${report.summary.errors}`,
`- Warnings: ${report.summary.warnings}`,
`- Schemas indexed: ${report.schemaCount}`,
''
]
for (const [title, list] of [['Errors', findings], ['Warnings', warnings]]) {
lines.push(`## ${title}`, '')
if (!list.length) lines.push('None', '')
for (const item of list) lines.push(`- **${item.file}:${item.line}** ${item.message}${item.evidence ? ` — \`${item.evidence}\`` : ''}`)
lines.push('')
}
fs.writeFileSync(path.join(reportDir, 'quality-report.md'), lines.join('\n'))
return report
}
function main() {
const files = collectFiles()
const schemas = collectSchemas()
const findings = []
const warnings = []
checkJsonParse(files, findings)
checkVue3(files, findings)
checkSensitiveFields(files, findings)
checkKnownPatterns(files, findings)
checkCollectionContracts(files, schemas, findings, warnings)
const report = makeReport(findings, warnings, schemas)
console.log(JSON.stringify(report.summary))
if (findings.length) process.exitCode = 1
}
main()