Repository navigation
Major release with breaking changes. The biggest one: operators now follow Symfony's (PHP 8's) rules by default. Read the upgrade guide before upgrading;
new ExpressionLanguage(null, [], { semantics: 'js' })restores the old behaviour, and'portable'finds what depends on it.Previously available as
3.0.0-beta.1.
This release brings the library back in line with Symfony's ExpressionLanguage, closes a hole that let expressions read
the host's globals, and fixes compile() output that was not valid JavaScript. It contains breaking changes.
Everything below is explained, with before/after and how to keep the old behaviour, in
UPGRADE-3.0.md.
⚠️ Breaking changes
Security
constant()andenum()are no longer built in. They used to resolve any name onglobalThis
(constant("process.env.HOME")worked). Registernew ConstantFunctionProvider([...allowed names])to get them
back, restricted to what you list. Unknown constants now throw instead of returningundefined, and the fallback to
thevaluesobject is gone.evaluate()refusesconstructor,prototype,__proto__,__defineGetter__, ... as property, method or item
(Access to "constructor" is not allowed.), unless the object owns such a property.
Behaviour changes (these now match Symfony)
-
Operators follow Symfony's (PHP 8's) rules by default (
semantics: 'symfony')."5" + 1is6(it was"51"),"abc" + 1is
aTypeError,null == falseistrue,"10" < "9"isfalse,"0"and[]are falsy,[1, 2] === [1, 2]istrue,
bitwise operators work on 64 bits,true ~ ""is"1", andlist.lengthis an error (an array is not an object in PHP).
Values of the type you expect behave as before. To get JavaScript's rules back:new ExpressionLanguage(null, [], { semantics: 'js' }).
'portable'applies both and throws aPortabilityErrorwhere they would differ: use it in your tests to find what depends on the rules.
Verified against Symfony over every operator and 20 kinds of operands (11,780 cases). -
compile()needsCompileRuntimeas__runtime(__runtime.symfony.add(a, b)), unless the semantics are'js'. -
contains,starts withandends withare now case-sensitive (they ignored case). Restore the old behaviour
withnew ExpressionLanguage(null, [], { caseInsensitiveStringOperators: true })or the
CASE_INSENSITIVE_STRING_OPERATORSflag. -
and/or/&&/||/xorreturn booleans, not one of their operands (0 || 5istrue, was5). -
/and%by zero throwDivisionByZeroError(they gaveInfinity/NaN). -
a ? b(no:) now has anullelse branch (it wasa ? a : b, anda ? "x"wasa ? "x" : ""). -
String literals are decoded like PHP's
stripcslashes():"\n"is a newline and an unknown escape loses its
backslash. Backslashes meant for a regex must be doubled ("/^\\d+$/"). -
matches: any delimiter, modifiersi m s uonly (gis rejected),SyntaxErrorfor an invalid pattern,
nullmatches as an empty string. -
..(range) counts down (3..1is[3, 2, 1]), follows PHP'srange()for numeric strings and characters ("1".."3"is[1, 2, 3],"a".."e"are the letters), refuses an array,
and a range of more than 10 million items is aValueError(it used to try to build it). The subject ofmatchescannot be an array (aTypeError, as in Symfony). -
in/not inthrow when the right operand is not an array or hash ("a" in "abc"used to work). -
min()/max()accept an array like PHP's and throw like PHP's on empty or non-array input
(min([3, 1])wasNaN). -
"a" ~ nullis"a"(it threw). -
3..1is[3, 2, 1](it was[]). -
Lexer:
not(x),a and(b)and1.now work; a word operator glued to a)((a)and b) no longer does. -
Nesting limit is now Symfony's: 256 levels of depth, as a
SyntaxError. It used to be about 1,000 sub-expressions
in total, as a plainError, so long flat lists now work and very deep nesting does not. -
An unclosed
/*is a syntax error (it was silently ignored). -
SyntaxError#messagenow includes the position, the expression and the "Did you mean" hint (it was only in
toString()). Several runtime error messages were reworded. -
Methods are called on their object:
thisworks in a method called from an expression (it wasnull). -
compile()output changed for~,xor,and/or,matches, the string operators,in,..,/,%,
??,min/max/count, hash literals and strings (see the table in the upgrade guide). -
GetAttrNode#attributes.is_short_circuitedwas removed; thenamesarray passed toparse()is no longer
reordered; the parse cache now takes the flags into account.
✨ New
expression-language-portability, a command of the package (npx expression-language-portability): finds the expressions of a code base, tells which ones do not mean the
same thing in PHP and in JavaScript, and can compare this library's answers with Symfony's own PHP for them.--fail-onmakes it usable in CI. See the README.CompiledExpressionLanguage: compile expressions ahead of time and evaluate them without parsing.dumpCompiled()
generates JavaScript (default; as an ES module, a CommonJS module or a bare expression) or PHP ({ target: 'php' }),
in the format Symfony's ownCompiledExpressionLanguageloads. See the README.- The
semanticsoption ('symfony'|'js'|'portable'), theSEMANTICS_JS/SEMANTICS_PORTABLEflags,PortabilityError.
ACompiledExpressionLanguagedump records the rules it was made with; a PHP dump needs'symfony'or'portable'. ExpressionFunctiontakes an optional PHP compiler (4th argument) and haswithPhpFunction();register()takes it too.
The PHP-named functions of the bundled providers,min,maxandcountalready declare it.- Null-safe array access:
foo?.[0]. count()built-in.ConstantFunctionProvider(allow-listedconstant()/enum()).caseInsensitiveStringOperatorsoption andCASE_INSENSITIVE_STRING_OPERATORSflag.ExpressionLanguageaccepts any iterable of providers, and anoptionsargument.- New exports:
LogicException,DivisionByZeroError,ConstantFunctionProvider,CASE_INSENSITIVE_STRING_OPERATORS. - A parity test suite that compares 400+ expressions with Symfony's own results, and
scripts/generate-parity-fixtures.phpto regenerate its fixtures.scripts/verify-compiled-php.phpchecks the PHP dump
with Symfony's own loader. Parser#getVariables();PhpCompileris exported.
🐛 Fixed
compile()generated invalid JavaScript for~,xor,matches,contains/starts with/ends with, hash
literals with computed keys, strings containing newlines or control characters,unknown ?? xanda.b.c ?? x.- A short-circuited
?.could leak into a later evaluation of the same parsed expression. - Parsing with
IGNORE_UNKNOWN_VARIABLEScould make a later strict parse of the same expression succeed (cache key
ignored the flags). - An unterminated string followed by another quoted string was mis-lexed.
- "Did you mean ...?" was never offered for unknown function names.
📦 Package contents
The package now publishes an allowlist of files (files in package.json, replacing .npmignore): lib, dist, bin, this changelog and the upgrade guide. The compiled tests,
CI workflows, build configuration and examples are no longer in the tarball (286 KB instead of 602 KB). Nothing under lib/ or dist/ moved, so imports of the package are unaffected.
Still different from Symfony
Numbers are doubles (1 === 1.0, integers above 2^53 are rounded), regexes are ECMAScript's, and a hash and a class instance are both objects.
See the README.