mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-20 11:22:15 +00:00
test: Add expression transform tests (#5497) (no-changelog)
test: add expression transform tests
This commit is contained in:
@@ -3,6 +3,7 @@ import { DateTime } from 'luxon';
|
||||
import { ExpressionExtensionError } from '../ExpressionError';
|
||||
import { parse, visit, types, print } from 'recast';
|
||||
import { getOption } from 'recast/lib/util';
|
||||
import type { Config as EsprimaConfig } from 'esprima-next';
|
||||
import { parse as esprimaParse } from 'esprima-next';
|
||||
|
||||
import { arrayExtensions } from './ArrayExtensions';
|
||||
@@ -12,6 +13,9 @@ import { stringExtensions } from './StringExtensions';
|
||||
import { objectExtensions } from './ObjectExtensions';
|
||||
import type { ExpressionKind } from 'ast-types/gen/kinds';
|
||||
|
||||
import type { ExpressionChunk, ExpressionCode } from './ExpressionParser';
|
||||
import { joinExpression, splitExpression } from './ExpressionParser';
|
||||
|
||||
const EXPRESSION_EXTENDER = 'extend';
|
||||
const EXPRESSION_EXTENDER_OPTIONAL = 'extendOptional';
|
||||
|
||||
@@ -89,17 +93,12 @@ function parseWithEsprimaNext(source: string, options?: any): any {
|
||||
loc: true,
|
||||
locations: true,
|
||||
comment: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
range: getOption(options, 'range', false),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
tolerant: getOption(options, 'tolerant', true),
|
||||
range: getOption(options, 'range', false) as boolean,
|
||||
tolerant: getOption(options, 'tolerant', true) as boolean,
|
||||
tokens: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
jsx: getOption(options, 'jsx', false),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
sourceType: getOption(options, 'sourceType', 'module'),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
jsx: getOption(options, 'jsx', false) as boolean,
|
||||
sourceType: getOption(options, 'sourceType', 'module') as string,
|
||||
} as EsprimaConfig);
|
||||
|
||||
return ast;
|
||||
}
|
||||
@@ -124,9 +123,8 @@ export const extendTransform = (expression: string): { code: string } | undefine
|
||||
|
||||
let currentChain = 1;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
// Polyfill optional chaining
|
||||
visit(ast, {
|
||||
// Polyfill optional chaining
|
||||
visitChainExpression(path) {
|
||||
this.traverse(path);
|
||||
const chainNumber = currentChain;
|
||||
@@ -138,6 +136,8 @@ export const extendTransform = (expression: string): { code: string } | undefine
|
||||
// @ts-ignore
|
||||
typeof window !== 'object' ? 'global' : 'window',
|
||||
);
|
||||
// We want to define all of our commonly used identifiers and member
|
||||
// expressions now so we don't have to create multiple instances
|
||||
const undefinedIdentifier = types.builders.identifier('undefined');
|
||||
const cancelIdentifier = types.builders.identifier(`chainCancelToken${chainNumber}`);
|
||||
const valueIdentifier = types.builders.identifier(`chainValue${chainNumber}`);
|
||||
@@ -152,6 +152,8 @@ export const extendTransform = (expression: string): { code: string } | undefine
|
||||
|
||||
const patchedStack: ExpressionKind[] = [];
|
||||
|
||||
// This builds the cancel check. This lets us slide to the end of the expression
|
||||
// if it's undefined/null at any of the optional points of the chain.
|
||||
const buildCancelCheckWrapper = (node: ExpressionKind): ExpressionKind => {
|
||||
return types.builders.conditionalExpression(
|
||||
types.builders.binaryExpression(
|
||||
@@ -164,10 +166,17 @@ export const extendTransform = (expression: string): { code: string } | undefine
|
||||
);
|
||||
};
|
||||
|
||||
// This is just a quick small wrapper to create the assignment expression
|
||||
// for the running value.
|
||||
const buildValueAssignWrapper = (node: ExpressionKind): ExpressionKind => {
|
||||
return types.builders.assignmentExpression('=', valueMemberExpression, node);
|
||||
};
|
||||
|
||||
// This builds what actually does the comparison. It wraps the current
|
||||
// chunk of the expression with a nullish coalescing operator that returns
|
||||
// undefined if it's null or undefined. We do this because optional chains
|
||||
// always return undefined if they fail part way, even if the value they
|
||||
// fail on is null.
|
||||
const buildOptionalWrapper = (node: ExpressionKind): ExpressionKind => {
|
||||
return types.builders.binaryExpression(
|
||||
'===',
|
||||
@@ -180,6 +189,7 @@ export const extendTransform = (expression: string): { code: string } | undefine
|
||||
);
|
||||
};
|
||||
|
||||
// Another small wrapper, but for assigning to the cancel token this time.
|
||||
const buildCancelAssignWrapper = (node: ExpressionKind): ExpressionKind => {
|
||||
return types.builders.assignmentExpression('=', cancelMemberExpression, node);
|
||||
};
|
||||
@@ -189,6 +199,9 @@ export const extendTransform = (expression: string): { code: string } | undefine
|
||||
let patchTop: ExpressionKind | null = null;
|
||||
let wrapNextTopInOptionalExtend = false;
|
||||
|
||||
// This patches the previous node to use our current one as it's left hand value.
|
||||
// It takes `window.chainValue1.test1` and `window.chainValue1.test2` and turns it
|
||||
// into `window.chainValue1.test2.test1`.
|
||||
const updatePatch = (toPatch: ExpressionKind, node: ExpressionKind) => {
|
||||
if (toPatch.type === 'MemberExpression' || toPatch.type === 'OptionalMemberExpression') {
|
||||
toPatch.object = node;
|
||||
@@ -200,7 +213,15 @@ export const extendTransform = (expression: string): { code: string } | undefine
|
||||
}
|
||||
};
|
||||
|
||||
// This loop walks down an optional chain from the top. This will walk
|
||||
// from right to left through an optional chain. We keep track of our current
|
||||
// top of the chain (furthest right) and create a chain below it. This chain
|
||||
// contains all of the (member and call) expressions that we need. These are
|
||||
// patched versions that reference our current chain value. We then push this
|
||||
// chain onto a stack when we hit an optional point in our chain.
|
||||
while (true) {
|
||||
// This should only ever be these types but you can optional chain on
|
||||
// JSX nodes, which we don't support.
|
||||
if (
|
||||
currentNode.type === 'MemberExpression' ||
|
||||
currentNode.type === 'OptionalMemberExpression' ||
|
||||
@@ -208,6 +229,10 @@ export const extendTransform = (expression: string): { code: string } | undefine
|
||||
currentNode.type === 'OptionalCallExpression'
|
||||
) {
|
||||
let patchNode: ExpressionKind;
|
||||
// Here we take the current node and extract the parts we actually care
|
||||
// about.
|
||||
// In the case of a member expression we take the property it's trying to
|
||||
// access and make the object it's accessing be our chain value.
|
||||
if (
|
||||
currentNode.type === 'MemberExpression' ||
|
||||
currentNode.type === 'OptionalMemberExpression'
|
||||
@@ -216,6 +241,8 @@ export const extendTransform = (expression: string): { code: string } | undefine
|
||||
valueMemberExpression,
|
||||
currentNode.property,
|
||||
);
|
||||
// In the case of a call expression we take the arguments and make the
|
||||
// callee our chain value.
|
||||
} else {
|
||||
patchNode = types.builders.callExpression(
|
||||
valueMemberExpression,
|
||||
@@ -223,16 +250,22 @@ export const extendTransform = (expression: string): { code: string } | undefine
|
||||
);
|
||||
}
|
||||
|
||||
// If we have a previous node we patch it here.
|
||||
if (currentPatch) {
|
||||
updatePatch(currentPatch, patchNode);
|
||||
}
|
||||
|
||||
// If we have no top patch (first run, or just pushed onto the stack) we
|
||||
// note it here.
|
||||
if (!patchTop) {
|
||||
patchTop = patchNode;
|
||||
}
|
||||
|
||||
currentPatch = patchNode;
|
||||
|
||||
// This is an optional in our chain. In here we'll push the node onto the
|
||||
// stack. We also do a polyfill if the top of the stack is function call
|
||||
// that might be a extended function.
|
||||
if (currentNode.optional) {
|
||||
// Implement polyfill described below
|
||||
if (wrapNextTopInOptionalExtend) {
|
||||
@@ -268,6 +301,7 @@ export const extendTransform = (expression: string): { code: string } | undefine
|
||||
}
|
||||
}
|
||||
|
||||
// Finally we get the next point AST to walk down.
|
||||
if (
|
||||
currentNode.type === 'MemberExpression' ||
|
||||
currentNode.type === 'OptionalMemberExpression'
|
||||
@@ -277,6 +311,8 @@ export const extendTransform = (expression: string): { code: string } | undefine
|
||||
currentNode = currentNode.callee;
|
||||
}
|
||||
} else {
|
||||
// We update the final patch to point to the first part of the optional chain
|
||||
// which is probably an identifier for an object.
|
||||
if (currentPatch) {
|
||||
updatePatch(currentPatch, currentNode);
|
||||
if (!patchTop) {
|
||||
@@ -298,6 +334,7 @@ export const extendTransform = (expression: string): { code: string } | undefine
|
||||
}
|
||||
}
|
||||
|
||||
// Push the first part of our chain to stack.
|
||||
if (patchTop) {
|
||||
patchedStack.push(patchTop);
|
||||
} else {
|
||||
@@ -307,28 +344,42 @@ export const extendTransform = (expression: string): { code: string } | undefine
|
||||
}
|
||||
}
|
||||
|
||||
// Since we're working from right to left we need to flip the stack
|
||||
// for the correct order of operations
|
||||
patchedStack.reverse();
|
||||
|
||||
// Walk the node stack and wrap all our expressions in cancel/assignment
|
||||
// wrappers.
|
||||
for (let i = 0; i < patchedStack.length; i++) {
|
||||
let node = patchedStack[i];
|
||||
|
||||
// We don't wrap the last expression in an assignment wrapper because
|
||||
// it's going to be returned anyway. We just wrap it in a cancel check
|
||||
// wrapper.
|
||||
if (i !== patchedStack.length - 1) {
|
||||
node = buildCancelAssignWrapper(buildOptionalWrapper(node));
|
||||
}
|
||||
|
||||
// Don't wrap the first part in a cancel wrapper because the cancel
|
||||
// token will always be undefined.
|
||||
if (i !== 0) {
|
||||
node = buildCancelCheckWrapper(node);
|
||||
}
|
||||
|
||||
// Replace the node in the stack with our wrapped one
|
||||
patchedStack[i] = node;
|
||||
}
|
||||
|
||||
// Put all our expressions in a sequence expression (also called a
|
||||
// group operator). These will all be executed in order and the value
|
||||
// of the final expression will be returned.
|
||||
const sequenceNode = types.builders.sequenceExpression(patchedStack);
|
||||
|
||||
path.replace(sequenceNode);
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
// Extended functions
|
||||
visit(ast, {
|
||||
visitCallExpression(path) {
|
||||
this.traverse(path);
|
||||
@@ -377,7 +428,6 @@ export const extendTransform = (expression: string): { code: string } | undefine
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument
|
||||
return print(ast);
|
||||
} catch (e) {
|
||||
return;
|
||||
@@ -515,3 +565,61 @@ export function extendOptional(
|
||||
return foundFunction.function(input, args);
|
||||
};
|
||||
}
|
||||
|
||||
const EXTENDED_SYNTAX_CACHE: Record<string, string> = {};
|
||||
|
||||
export function extendSyntax(bracketedExpression: string, forceExtend = false): string {
|
||||
const chunks = splitExpression(bracketedExpression);
|
||||
|
||||
const codeChunks = chunks
|
||||
.filter((c) => c.type === 'code')
|
||||
.map((c) => c.text.replace(/("|').*?("|')/, '').trim());
|
||||
|
||||
if (
|
||||
(!codeChunks.some(hasExpressionExtension) || hasNativeMethod(bracketedExpression)) &&
|
||||
!forceExtend
|
||||
) {
|
||||
return bracketedExpression;
|
||||
}
|
||||
|
||||
// If we've seen this expression before grab it from the cache
|
||||
if (bracketedExpression in EXTENDED_SYNTAX_CACHE) {
|
||||
return EXTENDED_SYNTAX_CACHE[bracketedExpression];
|
||||
}
|
||||
|
||||
const extendedChunks = chunks.map((chunk): ExpressionChunk => {
|
||||
if (chunk.type === 'code') {
|
||||
let output = extendTransform(chunk.text);
|
||||
|
||||
// esprima fails to parse bare objects (e.g. `{ data: something }`), we can
|
||||
// work around this by wrapping it in an parentheses
|
||||
if (!output?.code && chunk.text.trim()[0] === '{') {
|
||||
output = extendTransform(`(${chunk.text})`);
|
||||
}
|
||||
|
||||
if (!output?.code) {
|
||||
throw new ExpressionExtensionError('invalid syntax');
|
||||
}
|
||||
|
||||
let text = output.code;
|
||||
|
||||
// We need to cut off any trailing semicolons. These cause issues
|
||||
// with certain types of expression and cause the whole expression
|
||||
// to fail.
|
||||
if (text.trim().endsWith(';')) {
|
||||
text = text.trim().slice(0, -1);
|
||||
}
|
||||
|
||||
return {
|
||||
...chunk,
|
||||
text,
|
||||
} as ExpressionCode;
|
||||
}
|
||||
return chunk;
|
||||
});
|
||||
|
||||
const expression = joinExpression(extendedChunks);
|
||||
// Cache the expression so we don't have to do this transform again
|
||||
EXTENDED_SYNTAX_CACHE[bracketedExpression] = expression;
|
||||
return expression;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user