Skip to content

Commit

Permalink
perf(editor): Add lint rules for optimization-friendly syntax (#11681)
Browse files Browse the repository at this point in the history
  • Loading branch information
ivov authored Nov 12, 2024
1 parent 7b20c1e commit 88295c7
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 3 deletions.
74 changes: 72 additions & 2 deletions packages/editor-ui/src/components/CodeNodeEditor/linter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,9 @@ export const useLinter = (
message: i18n.baseText('codeNodeEditor.linter.allItems.unavailableProperty'),
actions: [
{
name: 'Remove',
name: 'Fix',
apply(view) {
view.dispatch({ changes: { from: start - '.'.length, to: end } });
view.dispatch({ changes: { from: start, to: end, insert: 'first()' } });
},
},
],
Expand Down Expand Up @@ -559,6 +559,76 @@ export const useLinter = (
});
});

/**
* Lint for `$(variable)` usage where variable is not a string, in both modes.
*
* $(nodeName) -> <no autofix>
*/
const isDollarSignWithVariable = (node: Node) =>
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === '$' &&
node.arguments.length === 1 &&
((node.arguments[0].type !== 'Literal' && node.arguments[0].type !== 'TemplateLiteral') ||
(node.arguments[0].type === 'TemplateLiteral' && node.arguments[0].expressions.length > 0));

type TargetCallNode = RangeNode & {
callee: { name: string };
arguments: Array<{ type: string }>;
};

walk<TargetCallNode>(ast, isDollarSignWithVariable).forEach((node) => {
const [start, end] = getRange(node);

lintings.push({
from: start,
to: end,
severity: 'warning',
message: i18n.baseText('codeNodeEditor.linter.bothModes.dollarSignVariable'),
});
});

/**
* Lint for $("myNode").item access in runOnceForAllItems mode
*
* $("myNode").item -> $("myNode").first()
*/
if (toValue(mode) === 'runOnceForEachItem') {
type DollarItemNode = RangeNode & {
property: { name: string; type: string } & RangeNode;
};

const isDollarNodeItemAccess = (node: Node) =>
node.type === 'MemberExpression' &&
!node.computed &&
node.object.type === 'CallExpression' &&
node.object.callee.type === 'Identifier' &&
node.object.callee.name === '$' &&
node.object.arguments.length === 1 &&
node.object.arguments[0].type === 'Literal' &&
node.property.type === 'Identifier' &&
node.property.name === 'item';

walk<DollarItemNode>(ast, isDollarNodeItemAccess).forEach((node) => {
const [start, end] = getRange(node.property);

lintings.push({
from: start,
to: end,
severity: 'warning',
message: i18n.baseText('codeNodeEditor.linter.eachItem.preferFirst'),
actions: [
{
name: 'Fix',
apply(view) {
view.dispatch({ changes: { from: start, to: end, insert: 'first()' } });
},
},
],
});
});
}

return lintings;
}

Expand Down
4 changes: 3 additions & 1 deletion packages/editor-ui/src/plugins/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@
"codeNodeEditor.linter.allItems.itemCall": "`item` is a property to access, not a method to call. Did you mean `.item` without brackets?",
"codeNodeEditor.linter.allItems.itemMatchingNoArg": "`.itemMatching()` expects an item index to be passed in as its argument.",
"codeNodeEditor.linter.allItems.unavailableItem": "Legacy `item` is only available in the 'Run Once for Each Item' mode.",
"codeNodeEditor.linter.allItems.unavailableProperty": "`.item` is only available in the 'Run Once for Each Item' mode.",
"codeNodeEditor.linter.allItems.unavailableProperty": "`.item` is only available in the 'Run Once for Each Item' mode. Use `.first()` instead.",
"codeNodeEditor.linter.allItems.unavailableVar": "is only available in the 'Run Once for Each Item' mode.",
"codeNodeEditor.linter.bothModes.directAccess.firstOrLastCall": "@:_reusableBaseText.codeNodeEditor.linter.useJson",
"codeNodeEditor.linter.bothModes.directAccess.itemProperty": "@:_reusableBaseText.codeNodeEditor.linter.useJson",
Expand All @@ -458,7 +458,9 @@
"codeNodeEditor.linter.eachItem.returnArray": "Code doesn't return an object. Array found instead. Please return an object representing the output item",
"codeNodeEditor.linter.eachItem.unavailableItems": "Legacy `items` is only available in the 'Run Once for All Items' mode.",
"codeNodeEditor.linter.eachItem.unavailableMethod": "Method `$input.{method}()` is only available in the 'Run Once for All Items' mode.",
"codeNodeEditor.linter.eachItem.preferFirst": "Prefer `.first()` over `.item` so n8n can optimize execution",
"codeNodeEditor.linter.bothModes.syntaxError": "Syntax error",
"codeNodeEditor.linter.bothModes.dollarSignVariable": "Use a string literal instead of a variable so n8n can optimize execution.",
"codeNodeEditor.askAi.placeholder": "Tell AI what you want the code to achieve. You can reference input data fields using dot notation (e.g. user.email)",
"codeNodeEditor.askAi.intro": "Hey AI, generate JavaScript code that...",
"codeNodeEditor.askAi.help": "Help",
Expand Down

0 comments on commit 88295c7

Please sign in to comment.