Files

517 lines
26 KiB
JavaScript
Executable File
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.

#!/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 };