From 6eabaecbad43bc16d30c6d962a9bc577f47c727a Mon Sep 17 00:00:00 2001 From: conico974 Date: Fri, 12 Jun 2026 14:26:17 +0200 Subject: [PATCH] Implement getVariables method for expression evaluation (#4304) --- .changeset/chubby-walls-sniff.md | 5 +++ packages/expr/src/__tests__/runtime.test.ts | 43 +++++++++++++++++++ packages/expr/src/runtime.ts | 24 ++++++++++- .../expr/types/eval-estree-expression.d.ts | 10 +++++ 4 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 .changeset/chubby-walls-sniff.md diff --git a/.changeset/chubby-walls-sniff.md b/.changeset/chubby-walls-sniff.md new file mode 100644 index 000000000..30bf1b06d --- /dev/null +++ b/.changeset/chubby-walls-sniff.md @@ -0,0 +1,5 @@ +--- +"@gitbook/expr": minor +--- + +Implement a getVariables function for ExpressionRuntime diff --git a/packages/expr/src/__tests__/runtime.test.ts b/packages/expr/src/__tests__/runtime.test.ts index 756484469..875794948 100644 --- a/packages/expr/src/__tests__/runtime.test.ts +++ b/packages/expr/src/__tests__/runtime.test.ts @@ -195,6 +195,49 @@ describe('ExpressionRuntime', () => { ); }); + describe('getVariables', () => { + it.each([ + { + scenario: 'single variable', + condition: 'isBetaUser === true', + expectedVariables: ['isBetaUser'], + }, + { + scenario: 'multiple variables', + condition: 'useProductA && !isBetaUser', + expectedVariables: ['useProductA', 'isBetaUser'], + }, + { + scenario: 'member expression', + condition: 'user.role === "admin"', + expectedVariables: ['user.role'], + }, + { + scenario: 'nested member expression with method call', + condition: 'products.includes("productA") && userSegments.alpha', + expectedVariables: ['products.includes', 'userSegments.alpha'], + }, + ])( + 'should return variables used in expression: $scenario', + ({ condition, expectedVariables }) => { + expect(runtime.getVariables(condition)).toEqual(expectedVariables); + } + ); + + it.each([ + { + scenario: 'invalid syntax', + condition: 't}=d', + }, + { + scenario: 'non conditional expression', + condition: 'const a = 1;', + }, + ])('should return an empty array for invalid expressions: $scenario', ({ condition }) => { + expect(runtime.getVariables(condition)).toEqual([]); + }); + }); + describe.skip('generate', () => { it.each([ { diff --git a/packages/expr/src/runtime.ts b/packages/expr/src/runtime.ts index 8d3bc33a5..2e9ca1fec 100644 --- a/packages/expr/src/runtime.ts +++ b/packages/expr/src/runtime.ts @@ -11,7 +11,7 @@ import { import { parse as parseLoose } from 'acorn-loose'; import escodegen from 'escodegen'; import evalESTreeExpr from 'eval-estree-expression'; -const { evaluate } = evalESTreeExpr; +const { evaluate, variables } = evalESTreeExpr; import { AutoComplete } from './autocomplete'; import { ExpressionError } from './errors'; @@ -162,6 +162,28 @@ export class ExpressionRuntime { .join(''); } + /** + * Given an expression, returns a list of variables used in the expression. + */ + public getVariables(expr: string): string[] { + try { + const parsed = this.parse(expr); + + if (parsed.invalidNodes.length > 0) { + throw new ExpressionError('Invalid nodes found when parsing'); + } + + return variables(parsed.result, { + functions: true, + withMembers: true, + generate: escodegen.generate, + }); + } catch (error) { + this.#logger.error(`Error while parsing expression ${expr} to get variables`, error); + return []; + } + } + /** * Parses a binary expression and returns an @ExpressionParserResult. */ diff --git a/packages/expr/types/eval-estree-expression.d.ts b/packages/expr/types/eval-estree-expression.d.ts index 4bb4ff387..6e542ca21 100644 --- a/packages/expr/types/eval-estree-expression.d.ts +++ b/packages/expr/types/eval-estree-expression.d.ts @@ -41,6 +41,16 @@ declare module 'eval-estree-expression' { options?: EvalESTreeExpressionOptions ): Promise; + /** + * Given an ESTree-compliant AST node, returns a list of variables used in the expression. + * @param ast An object representing an ESTree-compliant AST node. + * @param options Options for evaluation and compilation. + */ + export function variables( + ast: ASTNode, + options?: EvalESTreeExpressionOptions + ): string[]; + /** * Evaluates an ESTree expression synchronously against a given context. * @param expression - An object representing an ESTree-compliant AST node.