mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
159 lines
5.2 KiB
TypeScript
159 lines
5.2 KiB
TypeScript
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
|
|
*/
|
|
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 === '$item') {
|
|
// $item is legacy syntax that is basically an alias for WorkflowDataProxy
|
|
// and allows accessing any data. We need to support it for backwards
|
|
// compatibility, but we're not gonna implement any optimizations
|
|
state.markNeedsAllNodes();
|
|
} else if (
|
|
node.name === '$input' ||
|
|
node.name === '$json' ||
|
|
node.name === 'items' ||
|
|
// item is deprecated but we still need to support it
|
|
node.name === 'item'
|
|
) {
|
|
state.markInputAsNeeded();
|
|
} else if (node.name === '$node') {
|
|
// $node is legacy way of accessing any node's output. We need to
|
|
// support it for backward compatibility, but we're not gonna
|
|
// implement any optimizations
|
|
state.markNeedsAllNodes();
|
|
} 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';
|
|
}
|
|
}
|