reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
@@ -0,0 +1,516 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {
|
||||
caseDir: '',
|
||||
dir: '',
|
||||
file: '',
|
||||
maxLineLength: 180,
|
||||
maxFileLines: 500,
|
||||
maxFunctionLines: 90,
|
||||
json: false,
|
||||
markdown: false,
|
||||
};
|
||||
for (let i = 2; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === '--case-dir' || a === '--case' || a === '-d') args.caseDir = argv[++i] || '';
|
||||
else if (a === '--dir') args.dir = argv[++i] || '';
|
||||
else if (a === '--file' || a === '-f') args.file = argv[++i] || '';
|
||||
else if (a === '--max-line-length') args.maxLineLength = Number(argv[++i] || args.maxLineLength);
|
||||
else if (a === '--max-file-lines') args.maxFileLines = Number(argv[++i] || args.maxFileLines);
|
||||
else if (a === '--max-function-lines') args.maxFunctionLines = Number(argv[++i] || args.maxFunctionLines);
|
||||
else if (a === '--json') args.json = true;
|
||||
else if (a === '--markdown') args.markdown = true;
|
||||
else if (a === '--help' || a === '-h') args.help = true;
|
||||
else throw new Error(`未知参数:${a}`);
|
||||
}
|
||||
if (!args.json && !args.markdown) args.markdown = true;
|
||||
return args;
|
||||
}
|
||||
|
||||
function usage() {
|
||||
return `用法:
|
||||
node scripts/check_code_quality.js --case-dir case --markdown
|
||||
node scripts/check_code_quality.js --dir case/result --json
|
||||
node scripts/check_code_quality.js --file case/result/src/env/install-env.js --markdown
|
||||
|
||||
说明:检查最终补环境代码是否简洁、可读、模块化,并验证中文注释为 UTF-8、无乱码、无连续问号、中文注释不含问号。`;
|
||||
}
|
||||
|
||||
function exists(p) { try { fs.accessSync(p); return true; } catch { return false; } }
|
||||
function stat(p) { try { return fs.statSync(p); } catch { return null; } }
|
||||
function rel(root, p) { return (path.relative(root, p) || '.').replace(/\\/g, '/'); }
|
||||
function ext(p) { return path.extname(p).toLowerCase(); }
|
||||
|
||||
function walk(p, out = []) {
|
||||
if (!exists(p)) return out;
|
||||
const st = stat(p);
|
||||
if (!st) return out;
|
||||
if (st.isDirectory()) {
|
||||
let names = [];
|
||||
try { names = fs.readdirSync(p); } catch { names = []; }
|
||||
for (const name of names) walk(path.join(p, name), out);
|
||||
} else if (st.isFile()) out.push(p);
|
||||
return out;
|
||||
}
|
||||
|
||||
function isCodeFile(p) {
|
||||
return ['.js', '.mjs', '.cjs', '.py'].includes(ext(p));
|
||||
}
|
||||
|
||||
function shouldSkipFile(root, p) {
|
||||
const r = rel(root, p).toLowerCase();
|
||||
if (/(^|\/)(node_modules|dist|build|coverage|vendor|third_party|third-party)(\/|$)/.test(r)) return true;
|
||||
if (/(^|\/)src\/target\/(original|vendor|bundle|bundles)(\/|$)/.test(r)) return true;
|
||||
if (/(\.min\.js|bundle\.js|vendor\.js|package-lock\.json)$/i.test(r)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function readUtf8Strict(file) {
|
||||
const buf = fs.readFileSync(file);
|
||||
const hasBom = buf.length >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf;
|
||||
const text = buf.toString('utf8');
|
||||
return { text: text.replace(/^\uFEFF/, ''), hasBom };
|
||||
}
|
||||
|
||||
function hasChinese(s) {
|
||||
return /[\u4e00-\u9fff]/.test(String(s || ''));
|
||||
}
|
||||
|
||||
function extractJsComments(text) {
|
||||
const comments = [];
|
||||
const blockRe = /\/\*[\s\S]*?\*\//g;
|
||||
let m;
|
||||
while ((m = blockRe.exec(text))) {
|
||||
for (const line of m[0].split(/\r?\n/)) comments.push(line.replace(/^\/\*+|\*+\/$/g, '').replace(/^\s*\*\s?/, '').trim());
|
||||
}
|
||||
const lineRe = /(^|\s)\/\/(.*)$/gm;
|
||||
while ((m = lineRe.exec(text))) comments.push((m[2] || '').trim());
|
||||
return comments.filter(Boolean);
|
||||
}
|
||||
|
||||
function extractPyComments(text) {
|
||||
const comments = [];
|
||||
const lineRe = /^\s*#(.*)$/gm;
|
||||
let m;
|
||||
while ((m = lineRe.exec(text))) comments.push((m[1] || '').trim());
|
||||
return comments.filter(Boolean);
|
||||
}
|
||||
|
||||
function extractComments(file, text) {
|
||||
return ext(file) === '.py' ? extractPyComments(text) : extractJsComments(text);
|
||||
}
|
||||
|
||||
function firstNonEmptyLines(text, count = 8) {
|
||||
return text.split(/\r?\n/).map(x => x.trim()).filter(Boolean).slice(0, count);
|
||||
}
|
||||
|
||||
function stripJsLine(line) {
|
||||
return line
|
||||
.replace(/\/\*.*?\*\//g, '')
|
||||
.replace(/\/\/.*$/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function codeLineCount(file, lines) {
|
||||
if (ext(file) === '.py') return lines.filter(line => line.trim() && !line.trim().startsWith('#')).length;
|
||||
return lines.filter(line => stripJsLine(line)).length;
|
||||
}
|
||||
|
||||
function maxJsBraceDepth(lines) {
|
||||
let depth = 0;
|
||||
let maxDepth = 0;
|
||||
for (const raw of lines) {
|
||||
const line = stripJsLine(raw);
|
||||
for (const ch of line) {
|
||||
if (ch === '{') {
|
||||
depth += 1;
|
||||
if (depth > maxDepth) maxDepth = depth;
|
||||
} else if (ch === '}') {
|
||||
depth = Math.max(0, depth - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return maxDepth;
|
||||
}
|
||||
|
||||
function maxPyIndentDepth(lines) {
|
||||
let maxDepth = 0;
|
||||
for (const raw of lines) {
|
||||
if (!raw.trim() || raw.trim().startsWith('#')) continue;
|
||||
const spaces = raw.match(/^\s*/)[0].replace(/\t/g, ' ').length;
|
||||
maxDepth = Math.max(maxDepth, Math.floor(spaces / 2));
|
||||
}
|
||||
return maxDepth;
|
||||
}
|
||||
|
||||
function inspectJsFunctions(lines, maxFunctionLines) {
|
||||
const problems = [];
|
||||
const warnings = [];
|
||||
const starters = [
|
||||
/\bfunction\s+([A-Za-z_$][\w$]*)?\s*\([^)]*\)\s*\{/,
|
||||
/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{/,
|
||||
/\b(?:async\s+)?([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*\{/
|
||||
];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = stripJsLine(lines[i]);
|
||||
if (!line.includes('{')) continue;
|
||||
if (!starters.some(re => re.test(line))) continue;
|
||||
let depth = 0;
|
||||
let end = i;
|
||||
let seenOpen = false;
|
||||
for (let j = i; j < lines.length; j++) {
|
||||
const cur = stripJsLine(lines[j]);
|
||||
for (const ch of cur) {
|
||||
if (ch === '{') { depth += 1; seenOpen = true; }
|
||||
else if (ch === '}') depth -= 1;
|
||||
}
|
||||
if (seenOpen && depth <= 0) { end = j; break; }
|
||||
}
|
||||
const size = end - i + 1;
|
||||
if (size > maxFunctionLines) problems.push(`第 ${i + 1} 行附近函数过长:${size} 行,建议拆分到 ${maxFunctionLines} 行以内。`);
|
||||
const prev = lines.slice(Math.max(0, i - 3), i).join('\n');
|
||||
if (size > 15 && !hasChinese(prev)) warnings.push(`第 ${i + 1} 行附近函数较长但前置中文说明不足,建议补充职责、输入和输出说明。`);
|
||||
}
|
||||
return { problems, warnings };
|
||||
}
|
||||
|
||||
function inspectPyFunctions(lines, maxFunctionLines) {
|
||||
const problems = [];
|
||||
const warnings = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const m = lines[i].match(/^(\s*)def\s+([A-Za-z_]\w*)\s*\(/);
|
||||
if (!m) continue;
|
||||
const baseIndent = m[1].replace(/\t/g, ' ').length;
|
||||
let end = i;
|
||||
for (let j = i + 1; j < lines.length; j++) {
|
||||
if (!lines[j].trim()) { end = j; continue; }
|
||||
const indent = lines[j].match(/^\s*/)[0].replace(/\t/g, ' ').length;
|
||||
if (indent <= baseIndent) break;
|
||||
end = j;
|
||||
}
|
||||
const size = end - i + 1;
|
||||
if (size > maxFunctionLines) problems.push(`第 ${i + 1} 行附近函数过长:${size} 行,建议拆分到 ${maxFunctionLines} 行以内。`);
|
||||
const prev = lines.slice(Math.max(0, i - 3), i).join('\n');
|
||||
if (size > 15 && !hasChinese(prev)) warnings.push(`第 ${i + 1} 行附近函数较长但前置中文说明不足,建议补充职责、输入和输出说明。`);
|
||||
}
|
||||
return { problems, warnings };
|
||||
}
|
||||
|
||||
function inspectEmbeddedScriptStrings(relFile, text) {
|
||||
const problems = [];
|
||||
const warnings = [];
|
||||
const normalized = relFile.replace(/\\/g, '/');
|
||||
const fileName = path.basename(normalized).toLowerCase();
|
||||
const isEnvFile = /(^|\/)src\/env\//i.test(normalized) || /(^|\/)env\//i.test(normalized);
|
||||
if (!/\.(?:js|mjs|cjs)$/i.test(normalized)) return { problems, warnings };
|
||||
|
||||
const scriptRe = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*(?:SCRIPT|SOURCE|CODE)[A-Za-z_$]*)\s*=\s*String\.raw`([\s\S]*?)`/g;
|
||||
let match;
|
||||
while ((match = scriptRe.exec(text))) {
|
||||
const name = match[1];
|
||||
const body = match[2] || '';
|
||||
const bodyLines = body.split(/\r?\n/).length;
|
||||
const looksLikeWebApi = /\b(?:Navigator|Document|Window|Location|History|Screen|XMLHttpRequest|HTMLCanvasElement|WebGL|navigator|document|window|xbs\.|createProtoChains|createNativeFunction|createGetter|createSetter|createNativeCollection|xbs\.dom\.createDocument)\b/.test(body);
|
||||
if (isEnvFile && (bodyLines > 40 || looksLikeWebApi)) {
|
||||
problems.push('发现大段 isolated-vm 补环境字符串 `' + name + ' = String.raw 模板字符串`(约 ' + bodyLines + ' 行)。补环境源码必须拆成真实 .js 文件,例如 browser-objects/navigator.js、document.js、fingerprint/canvas.js,再由 runtime.runFile/runFiles 注入 Context。');
|
||||
} else if (bodyLines > 80) {
|
||||
warnings.push(`发现较大的 String.raw 字符串 \`${name}\`(约 ${bodyLines} 行),请确认不是补环境 WebAPI 主实现。`);
|
||||
}
|
||||
}
|
||||
|
||||
if (isEnvFile && /^script-(?:core|browser|.*objects|.*env).*\.js$/i.test(fileName) && /String\.raw`/.test(text)) {
|
||||
problems.push('发现 script-*.js 聚合字符串补环境文件。isolated-vm 交付应使用真实模块文件,不应把主要 WebAPI 放入 script-core.js 或 script-browser-objects.js。');
|
||||
}
|
||||
if (isEnvFile && /\b[A-Z][A-Z0-9_]*SCRIPT\b[\s\S]{0,200}\.join\(\s*['"]\\n['"]\s*\)/.test(text)) {
|
||||
problems.push('发现把多个 *_SCRIPT 字符串 join 后执行的补环境聚合方式。请改为 runtime.runFiles([...]) 按真实文件注入。');
|
||||
}
|
||||
if (isEnvFile && /module\.exports\s*=\s*\{[\s\S]{0,300}\b[A-Z][A-Z0-9_]*SCRIPT\b/.test(text)) {
|
||||
warnings.push('发现导出 *_SCRIPT 字符串。除极小 bootstrap 外,补环境实现应导出安装函数或通过真实文件加载。');
|
||||
}
|
||||
return { problems, warnings };
|
||||
}
|
||||
|
||||
const WEBAPI_DOMAIN_PATTERNS = [
|
||||
['window/global', /\b(?:window|globalThis|self|top|parent|frames|innerWidth|outerWidth|devicePixelRatio)\b/],
|
||||
['navigator', /\b(?:navigator|Navigator|PluginArray|MimeTypeArray|permissions|sendBeacon|hardwareConcurrency|webdriver|mimeTypes|plugins)\b/],
|
||||
['document/dom', /\b(?:document|Document|HTMLDocument|Element|HTMLElement|createElement|createTextNode|currentScript|DOMRect|MutationObserver|HTMLCollection|NodeList)\b/],
|
||||
['location/history', /\b(?:location|Location|history|History|pushState|replaceState)\b/],
|
||||
['screen', /\b(?:screen|Screen|availWidth|availHeight|colorDepth|pixelDepth)\b/],
|
||||
['storage/cookie', /\b(?:localStorage|sessionStorage|Storage|cookie|Cookie|document\.cookie)\b/],
|
||||
['network', /\b(?:XMLHttpRequest|fetch|Request|Response|Headers|sendBeacon|open\s*\(|setRequestHeader|readyState)\b/],
|
||||
['canvas', /\b(?:HTMLCanvasElement|CanvasRenderingContext2D|toDataURL|getImageData|measureText|canvas)\b/],
|
||||
['webgl', /\b(?:WebGLRenderingContext|WebGL2RenderingContext|WebGLShaderPrecisionFormat|getParameter|readPixels|getSupportedExtensions|webgl)\b/],
|
||||
['audio', /\b(?:AudioContext|OfflineAudioContext|AudioBuffer|HTMLAudioElement|getChannelData|startRendering|audio)\b/],
|
||||
['performance/timing', /\b(?:performance|Performance|PerformanceNavigationTiming|timeOrigin|navigationStart|requestAnimationFrame)\b/],
|
||||
['events', /\b(?:EventTarget|Event|MouseEvent|KeyboardEvent|CustomEvent|addEventListener|dispatchEvent|isTrusted)\b/],
|
||||
['worker/wasm/message', /\b(?:Worker|postMessage|MessageChannel|MessagePort|WebAssembly|BroadcastChannel)\b/],
|
||||
];
|
||||
|
||||
function detectWebApiDomains(text) {
|
||||
const found = [];
|
||||
for (const [name, pattern] of WEBAPI_DOMAIN_PATTERNS) {
|
||||
if (pattern.test(text)) found.push(name);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
function isEnvModulePath(relFile) {
|
||||
return /(^|\/)src\/(?:env|node-runtime\/env)\//i.test(relFile) || /(^|\/)env\//i.test(relFile);
|
||||
}
|
||||
|
||||
function inspectWebApiModuleBoundary(relFile, text, lines, args) {
|
||||
const problems = [];
|
||||
const warnings = [];
|
||||
const normalized = relFile.replace(/\\/g, '/');
|
||||
if (!/\.(?:js|mjs|cjs)$/i.test(normalized)) return { problems, warnings };
|
||||
|
||||
const domains = detectWebApiDomains(text);
|
||||
if (!domains.length) return { problems, warnings };
|
||||
|
||||
const envModule = isEnvModulePath(normalized);
|
||||
const orchestrationDir = /(^|\/)src\/(?:signer|request|resources)\//i.test(normalized);
|
||||
const signerFile = /(^|\/)src\/signer\//i.test(normalized);
|
||||
const probeLikeFile = /(^|\/)[^/]*(?:probe|diagnostic|runtime-runner|runtime_probe|runtime-probe)[^/]*\.(?:js|mjs|cjs)$/i.test(normalized);
|
||||
const codeLines = codeLineCount(normalized, lines);
|
||||
const implementationLike = /\b(?:installBrowserEnv|install[A-Z][\w$]*Env|create[A-Z][\w$]*(?:Environment|Constructors?|Env)|createNativeHelpers|defineValue\s*\(|defineGetter\s*\(|defineAccessor\s*\(|Object\.defineProperty|Object\.defineProperties|nativeApi\.fn|nativeApi\.getter|HTMLCanvasElement|WebGLRenderingContext|XMLHttpRequest)\b/.test(text);
|
||||
const domainText = domains.join('、');
|
||||
|
||||
if (signerFile && domains.length >= 3 && implementationLike) {
|
||||
problems.push(`src/signer 文件承载了 ${domains.length} 类 WebAPI 补环境主体(${domainText})。signer 只允许做入口编排、调用目标 signer 和整理输出;请把 WebAPI 安装逻辑拆到 src/env/ 或 src/node-runtime/env/ 下的真实模块。`);
|
||||
}
|
||||
|
||||
if (probeLikeFile && !envModule && domains.length >= 4 && codeLines > 180 && implementationLike) {
|
||||
problems.push(`probe / runtime 入口文件承载了 ${domains.length} 类 WebAPI 补环境主体(${domainText})。probe 只能是薄入口,建议少于 150 行;请拆分为 browser-objects、fingerprint、network、bootstrap 等模块。`);
|
||||
}
|
||||
|
||||
if (!envModule && domains.length >= 5 && codeLines > args.maxFileLines && implementationLike) {
|
||||
problems.push(`单个非 env 文件同时实现 ${domains.length} 类 WebAPI 且超过 ${args.maxFileLines} 行(${domainText}),属于补环境主体堆叠。请先规划目录并拆分模块后再继续验证。`);
|
||||
}
|
||||
|
||||
if (orchestrationDir && domains.length >= 2 && codeLines > 240 && implementationLike && !signerFile) {
|
||||
warnings.push(`编排目录文件包含多类 WebAPI 实现迹象(${domainText})。请确认它只做请求、资源或摘要编排;WebAPI 主体必须放入 src/env/ 或 src/node-runtime/env/。`);
|
||||
}
|
||||
|
||||
return { problems, warnings };
|
||||
}
|
||||
|
||||
function inspectProjectStructure(root, files) {
|
||||
const problems = [];
|
||||
const warnings = [];
|
||||
const codeFiles = files.filter(isCodeFile);
|
||||
const fileEntries = codeFiles.map(filePath => {
|
||||
let text = '';
|
||||
try { text = readUtf8Strict(filePath).text; } catch { text = ''; }
|
||||
return { filePath, relFile: rel(root, filePath), text };
|
||||
});
|
||||
const isolatedVmUsed = fileEntries.some(entry => /\b(?:isolated_vm\.node|xbs-isolated-vm|createXbsRuntime|createIsolatedVmRuntime|window\.xbs|xbs\.dom\.createDocument)\b/.test(entry.text));
|
||||
if (!isolatedVmUsed) return { problems, warnings };
|
||||
|
||||
const envEntries = fileEntries.filter(entry => /(^|\/)src\/env\//i.test(entry.relFile));
|
||||
const embeddedEnvScripts = envEntries.filter(entry => /String\.raw`/.test(entry.text) && /\b(?:Navigator|Document|Window|XMLHttpRequest|HTMLCanvasElement|WebGL|xbs\.|navigator|document|window)\b/.test(entry.text));
|
||||
if (embeddedEnvScripts.length) {
|
||||
problems.push('选择 isolated-vm 时发现补环境 WebAPI 仍以 String.raw 聚合字符串实现:' + embeddedEnvScripts.map(entry => entry.relFile).join('、') + '。请拆分为真实模块文件并用 runtime.runFile/runFiles 加载。');
|
||||
}
|
||||
|
||||
const hasBrowserObjectModule = envEntries.some(entry => /(^|\/)src\/env\/browser-objects\/[^/]+\.js$/i.test(entry.relFile));
|
||||
const touchesBrowserObjects = envEntries.some(entry => /\b(?:navigator|document|window|location|screen|XMLHttpRequest|history|storage)\b/i.test(entry.text));
|
||||
if (touchesBrowserObjects && !hasBrowserObjectModule) {
|
||||
problems.push('选择 isolated-vm 且补了浏览器对象,但未发现 src/env/browser-objects/*.js 模块。请至少按 navigator.js、document.js、window.js 等职责拆分。');
|
||||
}
|
||||
|
||||
const hasRunFiles = fileEntries.some(entry => /\brunFiles\s*\(|\brunFile\s*\(/.test(entry.text));
|
||||
if (embeddedEnvScripts.length && !hasRunFiles) {
|
||||
warnings.push('未发现 runtime.runFile/runFiles 文件化加载入口。建议在 xbs runtime 中提供按文件注入 Context 的能力。');
|
||||
}
|
||||
return { problems, warnings };
|
||||
}
|
||||
|
||||
function inspectFile(root, file, args) {
|
||||
const relFile = rel(root, file);
|
||||
const problems = [];
|
||||
const warnings = [];
|
||||
const { text, hasBom } = readUtf8Strict(file);
|
||||
const lines = text.split(/\r?\n/);
|
||||
const comments = extractComments(file, text);
|
||||
const chineseComments = comments.filter(hasChinese);
|
||||
const codeLines = codeLineCount(file, lines);
|
||||
const embeddedScriptCheck = inspectEmbeddedScriptStrings(relFile, text);
|
||||
problems.push(...embeddedScriptCheck.problems);
|
||||
warnings.push(...embeddedScriptCheck.warnings);
|
||||
const webApiBoundaryCheck = inspectWebApiModuleBoundary(relFile, text, lines, args);
|
||||
problems.push(...webApiBoundaryCheck.problems);
|
||||
warnings.push(...webApiBoundaryCheck.warnings);
|
||||
|
||||
if (hasBom) problems.push('文件带 UTF-8 BOM,建议使用无 BOM UTF-8。');
|
||||
if (/\uFFFD/.test(text)) problems.push('文件包含替换字符,疑似 UTF-8 解码或写入异常。');
|
||||
if (/\?{6,}/.test(text)) problems.push('文件包含连续问号,疑似中文编码问题。');
|
||||
|
||||
for (let i = 0; i < comments.length; i++) {
|
||||
const c = comments[i];
|
||||
if (hasChinese(c) && /[??]/.test(c)) problems.push(`中文注释包含问号:第 ${i + 1} 条注释“${c.slice(0, 60)}”。`);
|
||||
if (/\uFFFD|\?{3,}/.test(c)) problems.push(`注释疑似乱码:第 ${i + 1} 条注释“${c.slice(0, 60)}”。`);
|
||||
}
|
||||
|
||||
const header = firstNonEmptyLines(text, 8).join('\n');
|
||||
if (!hasChinese(header) || !/^\s*(\/\/|\/\*|\*|#)/m.test(header)) {
|
||||
problems.push('文件开头缺少中文职责注释;请在文件顶部说明模块用途、数据来源和边界。');
|
||||
}
|
||||
|
||||
if (codeLines > 20 && chineseComments.length === 0) problems.push('代码超过 20 行但没有中文注释。');
|
||||
if (codeLines > 80 && chineseComments.length < Math.ceil(codeLines / 120)) {
|
||||
problems.push(`中文注释过少:代码约 ${codeLines} 行,中文注释 ${chineseComments.length} 条。`);
|
||||
}
|
||||
|
||||
if (lines.length > args.maxFileLines) problems.push(`文件过大:${lines.length} 行,建议拆分到 ${args.maxFileLines} 行以内。`);
|
||||
lines.forEach((line, idx) => {
|
||||
if (line.length > args.maxLineLength) problems.push(`第 ${idx + 1} 行过长:${line.length} 字符,建议拆分到 ${args.maxLineLength} 字符以内。`);
|
||||
if (line.length > 320) problems.push(`第 ${idx + 1} 行疑似压缩或堆叠代码。`);
|
||||
});
|
||||
|
||||
const semicolonDense = lines.filter(line => (line.match(/;/g) || []).length >= 6);
|
||||
if (semicolonDense.length) problems.push(`发现 ${semicolonDense.length} 行包含大量分号,疑似压缩或多语句堆叠。`);
|
||||
|
||||
const denseLineProblems = [];
|
||||
lines.forEach((rawLine, idx) => {
|
||||
const line = stripJsLine(rawLine);
|
||||
if (!line) return;
|
||||
const lineNo = idx + 1;
|
||||
const defineCount = (line.match(/\b(?:Object\.defineProperty|Object\.defineProperties|defineValue|defineNativeValue|defineNativeGetter|defineNativeSetter|defineNativeAccessor)\s*\(/g) || []).length;
|
||||
if (defineCount >= 2) {
|
||||
denseLineProblems.push(`第 ${lineNo} 行同时包含 ${defineCount} 个属性定义调用,应拆成多行并补充 descriptor 说明。`);
|
||||
}
|
||||
if (/(?:Object\.defineProperty|Object\.defineProperties)\s*\([^;]+\{[^{}\n]*(?:value|get|set|writable|enumerable|configurable)[^{}\n]*\}[^;]*;?$/.test(line) && line.length > 110) {
|
||||
denseLineProblems.push(`第 ${lineNo} 行把属性描述符压在一行,建议展开 value/get/set/writable/enumerable/configurable。`);
|
||||
}
|
||||
if (/\bObject\.assign\s*\([^;]*\{.*\{.*\}/.test(line)) {
|
||||
denseLineProblems.push(`第 ${lineNo} 行疑似用 Object.assign 堆叠对象和方法,补环境代码应拆为 createProtoChains 与 defineProperty。`);
|
||||
}
|
||||
if (/\b(?:ctx|window|globalThis|globalObject|self)\s*(?:\.|\[).*=.*\{.*(?:function\b|=>|\b[A-Za-z_$][\w$]*\s*\([^)]*\)\s*\{)/.test(line)) {
|
||||
denseLineProblems.push(`第 ${lineNo} 行把全局 WebAPI 对象、方法或函数堆在一行,建议拆成模块化安装函数。`);
|
||||
}
|
||||
if (/\b(?:if|for|while|try|catch|finally)\b[^{;\n]*\{[^{}\n]*;[^{}\n]*\}/.test(line) && line.length > 120) {
|
||||
denseLineProblems.push(`第 ${lineNo} 行存在较长的单行控制流代码块,建议展开为多行。`);
|
||||
}
|
||||
if (/\bfunction\b[^{;\n]*\{[^{}\n]*;[^{}\n]*\}/.test(line) && line.length > 120) {
|
||||
denseLineProblems.push(`第 ${lineNo} 行存在单行函数体,建议提取为具名函数并展开实现。`);
|
||||
}
|
||||
});
|
||||
if (denseLineProblems.length) problems.push(...denseLineProblems.slice(0, 30));
|
||||
if (denseLineProblems.length > 30) problems.push(`还有 ${denseLineProblems.length - 30} 个疑似单行堆叠问题未逐条展示,请先格式化源码后重新检查。`);
|
||||
|
||||
if (/\bdebugger\s*;/.test(text)) problems.push('存在 debugger 语句,最终代码不得保留调试断点。');
|
||||
if (/TODO|FIXME|临时|随便|测试用|先这样|debug/i.test(text)) warnings.push('发现 TODO/FIXME/临时/调试类标记,交付前建议清理或改为正式说明。');
|
||||
if (/\b(?:var\s+[a-z]\b|function\s+[a-z]\s*\(|const\s+[a-z]\s*=|let\s+[a-z]\s*=)/.test(text)) warnings.push('发现过短变量或函数名,建议使用表达业务含义的命名。');
|
||||
|
||||
const depth = ext(file) === '.py' ? maxPyIndentDepth(lines) : maxJsBraceDepth(lines);
|
||||
if (depth > 8) problems.push(`嵌套层级过深:最大层级 ${depth},建议拆分函数或提前返回。`);
|
||||
else if (depth > 6) warnings.push(`嵌套层级偏深:最大层级 ${depth},建议优化结构。`);
|
||||
|
||||
const funcs = ext(file) === '.py' ? inspectPyFunctions(lines, args.maxFunctionLines) : inspectJsFunctions(lines, args.maxFunctionLines);
|
||||
problems.push(...funcs.problems);
|
||||
warnings.push(...funcs.warnings);
|
||||
|
||||
const anonymousCount = (text.match(/\bfunction\s*\(/g) || []).length + (text.match(/=>\s*\{/g) || []).length;
|
||||
if (anonymousCount > 8) warnings.push(`匿名函数较多:${anonymousCount} 个,建议提取为具名函数提升可读性。`);
|
||||
|
||||
return {
|
||||
file: relFile,
|
||||
clean: problems.length === 0,
|
||||
lines: lines.length,
|
||||
codeLines,
|
||||
chineseCommentCount: chineseComments.length,
|
||||
maxDepth: depth,
|
||||
problems,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
function check(args) {
|
||||
const root = args.file
|
||||
? path.resolve(path.dirname(args.file))
|
||||
: args.dir
|
||||
? path.resolve(args.dir)
|
||||
: path.join(path.resolve(args.caseDir || 'case'), 'result');
|
||||
const files = args.file ? [path.resolve(args.file)] : walk(root).filter(p => isCodeFile(p) && !shouldSkipFile(root, p));
|
||||
const problems = [];
|
||||
const warnings = [];
|
||||
if (!files.length) problems.push(`未找到可检查的最终代码文件:${root}`);
|
||||
const fileResults = files.map(f => inspectFile(root, f, args));
|
||||
for (const r of fileResults) {
|
||||
for (const p of r.problems) problems.push(`${r.file}: ${p}`);
|
||||
for (const w of r.warnings) warnings.push(`${r.file}: ${w}`);
|
||||
}
|
||||
const structure = inspectProjectStructure(root, files);
|
||||
for (const p of structure.problems) problems.push(p);
|
||||
for (const w of structure.warnings) warnings.push(w);
|
||||
return {
|
||||
root,
|
||||
clean: problems.length === 0,
|
||||
filesChecked: fileResults.length,
|
||||
limits: {
|
||||
maxLineLength: args.maxLineLength,
|
||||
maxFileLines: args.maxFileLines,
|
||||
maxFunctionLines: args.maxFunctionLines,
|
||||
},
|
||||
problems,
|
||||
warnings,
|
||||
fileResults,
|
||||
};
|
||||
}
|
||||
|
||||
function renderMarkdown(result) {
|
||||
const lines = [
|
||||
'# 补环境代码质量检查结果',
|
||||
'',
|
||||
`检查范围:${result.root}`,
|
||||
`是否通过:${result.clean ? '是' : '否'}`,
|
||||
`检查文件数:${result.filesChecked}`,
|
||||
'',
|
||||
'## 质量规则',
|
||||
`- 单行长度上限:${result.limits.maxLineLength}`,
|
||||
`- 单文件行数上限:${result.limits.maxFileLines}`,
|
||||
`- 单函数行数上限:${result.limits.maxFunctionLines}`,
|
||||
'- 必须有中文职责注释,中文注释不得包含问号、连续问号或乱码。',
|
||||
'- 禁止压缩代码、过度堆叠语句、调试断点和临时测试标记。',
|
||||
'- signer / probe / runtime 入口不得承载 navigator、document、canvas、webgl、performance 等多域 WebAPI 补环境主体。',
|
||||
'- XHR/fetch 实现应拆入 env/network 并通过 live session bridge;浏览器对象私有状态不得以 _ / __ own property 泄露。',
|
||||
'- isolated-vm 补环境不得以大段 String.raw / *_SCRIPT 字符串作为主要交付形态,必须拆成真实文件模块并通过 runFile/runFiles 注入。',
|
||||
'',
|
||||
];
|
||||
if (result.problems.length) {
|
||||
lines.push('## 问题');
|
||||
for (const p of result.problems) lines.push(`- ${p}`);
|
||||
lines.push('');
|
||||
}
|
||||
if (result.warnings.length) {
|
||||
lines.push('## 提醒');
|
||||
for (const w of result.warnings) lines.push(`- ${w}`);
|
||||
lines.push('');
|
||||
}
|
||||
lines.push('## 文件摘要');
|
||||
for (const f of result.fileResults) {
|
||||
lines.push(`- ${f.file}:${f.clean ? '通过' : '失败'},总行数 ${f.lines},代码行 ${f.codeLines},中文注释 ${f.chineseCommentCount},最大嵌套 ${f.maxDepth}`);
|
||||
}
|
||||
if (!result.fileResults.length) lines.push('- 无');
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
|
||||
try {
|
||||
const args = parseArgs(process.argv);
|
||||
if (args.help) { console.log(usage()); process.exit(0); }
|
||||
const result = check(args);
|
||||
if (args.json) console.log(JSON.stringify(result, null, 2));
|
||||
if (args.markdown) process.stdout.write(renderMarkdown(result));
|
||||
process.exit(result.clean ? 0 : 1);
|
||||
} catch (err) {
|
||||
console.error(err.message || String(err));
|
||||
console.error(usage());
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
module.exports = { check, inspectFile, extractComments, inspectEmbeddedScriptStrings, inspectWebApiModuleBoundary, inspectProjectStructure };
|
||||
Reference in New Issue
Block a user