Skip to content

v0.27.0

Choose a tag to compare

@github-actions github-actions released this 28 Oct 23:10
· 18 commits to main since this release
ae4aefb

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::UnpackNonArray and ForbiddenRule::UnpackStringKeys (php-rs-parser).
  • ForeachStmt::key_by_ref for a key taken by reference, invalid_modifier on a trait alias for static, abstract or readonly, and PropertyHook::empty_param_list for 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::phase tells syntax errors from compile errors (php-rs-parser).

Changed

  • new Foo(...) and #[Foo(...)] keep the ... as an ArgKind::SpreadPlaceholder argument 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;; a use whose implicit alias is a special class name, as in use 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 as trait int {} or, from PHP 8.0, trait mixed {}; and a reserved keyword where PHP expects a class name, as in new fn, new match, new array, catch (array $e), extends fn or use fn; in a class body.
  • readonly after new starts an anonymous class from PHP 8.1, so new readonly; reports the missing class and new readonly readonly class {} reports Multiple 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 in A&B|C $a or : 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 an 0o octal 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 = 1 or a property int $p = null, with PHP 7.4's wording there and PHP 8.1's Cannot use null as default value for an intersection type. A typed class constant also checks array values, and messages spell iterable as Traversable|array only 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. So const 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')::class are reported.
  • From PHP 8.0, ::class on 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 bool from 8.3, since the value depends on the build), more constants of the extensions every build has such as (HASH_HMAC)::class or, 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 int from 8.3).
  • From PHP 8.5, ! on a float is not folded, so !1.5 and isset(1 + 1); and const A = !1.5 && $a; are reported.
  • On PHP 7.4, folded arithmetic on an array literal, such as [] - 1, [1] + 1 or -[], and ~ on true, 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 function and public(set) const in 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 abstract from PHP 8.5). private(set) on a method or constant reports Cannot 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 final hook on a private property (Property hook cannot be both final and private), a set hook parameter with a default value (Parameter $value of set hook A::$x must not have a default value), a bare return; in a get hook of a typed property (A method with return type must return a value) and a returned value in a set hook (A void method must not return a value).
  • A return inside a declare block that the return type rejects, as in function f(): int { declare(ticks=1) { return; } } (A function with return type must return a value).
  • From PHP 8.0, a null default on a promoted parameter whose type is not nullable, as in public 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 array without a modifier, as in array $a; (expected modifier).
  • An empty declare directive, as in declare(strict_types=1, , ticks=1); (expected identifier, which declare( at the end of the file also reports instead of expected ')').
  • 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 in new A(?) (Cannot create Closure for new expression).
  • From PHP 8.5, #[\NoDiscard] on a magic method that returns nothing: __destruct, __clone, __set, __unset, __unserialize or __wakeup (Method A::__clone cannot be #[\NoDiscard]).
  • From PHP 8.0, a function named assert, as in namespace 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 given from PHP 8.4).
  • Before PHP 8.1, unpacking an array with a string key in an array literal PHP folds, as in [...['a' => 1]] or const 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 as str_contains($a, $b)[0] = 1;, also when imported, as in namespace 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 in strlen($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 mixes list() and [], as in foreach ($x as [list($a)]) (Cannot mix [] and list()).
  • From PHP 8.3, a static variable named like a use variable of its closure, as in function () use (&$a) { static $a; } (Duplicate declaration of static variable $a).
  • A branch PHP keeps after folding __CLASS__ in a constant expression, as in class 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 &&, ||, and or or when PHP folds the left one to a value that does not skip it, as in 1 < 2 && isset(1 + 1); or const C = __CLASS__ && $x;, and in a parameter default after an internal constant, which PHP does not substitute there, as in function 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, so function match() {}, match($x);, Match::X, class Match {}, const MATCH = 1; and the label match: are valid and only match (...) { reports that match expressions need PHP 8.0; mixed, so class mixed {}, use A\B as mixed; and function f(mixed $a) no longer report 'mixed type' requires PHP 8.0; and readonly followed by \, so private readonly \Closure $x; declares a property of type readonly\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 namespace declaration; parent in a class without a parent, as in parent::f(), new parent or a parent type, which no longer reports Cannot use "parent" when current class scope has no parent; ::class on a string literal or an unparenthesized true, false or null, as in "Foo"::class or ('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 #[x followed by a newline and ]}" reads $a[1].
  • Before PHP 8.3, yield and from separated by a comment are yield of the constant from, so yield /**/ from [1]; yields from[1] and yield/**/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 reports Cannot use "parent" when no class scope is active or 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}::$c or $a{0}->m(); PHP checks it only where it folds an array literal or a PHP 8.3 static initializer ([$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 &&, ||, and or or no longer reports compile errors when PHP may fold the left operand away. A print expression, literal arithmetic and concatenation, exit before PHP 8.4, empty() of a literal and a literal instanceof fold exactly, as in print $a or isset(1 + 1); and -1 || $a ? 1 : 2 ? 3 : 4;; comparisons and other operators on literals fold as PHP computes them, as in 1 > 2 && isset(1 + 1);, and a condition whose value is not computed, such as M_PI, +print $a or self::class in 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[]) for function f($x, ...$v) or f(...$y, x: $a[]), no longer reports Cannot use [] for reading.
  • ++ and -- bind tighter than **, so --$a ** 2 parses as (--$a) ** 2 instead of reporting Cannot 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 + 2 parses as ("a" . 1) + 2 and 1 . 2 << 1 as (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 at Traits cannot have constants (php-rs-parser).
  • Nesting past the depth limit reports one NestingTooDeep error 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 set errors_truncated (php-rs-parser).
  • Modifier errors use PHP's wording for the version: Cannot use 'static' as constant modifier before PHP 8.3 and Cannot use the static modifier on a class constant from it, likewise for abstract and readonly constants and readonly methods, and Cannot use the final modifier on an abstract method or abstract class member (php-rs-parser).
  • A value on a case of a pure enum says non-backed enum with the namespaced name, and from PHP 8.6 asymmetric visibility on a virtual property says get-only or set-only, as PHP does (php-rs-parser).
  • On PHP 8.4, #[\Deprecated] on a class, interface, trait or enum reports PHP's Attribute "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, abstract or readonly, new Foo(...), #[Foo(...)], a property hook with an empty (), a redeclared hook spelled in another case, such as set {} SET {}, and, when given the source, a destructuring target that mixes list() and [] (php-printer).
  • When printing for PHP 7.4, . is parenthesized at the precedence of + and -, so 1 . (2 << 1) keeps its parentheses (php-printer).
  • A bare true, false or null before :: is printed without parentheses, so true::class is not printed as the invalid (true)::class (php-printer).
  • A yield key or value that starts with the constant from is parenthesized, so it is not printed as yield from (php-printer).
  • A blank line between a doc comment and its use no 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 for var 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).