mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-30 16:26:59 +02:00
84 lines
2.7 KiB
TypeScript
84 lines
2.7 KiB
TypeScript
import type { BinaryCheck } from '../types';
|
|
import { extractExpressionsFromParams } from '../utils';
|
|
|
|
/**
|
|
* Regex patterns to extract node names from n8n expression syntaxes.
|
|
*
|
|
* Quoted patterns capture at group index 2; dot-notation captures at group index 1.
|
|
*/
|
|
const QUOTED_NODE_REFS: RegExp[] = [
|
|
/\$\(\s*(['"`])((?:\\.|(?!\1)[^\\])*)\1\s*\)/g, // $('Node Name')
|
|
/\$node\[\s*(['"])((?:\\.|(?!\1)[^\\])*)\1\s*\]/g, // $node["Node Name"]
|
|
/\$items\(\s*(['"])((?:\\.|(?!\1)[^\\])*)\1\s*[,)]/g, // $items("Node Name") or $items("Node Name", 0)
|
|
];
|
|
|
|
/** Legacy dot-notation: $node.NodeName.json... — name is a JS identifier after $node. */
|
|
const DOT_NODE_REF = /\$node\.(\w+)\./g;
|
|
|
|
/** Remove backslash escapes from a captured node name (e.g. `Node\'s` -> `Node's`). */
|
|
function unescapeNodeName(raw: string): string {
|
|
return raw.replace(/\\(.)/g, '$1');
|
|
}
|
|
|
|
/** Collect all matches from a global regex, returning captured groups at the given index. */
|
|
function collectMatches(pattern: RegExp, text: string, groupIndex: number): string[] {
|
|
return Array.from(text.matchAll(pattern), (m) => m[groupIndex]);
|
|
}
|
|
|
|
/** Extract all referenced node names from an expression string. */
|
|
function extractNodeNamesFromExpression(expression: string): string[] {
|
|
const names: string[] = [];
|
|
|
|
for (const pattern of QUOTED_NODE_REFS) {
|
|
for (const raw of collectMatches(pattern, expression, 2)) {
|
|
names.push(unescapeNodeName(raw));
|
|
}
|
|
}
|
|
|
|
for (const name of collectMatches(DOT_NODE_REF, expression, 1)) {
|
|
names.push(name);
|
|
}
|
|
|
|
return names;
|
|
}
|
|
|
|
export const expressionsReferenceExistingNodes: BinaryCheck = {
|
|
name: 'expressions_reference_existing_nodes',
|
|
description: 'Expressions only reference nodes that exist in the workflow',
|
|
kind: 'deterministic',
|
|
dimension: 'parameter_correctness',
|
|
run(workflow) {
|
|
const nodes = workflow.nodes ?? [];
|
|
|
|
const existingNodeNames = new Set(nodes.map((n) => n.name));
|
|
const invalid: string[] = [];
|
|
let sawAnyNodeRef = false;
|
|
|
|
for (const node of nodes) {
|
|
if (!node.parameters) continue;
|
|
|
|
const expressions = extractExpressionsFromParams(node.parameters);
|
|
for (const expr of expressions) {
|
|
const referencedNames = extractNodeNamesFromExpression(expr);
|
|
if (referencedNames.length > 0) sawAnyNodeRef = true;
|
|
for (const refName of referencedNames) {
|
|
if (!existingNodeNames.has(refName)) {
|
|
invalid.push(`"${refName}" (in node "${node.name}")`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!sawAnyNodeRef) return { pass: true, applicable: false };
|
|
|
|
const unique = [...new Set(invalid)];
|
|
|
|
return {
|
|
pass: unique.length === 0,
|
|
...(unique.length > 0
|
|
? { comment: `Expressions reference non-existent nodes: ${unique.join(', ')}` }
|
|
: {}),
|
|
};
|
|
},
|
|
};
|