Repository navigation
v0.27.0
Compile-time checks and constant folding follow PHP's for each version, and the printer keeps code with a syntax error invalid.
Added
ForbiddenRule::UnexpectedCast,ForbiddenRule::DefaultValueType,ForbiddenRule::ClassFetchOnFoldedValue,ForbiddenRule::UnsupportedOperandTypes,ForbiddenRule::PrivateFinalPropertyHook,ForbiddenRule::SetHookParameterDefault,ForbiddenRule::PartialApplicationWithUnpacking,ForbiddenRule::EnumCaseValueType,ForbiddenRule::DuplicateEnumCaseValue,ForbiddenRule::CustomAssertFunction,ForbiddenRule::PositionalAfterUnpacking,ForbiddenRule::MethodNameNotString,ForbiddenRule::LongArrayInDestructuring,ForbiddenRule::UnpackNonArrayandForbiddenRule::UnpackStringKeys(php-rs-parser).ForeachStmt::key_by_reffor a key taken by reference,invalid_modifieron a trait alias forstatic,abstractorreadonly, andPropertyHook::empty_param_listfor a hook written with()(php-ast).PrettyPrinter::syntax_errors: for source with a syntax error, the output starts with a syntax error so it stays invalid (php-printer).ParseError::phasetells syntax errors from compile errors (php-rs-parser).
Changed
new Foo(...)and#[Foo(...)]keep the...as anArgKind::SpreadPlaceholderargument instead of no arguments (php-rs-parser).
Fixed
Parser: invalid code PHP rejects
These constructs were accepted and are now reported with PHP's wording (php-rs-parser):
- Names and imports: an unqualified import named by a keyword, such as
use Class;,use A\{B, List};,use const Var;or, from PHP 8.0,use function match;; ausewhose implicit alias is a special class name, as inuse A\int;,use A\{self};or, from PHP 8.5,use A\array;(Cannot use A\int as int because 'int' is a special class name); a trait named after a reserved type, such astrait int {}or, from PHP 8.0,trait mixed {}; and a reserved keyword where PHP expects a class name, as innew fn,new match,new array,catch (array $e),extends fnoruse fn;in a class body. readonlyafternewstarts an anonymous class from PHP 8.1, sonew readonly;reports the missingclassandnew readonly readonly class {}reportsMultiple readonly modifiers are not allowed.- Syntax: a cast token where no cast can start, as in
foo(int),if (int) {}or, from PHP 8.5,foo(void)(unexpected token "(int)"); an intersection type without parentheses in a union, as inA&B|C $aor: A&B|C; and a number offset in simple interpolation with a misplaced_, as in"$a[1_]","$a[1__2]"or"$a[0x_1]", or an0ooctal before PHP 8.1. - Before PHP 8.4, an empty curly brace offset, as in
$a{}or$a{}->b(expected expression). - Before PHP 8.4, calling a new object without wrapping parentheses (
new Foo()()) and dereferencing a new anonymous class (new class {}->foo,new class {}()) ('dereferencing a new expression without parentheses' requires PHP 8.4 or higher). - Inline HTML before the first namespace, as in
x<?php namespace A;, or after a braced namespace. - Default values: a literal default that a parameter's or property's type rejects, as in
int $x = 'a',array $a = 1,Foo $f = 1or a propertyint $p = null, with PHP 7.4's wording there and PHP 8.1'sCannot use null as default valuefor an intersection type. A typed class constant also checks array values, and messages spelliterableasTraversable|arrayonly from PHP 8.2. - Constant expressions fold only what PHP folds: literals and their arithmetic and concatenation, magic constants,
X::class, and outside parameter defaults the internal constants PHP substitutes (unqualified outside a namespace, or fully qualified) and earlier constants of the class being declared. Soconst A = FOO ?? $x;,FOO ? 1 : $x,-0.0 || $x,(1-1) ? 1 : $x,function f($a = PHP_INT_MAX ?? $x),(B)::C,B::C::D,new (B)and('a' ? 'b' : 'c')::classare reported. - From PHP 8.0,
::classon a parenthesized expression PHP folds to a value, such as(-1)::class,(1 < 2)::class,(1 / 2)::class,(__NAMESPACE__)::class,(PHP_INT_MAX)::class, comparisons of arrays such as([] == [])::class, build flags such as(ZEND_THREAD_SAFE)::class(on boolfrom 8.3, since the value depends on the build), more constants of the extensions every build has such as(HASH_HMAC)::classor, before PHP 8.5 deprecates it,(MHASH_SHA1)::class, arithmetic on numeric strings such as(1 + '1')::class, operator chains of any length or, before 8.2,(1 . 2)::class(Cannot use "::class" on value of type int,on intfrom 8.3). - From PHP 8.5,
!on a float is not folded, so!1.5 and isset(1 + 1);andconst A = !1.5 && $a;are reported. - On PHP 7.4, folded arithmetic on an array literal, such as
[] - 1,[1] + 1or-[], and~ontrue,false,null, a folded!such as~!2, or an array (Unsupported operand types). - Enum members read their modifiers like class members, so
public public function,public private function,final final functionandpublic(set) constin an enum are reported instead of passing or cascading. Abstract enum methods report PHP's message for the version (Enum method E::f() must not be abstractfrom PHP 8.5).private(set)on a method or constant reportsCannot use the private(set) modifier on a method. - On PHP 7.4, an abstract private method in a trait (
Abstract function T::f() cannot be declared private). - On PHP 8.1, a literal backed enum case value of the wrong type or one that repeats an earlier case (
Enum case type string does not match enum backing type int,Duplicate value in enum E for cases A and B). - Property hooks: a
finalhook on a private property (Property hook cannot be both final and private), asethook parameter with a default value (Parameter $value of set hook A::$x must not have a default value), a barereturn;in agethook of a typed property (A method with return type must return a value) and a returned value in asethook (A void method must not return a value). - A
returninside adeclareblock that the return type rejects, as infunction f(): int { declare(ticks=1) { return; } }(A function with return type must return a value). - From PHP 8.0, a
nulldefault on a promoted parameter whose type is not nullable, as inpublic function __construct(private int $a = null)(Cannot use null as default value for parameter $a of type int), since promotion does not make the type implicitly nullable. - A class property of type
arraywithout a modifier, as inarray $a;(expected modifier). - An empty
declaredirective, as indeclare(strict_types=1, , ticks=1);(expected identifier, whichdeclare(at the end of the file also reports instead ofexpected ')'). - From PHP 8.6, a placeholder with argument unpacking, as in
f(...$a, ?)(Cannot combine partial application and unpacking), in an attribute argument list (Cannot create Closure as attribute argument) or innew A(?)(Cannot create Closure for new expression). - From PHP 8.5,
#[\NoDiscard]on a magic method that returns nothing:__destruct,__clone,__set,__unset,__unserializeor__wakeup(Method A::__clone cannot be #[\NoDiscard]). - From PHP 8.0, a function named
assert, as innamespace A; function assert() {}(Defining a custom assert() function is not allowed, as the function has special semantics). - A positional argument after argument unpacking, as in
var_dump(...[1, 2, 3], 4);(Cannot use positional argument after argument unpacking). - A method name PHP folds to a value other than a string, as in
$o->{1}(),$o->{true}(),A::{1 + 1}(),$o->{[]}()or$o->{[] + [1]}()(Method name must be a string). - Unpacking a constant that is not an array in an array literal PHP folds, as in
[...1],const C = [...true];or, with the?:and??PHP folds there,[...(false ? [1] : 2)]and[...(null ?? 1)](Only arrays and Traversables can be unpacked, with, int givenfrom PHP 8.4). - Before PHP 8.1, unpacking an array with a string key in an array literal PHP folds, as in
[...['a' => 1]]orconst C = [...[null => 1]];(Cannot unpack array with string keys). - From PHP 8.5, a first-class callable in a constant expression named by a number, as in
const C = (0)(...);(Illegal function name), or by a folded expression such as(1 + 1)(...)or(true)(...)(Cannot use dynamic function name in constant expression). - Writing to or taking a reference to the result of a call PHP compiles as a built-in, as in
strlen("foo")[0] = 1;,$x = &count($a);or, from PHP 8.4,sprintf("%s", $a)[0] = 1;, and from PHP 8.6 the functions it calls without a frame, such asstr_contains($a, $b)[0] = 1;, also when imported, as innamespace A; use function strlen; strlen("foo")[0] = 1;(Cannot use result of built-in function in write context). - Passing a fetch from the result of a call PHP compiles as a built-in to a by-reference parameter of a function declared earlier in the file, as in
function f(&$a) {} f(strlen("foo")[0]);, or before PHP 8.1 to any call PHP does not know takes it by value, as in$o->m(strlen("foo")[0]);(Cannot use result of built-in function in write context). - An append fetch passed to a parameter that takes its value, as in
function test($v) {} test($arr[]);, when the function is declared unconditionally at the top level earlier in the file or is an internal function of the extensions every PHP build has, as instrlen($a[]);(Cannot use [] for reading). - A nested destructuring target written with
array(), as in[array($a)] = $b;(Cannot assign to array(), use [] instead), and a foreach target that mixeslist()and[], as inforeach ($x as [list($a)])(Cannot mix [] and list()). - From PHP 8.3, a static variable named like a
usevariable of its closure, as infunction () use (&$a) { static $a; }(Duplicate declaration of static variable $a). - A branch PHP keeps after folding
__CLASS__in a constant expression, as inclass C { const X = __CLASS__ ? $a : 1; }or, in a function declared inside a method, where__CLASS__is empty,function f($p = __CLASS__ ? 1 : $a) {}(Constant expression contains invalid operations). - The right operand of
&&,||,andororwhen PHP folds the left one to a value that does not skip it, as in1 < 2 && isset(1 + 1);orconst C = __CLASS__ && $x;, and in a parameter default after an internal constant, which PHP does not substitute there, as infunction f($p = PHP_EOL !== 1 && $x) {}.
Parser: valid code PHP accepts
These constructs were reported and are now accepted, as PHP does (php-rs-parser):
- On PHP 7.4, names PHP 8 reserves:
match, sofunction match() {},match($x);,Match::X,class Match {},const MATCH = 1;and the labelmatch:are valid and onlymatch (...) {reports that match expressions need PHP 8.0;mixed, soclass mixed {},use A\B as mixed;andfunction f(mixed $a)no longer report'mixed type' requires PHP 8.0; andreadonlyfollowed by\, soprivate readonly \Closure $x;declares a property of typereadonly\Closure. - On PHP 7.4, checks PHP 7.4 leaves to run time or does not have: functions, imports and classes, interfaces and traits that PHP binds at compile time may come before the first
namespacedeclaration;parentin a class without a parent, as inparent::f(),new parentor aparenttype, which no longer reportsCannot use "parent" when current class scope has no parent;::classon a string literal or an unparenthesizedtrue,falseornull, as in"Foo"::classor('Foo')::class; and parameter and return types of magic methods. The magic method checks PHP 7.4 has (static or typed constructor, destructor and__clone, argument counts, by-reference arguments) use PHP 7.4's messages. - On PHP 7.4, strings lexed as PHP 7.4 does: a control character before or after a simple string offset, as in
"$a[\x01x]", is skipped with a warning, and#[inside a{$…}interpolation starts a comment, so"{$a[1 #[xfollowed by a newline and]}"reads$a[1]. - Before PHP 8.3,
yieldandfromseparated by a comment areyieldof the constantfrom, soyield /**/ from [1];yieldsfrom[1]andyield/**/from;is valid. - Before PHP 8.4,
parent::$prop::get()is an ordinary static call on a static property, so outside a class it no longer reportsCannot use "parent" when no class scope is activeor the property hook call errors. - On PHP 8.0 and 8.1, yielding a nullsafe chain from a by-reference generator (
function &f() { yield $a?->b; }); PHP checks it from 8.2. - On PHP 8.0, destructuring a nullsafe chain by reference, as in
[&$y] = $y->y?->y;; PHP checks it from 8.1. - On PHP 8.0 to 8.3, a curly brace offset that is the base of another fetch or call, as in
$a{0}->b,$a{0}[1],$a{0}::$cor$a{0}->m(); PHP checks it only where it folds an array literal or a PHP 8.3staticinitializer ([$a{0}->b]from PHP 8.2). - From PHP 8.6, a pipe is compiled as the call it makes, so its result can be written to like a call's, as in
(1 |> f(...))->p ??= 99;and(1 |> f(...))[0] = 1;. - The right operand of
&&,||,andororno longer reports compile errors when PHP may fold the left operand away. Aprintexpression, literal arithmetic and concatenation,exitbefore PHP 8.4,empty()of a literal and a literalinstanceoffold exactly, as inprint $a or isset(1 + 1);and-1 || $a ? 1 : 2 ? 3 : 4;; comparisons and other operators on literals fold as PHP computes them, as in1 > 2 && isset(1 + 1);, and a condition whose value is not computed, such asM_PI,+print $aorself::classin a class, is assumed to skip it. - An append fetch passed by name to a variadic parameter or after argument unpacking, which PHP binds at run time, as in
f(v: $a[])forfunction f($x, ...$v)orf(...$y, x: $a[]), no longer reportsCannot use [] for reading. ++and--bind tighter than**, so--$a ** 2parses as(--$a) ** 2instead of reportingCannot increment or decrement an expression that is not a variable.
Parser: wrong trees
- On PHP 7.4,
.has the precedence of+and-and binds tighter than<<and>>, so"a" . 1 + 2parses as("a" . 1) + 2and1 . 2 << 1as(1 . 2) << 1(php-rs-parser). - A doc comment written after the attributes of a class or enum member or a parameter, as in
#[A] /** doc */ public function f(), belongs to that declaration, as PHP reads it, instead of being dropped or moved to the next member (php-rs-parser).
Parser: messages and recovery
- Before PHP 8.2, a trait constant reports only that constants in traits require PHP 8.2, not errors in its declaration, such as
Constant expression contains invalid operations, since PHP stops atTraits cannot have constants(php-rs-parser). - Nesting past the depth limit reports one
NestingTooDeeperror instead of a cascade of up to 100 errors (php-rs-parser). - Errors that a later check withdraws, such as an empty array element before
=makes the array a destructuring target, no longer count toward the 100-error limit, so they do not crowd out later errors or seterrors_truncated(php-rs-parser). - Modifier errors use PHP's wording for the version:
Cannot use 'static' as constant modifierbefore PHP 8.3 andCannot use the static modifier on a class constantfrom it, likewise forabstractandreadonlyconstants and readonly methods, andCannot use the final modifier on an abstract methodorabstract class member(php-rs-parser). - A value on a case of a pure enum says
non-backed enumwith the namespaced name, and from PHP 8.6 asymmetric visibility on a virtual property saysget-onlyorset-only, as PHP does (php-rs-parser). - On PHP 8.4,
#[\Deprecated]on a class, interface, trait or enum reports PHP'sAttribute "Deprecated" cannot target class (allowed targets: function, method, class constant); classes and constants are targets from PHP 8.5. Attributes on a constant before PHP 8.5 report only the version error (php-rs-parser).
Printer
- The printer keeps PHP's error instead of printing valid code for a foreach key taken by reference, a trait alias with
static,abstractorreadonly,new Foo(...),#[Foo(...)], a property hook with an empty(), a redeclared hook spelled in another case, such asset {} SET {}, and, when given the source, a destructuring target that mixeslist()and[](php-printer). - When printing for PHP 7.4,
.is parenthesized at the precedence of+and-, so1 . (2 << 1)keeps its parentheses (php-printer). - A bare
true,falseornullbefore::is printed without parentheses, sotrue::classis not printed as the invalid(true)::class(php-printer). - A
yieldkey or value that starts with the constantfromis parenthesized, so it is not printed asyield from(php-printer). - A blank line between a doc comment and its
useno longer splits a run of imports, so sorting them is stable on a second print (php-printer). - A comment after the comma of a grouped property or constant, as in
public $a, /* a */ $b;, is printed before the member it precedes instead of moving on each print (php-printer). - A nested
array()in a destructuring target keeps that spelling, so[array($a)] = $b;is not printed as the valid[[$a]] = $b;(php-printer). - With
PrettyPrinter::syntax_errors(true), source with a syntax error no longer prints as valid code, as it did forvar public $x;, a trait alias whose name recovery dropped, an error node printed as/* error */, a heredoc whose closing marker is indented past its body, and$a = 1 $b = 2;. The playground passes it (php-printer,php-wasm).