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 (#796)
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 (#782)
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 (#781)
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 #761.
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 #769.
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 (#758)
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 (#767)
Missing argument in the getMessageWithColumnInfo call
2021-03-25 18:37:06 +01:00
WhizSid
d46f261ef9 Adding class constant to builder factory (#765)
* 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 #738 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 #741 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 (#740) 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 (#732) 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 #718: 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 #712.
2020-09-19 23:11:36 +02:00
87 changed files with 6176 additions and 3686 deletions

9
.editorconfig Normal file
View 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
View File

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

90
.github/workflows/main.yml vendored Normal file
View 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"

View File

@@ -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

View File

@@ -1,8 +1,98 @@
Version 4.10.1-dev Version 4.12.1-dev
------------------ ------------------
Nothing yet. 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) Version 4.10.0 (2020-09-19)
--------------------------- ---------------------------

View File

@@ -1,12 +1,12 @@
PHP Parser 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. 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). [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: 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. * Invalid code can be parsed into a partial AST.
* The AST contains accurate location information. * The AST contains accurate location information.
* Dumping the AST in human-readable form. * Dumping the AST in human-readable form.
@@ -215,7 +215,7 @@ Component documentation:
* [JSON representation](doc/component/JSON_representation.markdown) * [JSON representation](doc/component/JSON_representation.markdown)
* JSON encoding and decoding of ASTs * JSON encoding and decoding of ASTs
* [Performance](doc/component/Performance.markdown) * [Performance](doc/component/Performance.markdown)
* Disabling XDebug * Disabling Xdebug
* Reusing objects * Reusing objects
* Garbage collection impact * Garbage collection impact
* [Frequently asked questions](doc/component/FAQ.markdown) * [Frequently asked questions](doc/component/FAQ.markdown)

View File

@@ -10,7 +10,7 @@ foreach ([__DIR__ . '/../../../autoload.php', __DIR__ . '/../vendor/autoload.php
ini_set('xdebug.max_nesting_level', 3000); 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_children', -1);
ini_set('xdebug.var_display_max_data', -1); ini_set('xdebug.var_display_max_data', -1);
ini_set('xdebug.var_display_max_depth', -1); ini_set('xdebug.var_display_max_depth', -1);

View File

@@ -1,7 +1,7 @@
Introduction 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? What is this for?
----------------- -----------------
@@ -26,7 +26,11 @@ programmatic PHP code analysis are incidentally PHP developers, not C developers
What can it parse? 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 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. version it runs on), additionally a wrapper for emulating tokens from newer versions is provided.

View File

@@ -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 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. slower.
Parsing Parsing

View File

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

View File

@@ -36,7 +36,7 @@ if ($e->hasColumnInfo()) {
echo $e->getRawMessage() . ' from ' . $e->getStartLine() . ':' . $e->getStartColumn($code) echo $e->getRawMessage() . ' from ' . $e->getStartLine() . ':' . $e->getStartColumn($code)
. ' to ' . $e->getEndLine() . ':' . $e->getEndColumn($code); . ' to ' . $e->getEndLine() . ':' . $e->getEndColumn($code);
// or: // or:
echo $e->getMessageWithColumnInfo(); echo $e->getMessageWithColumnInfo($code);
} else { } else {
echo $e->getMessage(); 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.

View File

@@ -8,20 +8,20 @@ described in the following.
Xdebug Xdebug
------ ------
Running PHP with XDebug adds a lot of overhead, especially for code that performs many method calls. 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 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*. 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 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 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. 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), 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 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 [composer/xdebug-handler](https://github.com/composer/xdebug-handler) package can be used to do
this. 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 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. 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 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 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 (including for example the parser object itself) may hold cycles, so disabling of GC should be
approached with care. approached with care.

View File

@@ -20,6 +20,11 @@ top_statement_list:
if ($nop !== null) { $1[] = $nop; } $$ = $1; } 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: 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_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_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: optional_ref:
/* empty */ { $$ = false; } /* empty */ { $$ = false; }
| '&' { $$ = true; } | ampersand { $$ = true; }
;
optional_arg_ref:
/* empty */ { $$ = false; }
| T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG { $$ = true; }
; ;
optional_ellipsis: optional_ellipsis:
@@ -378,7 +388,7 @@ new_else_single:
foreach_variable: foreach_variable:
variable { $$ = array($1, false); } variable { $$ = array($1, false); }
| '&' variable { $$ = array($2, true); } | ampersand variable { $$ = array($2, true); }
| list_expr { $$ = array($1, false); } | list_expr { $$ = array($1, false); }
; ;
@@ -393,9 +403,9 @@ non_empty_parameter_list:
; ;
parameter: 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($$); } { $$ = 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($$); } { $$ = Node\Param[$4, $6, $1, $2, $3]; $this->checkParam($$); }
; ;
@@ -428,7 +438,7 @@ non_empty_argument_list:
argument: argument:
expr { $$ = Node\Arg[$1, false, false]; } 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]; } | T_ELLIPSIS expr { $$ = Node\Arg[$2, false, true]; }
; ;
@@ -562,8 +572,8 @@ expr:
variable { $$ = $1; } variable { $$ = $1; }
| list_expr '=' expr { $$ = Expr\Assign[$1, $3]; } | list_expr '=' expr { $$ = Expr\Assign[$1, $3]; }
| variable '=' expr { $$ = Expr\Assign[$1, $3]; } | variable '=' expr { $$ = Expr\Assign[$1, $3]; }
| variable '=' '&' variable { $$ = Expr\AssignRef[$1, $4]; } | variable '=' ampersand variable { $$ = Expr\AssignRef[$1, $4]; }
| variable '=' '&' new_expr { $$ = Expr\AssignRef[$1, $4]; } | variable '=' ampersand new_expr { $$ = Expr\AssignRef[$1, $4]; }
| new_expr { $$ = $1; } | new_expr { $$ = $1; }
| T_CLONE expr { $$ = Expr\Clone_[$2]; } | T_CLONE expr { $$ = Expr\Clone_[$2]; }
| variable T_PLUS_EQUAL expr { $$ = Expr\AssignOp\Plus [$1, $3]; } | 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_AND expr { $$ = Expr\BinaryOp\LogicalAnd[$1, $3]; }
| expr T_LOGICAL_XOR expr { $$ = Expr\BinaryOp\LogicalXor[$1, $3]; } | expr T_LOGICAL_XOR expr { $$ = Expr\BinaryOp\LogicalXor[$1, $3]; }
| expr '|' expr { $$ = Expr\BinaryOp\BitwiseOr [$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\BitwiseXor[$1, $3]; }
| expr '.' expr { $$ = Expr\BinaryOp\Concat [$1, $3]; } | expr '.' expr { $$ = Expr\BinaryOp\Concat [$1, $3]; }
| expr '+' expr { $$ = Expr\BinaryOp\Plus [$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_AND static_scalar { $$ = Expr\BinaryOp\LogicalAnd[$1, $3]; }
| static_scalar T_LOGICAL_XOR static_scalar { $$ = Expr\BinaryOp\LogicalXor[$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\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\BitwiseXor[$1, $3]; }
| static_scalar '.' static_scalar { $$ = Expr\BinaryOp\Concat [$1, $3]; } | static_scalar '.' static_scalar { $$ = Expr\BinaryOp\Concat [$1, $3]; }
| static_scalar '+' static_scalar { $$ = Expr\BinaryOp\Plus [$1, $3]; } | static_scalar '+' static_scalar { $$ = Expr\BinaryOp\Plus [$1, $3]; }
@@ -986,8 +1000,8 @@ non_empty_array_pair_list:
array_pair: array_pair:
expr T_DOUBLE_ARROW expr { $$ = Expr\ArrayItem[$3, $1, false]; } expr T_DOUBLE_ARROW expr { $$ = Expr\ArrayItem[$3, $1, false]; }
| expr { $$ = Expr\ArrayItem[$1, null, false]; } | expr { $$ = Expr\ArrayItem[$1, null, false]; }
| expr T_DOUBLE_ARROW '&' variable { $$ = Expr\ArrayItem[$4, $1, true]; } | expr T_DOUBLE_ARROW ampersand variable { $$ = Expr\ArrayItem[$4, $1, true]; }
| '&' variable { $$ = Expr\ArrayItem[$2, null, true]; } | ampersand variable { $$ = Expr\ArrayItem[$2, null, true]; }
| T_ELLIPSIS expr { $$ = Expr\ArrayItem[$2, null, false, attributes(), true]; } | T_ELLIPSIS expr { $$ = Expr\ArrayItem[$2, null, false, attributes(), true]; }
; ;

View File

@@ -20,6 +20,11 @@ top_statement_list:
if ($nop !== null) { $1[] = $nop; } $$ = $1; } 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: 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_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_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_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_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_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: semi_reserved:
reserved_non_modifiers 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: identifier_ex:
@@ -327,7 +332,12 @@ non_empty_variables_list:
optional_ref: optional_ref:
/* empty */ { $$ = false; } /* empty */ { $$ = false; }
| '&' { $$ = true; } | ampersand { $$ = true; }
;
optional_arg_ref:
/* empty */ { $$ = false; }
| T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG { $$ = true; }
; ;
optional_ellipsis: optional_ellipsis:
@@ -348,10 +358,7 @@ function_declaration_statement:
; ;
class_declaration_statement: class_declaration_statement:
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_[$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 '}'
{ $$ = Stmt\Class_[$3, ['type' => $2, 'extends' => $4, 'implements' => $5, 'stmts' => $7, 'attrGroups' => $1]]; { $$ = Stmt\Class_[$3, ['type' => $2, 'extends' => $4, 'implements' => $5, 'stmts' => $7, 'attrGroups' => $1]];
$this->checkClass($$, #3); } $this->checkClass($$, #3); }
| optional_attributes T_INTERFACE identifier interface_extends_list '{' class_statement_list '}' | optional_attributes T_INTERFACE identifier interface_extends_list '{' class_statement_list '}'
@@ -359,6 +366,18 @@ class_declaration_statement:
$this->checkInterface($$, #3); } $this->checkInterface($$, #3); }
| optional_attributes T_TRAIT identifier '{' class_statement_list '}' | optional_attributes T_TRAIT identifier '{' class_statement_list '}'
{ $$ = Stmt\Trait_[$3, ['stmts' => $5, 'attrGroups' => $1]]; } { $$ = 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: class_entry_type:
@@ -496,7 +515,7 @@ new_else_single:
foreach_variable: foreach_variable:
variable { $$ = array($1, false); } variable { $$ = array($1, false); }
| '&' variable { $$ = array($2, true); } | ampersand variable { $$ = array($2, true); }
| list_expr { $$ = array($1, false); } | list_expr { $$ = array($1, false); }
| array_short_syntax { $$ = array($1, false); } | array_short_syntax { $$ = array($1, false); }
; ;
@@ -516,16 +535,20 @@ optional_visibility_modifier:
| T_PUBLIC { $$ = Stmt\Class_::MODIFIER_PUBLIC; } | T_PUBLIC { $$ = Stmt\Class_::MODIFIER_PUBLIC; }
| T_PROTECTED { $$ = Stmt\Class_::MODIFIER_PROTECTED; } | T_PROTECTED { $$ = Stmt\Class_::MODIFIER_PROTECTED; }
| T_PRIVATE { $$ = Stmt\Class_::MODIFIER_PRIVATE; } | T_PRIVATE { $$ = Stmt\Class_::MODIFIER_PRIVATE; }
| T_READONLY { $$ = Stmt\Class_::MODIFIER_READONLY; }
; ;
parameter: 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); { $$ = new Node\Param($6, null, $3, $4, $5, attributes(), $2, $1);
$this->checkParam($$); } $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); { $$ = new Node\Param($6, $8, $3, $4, $5, attributes(), $2, $1);
$this->checkParam($$); } $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); } { $$ = new Node\Param(Expr\Error[], null, $3, $4, $5, attributes(), $2, $1); }
; ;
@@ -585,7 +608,7 @@ non_empty_argument_list:
argument: argument:
expr { $$ = Node\Arg[$1, false, false]; } 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]; } | T_ELLIPSIS expr { $$ = Node\Arg[$2, false, true]; }
| identifier_ex ':' expr | identifier_ex ':' expr
{ $$ = new Node\Arg($3, false, false, attributes(), $1); } { $$ = new Node\Arg($3, false, false, attributes(), $1); }
@@ -601,7 +624,7 @@ non_empty_global_var_list:
; ;
global_var: global_var:
simple_variable { $$ = Expr\Variable[$1]; } simple_variable { $$ = $1; }
; ;
static_var_list: static_var_list:
@@ -630,16 +653,18 @@ class_statement_list:
; ;
class_statement: 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); { $$ = new Stmt\Property($2, $4, attributes(), $3, $1);
$this->checkProperty($$, #2); } $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); { $$ = new Stmt\ClassConst($4, $2, attributes(), $1);
$this->checkClassConst($$, #2); } $this->checkClassConst($$, #2); }
| optional_attributes method_modifiers T_FUNCTION optional_ref identifier_ex '(' parameter_list ')' optional_return_type method_body | 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]]; { $$ = Stmt\ClassMethod[$5, ['type' => $2, 'byRef' => $4, 'params' => $7, 'returnType' => $9, 'stmts' => $10, 'attrGroups' => $1]];
$this->checkClassMethod($$, #2); } $this->checkClassMethod($$, #2); }
| T_USE class_name_list trait_adaptations { $$ = Stmt\TraitUse[$2, $3]; } | 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 */ } | error { $$ = null; /* will be skipped */ }
; ;
@@ -701,6 +726,7 @@ member_modifier:
| T_STATIC { $$ = Stmt\Class_::MODIFIER_STATIC; } | T_STATIC { $$ = Stmt\Class_::MODIFIER_STATIC; }
| T_ABSTRACT { $$ = Stmt\Class_::MODIFIER_ABSTRACT; } | T_ABSTRACT { $$ = Stmt\Class_::MODIFIER_ABSTRACT; }
| T_FINAL { $$ = Stmt\Class_::MODIFIER_FINAL; } | T_FINAL { $$ = Stmt\Class_::MODIFIER_FINAL; }
| T_READONLY { $$ = Stmt\Class_::MODIFIER_READONLY; }
; ;
property_declaration_list: property_declaration_list:
@@ -745,7 +771,7 @@ expr:
| list_expr '=' expr { $$ = Expr\Assign[$1, $3]; } | list_expr '=' expr { $$ = Expr\Assign[$1, $3]; }
| array_short_syntax '=' expr { $$ = Expr\Assign[$1, $3]; } | array_short_syntax '=' expr { $$ = Expr\Assign[$1, $3]; }
| variable '=' 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; } | new_expr { $$ = $1; }
| match { $$ = $1; } | match { $$ = $1; }
| T_CLONE expr { $$ = Expr\Clone_[$2]; } | T_CLONE expr { $$ = Expr\Clone_[$2]; }
@@ -772,7 +798,8 @@ expr:
| expr T_LOGICAL_AND expr { $$ = Expr\BinaryOp\LogicalAnd[$1, $3]; } | expr T_LOGICAL_AND expr { $$ = Expr\BinaryOp\LogicalAnd[$1, $3]; }
| expr T_LOGICAL_XOR expr { $$ = Expr\BinaryOp\LogicalXor[$1, $3]; } | expr T_LOGICAL_XOR expr { $$ = Expr\BinaryOp\LogicalXor[$1, $3]; }
| expr '|' expr { $$ = Expr\BinaryOp\BitwiseOr [$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\BitwiseXor[$1, $3]; }
| expr '.' expr { $$ = Expr\BinaryOp\Concat [$1, $3]; } | expr '.' expr { $$ = Expr\BinaryOp\Concat [$1, $3]; }
| expr '+' expr { $$ = Expr\BinaryOp\Plus [$1, $3]; } | expr '+' expr { $$ = Expr\BinaryOp\Plus [$1, $3]; }
@@ -832,18 +859,18 @@ expr:
| T_YIELD_FROM expr { $$ = Expr\YieldFrom[$2]; } | T_YIELD_FROM expr { $$ = Expr\YieldFrom[$2]; }
| T_THROW expr { $$ = Expr\Throw_[$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' => []]]; } { $$ = 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' => []]]; } { $$ = 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 | 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' => []]]; } { $$ = 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 | 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' => []]]; } { $$ = 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]]; } { $$ = 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]]; } { $$ = 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 | 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]]; } { $$ = Expr\Closure[['static' => false, 'byRef' => $3, 'params' => $5, 'uses' => $7, 'returnType' => $8, 'stmts' => $9, 'attrGroups' => $1]]; }
@@ -1007,7 +1034,7 @@ callable_expr:
; ;
callable_variable: callable_variable:
simple_variable { $$ = Expr\Variable[$1]; } simple_variable { $$ = $1; }
| array_object_dereferencable '[' optional_expr ']' { $$ = Expr\ArrayDimFetch[$1, $3]; } | array_object_dereferencable '[' optional_expr ']' { $$ = Expr\ArrayDimFetch[$1, $3]; }
| array_object_dereferencable '{' expr '}' { $$ = Expr\ArrayDimFetch[$1, $3]; } | array_object_dereferencable '{' expr '}' { $$ = Expr\ArrayDimFetch[$1, $3]; }
| function_call { $$ = $1; } | function_call { $$ = $1; }
@@ -1032,15 +1059,15 @@ variable:
; ;
simple_variable: simple_variable:
T_VARIABLE { $$ = parseVar($1); } plain_variable { $$ = $1; }
| '$' '{' expr '}' { $$ = $3; } | '$' '{' expr '}' { $$ = Expr\Variable[$3]; }
| '$' simple_variable { $$ = Expr\Variable[$2]; } | '$' simple_variable { $$ = Expr\Variable[$2]; }
| '$' error { $$ = Expr\Error[]; $this->errorState = 2; } | '$' error { $$ = Expr\Variable[Expr\Error[]]; $this->errorState = 2; }
; ;
static_member_prop_name: static_member_prop_name:
simple_variable simple_variable
{ $var = $1; $$ = \is_string($var) ? Node\VarLikeIdentifier[$var] : $var; } { $var = $1->name; $$ = \is_string($var) ? Node\VarLikeIdentifier[$var] : $var; }
; ;
static_member: static_member:
@@ -1049,7 +1076,7 @@ static_member:
; ;
new_variable: new_variable:
simple_variable { $$ = Expr\Variable[$1]; } simple_variable { $$ = $1; }
| new_variable '[' optional_expr ']' { $$ = Expr\ArrayDimFetch[$1, $3]; } | new_variable '[' optional_expr ']' { $$ = Expr\ArrayDimFetch[$1, $3]; }
| new_variable '{' expr '}' { $$ = Expr\ArrayDimFetch[$1, $3]; } | new_variable '{' expr '}' { $$ = Expr\ArrayDimFetch[$1, $3]; }
| new_variable T_OBJECT_OPERATOR property_name { $$ = Expr\PropertyFetch[$1, $3]; } | new_variable T_OBJECT_OPERATOR property_name { $$ = Expr\PropertyFetch[$1, $3]; }
@@ -1063,13 +1090,13 @@ new_variable:
member_name: member_name:
identifier_ex { $$ = $1; } identifier_ex { $$ = $1; }
| '{' expr '}' { $$ = $2; } | '{' expr '}' { $$ = $2; }
| simple_variable { $$ = Expr\Variable[$1]; } | simple_variable { $$ = $1; }
; ;
property_name: property_name:
identifier { $$ = $1; } identifier { $$ = $1; }
| '{' expr '}' { $$ = $2; } | '{' expr '}' { $$ = $2; }
| simple_variable { $$ = Expr\Variable[$1]; } | simple_variable { $$ = $1; }
| error { $$ = Expr\Error[]; $this->errorState = 2; } | error { $$ = Expr\Error[]; $this->errorState = 2; }
; ;
@@ -1095,10 +1122,10 @@ inner_array_pair_list:
array_pair: array_pair:
expr { $$ = Expr\ArrayItem[$1, null, false]; } 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]; } | list_expr { $$ = Expr\ArrayItem[$1, null, false]; }
| expr T_DOUBLE_ARROW expr { $$ = Expr\ArrayItem[$3, $1, 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]; } | expr T_DOUBLE_ARROW list_expr { $$ = Expr\ArrayItem[$3, $1, false]; }
| T_ELLIPSIS expr { $$ = Expr\ArrayItem[$2, null, false, attributes(), true]; } | T_ELLIPSIS expr { $$ = Expr\ArrayItem[$2, null, false, attributes(), true]; }
| /* empty */ { $$ = null; } | /* empty */ { $$ = null; }

