1
0
mirror of https://github.com/nikic/PHP-Parser.git synced 2025-07-22 23:01:26 +02:00

Compare commits

...

59 Commits

Author SHA1 Message Date
Nikita Popov
6608f01670 Release PHP-Parser 4.12.0 2021-07-21 12:44:31 +02:00
Máté Kocsis
55c4269232 Add support for new PHP 8.1 modifiers ()
Implement support for readonly properties (https://wiki.php.net/rfc/readonly_properties_v2) and
final class contstants (https://wiki.php.net/rfc/final_class_const).
2021-07-21 12:43:29 +02:00
Nikita Popov
c4304c76bd Try to pass --ignore-platform-req=php on 8.1 only 2021-07-21 12:11:59 +02:00
Nikita Popov
b099e8fc76 Pass --ignore-platform-req=php 2021-07-21 12:08:11 +02:00
Nikita Popov
acf16edc8e Add PHP 8.1 to GH actions 2021-07-21 11:58:38 +02:00
Nikita Popov
a8b5ed4306 Fix JSON encoding test on PHP 8.1
Duplicate the test expectation for a different order.
2021-07-21 11:49:28 +02:00
Nikita Popov
c758510a37 Add support for PHP 8.1
With the introduction of intersection types, PHP now lexes the
token '&' either as T_AMPERSAND_(NOT_)FOLLOWED_BY_VAR_OR_VARARG.
This completely breaks parsing of any code containing '&'.

Fix this by canonicalizing to the new token format (unconditionally,
independent of emulation) and adjusting the parser to use the two
new tokens.

This doesn't add actual support for intersection types yet.
2021-07-09 16:52:58 +02:00
Nikita Popov
feed91cf0f Avoid ctype_alnum() on integer
This is deprecated in PHP 8.1
2021-07-09 15:46:50 +02:00
Nikita Popov
fe14cf3672 Release PHP-Parser 4.11.0 2021-07-03 15:36:55 +02:00
Bob Weinand
3fb73520c1 Add handling for Enum(Case)s in NameResolver 2021-07-03 15:09:11 +02:00
simivar
c35cc4b2cb Add support for "never" type in the BuilderHelpers::normalizeType() 2021-06-18 17:25:28 +02:00
Rod Elias
e69ebbbfd9 chore: use the word Xdebug instead of XDebug 2021-06-17 18:00:12 +02:00
simivar
0b258d9a9e Add missing tests for methods of BuilderHelpers 2021-06-17 17:55:59 +02:00
simivar
2d193bb0e4 Add attributes to integration Builder test 2021-06-17 09:18:53 +02:00
Krystian Marcisz
49e9951f2c Add addAttribute() method to Builders with normalizer ()
Adds addAttribute() method to Builders of all nodes supporting attributes with BuilderHelpers::normalizeAttribute() usage inside so we can pass both Node\Attribute and Node\AttributeGroup instances.
2021-06-16 17:47:22 +02:00
Krystian Marcisz
eccf1bf464 Add Attribute builder and support for named args ()
Add BuilderFactory::attribute() and support named args in BuilderFactory::args().
2021-06-14 23:52:57 +02:00
Tom Klingenberg
b67560d388 Update CHANGELOG.md
very minor for the year.
2021-05-04 22:15:00 +02:00
Nikita Popov
4432ba399e Release PHP-Parser 4.10.5 2021-05-03 21:11:20 +02:00
Nikita Popov
37ac4ea9c2 Document that namespaced names containing whitespace are not supported 2021-04-25 22:47:15 +02:00
Nikita Popov
4848a0d734 Check for \r as newline in closing tag
Fixes .
2021-04-25 22:42:18 +02:00
Nikita Popov
8eb194ea1f Add never type
This should be recognized as Identifier instead of Name now.
2021-04-25 22:35:28 +02:00
Nikita Popov
e03d63cffb Fix precedence of arrow functions
Arrow functions should have lowest precedence.

Fixes .
2021-04-25 22:19:49 +02:00
Nikita Popov
ce91d139b5 Make sure match is one character long 2021-04-25 21:47:07 +02:00
Nikita Popov
33d7c8d3d8 Escape invalid UTF-8 in strings
To be friendlier to tooling that expects PHP files to be UTF-8
encoded, escape any sequences that are not legal under UTF-8.
2021-04-25 21:37:01 +02:00
Nikita Popov
6b409b96bb Use hex escaping for special characters in strings
Apart from \0, using the \xHH notation is more typical.
2021-04-25 21:22:15 +02:00
Tomas Votruba
f68e1a43ff [PHP 8.1] Add support for enums ()
RFC: https://wiki.php.net/rfc/enumerations

Co-authored-by: Nikita Popov <nikita.ppv@gmail.com>
2021-04-25 21:11:36 +02:00
Iskander (Alex) Sharipov
f767b9fd9f ParserAbstract: add missing '*' to the phpdoc
Otherwise, it's somewhat incompatible with the phpdoc definition.
2021-04-25 12:38:52 +02:00
Miguel
2a4bb7ef2c Moved phpy pseudo lang functions to separate file 2021-04-10 13:40:52 +02:00
Maxim L
4abdcde5f1 Fix missing argument in documentation ()
Missing argument in the getMessageWithColumnInfo call
2021-03-25 18:37:06 +01:00
WhizSid
d46f261ef9 Adding class constant to builder factory ()
* Adding class constant to builder factory

* Fix formatting,doc issues and Tweak returning node from ClassConst

* Fix PHPUnit
2021-03-21 20:58:21 +01:00
Nikita Popov
38aa0920c9 Add test for UTF-8 in string
These should not get escaped.
2021-03-21 17:03:15 +01:00
Nikita Popov
a8223f228a Add emulation for enum keyword 2021-03-20 17:49:44 +01:00
Oskar Stark
8165cf69fa CS fix 2021-02-02 09:57:47 +01:00
Tomas Votruba
46221a0914 Update Use_ builder to return specific type 2021-01-06 14:30:19 +01:00
Tomas Votruba
3b87eb721c make Namespace_ builder return Namespace_
Hi, just a little detail. This should fix invalid return type of docs
2020-12-30 10:13:54 +01:00
Ayesh Karunaratne
e3471d94d3 Update .gitattributes export-ignore rules
- Adds `.github` directory to `.gitattributes` file with an `export-ignore` rule.
 - This also aligns all `export-ignore` rules for more readability.
 - Removes `.travis.yml` file that is no longer used.
2020-12-22 22:58:39 +01:00
Nikita Popov
c6d052fc58 Release PHP-Parser 4.10.4 2020-12-20 11:01:03 +01:00
Nikita Popov
8008d07bef Remove Travis build badge
We no longer use Travis, so this isn't meaningful... And GitHub
already displays the build status, so there's no value in having
this kind of badge at all.
2020-12-20 10:58:16 +01:00
Nikita Popov
7284a4d019 Remove no longer necessary class parsing workaround
This was split to work around the attribute assignment bug fixed
in the previous commit, and as such is no longer necessary.
2020-12-20 10:53:45 +01:00
Nikita Popov
d3d1ee470a Fix incorrect start line for traits
Empty productions are supposed to be assigned the start attributes
of the lookahead token. Currently, this happens by assigning above
the current stack position when the token it read.

This fails in a situation where we first reduce an empty production
higher up in the stack, and then again reduce an empty production
lower in the stack, without consuming the lookahead token in the
meantime.

Fix this by moving the assignment into the reduction phase. We
also need to do this for error productions, which are effectively
empty.
2020-12-20 10:52:47 +01:00
Nikita Popov
893a5bce3f Fix incorrect varvar positions 2020-12-19 22:03:43 +01:00
Nikita Popov
bec74aceda Remove .travis.yml 2020-12-08 23:22:57 +01:00
Nikita Popov
7c09e096c9 Try to re-enable coveralls 2020-12-08 23:17:41 +01:00
Nikita Popov
0f64504317 Drop -n flag from test_old
In the hope of fixing the failure on GH actions.
2020-12-08 23:05:19 +01:00
Matt Glaman
5e36ef732e Add GitHub Actions workflow () 2020-12-08 23:00:10 +01:00
Nikita Popov
dbe56d23de Release PHP-Parser 4.10.3 2020-12-03 18:45:45 +01:00
Tommy Quissens
c64986fa55 Allow both '{' and T_CURLY_OPEN as curly bracket () 2020-12-03 18:42:00 +01:00
Stefano Arlandini
bc7a9bf9c2 Remove useless code left from old implementation 2020-12-02 12:54:15 +01:00
James Titcumb
2816485126 Updated README to indicate that parsing PHP 8.0 code is supported 2020-12-01 15:15:26 +01:00
Anders Jürisoo
1d1bc8a364 Upgrade some PrettyPrinter methods to protected
I noticed some methods in the Standard PrettyPrinter is private, can they be upgraded to `protected` to ease when extending it?
2020-10-17 22:04:40 +02:00
Nikita Popov
d520bc9e1d Don't allow failures on PHP 8.0 integration tests
These are passing now.
2020-09-30 21:15:05 +02:00
Nikita Popov
51e0b30843 Test against 8.0.0rc1
This should fix the remaining test failures.
2020-09-30 20:51:01 +02:00
Nikita Popov
eff72eeffa Fix : PrettyPrinter breaks nested ternaries
Mark ternary as non-associative operator, as left-associative use
is deprecated in PHP 7.4 and removed in PHP 8.0.
2020-09-30 20:42:58 +02:00
Nikita Popov
658f1be311 Release PHP-Parser 4.10.2 2020-09-26 12:30:38 +02:00
Nikita Popov
b9b65a2996 Fix check for token emulation conflicts 2020-09-26 12:29:16 +02:00
Nikita Popov
1b479e7592 Release PHP-Parser 4.10.1 2020-09-23 20:23:49 +02:00
Nikita Popov
b5351f883a Make keyword emulation check case-insensitive 2020-09-23 20:19:40 +02:00
Nikita Popov
88be6127fa FPPP: Fix remove + add at start of list 2020-09-22 22:41:02 +02:00
Nikita Popov
8a97fa157f Recover from missing semicolon after property or class const
Fixes .
2020-09-19 23:11:36 +02:00
87 changed files with 6176 additions and 3686 deletions
.editorconfig.gitattributes
.github/workflows
.travis.ymlCHANGELOG.mdREADME.md
bin
doc
grammar
lib/PhpParser
test
test_old

9
.editorconfig Normal file

@@ -0,0 +1,9 @@
root = true
[*.y]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4

16
.gitattributes vendored

@@ -1,9 +1,9 @@
/doc export-ignore
/test export-ignore
/test_old export-ignore
.gitattributes export-ignore
.gitignore export-ignore
.travis.yml export-ignore
CHANGELOG.md export-ignore
/.github export-ignore
/doc export-ignore
/test export-ignore
/test_old export-ignore
.gitattributes export-ignore
.gitignore export-ignore
CHANGELOG.md export-ignore
phpunit.xml.dist export-ignore
UPGRADE-*.md export-ignore
UPGRADE-*.md export-ignore

90
.github/workflows/main.yml vendored Normal file

@@ -0,0 +1,90 @@
# https://help.github.com/en/categories/automating-your-workflow-with-github-actions
name: Main
on:
push:
pull_request:
jobs:
tests_70:
runs-on: "ubuntu-latest"
name: "PHP 7.0 Unit Tests"
steps:
- name: "Checkout"
uses: "actions/checkout@v2"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
coverage: "xdebug"
php-version: "7.0"
tools: composer:v2
- name: "Install dependencies"
run: |
composer require php-coveralls/php-coveralls:^2.2 --dev --no-update
composer update --no-progress --prefer-dist
- name: "Tests"
run: "php vendor/bin/phpunit --coverage-clover build/logs/clover.xml"
- name: Coveralls
env:
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: "php vendor/bin/php-coveralls"
if: ${{ success() }}
tests:
runs-on: "ubuntu-latest"
name: "PHP ${{ matrix.php-version }} Unit Tests"
strategy:
matrix:
php-version:
- "7.1"
- "7.2"
- "7.3"
- "7.4"
- "8.0"
- "8.1"
include:
- php-version: "8.1"
flags: "--ignore-platform-req=php"
steps:
- name: "Checkout"
uses: "actions/checkout@v2"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
coverage: "none"
php-version: "${{ matrix.php-version }}"
tools: composer:v2
- name: "Install dependencies"
run: "composer update --no-progress --prefer-dist ${{ matrix.flags }}"
- name: "PHPUnit"
run: "php vendor/bin/phpunit"
test_old_73_80:
runs-on: "ubuntu-latest"
name: "PHP 7.3 Code on PHP 8.0 Integration Tests"
steps:
- name: "Checkout"
uses: "actions/checkout@v2"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
coverage: "none"
php-version: "8.0"
tools: composer:v2
- name: "Install PHP 8 dependencies"
run: "composer update --no-progress --prefer-dist"
- name: "Tests"
run: "test_old/run-php-src.sh 7.3.21"
test_old_80_70:
runs-on: "ubuntu-latest"
name: "PHP 8.0 Code on PHP 7.0 Integration Tests"
steps:
- name: "Checkout"
uses: "actions/checkout@v2"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
coverage: "none"
php-version: "7.0"
tools: composer:v2
- name: "Install PHP 8 dependencies"
run: "composer update --no-progress --prefer-dist"
- name: "Tests"
run: "test_old/run-php-src.sh 8.0.0"

@@ -1,43 +0,0 @@
language: php
dist: xenial
before_install: composer self-update --2
install: composer update --no-progress --prefer-dist
matrix:
include:
- name: PHP 7.0 Unit Tests
php: 7.0
install:
- composer require php-coveralls/php-coveralls:^2.2 --dev --no-update
- composer update --no-progress --prefer-dist
script: vendor/bin/phpunit --coverage-clover build/logs/clover.xml
after_success: php vendor/bin/coveralls
- name: PHP 7.1 Unit Tests
php: 7.1
- name: PHP 7.2 Unit Tests
php: 7.2
- name: PHP 7.3 Unit Tests
php: 7.3
- name: PHP 7.4 Unit Tests
php: 7.4
- name: PHP 8.0 Unit Tests
php: nightly
install:
- composer update --ignore-platform-req=php --no-progress --prefer-dist
- name: PHP 7.3 Code on PHP 8.0 Integration Tests
php: nightly
install:
- composer update --ignore-platform-req=php --no-progress --prefer-dist
script:
- test_old/run-php-src.sh 7.3.21
- name: PHP 8.0 Code on PHP 7.0 Integration Tests
php: 7.0
script:
- test_old/run-php-src.sh 8.0.0beta4
allow_failures:
- name: PHP 8.0 Code on PHP 7.0 Integration Tests
fast_finish: true
script: vendor/bin/phpunit

@@ -1,8 +1,98 @@
Version 4.10.1-dev
Version 4.12.1-dev
------------------
Nothing yet.
Version 4.12.0 (2021-07-21)
---------------------------
### Added
* [PHP 8.1] Added support for readonly properties (through a new `MODIFIER_READONLY`).
* [PHP 8.1] Added support for final class constants.
### Fixed
* Fixed compatibility with PHP 8.1. `&` tokens are now canonicalized to the
`T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG` and `T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG` tokens
used in PHP 8.1. This happens unconditionally, regardless of whether the emulative lexer is used.
Version 4.11.0 (2021-07-03)
---------------------------
### Added
* `BuilderFactory::args()` now accepts named arguments.
* `BuilderFactory::attribute()` has been added.
* An `addAttribute()` method accepting an `Attribute` or `AttributeGroup` has been adde to all
builders that accept attributes, such as `Builder\Class_`.
### Fixed
* `NameResolver` now handles enums.
* `PrettyPrinter` now prints backing enum type.
* Builder methods for types now property handle `never` type.
Version 4.10.5 (2021-05-03)
---------------------------
### Added
* [PHP 8.1] Added support for enums. These are represented using the `Stmt\Enum_` and
`Stmt\EnumCase` nodes.
* [PHP 8.1] Added support for never type. This type will now be returned as an `Identifier` rather
than `Name`.
* Added `ClassConst` builder.
### Changed
* Non-UTF-8 code units in strings will now be hex-encoded.
### Fixed
* Fixed precedence of arrow functions.
Version 4.10.4 (2020-12-20)
---------------------------
### Fixed
* Fixed position information for variable-variables (#741).
* Fixed position information for traits/interfaces preceded by if statement (#738).
Version 4.10.3 (2020-12-03)
---------------------------
### Fixed
* Fixed formatting-preserving pretty printing for `"{$x}"`.
* Ternary expressions are now treated as non-associative in the pretty printer, in order to
generate code that is compatible with the parentheses requirement introduced in PHP 8.
* Removed no longer necessary `error_clear_last()` call in lexer, which may interfere with fatal
error handlers if invoked during shutdown.
Version 4.10.2 (2020-09-26)
------------------
### Fixed
* Fixed check for token emulation conflicts with other libraries.
Version 4.10.1 (2020-09-23)
---------------------------
### Added
* Added support for recovering from a missing semicolon after a property or class constant
declaration.
### Fixed
* Fix spurious whitespace in formatting-preserving pretty printer when both removing and adding
elements at the start of a list.
* Fix incorrect case-sensitivity in keyword token emulation.
Version 4.10.0 (2020-09-19)
---------------------------

@@ -1,12 +1,12 @@
PHP Parser
==========
[![Build Status](https://travis-ci.org/nikic/PHP-Parser.svg?branch=master)](https://travis-ci.org/nikic/PHP-Parser) [![Coverage Status](https://coveralls.io/repos/github/nikic/PHP-Parser/badge.svg?branch=master)](https://coveralls.io/github/nikic/PHP-Parser?branch=master)
[![Coverage Status](https://coveralls.io/repos/github/nikic/PHP-Parser/badge.svg?branch=master)](https://coveralls.io/github/nikic/PHP-Parser?branch=master)
This is a PHP 5.2 to PHP 7.4 parser written in PHP. Its purpose is to simplify static code analysis and
This is a PHP 5.2 to PHP 8.0 parser written in PHP. Its purpose is to simplify static code analysis and
manipulation.
[**Documentation for version 4.x**][doc_master] (stable; for running on PHP >= 7.0; for parsing PHP 5.2 to PHP 7.4).
[**Documentation for version 4.x**][doc_master] (stable; for running on PHP >= 7.0; for parsing PHP 5.2 to PHP 8.0).
[Documentation for version 3.x][doc_3_x] (unsupported; for running on PHP >= 5.5; for parsing PHP 5.2 to PHP 7.2).
@@ -15,7 +15,7 @@ Features
The main features provided by this library are:
* Parsing PHP 5 and PHP 7 code into an abstract syntax tree (AST).
* Parsing PHP 5, PHP 7, and PHP 8 code into an abstract syntax tree (AST).
* Invalid code can be parsed into a partial AST.
* The AST contains accurate location information.
* Dumping the AST in human-readable form.
@@ -215,7 +215,7 @@ Component documentation:
* [JSON representation](doc/component/JSON_representation.markdown)
* JSON encoding and decoding of ASTs
* [Performance](doc/component/Performance.markdown)
* Disabling XDebug
* Disabling Xdebug
* Reusing objects
* Garbage collection impact
* [Frequently asked questions](doc/component/FAQ.markdown)

@@ -10,7 +10,7 @@ foreach ([__DIR__ . '/../../../autoload.php', __DIR__ . '/../vendor/autoload.php
ini_set('xdebug.max_nesting_level', 3000);
// Disable XDebug var_dump() output truncation
// Disable Xdebug var_dump() output truncation
ini_set('xdebug.var_display_max_children', -1);
ini_set('xdebug.var_display_max_data', -1);
ini_set('xdebug.var_display_max_depth', -1);

@@ -1,7 +1,7 @@
Introduction
============
This project is a PHP 5.2 to PHP 7.4 parser **written in PHP itself**.
This project is a PHP 5.2 to PHP 8.0 parser **written in PHP itself**.
What is this for?
-----------------
@@ -26,7 +26,11 @@ programmatic PHP code analysis are incidentally PHP developers, not C developers
What can it parse?
------------------
The parser supports parsing PHP 5.2-7.4.
The parser supports parsing PHP 5.2-8.0, with the following exceptions:
* Namespaced names containing whitespace (e.g. `Foo \ Bar` instead of `Foo\Bar`) are not supported.
These are illegal in PHP 8, but are legal in earlier version. However, PHP-Parser does not
support them for any version.
As the parser is based on the tokens returned by `token_get_all` (which is only able to lex the PHP
version it runs on), additionally a wrapper for emulating tokens from newer versions is provided.

@@ -19,7 +19,7 @@ ini_set('xdebug.max_nesting_level', 3000);
```
This ensures that there will be no errors when traversing highly nested node trees. However, it is
preferable to disable XDebug completely, as it can easily make this library more than five times
preferable to disable Xdebug completely, as it can easily make this library more than five times
slower.
Parsing

@@ -39,7 +39,7 @@ Component documentation
* [JSON representation](component/JSON_representation.markdown)
* JSON encoding and decoding of ASTs
* [Performance](component/Performance.markdown)
* Disabling XDebug
* Disabling Xdebug
* Reusing objects
* Garbage collection impact
* [Frequently asked questions](component/FAQ.markdown)

@@ -36,7 +36,7 @@ if ($e->hasColumnInfo()) {
echo $e->getRawMessage() . ' from ' . $e->getStartLine() . ':' . $e->getStartColumn($code)
. ' to ' . $e->getEndLine() . ':' . $e->getEndColumn($code);
// or:
echo $e->getMessageWithColumnInfo();
echo $e->getMessageWithColumnInfo($code);
} else {
echo $e->getMessage();
}
@@ -72,4 +72,4 @@ if (null !== $stmts) {
}
```
The `NameResolver` visitor also accepts an `ErrorHandler` as a constructor argument.
The `NameResolver` visitor also accepts an `ErrorHandler` as a constructor argument.

@@ -8,20 +8,20 @@ described in the following.
Xdebug
------
Running PHP with XDebug adds a lot of overhead, especially for code that performs many method calls.
Just by loading XDebug (without enabling profiling or other more intrusive XDebug features), you
Running PHP with Xdebug adds a lot of overhead, especially for code that performs many method calls.
Just by loading Xdebug (without enabling profiling or other more intrusive Xdebug features), you
can expect that code using PHP-Parser will be approximately *five times slower*.
As such, you should make sure that XDebug is not loaded when using this library. Note that setting
the `xdebug.default_enable=0` ini option does *not* disable XDebug. The *only* way to disable
XDebug is to not load the extension in the first place.
As such, you should make sure that Xdebug is not loaded when using this library. Note that setting
the `xdebug.default_enable=0` ini option does *not* disable Xdebug. The *only* way to disable
Xdebug is to not load the extension in the first place.
If you are building a command-line utility for use by developers (who often have XDebug enabled),
you may want to consider automatically restarting PHP with XDebug unloaded. The
If you are building a command-line utility for use by developers (who often have Xdebug enabled),
you may want to consider automatically restarting PHP with Xdebug unloaded. The
[composer/xdebug-handler](https://github.com/composer/xdebug-handler) package can be used to do
this.
If you do run with XDebug, you may need to increase the `xdebug.max_nesting_level` option to a
If you do run with Xdebug, you may need to increase the `xdebug.max_nesting_level` option to a
higher level, such as 3000. While the parser itself is recursion free, most other code working on
the AST uses recursion and will generate an error if the value of this option is too low.
@@ -62,4 +62,4 @@ disproportionally (and are usually generated anyway).
Of course, you can also try to (temporarily) disable GC. By design the AST generated by PHP-Parser
is cycle-free, so the AST itself will never cause leaks with GC disabled. However, other code
(including for example the parser object itself) may hold cycles, so disabling of GC should be
approached with care.
approached with care.

@@ -20,6 +20,11 @@ top_statement_list:
if ($nop !== null) { $1[] = $nop; } $$ = $1; }
;
ampersand:
T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG
| T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG
;
reserved_non_modifiers:
T_INCLUDE | T_INCLUDE_ONCE | T_EVAL | T_REQUIRE | T_REQUIRE_ONCE | T_LOGICAL_OR | T_LOGICAL_XOR | T_LOGICAL_AND
| T_INSTANCEOF | T_NEW | T_CLONE | T_EXIT | T_IF | T_ELSEIF | T_ELSE | T_ENDIF | T_ECHO | T_DO | T_WHILE
@@ -246,7 +251,12 @@ variables_list:
optional_ref:
/* empty */ { $$ = false; }
| '&' { $$ = true; }
| ampersand { $$ = true; }
;
optional_arg_ref:
/* empty */ { $$ = false; }
| T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG { $$ = true; }
;
optional_ellipsis:
@@ -378,7 +388,7 @@ new_else_single:
foreach_variable:
variable { $$ = array($1, false); }
| '&' variable { $$ = array($2, true); }
| ampersand variable { $$ = array($2, true); }
| list_expr { $$ = array($1, false); }
;
@@ -393,9 +403,9 @@ non_empty_parameter_list:
;
parameter:
optional_param_type optional_ref optional_ellipsis plain_variable
optional_param_type optional_arg_ref optional_ellipsis plain_variable
{ $$ = Node\Param[$4, null, $1, $2, $3]; $this->checkParam($$); }
| optional_param_type optional_ref optional_ellipsis plain_variable '=' static_scalar
| optional_param_type optional_arg_ref optional_ellipsis plain_variable '=' static_scalar
{ $$ = Node\Param[$4, $6, $1, $2, $3]; $this->checkParam($$); }
;
@@ -428,7 +438,7 @@ non_empty_argument_list:
argument:
expr { $$ = Node\Arg[$1, false, false]; }
| '&' variable { $$ = Node\Arg[$2, true, false]; }
| ampersand variable { $$ = Node\Arg[$2, true, false]; }
| T_ELLIPSIS expr { $$ = Node\Arg[$2, false, true]; }
;
@@ -562,8 +572,8 @@ expr:
variable { $$ = $1; }
| list_expr '=' expr { $$ = Expr\Assign[$1, $3]; }
| variable '=' expr { $$ = Expr\Assign[$1, $3]; }
| variable '=' '&' variable { $$ = Expr\AssignRef[$1, $4]; }
| variable '=' '&' new_expr { $$ = Expr\AssignRef[$1, $4]; }
| variable '=' ampersand variable { $$ = Expr\AssignRef[$1, $4]; }
| variable '=' ampersand new_expr { $$ = Expr\AssignRef[$1, $4]; }
| new_expr { $$ = $1; }
| T_CLONE expr { $$ = Expr\Clone_[$2]; }
| variable T_PLUS_EQUAL expr { $$ = Expr\AssignOp\Plus [$1, $3]; }
@@ -589,7 +599,8 @@ expr:
| expr T_LOGICAL_AND expr { $$ = Expr\BinaryOp\LogicalAnd[$1, $3]; }
| expr T_LOGICAL_XOR expr { $$ = Expr\BinaryOp\LogicalXor[$1, $3]; }
| expr '|' expr { $$ = Expr\BinaryOp\BitwiseOr [$1, $3]; }
| expr '&' expr { $$ = Expr\BinaryOp\BitwiseAnd[$1, $3]; }
| expr T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG expr { $$ = Expr\BinaryOp\BitwiseAnd[$1, $3]; }
| expr T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG expr { $$ = Expr\BinaryOp\BitwiseAnd[$1, $3]; }
| expr '^' expr { $$ = Expr\BinaryOp\BitwiseXor[$1, $3]; }
| expr '.' expr { $$ = Expr\BinaryOp\Concat [$1, $3]; }
| expr '+' expr { $$ = Expr\BinaryOp\Plus [$1, $3]; }
@@ -816,7 +827,10 @@ static_operation:
| static_scalar T_LOGICAL_AND static_scalar { $$ = Expr\BinaryOp\LogicalAnd[$1, $3]; }
| static_scalar T_LOGICAL_XOR static_scalar { $$ = Expr\BinaryOp\LogicalXor[$1, $3]; }
| static_scalar '|' static_scalar { $$ = Expr\BinaryOp\BitwiseOr [$1, $3]; }
| static_scalar '&' static_scalar { $$ = Expr\BinaryOp\BitwiseAnd[$1, $3]; }
| static_scalar T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG static_scalar
{ $$ = Expr\BinaryOp\BitwiseAnd[$1, $3]; }
| static_scalar T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG static_scalar
{ $$ = Expr\BinaryOp\BitwiseAnd[$1, $3]; }
| static_scalar '^' static_scalar { $$ = Expr\BinaryOp\BitwiseXor[$1, $3]; }
| static_scalar '.' static_scalar { $$ = Expr\BinaryOp\Concat [$1, $3]; }
| static_scalar '+' static_scalar { $$ = Expr\BinaryOp\Plus [$1, $3]; }
@@ -986,8 +1000,8 @@ non_empty_array_pair_list:
array_pair:
expr T_DOUBLE_ARROW expr { $$ = Expr\ArrayItem[$3, $1, false]; }
| expr { $$ = Expr\ArrayItem[$1, null, false]; }
| expr T_DOUBLE_ARROW '&' variable { $$ = Expr\ArrayItem[$4, $1, true]; }
| '&' variable { $$ = Expr\ArrayItem[$2, null, true]; }
| expr T_DOUBLE_ARROW ampersand variable { $$ = Expr\ArrayItem[$4, $1, true]; }
| ampersand variable { $$ = Expr\ArrayItem[$2, null, true]; }
| T_ELLIPSIS expr { $$ = Expr\ArrayItem[$2, null, false, attributes(), true]; }
;

@@ -20,6 +20,11 @@ top_statement_list:
if ($nop !== null) { $1[] = $nop; } $$ = $1; }
;
ampersand:
T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG
| T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG
;
reserved_non_modifiers:
T_INCLUDE | T_INCLUDE_ONCE | T_EVAL | T_REQUIRE | T_REQUIRE_ONCE | T_LOGICAL_OR | T_LOGICAL_XOR | T_LOGICAL_AND
| T_INSTANCEOF | T_NEW | T_CLONE | T_EXIT | T_IF | T_ELSEIF | T_ELSE | T_ENDIF | T_ECHO | T_DO | T_WHILE
@@ -28,12 +33,12 @@ reserved_non_modifiers:
| T_FUNCTION | T_CONST | T_RETURN | T_PRINT | T_YIELD | T_LIST | T_SWITCH | T_ENDSWITCH | T_CASE | T_DEFAULT
| T_BREAK | T_ARRAY | T_CALLABLE | T_EXTENDS | T_IMPLEMENTS | T_NAMESPACE | T_TRAIT | T_INTERFACE | T_CLASS
| T_CLASS_C | T_TRAIT_C | T_FUNC_C | T_METHOD_C | T_LINE | T_FILE | T_DIR | T_NS_C | T_HALT_COMPILER | T_FN
| T_MATCH
| T_MATCH | T_ENUM
;
semi_reserved:
reserved_non_modifiers
| T_STATIC | T_ABSTRACT | T_FINAL | T_PRIVATE | T_PROTECTED | T_PUBLIC
| T_STATIC | T_ABSTRACT | T_FINAL | T_PRIVATE | T_PROTECTED | T_PUBLIC | T_READONLY
;
identifier_ex:
@@ -327,7 +332,12 @@ non_empty_variables_list:
optional_ref:
/* empty */ { $$ = false; }
| '&' { $$ = true; }
| ampersand { $$ = true; }
;
optional_arg_ref:
/* empty */ { $$ = false; }
| T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG { $$ = true; }
;
optional_ellipsis:
@@ -348,10 +358,7 @@ function_declaration_statement:
;
class_declaration_statement:
class_entry_type identifier extends_from implements_list '{' class_statement_list '}'
{ $$ = Stmt\Class_[$2, ['type' => $1, 'extends' => $3, 'implements' => $4, 'stmts' => $6, 'attrGroups' => []]];
$this->checkClass($$, #2); }
| attributes class_entry_type identifier extends_from implements_list '{' class_statement_list '}'
optional_attributes class_entry_type identifier extends_from implements_list '{' class_statement_list '}'
{ $$ = Stmt\Class_[$3, ['type' => $2, 'extends' => $4, 'implements' => $5, 'stmts' => $7, 'attrGroups' => $1]];
$this->checkClass($$, #3); }
| optional_attributes T_INTERFACE identifier interface_extends_list '{' class_statement_list '}'
@@ -359,6 +366,18 @@ class_declaration_statement:
$this->checkInterface($$, #3); }
| optional_attributes T_TRAIT identifier '{' class_statement_list '}'
{ $$ = Stmt\Trait_[$3, ['stmts' => $5, 'attrGroups' => $1]]; }
| optional_attributes T_ENUM identifier enum_scalar_type implements_list '{' class_statement_list '}'
{ $$ = Stmt\Enum_[$3, ['scalarType' => $4, 'implements' => $5, 'stmts' => $7, 'attrGroups' => $1]];
$this->checkEnum($$, #3); }
;
enum_scalar_type:
/* empty */ { $$ = null; }
| ':' type { $$ = $2; }
enum_case_expr:
/* empty */ { $$ = null; }
| '=' expr { $$ = $2; }
;
class_entry_type:
@@ -496,7 +515,7 @@ new_else_single:
foreach_variable:
variable { $$ = array($1, false); }
| '&' variable { $$ = array($2, true); }
| ampersand variable { $$ = array($2, true); }
| list_expr { $$ = array($1, false); }
| array_short_syntax { $$ = array($1, false); }
;
@@ -516,16 +535,20 @@ optional_visibility_modifier:
| T_PUBLIC { $$ = Stmt\Class_::MODIFIER_PUBLIC; }
| T_PROTECTED { $$ = Stmt\Class_::MODIFIER_PROTECTED; }
| T_PRIVATE { $$ = Stmt\Class_::MODIFIER_PRIVATE; }
| T_READONLY { $$ = Stmt\Class_::MODIFIER_READONLY; }
;
parameter:
optional_attributes optional_visibility_modifier optional_type_without_static optional_ref optional_ellipsis plain_variable
optional_attributes optional_visibility_modifier optional_type_without_static
optional_arg_ref optional_ellipsis plain_variable
{ $$ = new Node\Param($6, null, $3, $4, $5, attributes(), $2, $1);
$this->checkParam($$); }
| optional_attributes optional_visibility_modifier optional_type_without_static optional_ref optional_ellipsis plain_variable '=' expr
| optional_attributes optional_visibility_modifier optional_type_without_static
optional_arg_ref optional_ellipsis plain_variable '=' expr
{ $$ = new Node\Param($6, $8, $3, $4, $5, attributes(), $2, $1);
$this->checkParam($$); }
| optional_attributes optional_visibility_modifier optional_type_without_static optional_ref optional_ellipsis error
| optional_attributes optional_visibility_modifier optional_type_without_static
optional_arg_ref optional_ellipsis error
{ $$ = new Node\Param(Expr\Error[], null, $3, $4, $5, attributes(), $2, $1); }
;
@@ -585,7 +608,7 @@ non_empty_argument_list:
argument:
expr { $$ = Node\Arg[$1, false, false]; }
| '&' variable { $$ = Node\Arg[$2, true, false]; }
| ampersand variable { $$ = Node\Arg[$2, true, false]; }
| T_ELLIPSIS expr { $$ = Node\Arg[$2, false, true]; }
| identifier_ex ':' expr
{ $$ = new Node\Arg($3, false, false, attributes(), $1); }
@@ -601,7 +624,7 @@ non_empty_global_var_list:
;
global_var:
simple_variable { $$ = Expr\Variable[$1]; }
simple_variable { $$ = $1; }
;
static_var_list:
@@ -630,16 +653,18 @@ class_statement_list:
;
class_statement:
optional_attributes variable_modifiers optional_type_without_static property_declaration_list ';'
optional_attributes variable_modifiers optional_type_without_static property_declaration_list semi
{ $$ = new Stmt\Property($2, $4, attributes(), $3, $1);
$this->checkProperty($$, #2); }
| optional_attributes method_modifiers T_CONST class_const_list ';'
| optional_attributes method_modifiers T_CONST class_const_list semi
{ $$ = new Stmt\ClassConst($4, $2, attributes(), $1);
$this->checkClassConst($$, #2); }
| optional_attributes method_modifiers T_FUNCTION optional_ref identifier_ex '(' parameter_list ')' optional_return_type method_body
{ $$ = Stmt\ClassMethod[$5, ['type' => $2, 'byRef' => $4, 'params' => $7, 'returnType' => $9, 'stmts' => $10, 'attrGroups' => $1]];
$this->checkClassMethod($$, #2); }
| T_USE class_name_list trait_adaptations { $$ = Stmt\TraitUse[$2, $3]; }
| optional_attributes T_CASE identifier enum_case_expr semi
{ $$ = Stmt\EnumCase[$3, $4, $1]; }
| error { $$ = null; /* will be skipped */ }
;
@@ -701,6 +726,7 @@ member_modifier:
| T_STATIC { $$ = Stmt\Class_::MODIFIER_STATIC; }
| T_ABSTRACT { $$ = Stmt\Class_::MODIFIER_ABSTRACT; }
| T_FINAL { $$ = Stmt\Class_::MODIFIER_FINAL; }
| T_READONLY { $$ = Stmt\Class_::MODIFIER_READONLY; }
;
property_declaration_list:
@@ -745,7 +771,7 @@ expr:
| list_expr '=' expr { $$ = Expr\Assign[$1, $3]; }
| array_short_syntax '=' expr { $$ = Expr\Assign[$1, $3]; }
| variable '=' expr { $$ = Expr\Assign[$1, $3]; }
| variable '=' '&' variable { $$ = Expr\AssignRef[$1, $4]; }
| variable '=' ampersand variable { $$ = Expr\AssignRef[$1, $4]; }
| new_expr { $$ = $1; }
| match { $$ = $1; }
| T_CLONE expr { $$ = Expr\Clone_[$2]; }
@@ -772,7 +798,8 @@ expr:
| expr T_LOGICAL_AND expr { $$ = Expr\BinaryOp\LogicalAnd[$1, $3]; }
| expr T_LOGICAL_XOR expr { $$ = Expr\BinaryOp\LogicalXor[$1, $3]; }
| expr '|' expr { $$ = Expr\BinaryOp\BitwiseOr [$1, $3]; }
| expr '&' expr { $$ = Expr\BinaryOp\BitwiseAnd[$1, $3]; }
| expr T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG expr { $$ = Expr\BinaryOp\BitwiseAnd[$1, $3]; }
| expr T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG expr { $$ = Expr\BinaryOp\BitwiseAnd[$1, $3]; }
| expr '^' expr { $$ = Expr\BinaryOp\BitwiseXor[$1, $3]; }
| expr '.' expr { $$ = Expr\BinaryOp\Concat [$1, $3]; }
| expr '+' expr { $$ = Expr\BinaryOp\Plus [$1, $3]; }
@@ -832,18 +859,18 @@ expr:
| T_YIELD_FROM expr { $$ = Expr\YieldFrom[$2]; }
| T_THROW expr { $$ = Expr\Throw_[$2]; }
| T_FN optional_ref '(' parameter_list ')' optional_return_type T_DOUBLE_ARROW expr
| T_FN optional_ref '(' parameter_list ')' optional_return_type T_DOUBLE_ARROW expr %prec T_THROW
{ $$ = Expr\ArrowFunction[['static' => false, 'byRef' => $2, 'params' => $4, 'returnType' => $6, 'expr' => $8, 'attrGroups' => []]]; }
| T_STATIC T_FN optional_ref '(' parameter_list ')' optional_return_type T_DOUBLE_ARROW expr
| T_STATIC T_FN optional_ref '(' parameter_list ')' optional_return_type T_DOUBLE_ARROW expr %prec T_THROW
{ $$ = Expr\ArrowFunction[['static' => true, 'byRef' => $3, 'params' => $5, 'returnType' => $7, 'expr' => $9, 'attrGroups' => []]]; }
| T_FUNCTION optional_ref '(' parameter_list ')' lexical_vars optional_return_type block_or_error
{ $$ = Expr\Closure[['static' => false, 'byRef' => $2, 'params' => $4, 'uses' => $6, 'returnType' => $7, 'stmts' => $8, 'attrGroups' => []]]; }
| T_STATIC T_FUNCTION optional_ref '(' parameter_list ')' lexical_vars optional_return_type block_or_error
{ $$ = Expr\Closure[['static' => true, 'byRef' => $3, 'params' => $5, 'uses' => $7, 'returnType' => $8, 'stmts' => $9, 'attrGroups' => []]]; }
| attributes T_FN optional_ref '(' parameter_list ')' optional_return_type T_DOUBLE_ARROW expr
| attributes T_FN optional_ref '(' parameter_list ')' optional_return_type T_DOUBLE_ARROW expr %prec T_THROW
{ $$ = Expr\ArrowFunction[['static' => false, 'byRef' => $3, 'params' => $5, 'returnType' => $7, 'expr' => $9, 'attrGroups' => $1]]; }
| attributes T_STATIC T_FN optional_ref '(' parameter_list ')' optional_return_type T_DOUBLE_ARROW expr
| attributes T_STATIC T_FN optional_ref '(' parameter_list ')' optional_return_type T_DOUBLE_ARROW expr %prec T_THROW
{ $$ = Expr\ArrowFunction[['static' => true, 'byRef' => $4, 'params' => $6, 'returnType' => $8, 'expr' => $10, 'attrGroups' => $1]]; }
| attributes T_FUNCTION optional_ref '(' parameter_list ')' lexical_vars optional_return_type block_or_error
{ $$ = Expr\Closure[['static' => false, 'byRef' => $3, 'params' => $5, 'uses' => $7, 'returnType' => $8, 'stmts' => $9, 'attrGroups' => $1]]; }
@@ -1007,7 +1034,7 @@ callable_expr:
;
callable_variable:
simple_variable { $$ = Expr\Variable[$1]; }
simple_variable { $$ = $1; }
| array_object_dereferencable '[' optional_expr ']' { $$ = Expr\ArrayDimFetch[$1, $3]; }
| array_object_dereferencable '{' expr '}' { $$ = Expr\ArrayDimFetch[$1, $3]; }
| function_call { $$ = $1; }
@@ -1032,15 +1059,15 @@ variable:
;
simple_variable:
T_VARIABLE { $$ = parseVar($1); }
| '$' '{' expr '}' { $$ = $3; }
plain_variable { $$ = $1; }
| '$' '{' expr '}' { $$ = Expr\Variable[$3]; }
| '$' simple_variable { $$ = Expr\Variable[$2]; }
| '$' error { $$ = Expr\Error[]; $this->errorState = 2; }
| '$' error { $$ = Expr\Variable[Expr\Error[]]; $this->errorState = 2; }
;
static_member_prop_name:
simple_variable
{ $var = $1; $$ = \is_string($var) ? Node\VarLikeIdentifier[$var] : $var; }
{ $var = $1->name; $$ = \is_string($var) ? Node\VarLikeIdentifier[$var] : $var; }
;
static_member:
@@ -1049,7 +1076,7 @@ static_member:
;
new_variable:
simple_variable { $$ = Expr\Variable[$1]; }
simple_variable { $$ = $1; }
| new_variable '[' optional_expr ']' { $$ = Expr\ArrayDimFetch[$1, $3]; }
| new_variable '{' expr '}' { $$ = Expr\ArrayDimFetch[$1, $3]; }
| new_variable T_OBJECT_OPERATOR property_name { $$ = Expr\PropertyFetch[$1, $3]; }
@@ -1063,13 +1090,13 @@ new_variable:
member_name:
identifier_ex { $$ = $1; }
| '{' expr '}' { $$ = $2; }
| simple_variable { $$ = Expr\Variable[$1]; }
| simple_variable { $$ = $1; }
;
property_name:
identifier { $$ = $1; }
| '{' expr '}' { $$ = $2; }
| simple_variable { $$ = Expr\Variable[$1]; }
| simple_variable { $$ = $1; }
| error { $$ = Expr\Error[]; $this->errorState = 2; }
;
@@ -1095,10 +1122,10 @@ inner_array_pair_list:
array_pair:
expr { $$ = Expr\ArrayItem[$1, null, false]; }
| '&' variable { $$ = Expr\ArrayItem[$2, null, true]; }
| ampersand variable { $$ = Expr\ArrayItem[$2, null, true]; }
| list_expr { $$ = Expr\ArrayItem[$1, null, false]; }
| expr T_DOUBLE_ARROW expr { $$ = Expr\ArrayItem[$3, $1, false]; }
| expr T_DOUBLE_ARROW '&' variable { $$ = Expr\ArrayItem[$4, $1, true]; }
| expr T_DOUBLE_ARROW ampersand variable { $$ = Expr\ArrayItem[$4, $1, true]; }
| expr T_DOUBLE_ARROW list_expr { $$ = Expr\ArrayItem[$3, $1, false]; }
| T_ELLIPSIS expr { $$ = Expr\ArrayItem[$2, null, false, attributes(), true]; }
| /* empty */ { $$ = null; }

192
grammar/phpyLang.php Normal file

@@ -0,0 +1,192 @@
<?php
///////////////////////////////
/// Utility regex constants ///
///////////////////////////////
const LIB = '(?(DEFINE)
(?<singleQuotedString>\'[^\\\\\']*+(?:\\\\.[^\\\\\']*+)*+\')
(?<doubleQuotedString>"[^\\\\"]*+(?:\\\\.[^\\\\"]*+)*+")
(?<string>(?&singleQuotedString)|(?&doubleQuotedString))
(?<comment>/\*[^*]*+(?:\*(?!/)[^*]*+)*+\*/)
(?<code>\{[^\'"/{}]*+(?:(?:(?&string)|(?&comment)|(?&code)|/)[^\'"/{}]*+)*+})
)';
const PARAMS = '\[(?<params>[^[\]]*+(?:\[(?&params)\][^[\]]*+)*+)\]';
const ARGS = '\((?<args>[^()]*+(?:\((?&args)\)[^()]*+)*+)\)';
///////////////////////////////
/// Preprocessing functions ///
///////////////////////////////
function preprocessGrammar($code) {
$code = resolveNodes($code);
$code = resolveMacros($code);
$code = resolveStackAccess($code);
return $code;
}
function resolveNodes($code) {
return preg_replace_callback(
'~\b(?<name>[A-Z][a-zA-Z_\\\\]++)\s*' . PARAMS . '~',
function($matches) {
// recurse
$matches['params'] = resolveNodes($matches['params']);
$params = magicSplit(
'(?:' . PARAMS . '|' . ARGS . ')(*SKIP)(*FAIL)|,',
$matches['params']
);
$paramCode = '';
foreach ($params as $param) {
$paramCode .= $param . ', ';
}
return 'new ' . $matches['name'] . '(' . $paramCode . 'attributes())';
},
$code
);
}
function resolveMacros($code) {
return preg_replace_callback(
'~\b(?<!::|->)(?!array\()(?<name>[a-z][A-Za-z]++)' . ARGS . '~',
function($matches) {
// recurse
$matches['args'] = resolveMacros($matches['args']);
$name = $matches['name'];
$args = magicSplit(
'(?:' . PARAMS . '|' . ARGS . ')(*SKIP)(*FAIL)|,',
$matches['args']
);
if ('attributes' === $name) {
assertArgs(0, $args, $name);
return '$this->startAttributeStack[#1] + $this->endAttributes';
}
if ('stackAttributes' === $name) {
assertArgs(1, $args, $name);
return '$this->startAttributeStack[' . $args[0] . ']'
. ' + $this->endAttributeStack[' . $args[0] . ']';
}
if ('init' === $name) {
return '$$ = array(' . implode(', ', $args) . ')';
}
if ('push' === $name) {
assertArgs(2, $args, $name);
return $args[0] . '[] = ' . $args[1] . '; $$ = ' . $args[0];
}
if ('pushNormalizing' === $name) {
assertArgs(2, $args, $name);
return 'if (is_array(' . $args[1] . ')) { $$ = array_merge(' . $args[0] . ', ' . $args[1] . '); }'
. ' else { ' . $args[0] . '[] = ' . $args[1] . '; $$ = ' . $args[0] . '; }';
}
if ('toArray' == $name) {
assertArgs(1, $args, $name);
return 'is_array(' . $args[0] . ') ? ' . $args[0] . ' : array(' . $args[0] . ')';
}
if ('parseVar' === $name) {
assertArgs(1, $args, $name);
return 'substr(' . $args[0] . ', 1)';
}
if ('parseEncapsed' === $name) {
assertArgs(3, $args, $name);
return 'foreach (' . $args[0] . ' as $s) { if ($s instanceof Node\Scalar\EncapsedStringPart) {'
. ' $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, ' . $args[1] . ', ' . $args[2] . '); } }';
}
if ('makeNop' === $name) {
assertArgs(3, $args, $name);
return '$startAttributes = ' . $args[1] . ';'
. ' if (isset($startAttributes[\'comments\']))'
. ' { ' . $args[0] . ' = new Stmt\Nop($startAttributes + ' . $args[2] . '); }'
. ' else { ' . $args[0] . ' = null; }';
}
if ('makeZeroLengthNop' == $name) {
assertArgs(2, $args, $name);
return '$startAttributes = ' . $args[1] . ';'
. ' if (isset($startAttributes[\'comments\']))'
. ' { ' . $args[0] . ' = new Stmt\Nop($this->createCommentNopAttributes($startAttributes[\'comments\'])); }'
. ' else { ' . $args[0] . ' = null; }';
}
if ('strKind' === $name) {
assertArgs(1, $args, $name);
return '(' . $args[0] . '[0] === "\'" || (' . $args[0] . '[1] === "\'" && '
. '(' . $args[0] . '[0] === \'b\' || ' . $args[0] . '[0] === \'B\')) '
. '? Scalar\String_::KIND_SINGLE_QUOTED : Scalar\String_::KIND_DOUBLE_QUOTED)';
}
if ('prependLeadingComments' === $name) {
assertArgs(1, $args, $name);
return '$attrs = $this->startAttributeStack[#1]; $stmts = ' . $args[0] . '; '
. 'if (!empty($attrs[\'comments\'])) {'
. '$stmts[0]->setAttribute(\'comments\', '
. 'array_merge($attrs[\'comments\'], $stmts[0]->getAttribute(\'comments\', []))); }';
}
return $matches[0];
},
$code
);
}
function assertArgs($num, $args, $name) {
if ($num != count($args)) {
die('Wrong argument count for ' . $name . '().');
}
}
function resolveStackAccess($code) {
$code = preg_replace('/\$\d+/', '$this->semStack[$0]', $code);
$code = preg_replace('/#(\d+)/', '$$1', $code);
return $code;
}
function removeTrailingWhitespace($code) {
$lines = explode("\n", $code);
$lines = array_map('rtrim', $lines);
return implode("\n", $lines);
}
//////////////////////////////
/// Regex helper functions ///
//////////////////////////////
function regex($regex) {
return '~' . LIB . '(?:' . str_replace('~', '\~', $regex) . ')~';
}
function magicSplit($regex, $string) {
$pieces = preg_split(regex('(?:(?&string)|(?&comment)|(?&code))(*SKIP)(*FAIL)|' . $regex), $string);
foreach ($pieces as &$piece) {
$piece = trim($piece);
}
if ($pieces === ['']) {
return [];
}
return $pieces;
}

@@ -1,5 +1,7 @@
<?php
require __DIR__ . '/phpyLang.php';
$grammarFileToName = [
__DIR__ . '/php5.y' => 'Php5',
__DIR__ . '/php7.y' => 'Php7',
@@ -23,21 +25,6 @@ $options = array_flip($argv);
$optionDebug = isset($options['--debug']);
$optionKeepTmpGrammar = isset($options['--keep-tmp-grammar']);
///////////////////////////////
/// Utility regex constants ///
///////////////////////////////
const LIB = '(?(DEFINE)
(?<singleQuotedString>\'[^\\\\\']*+(?:\\\\.[^\\\\\']*+)*+\')
(?<doubleQuotedString>"[^\\\\"]*+(?:\\\\.[^\\\\"]*+)*+")
(?<string>(?&singleQuotedString)|(?&doubleQuotedString))
(?<comment>/\*[^*]*+(?:\*(?!/)[^*]*+)*+\*/)
(?<code>\{[^\'"/{}]*+(?:(?:(?&string)|(?&comment)|(?&code)|/)[^\'"/{}]*+)*+})
)';
const PARAMS = '\[(?<params>[^[\]]*+(?:\[(?&params)\][^[\]]*+)*+)\]';
const ARGS = '\((?<args>[^()]*+(?:\((?&args)\)[^()]*+)*+)\)';
///////////////////
/// Main script ///
///////////////////
@@ -49,10 +36,7 @@ foreach ($grammarFileToName as $grammarFile => $name) {
$grammarCode = file_get_contents($grammarFile);
$grammarCode = str_replace('%tokens', $tokens, $grammarCode);
$grammarCode = resolveNodes($grammarCode);
$grammarCode = resolveMacros($grammarCode);
$grammarCode = resolveStackAccess($grammarCode);
$grammarCode = preprocessGrammar($grammarCode);
file_put_contents($tmpGrammarFile, $grammarCode);
@@ -77,151 +61,9 @@ foreach ($grammarFileToName as $grammarFile => $name) {
}
}
///////////////////////////////
/// Preprocessing functions ///
///////////////////////////////
function resolveNodes($code) {
return preg_replace_callback(
'~\b(?<name>[A-Z][a-zA-Z_\\\\]++)\s*' . PARAMS . '~',
function($matches) {
// recurse
$matches['params'] = resolveNodes($matches['params']);
$params = magicSplit(
'(?:' . PARAMS . '|' . ARGS . ')(*SKIP)(*FAIL)|,',
$matches['params']
);
$paramCode = '';
foreach ($params as $param) {
$paramCode .= $param . ', ';
}
return 'new ' . $matches['name'] . '(' . $paramCode . 'attributes())';
},
$code
);
}
function resolveMacros($code) {
return preg_replace_callback(
'~\b(?<!::|->)(?!array\()(?<name>[a-z][A-Za-z]++)' . ARGS . '~',
function($matches) {
// recurse
$matches['args'] = resolveMacros($matches['args']);
$name = $matches['name'];
$args = magicSplit(
'(?:' . PARAMS . '|' . ARGS . ')(*SKIP)(*FAIL)|,',
$matches['args']
);
if ('attributes' === $name) {
assertArgs(0, $args, $name);
return '$this->startAttributeStack[#1] + $this->endAttributes';
}
if ('stackAttributes' === $name) {
assertArgs(1, $args, $name);
return '$this->startAttributeStack[' . $args[0] . ']'
. ' + $this->endAttributeStack[' . $args[0] . ']';
}
if ('init' === $name) {
return '$$ = array(' . implode(', ', $args) . ')';
}
if ('push' === $name) {
assertArgs(2, $args, $name);
return $args[0] . '[] = ' . $args[1] . '; $$ = ' . $args[0];
}
if ('pushNormalizing' === $name) {
assertArgs(2, $args, $name);
return 'if (is_array(' . $args[1] . ')) { $$ = array_merge(' . $args[0] . ', ' . $args[1] . '); }'
. ' else { ' . $args[0] . '[] = ' . $args[1] . '; $$ = ' . $args[0] . '; }';
}
if ('toArray' == $name) {
assertArgs(1, $args, $name);
return 'is_array(' . $args[0] . ') ? ' . $args[0] . ' : array(' . $args[0] . ')';
}
if ('parseVar' === $name) {
assertArgs(1, $args, $name);
return 'substr(' . $args[0] . ', 1)';
}
if ('parseEncapsed' === $name) {
assertArgs(3, $args, $name);
return 'foreach (' . $args[0] . ' as $s) { if ($s instanceof Node\Scalar\EncapsedStringPart) {'
. ' $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, ' . $args[1] . ', ' . $args[2] . '); } }';
}
if ('makeNop' === $name) {
assertArgs(3, $args, $name);
return '$startAttributes = ' . $args[1] . ';'
. ' if (isset($startAttributes[\'comments\']))'
. ' { ' . $args[0] . ' = new Stmt\Nop($startAttributes + ' . $args[2] . '); }'
. ' else { ' . $args[0] . ' = null; }';
}
if ('makeZeroLengthNop' == $name) {
assertArgs(2, $args, $name);
return '$startAttributes = ' . $args[1] . ';'
. ' if (isset($startAttributes[\'comments\']))'
. ' { ' . $args[0] . ' = new Stmt\Nop($this->createCommentNopAttributes($startAttributes[\'comments\'])); }'
. ' else { ' . $args[0] . ' = null; }';
}
if ('strKind' === $name) {
assertArgs(1, $args, $name);
return '(' . $args[0] . '[0] === "\'" || (' . $args[0] . '[1] === "\'" && '
. '(' . $args[0] . '[0] === \'b\' || ' . $args[0] . '[0] === \'B\')) '
. '? Scalar\String_::KIND_SINGLE_QUOTED : Scalar\String_::KIND_DOUBLE_QUOTED)';
}
if ('prependLeadingComments' === $name) {
assertArgs(1, $args, $name);
return '$attrs = $this->startAttributeStack[#1]; $stmts = ' . $args[0] . '; '
. 'if (!empty($attrs[\'comments\'])) {'
. '$stmts[0]->setAttribute(\'comments\', '
. 'array_merge($attrs[\'comments\'], $stmts[0]->getAttribute(\'comments\', []))); }';
}
return $matches[0];
},
$code
);
}
function assertArgs($num, $args, $name) {
if ($num != count($args)) {
die('Wrong argument count for ' . $name . '().');
}
}
function resolveStackAccess($code) {
$code = preg_replace('/\$\d+/', '$this->semStack[$0]', $code);
$code = preg_replace('/#(\d+)/', '$$1', $code);
return $code;
}
function removeTrailingWhitespace($code) {
$lines = explode("\n", $code);
$lines = array_map('rtrim', $lines);
return implode("\n", $lines);
}
////////////////////////////////
/// Utility helper functions ///
////////////////////////////////
function ensureDirExists($dir) {
if (!is_dir($dir)) {
@@ -237,25 +79,3 @@ function execCmd($cmd) {
}
return $output;
}
//////////////////////////////
/// Regex helper functions ///
//////////////////////////////
function regex($regex) {
return '~' . LIB . '(?:' . str_replace('~', '\~', $regex) . ')~';
}
function magicSplit($regex, $string) {
$pieces = preg_split(regex('(?:(?&string)|(?&comment)|(?&code))(*SKIP)(*FAIL)|' . $regex), $string);
foreach ($pieces as &$piece) {
$piece = trim($piece);
}
if ($pieces === ['']) {
return [];
}
return $pieces;
}

@@ -18,7 +18,7 @@
%left T_BOOLEAN_AND
%left '|'
%left '^'
%left '&'
%left T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG
%nonassoc T_IS_EQUAL T_IS_NOT_EQUAL T_IS_IDENTICAL T_IS_NOT_IDENTICAL T_SPACESHIP
%nonassoc '<' T_IS_SMALLER_OR_EQUAL '>' T_IS_GREATER_OR_EQUAL
%left T_SL T_SR
@@ -74,7 +74,7 @@
%token T_USE
%token T_INSTEADOF
%token T_GLOBAL
%right T_STATIC T_ABSTRACT T_FINAL T_PRIVATE T_PROTECTED T_PUBLIC
%right T_STATIC T_ABSTRACT T_FINAL T_PRIVATE T_PROTECTED T_PUBLIC T_READONLY
%token T_VAR
%token T_UNSET
%token T_ISSET
@@ -83,6 +83,7 @@
%token T_CLASS
%token T_TRAIT
%token T_INTERFACE
%token T_ENUM
%token T_EXTENDS
%token T_IMPLEMENTS
%token T_OBJECT_OPERATOR
@@ -110,4 +111,5 @@
%token T_NAME_FULLY_QUALIFIED
%token T_NAME_QUALIFIED
%token T_NAME_RELATIVE
%token T_ATTRIBUTE
%token T_ATTRIBUTE
%token T_ENUM

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace PhpParser\Builder;
use PhpParser;
use PhpParser\BuilderHelpers;
use PhpParser\Node;
use PhpParser\Node\Const_;
use PhpParser\Node\Identifier;
use PhpParser\Node\Stmt;
class ClassConst implements PhpParser\Builder
{
protected $flags = 0;
protected $attributes = [];
protected $constants = [];
/** @var Node\AttributeGroup[] */
protected $attributeGroups = [];
/**
* Creates a class constant builder
*
* @param string|Identifier $name Name
* @param Node\Expr|bool|null|int|float|string|array $value Value
*/
public function __construct($name, $value) {
$this->constants = [new Const_($name, BuilderHelpers::normalizeValue($value))];
}
/**
* Add another constant to const group
*
* @param string|Identifier $name Name
* @param Node\Expr|bool|null|int|float|string|array $value Value
*
* @return $this The builder instance (for fluid interface)
*/
public function addConst($name, $value) {
$this->constants[] = new Const_($name, BuilderHelpers::normalizeValue($value));
return $this;
}
/**
* Makes the constant public.
*
* @return $this The builder instance (for fluid interface)
*/
public function makePublic() {
$this->flags = BuilderHelpers::addModifier($this->flags, Stmt\Class_::MODIFIER_PUBLIC);
return $this;
}
/**
* Makes the constant protected.
*
* @return $this The builder instance (for fluid interface)
*/
public function makeProtected() {
$this->flags = BuilderHelpers::addModifier($this->flags, Stmt\Class_::MODIFIER_PROTECTED);
return $this;
}
/**
* Makes the constant private.
*
* @return $this The builder instance (for fluid interface)
*/
public function makePrivate() {
$this->flags = BuilderHelpers::addModifier($this->flags, Stmt\Class_::MODIFIER_PRIVATE);
return $this;
}
/**
* Makes the constant final.
*
* @return $this The builder instance (for fluid interface)
*/
public function makeFinal() {
$this->flags = BuilderHelpers::addModifier($this->flags, Stmt\Class_::MODIFIER_FINAL);
return $this;
}
/**
* Sets doc comment for the constant.
*
* @param PhpParser\Comment\Doc|string $docComment Doc comment to set
*
* @return $this The builder instance (for fluid interface)
*/
public function setDocComment($docComment) {
$this->attributes = [
'comments' => [BuilderHelpers::normalizeDocComment($docComment)]
];
return $this;
}
/**
* Adds an attribute group.
*
* @param Node\Attribute|Node\AttributeGroup $attribute
*
* @return $this The builder instance (for fluid interface)
*/
public function addAttribute($attribute) {
$this->attributeGroups[] = BuilderHelpers::normalizeAttribute($attribute);
return $this;
}
/**
* Returns the built class node.
*
* @return Stmt\ClassConst The built constant node
*/
public function getNode(): PhpParser\Node {
return new Stmt\ClassConst(
$this->constants,
$this->flags,
$this->attributes,
$this->attributeGroups
);
}
}

@@ -4,6 +4,7 @@ namespace PhpParser\Builder;
use PhpParser;
use PhpParser\BuilderHelpers;
use PhpParser\Node;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt;
@@ -20,6 +21,9 @@ class Class_ extends Declaration
protected $properties = [];
protected $methods = [];
/** @var Node\AttributeGroup[] */
protected $attributeGroups = [];
/**
* Creates a class builder.
*
@@ -106,6 +110,19 @@ class Class_ extends Declaration
return $this;
}
/**
* Adds an attribute group.
*
* @param Node\Attribute|Node\AttributeGroup $attribute
*
* @return $this The builder instance (for fluid interface)
*/
public function addAttribute($attribute) {
$this->attributeGroups[] = BuilderHelpers::normalizeAttribute($attribute);
return $this;
}
/**
* Returns the built class node.
*
@@ -117,6 +134,7 @@ class Class_ extends Declaration
'extends' => $this->extends,
'implements' => $this->implements,
'stmts' => array_merge($this->uses, $this->constants, $this->properties, $this->methods),
'attrGroups' => $this->attributeGroups,
], $this->attributes);
}
}

@@ -12,6 +12,9 @@ class Function_ extends FunctionLike
protected $name;
protected $stmts = [];
/** @var Node\AttributeGroup[] */
protected $attributeGroups = [];
/**
* Creates a function builder.
*
@@ -34,6 +37,19 @@ class Function_ extends FunctionLike
return $this;
}
/**
* Adds an attribute group.
*
* @param Node\Attribute|Node\AttributeGroup $attribute
*
* @return $this The builder instance (for fluid interface)
*/
public function addAttribute($attribute) {
$this->attributeGroups[] = BuilderHelpers::normalizeAttribute($attribute);
return $this;
}
/**
* Returns the built function node.
*
@@ -45,6 +61,7 @@ class Function_ extends FunctionLike
'params' => $this->params,
'returnType' => $this->returnType,
'stmts' => $this->stmts,
'attrGroups' => $this->attributeGroups,
], $this->attributes);
}
}

@@ -4,6 +4,7 @@ namespace PhpParser\Builder;
use PhpParser;
use PhpParser\BuilderHelpers;
use PhpParser\Node;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt;
@@ -14,6 +15,9 @@ class Interface_ extends Declaration
protected $constants = [];
protected $methods = [];
/** @var Node\AttributeGroup[] */
protected $attributeGroups = [];
/**
* Creates an interface builder.
*
@@ -61,6 +65,19 @@ class Interface_ extends Declaration
return $this;
}
/**
* Adds an attribute group.
*
* @param Node\Attribute|Node\AttributeGroup $attribute
*
* @return $this The builder instance (for fluid interface)
*/
public function addAttribute($attribute) {
$this->attributeGroups[] = BuilderHelpers::normalizeAttribute($attribute);
return $this;
}
/**
* Returns the built interface node.
*
@@ -70,6 +87,7 @@ class Interface_ extends Declaration
return new Stmt\Interface_($this->name, [
'extends' => $this->extends,
'stmts' => array_merge($this->constants, $this->methods),
'attrGroups' => $this->attributeGroups,
], $this->attributes);
}
}

@@ -15,6 +15,9 @@ class Method extends FunctionLike
/** @var array|null */
protected $stmts = [];
/** @var Node\AttributeGroup[] */
protected $attributeGroups = [];
/**
* Creates a method builder.
*
@@ -112,6 +115,19 @@ class Method extends FunctionLike
return $this;
}
/**
* Adds an attribute group.
*
* @param Node\Attribute|Node\AttributeGroup $attribute
*
* @return $this The builder instance (for fluid interface)
*/
public function addAttribute($attribute) {
$this->attributeGroups[] = BuilderHelpers::normalizeAttribute($attribute);
return $this;
}
/**
* Returns the built method node.
*
@@ -124,6 +140,7 @@ class Method extends FunctionLike
'params' => $this->params,
'returnType' => $this->returnType,
'stmts' => $this->stmts,
'attrGroups' => $this->attributeGroups,
], $this->attributes);
}
}

@@ -37,7 +37,7 @@ class Namespace_ extends Declaration
/**
* Returns the built node.
*
* @return Node The built node
* @return Stmt\Namespace_ The built node
*/
public function getNode() : Node {
return new Stmt\Namespace_($this->name, $this->stmts, $this->attributes);

@@ -19,6 +19,9 @@ class Param implements PhpParser\Builder
protected $variadic = false;
/** @var Node\AttributeGroup[] */
protected $attributeGroups = [];
/**
* Creates a parameter builder.
*
@@ -92,6 +95,19 @@ class Param implements PhpParser\Builder
return $this;
}
/**
* Adds an attribute group.
*
* @param Node\Attribute|Node\AttributeGroup $attribute
*
* @return $this The builder instance (for fluid interface)
*/
public function addAttribute($attribute) {
$this->attributeGroups[] = BuilderHelpers::normalizeAttribute($attribute);
return $this;
}
/**
* Returns the built parameter node.
*
@@ -100,7 +116,7 @@ class Param implements PhpParser\Builder
public function getNode() : Node {
return new Node\Param(
new Node\Expr\Variable($this->name),
$this->default, $this->type, $this->byRef, $this->variadic
$this->default, $this->type, $this->byRef, $this->variadic, [], 0, $this->attributeGroups
);
}
}

@@ -4,6 +4,7 @@ namespace PhpParser\Builder;
use PhpParser;
use PhpParser\BuilderHelpers;
use PhpParser\Node;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\NullableType;
@@ -20,6 +21,9 @@ class Property implements PhpParser\Builder
/** @var null|Identifier|Name|NullableType */
protected $type;
/** @var Node\AttributeGroup[] */
protected $attributeGroups = [];
/**
* Creates a property builder.
*
@@ -73,6 +77,17 @@ class Property implements PhpParser\Builder
return $this;
}
/**
* Makes the property readonly.
*
* @return $this The builder instance (for fluid interface)
*/
public function makeReadonly() {
$this->flags = BuilderHelpers::addModifier($this->flags, Stmt\Class_::MODIFIER_READONLY);
return $this;
}
/**
* Sets default value for the property.
*
@@ -114,6 +129,19 @@ class Property implements PhpParser\Builder
return $this;
}
/**
* Adds an attribute group.
*
* @param Node\Attribute|Node\AttributeGroup $attribute
*
* @return $this The builder instance (for fluid interface)
*/
public function addAttribute($attribute) {
$this->attributeGroups[] = BuilderHelpers::normalizeAttribute($attribute);
return $this;
}
/**
* Returns the built class node.
*
@@ -126,7 +154,8 @@ class Property implements PhpParser\Builder
new Stmt\PropertyProperty($this->name, $this->default)
],
$this->attributes,
$this->type
$this->type,
$this->attributeGroups
);
}
}

@@ -4,6 +4,7 @@ namespace PhpParser\Builder;
use PhpParser;
use PhpParser\BuilderHelpers;
use PhpParser\Node;
use PhpParser\Node\Stmt;
class Trait_ extends Declaration
@@ -13,6 +14,9 @@ class Trait_ extends Declaration
protected $properties = [];
protected $methods = [];
/** @var Node\AttributeGroup[] */
protected $attributeGroups = [];
/**
* Creates an interface builder.
*
@@ -45,6 +49,19 @@ class Trait_ extends Declaration
return $this;
}
/**
* Adds an attribute group.
*
* @param Node\Attribute|Node\AttributeGroup $attribute
*
* @return $this The builder instance (for fluid interface)
*/
public function addAttribute($attribute) {
$this->attributeGroups[] = BuilderHelpers::normalizeAttribute($attribute);
return $this;
}
/**
* Returns the built trait node.
*
@@ -53,7 +70,8 @@ class Trait_ extends Declaration
public function getNode() : PhpParser\Node {
return new Stmt\Trait_(
$this->name, [
'stmts' => array_merge($this->uses, $this->properties, $this->methods)
'stmts' => array_merge($this->uses, $this->properties, $this->methods),
'attrGroups' => $this->attributeGroups,
], $this->attributes
);
}

@@ -39,7 +39,7 @@ class Use_ implements Builder
/**
* Returns the built node.
*
* @return Node The built node
* @return Stmt\Use_ The built node
*/
public function getNode() : Node {
return new Stmt\Use_([

@@ -12,6 +12,21 @@ use PhpParser\Node\Stmt\Use_;
class BuilderFactory
{
/**
* Creates an attribute node.
*
* @param string|Name $name Name of the attribute
* @param array $args Attribute named arguments
*
* @return Node\Attribute
*/
public function attribute($name, array $args = []) : Node\Attribute {
return new Node\Attribute(
BuilderHelpers::normalizeName($name),
$this->args($args)
);
}
/**
* Creates a namespace builder.
*
@@ -161,6 +176,18 @@ class BuilderFactory
return new Builder\Use_($name, Use_::TYPE_CONSTANT);
}
/**
* Creates a class constant builder.
*
* @param string|Identifier $name Name
* @param Node\Expr|bool|null|int|float|string|array $value Value
*
* @return Builder\ClassConst The created use const builder
*/
public function classConst($name, $value) : Builder\ClassConst {
return new Builder\ClassConst($name, $value);
}
/**
* Creates node a for a literal value.
*
@@ -198,12 +225,14 @@ class BuilderFactory
*/
public function args(array $args) : array {
$normalizedArgs = [];
foreach ($args as $arg) {
if ($arg instanceof Arg) {
$normalizedArgs[] = $arg;
} else {
$normalizedArgs[] = new Arg(BuilderHelpers::normalizeValue($arg));
foreach ($args as $key => $arg) {
if (!($arg instanceof Arg)) {
$arg = new Arg(BuilderHelpers::normalizeValue($arg));
}
if (\is_string($key)) {
$arg->name = BuilderHelpers::normalizeIdentifier($key);
}
$normalizedArgs[] = $arg;
}
return $normalizedArgs;
}

@@ -27,7 +27,9 @@ final class BuilderHelpers
public static function normalizeNode($node) : Node {
if ($node instanceof Builder) {
return $node->getNode();
} elseif ($node instanceof Node) {
}
if ($node instanceof Node) {
return $node;
}
@@ -127,18 +129,22 @@ final class BuilderHelpers
private static function normalizeNameCommon($name, bool $allowExpr) {
if ($name instanceof Name) {
return $name;
} elseif (is_string($name)) {
}
if (is_string($name)) {
if (!$name) {
throw new \LogicException('Name cannot be empty');
}
if ($name[0] === '\\') {
return new Name\FullyQualified(substr($name, 1));
} elseif (0 === strpos($name, 'namespace\\')) {
return new Name\Relative(substr($name, strlen('namespace\\')));
} else {
return new Name($name);
}
if (0 === strpos($name, 'namespace\\')) {
return new Name\Relative(substr($name, strlen('namespace\\')));
}
return new Name($name);
}
if ($allowExpr) {
@@ -148,9 +154,9 @@ final class BuilderHelpers
throw new \LogicException(
'Name must be a string or an instance of Node\Name or Node\Expr'
);
} else {
throw new \LogicException('Name must be a string or an instance of Node\Name');
}
throw new \LogicException('Name must be a string or an instance of Node\Name');
}
/**
@@ -183,7 +189,7 @@ final class BuilderHelpers
}
$builtinTypes = [
'array', 'callable', 'string', 'int', 'float', 'bool', 'iterable', 'void', 'object', 'mixed'
'array', 'callable', 'string', 'int', 'float', 'bool', 'iterable', 'void', 'object', 'mixed', 'never',
];
$lowerType = strtolower($type);
@@ -193,12 +199,11 @@ final class BuilderHelpers
$type = self::normalizeName($type);
}
if ($nullable && (string) $type === 'void') {
throw new \LogicException('void type cannot be nullable');
}
if ($nullable && (string) $type === 'mixed') {
throw new \LogicException('mixed type cannot be nullable');
$notNullableTypes = [
'void', 'mixed', 'never',
];
if ($nullable && in_array((string) $type, $notNullableTypes)) {
throw new \LogicException(sprintf('%s type cannot be nullable', $type));
}
return $nullable ? new NullableType($type) : $type;
@@ -215,21 +220,33 @@ final class BuilderHelpers
public static function normalizeValue($value) : Expr {
if ($value instanceof Node\Expr) {
return $value;
} elseif (is_null($value)) {
}
if (is_null($value)) {
return new Expr\ConstFetch(
new Name('null')
);
} elseif (is_bool($value)) {
}
if (is_bool($value)) {
return new Expr\ConstFetch(
new Name($value ? 'true' : 'false')
);
} elseif (is_int($value)) {
}
if (is_int($value)) {
return new Scalar\LNumber($value);
} elseif (is_float($value)) {
}
if (is_float($value)) {
return new Scalar\DNumber($value);
} elseif (is_string($value)) {
}
if (is_string($value)) {
return new Scalar\String_($value);
} elseif (is_array($value)) {
}
if (is_array($value)) {
$items = [];
$lastKey = -1;
foreach ($value as $itemKey => $itemValue) {
@@ -248,9 +265,9 @@ final class BuilderHelpers
}
return new Expr\Array_($items);
} else {
throw new \LogicException('Invalid value');
}
throw new \LogicException('Invalid value');
}
/**
@@ -263,11 +280,33 @@ final class BuilderHelpers
public static function normalizeDocComment($docComment) : Comment\Doc {
if ($docComment instanceof Comment\Doc) {
return $docComment;
} elseif (is_string($docComment)) {
return new Comment\Doc($docComment);
} else {
throw new \LogicException('Doc comment must be a string or an instance of PhpParser\Comment\Doc');
}
if (is_string($docComment)) {
return new Comment\Doc($docComment);
}
throw new \LogicException('Doc comment must be a string or an instance of PhpParser\Comment\Doc');
}
/**
* Normalizes a attribute: Converts attribute to the Attribute Group if needed.
*
* @param Node\Attribute|Node\AttributeGroup $attribute
*
* @return Node\AttributeGroup The Attribute Group
*/
public static function normalizeAttribute($attribute) : Node\AttributeGroup
{
if ($attribute instanceof Node\AttributeGroup) {
return $attribute;
}
if (!($attribute instanceof Node\Attribute)) {
throw new \LogicException('Attribute must be an instance of PhpParser\Node\Attribute or PhpParser\Node\AttributeGroup');
}
return new Node\AttributeGroup([$attribute]);
}
/**

@@ -46,7 +46,8 @@ class TokenStream
* @return bool
*/
public function haveBraces(int $startPos, int $endPos) : bool {
return $this->haveTokenImmediatelyBefore($startPos, '{')
return ($this->haveTokenImmediatelyBefore($startPos, '{')
|| $this->haveTokenImmediatelyBefore($startPos, T_CURLY_OPEN))
&& $this->haveTokenImmediatelyAfter($endPos, '}');
}
@@ -201,6 +202,7 @@ class TokenStream
public function haveBracesInRange(int $startPos, int $endPos) {
return $this->haveTokenInRange($startPos, $endPos, '{')
|| $this->haveTokenInRange($startPos, $endPos, T_CURLY_OPEN)
|| $this->haveTokenInRange($startPos, $endPos, '}');
}

@@ -85,7 +85,6 @@ class Lexer
$scream = ini_set('xdebug.scream', '0');
error_clear_last();
$this->tokens = @token_get_all($code);
$this->postprocessTokens($errorHandler);
@@ -135,10 +134,11 @@ class Lexer
// detected by finding "gaps" in the token array. Unterminated comments are detected
// by checking if a trailing comment has a "*/" at the end.
//
// Additionally, we canonicalize to the PHP 8 comment format here, which does not include
// the trailing whitespace anymore.
//
// We also canonicalize to the PHP 8 T_NAME_* tokens.
// Additionally, we perform a number of canonicalizations here:
// * Use the PHP 8.0 comment format, which does not include trailing whitespace anymore.
// * Use PHP 8.0 T_NAME_* tokens.
// * Use PHP 8.1 T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG and
// T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG tokens used to disambiguate intersection types.
$filePos = 0;
$line = 1;
@@ -209,6 +209,22 @@ class Lexer
}
}
if ($token === '&') {
$next = $i + 1;
while (isset($this->tokens[$next]) && $this->tokens[$next][0] === \T_WHITESPACE) {
$next++;
}
$followedByVarOrVarArg = isset($this->tokens[$next]) &&
($this->tokens[$next][0] === \T_VARIABLE || $this->tokens[$next][0] === \T_ELLIPSIS);
$this->tokens[$i] = $token = [
$followedByVarOrVarArg
? \T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG
: \T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG,
'&',
$line,
];
}
$tokenValue = \is_string($token) ? $token : $token[1];
$tokenLen = \strlen($tokenValue);
@@ -323,7 +339,8 @@ class Lexer
$value = $token[1];
$id = $this->tokenMap[$token[0]];
if (\T_CLOSE_TAG === $token[0]) {
$this->prevCloseTagHasNewline = false !== strpos($token[1], "\n");
$this->prevCloseTagHasNewline = false !== strpos($token[1], "\n")
|| false !== strpos($token[1], "\r");
} elseif (\T_INLINE_HTML === $token[0]) {
$startAttributes['hasLeadingNewline'] = $this->prevCloseTagHasNewline;
}
@@ -422,6 +439,11 @@ class Lexer
'T_MATCH',
'T_NULLSAFE_OBJECT_OPERATOR',
'T_ATTRIBUTE',
// PHP 8.1
'T_ENUM',
'T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG',
'T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG',
'T_READONLY',
];
// PHP-Parser might be used together with another library that also emulates some or all
@@ -439,7 +461,7 @@ class Lexer
$token, $clashingToken
));
}
$usedTokenIds[$token] = $tokenId;
$usedTokenIds[$tokenId] = $token;
}
}
@@ -512,6 +534,10 @@ class Lexer
$tokenMap[\T_MATCH] = Tokens::T_MATCH;
$tokenMap[\T_NULLSAFE_OBJECT_OPERATOR] = Tokens::T_NULLSAFE_OBJECT_OPERATOR;
$tokenMap[\T_ATTRIBUTE] = Tokens::T_ATTRIBUTE;
$tokenMap[\T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG] = Tokens::T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG;
$tokenMap[\T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG] = Tokens::T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG;
$tokenMap[\T_ENUM] = Tokens::T_ENUM;
$tokenMap[\T_READONLY] = Tokens::T_READONLY;
return $tokenMap;
}
@@ -520,7 +546,7 @@ class Lexer
// Based on semi_reserved production.
return array_fill_keys([
\T_STRING,
\T_STATIC, \T_ABSTRACT, \T_FINAL, \T_PRIVATE, \T_PROTECTED, \T_PUBLIC,
\T_STATIC, \T_ABSTRACT, \T_FINAL, \T_PRIVATE, \T_PROTECTED, \T_PUBLIC, \T_READONLY,
\T_INCLUDE, \T_INCLUDE_ONCE, \T_EVAL, \T_REQUIRE, \T_REQUIRE_ONCE, \T_LOGICAL_OR, \T_LOGICAL_XOR, \T_LOGICAL_AND,
\T_INSTANCEOF, \T_NEW, \T_CLONE, \T_EXIT, \T_IF, \T_ELSEIF, \T_ELSE, \T_ENDIF, \T_ECHO, \T_DO, \T_WHILE,
\T_ENDWHILE, \T_FOR, \T_ENDFOR, \T_FOREACH, \T_ENDFOREACH, \T_DECLARE, \T_ENDDECLARE, \T_AS, \T_TRY, \T_CATCH,

@@ -6,21 +6,23 @@ use PhpParser\Error;
use PhpParser\ErrorHandler;
use PhpParser\Lexer;
use PhpParser\Lexer\TokenEmulator\AttributeEmulator;
use PhpParser\Lexer\TokenEmulator\EnumTokenEmulator;
use PhpParser\Lexer\TokenEmulator\CoaleseEqualTokenEmulator;
use PhpParser\Lexer\TokenEmulator\FlexibleDocStringEmulator;
use PhpParser\Lexer\TokenEmulator\FnTokenEmulator;
use PhpParser\Lexer\TokenEmulator\MatchTokenEmulator;
use PhpParser\Lexer\TokenEmulator\NullsafeTokenEmulator;
use PhpParser\Lexer\TokenEmulator\NumericLiteralSeparatorEmulator;
use PhpParser\Lexer\TokenEmulator\ReadonlyTokenEmulator;
use PhpParser\Lexer\TokenEmulator\ReverseEmulator;
use PhpParser\Lexer\TokenEmulator\TokenEmulator;
use PhpParser\Parser\Tokens;
class Emulative extends Lexer
{
const PHP_7_3 = '7.3dev';
const PHP_7_4 = '7.4dev';
const PHP_8_0 = '8.0dev';
const PHP_8_1 = '8.1dev';
/** @var mixed[] Patches used to reverse changes introduced in the code */
private $patches = [];
@@ -38,7 +40,7 @@ class Emulative extends Lexer
*/
public function __construct(array $options = [])
{
$this->targetPhpVersion = $options['phpVersion'] ?? Emulative::PHP_8_0;
$this->targetPhpVersion = $options['phpVersion'] ?? Emulative::PHP_8_1;
unset($options['phpVersion']);
parent::__construct($options);
@@ -51,6 +53,8 @@ class Emulative extends Lexer
new NumericLiteralSeparatorEmulator(),
new NullsafeTokenEmulator(),
new AttributeEmulator(),
new EnumTokenEmulator(),
new ReadonlyTokenEmulator(),
];
// Collect emulators that are relevant for the PHP version we're running

@@ -0,0 +1,31 @@
<?php declare(strict_types=1);
namespace PhpParser\Lexer\TokenEmulator;
use PhpParser\Lexer\Emulative;
final class EnumTokenEmulator extends KeywordEmulator
{
public function getPhpVersion(): string
{
return Emulative::PHP_8_1;
}
public function getKeywordString(): string
{
return 'enum';
}
public function getKeywordToken(): int
{
return \T_ENUM;
}
protected function isKeywordContext(array $tokens, int $pos): bool
{
return parent::isKeywordContext($tokens, $pos)
&& isset($tokens[$pos + 2])
&& $tokens[$pos + 1][0] === \T_WHITESPACE
&& $tokens[$pos + 2][0] === \T_STRING;
}
}

@@ -9,19 +9,21 @@ abstract class KeywordEmulator extends TokenEmulator
public function isEmulationNeeded(string $code): bool
{
return strpos($code, $this->getKeywordString()) !== false;
return strpos(strtolower($code), $this->getKeywordString()) !== false;
}
protected function isKeywordContext(array $tokens, int $pos): bool
{
$previousNonSpaceToken = $this->getPreviousNonSpaceToken($tokens, $pos);
return $previousNonSpaceToken === null || $previousNonSpaceToken[0] !== \T_OBJECT_OPERATOR;
}
public function emulate(string $code, array $tokens): array
{
$keywordString = $this->getKeywordString();
foreach ($tokens as $i => $token) {
if ($token[0] === T_STRING && strtolower($token[1]) === $keywordString) {
$previousNonSpaceToken = $this->getPreviousNonSpaceToken($tokens, $i);
if ($previousNonSpaceToken !== null && $previousNonSpaceToken[0] === \T_OBJECT_OPERATOR) {
continue;
}
if ($token[0] === T_STRING && strtolower($token[1]) === $keywordString
&& $this->isKeywordContext($tokens, $i)) {
$tokens[$i][0] = $this->getKeywordToken();
}
}

@@ -0,0 +1,23 @@
<?php declare(strict_types=1);
namespace PhpParser\Lexer\TokenEmulator;
use PhpParser\Lexer\Emulative;
final class ReadonlyTokenEmulator extends KeywordEmulator
{
public function getPhpVersion(): string
{
return Emulative::PHP_8_1;
}
public function getKeywordString(): string
{
return 'readonly';
}
public function getKeywordToken(): int
{
return \T_READONLY;
}
}

@@ -13,7 +13,7 @@ class Variable extends Expr
* Constructs a variable node.
*
* @param string|Expr $name Name
* @param array $attributes Additional attributes
* @param array $attributes Additional attributes
*/
public function __construct($name, array $attributes = []) {
$this->attributes = $attributes;

@@ -65,6 +65,15 @@ class ClassConst extends Node\Stmt
return (bool) ($this->flags & Class_::MODIFIER_PRIVATE);
}
/**
* Whether constant is final.
*
* @return bool
*/
public function isFinal() : bool {
return (bool) ($this->flags & Class_::MODIFIER_FINAL);
}
public function getType() : string {
return 'Stmt_ClassConst';
}

@@ -13,6 +13,7 @@ class Class_ extends ClassLike
const MODIFIER_STATIC = 8;
const MODIFIER_ABSTRACT = 16;
const MODIFIER_FINAL = 32;
const MODIFIER_READONLY = 64;
const VISIBILITY_MODIFIER_MASK = 7; // 1 | 2 | 4
@@ -32,7 +33,7 @@ class Class_ extends ClassLike
* 'extends' => null : Name of extended class
* 'implements' => array(): Names of implemented interfaces
* 'stmts' => array(): Statements
* '$attrGroups' => array(): PHP attribute groups
* 'attrGroups' => array(): PHP attribute groups
* @param array $attributes Additional attributes
*/
public function __construct($name, array $subNodes = [], array $attributes = []) {
@@ -96,6 +97,10 @@ class Class_ extends ClassLike
throw new Error('Multiple final modifiers are not allowed');
}
if ($a & self::MODIFIER_READONLY && $b & self::MODIFIER_READONLY) {
throw new Error('Multiple readonly modifiers are not allowed');
}
if ($a & 48 && $b & 48) {
throw new Error('Cannot use the final modifier on an abstract class member');
}

@@ -0,0 +1,37 @@
<?php declare(strict_types=1);
namespace PhpParser\Node\Stmt;
use PhpParser\Node;
use PhpParser\Node\AttributeGroup;
class EnumCase extends Node\Stmt
{
/** @var Node\Identifier Enum case name */
public $name;
/** @var Node\Expr|null Enum case expression */
public $expr;
/** @var Node\AttributeGroup[] PHP attribute groups */
public $attrGroups;
/**
* @param string|Node\Identifier $name Enum case name
* @param Node\Expr|null $expr Enum case expression
* @param AttributeGroup[] $attrGroups PHP attribute groups
* @param array $attributes Additional attributes
*/
public function __construct($name, Node\Expr $expr = null, array $attrGroups = [], array $attributes = []) {
parent::__construct($attributes);
$this->name = \is_string($name) ? new Node\Identifier($name) : $name;
$this->expr = $expr;
$this->attrGroups = $attrGroups;
}
public function getSubNodeNames() : array {
return ['attrGroups', 'name', 'expr'];
}
public function getType() : string {
return 'Stmt_EnumCase';
}
}

@@ -0,0 +1,40 @@
<?php declare(strict_types=1);
namespace PhpParser\Node\Stmt;
use PhpParser\Node;
class Enum_ extends ClassLike
{
/** @var null|Node\Identifier Scalar Type */
public $scalarType;
/** @var Node\Name[] Names of implemented interfaces */
public $implements;
/**
* @param string|Node\Identifier|null $name Name
* @param array $subNodes Array of the following optional subnodes:
* 'scalarType' => null : Scalar type
* 'implements' => array() : Names of implemented interfaces
* 'stmts' => array() : Statements
* 'attrGroups' => array() : PHP attribute groups
* @param array $attributes Additional attributes
*/
public function __construct($name, array $subNodes = [], array $attributes = []) {
$this->name = \is_string($name) ? new Node\Identifier($name) : $name;
$this->scalarType = $subNodes['scalarType'] ?? null;
$this->implements = $subNodes['implements'] ?? [];
$this->stmts = $subNodes['stmts'] ?? [];
$this->attrGroups = $subNodes['attrGroups'] ?? [];
parent::__construct($attributes);
}
public function getSubNodeNames() : array {
return ['attrGroups', 'name', 'scalarType', 'implements', 'stmts'];
}
public function getType() : string {
return 'Stmt_Enum';
}
}

@@ -77,6 +77,15 @@ class Property extends Node\Stmt
return (bool) ($this->flags & Class_::MODIFIER_STATIC);
}
/**
* Whether the property is readonly.
*
* @return bool
*/
public function isReadonly() : bool {
return (bool) ($this->flags & Class_::MODIFIER_READONLY);
}
public function getType() : string {
return 'Stmt_Property';
}

@@ -128,6 +128,9 @@ class NodeDumper
if ($flags & Class_::MODIFIER_FINAL) {
$strs[] = 'MODIFIER_FINAL';
}
if ($flags & Class_::MODIFIER_READONLY) {
$strs[] = 'MODIFIER_READONLY';
}
if ($strs) {
return implode(' | ', $strs) . ' (' . $flags . ')';

@@ -86,6 +86,15 @@ class NameResolver extends NodeVisitorAbstract
$this->resolveAttrGroups($node);
$this->addNamespacedName($node);
} elseif ($node instanceof Stmt\Enum_) {
foreach ($node->implements as &$interface) {
$interface = $this->resolveClassName($interface);
}
$this->resolveAttrGroups($node);
if (null !== $node->name) {
$this->addNamespacedName($node);
}
} elseif ($node instanceof Stmt\Trait_) {
$this->resolveAttrGroups($node);
$this->addNamespacedName($node);
@@ -110,6 +119,8 @@ class NameResolver extends NodeVisitorAbstract
}
} else if ($node instanceof Stmt\ClassConst) {
$this->resolveAttrGroups($node);
} else if ($node instanceof Stmt\EnumCase) {
$this->resolveAttrGroups($node);
} elseif ($node instanceof Expr\StaticCall
|| $node instanceof Expr\StaticPropertyFetch
|| $node instanceof Expr\ClassConstFetch

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@@ -35,110 +35,114 @@ final class Tokens
const T_COALESCE = 283;
const T_BOOLEAN_OR = 284;
const T_BOOLEAN_AND = 285;
const T_IS_EQUAL = 286;
const T_IS_NOT_EQUAL = 287;
const T_IS_IDENTICAL = 288;
const T_IS_NOT_IDENTICAL = 289;
const T_SPACESHIP = 290;
const T_IS_SMALLER_OR_EQUAL = 291;
const T_IS_GREATER_OR_EQUAL = 292;
const T_SL = 293;
const T_SR = 294;
const T_INSTANCEOF = 295;
const T_INC = 296;
const T_DEC = 297;
const T_INT_CAST = 298;
const T_DOUBLE_CAST = 299;
const T_STRING_CAST = 300;
const T_ARRAY_CAST = 301;
const T_OBJECT_CAST = 302;
const T_BOOL_CAST = 303;
const T_UNSET_CAST = 304;
const T_POW = 305;
const T_NEW = 306;
const T_CLONE = 307;
const T_EXIT = 308;
const T_IF = 309;
const T_ELSEIF = 310;
const T_ELSE = 311;
const T_ENDIF = 312;
const T_LNUMBER = 313;
const T_DNUMBER = 314;
const T_STRING = 315;
const T_STRING_VARNAME = 316;
const T_VARIABLE = 317;
const T_NUM_STRING = 318;
const T_INLINE_HTML = 319;
const T_ENCAPSED_AND_WHITESPACE = 320;
const T_CONSTANT_ENCAPSED_STRING = 321;
const T_ECHO = 322;
const T_DO = 323;
const T_WHILE = 324;
const T_ENDWHILE = 325;
const T_FOR = 326;
const T_ENDFOR = 327;
const T_FOREACH = 328;
const T_ENDFOREACH = 329;
const T_DECLARE = 330;
const T_ENDDECLARE = 331;
const T_AS = 332;
const T_SWITCH = 333;
const T_MATCH = 334;
const T_ENDSWITCH = 335;
const T_CASE = 336;
const T_DEFAULT = 337;
const T_BREAK = 338;
const T_CONTINUE = 339;
const T_GOTO = 340;
const T_FUNCTION = 341;
const T_FN = 342;
const T_CONST = 343;
const T_RETURN = 344;
const T_TRY = 345;
const T_CATCH = 346;
const T_FINALLY = 347;
const T_USE = 348;
const T_INSTEADOF = 349;
const T_GLOBAL = 350;
const T_STATIC = 351;
const T_ABSTRACT = 352;
const T_FINAL = 353;
const T_PRIVATE = 354;
const T_PROTECTED = 355;
const T_PUBLIC = 356;
const T_VAR = 357;
const T_UNSET = 358;
const T_ISSET = 359;
const T_EMPTY = 360;
const T_HALT_COMPILER = 361;
const T_CLASS = 362;
const T_TRAIT = 363;
const T_INTERFACE = 364;
const T_EXTENDS = 365;
const T_IMPLEMENTS = 366;
const T_OBJECT_OPERATOR = 367;
const T_NULLSAFE_OBJECT_OPERATOR = 368;
const T_LIST = 369;
const T_ARRAY = 370;
const T_CALLABLE = 371;
const T_CLASS_C = 372;
const T_TRAIT_C = 373;
const T_METHOD_C = 374;
const T_FUNC_C = 375;
const T_LINE = 376;
const T_FILE = 377;
const T_START_HEREDOC = 378;
const T_END_HEREDOC = 379;
const T_DOLLAR_OPEN_CURLY_BRACES = 380;
const T_CURLY_OPEN = 381;
const T_PAAMAYIM_NEKUDOTAYIM = 382;
const T_NAMESPACE = 383;
const T_NS_C = 384;
const T_DIR = 385;
const T_NS_SEPARATOR = 386;
const T_ELLIPSIS = 387;
const T_NAME_FULLY_QUALIFIED = 388;
const T_NAME_QUALIFIED = 389;
const T_NAME_RELATIVE = 390;
const T_ATTRIBUTE = 391;
const T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG = 286;
const T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG = 287;
const T_IS_EQUAL = 288;
const T_IS_NOT_EQUAL = 289;
const T_IS_IDENTICAL = 290;
const T_IS_NOT_IDENTICAL = 291;
const T_SPACESHIP = 292;
const T_IS_SMALLER_OR_EQUAL = 293;
const T_IS_GREATER_OR_EQUAL = 294;
const T_SL = 295;
const T_SR = 296;
const T_INSTANCEOF = 297;
const T_INC = 298;
const T_DEC = 299;
const T_INT_CAST = 300;
const T_DOUBLE_CAST = 301;
const T_STRING_CAST = 302;
const T_ARRAY_CAST = 303;
const T_OBJECT_CAST = 304;
const T_BOOL_CAST = 305;
const T_UNSET_CAST = 306;
const T_POW = 307;
const T_NEW = 308;
const T_CLONE = 309;
const T_EXIT = 310;
const T_IF = 311;
const T_ELSEIF = 312;
const T_ELSE = 313;
const T_ENDIF = 314;
const T_LNUMBER = 315;
const T_DNUMBER = 316;
const T_STRING = 317;
const T_STRING_VARNAME = 318;
const T_VARIABLE = 319;
const T_NUM_STRING = 320;
const T_INLINE_HTML = 321;
const T_ENCAPSED_AND_WHITESPACE = 322;
const T_CONSTANT_ENCAPSED_STRING = 323;
const T_ECHO = 324;
const T_DO = 325;
const T_WHILE = 326;
const T_ENDWHILE = 327;
const T_FOR = 328;
const T_ENDFOR = 329;
const T_FOREACH = 330;
const T_ENDFOREACH = 331;
const T_DECLARE = 332;
const T_ENDDECLARE = 333;
const T_AS = 334;
const T_SWITCH = 335;
const T_MATCH = 336;
const T_ENDSWITCH = 337;
const T_CASE = 338;
const T_DEFAULT = 339;
const T_BREAK = 340;
const T_CONTINUE = 341;
const T_GOTO = 342;
const T_FUNCTION = 343;
const T_FN = 344;
const T_CONST = 345;
const T_RETURN = 346;
const T_TRY = 347;
const T_CATCH = 348;
const T_FINALLY = 349;
const T_USE = 350;
const T_INSTEADOF = 351;
const T_GLOBAL = 352;
const T_STATIC = 353;
const T_ABSTRACT = 354;
const T_FINAL = 355;
const T_PRIVATE = 356;
const T_PROTECTED = 357;
const T_PUBLIC = 358;
const T_READONLY = 359;
const T_VAR = 360;
const T_UNSET = 361;
const T_ISSET = 362;
const T_EMPTY = 363;
const T_HALT_COMPILER = 364;
const T_CLASS = 365;
const T_TRAIT = 366;
const T_INTERFACE = 367;
const T_ENUM = 368;
const T_EXTENDS = 369;
const T_IMPLEMENTS = 370;
const T_OBJECT_OPERATOR = 371;
const T_NULLSAFE_OBJECT_OPERATOR = 372;
const T_LIST = 373;
const T_ARRAY = 374;
const T_CALLABLE = 375;
const T_CLASS_C = 376;
const T_TRAIT_C = 377;
const T_METHOD_C = 378;
const T_FUNC_C = 379;
const T_LINE = 380;
const T_FILE = 381;
const T_START_HEREDOC = 382;
const T_END_HEREDOC = 383;
const T_DOLLAR_OPEN_CURLY_BRACES = 384;
const T_CURLY_OPEN = 385;
const T_PAAMAYIM_NEKUDOTAYIM = 386;
const T_NAMESPACE = 387;
const T_NS_C = 388;
const T_DIR = 389;
const T_NS_SEPARATOR = 390;
const T_ELLIPSIS = 391;
const T_NAME_FULLY_QUALIFIED = 392;
const T_NAME_QUALIFIED = 393;
const T_NAME_RELATIVE = 394;
const T_ATTRIBUTE = 395;
}

@@ -16,6 +16,7 @@ use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassConst;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Enum_;
use PhpParser\Node\Stmt\Interface_;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Property;
@@ -60,7 +61,7 @@ abstract class ParserAbstract implements Parser
/** @var int[] Map of states to a displacement into the $action table. The corresponding action for this
* state/symbol pair is $action[$actionBase[$state] + $symbol]. If $actionBase[$state] is 0, the
action is defaulted, i.e. $actionDefault[$state] should be used instead. */
* action is defaulted, i.e. $actionDefault[$state] should be used instead. */
protected $actionBase;
/** @var int[] Table of actions. Indexed according to $actionBase comment. */
protected $action;
@@ -219,10 +220,7 @@ abstract class ParserAbstract implements Parser
));
}
// This is necessary to assign some meaningful attributes to /* empty */ productions. They'll get
// the attributes of the next token, even though they don't contain it themselves.
$this->startAttributeStack[$stackPos+1] = $startAttributes;
$this->endAttributeStack[$stackPos+1] = $endAttributes;
// Allow productions to access the start attributes of the lookahead token.
$this->lookaheadStartAttributes = $startAttributes;
//$this->traceRead($symbol);
@@ -294,7 +292,8 @@ abstract class ParserAbstract implements Parser
/* Goto - shift nonterminal */
$lastEndAttributes = $this->endAttributeStack[$stackPos];
$stackPos -= $this->ruleToLength[$rule];
$ruleLength = $this->ruleToLength[$rule];
$stackPos -= $ruleLength;
$nonTerminal = $this->ruleToNonTerminal[$rule];
$idx = $this->gotoBase[$nonTerminal] + $stateStack[$stackPos];
if ($idx >= 0 && $idx < $this->gotoTableSize && $this->gotoCheck[$idx] === $nonTerminal) {
@@ -307,6 +306,10 @@ abstract class ParserAbstract implements Parser
$stateStack[$stackPos] = $state;
$this->semStack[$stackPos] = $this->semValue;
$this->endAttributeStack[$stackPos] = $lastEndAttributes;
if ($ruleLength === 0) {
// Empty productions use the start attributes of the lookahead token.
$this->startAttributeStack[$stackPos] = $this->lookaheadStartAttributes;
}
} else {
/* error */
switch ($this->errorState) {
@@ -340,6 +343,7 @@ abstract class ParserAbstract implements Parser
// We treat the error symbol as being empty, so we reset the end attributes
// to the end attributes of the last non-error symbol
$this->startAttributeStack[$stackPos] = $this->lookaheadStartAttributes;
$this->endAttributeStack[$stackPos] = $this->endAttributeStack[$stackPos - 1];
$this->endAttributes = $this->endAttributeStack[$stackPos - 1];
break;
@@ -659,6 +663,7 @@ abstract class ParserAbstract implements Parser
'null' => true,
'false' => true,
'mixed' => true,
'never' => true,
];
if (!$name->isUnqualified()) {
@@ -909,22 +914,17 @@ abstract class ParserAbstract implements Parser
}
}
protected function checkClass(Class_ $node, $namePos) {
if (null !== $node->name && $node->name->isSpecialClassName()) {
private function checkClassName($name, $namePos) {
if (null !== $name && $name->isSpecialClassName()) {
$this->emitError(new Error(
sprintf('Cannot use \'%s\' as class name as it is reserved', $node->name),
sprintf('Cannot use \'%s\' as class name as it is reserved', $name),
$this->getAttributesAt($namePos)
));
}
}
if ($node->extends && $node->extends->isSpecialClassName()) {
$this->emitError(new Error(
sprintf('Cannot use \'%s\' as class name as it is reserved', $node->extends),
$node->extends->getAttributes()
));
}
foreach ($node->implements as $interface) {
private function checkImplementedInterfaces(array $interfaces) {
foreach ($interfaces as $interface) {
if ($interface->isSpecialClassName()) {
$this->emitError(new Error(
sprintf('Cannot use \'%s\' as interface name as it is reserved', $interface),
@@ -934,22 +934,27 @@ abstract class ParserAbstract implements Parser
}
}
protected function checkInterface(Interface_ $node, $namePos) {
if (null !== $node->name && $node->name->isSpecialClassName()) {
protected function checkClass(Class_ $node, $namePos) {
$this->checkClassName($node->name, $namePos);
if ($node->extends && $node->extends->isSpecialClassName()) {
$this->emitError(new Error(
sprintf('Cannot use \'%s\' as class name as it is reserved', $node->name),
$this->getAttributesAt($namePos)
sprintf('Cannot use \'%s\' as class name as it is reserved', $node->extends),
$node->extends->getAttributes()
));
}
foreach ($node->extends as $interface) {
if ($interface->isSpecialClassName()) {
$this->emitError(new Error(
sprintf('Cannot use \'%s\' as interface name as it is reserved', $interface),
$interface->getAttributes()
));
}
}
$this->checkImplementedInterfaces($node->implements);
}
protected function checkInterface(Interface_ $node, $namePos) {
$this->checkClassName($node->name, $namePos);
$this->checkImplementedInterfaces($node->extends);
}
protected function checkEnum(Enum_ $node, $namePos) {
$this->checkClassName($node->name, $namePos);
$this->checkImplementedInterfaces($node->implements);
}
protected function checkClassMethod(ClassMethod $node, $modifierPos) {
@@ -972,6 +977,12 @@ abstract class ParserAbstract implements Parser
break;
}
}
if ($node->flags & Class_::MODIFIER_READONLY) {
$this->emitError(new Error(
sprintf('Method %s() cannot be readonly', $node->name),
$this->getAttributesAt($modifierPos)));
}
}
protected function checkClassConst(ClassConst $node, $modifierPos) {
@@ -985,9 +996,9 @@ abstract class ParserAbstract implements Parser
"Cannot use 'abstract' as constant modifier",
$this->getAttributesAt($modifierPos)));
}
if ($node->flags & Class_::MODIFIER_FINAL) {
if ($node->flags & Class_::MODIFIER_READONLY) {
$this->emitError(new Error(
"Cannot use 'final' as constant modifier",
"Cannot use 'readonly' as constant modifier",
$this->getAttributesAt($modifierPos)));
}
}

@@ -727,6 +727,14 @@ class Standard extends PrettyPrinterAbstract
. $this->nl . '{' . $this->pStmts($node->stmts) . $this->nl . '}';
}
protected function pStmt_Enum(Stmt\Enum_ $node) {
return $this->pAttrGroups($node->attrGroups)
. 'enum ' . $node->name
. ($node->scalarType ? " : $node->scalarType" : '')
. (!empty($node->implements) ? ' implements ' . $this->pCommaSeparated($node->implements) : '')
. $this->nl . '{' . $this->pStmts($node->stmts) . $this->nl . '}';
}
protected function pStmt_Class(Stmt\Class_ $node) {
return $this->pClassCommon($node, ' ' . $node->name);
}
@@ -737,6 +745,13 @@ class Standard extends PrettyPrinterAbstract
. $this->nl . '{' . $this->pStmts($node->stmts) . $this->nl . '}';
}
protected function pStmt_EnumCase(Stmt\EnumCase $node) {
return $this->pAttrGroups($node->attrGroups)
. 'case ' . $node->name
. ($node->expr ? ' = ' . $this->p($node->expr) : '')
. ';';
}
protected function pStmt_TraitUse(Stmt\TraitUse $node) {
return 'use ' . $this->pCommaSeparated($node->traits)
. (empty($node->adaptations)
@@ -984,14 +999,27 @@ class Standard extends PrettyPrinterAbstract
$escaped = addcslashes($string, "\n\r\t\f\v$" . $quote . "\\");
}
// Escape other control characters
return preg_replace_callback('/([\0-\10\16-\37])(?=([0-7]?))/', function ($matches) {
$oct = decoct(ord($matches[1]));
if ($matches[2] !== '') {
// If there is a trailing digit, use the full three character form
return '\\' . str_pad($oct, 3, '0', \STR_PAD_LEFT);
}
return '\\' . $oct;
// Escape control characters and non-UTF-8 characters.
// Regex based on https://stackoverflow.com/a/11709412/385378.
$regex = '/(
[\x00-\x08\x0E-\x1F] # Control characters
| [\xC0-\xC1] # Invalid UTF-8 Bytes
| [\xF5-\xFF] # Invalid UTF-8 Bytes
| \xE0(?=[\x80-\x9F]) # Overlong encoding of prior code point
| \xF0(?=[\x80-\x8F]) # Overlong encoding of prior code point
| [\xC2-\xDF](?![\x80-\xBF]) # Invalid UTF-8 Sequence Start
| [\xE0-\xEF](?![\x80-\xBF]{2}) # Invalid UTF-8 Sequence Start
| [\xF0-\xF4](?![\x80-\xBF]{3}) # Invalid UTF-8 Sequence Start
| (?<=[\x00-\x7F\xF5-\xFF])[\x80-\xBF] # Invalid UTF-8 Sequence Middle
| (?<![\xC2-\xDF]|[\xE0-\xEF]|[\xE0-\xEF][\x80-\xBF]|[\xF0-\xF4]|[\xF0-\xF4][\x80-\xBF]|[\xF0-\xF4][\x80-\xBF]{2})[\x80-\xBF] # Overlong Sequence
| (?<=[\xE0-\xEF])[\x80-\xBF](?![\x80-\xBF]) # Short 3 byte sequence
| (?<=[\xF0-\xF4])[\x80-\xBF](?![\x80-\xBF]{2}) # Short 4 byte sequence
| (?<=[\xF0-\xF4][\x80-\xBF])[\x80-\xBF](?![\x80-\xBF]) # Short 4 byte sequence (2)
)/x';
return preg_replace_callback($regex, function ($matches) {
assert(strlen($matches[0]) === 1);
$hex = dechex(ord($matches[0]));;
return '\\x' . str_pad($hex, 2, '0', \STR_PAD_LEFT);
}, $escaped);
}
@@ -1040,7 +1068,7 @@ class Standard extends PrettyPrinterAbstract
* @param Node[] $nodes
* @return bool
*/
private function hasNodeWithComments(array $nodes) {
protected function hasNodeWithComments(array $nodes) {
foreach ($nodes as $node) {
if ($node && $node->getComments()) {
return true;
@@ -1049,7 +1077,7 @@ class Standard extends PrettyPrinterAbstract
return false;
}
private function pMaybeMultiline(array $nodes, bool $trailingComma = false) {
protected function pMaybeMultiline(array $nodes, bool $trailingComma = false) {
if (!$this->hasNodeWithComments($nodes)) {
return $this->pCommaSeparated($nodes);
} else {
@@ -1057,7 +1085,7 @@ class Standard extends PrettyPrinterAbstract
}
}
private function pAttrGroups(array $nodes, bool $inline = false): string {
protected function pAttrGroups(array $nodes, bool $inline = false): string {
$result = '';
$sep = $inline ? ' ' : $this->nl;
foreach ($nodes as $node) {

@@ -66,7 +66,7 @@ abstract class PrettyPrinterAbstract
BinaryOp\BooleanAnd::class => [120, -1],
BinaryOp\BooleanOr::class => [130, -1],
BinaryOp\Coalesce::class => [140, 1],
Expr\Ternary::class => [150, -1],
Expr\Ternary::class => [150, 0],
// parser uses %left for assignments, but they really behave as %right
Expr\Assign::class => [160, 1],
Expr\AssignRef::class => [160, 1],
@@ -756,7 +756,7 @@ abstract class PrettyPrinterAbstract
$itemStartPos = $origArrItem->getStartTokenPos();
$itemEndPos = $origArrItem->getEndTokenPos();
\assert($itemStartPos >= 0 && $itemEndPos >= 0);
\assert($itemStartPos >= 0 && $itemEndPos >= 0 && $itemStartPos >= $pos);
$origIndentLevel = $this->indentLevel;
$lastElemIndentLevel = $this->origTokens->getIndentationBefore($itemStartPos) + $indentAdjustment;
@@ -767,16 +767,25 @@ abstract class PrettyPrinterAbstract
$commentStartPos = $origComments ? $origComments[0]->getStartTokenPos() : $itemStartPos;
\assert($commentStartPos >= 0);
$commentsChanged = $comments !== $origComments;
if ($commentsChanged) {
// Remove old comments
$itemStartPos = $commentStartPos;
if ($commentStartPos < $pos) {
// Comments may be assigned to multiple nodes if they start at the same position.
// Make sure we don't try to print them multiple times.
$commentStartPos = $itemStartPos;
}
if ($skipRemovedNode) {
if ($isStmtList && $this->origTokens->haveBracesInRange($pos, $itemStartPos)) {
// We'd remove the brace of a code block.
// TODO: Preserve formatting.
$this->setIndentLevel($origIndentLevel);
return null;
}
} else {
$result .= $this->origTokens->getTokenCode(
$pos, $commentStartPos, $indentAdjustment);
}
if (!empty($delayedAdd)) {
$result .= $this->origTokens->getTokenCode(
$pos, $commentStartPos, $indentAdjustment);
/** @var Node $delayedAddNode */
foreach ($delayedAdd as $delayedAddNode) {
if ($insertNewline) {
@@ -795,25 +804,16 @@ abstract class PrettyPrinterAbstract
}
}
$result .= $this->origTokens->getTokenCode(
$commentStartPos, $itemStartPos, $indentAdjustment);
$delayedAdd = [];
} else if (!$skipRemovedNode) {
$result .= $this->origTokens->getTokenCode(
$pos, $itemStartPos, $indentAdjustment);
} else {
if ($isStmtList && $this->origTokens->haveBracesInRange($pos, $itemStartPos)) {
// We'd remove the brace of a code block.
// TODO: Preserve formatting.
$this->setIndentLevel($origIndentLevel);
return null;
}
}
if ($commentsChanged && $comments) {
// Add new comments
$result .= $this->pComments($comments) . $this->nl;
if ($comments !== $origComments) {
if ($comments) {
$result .= $this->pComments($comments) . $this->nl;
}
} else {
$result .= $this->origTokens->getTokenCode(
$commentStartPos, $itemStartPos, $indentAdjustment);
}
// If we had to remove anything, we have done so now.
@@ -1074,7 +1074,8 @@ abstract class PrettyPrinterAbstract
. ($modifiers & Stmt\Class_::MODIFIER_PRIVATE ? 'private ' : '')
. ($modifiers & Stmt\Class_::MODIFIER_STATIC ? 'static ' : '')
. ($modifiers & Stmt\Class_::MODIFIER_ABSTRACT ? 'abstract ' : '')
. ($modifiers & Stmt\Class_::MODIFIER_FINAL ? 'final ' : '');
. ($modifiers & Stmt\Class_::MODIFIER_FINAL ? 'final ' : '')
. ($modifiers & Stmt\Class_::MODIFIER_READONLY ? 'readonly ' : '');
}
/**
@@ -1123,7 +1124,8 @@ abstract class PrettyPrinterAbstract
for ($i = 0; $i < 256; $i++) {
// Since PHP 7.1 The lower range is 0x80. However, we also want to support code for
// older versions.
$this->labelCharMap[chr($i)] = $i >= 0x7f || ctype_alnum($i);
$chr = chr($i);
$this->labelCharMap[$chr] = $i >= 0x7f || ctype_alnum($chr);
}
}
@@ -1242,7 +1244,7 @@ abstract class PrettyPrinterAbstract
/**
* Lazily initializes the removal map.
*
* The removal map is used to determine which additional tokens should be returned when a
* The removal map is used to determine which additional tokens should be removed when a
* certain node is replaced by null.
*/
protected function initializeRemovalMap() {
@@ -1269,6 +1271,8 @@ abstract class PrettyPrinterAbstract
'Stmt_Catch->var' => $stripLeft,
'Stmt_ClassMethod->returnType' => $stripColon,
'Stmt_Class->extends' => ['left' => \T_EXTENDS],
'Stmt_Enum->scalarType' => $stripColon,
'Stmt_EnumCase->expr' => $stripEquals,
'Expr_PrintableNewAnonClass->extends' => ['left' => \T_EXTENDS],
'Stmt_Continue->num' => $stripBoth,
'Stmt_Foreach->keyVar' => $stripDoubleArrow,
@@ -1307,6 +1311,8 @@ abstract class PrettyPrinterAbstract
'Stmt_Catch->var' => [null, false, ' ', null],
'Stmt_ClassMethod->returnType' => [')', false, ' : ', null],
'Stmt_Class->extends' => [null, false, ' extends ', null],
'Stmt_Enum->scalarType' => [null, false, ' : ', null],
'Stmt_EnumCase->expr' => [null, false, ' = ', null],
'Expr_PrintableNewAnonClass->extends' => [null, ' extends ', null],
'Stmt_Continue->num' => [\T_CONTINUE, false, ' ', null],
'Stmt_Foreach->keyVar' => [\T_AS, false, null, ' => '],
@@ -1356,6 +1362,7 @@ abstract class PrettyPrinterAbstract
'Stmt_ClassConst->consts' => ', ',
'Stmt_ClassMethod->params' => ', ',
'Stmt_Class->implements' => ', ',
'Stmt_Enum->implements' => ', ',
'Expr_PrintableNewAnonClass->implements' => ', ',
'Stmt_Const->consts' => ', ',
'Stmt_Declare->declares' => ', ',
@@ -1382,6 +1389,7 @@ abstract class PrettyPrinterAbstract
'Stmt_Case->stmts' => "\n",
'Stmt_Catch->stmts' => "\n",
'Stmt_Class->stmts' => "\n",
'Stmt_Enum->stmts' => "\n",
'Expr_PrintableNewAnonClass->stmts' => "\n",
'Stmt_Interface->stmts' => "\n",
'Stmt_Trait->stmts' => "\n",
@@ -1397,6 +1405,8 @@ abstract class PrettyPrinterAbstract
'Stmt_If->stmts' => "\n",
'Stmt_Namespace->stmts' => "\n",
'Stmt_Class->attrGroups' => "\n",
'Stmt_Enum->attrGroups' => "\n",
'Stmt_EnumCase->attrGroups' => "\n",
'Stmt_Interface->attrGroups' => "\n",
'Stmt_Trait->attrGroups' => "\n",
'Stmt_Function->attrGroups' => "\n",
@@ -1435,6 +1445,7 @@ abstract class PrettyPrinterAbstract
'Expr_PrintableNewAnonClass->implements' => [null, ' implements ', ''],
'Expr_StaticCall->args' => ['(', '', ''],
'Stmt_Class->implements' => [null, ' implements ', ''],
'Stmt_Enum->implements' => [null, ' implements ', ''],
'Stmt_ClassMethod->params' => ['(', '', ''],
'Stmt_Interface->extends' => [null, ' extends ', ''],
'Stmt_Function->params' => ['(', '', ''],

@@ -0,0 +1,209 @@
<?php declare(strict_types=1);
namespace PhpParser\Builder;
use PhpParser\Comment;
use PhpParser\Node\Arg;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Const_;
use PhpParser\Node\Expr;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Stmt;
class ClassConstTest extends \PHPUnit\Framework\TestCase
{
public function createClassConstBuilder($name, $value) {
return new ClassConst($name, $value);
}
public function testModifiers() {
$node = $this->createClassConstBuilder("TEST", 1)
->makePrivate()
->getNode()
;
$this->assertEquals(
new Stmt\ClassConst(
[
new Const_("TEST", new LNumber(1))
],
Stmt\Class_::MODIFIER_PRIVATE
),
$node
);
$node = $this->createClassConstBuilder("TEST", 1)
->makeProtected()
->getNode()
;
$this->assertEquals(
new Stmt\ClassConst(
[
new Const_("TEST", new LNumber(1) )
],
Stmt\Class_::MODIFIER_PROTECTED
),
$node
);
$node = $this->createClassConstBuilder("TEST", 1)
->makePublic()
->getNode()
;
$this->assertEquals(
new Stmt\ClassConst(
[
new Const_("TEST", new LNumber(1) )
],
Stmt\Class_::MODIFIER_PUBLIC
),
$node
);
$node = $this->createClassConstBuilder("TEST", 1)
->makeFinal()
->getNode()
;
$this->assertEquals(
new Stmt\ClassConst(
[
new Const_("TEST", new LNumber(1) )
],
Stmt\Class_::MODIFIER_FINAL
),
$node
);
}
public function testDocComment() {
$node = $this->createClassConstBuilder('TEST',1)
->setDocComment('/** Test */')
->makePublic()
->getNode();
$this->assertEquals(
new Stmt\ClassConst(
[
new Const_("TEST", new LNumber(1) )
],
Stmt\Class_::MODIFIER_PUBLIC,
[
'comments' => [new Comment\Doc('/** Test */')]
]
),
$node
);
}
public function testAddConst() {
$node = $this->createClassConstBuilder('FIRST_TEST',1)
->addConst("SECOND_TEST",2)
->getNode();
$this->assertEquals(
new Stmt\ClassConst(
[
new Const_("FIRST_TEST", new LNumber(1)),
new Const_("SECOND_TEST", new LNumber(2))
]
),
$node
);
}
public function testAddAttribute() {
$attribute = new Attribute(
new Name('Attr'),
[new Arg(new LNumber(1), false, false, [], new Identifier('name'))]
);
$attributeGroup = new AttributeGroup([$attribute]);
$node = $this->createClassConstBuilder('ATTR_GROUP', 1)
->addAttribute($attributeGroup)
->getNode();
$this->assertEquals(
new Stmt\ClassConst(
[
new Const_("ATTR_GROUP", new LNumber(1) )
],
0,
[],
[$attributeGroup]
),
$node
);
}
/**
* @dataProvider provideTestDefaultValues
*/
public function testValues($value, $expectedValueNode) {
$node = $this->createClassConstBuilder('TEST', $value)
->getNode()
;
$this->assertEquals($expectedValueNode, $node->consts[0]->value);
}
public function provideTestDefaultValues() {
return [
[
null,
new Expr\ConstFetch(new Name('null'))
],
[
true,
new Expr\ConstFetch(new Name('true'))
],
[
false,
new Expr\ConstFetch(new Name('false'))
],
[
31415,
new Scalar\LNumber(31415)
],
[
3.1415,
new Scalar\DNumber(3.1415)
],
[
'Hallo World',
new Scalar\String_('Hallo World')
],
[
[1, 2, 3],
new Expr\Array_([
new Expr\ArrayItem(new Scalar\LNumber(1)),
new Expr\ArrayItem(new Scalar\LNumber(2)),
new Expr\ArrayItem(new Scalar\LNumber(3)),
])
],
[
['foo' => 'bar', 'bar' => 'foo'],
new Expr\Array_([
new Expr\ArrayItem(
new Scalar\String_('bar'),
new Scalar\String_('foo')
),
new Expr\ArrayItem(
new Scalar\String_('foo'),
new Scalar\String_('bar')
),
])
],
[
new Scalar\MagicConst\Dir,
new Scalar\MagicConst\Dir
]
];
}
}

@@ -4,7 +4,12 @@ namespace PhpParser\Builder;
use PhpParser\Comment;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Stmt;
class ClassTest extends \PHPUnit\Framework\TestCase
@@ -122,6 +127,27 @@ DOC;
);
}
public function testAddAttribute() {
$attribute = new Attribute(
new Name('Attr'),
[new Arg(new LNumber(1), false, false, [], new Identifier('name'))]
);
$attributeGroup = new AttributeGroup([$attribute]);
$class = $this->createClassBuilder('ATTR_GROUP')
->addAttribute($attributeGroup)
->getNode();
$this->assertEquals(
new Stmt\Class_('ATTR_GROUP', [
'attrGroups' => [
$attributeGroup,
]
], []),
$class
);
}
public function testInvalidStmtError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Unexpected node of type "Stmt_Echo"');

@@ -4,8 +4,14 @@ namespace PhpParser\Builder;
use PhpParser\Comment;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Expr\Print_;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt;
@@ -81,6 +87,22 @@ class FunctionTest extends \PHPUnit\Framework\TestCase
]), $node);
}
public function testAddAttribute() {
$attribute = new Attribute(
new Name('Attr'),
[new Arg(new LNumber(1), false, false, [], new Identifier('name'))]
);
$attributeGroup = new AttributeGroup([$attribute]);
$node = $this->createFunctionBuilder('attrGroup')
->addAttribute($attributeGroup)
->getNode();
$this->assertEquals(new Stmt\Function_('attrGroup', [
'attrGroups' => [$attributeGroup],
], []), $node);
}
public function testReturnType() {
$node = $this->createFunctionBuilder('test')
->setReturnType('void')

@@ -4,7 +4,13 @@ namespace PhpParser\Builder;
use PhpParser\Comment;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\DNumber;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Stmt;
class InterfaceTest extends \PHPUnit\Framework\TestCase
@@ -76,6 +82,22 @@ class InterfaceTest extends \PHPUnit\Framework\TestCase
]), $node);
}
public function testAddAttribute() {
$attribute = new Attribute(
new Name('Attr'),
[new Arg(new LNumber(1), false, false, [], new Identifier('name'))]
);
$attributeGroup = new AttributeGroup([$attribute]);
$node = $this->createInterfaceBuilder()
->addAttribute($attributeGroup)
->getNode();
$this->assertEquals(new Stmt\Interface_('Contract', [
'attrGroups' => [$attributeGroup],
], []), $node);
}
public function testInvalidStmtError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Unexpected node of type "Stmt_PropertyProperty"');

@@ -4,8 +4,14 @@ namespace PhpParser\Builder;
use PhpParser\Comment;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Expr\Print_;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt;
@@ -125,6 +131,22 @@ class MethodTest extends \PHPUnit\Framework\TestCase
]), $node);
}
public function testAddAttribute() {
$attribute = new Attribute(
new Name('Attr'),
[new Arg(new LNumber(1), false, false, [], new Identifier('name'))]
);
$attributeGroup = new AttributeGroup([$attribute]);
$node = $this->createMethodBuilder('attributeGroup')
->addAttribute($attributeGroup)
->getNode();
$this->assertEquals(new Stmt\ClassMethod('attributeGroup', [
'attrGroups' => [$attributeGroup],
], []), $node);
}
public function testReturnType() {
$node = $this->createMethodBuilder('test')
->setReturnType('bool')

@@ -3,8 +3,14 @@
namespace PhpParser\Builder;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Expr;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar;
use PhpParser\Node\Scalar\LNumber;
class ParamTest extends \PHPUnit\Framework\TestCase
{
@@ -198,4 +204,21 @@ class ParamTest extends \PHPUnit\Framework\TestCase
$node
);
}
public function testAddAttribute() {
$attribute = new Attribute(
new Name('Attr'),
[new Arg(new LNumber(1), false, false, [], new Identifier('name'))]
);
$attributeGroup = new AttributeGroup([$attribute]);
$node = $this->createParamBuilder('attributeGroup')
->addAttribute($attributeGroup)
->getNode();
$this->assertEquals(
new Node\Param(new Expr\Variable('attributeGroup'), null, null, false, false, [], 0, [$attributeGroup]),
$node
);
}
}

@@ -3,9 +3,14 @@
namespace PhpParser\Builder;
use PhpParser\Comment;
use PhpParser\Node\Arg;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Expr;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Stmt;
class PropertyTest extends \PHPUnit\Framework\TestCase
@@ -61,6 +66,21 @@ class PropertyTest extends \PHPUnit\Framework\TestCase
),
$node
);
$node = $this->createPropertyBuilder('test')
->makeReadonly()
->getNode()
;
$this->assertEquals(
new Stmt\Property(
Stmt\Class_::MODIFIER_READONLY,
[
new Stmt\PropertyProperty('test')
]
),
$node
);
}
public function testDocComment() {
@@ -91,6 +111,32 @@ class PropertyTest extends \PHPUnit\Framework\TestCase
$this->assertEquals($expectedValueNode, $node->props[0]->default);
}
public function testAddAttribute() {
$attribute = new Attribute(
new Name('Attr'),
[new Arg(new LNumber(1), false, false, [], new Identifier('name'))]
);
$attributeGroup = new AttributeGroup([$attribute]);
$node = $this->createPropertyBuilder('test')
->addAttribute($attributeGroup)
->getNode()
;
$this->assertEquals(
new Stmt\Property(
Stmt\Class_::MODIFIER_PUBLIC,
[
new Stmt\PropertyProperty('test')
],
[],
null,
[$attributeGroup]
),
$node
);
}
public function provideTestDefaultValues() {
return [
[

@@ -3,7 +3,12 @@
namespace PhpParser\Builder;
use PhpParser\Comment;
use PhpParser\Node\Arg;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassConst;
@@ -88,4 +93,27 @@ class TraitTest extends \PHPUnit\Framework\TestCase
$this->assertSame($properties, $trait->getProperties());
}
public function testAddAttribute() {
$attribute = new Attribute(
new Name('Attr'),
[new Arg(new LNumber(1), false, false, [], new Identifier('name'))]
);
$attributeGroup = new AttributeGroup([$attribute]);
$node = $this->createTraitBuilder('AttributeGroup')
->addAttribute($attributeGroup)
->getNode()
;
$this->assertEquals(
new Stmt\Trait_(
'AttributeGroup',
[
'attrGroups' => [$attributeGroup],
]
),
$node
);
}
}

@@ -3,6 +3,7 @@
namespace PhpParser;
use PhpParser\Node\Arg;
use PhpParser\Node\Attribute;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\BinaryOp\Concat;
use PhpParser\Node\Identifier;
@@ -36,6 +37,21 @@ class BuilderFactoryTest extends \PHPUnit\Framework\TestCase
];
}
public function testFactoryClassConst() {
$factory = new BuilderFactory;
$this->assertInstanceOf(Builder\ClassConst::class, $factory->classConst('TEST',1));
}
public function testAttribute() {
$factory = new BuilderFactory();
$this->assertEquals(
new Attribute(new Name('AttributeName'), [new Arg(
new String_('bar'), false, false, [], new Identifier('foo')
)]),
$factory->attribute('AttributeName', ['foo' => 'bar'])
);
}
public function testVal() {
// This method is a wrapper around BuilderHelpers::normalizeValue(),
// which is already tested elsewhere
@@ -91,6 +107,17 @@ class BuilderFactoryTest extends \PHPUnit\Framework\TestCase
);
}
public function testNamedArgs() {
$factory = new BuilderFactory();
$this->assertEquals(
[
new Arg(new String_('foo')),
new Arg(new String_('baz'), false, false, [], new Identifier('bar')),
],
$factory->args(['foo', 'bar' => 'baz'])
);
}
public function testCalls() {
$factory = new BuilderFactory();
@@ -246,6 +273,7 @@ class BuilderFactoryTest extends \PHPUnit\Framework\TestCase
->class('SomeClass')
->extend('SomeOtherClass')
->implement('A\Few', '\Interfaces')
->addAttribute($factory->attribute('ClassAttribute', ['repository' => 'fqcn']))
->makeAbstract()
->addStmt($factory->useTrait('FirstTrait'))
@@ -256,7 +284,9 @@ class BuilderFactoryTest extends \PHPUnit\Framework\TestCase
->with($factory->traitUseAdaptation('AnotherTrait', 'baz')->as('test'))
->with($factory->traitUseAdaptation('AnotherTrait', 'func')->insteadof('SecondTrait')))
->addStmt($factory->method('firstMethod'))
->addStmt($factory->method('firstMethod')
->addAttribute($factory->attribute('Route', ['/index', 'name' => 'homepage']))
)
->addStmt($factory->method('someMethod')
->makePublic()
@@ -270,13 +300,28 @@ class BuilderFactoryTest extends \PHPUnit\Framework\TestCase
->addStmt($factory->method('anotherMethod')
->makeProtected()
->addParam($factory->param('someParam')->setDefault('test'))
->addParam($factory->param('someParam')
->setDefault('test')
->addAttribute($factory->attribute('TaggedIterator', ['app.handlers']))
)
->addStmt(new Expr\Print_(new Expr\Variable('someParam'))))
->addStmt($factory->property('someProperty')->makeProtected())
->addStmt($factory->property('anotherProperty')
->makePrivate()
->setDefault([1, 2, 3])))
->setDefault([1, 2, 3]))
->addStmt($factory->property('integerProperty')
->setType('int')
->addAttribute($factory->attribute('Column', ['options' => ['unsigned' => true]]))
->setDefault(1))
->addStmt($factory->classConst('CONST_WITH_ATTRIBUTE', 1)
->makePublic()
->addAttribute($factory->attribute('ConstAttribute'))
)
->addStmt($factory->classConst("FIRST_CLASS_CONST", 1)
->addConst("SECOND_CLASS_CONST",2)
->makePrivate()))
->getNode()
;
@@ -289,6 +334,7 @@ use Foo\Bar\SomeOtherClass;
use Foo\Bar as A;
use function strlen;
use const PHP_VERSION;
#[ClassAttribute(repository: 'fqcn')]
abstract class SomeClass extends SomeOtherClass implements A\Few, \Interfaces
{
use FirstTrait;
@@ -297,8 +343,14 @@ abstract class SomeClass extends SomeOtherClass implements A\Few, \Interfaces
AnotherTrait::baz as test;
AnotherTrait::func insteadof SecondTrait;
}
#[ConstAttribute]
public const CONST_WITH_ATTRIBUTE = 1;
private const FIRST_CLASS_CONST = 1, SECOND_CLASS_CONST = 2;
protected $someProperty;
private $anotherProperty = array(1, 2, 3);
#[Column(options: array('unsigned' => true))]
public int $integerProperty = 1;
#[Route('/index', name: 'homepage')]
function firstMethod()
{
}
@@ -308,7 +360,7 @@ abstract class SomeClass extends SomeOtherClass implements A\Few, \Interfaces
* @param SomeClass And takes a parameter
*/
public abstract function someMethod(SomeClass $someParam);
protected function anotherMethod($someParam = 'test')
protected function anotherMethod(#[TaggedIterator('app.handlers')] $someParam = 'test')
{
print $someParam;
}

@@ -0,0 +1,219 @@
<?php declare(strict_types=1);
namespace PhpParser;
use PhpParser\Builder\Class_;
use PhpParser\Node\Scalar;
use PhpParser\Node\Stmt;
use PhpParser\Node\Expr;
class BuilderHelpersTest extends \PHPUnit\Framework\TestCase
{
public function testNormalizeNode() {
$builder = new Class_('SomeClass');
$this->assertEquals($builder->getNode(), BuilderHelpers::normalizeNode($builder));
$attribute = new Node\Attribute(new Node\Name('Test'));
$this->assertSame($attribute, BuilderHelpers::normalizeNode($attribute));
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Expected node or builder object');
BuilderHelpers::normalizeNode('test');
}
public function testNormalizeStmt() {
$stmt = new Node\Stmt\Class_('Class');
$this->assertSame($stmt, BuilderHelpers::normalizeStmt($stmt));
$expr = new Expr\Variable('fn');
$normalizedExpr = BuilderHelpers::normalizeStmt($expr);
$this->assertEquals(new Stmt\Expression($expr), $normalizedExpr);
$this->assertSame($expr, $normalizedExpr->expr);
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Expected statement or expression node');
BuilderHelpers::normalizeStmt(new Node\Attribute(new Node\Name('Test')));
}
public function testNormalizeStmtInvalidType() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Expected node or builder object');
BuilderHelpers::normalizeStmt('test');
}
public function testNormalizeIdentifier() {
$identifier = new Node\Identifier('fn');
$this->assertSame($identifier, BuilderHelpers::normalizeIdentifier($identifier));
$this->assertEquals($identifier, BuilderHelpers::normalizeIdentifier('fn'));
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Expected string or instance of Node\Identifier');
BuilderHelpers::normalizeIdentifier(1);
}
public function testNormalizeIdentifierOrExpr() {
$identifier = new Node\Identifier('fn');
$this->assertSame($identifier, BuilderHelpers::normalizeIdentifierOrExpr($identifier));
$expr = new Expr\Variable('fn');
$this->assertSame($expr, BuilderHelpers::normalizeIdentifierOrExpr($expr));
$this->assertEquals($identifier, BuilderHelpers::normalizeIdentifierOrExpr('fn'));
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Expected string or instance of Node\Identifier');
BuilderHelpers::normalizeIdentifierOrExpr(1);
}
public function testNormalizeName() {
$name = new Node\Name('test');
$this->assertSame($name, BuilderHelpers::normalizeName($name));
$this->assertEquals(
new Node\Name\FullyQualified(['Namespace', 'Test']),
BuilderHelpers::normalizeName('\\Namespace\\Test')
);
$this->assertEquals(
new Node\Name\Relative(['Test']),
BuilderHelpers::normalizeName('namespace\\Test')
);
$this->assertEquals($name, BuilderHelpers::normalizeName('test'));
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Name cannot be empty');
BuilderHelpers::normalizeName('');
}
public function testNormalizeNameInvalidType() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Name must be a string or an instance of Node\Name');
BuilderHelpers::normalizeName(1);
}
public function testNormalizeNameOrExpr() {
$expr = new Expr\Variable('fn');
$this->assertSame($expr, BuilderHelpers::normalizeNameOrExpr($expr));
$name = new Node\Name('test');
$this->assertSame($name, BuilderHelpers::normalizeNameOrExpr($name));
$this->assertEquals(
new Node\Name\FullyQualified(['Namespace', 'Test']),
BuilderHelpers::normalizeNameOrExpr('\\Namespace\\Test')
);
$this->assertEquals(
new Node\Name\Relative(['Test']),
BuilderHelpers::normalizeNameOrExpr('namespace\\Test')
);
$this->assertEquals($name, BuilderHelpers::normalizeNameOrExpr('test'));
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Name cannot be empty');
BuilderHelpers::normalizeNameOrExpr('');
}
public function testNormalizeNameOrExpInvalidType() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Name must be a string or an instance of Node\Name or Node\Expr');
BuilderHelpers::normalizeNameOrExpr(1);
}
public function testNormalizeType() {
$this->assertEquals(new Node\Identifier('array'), BuilderHelpers::normalizeType('array'));
$this->assertEquals(new Node\Identifier('callable'), BuilderHelpers::normalizeType('callable'));
$this->assertEquals(new Node\Identifier('string'), BuilderHelpers::normalizeType('string'));
$this->assertEquals(new Node\Identifier('int'), BuilderHelpers::normalizeType('int'));
$this->assertEquals(new Node\Identifier('float'), BuilderHelpers::normalizeType('float'));
$this->assertEquals(new Node\Identifier('bool'), BuilderHelpers::normalizeType('bool'));
$this->assertEquals(new Node\Identifier('iterable'), BuilderHelpers::normalizeType('iterable'));
$this->assertEquals(new Node\Identifier('void'), BuilderHelpers::normalizeType('void'));
$this->assertEquals(new Node\Identifier('object'), BuilderHelpers::normalizeType('object'));
$this->assertEquals(new Node\Identifier('mixed'), BuilderHelpers::normalizeType('mixed'));
$this->assertEquals(new Node\Identifier('never'), BuilderHelpers::normalizeType('never'));
$intIdentifier = new Node\Identifier('int');
$this->assertSame($intIdentifier, BuilderHelpers::normalizeType($intIdentifier));
$intName = new Node\Name('int');
$this->assertSame($intName, BuilderHelpers::normalizeType($intName));
$intNullable = new Node\NullableType('int');
$this->assertSame($intNullable, BuilderHelpers::normalizeType($intNullable));
$unionType = new Node\UnionType([new Node\Identifier('int'), new Node\Identifier('string')]);
$this->assertSame($unionType, BuilderHelpers::normalizeType($unionType));
$expectedNullable = new Node\NullableType($intIdentifier);
$nullable = BuilderHelpers::normalizeType('?int');
$this->assertEquals($expectedNullable, $nullable);
$this->assertEquals($intIdentifier, $nullable->type);
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Type must be a string, or an instance of Name, Identifier, NullableType or UnionType');
BuilderHelpers::normalizeType(1);
}
public function testNormalizeTypeNullableVoid() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('void type cannot be nullable');
BuilderHelpers::normalizeType('?void');
}
public function testNormalizeTypeNullableMixed() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('mixed type cannot be nullable');
BuilderHelpers::normalizeType('?mixed');
}
public function testNormalizeTypeNullableNever() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('never type cannot be nullable');
BuilderHelpers::normalizeType('?never');
}
public function testNormalizeValue() {
$expression = new Scalar\LNumber(1);
$this->assertSame($expression, BuilderHelpers::normalizeValue($expression));
$this->assertEquals(new Expr\ConstFetch(new Node\Name('null')), BuilderHelpers::normalizeValue(null));
$this->assertEquals(new Expr\ConstFetch(new Node\Name('true')), BuilderHelpers::normalizeValue(true));
$this->assertEquals(new Expr\ConstFetch(new Node\Name('false')), BuilderHelpers::normalizeValue(false));
$this->assertEquals(new Scalar\LNumber(2), BuilderHelpers::normalizeValue(2));
$this->assertEquals(new Scalar\DNumber(2.5), BuilderHelpers::normalizeValue(2.5));
$this->assertEquals(new Scalar\String_('text'), BuilderHelpers::normalizeValue('text'));
$this->assertEquals(
new Expr\Array_([
new Expr\ArrayItem(new Scalar\LNumber(0)),
new Expr\ArrayItem(new Scalar\LNumber(1), new Scalar\String_('test')),
]),
BuilderHelpers::normalizeValue([
0,
'test' => 1,
])
);
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Invalid value');
BuilderHelpers::normalizeValue(new \stdClass());
}
public function testNormalizeDocComment() {
$docComment = new Comment\Doc('Some doc comment');
$this->assertSame($docComment, BuilderHelpers::normalizeDocComment($docComment));
$this->assertEquals($docComment, BuilderHelpers::normalizeDocComment('Some doc comment'));
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Doc comment must be a string or an instance of PhpParser\Comment\Doc');
BuilderHelpers::normalizeDocComment(1);
}
public function testNormalizeAttribute() {
$attribute = new Node\Attribute(new Node\Name('Test'));
$attributeGroup = new Node\AttributeGroup([$attribute]);
$this->assertEquals($attributeGroup, BuilderHelpers::normalizeAttribute($attribute));
$this->assertSame($attributeGroup, BuilderHelpers::normalizeAttribute($attributeGroup));
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Attribute must be an instance of PhpParser\Node\Attribute or PhpParser\Node\AttributeGroup');
BuilderHelpers::normalizeAttribute('test');
}
}

@@ -107,8 +107,11 @@ class CodeParsingTest extends CodeTestAbstract
$endFilePos < $startFilePos ||
$endTokenPos < $startTokenPos
) {
// Nops and error can have inverted order, if they are empty
if (!$node instanceof Stmt\Nop && !$node instanceof Expr\Error) {
// Nop and Error can have inverted order, if they are empty.
// This can also happen for a Param containing an Error.
if (!$node instanceof Stmt\Nop && !$node instanceof Expr\Error &&
!$node instanceof Node\Param
) {
throw new \Exception('End < start on ' . $node->getType());
}
}

@@ -24,6 +24,17 @@ class EmulativeTest extends LexerTest
$this->assertSame(0, $lexer->getNextToken());
}
/**
* @dataProvider provideTestReplaceKeywords
*/
public function testReplaceKeywordsUppercase($keyword, $expectedToken) {
$lexer = $this->getLexer();
$lexer->startLexing('<?php ' . strtoupper($keyword));
$this->assertSame($expectedToken, $lexer->getNextToken());
$this->assertSame(0, $lexer->getNextToken());
}
/**
* @dataProvider provideTestReplaceKeywords
*/
@@ -302,6 +313,35 @@ class EmulativeTest extends LexerTest
[Tokens::T_END_HEREDOC, " LABEL"],
[ord(','), ','],
]],
// Enums use a contextual keyword
['enum Foo {}', [
[Tokens::T_ENUM, 'enum'],
[Tokens::T_STRING, 'Foo'],
[ord('{'), '{'],
[ord('}'), '}'],
]],
['class Enum {}', [
[Tokens::T_CLASS, 'class'],
[Tokens::T_STRING, 'Enum'],
[ord('{'), '{'],
[ord('}'), '}'],
]],
['class Enum extends X {}', [
[Tokens::T_CLASS, 'class'],
[Tokens::T_STRING, 'Enum'],
[Tokens::T_EXTENDS, 'extends'],
[Tokens::T_STRING, 'X'],
[ord('{'), '{'],
[ord('}'), '}'],
]],
['class Enum implements X {}', [
[Tokens::T_CLASS, 'class'],
[Tokens::T_STRING, 'Enum'],
[Tokens::T_IMPLEMENTS, 'implements'],
[Tokens::T_STRING, 'X'],
[ord('{'), '{'],
[ord('}'), '}'],
]],
];
}
@@ -318,8 +358,11 @@ class EmulativeTest extends LexerTest
return [
['8.0', 'match', [[Tokens::T_MATCH, 'match']]],
['7.4', 'match', [[Tokens::T_STRING, 'match']]],
// Keywords are not case-sensitive.
['7.4', 'fn', [[Tokens::T_FN, 'fn']]],
['7.4', 'FN', [[Tokens::T_FN, 'FN']]],
['7.3', 'fn', [[Tokens::T_STRING, 'fn']]],
['7.3', 'FN', [[Tokens::T_STRING, 'FN']]],
// Tested here to skip testLeaveStuffAloneInStrings.
['8.0', '"$foo?->bar"', [
[ord('"'), '"'],

@@ -22,6 +22,7 @@ class ClassConstTest extends \PHPUnit\Framework\TestCase
$this->assertTrue($node->isPublic());
$this->assertFalse($node->isProtected());
$this->assertFalse($node->isPrivate());
$this->assertFalse($node->isFinal());
}
public function provideModifiers() {
@@ -29,6 +30,7 @@ class ClassConstTest extends \PHPUnit\Framework\TestCase
['public'],
['protected'],
['private'],
['final'],
];
}
}

@@ -23,6 +23,7 @@ class PropertyTest extends \PHPUnit\Framework\TestCase
$this->assertFalse($node->isProtected());
$this->assertFalse($node->isPrivate());
$this->assertFalse($node->isStatic());
$this->assertFalse($node->isReadonly());
}
public function testStaticImplicitlyPublic() {
@@ -31,6 +32,12 @@ class PropertyTest extends \PHPUnit\Framework\TestCase
$this->assertFalse($node->isProtected());
$this->assertFalse($node->isPrivate());
$this->assertTrue($node->isStatic());
$this->assertFalse($node->isReadonly());
}
public function testReadonly() {
$node = new Property(Class_::MODIFIER_READONLY, []);
$this->assertTrue($node->isReadonly());
}
public function provideModifiers() {

@@ -334,6 +334,133 @@ PHP;
}
]
JSON;
$expected81 = <<<'JSON'
[
{
"nodeType": "Stmt_Function",
"attributes": {
"startLine": 4,
"comments": [
{
"nodeType": "Comment",
"text": "\/\/ comment",
"line": 2,
"filePos": 6,
"tokenPos": 1,
"endLine": 2,
"endFilePos": 15,
"endTokenPos": 1
},
{
"nodeType": "Comment_Doc",
"text": "\/** doc comment *\/",
"line": 3,
"filePos": 17,
"tokenPos": 3,
"endLine": 3,
"endFilePos": 34,
"endTokenPos": 3
}
],
"endLine": 6
},
"byRef": false,
"name": {
"nodeType": "Identifier",
"attributes": {
"startLine": 4,
"endLine": 4
},
"name": "functionName"
},
"params": [
{
"nodeType": "Param",
"attributes": {
"startLine": 4,
"endLine": 4
},
"type": null,
"byRef": true,
"variadic": false,
"var": {
"nodeType": "Expr_Variable",
"attributes": {
"startLine": 4,
"endLine": 4
},
"name": "a"
},
"default": {
"nodeType": "Scalar_LNumber",
"attributes": {
"startLine": 4,
"endLine": 4,
"kind": 10
},
"value": 0
},
"flags": 0,
"attrGroups": []
},
{
"nodeType": "Param",
"attributes": {
"startLine": 4,
"endLine": 4
},
"type": null,
"byRef": false,
"variadic": false,
"var": {
"nodeType": "Expr_Variable",
"attributes": {
"startLine": 4,
"endLine": 4
},
"name": "b"
},
"default": {
"nodeType": "Scalar_DNumber",
"attributes": {
"startLine": 4,
"endLine": 4
},
"value": 1
},
"flags": 0,
"attrGroups": []
}
],
"returnType": null,
"stmts": [
{
"nodeType": "Stmt_Echo",
"attributes": {
"startLine": 5,
"endLine": 5
},
"exprs": [
{
"nodeType": "Scalar_String",
"attributes": {
"startLine": 5,
"endLine": 5,
"kind": 1
},
"value": "Foo"
}
]
}
],
"attrGroups": []
}
]
JSON;
if (version_compare(PHP_VERSION, '8.1', '>=')) {
$expected = $expected81;
}
$parser = new Parser\Php7(new Lexer());
$stmts = $parser->parse(canonicalize($code));

@@ -206,6 +206,12 @@ interface A extends C, D {
public function b(A|B|int $a): A|B|int;
}
#[X]
enum E: int {
#[X]
case A = 1;
}
#[X]
trait A {}
@@ -264,6 +270,12 @@ interface A extends \NS\C, \NS\D
public function b(\NS\A|\NS\B|int $a) : \NS\A|\NS\B|int;
}
#[\NS\X]
enum E : int
{
#[\NS\X]
case A = 1;
}
#[\NS\X]
trait A
{
}
@@ -327,6 +339,7 @@ EOC;
]),
new Stmt\Trait_('E'),
new Expr\New_(new Stmt\Class_(null)),
new Stmt\Enum_('F'),
];
$traverser = new PhpParser\NodeTraverser;
@@ -339,6 +352,7 @@ EOC;
$this->assertSame('NS\\D', (string) $stmts[0]->stmts[3]->consts[0]->namespacedName);
$this->assertSame('NS\\E', (string) $stmts[0]->stmts[4]->namespacedName);
$this->assertObjectNotHasAttribute('namespacedName', $stmts[0]->stmts[5]->class);
$this->assertSame('NS\\F', (string) $stmts[0]->stmts[6]->namespacedName);
$stmts = $traverser->traverse([new Stmt\Namespace_(null, $nsStmts)]);
$this->assertSame('A', (string) $stmts[0]->stmts[0]->namespacedName);
@@ -347,6 +361,7 @@ EOC;
$this->assertSame('D', (string) $stmts[0]->stmts[3]->consts[0]->namespacedName);
$this->assertSame('E', (string) $stmts[0]->stmts[4]->namespacedName);
$this->assertObjectNotHasAttribute('namespacedName', $stmts[0]->stmts[5]->class);
$this->assertSame('F', (string) $stmts[0]->stmts[6]->namespacedName);
}
public function testAddRuntimeResolvedNamespacedName() {

@@ -0,0 +1,100 @@
Enum formatting preservation
-----
<?php
enum X: int
{}
-----
$stmts[0]->scalarType = null;
-----
<?php
enum X
{}
-----
<?php
enum X {
case
Y = 1;
}
-----
$stmts[0]->stmts[0]->expr = null;
-----
<?php
enum X {
case
Y;
}
-----
<?php
enum X
{}
-----
$stmts[0]->scalarType = new Node\Identifier('int');
-----
<?php
enum X : int
{}
-----
<?php
enum X
implements Y
{}
-----
$stmts[0]->scalarType = new Node\Identifier('int');
-----
<?php
enum X : int
implements Y
{}
-----
<?php
enum X {
case
Y;
}
-----
$stmts[0]->stmts[0]->expr = new Scalar\LNumber(1);
-----
<?php
enum X {
case
Y = 1;
}
-----
<?php
enum X {
case A;
case B;
}
-----
$stmts[0]->stmts[] = new Node\Stmt\EnumCase('C');
-----
<?php
enum X {
case A;
case B;
case C;
}
-----
<?php
enum X
implements Y
{}
-----
$stmts[0]->implements[] = new Node\Name('Z');
-----
<?php
enum X
implements Y, Z
{}
-----
<?php
enum X
{}
-----
$stmts[0]->implements[] = new Node\Name('Y');
-----
<?php
enum X implements Y
{}

@@ -316,4 +316,23 @@ $stmts[0]->returnType->types[] = new Node\Name('C');
-----
<?php
function test(): A
|B|C {}
|B|C {}
-----
<?php
function test() {
if ($x) {
$a;
$b;
}
$z;
}
-----
$fnStmts =& $stmts[0]->stmts;
array_splice($fnStmts, 0, 1, $fnStmts[0]->stmts);
-----
<?php
function test() {
$a;
$b;
$z;
}

@@ -0,0 +1,9 @@
Rewrite string with variable interpolation
-----
<?php
"{$e}";
-----
$stmts[0]->expr->parts[0]->setAttribute('origNode', null);
-----
<?php
"{$e}";

@@ -1008,7 +1008,7 @@ function(Foo);
Syntax error, unexpected ')', expecting T_VARIABLE from 3:18 to 3:18
Syntax error, unexpected ')', expecting T_VARIABLE from 7:31 to 7:31
Syntax error, unexpected ')', expecting T_VARIABLE from 11:17 to 11:17
Syntax error, unexpected ')', expecting T_VARIABLE from 15:15 to 15:15
Syntax error, unexpected T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG, expecting T_VARIABLE from 15:14 to 15:14
Syntax error, unexpected ')', expecting T_VARIABLE from 19:17 to 19:17
Syntax error, unexpected ')', expecting T_VARIABLE from 22:21 to 22:21
Syntax error, unexpected ')', expecting T_VARIABLE from 25:13 to 25:13
@@ -1137,7 +1137,7 @@ array(
)
flags: 0
type: null
byRef: true
byRef: false
variadic: false
var: Expr_Error(
)
@@ -1401,4 +1401,124 @@ array(
$a = ["a "thing"];
-----
!!php7
Syntax error, unexpected T_STRING, expecting ',' or ']' or ')' from 2:11 to 2:15
Syntax error, unexpected T_STRING, expecting ',' or ']' or ')' from 2:11 to 2:15
-----
<?php
class A {
/** @var ?string */
private $foo
public function __construct(string $s) {
$this->foo = $s;
}
}
class B {
const X = 1
}
-----
!!php7
Syntax error, unexpected T_PUBLIC, expecting ';' from 6:5 to 6:10
Syntax error, unexpected '}', expecting ';' from 12:1 to 12:1
array(
0: Stmt_Class(
attrGroups: array(
)
flags: 0
name: Identifier(
name: A
)
extends: null
implements: array(
)
stmts: array(
0: Stmt_Property(
attrGroups: array(
)
flags: MODIFIER_PRIVATE (4)
type: null
props: array(
0: Stmt_PropertyProperty(
name: VarLikeIdentifier(
name: foo
)
default: null
)
)
comments: array(
0: /** @var ?string */
)
)
1: Stmt_ClassMethod(
attrGroups: array(
)
flags: MODIFIER_PUBLIC (1)
byRef: false
name: Identifier(
name: __construct
)
params: array(
0: Param(
attrGroups: array(
)
flags: 0
type: Identifier(
name: string
)
byRef: false
variadic: false
var: Expr_Variable(
name: s
)
default: null
)
)
returnType: null
stmts: array(
0: Stmt_Expression(
expr: Expr_Assign(
var: Expr_PropertyFetch(
var: Expr_Variable(
name: this
)
name: Identifier(
name: foo
)
)
expr: Expr_Variable(
name: s
)
)
)
)
)
)
)
1: Stmt_Class(
attrGroups: array(
)
flags: 0
name: Identifier(
name: B
)
extends: null
implements: array(
)
stmts: array(
0: Stmt_ClassConst(
attrGroups: array(
)
flags: 0
consts: array(
0: Const(
name: Identifier(
name: X
)
value: Scalar_LNumber(
value: 1
)
)
)
)
)
)
)

@@ -7,6 +7,9 @@ static fn(&$x) => $x;
fn&($x) => $x;
fn($x, ...$rest) => $rest;
fn(): int => $x;
fn($a, $b) => $a and $b;
fn($a, $b) => $a && $b;
-----
!!php7
array(
@@ -172,4 +175,90 @@ array(
)
)
)
6: Stmt_Expression(
expr: Expr_ArrowFunction(
attrGroups: array(
)
static: false
byRef: false
params: array(
0: Param(
attrGroups: array(
)
flags: 0
type: null
byRef: false
variadic: false
var: Expr_Variable(
name: a
)
default: null
)
1: Param(
attrGroups: array(
)
flags: 0
type: null
byRef: false
variadic: false
var: Expr_Variable(
name: b
)
default: null
)
)
returnType: null
expr: Expr_BinaryOp_LogicalAnd(
left: Expr_Variable(
name: a
)
right: Expr_Variable(
name: b
)
)
)
)
7: Stmt_Expression(
expr: Expr_ArrowFunction(
attrGroups: array(
)
static: false
byRef: false
params: array(
0: Param(
attrGroups: array(
)
flags: 0
type: null
byRef: false
variadic: false
var: Expr_Variable(
name: a
)
default: null
)
1: Param(
attrGroups: array(
)
flags: 0
type: null
byRef: false
variadic: false
var: Expr_Variable(
name: b
)
default: null
)
)
returnType: null
expr: Expr_BinaryOp_BooleanAnd(
left: Expr_Variable(
name: a
)
right: Expr_Variable(
name: b
)
)
)
)
)

@@ -0,0 +1,17 @@
Variable variable positions
-----
<?php
$$$x;
-----
!!positions
array(
0: Stmt_Expression[2:1 - 2:5](
expr: Expr_Variable[2:1 - 2:4](
name: Expr_Variable[2:2 - 2:4](
name: Expr_Variable[2:3 - 2:4](
name: x
)
)
)
)
)

@@ -2,26 +2,28 @@ Class position
-----
<?php
class A {
}
if (1);
try {
} catch (Exception $e) {
}
class B {
}
?>
class C {}
-----
!!positions
array(
0: Stmt_Class[3:1 - 4:1](
0: Stmt_If[3:1 - 3:7](
cond: Scalar_LNumber[3:5 - 3:5](
value: 1
)
stmts: array(
)
elseifs: array(
)
else: null
)
1: Stmt_Class[5:1 - 5:10](
attrGroups: array(
)
flags: 0
name: Identifier[3:7 - 3:7](
name: A
name: Identifier[5:7 - 5:7](
name: C
)
extends: null
implements: array(
@@ -29,36 +31,62 @@ array(
stmts: array(
)
)
1: Stmt_TryCatch[6:1 - 8:1](
)
-----
<?php
if (1);
trait X {}
-----
!!positions
array(
0: Stmt_If[3:1 - 3:7](
cond: Scalar_LNumber[3:5 - 3:5](
value: 1
)
stmts: array(
)
catches: array(
0: Stmt_Catch[7:3 - 8:1](
types: array(
0: Name[7:10 - 7:18](
parts: array(
0: Exception
)
)
)
var: Expr_Variable[7:20 - 7:21](
name: e
)
stmts: array(
)
)
elseifs: array(
)
finally: null
else: null
)
2: Stmt_Class[10:1 - 11:1](
1: Stmt_Trait[5:1 - 5:10](
attrGroups: array(
)
flags: 0
name: Identifier[10:7 - 10:7](
name: B
name: Identifier[5:7 - 5:7](
name: X
)
extends: null
implements: array(
stmts: array(
)
)
)
-----
<?php
if (1);
interface X {}
-----
!!positions
array(
0: Stmt_If[3:1 - 3:7](
cond: Scalar_LNumber[3:5 - 3:5](
value: 1
)
stmts: array(
)
elseifs: array(
)
else: null
)
1: Stmt_Interface[5:1 - 5:14](
attrGroups: array(
)
name: Identifier[5:11 - 5:11](
name: X
)
extends: array(
)
stmts: array(
)

@@ -78,11 +78,11 @@ array(
-----
<?php
class A {
final const X = 1;
readonly const X = 1;
}
-----
!!php7
Cannot use 'final' as constant modifier from 3:5 to 3:9
Cannot use 'readonly' as constant modifier from 3:5 to 3:12
array(
0: Stmt_Class(
attrGroups: array(
@@ -98,7 +98,7 @@ array(
0: Stmt_ClassConst(
attrGroups: array(
)
flags: MODIFIER_FINAL (32)
flags: MODIFIER_READONLY (64)
consts: array(
0: Const(
name: Identifier(
@@ -150,4 +150,4 @@ array(
)
)
)
)
)

@@ -7,6 +7,7 @@ class Foo {
public const B = 2;
protected const C = 3;
private const D = 4;
final const E = 5;
}
-----
!!php7
@@ -82,6 +83,21 @@ array(
)
)
)
4: Stmt_ClassConst(
attrGroups: array(
)
flags: MODIFIER_FINAL (32)
consts: array(
0: Const(
name: Identifier(
name: E
)
value: Scalar_LNumber(
value: 5
)
)
)
)
)
)
)

@@ -0,0 +1,65 @@
Enum
-----
<?php
enum A {}
enum B implements Bar, Baz {
}
enum C: int implements Bar {}
-----
!!php7
array(
0: Stmt_Enum(
attrGroups: array(
)
name: Identifier(
name: A
)
scalarType: null
implements: array(
)
stmts: array(
)
)
1: Stmt_Enum(
attrGroups: array(
)
name: Identifier(
name: B
)
scalarType: null
implements: array(
0: Name(
parts: array(
0: Bar
)
)
1: Name(
parts: array(
0: Baz
)
)
)
stmts: array(
)
)
2: Stmt_Enum(
attrGroups: array(
)
name: Identifier(
name: C
)
scalarType: Identifier(
name: int
)
implements: array(
0: Name(
parts: array(
0: Bar
)
)
)
stmts: array(
)
)
)

@@ -0,0 +1,67 @@
Enum
-----
<?php
enum Suit: string
{
case Hearts = 'H';
case Diamonds;
case Clubs = 'C';
case Spades = 'S';
}
-----
!!php7
array(
0: Stmt_Enum(
attrGroups: array(
)
name: Identifier(
name: Suit
)
scalarType: Identifier(
name: string
)
implements: array(
)
stmts: array(
0: Stmt_EnumCase(
attrGroups: array(
)
name: Identifier(
name: Hearts
)
expr: Scalar_String(
value: H
)
)
1: Stmt_EnumCase(
attrGroups: array(
)
name: Identifier(
name: Diamonds
)
expr: null
)
2: Stmt_EnumCase(
attrGroups: array(
)
name: Identifier(
name: Clubs
)
expr: Scalar_String(
value: C
)
)
3: Stmt_EnumCase(
attrGroups: array(
)
name: Identifier(
name: Spades
)
expr: Scalar_String(
value: S
)
)
)
)
)

@@ -66,6 +66,40 @@ array(
)
)
-----
<?php class A { readonly readonly $a; }
-----
!!php7
Multiple readonly modifiers are not allowed from 1:26 to 1:33
array(
0: Stmt_Class(
attrGroups: array(
)
flags: 0
name: Identifier(
name: A
)
extends: null
implements: array(
)
stmts: array(
0: Stmt_Property(
attrGroups: array(
)
flags: MODIFIER_READONLY (64)
type: null
props: array(
0: Stmt_PropertyProperty(
name: VarLikeIdentifier(
name: a
)
default: null
)
)
)
)
)
)
-----
<?php class A { abstract abstract function a(); }
-----
Multiple abstract modifiers are not allowed from 1:26 to 1:33
@@ -221,6 +255,31 @@ array(
)
)
-----
<?php readonly class A { }
// Type in the partial parse could conceivably be any of 0, 16 or 32
-----
Syntax error, unexpected T_READONLY from 1:7 to 1:14
array(
0: Stmt_Class(
attrGroups: array(
)
flags: 0
name: Identifier(
name: A
)
extends: null
implements: array(
)
stmts: array(
)
)
1: Stmt_Nop(
comments: array(
0: // Type in the partial parse could conceivably be any of 0, 16 or 32
)
)
)
-----
<?php class A { abstract $a; }
-----
Properties cannot be declared abstract from 1:17 to 1:24

@@ -6,6 +6,7 @@ class A {
public string $a;
protected static D $b;
private ?float $c;
readonly static public ?int $d;
}
-----
!!php7
@@ -73,6 +74,24 @@ array(
)
)
)
3: Stmt_Property(
attrGroups: array(
)
flags: MODIFIER_PUBLIC | MODIFIER_STATIC | MODIFIER_READONLY (73)
type: NullableType(
type: Identifier(
name: int
)
)
props: array(
0: Stmt_PropertyProperty(
name: VarLikeIdentifier(
name: d
)
default: null
)
)
)
)
)
)

@@ -0,0 +1,35 @@
Methods cannot be readonly
-----
<?php class A { readonly function foo() {} }
-----
!!php7
Method foo() cannot be readonly from 1:17 to 1:24
array(
0: Stmt_Class(
attrGroups: array(
)
flags: 0
name: Identifier(
name: A
)
extends: null
implements: array(
)
stmts: array(
0: Stmt_ClassMethod(
attrGroups: array(
)
flags: MODIFIER_READONLY (64)
byRef: false
name: Identifier(
name: foo
)
params: array(
)
returnType: null
stmts: array(
)
)
)
)
)

@@ -0,0 +1,23 @@
Never type
-----
<?php
function test(): never {}
-----
!!php7
array(
0: Stmt_Function(
attrGroups: array(
)
byRef: false
name: Identifier(
name: test
)
params: array(
)
returnType: Identifier(
name: never
)
stmts: array(
)
)
)

@@ -8,6 +8,8 @@ fn(&$x) => $x;
fn&($x) => $x;
static fn($x, ...$rest) => $rest;
fn(): int => $x;
fn($a, $b) => $a and $b;
fn($a, $b) => $a && $b;
-----
!!php7
fn($a) => $a;
@@ -16,3 +18,5 @@ fn(&$x) => $x;
fn&($x) => $x;
static fn($x, ...$rest) => $rest;
fn(): int => $x;
fn($a, $b) => $a and $b;
fn($a, $b) => $a && $b;

@@ -59,7 +59,7 @@ $a = $b = $c = $d = $f && true;
($a = $b = $c = $d = $f) && true;
$a = $b = $c = $d = $f and true;
$a = $b = $c = $d = ($f and true);
$a ? $b : $c ? $d : $e ? $f : $g;
(($a ? $b : $c) ? $d : $e) ? $f : $g;
$a ? $b : ($c ? $d : ($e ? $f : $g));
$a ? $b ? $c : $d : $f;
$a ?? $b ?? $c;

@@ -4,20 +4,30 @@ Escape sequences in double-quoted strings
"\n\r\t\f\v\$\"\\";
"@@{ implode(range("\0", "\37")) }@@";
"\0000\0001";
"äöü";
"\xc0\x80";
"\xd0\x01";
"\xf0\x80\x80";
<<<DOC
\n\r\t\f\v\$\"\\
@@{ implode(range("\0", "\37")) }@@
\0000\0001
äöü
DOC;
-----
"\n\r\t\f\v\$\"\\";
"\0\1\2\3\4\5\6\7\10\t\n\v\f\r\16\17\20\21\22\23\24\25\26\27\30\31\32\33\34\35\36\37";
"\0000\0001";
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f";
"\x000\x001";
"äöü";
"\xc0\x80";
"\xd0\x01";
"\xf0\x80\x80";
<<<DOC
@@{ "\n\r" }@@\t\f\v\$\\"\\
\0\1\2\3\4\5\6\7\10\t@@{ "\n" }@@\v\f@@{ "\r" }@@\16\17\20\21\22\23\24\25\26\27\30\31\32\33\34\35\36\37
\0000\0001
\x00\x01\x02\x03\x04\x05\x06\x07\x08\t@@{ "\n" }@@\v\f@@{ "\r" }@@\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f
\x000\x001
äöü
DOC
;

@@ -55,4 +55,13 @@ HTML<?php echo 'PHP'; ?>HTML
-----
HTML<?php
echo 'PHP';
?>HTML
?>HTML
-----
<?php
echo 'PHP';
?>@@{ "\r\r\n" }@@Test
-----
<?php
echo 'PHP';
?>@@{ "\n\n" }@@Test

@@ -0,0 +1,39 @@
Enum
-----
<?php
enum A implements B
{
case X;
case Y;
public function foo() {}
}
enum B: int {
case X = 1;
case Y = 2;
}
enum C: string implements D {
case Z = 'A';
}
-----
!!php7
enum A implements B
{
case X;
case Y;
public function foo()
{
}
}
enum B : int
{
case X = 1;
case Y = 2;
}
enum C : string implements D
{
case Z = 'A';
}

@@ -8,6 +8,7 @@ class A
public string $b;
protected static ?float $c = 5.0;
private static ?self $d;
public readonly int|float $e;
}
-----
!!php7
@@ -17,4 +18,5 @@ class A
public string $b;
protected static ?float $c = 5.0;
private static ?self $d;
public readonly int|float $e;
}

@@ -2,4 +2,4 @@ VERSION=$1
wget -q https://github.com/php/php-src/archive/php-$VERSION.tar.gz
mkdir -p ./data/php-src
tar -xzf ./php-$VERSION.tar.gz -C ./data/php-src --strip-components=1
php -n test_old/run.php --verbose --no-progress --php-version=$VERSION PHP ./data/php-src
php test_old/run.php --verbose --no-progress --php-version=$VERSION PHP ./data/php-src