// client/tool/check_ds_code.mjs // 代码端「设计真相源」闸 —— 与原型 check-ds.mjs 对应,把单一真源纪律延伸到 Flutter 代码。 // 红线:颜色只走 context.tokens / AppChrome / AppDims(从 tokens.css codegen 单源), // 禁硬编码 hex(Color(0x..))与具名色(Colors.red 等)。合理特例须显式 `// ds-ignore: 理由` 豁免。 // // 用法(在 client/ 下): // node tool/check_ds_code.mjs # 全量扫描,报存量(不 fail,便于逐步清零) // node tool/check_ds_code.mjs --strict # 全量,有违规即 exit 1 // node tool/check_ds_code.mjs --changed # 仅扫 git 改动的 dart 文件,有违规即 exit 1(pre-commit 用) // // 豁免:在该行加 `// ds-ignore: <理由>`(理由必填,便于审计)。 import fs from 'node:fs'; import path from 'node:path'; import { execFileSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); // client/ const LIB = path.join(ROOT, 'lib'); const args = new Set(process.argv.slice(2)); const CHANGED = args.has('--changed'); const STRICT = args.has('--strict') || CHANGED; // 违规模式:硬编码 hex 颜色;具名色(Colors.x,除 transparent=无色合理)。 const HEX = /Color\(0x[0-9A-Fa-f]{6,8}\)/; const NAMED = /\bColors\.(?!transparent\b)[a-zA-Z]\w*/; const IGNORE = /\/\/\s*ds-ignore/; function listDartFiles() { if (CHANGED) { let out = ''; try { out = execFileSync( 'git', ['diff', '--name-only', '--diff-filter=ACM', 'HEAD'], { cwd: ROOT, encoding: 'utf8' }); } catch { out = ''; } return out.split('\n') .map((s) => s.trim()) .filter((f) => f.endsWith('.dart') && !f.endsWith('.g.dart')) .map((f) => path.resolve(ROOT, '..', f)) // git 路径相对仓库根 .filter((f) => f.startsWith(LIB) && fs.existsSync(f)); } const out = []; (function walk(d) { for (const e of fs.readdirSync(d, { withFileTypes: true })) { const p = path.join(d, e.name); if (e.isDirectory()) walk(p); else if (e.name.endsWith('.dart') && !e.name.endsWith('.g.dart')) out.push(p); } })(LIB); return out; } const violations = []; for (const file of listDartFiles()) { const lines = fs.readFileSync(file, 'utf8').split('\n'); lines.forEach((line, i) => { if (IGNORE.test(line)) return; // 显式豁免 const m = HEX.exec(line) || NAMED.exec(line); if (m) { violations.push({ file: path.relative(ROOT, file), line: i + 1, hit: m[0], text: line.trim().slice(0, 90), }); } }); } const banner = (s) => `\n${'─'.repeat(60)}\n${s}\n${'─'.repeat(60)}`; if (violations.length) { console.log(banner(`✗ 硬编码颜色 ${violations.length} 处(应走 token,或显式 // ds-ignore: 理由)`)); for (const v of violations) console.log(` ${v.file}:${v.line} [${v.hit}] ${v.text}`); } else { console.log(banner('✓ 通过:颜色全部走 token / 已显式豁免')); } if (STRICT && violations.length) process.exit(1); console.log(`\n范围:${CHANGED ? 'git 改动文件' : '全量 lib/'} 违规:${violations.length}${STRICT ? '(strict)' : '(仅报告)'}`);