192
grammar/phpyLang.php Normal file
View 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;
}

View File

@@ -1,5 +1,7 @@
<?php <?php
require __DIR__ . '/phpyLang.php';
$grammarFileToName = [ $grammarFileToName = [
__DIR__ . '/php5.y' => 'Php5', __DIR__ . '/php5.y' => 'Php5',
__DIR__ . '/php7.y' => 'Php7', __DIR__ . '/php7.y' => 'Php7',
@@ -23,21 +25,6 @@ $options = array_flip($argv);
$optionDebug = isset($options['--debug']); $optionDebug = isset($options['--debug']);
$optionKeepTmpGrammar = isset($options['--keep-tmp-grammar']); $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 /// /// Main script ///
/////////////////// ///////////////////
@@ -49,10 +36,7 @@ foreach ($grammarFileToName as $grammarFile => $name) {
$grammarCode = file_get_contents($grammarFile); $grammarCode = file_get_contents($grammarFile);
$grammarCode = str_replace('%tokens', $tokens, $grammarCode); $grammarCode = str_replace('%tokens', $tokens, $grammarCode);
$grammarCode = preprocessGrammar($grammarCode);
$grammarCode = resolveNodes($grammarCode);
$grammarCode = resolveMacros($grammarCode);
$grammarCode = resolveStackAccess($grammarCode);
file_put_contents($tmpGrammarFile, $grammarCode); file_put_contents($tmpGrammarFile, $grammarCode);
@@ -77,151 +61,9 @@ foreach ($grammarFileToName as $grammarFile => $name) {
} }
} }
/////////////////////////////// ////////////////////////////////
/// Preprocessing functions /// /// Utility helper 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);
}
function ensureDirExists($dir) { function ensureDirExists($dir) {
if (!is_dir($dir)) { if (!is_dir($dir)) {
@@ -237,25 +79,3 @@ function execCmd($cmd) {
} }
return $output; 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;
}

View File

@@ -18,7 +18,7 @@
%left T_BOOLEAN_AND %left T_BOOLEAN_AND
%left '|' %left '|'
%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_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 %nonassoc '<' T_IS_SMALLER_OR_EQUAL '>' T_IS_GREATER_OR_EQUAL
%left T_SL T_SR %left T_SL T_SR
@@ -74,7 +74,7 @@
%token T_USE %token T_USE
%token T_INSTEADOF %token T_INSTEADOF
%token T_GLOBAL %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_VAR
%token T_UNSET %token T_UNSET
%token T_ISSET %token T_ISSET
@@ -83,6 +83,7 @@
%token T_CLASS %token T_CLASS
%token T_TRAIT %token T_TRAIT
%token T_INTERFACE %token T_INTERFACE
%token T_ENUM
%token T_EXTENDS %token T_EXTENDS
%token T_IMPLEMENTS %token T_IMPLEMENTS
%token T_OBJECT_OPERATOR %token T_OBJECT_OPERATOR
@@ -110,4 +111,5 @@
%token T_NAME_FULLY_QUALIFIED %token T_NAME_FULLY_QUALIFIED
%token T_NAME_QUALIFIED %token T_NAME_QUALIFIED
%token T_NAME_RELATIVE %token T_NAME_RELATIVE
%token T_ATTRIBUTE %token T_ATTRIBUTE
%token T_ENUM

View File

@@ -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
);
}
}

