#!/usr/bin/env node 'use strict'; const fs = require('fs'); const path = require('path'); function parseArgs(argv) { const args = { caseDir: '', dir: '', file: '', json: false, markdown: false, warningsAsErrors: false, }; for (let i = 2; i < argv.length; i++) { const arg = argv[i]; if (arg === '--case-dir' || arg === '--case' || arg === '-d') args.caseDir = argv[++i] || ''; else if (arg === '--dir') args.dir = argv[++i] || ''; else if (arg === '--file' || arg === '-f') args.file = argv[++i] || ''; else if (arg === '--json') args.json = true; else if (arg === '--markdown') args.markdown = true; else if (arg === '--warnings-as-errors') args.warningsAsErrors = true; else if (arg === '--help' || arg === '-h') args.help = true; else throw new Error(`未知参数:${arg}`); } if (!args.json && !args.markdown) args.markdown = true; return args; } function usage() { return `用法: node scripts/check_webapi_addon_coverage.js --case-dir case --markdown node scripts/check_webapi_addon_coverage.js --dir case/result/src/env --json node scripts/check_webapi_addon_coverage.js --file case/result/src/env/install-env.js --markdown 说明:检查补环境代码是否存在普通函数、普通对象、直接复用宿主 Web API、prototype 对象字面量等绕过 addon-first 的写法。该检查用于交付门禁,失败时不得交付最终项目。`; } function exists(filePath) { try { fs.accessSync(filePath); return true; } catch { return false; } } function stat(filePath) { try { return fs.statSync(filePath); } catch { return null; } } function rel(root, filePath) { return (path.relative(root, filePath) || '.').replace(/\\/g, '/'); } function readText(filePath) { return fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''); } function walk(filePath, out = []) { if (!exists(filePath)) return out; const st = stat(filePath); if (!st) return out; if (st.isDirectory()) { let names = []; try { names = fs.readdirSync(filePath); } catch { names = []; } for (const name of names) walk(path.join(filePath, name), out); } else if (st.isFile()) { out.push(filePath); } return out; } function isJsFile(filePath) { return ['.js', '.mjs', '.cjs'].includes(path.extname(filePath).toLowerCase()); } function shouldSkip(root, filePath) { const relative = rel(root, filePath).toLowerCase(); if (/(^|\/)(node_modules|dist|build|coverage|vendor|third_party|third-party)(\/|$)/.test(relative)) return true; if (/(^|\/)src\/target\/(original|vendor|bundle|bundles)(\/|$)/.test(relative)) return true; if (/(\.min\.js|bundle\.js|vendor\.js|package-lock\.json)$/i.test(relative)) return true; return false; } function lineOf(text, index) { return text.slice(0, index).split(/\r?\n/).length; } function lineText(text, line) { const lines = text.split(/\r?\n/); return (lines[line - 1] || '').trim(); } function addIssue(list, severity, file, line, type, message, suggestion, snippet) { list.push({ severity, file, line, type, message, suggestion, snippet: String(snippet || '').slice(0, 220), }); } function targetExpr(name) { return `(?:ctx|window|self|globalThis|globalObject|global)\\s*(?:\\.\\s*${name}|\\[\\s*['"]${name}['"]\\s*\\])`; } function escapedJoin(names) { return names.map(name => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'); } const CONSTRUCTORS = [ 'Blob', 'File', 'FormData', 'Event', 'CustomEvent', 'MessageEvent', 'XMLHttpRequest', 'Worker', 'Image', 'MutationObserver', 'IntersectionObserver', 'ResizeObserver', 'BroadcastChannel', 'MessageChannel', 'MessagePort', 'AudioContext', 'OfflineAudioContext', 'Screen', 'ScreenOrientation', 'URL', 'Headers', 'Request', 'Response', 'WebSocket', 'IDBFactory', 'IDBOpenDBRequest', 'IDBRequest', 'IDBDatabase', 'IDBTransaction', 'IDBObjectStore', 'IDBIndex', 'IDBCursor', 'IDBKeyRange', 'HTMLCanvasElement', 'CanvasRenderingContext2D', 'WebGLRenderingContext', 'WebGL2RenderingContext', 'CSSStyleDeclaration', 'HTMLCollection', 'NodeList', 'PluginArray', 'MimeTypeArray', 'Plugin', 'MimeType', 'DOMTokenList', 'StyleSheetList', ]; const OBJECT_LITERAL_TARGETS = [ 'screen', 'orientation', 'indexedDB', 'IDBKeyRange', 'CSS', 'navigator', 'document', 'location', 'localStorage', 'sessionStorage', 'crypto', 'performance', 'history', 'visualViewport', ]; const WEBAPI_METHODS = [ 'open', 'deleteDatabase', 'cmp', 'databases', 'createObjectURL', 'revokeObjectURL', 'supports', 'escape', 'getContext', 'toDataURL', 'toBlob', 'getImageData', 'putImageData', 'measureText', 'getParameter', 'readPixels', 'getSupportedExtensions', 'getShaderPrecisionFormat', 'startRendering', 'createElement', 'querySelector', 'querySelectorAll', 'getElementById', 'addEventListener', 'removeEventListener', 'dispatchEvent', 'postMessage', 'close', 'send', 'abort', 'setRequestHeader', 'getResponseHeader', 'getAllResponseHeaders', 'item', 'namedItem', ]; const HOST_REUSE = [ 'TextEncoder', 'TextDecoder', 'URL', 'URLSearchParams', 'WebAssembly', 'fetch', 'Headers', 'Request', 'Response', 'Event', 'EventTarget', 'MessageChannel', 'BroadcastChannel', 'crypto', 'ReadableStream', 'WritableStream', 'TransformStream', 'Blob', 'File', 'FormData', ]; const COLLECTION_CONSTRUCTORS = [ 'HTMLCollection', 'NodeList', 'PluginArray', 'MimeTypeArray', 'DOMTokenList', 'StyleSheetList', ]; const HELPER_EVIDENCE = /(?:createNativeFunction|createNativeConstructor|createGetter|createSetter|createNativeGetter|createNativeSetter|defineNativeGetter|defineNativeSetter|defineNativeAccessor|createProtoChains|createNativeObject|createNativeCollection|getMimeTypesAndPlugins|createInterceptor|createUndetectable|setPrivate|getPrivate|hasPrivate|deletePrivate|nativeApi|addon\.|xbs\.|window\.xbs\.|globalThis\.xbs\.)\s*\(/; function scanMatches(text, pattern, onMatch) { pattern.lastIndex = 0; let match; while ((match = pattern.exec(text))) { onMatch(match); if (match[0].length === 0) pattern.lastIndex += 1; } } function hasConstructorAddonEvidence(text, name) { const quoted = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const createNative = new RegExp(`createNativeFunction\\s*\\(\\s*true\\s*,\\s*['"]${quoted}['"]`); const createCtor = new RegExp(`createNativeConstructor\\s*\\(\\s*['"]${quoted}['"]`); const protoChains = new RegExp(`createProtoChains\\s*\\([\\s\\S]{0,1200}name\\s*:\\s*['"]${quoted}['"]`); return createNative.test(text) || createCtor.test(text) || protoChains.test(text); } function inspectFile(root, filePath) { const relative = rel(root, filePath); const text = readText(filePath); const issues = []; const warnings = []; for (const name of CONSTRUCTORS) { const directFunction = new RegExp(`${targetExpr(name)}\\s*=\\s*(?:async\\s*)?function\\b`, 'g'); scanMatches(text, directFunction, match => { const line = lineOf(text, match.index); addIssue( issues, 'error', relative, line, 'webapi-constructor-plain-function', `${name} 被直接赋值为普通 function,绕过了 addon-first 的构造函数与 toString 保护。`, `使用 addon.createProtoChains([{ name: '${name}', ... }]) 或 addon.createNativeFunction(true, '${name}', length, callback) 创建,并设置 prototype.constructor、Symbol.toStringTag、非法构造行为和描述符。`, lineText(text, line) ); }); const classDeclaration = new RegExp(`\\bclass\\s+${name}\\b`, 'g'); scanMatches(text, classDeclaration, match => { const line = lineOf(text, match.index); addIssue( issues, 'error', relative, line, 'webapi-constructor-plain-class', `${name} 使用普通 class 实现,默认不具备浏览器 native-like 构造函数行为。`, `优先使用 addon.createProtoChains 或 addon.createNativeFunction(true, '${name}', ...);addon 不可用时才用 JS fallback,并记录降级原因。`, lineText(text, line) ); }); const identifierAssign = new RegExp(`${targetExpr(name)}\\s*=\\s*${name}\\s*;`, 'g'); scanMatches(text, identifierAssign, match => { if (hasConstructorAddonEvidence(text, name)) return; const line = lineOf(text, match.index); addIssue( warnings, 'warning', relative, line, 'webapi-constructor-no-addon-evidence', `${name} 被挂载到全局,但当前文件没有明显的 createProtoChains / createNativeFunction(true, '${name}') 证据。`, `确认 ${name} 的构造函数来自 addon-first helper;如果由其他模块创建,应在本文件或阶段报告中记录来源,并确保交付前整体运行本检查。`, lineText(text, line) ); }); } for (const name of OBJECT_LITERAL_TARGETS) { const objectLiteral = new RegExp(`${targetExpr(name)}\\s*=\\s*\\{`, 'g'); scanMatches(text, objectLiteral, match => { const line = lineOf(text, match.index); addIssue( issues, 'error', relative, line, 'webapi-instance-plain-object', `${name} 被直接赋值为普通对象,缺少浏览器原型链、实例 toString、descriptor 和 addon-first 证据。`, `为 ${name} 建立对应构造函数和 prototype,例如 Screen / IDBFactory / CSS 等;使用 createProtoChains 创建实例,再用 defineProperty 安装属性和 native-like 方法。`, lineText(text, line) ); }); } const pluginMimeInstall = /\b(?:plugins|mimeTypes)\b/; const installsNavigatorPluginMime = /Object\s*\.\s*defineProperty\s*\(\s*(?:navigator|Navigator\s*\.\s*prototype)[\s\S]{0,260}['"](?:plugins|mimeTypes)['"]|(?:navigator|Navigator\s*\.\s*prototype)\s*\.\s*(?:plugins|mimeTypes)\s*=/.test(text); const hasMimePluginAddon = /\b(?:getMimeTypesAndPlugins|addon\s*\.\s*getMimeTypesAndPlugins|nativeApi\s*\.\s*getMimeTypesAndPlugins|xbs\s*\.\s*getMimeTypesAndPlugins|window\s*\.\s*xbs\s*\.\s*getMimeTypesAndPlugins|globalThis\s*\.\s*xbs\s*\.\s*getMimeTypesAndPlugins)\s*\(/.test(text); if (pluginMimeInstall && installsNavigatorPluginMime && !hasMimePluginAddon) { const index = text.search(/(?:plugins|mimeTypes)/); const line = lineOf(text, index >= 0 ? index : 0); addIssue( issues, 'error', relative, line, 'navigator-plugins-mimetypes-without-addon', '发现 navigator.plugins / navigator.mimeTypes 安装逻辑,但未发现 getMimeTypesAndPlugins addon-first 证据。', 'addon.node 或 xbs isolated-vm 可用时必须优先使用 addon.getMimeTypesAndPlugins(config) / xbs.getMimeTypesAndPlugins(config) 或 addon-first helper;真实浏览器插件数据不同则传入 config。只有 native 能力不可用或用户明确禁用时才允许 JS fallback,并记录原因。', lineText(text, line) ); } const pluginMimePlainArray = /(?:plugins|mimeTypes)\s*[:=]\s*\[\s*\]/g; scanMatches(text, pluginMimePlainArray, match => { const nearby = text.slice(Math.max(0, match.index - 240), Math.min(text.length, match.index + 240)); if (/fallback|降级|addon 不可用|addon不可用|getMimeTypesAndPlugins/.test(nearby)) return; const line = lineOf(text, match.index); addIssue( issues, 'error', relative, line, 'navigator-plugins-mimetypes-plain-array', 'plugins / mimeTypes 被实现为普通数组,缺少 PluginArray / MimeTypeArray 原型、item / namedItem、索引访问和 native-like 行为。', '使用 addon.getMimeTypesAndPlugins(config) 或 xbs.getMimeTypesAndPlugins(config);native 能力不可用时至少通过 createNativeCollection fallback 并记录差异。', lineText(text, line) ); }); for (const name of COLLECTION_CONSTRUCTORS) { const mentionsCollection = new RegExp(`\\b${name}\\b`).test(text); if (!mentionsCollection) continue; const fromMimePluginApi = ['PluginArray', 'MimeTypeArray', 'Plugin', 'MimeType'].includes(name) && /getMimeTypesAndPlugins\s*\(/.test(text); const hasCollectionAddon = new RegExp(`createNativeCollection\\s*\\([\\s\\S]{0,800}name\\s*:\\s*['"]${name}['"]`).test(text) || fromMimePluginApi; const plainCollection = new RegExp(`(?:class\\s+${name}\\b|function\\s+${name}\\s*\\(|${targetExpr(name)}\\s*=\\s*(?:function|class|\\{)|\\b${name}\\s*=\\s*\\[)`, 'g'); scanMatches(text, plainCollection, match => { if (hasCollectionAddon) return; const line = lineOf(text, match.index); addIssue( issues, 'error', relative, line, 'webapi-collection-without-createNativeCollection', `${name} 使用普通 JS 结构实现,未体现 createNativeCollection / getMimeTypesAndPlugins addon-first。`, `浏览器集合对象 ${name} 应优先使用 addon.createNativeCollection({ name: '${name}', ... });PluginArray / MimeTypeArray 应优先由 addon.getMimeTypesAndPlugins(config) 返回。`, lineText(text, line) ); }); } const methodNames = escapedJoin(WEBAPI_METHODS); const methodAssign = new RegExp(`(?:\\.|['"])(${methodNames})(?:['"])?\\s*(?:=|:)\\s*(?:async\\s*)?(?:function\\b|\\([^)]*\\)\\s*=>|[A-Za-z_$][\\w$]*\\s*\\([^)]*\\)\\s*\\{)`, 'g'); scanMatches(text, methodAssign, match => { const index = match.index; const line = lineOf(text, index); const snippet = lineText(text, line); if (HELPER_EVIDENCE.test(snippet)) return; if (/^\s*\/\/|^\s*\*/.test(snippet)) return; addIssue( issues, 'error', relative, line, 'webapi-method-plain-function', `WebAPI 方法 ${match[1]} 使用普通函数或对象字面量方法,未体现 addon-first/native-like 创建。`, `将 ${match[1]} 提取为具名函数,并通过 addon.createNativeFunction(false, '${match[1]}', length, callback) 或 addon-first helper 安装到正确 prototype / 对象描述符上。`, snippet ); }); const urlStatic = /(?:ctx|window|self|globalThis|globalObject|global)\s*\.\s*URL\s*\.\s*(createObjectURL|revokeObjectURL)\s*=\s*(?:async\s*)?function\b/g; scanMatches(text, urlStatic, match => { const line = lineOf(text, match.index); addIssue( issues, 'error', relative, line, 'url-static-method-plain-function', `URL.${match[1]} 被直接赋值为普通 function。`, `使用 addon.createNativeFunction(false, '${match[1]}', ...) 创建,并用 Object.defineProperty 安装 descriptor。`, lineText(text, line) ); }); const prototypeLiteral = /\b(?:[A-Z][A-Za-z0-9_$]*\s*\.\s*)?prototype\s*=\s*\{/g; scanMatches(text, prototypeLiteral, match => { const line = lineOf(text, match.index); addIssue( issues, 'error', relative, line, 'prototype-object-literal', '发现 prototype 直接替换为对象字面量,容易丢失 constructor、属性描述符、Symbol.toStringTag 和 native-like 方法。', '使用 addon.createProtoChains 建立原型链,或至少 Object.defineProperties 安装 prototype.constructor、Symbol.toStringTag 和每个 addon-first 方法。', lineText(text, line) ); }); const hostNames = escapedJoin(HOST_REUSE); const hostReuse = new RegExp(`(?:ctx|window|self|globalThis|globalObject|global)\\s*(?:\\.\\s*(${hostNames})|\\[\\s*['"](${hostNames})['"]\\s*\\])\\s*=\\s*(?:globalThis|global|window)\\s*\\.\\s*(?:\\1|\\2)`, 'g'); scanMatches(text, hostReuse, match => { const name = match[1] || match[2]; const line = lineOf(text, match.index); addIssue( issues, 'error', relative, line, 'host-webapi-direct-reuse', `${name} 直接复用宿主同名 Web API,可能泄露 Node.js / undici / 宿主实现特征。`, `不要盲目透传宿主 ${name};按浏览器样本、RuyiTrace 证据和 addon-first helper 创建可控实现。`, lineText(text, line) ); }); const atobBtoa = /\b(?:atob|btoa)\s*=\s*(?:async\s*)?function[\s\S]{0,240}\bBuffer\s*\.\s*from\b/g; scanMatches(text, atobBtoa, match => { const line = lineOf(text, match.index); addIssue( issues, 'error', relative, line, 'buffer-backed-atob-btoa', 'atob / btoa 使用 Buffer.from 实现,可能暴露 Node.js 语义与错误信息差异。', '为 atob / btoa 使用浏览器样本校验过的实现,并通过 addon.createNativeFunction 或 addon-first helper 做 native-like 包装;不得在目标上下文暴露 Buffer。', lineText(text, line) ); }); const objectAssignWebApi = /Object\s*\.\s*assign\s*\([^)]*\{[\s\S]{0,1200}\b(?:history|indexedDB|CSS|screen)\s*:\s*\{[\s\S]{0,500}\b[A-Za-z_$][\w$]*\s*\([^)]*\)\s*\{/g; scanMatches(text, objectAssignWebApi, match => { const line = lineOf(text, match.index); addIssue( issues, 'error', relative, line, 'object-assign-webapi-methods', 'Object.assign 中发现 WebAPI 对象字面量和普通方法,绕过 descriptor、原型链与 addon-first。', '拆分为对应模块:先 createProtoChains 创建构造函数和实例,再用 Object.defineProperty 安装 addon native-like 方法。', lineText(text, line) ); }); const genericIllegalConstructor = /throw\s+new\s+TypeError\s*\(\s*['"]Illegal constructor['"]\s*\)/g; scanMatches(text, genericIllegalConstructor, match => { const line = lineOf(text, match.index); addIssue( issues, 'error', relative, line, 'constructor-error-generic-message', '构造函数报错信息使用了泛化的 Illegal constructor,未证明与目标浏览器的错误类型和错误信息一致。', '先用已确认取证浏览器采样直接调用和 new 调用的错误:error.name、constructor.name、message、stack 首行;再用 addon.throwTypeError 或 throwBrowserTypeError 复现对应 TypeError / DOMException 等类型和 message。', lineText(text, line) ); }); const nonTypeErrorIllegalConstructor = /throw\s+new\s+(?!TypeError\b)([A-Za-z_$][\w$]*)\s*\(\s*['"][^'"]*(?:Illegal constructor|Please use the 'new' operator|constructor cannot be called)/g; scanMatches(text, nonTypeErrorIllegalConstructor, match => { const line = lineOf(text, match.index); addIssue( issues, 'error', relative, line, 'constructor-error-type-mismatch-risk', `构造函数相关报错使用 ${match[1]},存在与浏览器 TypeError / DOMException 类型不一致的风险。`, '按浏览器样本确认错误类型;多数 DOM 构造函数错误应为 TypeError,但必须以目标浏览器采样为准。', lineText(text, line) ); }); const hardcodedConstructorMessage = /Failed to construct ['"][^'"]+['"]:\s*(?:Illegal constructor|Please use the ['"]new['"] operator)/g; const hasConstructorErrorProfile = /constructorErrors|constructor-errors\.fixture|构造函数报错样本|browserErrorProfile|errorProfile/.test(text); scanMatches(text, hardcodedConstructorMessage, match => { if (hasConstructorErrorProfile) return; const line = lineOf(text, match.index); addIssue( warnings, 'warning', relative, line, 'constructor-error-profile-missing', '源码硬编码了浏览器构造函数错误信息,但未发现构造函数报错样本或 error profile 证据。', '建议保存 constructor-errors fixture 或在阶段报告中记录目标浏览器采样结果,避免把看似合理的报错信息误当成浏览器一致。', lineText(text, line) ); }); const emptyNativeConstructor = /createNativeConstructor\s*\(\s*['"]([A-Z][A-Za-z0-9_$]*)['"]\s*,\s*\d+\s*,\s*function(?:\s+[A-Za-z_$][\w$]*)?\s*\([^)]*\)\s*\{\s*\}/g; scanMatches(text, emptyNativeConstructor, match => { const line = lineOf(text, match.index); addIssue( issues, 'error', relative, line, 'constructor-empty-behavior', `${match[1]} 构造函数 callback 为空,未体现直接调用 / new 调用行为、非法构造报错或实例初始化。`, '按浏览器采样补齐构造函数行为:可构造对象要初始化实例状态;不可构造对象要复现错误类型和 message。', lineText(text, line) ); }); const emptyAddonConstructor = /createNativeFunction\s*\(\s*true\s*,\s*['"]([A-Z][A-Za-z0-9_$]*)['"]\s*,\s*\d+\s*,\s*function(?:\s+[A-Za-z_$][\w$]*)?\s*\([^)]*\)\s*\{\s*\}/g; scanMatches(text, emptyAddonConstructor, match => { const line = lineOf(text, match.index); addIssue( issues, 'error', relative, line, 'constructor-empty-behavior', `${match[1]} addon 构造函数 callback 为空,无法证明构造行为与浏览器一致。`, '按浏览器采样实现 new / call 分支、错误类型和 message,或通过实例工厂初始化可构造对象状态。', lineText(text, line) ); }); const unknownMarkObjectType = /\b(?:native|nativeApi|protect)?\.?\s*markObjectType\s*\(/g; scanMatches(text, unknownMarkObjectType, match => { const line = lineOf(text, match.index); addIssue( issues, 'error', relative, line, 'unknown-mark-object-type', '发现 markObjectType 调用;该名称不是本 Skill 批准的 addon-first API,语义不明确。', '如果对象由 addon.createProtoChains / addon 构造函数实例工厂创建,不要额外标记对象类型;addon 已负责实例 Object.prototype.toString。只有 JS fallback 对象才允许使用 Symbol.toStringTag / markObjectToString,并记录 fallback 原因。', lineText(text, line) ); }); const markObjectToStringCall = /\b(?:native|nativeApi|protect)\s*\.\s*markObjectToString\s*\(/g; scanMatches(text, markObjectToStringCall, match => { const nearby = text.slice(Math.max(0, match.index - 240), Math.min(text.length, match.index + 240)); if (/fallback|降级|addon 不可用|addon不可用|JS fallback|NativeProtect/.test(nearby)) return; const line = lineOf(text, match.index); addIssue( warnings, 'warning', relative, line, 'mark-object-to-string-without-fallback-reason', '发现 markObjectToString 调用但附近没有 fallback 原因;它只能作为 addon 不可用时的 JS fallback。', '优先用 addon 构造函数或 createProtoChains 实例工厂创建对象。addon 已创建的实例不应再用 markObjectToString 叠加伪装;只有普通 JS fallback 对象才需要该函数。', lineText(text, line) ); }); const hasPlainWebApiPattern = issues.some(issue => issue.file === relative); const usesAnyNativeHelper = /createNativeFunction|createGetter|createSetter|createProtoChains|createNativeCollection|getMimeTypesAndPlugins|createInterceptor|createUndetectable|setPrivate|getPrivate|hasPrivate|deletePrivate|loadNativeAddon|WEB_JS_ENV_PATCHER_ADDON|WEB_JS_ENV_PATCHER_XBS_ISOLATED_VM|addon\.node|isolated_vm\.node|xbs-isolated-vm|xbs\.|window\.xbs|globalThis\.xbs/.test(text); if (hasPlainWebApiPattern && !usesAnyNativeHelper) { addIssue( warnings, 'warning', relative, 1, 'file-no-native-helper', '文件中存在 WebAPI 普通实现问题,且未发现任何 addon-first / native helper 证据。', '在模块初始化阶段注入 nativeApi / addon,并统一通过 helper 创建函数、访问器、构造函数和实例对象。', '' ); } return { issues, warnings }; } function resolveInputs(args) { if (args.file) { const file = path.resolve(args.file); return { root: path.dirname(file), files: [file] }; } if (args.dir) { const root = path.resolve(args.dir); return { root, files: walk(root).filter(file => isJsFile(file) && !shouldSkip(root, file)) }; } if (args.caseDir) { const caseDir = path.resolve(args.caseDir); const preferred = path.join(caseDir, 'result'); const root = exists(preferred) ? preferred : caseDir; return { root, files: walk(root).filter(file => isJsFile(file) && !shouldSkip(root, file)) }; } throw new Error('必须指定 --case-dir、--dir 或 --file。'); } function renderMarkdown(result) { const lines = []; lines.push('# WebAPI addon 覆盖检查报告'); lines.push(''); lines.push(`- 检查结果:${result.passed ? '通过' : '不通过'}`); lines.push(`- 扫描文件数:${result.scannedFiles.length}`); lines.push(`- 错误数:${result.issueCount}`); lines.push(`- 警告数:${result.warningCount}`); lines.push(''); if (result.issues.length) { lines.push('## 错误'); lines.push(''); lines.push('| 文件 | 行号 | 类型 | 问题 | 修复建议 |'); lines.push('|---|---:|---|---|---|'); for (const issue of result.issues) { lines.push(`| ${issue.file} | ${issue.line || ''} | ${issue.type} | ${issue.message} | ${issue.suggestion} |`); } lines.push(''); } if (result.warnings.length) { lines.push('## 警告'); lines.push(''); lines.push('| 文件 | 行号 | 类型 | 提醒 | 建议 |'); lines.push('|---|---:|---|---|---|'); for (const warning of result.warnings) { lines.push(`| ${warning.file} | ${warning.line || ''} | ${warning.type} | ${warning.message} | ${warning.suggestion} |`); } lines.push(''); } if (!result.issues.length && !result.warnings.length) { lines.push('未发现 WebAPI 普通函数、普通对象、宿主透传或 prototype 对象字面量等 addon-first 覆盖问题。'); lines.push(''); } lines.push('## 交付门禁说明'); lines.push(''); lines.push('- 该检查失败时不得交付最终补环境项目。'); lines.push('- 修复方向不是“删除检查”,而是把 WebAPI 构造函数、普通方法、getter、setter、实例对象、集合对象、plugins/mimeTypes 和特殊对象统一迁移到 addon-first helper。'); lines.push('- addon.node / xbs native API 不可用时才允许 NativeProtect / JS fallback,并必须在阶段报告与最终总结中记录原因。'); return lines.join('\n'); } function main() { const args = parseArgs(process.argv); if (args.help) { console.log(usage()); return; } const { root, files } = resolveInputs(args); const jsFiles = files.filter(file => exists(file) && isJsFile(file)); const issues = []; const warnings = []; for (const file of jsFiles) { const inspected = inspectFile(root, file); issues.push(...inspected.issues); warnings.push(...inspected.warnings); } const warningCount = warnings.length; const issueCount = issues.length + (args.warningsAsErrors ? warningCount : 0); const result = { passed: issueCount === 0, root, scannedFiles: jsFiles.map(file => rel(root, file)), issueCount, warningCount, warningsAsErrors: args.warningsAsErrors, issues, warnings, }; if (args.json) console.log(JSON.stringify(result, null, 2)); else console.log(renderMarkdown(result)); if (!result.passed) process.exitCode = 1; } try { main(); } catch (err) { console.error(err && err.message ? err.message : String(err)); process.exitCode = 2; }