From 88295c70495ae3d017674d5745972a346fcbaf12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 12 Nov 2024 13:28:51 +0100 Subject: [PATCH] perf(editor): Add lint rules for optimization-friendly syntax (#11681) --- .../src/components/CodeNodeEditor/linter.ts | 74 ++++++++++++++++++- .../src/plugins/i18n/locales/en.json | 4 +- 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/packages/editor-ui/src/components/CodeNodeEditor/linter.ts b/packages/editor-ui/src/components/CodeNodeEditor/linter.ts index bf7615faaff..defbdbfa73d 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/linter.ts +++ b/packages/editor-ui/src/components/CodeNodeEditor/linter.ts @@ -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()' } }); }, }, ], @@ -559,6 +559,76 @@ export const useLinter = ( }); }); + /** + * Lint for `$(variable)` usage where variable is not a string, in both modes. + * + * $(nodeName) -> + */ + 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(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(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; } diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 844fd9786b7..64b329518f4 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -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", @@ -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",