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 = / { if (sensitive.test(line)) { addFinding(findings, 'error', file, index + 1, '列表页面包含敏感字段,禁止直接查询、展示或导出', line.trim()) } }) } } function checkCollectionContracts(files, schemas, findings, warnings) { // B1-2: 只认 主标签的字面量 collection;辅助集合(uni-data-select 等) // 只登记不比对,避免噪音 warning。:collection="变量" 绑定由 script 区首个定义解析。 const udbRe = /]*>/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()