Module: lib/core/expression/parser

A JavaScript expression parser for Avenx template expressions.

Why Avenx needs its own

Template expressions used to be handed straight to the engine:

new Function(`with(this) { return (${expression}) }`)

Two consequences followed from that line, and neither could be fixed while it stood.

unsafe-eval was structural. Any Content-Security-Policy without 'unsafe-eval' stopped the framework working, which excludes it from regulated environments, browser extensions, and a good many default enterprise policies.

The sandbox was not a boundary. AvenxSandbox wrapped values that reached an expression through the scope and blocked the literal identifiers constructor, __proto__ and prototype with a regular expression over the source text. An object created inside the expression never passed through the scope, so it was never wrapped, and the text check fell to string concatenation:

({})['const'+'ructor']['const'+'ructor']('return 1')()   // executed

There is no version of a source-text blacklist that closes that. The value being guarded is produced by the engine, inside code the guard has already handed over.

What parsing changes

Once Avenx holds the AST, it decides what every operation means. A member access is a call into module:lib/core/expression/evaluator, with the property key already resolved — so x['const'+'ructor'] and x.constructor arrive at the same check, because by then they are the same string. Nothing is handed to the engine as code, so nothing needs unsafe-eval.

Scope

This parses the expression language Avenx templates use — everything up to and including arrow functions, which templates need for items.filter(i => i.done). It is not a full ECMAScript parser and does not try to be: statements, classes, generators, async/await, destructuring patterns and regular-expression literals are out of scope. An expression it cannot parse raises ExpressionParseError rather than guessing, and the caller decides what to do about it.

Source:

Classes

ExpressionParseError
Parser

Members

(inner, constant) ASSIGNMENT_OPERATORS :Set:.<string:>

Assignment operators.

Type:
  • Set:.<string:>
Source:

(inner, constant) BINARY_PRECEDENCE :Object:.<string:, number:>

Binary operator precedence, higher binds tighter.

Type:
  • Object:.<string:, number:>
Source:

(inner, constant) LITERAL_KEYWORDS :Object:.<string:, any:>

Keywords that are values rather than identifiers.

Type:
  • Object:.<string:, any:>
Source:

(inner, constant) PUNCTUATORS :Array:.<string:>

Operators sorted longest-first, so >>>= is matched before >>>.

Type:
  • Array:.<string:>
Source:

(inner, constant) UNARY_OPERATORS :Set:.<string:>

Prefix operators.

Type:
  • Set:.<string:>
Source:

Methods

(static) parseExpression(source) → {object}

Parses an expression into an AST.

Parameters:
Name Type Description
source string

The expression source.

Source:
Throws:

When the source is outside the supported language.

Type
ExpressionParseError
Returns:

The root AST node.

Type
object

(static) parseExpressionProgram(source) → {object}

Parses a semicolon-separated run of expression statements.

Most inline handlers and short action bodies are one or more expressions: count++, busy = true; count++, state.text = event.target.value. Those take the AST path and need no eval. A body with real statement syntax -- if, for, return, a declaration -- is not an expression program and raises, so the caller falls back.

Parameters:
Name Type Description
source string

The statement source.

Source:
Throws:

When the source is not a run of expressions.

Type
ExpressionParseError
Returns:

A Program node.

Type
object

(static) tokenize(source) → {Array:.<object:>}

Turns an expression into tokens.

Template literals are tokenised whole and re-parsed by the parser, because their ${} holes contain expressions and nesting them in the tokenizer would require a second stack for no benefit.

Parameters:
Name Type Description
source string

The expression source.

Source:
Throws:

On an unterminated string or unknown character.

Type
ExpressionParseError
Returns:

The token stream, terminated by an eof token.

Type
Array:.<object:>

(inner) escapeLength(source, i) → {number}

How many source characters an escape sequence occupies.

Parameters:
Name Type Description
source string

The source.

i number

Offset of the backslash.

Source:
Returns:

The sequence length including the backslash.

Type
number

(inner) isIdentPart(ch) → {boolean}

Whether a character can continue an identifier.

Parameters:
Name Type Description
ch string

A single character.

Source:
Returns:

True when it can continue an identifier.

Type
boolean

(inner) isIdentStart(ch) → {boolean}

Whether a character can start an identifier.

Parameters:
Name Type Description
ch string

A single character.

Source:
Returns:

True when it can begin an identifier.

Type
boolean

(inner) readTemplateLiteral(source, start) → {Object}

Reads a template literal, tracking nested braces and strings inside holes.

Parameters:
Name Type Description
source string

The source.

start number

Offset of the opening backtick.

Source:
Throws:

When the literal is unterminated.

Type
ExpressionParseError
Returns:

The literal text and the offset after it.

Type
Object

(inner) unescapeChar(source, i) → {string}

Resolves an escape sequence to the character it denotes.

Parameters:
Name Type Description
source string

The source.

i number

Offset of the backslash.

Source:
Returns:

The unescaped character.

Type
string