View File

@@ -4,6 +4,7 @@ namespace PhpParser\Builder;
use PhpParser; use PhpParser;
use PhpParser\BuilderHelpers; use PhpParser\BuilderHelpers;
use PhpParser\Node;
use PhpParser\Node\Name; use PhpParser\Node\Name;
use PhpParser\Node\Stmt; use PhpParser\Node\Stmt;
@@ -20,6 +21,9 @@ class Class_ extends Declaration
protected $properties = []; protected $properties = [];
protected $methods = []; protected $methods = [];
/** @var Node\AttributeGroup[] */
protected $attributeGroups = [];
/** /**
* Creates a class builder. * Creates a class builder.
* *
@@ -106,6 +110,19 @@ class Class_ extends Declaration
return $this; 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. * Returns the built class node.
* *
@@ -117,6 +134,7 @@ class Class_ extends Declaration
'extends' => $this->extends, 'extends' => $this->extends,
'implements' => $this->implements, 'implements' => $this->implements,
'stmts' => array_merge($this->uses, $this->constants, $this->properties, $this->methods), 'stmts' => array_merge($this->uses, $this->constants, $this->properties, $this->methods),
'attrGroups' => $this->attributeGroups,
], $this->attributes); ], $this->attributes);
} }
} }

View File

@@ -12,6 +12,9 @@ class Function_ extends FunctionLike
protected $name; protected $name;
protected $stmts = []; protected $stmts = [];
/** @var Node\AttributeGroup[] */
protected $attributeGroups = [];
/** /**
* Creates a function builder. * Creates a function builder.
* *
@@ -34,6 +37,19 @@ class Function_ extends FunctionLike
return $this; 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. * Returns the built function node.
* *
@@ -45,6 +61,7 @@ class Function_ extends FunctionLike
'params' => $this->params, 'params' => $this->params,
'returnType' => $this->returnType, 'returnType' => $this->returnType,
'stmts' => $this->stmts, 'stmts' => $this->stmts,
'attrGroups' => $this->attributeGroups,
], $this->attributes); ], $this->attributes);
} }
} }

View File

@@ -4,6 +4,7 @@ namespace PhpParser\Builder;
use PhpParser; use PhpParser;
use PhpParser\BuilderHelpers; use PhpParser\BuilderHelpers;
use PhpParser\Node;
use PhpParser\Node\Name; use PhpParser\Node\Name;
use PhpParser\Node\Stmt; use PhpParser\Node\Stmt;
@@ -14,6 +15,9 @@ class Interface_ extends Declaration
protected $constants = []; protected $constants = [];
protected $methods = []; protected $methods = [];
/** @var Node\AttributeGroup[] */
protected $attributeGroups = [];
/** /**
* Creates an interface builder. * Creates an interface builder.
* *
@@ -61,6 +65,19 @@ class Interface_ extends Declaration
return $this; 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. * Returns the built interface node.
* *
@@ -70,6 +87,7 @@ class Interface_ extends Declaration
return new Stmt\Interface_($this->name, [ return new Stmt\Interface_($this->name, [
'extends' => $this->extends, 'extends' => $this->extends,
'stmts' => array_merge($this->constants, $this->methods), 'stmts' => array_merge($this->constants, $this->methods),
'attrGroups' => $this->attributeGroups,
], $this->attributes); ], $this->attributes);
} }
} }

View File

@@ -15,6 +15,9 @@ class Method extends FunctionLike
/** @var array|null */ /** @var array|null */
protected $stmts = []; protected $stmts = [];
/** @var Node\AttributeGroup[] */
protected $attributeGroups = [];
/** /**
* Creates a method builder. * Creates a method builder.
* *
@@ -112,6 +115,19 @@ class Method extends FunctionLike
return $this; 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. * Returns the built method node.
* *
@@ -124,6 +140,7 @@ class Method extends FunctionLike
'params' => $this->params, 'params' => $this->params,
'returnType' => $this->returnType, 'returnType' => $this->returnType,
'stmts' => $this->stmts, 'stmts' => $this->stmts,
'attrGroups' => $this->attributeGroups,
], $this->attributes); ], $this->attributes);
} }
} }

View File

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

View File

@@ -19,6 +19,9 @@ class Param implements PhpParser\Builder
protected $variadic = false; protected $variadic = false;
/** @var Node\AttributeGroup[] */
protected $attributeGroups = [];
/** /**
* Creates a parameter builder. * Creates a parameter builder.
* *
@@ -92,6 +95,19 @@ class Param implements PhpParser\Builder
return $this; 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. * Returns the built parameter node.
* *
@@ -100,7 +116,7 @@ class Param implements PhpParser\Builder
public function getNode() : Node { public function getNode() : Node {
return new Node\Param( return new Node\Param(
new Node\Expr\Variable($this->name), 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
); );
} }
} }

View File

@@ -4,6 +4,7 @@ namespace PhpParser\Builder;
use PhpParser; use PhpParser;
use PhpParser\BuilderHelpers; use PhpParser\BuilderHelpers;
use PhpParser\Node;
use PhpParser\Node\Identifier; use PhpParser\Node\Identifier;
use PhpParser\Node\Name; use PhpParser\Node\Name;
use PhpParser\Node\NullableType; use PhpParser\Node\NullableType;
@@ -20,6 +21,9 @@ class Property implements PhpParser\Builder
/** @var null|Identifier|Name|NullableType */ /** @var null|Identifier|Name|NullableType */
protected $type; protected $type;
/** @var Node\AttributeGroup[] */
protected $attributeGroups = [];
/** /**
* Creates a property builder. * Creates a property builder.
* *
@@ -73,6 +77,17 @@ class Property implements PhpParser\Builder
return $this; 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. * Sets default value for the property.
* *
@@ -114,6 +129,19 @@ class Property implements PhpParser\Builder
return $this; 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. * Returns the built class node.
* *
@@ -126,7 +154,8 @@ class Property implements PhpParser\Builder
new Stmt\PropertyProperty($this->name, $this->default) new Stmt\PropertyProperty($this->name, $this->default)
], ],
$this->attributes, $this->attributes,
$this->type $this->type,
$this->attributeGroups
); );
} }
} }

View File

@@ -4,6 +4,7 @@ namespace PhpParser\Builder;
use PhpParser; use PhpParser;
use PhpParser\BuilderHelpers; use PhpParser\BuilderHelpers;
use PhpParser\Node;
use PhpParser\Node\Stmt; use PhpParser\Node\Stmt;
class Trait_ extends Declaration class Trait_ extends Declaration
@@ -13,6 +14,9 @@ class Trait_ extends Declaration
protected $properties = []; protected $properties = [];
protected $methods = []; protected $methods = [];
/** @var Node\AttributeGroup[] */
protected $attributeGroups = [];
/** /**
* Creates an interface builder. * Creates an interface builder.
* *
@@ -45,6 +49,19 @@ class Trait_ extends Declaration
return $this; 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. * Returns the built trait node.
* *
@@ -53,7 +70,8 @@ class Trait_ extends Declaration
public function getNode() : PhpParser\Node { public function getNode() : PhpParser\Node {
return new Stmt\Trait_( return new Stmt\Trait_(
$this->name, [ $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 ], $this->attributes
); );
} }

View File

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

View File

@@ -12,6 +12,21 @@ use PhpParser\Node\Stmt\Use_;
class BuilderFactory 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. * Creates a namespace builder.
* *
@@ -161,6 +176,18 @@ class BuilderFactory
return new Builder\Use_($name, Use_::TYPE_CONSTANT); 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. * Creates node a for a literal value.
* *
@@ -198,12 +225,14 @@ class BuilderFactory
*/ */
public function args(array $args) : array { public function args(array $args) : array {
$normalizedArgs = []; $normalizedArgs = [];
foreach ($args as $arg) { foreach ($args as $key => $arg) {
if ($arg instanceof Arg) { if (!($arg instanceof Arg)) {
$normalizedArgs[] = $arg; $arg = new Arg(BuilderHelpers::normalizeValue($arg));
} else {
$normalizedArgs[] = new Arg(BuilderHelpers::normalizeValue($arg));
} }
if (\is_string($key)) {
$arg->name = BuilderHelpers::normalizeIdentifier($key);
}
$normalizedArgs[] = $arg;
} }
return $normalizedArgs; return $normalizedArgs;
} }

