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
Members
(inner, constant) ASSIGNMENT_OPERATORS :Set:.<string:>
Assignment operators.
Type:
- Source:
(inner, constant) BINARY_PRECEDENCE :Object:.<string:, number:>
Binary operator precedence, higher binds tighter.
Type:
- Source:
(inner, constant) LITERAL_KEYWORDS :Object:.<string:, any:>
Keywords that are values rather than identifiers.
Type:
- Source:
(inner, constant) PUNCTUATORS :Array:.<string:>
Operators sorted longest-first, so >>>= is matched before >>>.
Type:
- Source:
(inner, constant) UNARY_OPERATORS :Set:.<string:>
Prefix operators.
Type:
- 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.
(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