feat: Only send needed data to task runner (no-changelog) (#11487)

This commit is contained in:
Tomi Turtiainen
2024-11-04 11:13:09 +02:00
committed by GitHub
parent 2104fa1733
commit e4aa1d01f3
24 changed files with 1511 additions and 252 deletions

View File

@@ -0,0 +1,142 @@
import type { CallExpression, Identifier, Node, Program } from 'acorn';
import { parse } from 'acorn';
import { ancestor } from 'acorn-walk';
import type { Result } from 'n8n-workflow';
import { toResult } from 'n8n-workflow';
import {
isAssignmentExpression,
isIdentifier,
isLiteral,
isMemberExpression,
isVariableDeclarator,
} from './acorn-helpers';
import { BuiltInsParserState } from './built-ins-parser-state';
/**
* Class for parsing Code Node code to identify which built-in variables
* are accessed
*/
export class BuiltInsParser {
/**
* Parses which built-in variables are accessed in the given code
*/
public parseUsedBuiltIns(code: string): Result<BuiltInsParserState, Error> {
return toResult(() => {
const wrappedCode = `async function VmCodeWrapper() { ${code} }`;
const ast = parse(wrappedCode, { ecmaVersion: 2025, sourceType: 'module' });
return this.identifyBuiltInsByWalkingAst(ast);
});
}
/** Traverse the AST of the script and mark any data needed for it to run. */
private identifyBuiltInsByWalkingAst(ast: Program) {
const accessedBuiltIns = new BuiltInsParserState();
ancestor(
ast,
{
CallExpression: this.visitCallExpression,
Identifier: this.visitIdentifier,
},
undefined,
accessedBuiltIns,
);
return accessedBuiltIns;
}
private visitCallExpression = (
node: CallExpression,
state: BuiltInsParserState,
ancestors: Node[],
) => {
// $(...)
const isDollar = node.callee.type === 'Identifier' && node.callee.name === '$';
if (!isDollar) return;
// $(): This is not valid, ignore
if (node.arguments.length === 0) {
return;
}
const firstArg = node.arguments[0];
if (!isLiteral(firstArg)) {
// $(variable): Can't easily determine statically, mark all nodes as needed
state.markNeedsAllNodes();
return;
}
if (typeof firstArg.value !== 'string') {
// $(123): Static value, but not a string --> invalid code --> ignore
return;
}
// $("node"): Static value, mark 'nodeName' as needed
state.markNodeAsNeeded(firstArg.value);
// Determine how $("node") is used
this.handlePrevNodeCall(node, state, ancestors);
};
private handlePrevNodeCall(_node: CallExpression, state: BuiltInsParserState, ancestors: Node[]) {
// $("node").item, .pairedItem or .itemMatching: In a case like this, the execution
// engine will traverse back from current node (i.e. the Code Node) to
// the "node" node and use `pairedItem`s to find which item is linked
// to the current item. So, we need to mark all nodes as needed.
// TODO: We could also mark all the nodes between the current node and
// the "node" node as needed, but that would require more complex logic.
const directParent = ancestors[ancestors.length - 2];
if (isMemberExpression(directParent)) {
const accessedProperty = directParent.property;
if (directParent.computed) {
// $("node")["item"], ["pairedItem"] or ["itemMatching"]
if (isLiteral(accessedProperty)) {
if (this.isPairedItemProperty(accessedProperty.value)) {
state.markNeedsAllNodes();
}
// Else: $("node")[123]: Static value, but not any of the ones above --> ignore
}
// $("node")[variable]
else if (isIdentifier(accessedProperty)) {
state.markNeedsAllNodes();
}
}
// $("node").item, .pairedItem or .itemMatching
else if (isIdentifier(accessedProperty) && this.isPairedItemProperty(accessedProperty.name)) {
state.markNeedsAllNodes();
}
} else if (isVariableDeclarator(directParent) || isAssignmentExpression(directParent)) {
// const variable = $("node") or variable = $("node"):
// In this case we would need to track down all the possible use sites
// of 'variable' and determine if `.item` is accessed on it. This is
// more complex and skipped for now.
// TODO: Optimize for this case
state.markNeedsAllNodes();
} else {
// Something else than the cases above. Mark all nodes as needed as it
// could be a dynamic access.
state.markNeedsAllNodes();
}
}
private visitIdentifier = (node: Identifier, state: BuiltInsParserState) => {
if (node.name === '$env') {
state.markEnvAsNeeded();
} else if (node.name === '$input' || node.name === '$json') {
state.markInputAsNeeded();
} else if (node.name === '$execution') {
state.markExecutionAsNeeded();
} else if (node.name === '$prevNode') {
state.markPrevNodeAsNeeded();
}
};
private isPairedItemProperty(
property?: string | boolean | null | number | RegExp | bigint,
): boolean {
return property === 'item' || property === 'pairedItem' || property === 'itemMatching';
}
}