View File

@@ -27,7 +27,9 @@ final class BuilderHelpers
public static function normalizeNode($node) : Node { public static function normalizeNode($node) : Node {
if ($node instanceof Builder) { if ($node instanceof Builder) {
return $node->getNode(); return $node->getNode();
} elseif ($node instanceof Node) { }
if ($node instanceof Node) {
return $node; return $node;
} }
@@ -127,18 +129,22 @@ final class BuilderHelpers
private static function normalizeNameCommon($name, bool $allowExpr) { private static function normalizeNameCommon($name, bool $allowExpr) {
if ($name instanceof Name) { if ($name instanceof Name) {
return $name; return $name;
} elseif (is_string($name)) { }
if (is_string($name)) {
if (!$name) { if (!$name) {
throw new \LogicException('Name cannot be empty'); throw new \LogicException('Name cannot be empty');
} }
if ($name[0] === '\\') { if ($name[0] === '\\') {
return new Name\FullyQualified(substr($name, 1)); 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) { if ($allowExpr) {
@@ -148,9 +154,9 @@ final class BuilderHelpers
throw new \LogicException( throw new \LogicException(
'Name must be a string or an instance of Node\Name or Node\Expr' '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 = [ $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); $lowerType = strtolower($type);
@@ -193,12 +199,11 @@ final class BuilderHelpers
$type = self::normalizeName($type); $type = self::normalizeName($type);
} }
if ($nullable && (string) $type === 'void') { $notNullableTypes = [
throw new \LogicException('void type cannot be nullable'); 'void', 'mixed', 'never',
} ];
if ($nullable && in_array((string) $type, $notNullableTypes)) {
if ($nullable && (string) $type === 'mixed') { throw new \LogicException(sprintf('%s type cannot be nullable', $type));
throw new \LogicException('mixed type cannot be nullable');
} }
return $nullable ? new NullableType($type) : $type; return $nullable ? new NullableType($type) : $type;
@@ -215,21 +220,33 @@ final class BuilderHelpers
public static function normalizeValue($value) : Expr { public static function normalizeValue($value) : Expr {
if ($value instanceof Node\Expr) { if ($value instanceof Node\Expr) {
return $value; return $value;
} elseif (is_null($value)) { }
if (is_null($value)) {
return new Expr\ConstFetch( return new Expr\ConstFetch(
new Name('null') new Name('null')
); );
} elseif (is_bool($value)) { }
if (is_bool($value)) {
return new Expr\ConstFetch( return new Expr\ConstFetch(
new Name($value ? 'true' : 'false') new Name($value ? 'true' : 'false')
); );
} elseif (is_int($value)) { }
if (is_int($value)) {
return new Scalar\LNumber($value); return new Scalar\LNumber($value);
} elseif (is_float($value)) { }
if (is_float($value)) {
return new Scalar\DNumber($value); return new Scalar\DNumber($value);
} elseif (is_string($value)) { }
if (is_string($value)) {
return new Scalar\String_($value); return new Scalar\String_($value);
} elseif (is_array($value)) { }
if (is_array($value)) {
$items = []; $items = [];
$lastKey = -1; $lastKey = -1;
foreach ($value as $itemKey => $itemValue) { foreach ($value as $itemKey => $itemValue) {
@@ -248,9 +265,9 @@ final class BuilderHelpers
} }
return new Expr\Array_($items); 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 { public static function normalizeDocComment($docComment) : Comment\Doc {
if ($docComment instanceof Comment\Doc) { if ($docComment instanceof Comment\Doc) {
return $docComment; 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]);
} }
/** /**

View File

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

View File

@@ -85,7 +85,6 @@ class Lexer
$scream = ini_set('xdebug.scream', '0'); $scream = ini_set('xdebug.scream', '0');
error_clear_last();
$this->tokens = @token_get_all($code); $this->tokens = @token_get_all($code);
$this->postprocessTokens($errorHandler); $this->postprocessTokens($errorHandler);
@@ -135,10 +134,11 @@ class Lexer
// detected by finding "gaps" in the token array. Unterminated comments are detected // detected by finding "gaps" in the token array. Unterminated comments are detected
// by checking if a trailing comment has a "*/" at the end. // 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 // Additionally, we perform a number of canonicalizations here:
// the trailing whitespace anymore. // * Use the PHP 8.0 comment format, which does not include trailing whitespace anymore.
// // * Use PHP 8.0 T_NAME_* tokens.
// We also canonicalize to the PHP 8 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; $filePos = 0;
$line = 1; $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]; $tokenValue = \is_string($token) ? $token : $token[1];
$tokenLen = \strlen($tokenValue); $tokenLen = \strlen($tokenValue);
@@ -323,7 +339,8 @@ class Lexer
$value = $token[1]; $value = $token[1];
$id = $this->tokenMap[$token[0]]; $id = $this->tokenMap[$token[0]];
if (\T_CLOSE_TAG === $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]) { } elseif (\T_INLINE_HTML === $token[0]) {
$startAttributes['hasLeadingNewline'] = $this->prevCloseTagHasNewline; $startAttributes['hasLeadingNewline'] = $this->prevCloseTagHasNewline;
} }
@@ -422,6 +439,11 @@ class Lexer
'T_MATCH', 'T_MATCH',
'T_NULLSAFE_OBJECT_OPERATOR', 'T_NULLSAFE_OBJECT_OPERATOR',
'T_ATTRIBUTE', '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 // PHP-Parser might be used together with another library that also emulates some or all
@@ -439,7 +461,7 @@ class Lexer
$token, $clashingToken $token, $clashingToken
)); ));
} }
$usedTokenIds[$token] = $tokenId; $usedTokenIds[$tokenId] = $token;
} }
} }
@@ -512,6 +534,10 @@ class Lexer
$tokenMap[\T_MATCH] = Tokens::T_MATCH; $tokenMap[\T_MATCH] = Tokens::T_MATCH;
$tokenMap[\T_NULLSAFE_OBJECT_OPERATOR] = Tokens::T_NULLSAFE_OBJECT_OPERATOR; $tokenMap[\T_NULLSAFE_OBJECT_OPERATOR] = Tokens::T_NULLSAFE_OBJECT_OPERATOR;
$tokenMap[\T_ATTRIBUTE] = Tokens::T_ATTRIBUTE; $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; return $tokenMap;
} }
@@ -520,7 +546,7 @@ class Lexer
// Based on semi_reserved production. // Based on semi_reserved production.
return array_fill_keys([ return array_fill_keys([
\T_STRING, \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_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_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, \T_ENDWHILE, \T_FOR, \T_ENDFOR, \T_FOREACH, \T_ENDFOREACH, \T_DECLARE, \T_ENDDECLARE, \T_AS, \T_TRY, \T_CATCH,

View File

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

View File

@@ -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;
}
}

View File

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

View File

@@ -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;
}
}

