Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Please consider following this project's author, [Jon Schlinkert](https://github
* [.variables](#variables)
- [Options](#options)
* [booleanLogicalOperators](#booleanlogicaloperators)
* [budget](#budget)
* [functions](#functions)
* [Top-Level await](#top-level-await)
* [generate](#generate)
Expand Down Expand Up @@ -164,6 +165,25 @@ console.log(await evaluate(parse('a && b'), { a: undefined, b: false }, options)
console.log(await evaluate(parse('a || b'), { a: false, b: undefined }, options)); //=> false
```

### budget

Type: `number`
Default: `undefined`

Limit the complexity of an expression by capping the number of AST node visits during evaluation. When the budget is exceeded, evaluation throws a `RangeError` with the message `Expression complexity budget exceeded`.

```js
// Allows up to 1 node visit (the NumericLiteral)
console.log(evaluate.sync(parse('1'), {}, { budget: 1 })); //=> 1

// Exceeds budget: BinaryExpression + two NumericLiterals = 3 visits
try {
evaluate.sync(parse('1 + 2'), {}, { budget: 2 });
} catch (err) {
console.log(err.message); //=> 'Expression complexity budget exceeded'
}
```

### functions

Type: `boolean`
Expand Down Expand Up @@ -389,4 +409,4 @@ Released under the [MIT License](LICENSE).

***

_This file was generated by [verb-generate-readme](https://github.com/verbose/verb-generate-readme), v0.8.0, on April 19, 2025._
_This file was generated by [verb-generate-readme](https://github.com/verbose/verb-generate-readme), v0.8.0, on April 19, 2025._
68 changes: 68 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// TypeScript declarations for eval-estree-expression

export interface VariablesOptions {
withMembers?: boolean;
}

export interface VisitorsMap {
[nodeType: string]: (node: any, context: any, parent?: any) => any;
}

export interface EvaluateOptions {
// Behavior
booleanLogicalOperators?: boolean;
strict?: boolean;
regexOperator?: boolean; // defaults true in runtime
allowContextStringLiterals?: boolean;

// Complexity limits
budget?: number;
maxArrayLength?: number;
maxExpressionDepth?: number;

// Function support
functions?: boolean;
generate?: (node: any) => string; // escodegen.generate

// Custom visitors
visitors?: VisitorsMap;
}

export type Context = Record<string, any> | undefined;

export interface EvaluateFn {
(tree: any, context?: Context, options?: EvaluateOptions): Promise<any>;
sync(tree: any, context?: Context, options?: EvaluateOptions): any;
}

export declare class ExpressionSync {
constructor(tree: any, options?: EvaluateOptions, internalState?: { created?: boolean });

// Static
static readonly FAIL: unique symbol;
static readonly ExpressionSync: typeof ExpressionSync;
static variables(tree: any, options?: VariablesOptions): string[];
static evaluate(tree: any, context?: Context, options?: EvaluateOptions): any;

// Instance
evaluate(context?: Context): any;
}

export declare class Expression extends ExpressionSync {
// Static
static readonly isAsync: true;
static readonly Expression: typeof Expression;
static readonly ExpressionSync: typeof ExpressionSync;
static variables(tree: any, options?: VariablesOptions): string[];
static evaluate: EvaluateFn;

// Instance
evaluate(context?: Context): Promise<any>;
}

export const variables: (tree: any, options?: VariablesOptions) => string[];
export const evaluate: EvaluateFn;

export { Expression, ExpressionSync };
export default Expression;

17 changes: 16 additions & 1 deletion lib/ExpressionSync.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ class ExpressionSync {
this.options = {
maxExpressionDepth: MAX_EXPRESSION_DEPTH,
maxArrayLength: MAX_ARRAY_LENGTH,
// Maximum allowed number of node visits during evaluation.
// When provided, the evaluator will decrement this "budget" on each
// node visit and throw if it reaches zero. This guards against
// overly complex expressions consuming too much CPU.
budget: undefined,
...options
};

Expand All @@ -32,7 +37,9 @@ class ExpressionSync {
stackDepth: 0,
expressionDepth: 0,
seenObjects: new WeakSet(),
templateLiterals: new Set()
templateLiterals: new Set(),
// Remaining budget of visits. Undefined means unlimited.
budget: typeof this.options.budget === 'number' ? this.options.budget : undefined
};

this.stack = [];
Expand All @@ -46,6 +53,14 @@ class ExpressionSync {
}

incrementDepth() {
// Enforce complexity budget if specified
if (typeof this.state.budget === 'number') {
if (this.state.budget <= 0) {
throw new RangeError('Expression complexity budget exceeded');
}
this.state.budget--;
}

this.state.expressionDepth++;

if (this.state.expressionDepth > MAX_EXPRESSION_DEPTH) {
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
"url": "https://github.com/jonschlinkert/eval-estree-expression/issues"
},
"main": "index.js",
"types": "index.d.ts",
"files": [
"index.js",
"index.d.ts",
"lib"
],
"scripts": {
Expand Down
22 changes: 22 additions & 0 deletions test/async/budget.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';

const { strict: assert } = require('assert');
const { evaluate } = require('../support');

const e = (input, context, options) => evaluate(input, context, { ...options });

describe('budget (async)', () => {
it('should allow simple expression within budget', async () => {
assert.equal(await e('1', {}, { budget: 1 }), 1);
});

it('should reject when budget is exceeded on binary expression', async () => {
await assert.rejects(() => e('1 + 2', {}, { budget: 2 }), /Expression complexity budget exceeded/);
});

it('should reject when budget is exceeded on array expression', async () => {
// Visits: ArrayExpression + 3 NumericLiterals = 4 visits
await assert.rejects(() => e('[1,2,3]', {}, { budget: 3 }), /Expression complexity budget exceeded/);
});
});

20 changes: 20 additions & 0 deletions test/sync/budget.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use strict';

const assert = require('node:assert/strict');
const { evaluate: e } = require('../support');

describe('budget (sync)', () => {
it('should allow simple expression within budget', () => {
assert.equal(e.sync('1', {}, { budget: 1 }), 1);
});

it('should throw when budget is exceeded on binary expression', () => {
assert.throws(() => e.sync('1 + 2', {}, { budget: 2 }), /Expression complexity budget exceeded/);
});

it('should throw when budget is exceeded on array expression', () => {
// Visits: ArrayExpression + 3 NumericLiterals = 4 visits
assert.throws(() => e.sync('[1,2,3]', {}, { budget: 3 }), /Expression complexity budget exceeded/);
});
});