mirror of
https://github.com/electron/electron.git
synced 2026-05-02 03:00:22 -04:00
lint: add rule banning foreign imports in itremote/remotely closures
remote-tools-imports/no-foreign-imports-in-remote-closure flags any identifier inside an itremote()/remotely() closure that is bound to an import from somewhere other than spec/lib/remote-tools. Those bindings get rewritten to __vite_ssr_import_N__ by vite's SSR transform and fail when the closure is stringified and eval'd in a remote context. Skips type-only imports and type-annotation positions. Currently flags 50 sites across 5 files (node, webview, chromium, fuses, api-native-image).
This commit is contained in:
@@ -11,6 +11,10 @@
|
||||
{
|
||||
"name": "no-only-tests",
|
||||
"specifier": "./script/lint-plugins/no-only-tests.mjs"
|
||||
},
|
||||
{
|
||||
"name": "remote-tools-imports",
|
||||
"specifier": "./script/lint-plugins/remote-tools-imports.mjs"
|
||||
}
|
||||
],
|
||||
"categories": {
|
||||
@@ -315,7 +319,8 @@
|
||||
{
|
||||
"files": ["spec/**/*.ts", "spec/**/*.js", "spec/**/*.mjs"],
|
||||
"rules": {
|
||||
"no-only-tests/no-only-tests": "error"
|
||||
"no-only-tests/no-only-tests": "error",
|
||||
"remote-tools-imports/no-foreign-imports-in-remote-closure": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
154
script/lint-plugins/remote-tools-imports.mjs
Normal file
154
script/lint-plugins/remote-tools-imports.mjs
Normal file
@@ -0,0 +1,154 @@
|
||||
// Flags imports (other than from spec/lib/remote-tools) that are referenced
|
||||
// inside itremote()/remotely() closures. vite's SSR transform rewrites import
|
||||
// bindings to __vite_ssr_import_N__, which breaks when the closure is
|
||||
// stringified and eval'd in a renderer/child process. Importing from
|
||||
// remote-tools instead lets runRemote()'s __rt shim resolve them.
|
||||
|
||||
const REMOTE_TOOLS_RE = /[./]lib\/remote-tools(\.ts)?$/;
|
||||
|
||||
function isRemoteCall(callee) {
|
||||
// itremote(name, fn), itremote.only(name, fn), itremote.skip(name, fn)
|
||||
if (callee.type === 'Identifier' && callee.name === 'itremote') return { fnArg: 1 };
|
||||
if (
|
||||
callee.type === 'MemberExpression' &&
|
||||
callee.object.type === 'Identifier' &&
|
||||
callee.object.name === 'itremote' &&
|
||||
callee.property.type === 'Identifier' &&
|
||||
(callee.property.name === 'only' || callee.property.name === 'skip')
|
||||
) {
|
||||
return { fnArg: 1 };
|
||||
}
|
||||
// <anything>.remotely(fn, ...args)
|
||||
if (
|
||||
callee.type === 'MemberExpression' &&
|
||||
callee.property.type === 'Identifier' &&
|
||||
callee.property.name === 'remotely'
|
||||
) {
|
||||
return { fnArg: 0 };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function collectDeclared(node, declared) {
|
||||
if (!node) return;
|
||||
if (node.type === 'Identifier') {
|
||||
declared.add(node.name);
|
||||
} else if (node.type === 'ObjectPattern') {
|
||||
for (const p of node.properties) collectDeclared(p.value ?? p.argument, declared);
|
||||
} else if (node.type === 'ArrayPattern') {
|
||||
for (const e of node.elements) collectDeclared(e, declared);
|
||||
} else if (node.type === 'RestElement') {
|
||||
collectDeclared(node.argument, declared);
|
||||
} else if (node.type === 'AssignmentPattern') {
|
||||
collectDeclared(node.left, declared);
|
||||
}
|
||||
}
|
||||
|
||||
function walkClosure(node, onIdentifierRef, declared) {
|
||||
if (!node || typeof node !== 'object') return;
|
||||
switch (node.type) {
|
||||
case 'Identifier':
|
||||
if (!declared.has(node.name)) onIdentifierRef(node);
|
||||
return;
|
||||
case 'MemberExpression':
|
||||
walkClosure(node.object, onIdentifierRef, declared);
|
||||
if (node.computed) walkClosure(node.property, onIdentifierRef, declared);
|
||||
return;
|
||||
case 'Property':
|
||||
if (node.computed) walkClosure(node.key, onIdentifierRef, declared);
|
||||
walkClosure(node.value, onIdentifierRef, declared);
|
||||
return;
|
||||
case 'VariableDeclarator':
|
||||
collectDeclared(node.id, declared);
|
||||
walkClosure(node.init, onIdentifierRef, declared);
|
||||
return;
|
||||
case 'FunctionDeclaration':
|
||||
case 'FunctionExpression':
|
||||
case 'ArrowFunctionExpression': {
|
||||
const inner = new Set(declared);
|
||||
if (node.id) inner.add(node.id.name);
|
||||
for (const p of node.params) collectDeclared(p, inner);
|
||||
walkClosure(node.body, onIdentifierRef, inner);
|
||||
return;
|
||||
}
|
||||
case 'CatchClause': {
|
||||
const inner = new Set(declared);
|
||||
collectDeclared(node.param, inner);
|
||||
walkClosure(node.body, onIdentifierRef, inner);
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const key of Object.keys(node)) {
|
||||
if (
|
||||
key === 'type' ||
|
||||
key === 'loc' ||
|
||||
key === 'range' ||
|
||||
key === 'start' ||
|
||||
key === 'end' ||
|
||||
key === 'parent' ||
|
||||
key === 'typeAnnotation' ||
|
||||
key === 'typeParameters' ||
|
||||
key === 'returnType'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const child = node[key];
|
||||
if (Array.isArray(child)) {
|
||||
for (const c of child) walkClosure(c, onIdentifierRef, declared);
|
||||
} else if (child && typeof child === 'object' && typeof child.type === 'string') {
|
||||
walkClosure(child, onIdentifierRef, declared);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
meta: { name: 'remote-tools-imports' },
|
||||
rules: {
|
||||
'no-foreign-imports-in-remote-closure': {
|
||||
meta: { type: 'problem' },
|
||||
create(context) {
|
||||
// Map of import-binding name -> { source, node } for everything NOT
|
||||
// from remote-tools.
|
||||
const foreignImports = new Map();
|
||||
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const source = node.source.value;
|
||||
if (REMOTE_TOOLS_RE.test(source)) return;
|
||||
if (node.importKind === 'type') return;
|
||||
for (const spec of node.specifiers) {
|
||||
if (spec.importKind === 'type') continue;
|
||||
foreignImports.set(spec.local.name, { source, node: spec.local });
|
||||
}
|
||||
},
|
||||
CallExpression(node) {
|
||||
const match = isRemoteCall(node.callee);
|
||||
if (!match) return;
|
||||
const fnArg = node.arguments[match.fnArg];
|
||||
if (!fnArg || (fnArg.type !== 'FunctionExpression' && fnArg.type !== 'ArrowFunctionExpression')) {
|
||||
return;
|
||||
}
|
||||
const reported = new Set();
|
||||
walkClosure(
|
||||
fnArg,
|
||||
(id) => {
|
||||
const hit = foreignImports.get(id.name);
|
||||
if (hit && !reported.has(id.name)) {
|
||||
reported.add(id.name);
|
||||
context.report({
|
||||
node: id,
|
||||
message:
|
||||
`'${id.name}' is imported from '${hit.source}' but used inside a ` +
|
||||
`stringified remote closure. Import it from './lib/remote-tools' ` +
|
||||
`instead (add it there if missing).`
|
||||
});
|
||||
}
|
||||
},
|
||||
new Set()
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -13,8 +13,9 @@ export * as util from 'node:util';
|
||||
export * as os from 'node:os';
|
||||
export * as cp from 'node:child_process';
|
||||
export { once } from 'node:events';
|
||||
export { setTimeout as delay } from 'node:timers/promises';
|
||||
export { setTimeout } from 'node:timers/promises';
|
||||
export { expect } from 'chai';
|
||||
export { BrowserWindow, nativeImage, webContents } from 'electron/main';
|
||||
|
||||
// Renderer-side mirror of the exports above. Keep the keys in sync.
|
||||
export const REMOTE_TOOLS_SHIM = `{
|
||||
@@ -25,8 +26,11 @@ export const REMOTE_TOOLS_SHIM = `{
|
||||
os: require('node:os'),
|
||||
cp: require('node:child_process'),
|
||||
once: require('node:events').once,
|
||||
delay: require('node:timers/promises').setTimeout,
|
||||
setTimeout: require('node:timers/promises').setTimeout,
|
||||
expect: require('chai').expect,
|
||||
BrowserWindow: require('electron').BrowserWindow,
|
||||
nativeImage: require('electron').nativeImage,
|
||||
webContents: require('electron').webContents,
|
||||
}`;
|
||||
|
||||
const SSR_IMPORT_RE = /__vite_ssr_import_\d+__/g;
|
||||
|
||||
Reference in New Issue
Block a user