View File

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

View File

@@ -65,6 +65,15 @@ class ClassConst extends Node\Stmt
return (bool) ($this->flags & Class_::MODIFIER_PRIVATE); 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 { public function getType() : string {
return 'Stmt_ClassConst'; return 'Stmt_ClassConst';
} }

View File

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

View File

@@ -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';
}
}

View File

@@ -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';
}
}

View File

@@ -77,6 +77,15 @@ class Property extends Node\Stmt
return (bool) ($this->flags & Class_::MODIFIER_STATIC); 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 { public function getType() : string {
return 'Stmt_Property'; return 'Stmt_Property';
} }

View File

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

View File

@@ -86,6 +86,15 @@ class NameResolver extends NodeVisitorAbstract
$this->resolveAttrGroups($node); $this->resolveAttrGroups($node);
$this->addNamespacedName($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_) { } elseif ($node instanceof Stmt\Trait_) {
$this->resolveAttrGroups($node); $this->resolveAttrGroups($node);
$this->addNamespacedName($node); $this->addNamespacedName($node);
@@ -110,6 +119,8 @@ class NameResolver extends NodeVisitorAbstract
} }
} else if ($node instanceof Stmt\ClassConst) { } else if ($node instanceof Stmt\ClassConst) {
$this->resolveAttrGroups($node); $this->resolveAttrGroups($node);
} else if ($node instanceof Stmt\EnumCase) {
$this->resolveAttrGroups($node);
} elseif ($node instanceof Expr\StaticCall } elseif ($node instanceof Expr\StaticCall
|| $node instanceof Expr\StaticPropertyFetch || $node instanceof Expr\StaticPropertyFetch
|| $node instanceof Expr\ClassConstFetch || $node instanceof Expr\ClassConstFetch

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -66,7 +66,7 @@ abstract class PrettyPrinterAbstract
BinaryOp\BooleanAnd::class => [120, -1], BinaryOp\BooleanAnd::class => [120, -1],
BinaryOp\BooleanOr::class => [130, -1], BinaryOp\BooleanOr::class => [130, -1],
BinaryOp\Coalesce::class => [140, 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 // parser uses %left for assignments, but they really behave as %right
Expr\Assign::class => [160, 1], Expr\Assign::class => [160, 1],
Expr\AssignRef::class => [160, 1], Expr\AssignRef::class => [160, 1],
@@ -756,7 +756,7 @@ abstract class PrettyPrinterAbstract
$itemStartPos = $origArrItem->getStartTokenPos(); $itemStartPos = $origArrItem->getStartTokenPos();
$itemEndPos = $origArrItem->getEndTokenPos(); $itemEndPos = $origArrItem->getEndTokenPos();
\assert($itemStartPos >= 0 && $itemEndPos >= 0); \assert($itemStartPos >= 0 && $itemEndPos >= 0 && $itemStartPos >= $pos);
$origIndentLevel = $this->indentLevel; $origIndentLevel = $this->indentLevel;
$lastElemIndentLevel = $this->origTokens->getIndentationBefore($itemStartPos) + $indentAdjustment; $lastElemIndentLevel = $this->origTokens->getIndentationBefore($itemStartPos) + $indentAdjustment;
@@ -767,16 +767,25 @@ abstract class PrettyPrinterAbstract
$commentStartPos = $origComments ? $origComments[0]->getStartTokenPos() : $itemStartPos; $commentStartPos = $origComments ? $origComments[0]->getStartTokenPos() : $itemStartPos;
\assert($commentStartPos >= 0); \assert($commentStartPos >= 0);
$commentsChanged = $comments !== $origComments; if ($commentStartPos < $pos) {
if ($commentsChanged) { // Comments may be assigned to multiple nodes if they start at the same position.
// Remove old comments // Make sure we don't try to print them multiple times.
$itemStartPos = $commentStartPos; $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)) { if (!empty($delayedAdd)) {
$result .= $this->origTokens->getTokenCode(
$pos, $commentStartPos, $indentAdjustment);
/** @var Node $delayedAddNode */ /** @var Node $delayedAddNode */
foreach ($delayedAdd as $delayedAddNode) { foreach ($delayedAdd as $delayedAddNode) {
if ($insertNewline) { if ($insertNewline) {
@@ -795,25 +804,16 @@ abstract class PrettyPrinterAbstract
} }
} }
$result .= $this->origTokens->getTokenCode(
$commentStartPos, $itemStartPos, $indentAdjustment);
$delayedAdd = []; $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) { if ($comments !== $origComments) {
// Add new comments if ($comments) {
$result .= $this->pComments($comments) . $this->nl; $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. // 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_PRIVATE ? 'private ' : '')
. ($modifiers & Stmt\Class_::MODIFIER_STATIC ? 'static ' : '') . ($modifiers & Stmt\Class_::MODIFIER_STATIC ? 'static ' : '')
. ($modifiers & Stmt\Class_::MODIFIER_ABSTRACT ? 'abstract ' : '') . ($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++) { for ($i = 0; $i < 256; $i++) {
// Since PHP 7.1 The lower range is 0x80. However, we also want to support code for // Since PHP 7.1 The lower range is 0x80. However, we also want to support code for
// older versions. // 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. * 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. * certain node is replaced by null.
*/ */
protected function initializeRemovalMap() { protected function initializeRemovalMap() {
@@ -1269,6 +1271,8 @@ abstract class PrettyPrinterAbstract
'Stmt_Catch->var' => $stripLeft, 'Stmt_Catch->var' => $stripLeft,
'Stmt_ClassMethod->returnType' => $stripColon, 'Stmt_ClassMethod->returnType' => $stripColon,
'Stmt_Class->extends' => ['left' => \T_EXTENDS], 'Stmt_Class->extends' => ['left' => \T_EXTENDS],
'Stmt_Enum->scalarType' => $stripColon,
'Stmt_EnumCase->expr' => $stripEquals,
'Expr_PrintableNewAnonClass->extends' => ['left' => \T_EXTENDS], 'Expr_PrintableNewAnonClass->extends' => ['left' => \T_EXTENDS],
'Stmt_Continue->num' => $stripBoth, 'Stmt_Continue->num' => $stripBoth,
'Stmt_Foreach->keyVar' => $stripDoubleArrow, 'Stmt_Foreach->keyVar' => $stripDoubleArrow,
@@ -1307,6 +1311,8 @@ abstract class PrettyPrinterAbstract
'Stmt_Catch->var' => [null, false, ' ', null], 'Stmt_Catch->var' => [null, false, ' ', null],
'Stmt_ClassMethod->returnType' => [')', false, ' : ', null], 'Stmt_ClassMethod->returnType' => [')', false, ' : ', null],
'Stmt_Class->extends' => [null, false, ' extends ', 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], 'Expr_PrintableNewAnonClass->extends' => [null, ' extends ', null],
'Stmt_Continue->num' => [\T_CONTINUE, false, ' ', null], 'Stmt_Continue->num' => [\T_CONTINUE, false, ' ', null],
'Stmt_Foreach->keyVar' => [\T_AS, false, null, ' => '], 'Stmt_Foreach->keyVar' => [\T_AS, false, null, ' => '],
@@ -1356,6 +1362,7 @@ abstract class PrettyPrinterAbstract
'Stmt_ClassConst->consts' => ', ', 'Stmt_ClassConst->consts' => ', ',
'Stmt_ClassMethod->params' => ', ', 'Stmt_ClassMethod->params' => ', ',
'Stmt_Class->implements' => ', ', 'Stmt_Class->implements' => ', ',
'Stmt_Enum->implements' => ', ',
'Expr_PrintableNewAnonClass->implements' => ', ', 'Expr_PrintableNewAnonClass->implements' => ', ',
'Stmt_Const->consts' => ', ', 'Stmt_Const->consts' => ', ',
'Stmt_Declare->declares' => ', ', 'Stmt_Declare->declares' => ', ',
@@ -1382,6 +1389,7 @@ abstract class PrettyPrinterAbstract
'Stmt_Case->stmts' => "\n", 'Stmt_Case->stmts' => "\n",
'Stmt_Catch->stmts' => "\n", 'Stmt_Catch->stmts' => "\n",
'Stmt_Class->stmts' => "\n", 'Stmt_Class->stmts' => "\n",
'Stmt_Enum->stmts' => "\n",
'Expr_PrintableNewAnonClass->stmts' => "\n", 'Expr_PrintableNewAnonClass->stmts' => "\n",
'Stmt_Interface->stmts' => "\n", 'Stmt_Interface->stmts' => "\n",
'Stmt_Trait->stmts' => "\n", 'Stmt_Trait->stmts' => "\n",
@@ -1397,6 +1405,8 @@ abstract class PrettyPrinterAbstract
'Stmt_If->stmts' => "\n", 'Stmt_If->stmts' => "\n",
'Stmt_Namespace->stmts' => "\n", 'Stmt_Namespace->stmts' => "\n",
'Stmt_Class->attrGroups' => "\n", 'Stmt_Class->attrGroups' => "\n",
'Stmt_Enum->attrGroups' => "\n",
'Stmt_EnumCase->attrGroups' => "\n",
'Stmt_Interface->attrGroups' => "\n", 'Stmt_Interface->attrGroups' => "\n",
'Stmt_Trait->attrGroups' => "\n", 'Stmt_Trait->attrGroups' => "\n",
'Stmt_Function->attrGroups' => "\n", 'Stmt_Function->attrGroups' => "\n",
@@ -1435,6 +1445,7 @@ abstract class PrettyPrinterAbstract
'Expr_PrintableNewAnonClass->implements' => [null, ' implements ', ''], 'Expr_PrintableNewAnonClass->implements' => [null, ' implements ', ''],
'Expr_StaticCall->args' => ['(', '', ''], 'Expr_StaticCall->args' => ['(', '', ''],
'Stmt_Class->implements' => [null, ' implements ', ''], 'Stmt_Class->implements' => [null, ' implements ', ''],
'Stmt_Enum->implements' => [null, ' implements ', ''],
'Stmt_ClassMethod->params' => ['(', '', ''], 'Stmt_ClassMethod->params' => ['(', '', ''],
'Stmt_Interface->extends' => [null, ' extends ', ''], 'Stmt_Interface->extends' => [null, ' extends ', ''],
'Stmt_Function->params' => ['(', '', ''], 'Stmt_Function->params' => ['(', '', ''],

View File

@@ -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
]
];
}
}

View File

@@ -4,7 +4,12 @@ namespace PhpParser\Builder;
use PhpParser\Comment; use PhpParser\Comment;
use PhpParser\Node; 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\Name;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Stmt; use PhpParser\Node\Stmt;
class ClassTest extends \PHPUnit\Framework\TestCase 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() { public function testInvalidStmtError() {
$this->expectException(\LogicException::class); $this->expectException(\LogicException::class);
$this->expectExceptionMessage('Unexpected node of type "Stmt_Echo"'); $this->expectExceptionMessage('Unexpected node of type "Stmt_Echo"');

View File

@@ -4,8 +4,14 @@ namespace PhpParser\Builder;
use PhpParser\Comment; use PhpParser\Comment;
use PhpParser\Node; use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Expr\Print_; use PhpParser\Node\Expr\Print_;
use PhpParser\Node\Expr\Variable; 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\Scalar\String_;
use PhpParser\Node\Stmt; use PhpParser\Node\Stmt;
@@ -81,6 +87,22 @@ class FunctionTest extends \PHPUnit\Framework\TestCase
]), $node); ]), $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() { public function testReturnType() {
$node = $this->createFunctionBuilder('test') $node = $this->createFunctionBuilder('test')
->setReturnType('void') ->setReturnType('void')

View File

@@ -4,7 +4,13 @@ namespace PhpParser\Builder;
use PhpParser\Comment; use PhpParser\Comment;
use PhpParser\Node; 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\DNumber;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Stmt; use PhpParser\Node\Stmt;
class InterfaceTest extends \PHPUnit\Framework\TestCase class InterfaceTest extends \PHPUnit\Framework\TestCase
@@ -76,6 +82,22 @@ class InterfaceTest extends \PHPUnit\Framework\TestCase
]), $node); ]), $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() { public function testInvalidStmtError() {
$this->expectException(\LogicException::class); $this->expectException(\LogicException::class);
$this->expectExceptionMessage('Unexpected node of type "Stmt_PropertyProperty"'); $this->expectExceptionMessage('Unexpected node of type "Stmt_PropertyProperty"');

View File

@@ -4,8 +4,14 @@ namespace PhpParser\Builder;
use PhpParser\Comment; use PhpParser\Comment;
use PhpParser\Node; use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Expr\Print_; use PhpParser\Node\Expr\Print_;
use PhpParser\Node\Expr\Variable; 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\Scalar\String_;
use PhpParser\Node\Stmt; use PhpParser\Node\Stmt;
@@ -125,6 +131,22 @@ class MethodTest extends \PHPUnit\Framework\TestCase
]), $node); ]), $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() { public function testReturnType() {
$node = $this->createMethodBuilder('test') $node = $this->createMethodBuilder('test')
->setReturnType('bool') ->setReturnType('bool')

View File

@@ -3,8 +3,14 @@
namespace PhpParser\Builder; namespace PhpParser\Builder;
use PhpParser\Node; use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Expr; use PhpParser\Node\Expr;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar; use PhpParser\Node\Scalar;
use PhpParser\Node\Scalar\LNumber;
class ParamTest extends \PHPUnit\Framework\TestCase class ParamTest extends \PHPUnit\Framework\TestCase
{ {
@@ -198,4 +204,21 @@ class ParamTest extends \PHPUnit\Framework\TestCase
$node $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
);
}
} }

