reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
@@ -0,0 +1,738 @@
|
||||
#!/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;
|
||||
}
|
||||
Reference in New Issue
Block a user