mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
build: Add lint rule to ban argument spreads to prevent stack overflow (#17493)
This commit is contained in:
@@ -5,16 +5,16 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"exports": {
|
"exports": {
|
||||||
"./base": {
|
"./base": {
|
||||||
"default": "./dist/configs/base.js",
|
"types": "./dist/configs/base.d.js",
|
||||||
"types": "./dist/configs/base.d.js"
|
"default": "./dist/configs/base.js"
|
||||||
},
|
},
|
||||||
"./frontend": {
|
"./frontend": {
|
||||||
"default": "./dist/configs/frontend.js",
|
"types": "./dist/configs/frontend.d.js",
|
||||||
"types": "./dist/configs/frontend.d.js"
|
"default": "./dist/configs/frontend.js"
|
||||||
},
|
},
|
||||||
"./node": {
|
"./node": {
|
||||||
"default": "./dist/configs/node.js",
|
"types": "./dist/configs/node.d.js",
|
||||||
"types": "./dist/configs/node.d.js"
|
"default": "./dist/configs/node.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -369,7 +369,7 @@ export const baseConfig = tseslint.config(
|
|||||||
/**
|
/**
|
||||||
* https://eslint.org/docs/rules/prefer-spread
|
* https://eslint.org/docs/rules/prefer-spread
|
||||||
*/
|
*/
|
||||||
'prefer-spread': 'error',
|
'prefer-spread': 'off',
|
||||||
|
|
||||||
// These are tuned off since we use `noUnusedLocals` and `noUnusedParameters` now
|
// These are tuned off since we use `noUnusedLocals` and `noUnusedParameters` now
|
||||||
'no-unused-vars': 'off',
|
'no-unused-vars': 'off',
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const localRulesPlugin = {
|
|||||||
'n8n-local-rules/no-interpolation-in-regular-string': 'error',
|
'n8n-local-rules/no-interpolation-in-regular-string': 'error',
|
||||||
'n8n-local-rules/no-unused-param-in-catch-clause': 'error',
|
'n8n-local-rules/no-unused-param-in-catch-clause': 'error',
|
||||||
'n8n-local-rules/no-useless-catch-throw': 'error',
|
'n8n-local-rules/no-useless-catch-throw': 'error',
|
||||||
|
'n8n-local-rules/no-argument-spread': 'warn', // TODO: mark error
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { NoUntypedConfigClassFieldRule } from './no-untyped-config-class-field.j
|
|||||||
import { NoTopLevelRelativeImportsInBackendModuleRule } from './no-top-level-relative-imports-in-backend-module.js';
|
import { NoTopLevelRelativeImportsInBackendModuleRule } from './no-top-level-relative-imports-in-backend-module.js';
|
||||||
import { NoConstructorInBackendModuleRule } from './no-constructor-in-backend-module.js';
|
import { NoConstructorInBackendModuleRule } from './no-constructor-in-backend-module.js';
|
||||||
import type { AnyRuleModule } from '@typescript-eslint/utils/ts-eslint';
|
import type { AnyRuleModule } from '@typescript-eslint/utils/ts-eslint';
|
||||||
|
import { NoArgumentSpreadRule } from './no-argument-spread.js';
|
||||||
|
|
||||||
export const rules = {
|
export const rules = {
|
||||||
'no-uncaught-json-parse': NoUncaughtJsonParseRule,
|
'no-uncaught-json-parse': NoUncaughtJsonParseRule,
|
||||||
@@ -29,4 +30,5 @@ export const rules = {
|
|||||||
'no-untyped-config-class-field': NoUntypedConfigClassFieldRule,
|
'no-untyped-config-class-field': NoUntypedConfigClassFieldRule,
|
||||||
'no-top-level-relative-imports-in-backend-module': NoTopLevelRelativeImportsInBackendModuleRule,
|
'no-top-level-relative-imports-in-backend-module': NoTopLevelRelativeImportsInBackendModuleRule,
|
||||||
'no-constructor-in-backend-module': NoConstructorInBackendModuleRule,
|
'no-constructor-in-backend-module': NoConstructorInBackendModuleRule,
|
||||||
|
'no-argument-spread': NoArgumentSpreadRule,
|
||||||
} satisfies Record<string, AnyRuleModule>;
|
} satisfies Record<string, AnyRuleModule>;
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { RuleTester } from '@typescript-eslint/rule-tester';
|
||||||
|
import { NoArgumentSpreadRule } from './no-argument-spread.js';
|
||||||
|
|
||||||
|
const ruleTester = new RuleTester();
|
||||||
|
|
||||||
|
ruleTester.run('no-unbounded-argument-spread', NoArgumentSpreadRule, {
|
||||||
|
valid: [
|
||||||
|
{ code: 'fn(1, 2, 3)' },
|
||||||
|
{ code: 'fn(...[1, 2, 3])' },
|
||||||
|
{ code: 'new Foo(...[1, 2])' },
|
||||||
|
{ code: 'fn.apply(null, deps)' },
|
||||||
|
{ code: 'Reflect.construct(Foo, deps)' },
|
||||||
|
],
|
||||||
|
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
code: 'fn(...deps)',
|
||||||
|
output: 'fn.apply(undefined, deps)',
|
||||||
|
errors: [{ messageId: 'replaceWithApply' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'obj.fn(...deps)',
|
||||||
|
output: 'obj.fn.apply(obj, deps)',
|
||||||
|
errors: [{ messageId: 'replaceWithApply' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'instance = metadata.factory(...dependencies);',
|
||||||
|
output: 'instance = metadata.factory.apply(metadata, dependencies);',
|
||||||
|
errors: [{ messageId: 'replaceWithApply' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'new Foo(...deps)',
|
||||||
|
output: 'Reflect.construct(Foo, deps)',
|
||||||
|
errors: [{ messageId: 'replaceWithReflect' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'someFunction(a, ...deps)',
|
||||||
|
output: null, // multiple args — no fix
|
||||||
|
errors: [{ messageId: 'replaceWithApply' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'new Bar(a, ...deps)',
|
||||||
|
output: null,
|
||||||
|
errors: [{ messageId: 'replaceWithReflect' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
87
packages/@n8n/eslint-config/src/rules/no-argument-spread.ts
Normal file
87
packages/@n8n/eslint-config/src/rules/no-argument-spread.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { ESLintUtils } from '@typescript-eslint/utils';
|
||||||
|
|
||||||
|
export const NoArgumentSpreadRule = ESLintUtils.RuleCreator.withoutDocs({
|
||||||
|
meta: {
|
||||||
|
type: 'problem',
|
||||||
|
docs: {
|
||||||
|
description:
|
||||||
|
'Avoid spreading potentially large arrays in function or constructor calls — can cause stack overflows. Use `.apply` or `Reflect.construct` instead.',
|
||||||
|
},
|
||||||
|
fixable: 'code',
|
||||||
|
messages: {
|
||||||
|
noUnboundedSpread:
|
||||||
|
'Avoid spreading an array in function or constructor calls unless known to be small.',
|
||||||
|
replaceWithApply:
|
||||||
|
'Replace `array.push(...largeArray)` with `array.push.apply(array, largeArray)` to avoid potential stack overflows.',
|
||||||
|
replaceWithReflect:
|
||||||
|
'Replace `new Constructor(...args)` with `Reflect.construct(Constructor, args)` to avoid potential stack overflows.',
|
||||||
|
},
|
||||||
|
schema: [],
|
||||||
|
},
|
||||||
|
defaultOptions: [],
|
||||||
|
create(context) {
|
||||||
|
return {
|
||||||
|
CallExpression(node) {
|
||||||
|
for (const arg of node.arguments) {
|
||||||
|
if (arg.type !== 'SpreadElement') continue;
|
||||||
|
|
||||||
|
const spreadArg = arg.argument;
|
||||||
|
|
||||||
|
// Allow spread of inline arrays
|
||||||
|
if (spreadArg.type === 'ArrayExpression') return;
|
||||||
|
|
||||||
|
// Only autofix if it's the sole argument
|
||||||
|
const canFix = node.arguments.length === 1;
|
||||||
|
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'replaceWithApply',
|
||||||
|
fix: canFix
|
||||||
|
? (fixer) => {
|
||||||
|
const source = context.sourceCode;
|
||||||
|
|
||||||
|
if (node.callee.type === 'MemberExpression') {
|
||||||
|
// Preserve `this`
|
||||||
|
const thisText = source.getText(node.callee.object);
|
||||||
|
const calleeText = source.getText(node.callee);
|
||||||
|
const argText = source.getText(spreadArg);
|
||||||
|
return fixer.replaceText(node, `${calleeText}.apply(${thisText}, ${argText})`);
|
||||||
|
} else {
|
||||||
|
// Not a memberexpression, use undefined as thisArg
|
||||||
|
const calleeText = source.getText(node.callee);
|
||||||
|
const argText = source.getText(spreadArg);
|
||||||
|
return fixer.replaceText(node, `${calleeText}.apply(undefined, ${argText})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
NewExpression(node) {
|
||||||
|
for (const arg of node.arguments || []) {
|
||||||
|
if (arg.type !== 'SpreadElement') continue;
|
||||||
|
|
||||||
|
const spreadArg = arg.argument;
|
||||||
|
|
||||||
|
if (spreadArg.type === 'ArrayExpression') return;
|
||||||
|
|
||||||
|
const canFix = node.arguments.length === 1;
|
||||||
|
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'replaceWithReflect',
|
||||||
|
fix: canFix
|
||||||
|
? (fixer) => {
|
||||||
|
const source = context.sourceCode;
|
||||||
|
const ctorText = source.getText(node.callee);
|
||||||
|
const argText = source.getText(spreadArg);
|
||||||
|
return fixer.replaceText(node, `Reflect.construct(${ctorText}, ${argText})`);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user