View File

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

View File

@@ -3,7 +3,12 @@
namespace PhpParser\Builder; namespace PhpParser\Builder;
use PhpParser\Comment; 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\Name;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Stmt; use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassConst; use PhpParser\Node\Stmt\ClassConst;
@@ -88,4 +93,27 @@ class TraitTest extends \PHPUnit\Framework\TestCase
$this->assertSame($properties, $trait->getProperties()); $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
);
}
} }

View File

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

View File

@@ -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');
}
}

View File

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

View File

@@ -24,6 +24,17 @@ class EmulativeTest extends LexerTest
$this->assertSame(0, $lexer->getNextToken()); $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 * @dataProvider provideTestReplaceKeywords
*/ */
@@ -302,6 +313,35 @@ class EmulativeTest extends LexerTest
[Tokens::T_END_HEREDOC, " LABEL"], [Tokens::T_END_HEREDOC, " LABEL"],
[ord(','), ','], [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 [ return [
['8.0', 'match', [[Tokens::T_MATCH, 'match']]], ['8.0', 'match', [[Tokens::T_MATCH, 'match']]],
['7.4', 'match', [[Tokens::T_STRING, '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.4', 'FN', [[Tokens::T_FN, 'FN']]],
['7.3', 'fn', [[Tokens::T_STRING, 'fn']]], ['7.3', 'fn', [[Tokens::T_STRING, 'fn']]],
['7.3', 'FN', [[Tokens::T_STRING, 'FN']]],
// Tested here to skip testLeaveStuffAloneInStrings. // Tested here to skip testLeaveStuffAloneInStrings.
['8.0', '"$foo?->bar"', [ ['8.0', '"$foo?->bar"', [
[ord('"'), '"'], [ord('"'), '"'],

View File

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

View File

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

View File

@@ -334,6 +334,133 @@ PHP;
} }
] ]
JSON; 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()); $parser = new Parser\Php7(new Lexer());
$stmts = $parser->parse(canonicalize($code)); $stmts = $parser->parse(canonicalize($code));

View File

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

View File

@@ -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
{}

View File

@@ -316,4 +316,23 @@ $stmts[0]->returnType->types[] = new Node\Name('C');
----- -----
<?php <?php
function test(): A 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;
}

View File

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

View File

@@ -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 3:18 to 3:18
Syntax error, unexpected ')', expecting T_VARIABLE from 7:31 to 7:31 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 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 19:17 to 19:17
Syntax error, unexpected ')', expecting T_VARIABLE from 22:21 to 22:21 Syntax error, unexpected ')', expecting T_VARIABLE from 22:21 to 22:21
Syntax error, unexpected ')', expecting T_VARIABLE from 25:13 to 25:13 Syntax error, unexpected ')', expecting T_VARIABLE from 25:13 to 25:13
@@ -1137,7 +1137,7 @@ array(
) )
flags: 0 flags: 0
type: null type: null
byRef: true byRef: false
variadic: false variadic: false
var: Expr_Error( var: Expr_Error(
) )
@@ -1401,4 +1401,124 @@ array(
$a = ["a "thing"]; $a = ["a "thing"];
----- -----
!!php7 !!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
)
)
)
)
)
)
)

View File

@@ -7,6 +7,9 @@ static fn(&$x) => $x;
fn&($x) => $x; fn&($x) => $x;
fn($x, ...$rest) => $rest; fn($x, ...$rest) => $rest;
fn(): int => $x; fn(): int => $x;
fn($a, $b) => $a and $b;
fn($a, $b) => $a && $b;
----- -----
!!php7 !!php7
array( 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
)
)
)
)
) )

View File

@@ -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
)
)
)
)
)

View File

@@ -2,26 +2,28 @@ Class position
----- -----
<?php <?php
class A { if (1);
}
try { class C {}
} catch (Exception $e) {
}
class B {
}
?>
----- -----
!!positions !!positions
array( 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( attrGroups: array(
) )
flags: 0 flags: 0
name: Identifier[3:7 - 3:7]( name: Identifier[5:7 - 5:7](
name: A name: C
) )
extends: null extends: null
implements: array( implements: array(
@@ -29,36 +31,62 @@ array(
stmts: 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( stmts: array(
) )
catches: array( elseifs: 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(
)
)
) )
finally: null else: null
) )
2: Stmt_Class[10:1 - 11:1]( 1: Stmt_Trait[5:1 - 5:10](
attrGroups: array( attrGroups: array(
) )
flags: 0 name: Identifier[5:7 - 5:7](
name: Identifier[10:7 - 10:7]( name: X
name: B
) )
extends: null stmts: array(
implements: 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( stmts: array(
) )

View File

@@ -78,11 +78,11 @@ array(
----- -----
<?php <?php
class A { class A {
final const X = 1; readonly const X = 1;
} }
----- -----
!!php7 !!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( array(
0: Stmt_Class( 0: Stmt_Class(
attrGroups: array( attrGroups: array(
@@ -98,7 +98,7 @@ array(
0: Stmt_ClassConst( 0: Stmt_ClassConst(
attrGroups: array( attrGroups: array(
) )
flags: MODIFIER_FINAL (32) flags: MODIFIER_READONLY (64)
consts: array( consts: array(
0: Const( 0: Const(
name: Identifier( name: Identifier(
@@ -150,4 +150,4 @@ array(
) )
) )
) )
) )

View File

@@ -7,6 +7,7 @@ class Foo {
public const B = 2; public const B = 2;
protected const C = 3; protected const C = 3;
private const D = 4; private const D = 4;
final const E = 5;
} }
----- -----
!!php7 !!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
)
)
)
)
) )
) )
) )

View File

@@ -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(
)
)
)

View File

@@ -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
)
)
)
)
)

View File

@@ -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(); } <?php class A { abstract abstract function a(); }
----- -----
Multiple abstract modifiers are not allowed from 1:26 to 1:33 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; } <?php class A { abstract $a; }
----- -----
Properties cannot be declared abstract from 1:17 to 1:24 Properties cannot be declared abstract from 1:17 to 1:24

View File

@@ -6,6 +6,7 @@ class A {
public string $a; public string $a;
protected static D $b; protected static D $b;
private ?float $c; private ?float $c;
readonly static public ?int $d;
} }
----- -----
!!php7 !!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
)
)
)
) )
) )
) )

View File

@@ -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(
)
)
)
)
)

View File

@@ -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(
)
)
)

View File

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

View File

@@ -59,7 +59,7 @@ $a = $b = $c = $d = $f && true;
($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 = ($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 : ($e ? $f : $g));
$a ? $b ? $c : $d : $f; $a ? $b ? $c : $d : $f;
$a ?? $b ?? $c; $a ?? $b ?? $c;

View File

@@ -4,20 +4,30 @@ Escape sequences in double-quoted strings
"\n\r\t\f\v\$\"\\"; "\n\r\t\f\v\$\"\\";
"@@{ implode(range("\0", "\37")) }@@"; "@@{ implode(range("\0", "\37")) }@@";
"\0000\0001"; "\0000\0001";
"äöü";
"\xc0\x80";
"\xd0\x01";
"\xf0\x80\x80";
<<<DOC <<<DOC
\n\r\t\f\v\$\"\\ \n\r\t\f\v\$\"\\
@@{ implode(range("\0", "\37")) }@@ @@{ implode(range("\0", "\37")) }@@
\0000\0001 \0000\0001
äöü
DOC; DOC;
----- -----
"\n\r\t\f\v\$\"\\"; "\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"; "\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";
"\0000\0001"; "\x000\x001";
"äöü";
"\xc0\x80";
"\xd0\x01";
"\xf0\x80\x80";
<<<DOC <<<DOC
@@{ "\n\r" }@@\t\f\v\$\\"\\ @@{ "\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 \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
\0000\0001 \x000\x001
äöü
DOC DOC
; ;

View File

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

View File

@@ -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';
}

View File

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

View File

@@ -2,4 +2,4 @@ VERSION=$1
wget -q https://github.com/php/php-src/archive/php-$VERSION.tar.gz wget -q https://github.com/php/php-src/archive/php-$VERSION.tar.gz
mkdir -p ./data/php-src mkdir -p ./data/php-src
tar -xzf ./php-$VERSION.tar.gz -C ./data/php-src --strip-components=1 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