Compare commits

...

88 Commits

Author SHA1 Message Date
210577fe3c Release PHP-Parser 4.13.2 2021-11-30 20:35:32 +01:00
f09f22760e Declare namespacedName property
For historical reasons, this property is used by the NameResolver
(in default mode). Declare it explicitly, rather than using a doc
comment.
2021-11-27 21:02:58 +01:00
d4cb98ae38 Fix typo in property name
This test was working on a dynamic "subNode" property, rather
than an actual subnode (either subNode1 or subNode2).

This test is generally not very valuable, I think it dates back
to a time where __get()/__set() were used.
2021-11-27 20:57:41 +01:00
68d2a52b42 Avoid creation of dynamic property in test
This test requires a property that is not a subnode -- but it does
not need to be dynamic, a declared property works just as well.
2021-11-27 20:53:55 +01:00
63f8699143 Add CallLike test for NullsafeMethodCall 2021-11-14 17:44:47 +01:00
4122ff3a91 Make NullsafeMethodCall extend from CallLike 2021-11-14 17:43:29 +01:00
6f1f206862 Fix minor typo 2021-11-13 22:39:16 +01:00
99a24b6a55 Added builders for enum and enum case 2021-11-05 22:26:04 +01:00
63a79e8daa Release PHP-Parser 4.13.1 2021-11-03 21:52:16 +01:00
6a21234e58 Code highlighting 2021-10-31 09:11:00 +01:00
7064539974 Rename identifier/identifier_ex productions
The names were easy to get wrong, because the corresponding names
in zend_language_parser.y are T_STRING/identifier, so just copying
identifier from upstream gives the wrong behavior.
2021-10-17 20:26:06 +02:00
4bfc4595ed Support reserved keywords as enum cases
Fixes #807.
2021-10-17 20:20:35 +02:00
54f19a0a66 Fixed array value evaluation with unpacked array 2021-10-11 21:35:21 +02:00
8da6d7ac62 Fixed ArrowFunction::$expr 2021-10-09 11:26:27 +02:00
f6e1fbf3a2 Update .gitattributes 2021-10-08 21:23:55 +02:00
50953a2691 Release PHP-Parser 4.13.0 2021-09-20 14:20:58 +02:00
0a20979a62 Unified builder methods for setting types 2021-09-14 18:09:07 +02:00
a45fb2a621 Add CallLike parent class
This provides a helper to determine whether a call is a first-class
callable, and a way to strip the args type to Arg[] if it isn't.
2021-09-12 22:29:08 +02:00
08501991d4 Don't make VariadicPlaceholder an expression
And don't store it in an Arg.
2021-09-12 21:59:26 +02:00
b5234eacd0 Tweak coding style 2021-09-12 21:54:03 +02:00
632ead3a82 Print comma before comments for new array item (#805)
Print comma before rather than after comments. Also switch to multiline mode if inserting an item with comments.

Fixes #804.
2021-09-12 21:51:25 +02:00
13549aa794 Add support for first-class callables
I'm somewhat unsure about the AST structure here.
VariadicPlaceholder is not a general expression. Maybe Arg->expr
should be Expr|VariadicPlaceholder? Or possibly the call arguments
should be an array of Arg|VariadicPlaceholder?
2021-09-03 17:18:40 +02:00
d2c645f163 Adjust token count in octal emulator 2021-09-03 16:35:10 +02:00
def24f2224 Add support for explicit octal literals 2021-09-03 16:29:30 +02:00
cfeb195205 Add test for new in initializer
This already works because we don't validate initializer contents,
everything is accepted from a parser perspective.
2021-09-03 15:10:29 +02:00
ace6c67a8a Add support for intersection types 2021-09-03 15:06:33 +02:00
0483391aca Introduce ComplexType base class
With the upcoming addition of intersection types, a type can
be Identifier|Name|NullableType|UnionType|IntersectionType, which
is quite the mouthful. Give NullableType and UnionType a common
base class ComplexType, which does not have any behavior, but
allows to write these types (and check them in instanceof) more
easily.
2021-09-02 18:35:05 +02:00
9aebf377fc Allow multiple modifiers for property promotion
Fixes issue #800.
2021-08-08 19:12:44 +02:00
5a43015499 Simplify BuilderHelpers::normalizeName() implementation
In order to get rid of the flag in `BuilderHelpers::normalizeNameCommon()` I have moved all the logic related to the normalization of the name to the `BuilderHelpers::normalizeName()` method and expr-related stuff to the `BuilderHelpers::normalizeNameOrExpr()` method which later calls the basic `normalizeName()` as well
2021-07-21 12:51:18 +02:00
6608f01670 Release PHP-Parser 4.12.0 2021-07-21 12:44:31 +02:00
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
c4304c76bd Try to pass --ignore-platform-req=php on 8.1 only 2021-07-21 12:11:59 +02:00
b099e8fc76 Pass --ignore-platform-req=php 2021-07-21 12:08:11 +02:00
acf16edc8e Add PHP 8.1 to GH actions 2021-07-21 11:58:38 +02:00
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
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
feed91cf0f Avoid ctype_alnum() on integer
This is deprecated in PHP 8.1
2021-07-09 15:46:50 +02:00
fe14cf3672 Release PHP-Parser 4.11.0 2021-07-03 15:36:55 +02:00
3fb73520c1 Add handling for Enum(Case)s in NameResolver 2021-07-03 15:09:11 +02:00
c35cc4b2cb Add support for "never" type in the BuilderHelpers::normalizeType() 2021-06-18 17:25:28 +02:00
e69ebbbfd9 chore: use the word Xdebug instead of XDebug 2021-06-17 18:00:12 +02:00
0b258d9a9e Add missing tests for methods of BuilderHelpers 2021-06-17 17:55:59 +02:00
2d193bb0e4 Add attributes to integration Builder test 2021-06-17 09:18:53 +02:00
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
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
b67560d388 Update CHANGELOG.md
very minor for the year.
2021-05-04 22:15:00 +02:00
4432ba399e Release PHP-Parser 4.10.5 2021-05-03 21:11:20 +02:00
37ac4ea9c2 Document that namespaced names containing whitespace are not supported 2021-04-25 22:47:15 +02:00
4848a0d734 Check for \r as newline in closing tag
Fixes #761.
2021-04-25 22:42:18 +02:00
8eb194ea1f Add never type
This should be recognized as Identifier instead of Name now.
2021-04-25 22:35:28 +02:00
e03d63cffb Fix precedence of arrow functions
Arrow functions should have lowest precedence.

Fixes #769.
2021-04-25 22:19:49 +02:00
ce91d139b5 Make sure match is one character long 2021-04-25 21:47:07 +02:00
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
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
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
f767b9fd9f ParserAbstract: add missing '*' to the phpdoc
Otherwise, it's somewhat incompatible with the phpdoc definition.
2021-04-25 12:38:52 +02:00
2a4bb7ef2c Moved phpy pseudo lang functions to separate file 2021-04-10 13:40:52 +02:00
4abdcde5f1 Fix missing argument in documentation (#767)
Missing argument in the getMessageWithColumnInfo call
2021-03-25 18:37:06 +01:00
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
38aa0920c9 Add test for UTF-8 in string
These should not get escaped.
2021-03-21 17:03:15 +01:00
a8223f228a Add emulation for enum keyword 2021-03-20 17:49:44 +01:00
8165cf69fa CS fix 2021-02-02 09:57:47 +01:00
46221a0914 Update Use_ builder to return specific type 2021-01-06 14:30:19 +01:00
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
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
c6d052fc58 Release PHP-Parser 4.10.4 2020-12-20 11:01:03 +01:00
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
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
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
893a5bce3f Fix #741 incorrect varvar positions 2020-12-19 22:03:43 +01:00
bec74aceda Remove .travis.yml 2020-12-08 23:22:57 +01:00
7c09e096c9 Try to re-enable coveralls 2020-12-08 23:17:41 +01:00
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
5e36ef732e Add GitHub Actions workflow (#740) 2020-12-08 23:00:10 +01:00
dbe56d23de Release PHP-Parser 4.10.3 2020-12-03 18:45:45 +01:00
c64986fa55 Allow both '{' and T_CURLY_OPEN as curly bracket (#732) 2020-12-03 18:42:00 +01:00
bc7a9bf9c2 Remove useless code left from old implementation 2020-12-02 12:54:15 +01:00
2816485126 Updated README to indicate that parsing PHP 8.0 code is supported 2020-12-01 15:15:26 +01:00
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
d520bc9e1d Don't allow failures on PHP 8.0 integration tests
These are passing now.
2020-09-30 21:15:05 +02:00
51e0b30843 Test against 8.0.0rc1
This should fix the remaining test failures.
2020-09-30 20:51:01 +02:00
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
658f1be311 Release PHP-Parser 4.10.2 2020-09-26 12:30:38 +02:00
b9b65a2996 Fix check for token emulation conflicts 2020-09-26 12:29:16 +02:00
1b479e7592 Release PHP-Parser 4.10.1 2020-09-23 20:23:49 +02:00
b5351f883a Make keyword emulation check case-insensitive 2020-09-23 20:19:40 +02:00
88be6127fa FPPP: Fix remove + add at start of list 2020-09-22 22:41:02 +02:00
8a97fa157f Recover from missing semicolon after property or class const
Fixes #712.
2020-09-19 23:11:36 +02:00
128 changed files with 7676 additions and 3758 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

17
.gitattributes vendored
View File

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

90
.github/workflows/main.yml vendored Normal file
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,141 @@
Version 4.10.1-dev
Version 4.13.3-dev
------------------
Nothing yet.
Version 4.13.2 (2021-11-30)
---------------------------
### Added
* Added builders for enums and enum cases.
### Fixed
* NullsafeMethodCall now extends from CallLike.
* The `namespacedName` property populated by the `NameResolver` is now declared on relevant nodes,
to avoid a dynamic property deprecation warning with PHP 8.2.
Version 4.13.1 (2021-11-03)
---------------------------
### Fixed
* Support reserved keywords as enum cases.
* Support array unpacking in constant expression evaluator.
Version 4.13.0 (2021-09-20)
---------------------------
### Added
* [PHP 8.1] Added support for intersection types using a new `IntersectionType` node. Additionally
a `ComplexType` parent class for `NullableType`, `UnionType` and `IntersectionType` has been
added.
* [PHP 8.1] Added support for explicit octal literals.
* [PHP 8.1] Added support for first-class callables. These are represented using a call whose first
argument is a `VariadicPlaceholder`. The representation is intended to be forward-compatible with
partial function application, just like the PHP feature itself. Call nodes now extend from
`Expr\CallLike`, which provides an `isFirstClassCallable()` method to determine whether a
placeholder id present. `getArgs()` can be used to assert that the call is not a first-class
callable and returns `Arg[]` rather than `array<Arg|VariadicPlaceholder>`.
### Fixed
* Multiple modifiers for promoted properties are now accepted. In particular this allows something
like `public readonly` for promoted properties.
* Formatting-preserving pretty printing for comments in array literals has been fixed.
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)
---------------------------

View File

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

View File

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

View File

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

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
preferable to disable XDebug completely, as it can easily make this library more than five times
preferable to disable Xdebug completely, as it can easily make this library more than five times
slower.
Parsing

View File

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

View File

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

View File

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

View File

@ -193,7 +193,7 @@ anonymous classes), you know that once you've seen a class declaration, there is
checking all it's child nodes, because PHP does not allow nesting classes. In this case, you can
instruct the traverser to not recurse into the class node:
```
```php
private $classes = [];
public function enterNode(Node $node) {
if ($node instanceof Node\Stmt\Class_) {
@ -211,7 +211,7 @@ after finding it. For example, if you are looking for the node of a class with a
discounting exotic cases like conditionally defining a class two times), you can stop traversal
once you found it:
```
```php
private $class = null;
public function enterNode(Node $node) {
if ($node instanceof Node\Stmt\Class_ &&
@ -251,7 +251,7 @@ Stmt_Return(
the following method calls will be performed:
```
```php
$visitorA->enterNode(Stmt_Return)
$visitorB->enterNode(Stmt_Return)
$visitorA->enterNode(Expr_Variable)

View File

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

View File

@ -20,6 +20,11 @@ top_statement_list:
if ($nop !== null) { $1[] = $nop; } $$ = $1; }
;
ampersand:
T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG
| T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG
;
reserved_non_modifiers:
T_INCLUDE | T_INCLUDE_ONCE | T_EVAL | T_REQUIRE | T_REQUIRE_ONCE | T_LOGICAL_OR | T_LOGICAL_XOR | T_LOGICAL_AND
| T_INSTANCEOF | T_NEW | T_CLONE | T_EXIT | T_IF | T_ELSEIF | T_ELSE | T_ENDIF | T_ECHO | T_DO | T_WHILE
@ -28,20 +33,20 @@ reserved_non_modifiers:
| T_FUNCTION | T_CONST | T_RETURN | T_PRINT | T_YIELD | T_LIST | T_SWITCH | T_ENDSWITCH | T_CASE | T_DEFAULT
| T_BREAK | T_ARRAY | T_CALLABLE | T_EXTENDS | T_IMPLEMENTS | T_NAMESPACE | T_TRAIT | T_INTERFACE | T_CLASS
| T_CLASS_C | T_TRAIT_C | T_FUNC_C | T_METHOD_C | T_LINE | T_FILE | T_DIR | T_NS_C | T_HALT_COMPILER | T_FN
| T_MATCH
| T_MATCH | T_ENUM
;
semi_reserved:
reserved_non_modifiers
| T_STATIC | T_ABSTRACT | T_FINAL | T_PRIVATE | T_PROTECTED | T_PUBLIC
| T_STATIC | T_ABSTRACT | T_FINAL | T_PRIVATE | T_PROTECTED | T_PUBLIC | T_READONLY
;
identifier_ex:
identifier_maybe_reserved:
T_STRING { $$ = Node\Identifier[$1]; }
| semi_reserved { $$ = Node\Identifier[$1]; }
;
identifier:
identifier_not_reserved:
T_STRING { $$ = Node\Identifier[$1]; }
;
@ -176,14 +181,14 @@ non_empty_inline_use_declarations:
unprefixed_use_declaration:
namespace_name
{ $$ = Stmt\UseUse[$1, null, Stmt\Use_::TYPE_UNKNOWN]; $this->checkUseUse($$, #1); }
| namespace_name T_AS identifier
| namespace_name T_AS identifier_not_reserved
{ $$ = Stmt\UseUse[$1, $3, Stmt\Use_::TYPE_UNKNOWN]; $this->checkUseUse($$, #3); }
;
use_declaration:
legacy_namespace_name
{ $$ = Stmt\UseUse[$1, null, Stmt\Use_::TYPE_UNKNOWN]; $this->checkUseUse($$, #1); }
| legacy_namespace_name T_AS identifier
| legacy_namespace_name T_AS identifier_not_reserved
{ $$ = Stmt\UseUse[$1, $3, Stmt\Use_::TYPE_UNKNOWN]; $this->checkUseUse($$, #3); }
;
@ -203,7 +208,7 @@ non_empty_constant_declaration_list:
;
constant_declaration:
identifier '=' expr { $$ = Node\Const_[$1, $3]; }
identifier_not_reserved '=' expr { $$ = Node\Const_[$1, $3]; }
;
class_const_list:
@ -216,7 +221,7 @@ non_empty_class_const_list:
;
class_const:
identifier_ex '=' expr { $$ = Node\Const_[$1, $3]; }
identifier_maybe_reserved '=' expr { $$ = Node\Const_[$1, $3]; }
;
inner_statement_list_ex:
@ -284,8 +289,8 @@ non_empty_statement:
| T_DECLARE '(' declare_list ')' declare_statement { $$ = Stmt\Declare_[$3, $5]; }
| T_TRY '{' inner_statement_list '}' catches optional_finally
{ $$ = Stmt\TryCatch[$3, $5, $6]; $this->checkTryCatch($$); }
| T_GOTO identifier semi { $$ = Stmt\Goto_[$2]; }
| identifier ':' { $$ = Stmt\Label[$1]; }
| T_GOTO identifier_not_reserved semi { $$ = Stmt\Goto_[$2]; }
| identifier_not_reserved ':' { $$ = Stmt\Label[$1]; }
| error { $$ = array(); /* means: no statement */ }
;
@ -327,7 +332,12 @@ non_empty_variables_list:
optional_ref:
/* empty */ { $$ = false; }
| '&' { $$ = true; }
| ampersand { $$ = true; }
;
optional_arg_ref:
/* empty */ { $$ = false; }
| T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG { $$ = true; }
;
optional_ellipsis:
@ -341,24 +351,33 @@ block_or_error:
;
function_declaration_statement:
T_FUNCTION optional_ref identifier '(' parameter_list ')' optional_return_type block_or_error
T_FUNCTION optional_ref identifier_not_reserved '(' parameter_list ')' optional_return_type block_or_error
{ $$ = Stmt\Function_[$3, ['byRef' => $2, 'params' => $5, 'returnType' => $7, 'stmts' => $8, 'attrGroups' => []]]; }
| attributes T_FUNCTION optional_ref identifier '(' parameter_list ')' optional_return_type block_or_error
| attributes T_FUNCTION optional_ref identifier_not_reserved '(' parameter_list ')' optional_return_type block_or_error
{ $$ = Stmt\Function_[$4, ['byRef' => $3, 'params' => $6, 'returnType' => $8, 'stmts' => $9, 'attrGroups' => $1]]; }
;
class_declaration_statement:
class_entry_type identifier extends_from implements_list '{' class_statement_list '}'
{ $$ = Stmt\Class_[$2, ['type' => $1, 'extends' => $3, 'implements' => $4, 'stmts' => $6, 'attrGroups' => []]];
$this->checkClass($$, #2); }
| attributes class_entry_type identifier extends_from implements_list '{' class_statement_list '}'
optional_attributes class_entry_type identifier_not_reserved extends_from implements_list '{' class_statement_list '}'
{ $$ = Stmt\Class_[$3, ['type' => $2, 'extends' => $4, 'implements' => $5, 'stmts' => $7, 'attrGroups' => $1]];
$this->checkClass($$, #3); }
| optional_attributes T_INTERFACE identifier interface_extends_list '{' class_statement_list '}'
| optional_attributes T_INTERFACE identifier_not_reserved interface_extends_list '{' class_statement_list '}'
{ $$ = Stmt\Interface_[$3, ['extends' => $4, 'stmts' => $6, 'attrGroups' => $1]];
$this->checkInterface($$, #3); }
| optional_attributes T_TRAIT identifier '{' class_statement_list '}'
| optional_attributes T_TRAIT identifier_not_reserved '{' class_statement_list '}'
{ $$ = Stmt\Trait_[$3, ['stmts' => $5, 'attrGroups' => $1]]; }
| optional_attributes T_ENUM identifier_not_reserved 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:
@ -417,7 +436,7 @@ non_empty_declare_list:
;
declare_list_element:
identifier '=' expr { $$ = Stmt\DeclareDeclare[$1, $3]; }
identifier_not_reserved '=' expr { $$ = Stmt\DeclareDeclare[$1, $3]; }
;
switch_case_list:
@ -496,7 +515,7 @@ new_else_single:
foreach_variable:
variable { $$ = array($1, false); }
| '&' variable { $$ = array($2, true); }
| ampersand variable { $$ = array($2, true); }
| list_expr { $$ = array($1, false); }
| array_short_syntax { $$ = array($1, false); }
;
@ -511,21 +530,30 @@ non_empty_parameter_list:
| non_empty_parameter_list ',' parameter { push($1, $3); }
;
optional_visibility_modifier:
optional_property_modifiers:
/* empty */ { $$ = 0; }
| T_PUBLIC { $$ = Stmt\Class_::MODIFIER_PUBLIC; }
| optional_property_modifiers property_modifier
{ $this->checkModifier($1, $2, #2); $$ = $1 | $2; }
;
property_modifier:
T_PUBLIC { $$ = Stmt\Class_::MODIFIER_PUBLIC; }
| T_PROTECTED { $$ = Stmt\Class_::MODIFIER_PROTECTED; }
| T_PRIVATE { $$ = Stmt\Class_::MODIFIER_PRIVATE; }
| T_READONLY { $$ = Stmt\Class_::MODIFIER_READONLY; }
;
parameter:
optional_attributes optional_visibility_modifier optional_type_without_static optional_ref optional_ellipsis plain_variable
optional_attributes optional_property_modifiers optional_type_without_static
optional_arg_ref optional_ellipsis plain_variable
{ $$ = new Node\Param($6, null, $3, $4, $5, attributes(), $2, $1);
$this->checkParam($$); }
| optional_attributes optional_visibility_modifier optional_type_without_static optional_ref optional_ellipsis plain_variable '=' expr
| optional_attributes optional_property_modifiers optional_type_without_static
optional_arg_ref optional_ellipsis plain_variable '=' expr
{ $$ = new Node\Param($6, $8, $3, $4, $5, attributes(), $2, $1);
$this->checkParam($$); }
| optional_attributes optional_visibility_modifier optional_type_without_static optional_ref optional_ellipsis error
| optional_attributes optional_property_modifiers optional_type_without_static
optional_arg_ref optional_ellipsis error
{ $$ = new Node\Param(Expr\Error[], null, $3, $4, $5, attributes(), $2, $1); }
;
@ -533,6 +561,7 @@ type_expr:
type { $$ = $1; }
| '?' type { $$ = Node\NullableType[$2]; }
| union_type { $$ = Node\UnionType[$1]; }
| intersection_type { $$ = Node\IntersectionType[$1]; }
;
type:
@ -556,10 +585,24 @@ union_type_without_static:
| union_type_without_static '|' type_without_static { push($1, $3); }
;
intersection_type:
type T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG type { init($1, $3); }
| intersection_type T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG type
{ push($1, $3); }
;
intersection_type_without_static:
type_without_static T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG type_without_static
{ init($1, $3); }
| intersection_type_without_static T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG type_without_static
{ push($1, $3); }
;
type_expr_without_static:
type_without_static { $$ = $1; }
| '?' type_without_static { $$ = Node\NullableType[$2]; }
| union_type_without_static { $$ = Node\UnionType[$1]; }
| intersection_type_without_static { $$ = Node\IntersectionType[$1]; }
;
optional_type_without_static:
@ -576,6 +619,11 @@ optional_return_type:
argument_list:
'(' ')' { $$ = array(); }
| '(' non_empty_argument_list optional_comma ')' { $$ = $2; }
| '(' variadic_placeholder ')' { init($2); }
;
variadic_placeholder:
T_ELLIPSIS { $$ = Node\VariadicPlaceholder[]; }
;
non_empty_argument_list:
@ -585,9 +633,9 @@ non_empty_argument_list:
argument:
expr { $$ = Node\Arg[$1, false, false]; }
| '&' variable { $$ = Node\Arg[$2, true, false]; }
| ampersand variable { $$ = Node\Arg[$2, true, false]; }
| T_ELLIPSIS expr { $$ = Node\Arg[$2, false, true]; }
| identifier_ex ':' expr
| identifier_maybe_reserved ':' expr
{ $$ = new Node\Arg($3, false, false, attributes(), $1); }
;
@ -601,7 +649,7 @@ non_empty_global_var_list:
;
global_var:
simple_variable { $$ = Expr\Variable[$1]; }
simple_variable { $$ = $1; }
;
static_var_list:
@ -630,16 +678,19 @@ class_statement_list:
;
class_statement:
optional_attributes variable_modifiers optional_type_without_static property_declaration_list ';'
optional_attributes variable_modifiers optional_type_without_static property_declaration_list semi
{ $$ = new Stmt\Property($2, $4, attributes(), $3, $1);
$this->checkProperty($$, #2); }
| optional_attributes method_modifiers T_CONST class_const_list ';'
| optional_attributes method_modifiers T_CONST class_const_list semi
{ $$ = new Stmt\ClassConst($4, $2, attributes(), $1);
$this->checkClassConst($$, #2); }
| optional_attributes method_modifiers T_FUNCTION optional_ref identifier_ex '(' parameter_list ')' optional_return_type method_body
| optional_attributes method_modifiers T_FUNCTION optional_ref identifier_maybe_reserved '(' parameter_list ')'
optional_return_type method_body
{ $$ = Stmt\ClassMethod[$5, ['type' => $2, 'byRef' => $4, 'params' => $7, 'returnType' => $9, 'stmts' => $10, 'attrGroups' => $1]];
$this->checkClassMethod($$, #2); }
| T_USE class_name_list trait_adaptations { $$ = Stmt\TraitUse[$2, $3]; }
| optional_attributes T_CASE identifier_maybe_reserved enum_case_expr semi
{ $$ = Stmt\EnumCase[$3, $4, $1]; }
| error { $$ = null; /* will be skipped */ }
;
@ -656,22 +707,22 @@ trait_adaptation_list:
trait_adaptation:
trait_method_reference_fully_qualified T_INSTEADOF class_name_list ';'
{ $$ = Stmt\TraitUseAdaptation\Precedence[$1[0], $1[1], $3]; }
| trait_method_reference T_AS member_modifier identifier_ex ';'
| trait_method_reference T_AS member_modifier identifier_maybe_reserved ';'
{ $$ = Stmt\TraitUseAdaptation\Alias[$1[0], $1[1], $3, $4]; }
| trait_method_reference T_AS member_modifier ';'
{ $$ = Stmt\TraitUseAdaptation\Alias[$1[0], $1[1], $3, null]; }
| trait_method_reference T_AS identifier ';'
| trait_method_reference T_AS identifier_not_reserved ';'
{ $$ = Stmt\TraitUseAdaptation\Alias[$1[0], $1[1], null, $3]; }
| trait_method_reference T_AS reserved_non_modifiers_identifier ';'
{ $$ = Stmt\TraitUseAdaptation\Alias[$1[0], $1[1], null, $3]; }
;
trait_method_reference_fully_qualified:
name T_PAAMAYIM_NEKUDOTAYIM identifier_ex { $$ = array($1, $3); }
name T_PAAMAYIM_NEKUDOTAYIM identifier_maybe_reserved { $$ = array($1, $3); }
;
trait_method_reference:
trait_method_reference_fully_qualified { $$ = $1; }
| identifier_ex { $$ = array(null, $1); }
| identifier_maybe_reserved { $$ = array(null, $1); }
;
method_body:
@ -701,6 +752,7 @@ member_modifier:
| T_STATIC { $$ = Stmt\Class_::MODIFIER_STATIC; }
| T_ABSTRACT { $$ = Stmt\Class_::MODIFIER_ABSTRACT; }
| T_FINAL { $$ = Stmt\Class_::MODIFIER_FINAL; }
| T_READONLY { $$ = Stmt\Class_::MODIFIER_READONLY; }
;
property_declaration_list:
@ -745,7 +797,7 @@ expr:
| list_expr '=' expr { $$ = Expr\Assign[$1, $3]; }
| array_short_syntax '=' expr { $$ = Expr\Assign[$1, $3]; }
| variable '=' expr { $$ = Expr\Assign[$1, $3]; }
| variable '=' '&' variable { $$ = Expr\AssignRef[$1, $4]; }
| variable '=' ampersand variable { $$ = Expr\AssignRef[$1, $4]; }
| new_expr { $$ = $1; }
| match { $$ = $1; }
| T_CLONE expr { $$ = Expr\Clone_[$2]; }
@ -772,7 +824,8 @@ expr:
| expr T_LOGICAL_AND expr { $$ = Expr\BinaryOp\LogicalAnd[$1, $3]; }
| expr T_LOGICAL_XOR expr { $$ = Expr\BinaryOp\LogicalXor[$1, $3]; }
| expr '|' expr { $$ = Expr\BinaryOp\BitwiseOr [$1, $3]; }
| expr '&' expr { $$ = Expr\BinaryOp\BitwiseAnd[$1, $3]; }
| expr T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG expr { $$ = Expr\BinaryOp\BitwiseAnd[$1, $3]; }
| expr T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG expr { $$ = Expr\BinaryOp\BitwiseAnd[$1, $3]; }
| expr '^' expr { $$ = Expr\BinaryOp\BitwiseXor[$1, $3]; }
| expr '.' expr { $$ = Expr\BinaryOp\Concat [$1, $3]; }
| expr '+' expr { $$ = Expr\BinaryOp\Plus [$1, $3]; }
@ -832,18 +885,18 @@ expr:
| T_YIELD_FROM expr { $$ = Expr\YieldFrom[$2]; }
| T_THROW expr { $$ = Expr\Throw_[$2]; }
| T_FN optional_ref '(' parameter_list ')' optional_return_type T_DOUBLE_ARROW expr
| T_FN optional_ref '(' parameter_list ')' optional_return_type T_DOUBLE_ARROW expr %prec T_THROW
{ $$ = Expr\ArrowFunction[['static' => false, 'byRef' => $2, 'params' => $4, 'returnType' => $6, 'expr' => $8, 'attrGroups' => []]]; }
| T_STATIC T_FN optional_ref '(' parameter_list ')' optional_return_type T_DOUBLE_ARROW expr
| T_STATIC T_FN optional_ref '(' parameter_list ')' optional_return_type T_DOUBLE_ARROW expr %prec T_THROW
{ $$ = Expr\ArrowFunction[['static' => true, 'byRef' => $3, 'params' => $5, 'returnType' => $7, 'expr' => $9, 'attrGroups' => []]]; }
| T_FUNCTION optional_ref '(' parameter_list ')' lexical_vars optional_return_type block_or_error
{ $$ = Expr\Closure[['static' => false, 'byRef' => $2, 'params' => $4, 'uses' => $6, 'returnType' => $7, 'stmts' => $8, 'attrGroups' => []]]; }
| T_STATIC T_FUNCTION optional_ref '(' parameter_list ')' lexical_vars optional_return_type block_or_error
{ $$ = Expr\Closure[['static' => true, 'byRef' => $3, 'params' => $5, 'uses' => $7, 'returnType' => $8, 'stmts' => $9, 'attrGroups' => []]]; }
| attributes T_FN optional_ref '(' parameter_list ')' optional_return_type T_DOUBLE_ARROW expr
| attributes T_FN optional_ref '(' parameter_list ')' optional_return_type T_DOUBLE_ARROW expr %prec T_THROW
{ $$ = Expr\ArrowFunction[['static' => false, 'byRef' => $3, 'params' => $5, 'returnType' => $7, 'expr' => $9, 'attrGroups' => $1]]; }
| attributes T_STATIC T_FN optional_ref '(' parameter_list ')' optional_return_type T_DOUBLE_ARROW expr
| attributes T_STATIC T_FN optional_ref '(' parameter_list ')' optional_return_type T_DOUBLE_ARROW expr %prec T_THROW
{ $$ = Expr\ArrowFunction[['static' => true, 'byRef' => $4, 'params' => $6, 'returnType' => $8, 'expr' => $10, 'attrGroups' => $1]]; }
| attributes T_FUNCTION optional_ref '(' parameter_list ')' lexical_vars optional_return_type block_or_error
{ $$ = Expr\Closure[['static' => false, 'byRef' => $3, 'params' => $5, 'uses' => $7, 'returnType' => $8, 'stmts' => $9, 'attrGroups' => $1]]; }
@ -942,7 +995,7 @@ constant:
;
class_constant:
class_name_or_var T_PAAMAYIM_NEKUDOTAYIM identifier_ex
class_name_or_var T_PAAMAYIM_NEKUDOTAYIM identifier_maybe_reserved
{ $$ = Expr\ClassConstFetch[$1, $3]; }
/* We interpret an isolated FOO:: as an unfinished class constant fetch. It could also be
an unfinished static property fetch or unfinished scoped call. */
@ -1007,7 +1060,7 @@ callable_expr:
;
callable_variable:
simple_variable { $$ = Expr\Variable[$1]; }
simple_variable { $$ = $1; }
| array_object_dereferencable '[' optional_expr ']' { $$ = Expr\ArrayDimFetch[$1, $3]; }
| array_object_dereferencable '{' expr '}' { $$ = Expr\ArrayDimFetch[$1, $3]; }
| function_call { $$ = $1; }
@ -1032,15 +1085,15 @@ variable:
;
simple_variable:
T_VARIABLE { $$ = parseVar($1); }
| '$' '{' expr '}' { $$ = $3; }
plain_variable { $$ = $1; }
| '$' '{' expr '}' { $$ = Expr\Variable[$3]; }
| '$' simple_variable { $$ = Expr\Variable[$2]; }
| '$' error { $$ = Expr\Error[]; $this->errorState = 2; }
| '$' error { $$ = Expr\Variable[Expr\Error[]]; $this->errorState = 2; }
;
static_member_prop_name:
simple_variable
{ $var = $1; $$ = \is_string($var) ? Node\VarLikeIdentifier[$var] : $var; }
{ $var = $1->name; $$ = \is_string($var) ? Node\VarLikeIdentifier[$var] : $var; }
;
static_member:
@ -1049,7 +1102,7 @@ static_member:
;
new_variable:
simple_variable { $$ = Expr\Variable[$1]; }
simple_variable { $$ = $1; }
| new_variable '[' optional_expr ']' { $$ = Expr\ArrayDimFetch[$1, $3]; }
| new_variable '{' expr '}' { $$ = Expr\ArrayDimFetch[$1, $3]; }
| new_variable T_OBJECT_OPERATOR property_name { $$ = Expr\PropertyFetch[$1, $3]; }
@ -1061,15 +1114,15 @@ new_variable:
;
member_name:
identifier_ex { $$ = $1; }
identifier_maybe_reserved { $$ = $1; }
| '{' expr '}' { $$ = $2; }
| simple_variable { $$ = Expr\Variable[$1]; }
| simple_variable { $$ = $1; }
;
property_name:
identifier { $$ = $1; }
identifier_not_reserved { $$ = $1; }
| '{' expr '}' { $$ = $2; }
| simple_variable { $$ = Expr\Variable[$1]; }
| simple_variable { $$ = $1; }
| error { $$ = Expr\Error[]; $this->errorState = 2; }
;
@ -1095,10 +1148,10 @@ inner_array_pair_list:
array_pair:
expr { $$ = Expr\ArrayItem[$1, null, false]; }
| '&' variable { $$ = Expr\ArrayItem[$2, null, true]; }
| ampersand variable { $$ = Expr\ArrayItem[$2, null, true]; }
| list_expr { $$ = Expr\ArrayItem[$1, null, false]; }
| expr T_DOUBLE_ARROW expr { $$ = Expr\ArrayItem[$3, $1, false]; }
| expr T_DOUBLE_ARROW '&' variable { $$ = Expr\ArrayItem[$4, $1, true]; }
| expr T_DOUBLE_ARROW ampersand variable { $$ = Expr\ArrayItem[$4, $1, true]; }
| expr T_DOUBLE_ARROW list_expr { $$ = Expr\ArrayItem[$3, $1, false]; }
| T_ELLIPSIS expr { $$ = Expr\ArrayItem[$2, null, false, attributes(), true]; }
| /* empty */ { $$ = null; }
@ -1122,8 +1175,10 @@ encaps_str_varname:
encaps_var:
plain_variable { $$ = $1; }
| plain_variable '[' encaps_var_offset ']' { $$ = Expr\ArrayDimFetch[$1, $3]; }
| plain_variable T_OBJECT_OPERATOR identifier { $$ = Expr\PropertyFetch[$1, $3]; }
| plain_variable T_NULLSAFE_OBJECT_OPERATOR identifier { $$ = Expr\NullsafePropertyFetch[$1, $3]; }
| plain_variable T_OBJECT_OPERATOR identifier_not_reserved
{ $$ = Expr\PropertyFetch[$1, $3]; }
| plain_variable T_NULLSAFE_OBJECT_OPERATOR identifier_not_reserved
{ $$ = Expr\NullsafePropertyFetch[$1, $3]; }
| T_DOLLAR_OPEN_CURLY_BRACES expr '}' { $$ = Expr\Variable[$2]; }
| T_DOLLAR_OPEN_CURLY_BRACES T_STRING_VARNAME '}' { $$ = Expr\Variable[$2]; }
| T_DOLLAR_OPEN_CURLY_BRACES encaps_str_varname '[' expr ']' '}'

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

View File

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

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

View File

@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace PhpParser\Builder;
use PhpParser;
use PhpParser\BuilderHelpers;
use PhpParser\Node;
use PhpParser\Node\Identifier;
use PhpParser\Node\Stmt;
class EnumCase implements PhpParser\Builder
{
protected $name;
protected $value = null;
protected $attributes = [];
/** @var Node\AttributeGroup[] */
protected $attributeGroups = [];
/**
* Creates an enum case builder.
*
* @param string|Identifier $name Name
*/
public function __construct($name) {
$this->name = $name;
}
/**
* Sets the value.
*
* @param Node\Expr|string|int $value
*
* @return $this
*/
public function setValue($value) {
$this->value = BuilderHelpers::normalizeValue($value);
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 enum case node.
*
* @return Stmt\EnumCase The built constant node
*/
public function getNode(): PhpParser\Node {
return new Stmt\EnumCase(
$this->name,
$this->value,
$this->attributes,
$this->attributeGroups
);
}
}

View File

@ -0,0 +1,117 @@
<?php declare(strict_types=1);
namespace PhpParser\Builder;
use PhpParser;
use PhpParser\BuilderHelpers;
use PhpParser\Node;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt;
class Enum_ extends Declaration
{
protected $name;
protected $scalarType = null;
protected $implements = [];
protected $uses = [];
protected $enumCases = [];
protected $constants = [];
protected $methods = [];
/** @var Node\AttributeGroup[] */
protected $attributeGroups = [];
/**
* Creates an enum builder.
*
* @param string $name Name of the enum
*/
public function __construct(string $name) {
$this->name = $name;
}
/**
* Sets the scalar type.
*
* @param string|Identifier $type
*
* @return $this
*/
public function setScalarType($scalarType) {
$this->scalarType = BuilderHelpers::normalizeType($scalarType);
return $this;
}
/**
* Implements one or more interfaces.
*
* @param Name|string ...$interfaces Names of interfaces to implement
*
* @return $this The builder instance (for fluid interface)
*/
public function implement(...$interfaces) {
foreach ($interfaces as $interface) {
$this->implements[] = BuilderHelpers::normalizeName($interface);
}
return $this;
}
/**
* Adds a statement.
*
* @param Stmt|PhpParser\Builder $stmt The statement to add
*
* @return $this The builder instance (for fluid interface)
*/
public function addStmt($stmt) {
$stmt = BuilderHelpers::normalizeNode($stmt);
$targets = [
Stmt\TraitUse::class => &$this->uses,
Stmt\EnumCase::class => &$this->enumCases,
Stmt\ClassConst::class => &$this->constants,
Stmt\ClassMethod::class => &$this->methods,
];
$class = \get_class($stmt);
if (!isset($targets[$class])) {
throw new \LogicException(sprintf('Unexpected node of type "%s"', $stmt->getType()));
}
$targets[$class][] = $stmt;
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\Enum_ The built enum node
*/
public function getNode() : PhpParser\Node {
return new Stmt\Enum_($this->name, [
'scalarType' => $this->scalarType,
'implements' => $this->implements,
'stmts' => array_merge($this->uses, $this->enumCases, $this->constants, $this->methods),
'attrGroups' => $this->attributeGroups,
], $this->attributes);
}
}

View File

@ -61,8 +61,7 @@ abstract class FunctionLike extends Declaration
/**
* Sets the return type for PHP 7.
*
* @param string|Node\Name|Node\NullableType $type One of array, callable, string, int, float,
* bool, iterable, or a class/interface name.
* @param string|Node\Name|Node\Identifier|Node\ComplexType $type
*
* @return $this The builder instance (for fluid interface)
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,21 @@ use PhpParser\Node\Stmt\Use_;
class BuilderFactory
{
/**
* Creates an attribute node.
*
* @param string|Name $name Name of the attribute
* @param array $args Attribute named arguments
*
* @return Node\Attribute
*/
public function attribute($name, array $args = []) : Node\Attribute {
return new Node\Attribute(
BuilderHelpers::normalizeName($name),
$this->args($args)
);
}
/**
* Creates a namespace builder.
*
@ -56,6 +71,17 @@ class BuilderFactory
return new Builder\Trait_($name);
}
/**
* Creates an enum builder.
*
* @param string $name Name of the enum
*
* @return Builder\Enum_ The created enum builder
*/
public function enum(string $name) : Builder\Enum_ {
return new Builder\Enum_($name);
}
/**
* Creates a trait use builder.
*
@ -161,6 +187,29 @@ class BuilderFactory
return new Builder\Use_($name, Use_::TYPE_CONSTANT);
}
/**
* Creates a class constant builder.
*
* @param string|Identifier $name Name
* @param Node\Expr|bool|null|int|float|string|array $value Value
*
* @return Builder\ClassConst The created use const builder
*/
public function classConst($name, $value) : Builder\ClassConst {
return new Builder\ClassConst($name, $value);
}
/**
* Creates an enum case builder.
*
* @param string|Identifier $name Name
*
* @return Builder\EnumCase The created use const builder
*/
public function enumCase($name) : Builder\EnumCase {
return new Builder\EnumCase($name);
}
/**
* Creates node a for a literal value.
*
@ -198,12 +247,14 @@ class BuilderFactory
*/
public function args(array $args) : array {
$normalizedArgs = [];
foreach ($args as $arg) {
if ($arg instanceof Arg) {
$normalizedArgs[] = $arg;
} else {
$normalizedArgs[] = new Arg(BuilderHelpers::normalizeValue($arg));
foreach ($args as $key => $arg) {
if (!($arg instanceof Arg)) {
$arg = new Arg(BuilderHelpers::normalizeValue($arg));
}
if (\is_string($key)) {
$arg->name = BuilderHelpers::normalizeIdentifier($key);
}
$normalizedArgs[] = $arg;
}
return $normalizedArgs;
}
@ -282,7 +333,7 @@ class BuilderFactory
public function constFetch($name) : Expr\ConstFetch {
return new Expr\ConstFetch(BuilderHelpers::normalizeName($name));
}
/**
* Creates a property fetch node.
*

View File

@ -2,13 +2,13 @@
namespace PhpParser;
use PhpParser\Node\ComplexType;
use PhpParser\Node\Expr;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\NullableType;
use PhpParser\Node\Scalar;
use PhpParser\Node\Stmt;
use PhpParser\Node\UnionType;
/**
* This class defines helpers used in the implementation of builders. Don't use it directly.
@ -27,7 +27,9 @@ final class BuilderHelpers
public static function normalizeNode($node) : Node {
if ($node instanceof Builder) {
return $node->getNode();
} elseif ($node instanceof Node) {
}
if ($node instanceof Node) {
return $node;
}
@ -102,7 +104,27 @@ final class BuilderHelpers
* @return Name The normalized name
*/
public static function normalizeName($name) : Name {
return self::normalizeNameCommon($name, false);
if ($name instanceof Name) {
return $name;
}
if (is_string($name)) {
if (!$name) {
throw new \LogicException('Name cannot be empty');
}
if ($name[0] === '\\') {
return new Name\FullyQualified(substr($name, 1));
}
if (0 === strpos($name, 'namespace\\')) {
return new Name\Relative(substr($name, strlen('namespace\\')));
}
return new Name($name);
}
throw new \LogicException('Name must be a string or an instance of Node\Name');
}
/**
@ -113,44 +135,17 @@ final class BuilderHelpers
* @return Name|Expr The normalized name or expression
*/
public static function normalizeNameOrExpr($name) {
return self::normalizeNameCommon($name, true);
}
/**
* Normalizes a name: Converts string names to Name nodes, optionally allowing expressions.
*
* @param Expr|Name|string $name The name to normalize
* @param bool $allowExpr Whether to also allow expressions
*
* @return Name|Expr The normalized name, or expression (if allowed)
*/
private static function normalizeNameCommon($name, bool $allowExpr) {
if ($name instanceof Name) {
if ($name instanceof Expr) {
return $name;
} elseif (is_string($name)) {
if (!$name) {
throw new \LogicException('Name cannot be empty');
}
if ($name[0] === '\\') {
return new Name\FullyQualified(substr($name, 1));
} elseif (0 === strpos($name, 'namespace\\')) {
return new Name\Relative(substr($name, strlen('namespace\\')));
} else {
return new Name($name);
}
}
if ($allowExpr) {
if ($name instanceof Expr) {
return $name;
}
if (!is_string($name) && !($name instanceof Name)) {
throw new \LogicException(
'Name must be a string or an instance of Node\Name or Node\Expr'
);
} else {
throw new \LogicException('Name must be a string or an instance of Node\Name');
}
return self::normalizeName($name);
}
/**
@ -159,18 +154,18 @@ final class BuilderHelpers
* In particular, builtin types become Identifiers, custom types become Names and nullables
* are wrapped in NullableType nodes.
*
* @param string|Name|Identifier|NullableType|UnionType $type The type to normalize
* @param string|Name|Identifier|ComplexType $type The type to normalize
*
* @return Name|Identifier|NullableType|UnionType The normalized type
* @return Name|Identifier|ComplexType The normalized type
*/
public static function normalizeType($type) {
if (!is_string($type)) {
if (
!$type instanceof Name && !$type instanceof Identifier &&
!$type instanceof NullableType && !$type instanceof UnionType
!$type instanceof ComplexType
) {
throw new \LogicException(
'Type must be a string, or an instance of Name, Identifier, NullableType or UnionType'
'Type must be a string, or an instance of Name, Identifier or ComplexType'
);
}
return $type;
@ -183,7 +178,7 @@ final class BuilderHelpers
}
$builtinTypes = [
'array', 'callable', 'string', 'int', 'float', 'bool', 'iterable', 'void', 'object', 'mixed'
'array', 'callable', 'string', 'int', 'float', 'bool', 'iterable', 'void', 'object', 'mixed', 'never',
];
$lowerType = strtolower($type);
@ -193,12 +188,11 @@ final class BuilderHelpers
$type = self::normalizeName($type);
}
if ($nullable && (string) $type === 'void') {
throw new \LogicException('void type cannot be nullable');
}
if ($nullable && (string) $type === 'mixed') {
throw new \LogicException('mixed type cannot be nullable');
$notNullableTypes = [
'void', 'mixed', 'never',
];
if ($nullable && in_array((string) $type, $notNullableTypes)) {
throw new \LogicException(sprintf('%s type cannot be nullable', $type));
}
return $nullable ? new NullableType($type) : $type;
@ -215,21 +209,33 @@ final class BuilderHelpers
public static function normalizeValue($value) : Expr {
if ($value instanceof Node\Expr) {
return $value;
} elseif (is_null($value)) {
}
if (is_null($value)) {
return new Expr\ConstFetch(
new Name('null')
);
} elseif (is_bool($value)) {
}
if (is_bool($value)) {
return new Expr\ConstFetch(
new Name($value ? 'true' : 'false')
);
} elseif (is_int($value)) {
}
if (is_int($value)) {
return new Scalar\LNumber($value);
} elseif (is_float($value)) {
}
if (is_float($value)) {
return new Scalar\DNumber($value);
} elseif (is_string($value)) {
}
if (is_string($value)) {
return new Scalar\String_($value);
} elseif (is_array($value)) {
}
if (is_array($value)) {
$items = [];
$lastKey = -1;
foreach ($value as $itemKey => $itemValue) {
@ -248,9 +254,9 @@ final class BuilderHelpers
}
return new Expr\Array_($items);
} else {
throw new \LogicException('Invalid value');
}
throw new \LogicException('Invalid value');
}
/**
@ -263,11 +269,33 @@ final class BuilderHelpers
public static function normalizeDocComment($docComment) : Comment\Doc {
if ($docComment instanceof Comment\Doc) {
return $docComment;
} elseif (is_string($docComment)) {
return new Comment\Doc($docComment);
} else {
throw new \LogicException('Doc comment must be a string or an instance of PhpParser\Comment\Doc');
}
if (is_string($docComment)) {
return new Comment\Doc($docComment);
}
throw new \LogicException('Doc comment must be a string or an instance of PhpParser\Comment\Doc');
}
/**
* Normalizes a attribute: Converts attribute to the Attribute Group if needed.
*
* @param Node\Attribute|Node\AttributeGroup $attribute
*
* @return Node\AttributeGroup The Attribute Group
*/
public static function normalizeAttribute($attribute) : Node\AttributeGroup
{
if ($attribute instanceof Node\AttributeGroup) {
return $attribute;
}
if (!($attribute instanceof Node\Attribute)) {
throw new \LogicException('Attribute must be an instance of PhpParser\Node\Attribute or PhpParser\Node\AttributeGroup');
}
return new Node\AttributeGroup([$attribute]);
}
/**

View File

@ -2,6 +2,7 @@
namespace PhpParser;
use function array_merge;
use PhpParser\Node\Expr;
use PhpParser\Node\Scalar;
@ -150,6 +151,8 @@ class ConstExprEvaluator
foreach ($expr->items as $item) {
if (null !== $item->key) {
$array[$this->evaluate($item->key)] = $this->evaluate($item->value);
} elseif ($item->unpack) {
$array = array_merge($array, $this->evaluate($item->value));
} else {
$array[] = $this->evaluate($item->value);
}

View File

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

View File

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

View File

@ -6,21 +6,24 @@ use PhpParser\Error;
use PhpParser\ErrorHandler;
use PhpParser\Lexer;
use PhpParser\Lexer\TokenEmulator\AttributeEmulator;
use PhpParser\Lexer\TokenEmulator\EnumTokenEmulator;
use PhpParser\Lexer\TokenEmulator\CoaleseEqualTokenEmulator;
use PhpParser\Lexer\TokenEmulator\ExplicitOctalEmulator;
use PhpParser\Lexer\TokenEmulator\FlexibleDocStringEmulator;
use PhpParser\Lexer\TokenEmulator\FnTokenEmulator;
use PhpParser\Lexer\TokenEmulator\MatchTokenEmulator;
use PhpParser\Lexer\TokenEmulator\NullsafeTokenEmulator;
use PhpParser\Lexer\TokenEmulator\NumericLiteralSeparatorEmulator;
use PhpParser\Lexer\TokenEmulator\ReadonlyTokenEmulator;
use PhpParser\Lexer\TokenEmulator\ReverseEmulator;
use PhpParser\Lexer\TokenEmulator\TokenEmulator;
use PhpParser\Parser\Tokens;
class Emulative extends Lexer
{
const PHP_7_3 = '7.3dev';
const PHP_7_4 = '7.4dev';
const PHP_8_0 = '8.0dev';
const PHP_8_1 = '8.1dev';
/** @var mixed[] Patches used to reverse changes introduced in the code */
private $patches = [];
@ -34,11 +37,11 @@ class Emulative extends Lexer
/**
* @param mixed[] $options Lexer options. In addition to the usual options,
* accepts a 'phpVersion' string that specifies the
* version to emulated. Defaults to newest supported.
* version to emulate. Defaults to newest supported.
*/
public function __construct(array $options = [])
{
$this->targetPhpVersion = $options['phpVersion'] ?? Emulative::PHP_8_0;
$this->targetPhpVersion = $options['phpVersion'] ?? Emulative::PHP_8_1;
unset($options['phpVersion']);
parent::__construct($options);
@ -51,6 +54,9 @@ class Emulative extends Lexer
new NumericLiteralSeparatorEmulator(),
new NullsafeTokenEmulator(),
new AttributeEmulator(),
new EnumTokenEmulator(),
new ReadonlyTokenEmulator(),
new ExplicitOctalEmulator(),
];
// 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

@ -0,0 +1,44 @@
<?php declare(strict_types=1);
namespace PhpParser\Lexer\TokenEmulator;
use PhpParser\Lexer\Emulative;
class ExplicitOctalEmulator extends TokenEmulator {
public function getPhpVersion(): string {
return Emulative::PHP_8_1;
}
public function isEmulationNeeded(string $code): bool {
return strpos($code, '0o') !== false || strpos($code, '0O') !== false;
}
public function emulate(string $code, array $tokens): array {
for ($i = 0, $c = count($tokens); $i < $c; ++$i) {
if ($tokens[$i][0] == \T_LNUMBER && $tokens[$i][1] === '0' &&
isset($tokens[$i + 1]) && $tokens[$i + 1][0] == \T_STRING &&
preg_match('/[oO][0-7]+(?:_[0-7]+)*/', $tokens[$i + 1][1])
) {
$tokenKind = $this->resolveIntegerOrFloatToken($tokens[$i + 1][1]);
array_splice($tokens, $i, 2, [
[$tokenKind, '0' . $tokens[$i + 1][1], $tokens[$i][2]],
]);
$c--;
}
}
return $tokens;
}
private function resolveIntegerOrFloatToken(string $str): int
{
$str = substr($str, 1);
$str = str_replace('_', '', $str);
$num = octdec($str);
return is_float($num) ? \T_DNUMBER : \T_LNUMBER;
}
public function reverseEmulate(string $code, array $tokens): array {
// Explicit octals were not legal code previously, don't bother.
return $tokens;
}
}

View File

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

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

@ -2,6 +2,7 @@
namespace PhpParser\Node;
use PhpParser\Node\VariadicPlaceholder;
use PhpParser\NodeAbstract;
class Arg extends NodeAbstract

View File

@ -0,0 +1,14 @@
<?php declare(strict_types=1);
namespace PhpParser\Node;
use PhpParser\NodeAbstract;
/**
* This is a base class for complex types, including nullable types and union types.
*
* It does not provide any shared behavior and exists only for type-checking purposes.
*/
abstract class ComplexType extends NodeAbstract
{
}

View File

@ -4,9 +4,6 @@ namespace PhpParser\Node;
use PhpParser\NodeAbstract;
/**
* @property Name $namespacedName Namespaced name (for global constants, if using NameResolver)
*/
class Const_ extends NodeAbstract
{
/** @var Identifier Name */
@ -14,6 +11,9 @@ class Const_ extends NodeAbstract
/** @var Expr Value */
public $value;
/** @var Name Namespaced name (if using NameResolver) */
public $namespacedName;
/**
* Constructs a const node for use in class const and const statements.
*

View File

@ -17,7 +17,7 @@ class ArrowFunction extends Expr implements FunctionLike
/** @var Node\Param[] */
public $params = [];
/** @var null|Node\Identifier|Node\Name|Node\NullableType|Node\UnionType */
/** @var null|Node\Identifier|Node\Name|Node\ComplexType */
public $returnType;
/** @var Expr */
@ -42,7 +42,7 @@ class ArrowFunction extends Expr implements FunctionLike
$this->params = $subNodes['params'] ?? [];
$returnType = $subNodes['returnType'] ?? null;
$this->returnType = \is_string($returnType) ? new Node\Identifier($returnType) : $returnType;
$this->expr = $subNodes['expr'] ?? null;
$this->expr = $subNodes['expr'];
$this->attrGroups = $subNodes['attrGroups'] ?? [];
}

View File

@ -0,0 +1,39 @@
<?php declare(strict_types=1);
namespace PhpParser\Node\Expr;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\VariadicPlaceholder;
abstract class CallLike extends Expr {
/**
* Return raw arguments, which may be actual Args, or VariadicPlaceholders for first-class
* callables.
*
* @return array<Arg|VariadicPlaceholder>
*/
abstract public function getRawArgs(): array;
/**
* Returns whether this call expression is actually a first class callable.
*/
public function isFirstClassCallable(): bool {
foreach ($this->getRawArgs() as $arg) {
if ($arg instanceof VariadicPlaceholder) {
return true;
}
}
return false;
}
/**
* Assert that this is not a first-class callable and return only ordinary Args.
*
* @return Arg[]
*/
public function getArgs(): array {
assert(!$this->isFirstClassCallable());
return $this->getRawArgs();
}
}

View File

@ -16,7 +16,7 @@ class Closure extends Expr implements FunctionLike
public $params;
/** @var ClosureUse[] use()s */
public $uses;
/** @var null|Node\Identifier|Node\Name|Node\NullableType|Node\UnionType Return type */
/** @var null|Node\Identifier|Node\Name|Node\ComplexType Return type */
public $returnType;
/** @var Node\Stmt[] Statements */
public $stmts;

View File

@ -5,19 +5,19 @@ namespace PhpParser\Node\Expr;
use PhpParser\Node;
use PhpParser\Node\Expr;
class FuncCall extends Expr
class FuncCall extends CallLike
{
/** @var Node\Name|Expr Function name */
public $name;
/** @var Node\Arg[] Arguments */
/** @var array<Node\Arg|Node\VariadicPlaceholder> Arguments */
public $args;
/**
* Constructs a function call node.
*
* @param Node\Name|Expr $name Function name
* @param Node\Arg[] $args Arguments
* @param array $attributes Additional attributes
* @param Node\Name|Expr $name Function name
* @param array<Node\Arg|Node\VariadicPlaceholder> $args Arguments
* @param array $attributes Additional attributes
*/
public function __construct($name, array $args = [], array $attributes = []) {
$this->attributes = $attributes;
@ -32,4 +32,8 @@ class FuncCall extends Expr
public function getType() : string {
return 'Expr_FuncCall';
}
public function getRawArgs(): array {
return $this->args;
}
}

View File

@ -5,23 +5,24 @@ namespace PhpParser\Node\Expr;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Identifier;
use PhpParser\Node\VariadicPlaceholder;
class MethodCall extends Expr
class MethodCall extends CallLike
{
/** @var Expr Variable holding object */
public $var;
/** @var Identifier|Expr Method name */
public $name;
/** @var Arg[] Arguments */
/** @var array<Arg|VariadicPlaceholder> Arguments */
public $args;
/**
* Constructs a function call node.
*
* @param Expr $var Variable holding object
* @param string|Identifier|Expr $name Method name
* @param Arg[] $args Arguments
* @param array $attributes Additional attributes
* @param Expr $var Variable holding object
* @param string|Identifier|Expr $name Method name
* @param array<Arg|VariadicPlaceholder> $args Arguments
* @param array $attributes Additional attributes
*/
public function __construct(Expr $var, $name, array $args = [], array $attributes = []) {
$this->attributes = $attributes;
@ -37,4 +38,8 @@ class MethodCall extends Expr
public function getType() : string {
return 'Expr_MethodCall';
}
public function getRawArgs(): array {
return $this->args;
}
}

View File

@ -3,20 +3,22 @@
namespace PhpParser\Node\Expr;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\VariadicPlaceholder;
class New_ extends Expr
class New_ extends CallLike
{
/** @var Node\Name|Expr|Node\Stmt\Class_ Class name */
public $class;
/** @var Node\Arg[] Arguments */
/** @var array<Arg|VariadicPlaceholder> Arguments */
public $args;
/**
* Constructs a function call node.
*
* @param Node\Name|Expr|Node\Stmt\Class_ $class Class name (or class node for anonymous classes)
* @param Node\Arg[] $args Arguments
* @param array<Arg|VariadicPlaceholder> $args Arguments
* @param array $attributes Additional attributes
*/
public function __construct($class, array $args = [], array $attributes = []) {
@ -32,4 +34,8 @@ class New_ extends Expr
public function getType() : string {
return 'Expr_New';
}
public function getRawArgs(): array {
return $this->args;
}
}

View File

@ -5,23 +5,24 @@ namespace PhpParser\Node\Expr;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Identifier;
use PhpParser\Node\VariadicPlaceholder;
class NullsafeMethodCall extends Expr
class NullsafeMethodCall extends CallLike
{
/** @var Expr Variable holding object */
public $var;
/** @var Identifier|Expr Method name */
public $name;
/** @var Arg[] Arguments */
/** @var array<Arg|VariadicPlaceholder> Arguments */
public $args;
/**
* Constructs a nullsafe method call node.
*
* @param Expr $var Variable holding object
* @param string|Identifier|Expr $name Method name
* @param Arg[] $args Arguments
* @param array $attributes Additional attributes
* @param Expr $var Variable holding object
* @param string|Identifier|Expr $name Method name
* @param array<Arg|VariadicPlaceholder> $args Arguments
* @param array $attributes Additional attributes
*/
public function __construct(Expr $var, $name, array $args = [], array $attributes = []) {
$this->attributes = $attributes;
@ -37,4 +38,8 @@ class NullsafeMethodCall extends Expr
public function getType() : string {
return 'Expr_NullsafeMethodCall';
}
public function getRawArgs(): array {
return $this->args;
}
}

View File

@ -3,25 +3,27 @@
namespace PhpParser\Node\Expr;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Identifier;
use PhpParser\Node\VariadicPlaceholder;
class StaticCall extends Expr
class StaticCall extends CallLike
{
/** @var Node\Name|Expr Class name */
public $class;
/** @var Identifier|Expr Method name */
public $name;
/** @var Node\Arg[] Arguments */
/** @var array<Arg|VariadicPlaceholder> Arguments */
public $args;
/**
* Constructs a static method call node.
*
* @param Node\Name|Expr $class Class name
* @param string|Identifier|Expr $name Method name
* @param Node\Arg[] $args Arguments
* @param array $attributes Additional attributes
* @param Node\Name|Expr $class Class name
* @param string|Identifier|Expr $name Method name
* @param array<Arg|VariadicPlaceholder> $args Arguments
* @param array $attributes Additional attributes
*/
public function __construct($class, $name, array $args = [], array $attributes = []) {
$this->attributes = $attributes;
@ -37,4 +39,8 @@ class StaticCall extends Expr
public function getType() : string {
return 'Expr_StaticCall';
}
public function getRawArgs(): array {
return $this->args;
}
}

View File

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

View File

@ -23,7 +23,7 @@ interface FunctionLike extends Node
/**
* Get the declared return type or null
*
* @return null|Identifier|Name|NullableType|UnionType
* @return null|Identifier|Name|ComplexType
*/
public function getReturnType();

View File

@ -0,0 +1,30 @@
<?php declare(strict_types=1);
namespace PhpParser\Node;
use PhpParser\NodeAbstract;
class IntersectionType extends ComplexType
{
/** @var (Identifier|Name)[] Types */
public $types;
/**
* Constructs an intersection type.
*
* @param (Identifier|Name)[] $types Types
* @param array $attributes Additional attributes
*/
public function __construct(array $types, array $attributes = []) {
$this->attributes = $attributes;
$this->types = $types;
}
public function getSubNodeNames() : array {
return ['types'];
}
public function getType() : string {
return 'IntersectionType';
}
}

View File

@ -2,9 +2,7 @@
namespace PhpParser\Node;
use PhpParser\NodeAbstract;
class NullableType extends NodeAbstract
class NullableType extends ComplexType
{
/** @var Identifier|Name Type */
public $type;

View File

@ -6,7 +6,7 @@ use PhpParser\NodeAbstract;
class Param extends NodeAbstract
{
/** @var null|Identifier|Name|NullableType|UnionType Type declaration */
/** @var null|Identifier|Name|ComplexType Type declaration */
public $type;
/** @var bool Whether parameter is passed by reference */
public $byRef;
@ -24,14 +24,14 @@ class Param extends NodeAbstract
/**
* Constructs a parameter node.
*
* @param Expr\Variable|Expr\Error $var Parameter variable
* @param null|Expr $default Default value
* @param null|string|Identifier|Name|NullableType|UnionType $type Type declaration
* @param bool $byRef Whether is passed by reference
* @param bool $variadic Whether this is a variadic argument
* @param array $attributes Additional attributes
* @param int $flags Optional visibility flags
* @param AttributeGroup[] $attrGroups PHP attribute groups
* @param Expr\Variable|Expr\Error $var Parameter variable
* @param null|Expr $default Default value
* @param null|string|Identifier|Name|ComplexType $type Type declaration
* @param bool $byRef Whether is passed by reference
* @param bool $variadic Whether this is a variadic argument
* @param array $attributes Additional attributes
* @param int $flags Optional visibility flags
* @param AttributeGroup[] $attrGroups PHP attribute groups
*/
public function __construct(
$var, Expr $default = null, $type = null,

View File

@ -62,6 +62,11 @@ class LNumber extends Scalar
throw new Error('Invalid numeric literal', $attributes);
}
// Strip optional explicit octal prefix.
if ('o' === $str[1] || 'O' === $str[1]) {
$str = substr($str, 2);
}
// use intval instead of octdec to get proper cutting behavior with malformed numbers
$attributes['kind'] = LNumber::KIND_OCT;
return new LNumber(intval($str, 8), $attributes);

View File

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

View File

@ -4,9 +4,6 @@ namespace PhpParser\Node\Stmt;
use PhpParser\Node;
/**
* @property Node\Name $namespacedName Namespaced name (if using NameResolver)
*/
abstract class ClassLike extends Node\Stmt
{
/** @var Node\Identifier|null Name */
@ -16,6 +13,9 @@ abstract class ClassLike extends Node\Stmt
/** @var Node\AttributeGroup[] PHP attribute groups */
public $attrGroups;
/** @var Node\Name Namespaced name (if using NameResolver) */
public $namespacedName;
/**
* @return TraitUse[]
*/

View File

@ -15,7 +15,7 @@ class ClassMethod extends Node\Stmt implements FunctionLike
public $name;
/** @var Node\Param[] Parameters */
public $params;
/** @var null|Node\Identifier|Node\Name|Node\NullableType|Node\UnionType Return type */
/** @var null|Node\Identifier|Node\Name|Node\ComplexType Return type */
public $returnType;
/** @var Node\Stmt[]|null Statements */
public $stmts;

View File

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

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

@ -5,9 +5,6 @@ namespace PhpParser\Node\Stmt;
use PhpParser\Node;
use PhpParser\Node\FunctionLike;
/**
* @property Node\Name $namespacedName Namespaced name (if using NameResolver)
*/
class Function_ extends Node\Stmt implements FunctionLike
{
/** @var bool Whether function returns by reference */
@ -16,13 +13,16 @@ class Function_ extends Node\Stmt implements FunctionLike
public $name;
/** @var Node\Param[] Parameters */
public $params;
/** @var null|Node\Identifier|Node\Name|Node\NullableType|Node\UnionType Return type */
/** @var null|Node\Identifier|Node\Name|Node\ComplexType Return type */
public $returnType;
/** @var Node\Stmt[] Statements */
public $stmts;
/** @var Node\AttributeGroup[] PHP attribute groups */
public $attrGroups;
/** @var Node\Name Namespaced name (if using NameResolver) */
public $namespacedName;
/**
* Constructs a function node.
*

View File

@ -3,10 +3,9 @@
namespace PhpParser\Node\Stmt;
use PhpParser\Node;
use PhpParser\Node\ComplexType;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\NullableType;
use PhpParser\Node\UnionType;
class Property extends Node\Stmt
{
@ -14,7 +13,7 @@ class Property extends Node\Stmt
public $flags;
/** @var PropertyProperty[] Properties */
public $props;
/** @var null|Identifier|Name|NullableType|UnionType Type declaration */
/** @var null|Identifier|Name|ComplexType Type declaration */
public $type;
/** @var Node\AttributeGroup[] PHP attribute groups */
public $attrGroups;
@ -22,11 +21,11 @@ class Property extends Node\Stmt
/**
* Constructs a class property list node.
*
* @param int $flags Modifiers
* @param PropertyProperty[] $props Properties
* @param array $attributes Additional attributes
* @param null|string|Identifier|Name|NullableType|UnionType $type Type declaration
* @param Node\AttributeGroup[] $attrGroups PHP attribute groups
* @param int $flags Modifiers
* @param PropertyProperty[] $props Properties
* @param array $attributes Additional attributes
* @param null|string|Identifier|Name|ComplexType $type Type declaration
* @param Node\AttributeGroup[] $attrGroups PHP attribute groups
*/
public function __construct(int $flags, array $props, array $attributes = [], $type = null, array $attrGroups = []) {
$this->attributes = $attributes;
@ -77,6 +76,15 @@ class Property extends Node\Stmt
return (bool) ($this->flags & Class_::MODIFIER_STATIC);
}
/**
* Whether the property is readonly.
*
* @return bool
*/
public function isReadonly() : bool {
return (bool) ($this->flags & Class_::MODIFIER_READONLY);
}
public function getType() : string {
return 'Stmt_Property';
}

View File

@ -2,9 +2,7 @@
namespace PhpParser\Node;
use PhpParser\NodeAbstract;
class UnionType extends NodeAbstract
class UnionType extends ComplexType
{
/** @var (Identifier|Name)[] Types */
public $types;

View File

@ -0,0 +1,27 @@
<?php declare(strict_types=1);
namespace PhpParser\Node;
use PhpParser\NodeAbstract;
/**
* Represents the "..." in "foo(...)" of the first-class callable syntax.
*/
class VariadicPlaceholder extends NodeAbstract {
/**
* Create a variadic argument placeholder (first-class callable syntax).
*
* @param array $attributes Additional attributes
*/
public function __construct(array $attributes = []) {
$this->attributes = $attributes;
}
public function getType(): string {
return 'VariadicPlaceholder';
}
public function getSubNodeNames(): array {
return [];
}
}

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

@ -0,0 +1,84 @@
<?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\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Stmt;
class EnumCaseTest extends \PHPUnit\Framework\TestCase
{
public function createEnumCaseBuilder($name) {
return new EnumCase($name);
}
public function testDocComment() {
$node = $this->createEnumCaseBuilder('TEST')
->setDocComment('/** Test */')
->getNode();
$this->assertEquals(
new Stmt\EnumCase(
"TEST",
null,
[
'comments' => [new Comment\Doc('/** Test */')]
]
),
$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->createEnumCaseBuilder('ATTR_GROUP')
->addAttribute($attributeGroup)
->getNode();
$this->assertEquals(
new Stmt\EnumCase(
"ATTR_GROUP",
null,
[],
[$attributeGroup]
),
$node
);
}
/**
* @dataProvider provideTestDefaultValues
*/
public function testValues($value, $expectedValueNode) {
$node = $this->createEnumCaseBuilder('TEST')
->setValue($value)
->getNode()
;
$this->assertEquals($expectedValueNode, $node->expr);
}
public function provideTestDefaultValues() {
return [
[
31415,
new Scalar\LNumber(31415)
],
[
'Hallo World',
new Scalar\String_('Hallo World')
],
];
}
}

View File

@ -0,0 +1,159 @@
<?php declare(strict_types=1);
namespace PhpParser\Builder;
use PhpParser\Comment;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Stmt;
class EnumTest extends \PHPUnit\Framework\TestCase
{
protected function createEnumBuilder($class) {
return new Enum_($class);
}
public function testImplements() {
$node = $this->createEnumBuilder('SomeEnum')
->implement('Namespaced\SomeInterface', new Name('OtherInterface'))
->getNode()
;
$this->assertEquals(
new Stmt\Enum_('SomeEnum', [
'implements' => [
new Name('Namespaced\SomeInterface'),
new Name('OtherInterface'),
],
]),
$node
);
}
public function testSetScalarType() {
$node = $this->createEnumBuilder('Test')
->setScalarType('int')
->getNode()
;
$this->assertEquals(
new Stmt\Enum_('Test', [
'scalarType' => new Identifier('int'),
]),
$node
);
}
public function testStatementOrder() {
$method = new Stmt\ClassMethod('testMethod');
$enumCase = new Stmt\EnumCase(
'TEST_ENUM_CASE'
);
$const = new Stmt\ClassConst([
new Node\Const_('TEST_CONST', new Node\Scalar\String_('ABC'))
]);
$use = new Stmt\TraitUse([new Name('SomeTrait')]);
$node = $this->createEnumBuilder('Test')
->addStmt($method)
->addStmt($enumCase)
->addStmts([$const, $use])
->getNode()
;
$this->assertEquals(
new Stmt\Enum_('Test', [
'stmts' => [$use, $enumCase, $const, $method]
]),
$node
);
}
public function testDocComment() {
$docComment = <<<'DOC'
/**
* Test
*/
DOC;
$enum = $this->createEnumBuilder('Test')
->setDocComment($docComment)
->getNode();
$this->assertEquals(
new Stmt\Enum_('Test', [], [
'comments' => [
new Comment\Doc($docComment)
]
]),
$enum
);
$enum = $this->createEnumBuilder('Test')
->setDocComment(new Comment\Doc($docComment))
->getNode();
$this->assertEquals(
new Stmt\Enum_('Test', [], [
'comments' => [
new Comment\Doc($docComment)
]
]),
$enum
);
}
public function testAddAttribute() {
$attribute = new Attribute(
new Name('Attr'),
[new Arg(new LNumber(1), false, false, [], new Identifier('name'))]
);
$attributeGroup = new AttributeGroup([$attribute]);
$enum = $this->createEnumBuilder('ATTR_GROUP')
->addAttribute($attributeGroup)
->getNode();
$this->assertEquals(
new Stmt\Enum_('ATTR_GROUP', [
'attrGroups' => [
$attributeGroup,
]
], []),
$enum
);
}
public function testInvalidStmtError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Unexpected node of type "Stmt_PropertyProperty"');
$this->createEnumBuilder('Test')
->addStmt(new Stmt\PropertyProperty('property'))
;
}
public function testInvalidDocComment() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Doc comment must be a string or an instance of PhpParser\Comment\Doc');
$this->createEnumBuilder('Test')
->setDocComment(new Comment('Test'));
}
public function testEmptyName() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Name cannot be empty');
$this->createEnumBuilder('Test')
->implement('');
}
public function testInvalidName() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Name must be a string or an instance of Node\Name');
$this->createEnumBuilder('Test')
->implement(['Foo']);
}
}

View File

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

View File

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

View File

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

View File

@ -3,8 +3,14 @@
namespace PhpParser\Builder;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Expr;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar;
use PhpParser\Node\Scalar\LNumber;
class ParamTest extends \PHPUnit\Framework\TestCase
{
@ -171,7 +177,7 @@ class ParamTest extends \PHPUnit\Framework\TestCase
public function testInvalidTypeError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Type must be a string, or an instance of Name, Identifier, NullableType or UnionType');
$this->expectExceptionMessage('Type must be a string, or an instance of Name, Identifier or ComplexType');
$this->createParamBuilder('test')->setType(new \stdClass);
}
@ -198,4 +204,21 @@ class ParamTest extends \PHPUnit\Framework\TestCase
$node
);
}
public function testAddAttribute() {
$attribute = new Attribute(
new Name('Attr'),
[new Arg(new LNumber(1), false, false, [], new Identifier('name'))]
);
$attributeGroup = new AttributeGroup([$attribute]);
$node = $this->createParamBuilder('attributeGroup')
->addAttribute($attributeGroup)
->getNode();
$this->assertEquals(
new Node\Param(new Expr\Variable('attributeGroup'), null, null, false, false, [], 0, [$attributeGroup]),
$node
);
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,222 @@
<?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));
$intersectionType = new Node\IntersectionType([new Node\Name('A'), new Node\Name('B')]);
$this->assertSame($intersectionType, BuilderHelpers::normalizeType($intersectionType));
$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 or ComplexType');
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 ||
$endTokenPos < $startTokenPos
) {
// Nops and error can have inverted order, if they are empty
if (!$node instanceof Stmt\Nop && !$node instanceof Expr\Error) {
// Nop and Error can have inverted order, if they are empty.
// This can also happen for a Param containing an Error.
if (!$node instanceof Stmt\Nop && !$node instanceof Expr\Error &&
!$node instanceof Node\Param
) {
throw new \Exception('End < start on ' . $node->getType());
}
}

View File

@ -22,6 +22,9 @@ class ConstExprEvaluatorTest extends \PHPUnit\Framework\TestCase
['"foo"', "foo"],
['[0, 1]', [0, 1]],
['["foo" => "bar"]', ["foo" => "bar"]],
['[...["bar"]]', ["bar"]],
['[...["foo" => "bar"]]', ["foo" => "bar"]],
['["a", "b" => "b", ...["b" => "bb", "c"]]', ["a", "b" => "bb", "c"]],
['NULL', null],
['False', false],
['true', true],

View File

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

View File

@ -0,0 +1,38 @@
<?php declare(strict_types=1);
namespace PhpParser\Node\Expr;
use PhpParser\Node\Arg;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\VariadicPlaceholder;
class CallableLikeTest extends \PHPUnit\Framework\TestCase {
/**
* @dataProvider provideTestIsFirstClassCallable
*/
public function testIsFirstClassCallable(CallLike $node, bool $isFirstClassCallable) {
$this->assertSame($isFirstClassCallable, $node->isFirstClassCallable());
if (!$isFirstClassCallable) {
$this->assertSame($node->getRawArgs(), $node->getArgs());
}
}
public function provideTestIsFirstClassCallable() {
$normalArgs = [new Arg(new LNumber(1))];
$callableArgs = [new VariadicPlaceholder()];
return [
[new FuncCall(new Name('test'), $normalArgs), false],
[new FuncCall(new Name('test'), $callableArgs), true],
[new MethodCall(new Variable('this'), 'test', $normalArgs), false],
[new MethodCall(new Variable('this'), 'test', $callableArgs), true],
[new StaticCall(new Name('Test'), 'test', $normalArgs), false],
[new StaticCall(new Name('Test'), 'test', $callableArgs), true],
[new New_(new Name('Test'), $normalArgs), false],
[new NullsafeMethodCall(new Variable('this'), 'test', $normalArgs), false],
// This is not legal code, but accepted by the parser.
[new New_(new Name('Test'), $callableArgs), true],
[new NullsafeMethodCall(new Variable('this'), 'test', $callableArgs), true],
];
}
}

View File

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

View File

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

View File

@ -6,11 +6,13 @@ class DummyNode extends NodeAbstract
{
public $subNode1;
public $subNode2;
public $notSubNode;
public function __construct($subNode1, $subNode2, $attributes) {
public function __construct($subNode1, $subNode2, $notSubNode, $attributes) {
parent::__construct($attributes);
$this->subNode1 = $subNode1;
$this->subNode2 = $subNode2;
$this->notSubNode = $notSubNode;
}
public function getSubNodeNames() : array {
@ -40,8 +42,7 @@ class NodeAbstractTest extends \PHPUnit\Framework\TestCase
],
];
$node = new DummyNode('value1', 'value2', $attributes);
$node->notSubNode = 'value3';
$node = new DummyNode('value1', 'value2', 'value3', $attributes);
return [
[$attributes, $node],
@ -90,7 +91,7 @@ class NodeAbstractTest extends \PHPUnit\Framework\TestCase
}
public function testSetDocComment() {
$node = new DummyNode(null, null, []);
$node = new DummyNode(null, null, null, []);
// Add doc comment to node without comments
$docComment = new Comment\Doc('/** doc */');
@ -120,19 +121,19 @@ class NodeAbstractTest extends \PHPUnit\Framework\TestCase
/**
* @dataProvider provideNodes
*/
public function testChange(array $attributes, Node $node) {
public function testChange(array $attributes, DummyNode $node) {
// direct modification
$node->subNode = 'newValue';
$this->assertSame('newValue', $node->subNode);
$node->subNode1 = 'newValue';
$this->assertSame('newValue', $node->subNode1);
// indirect modification
$subNode =& $node->subNode;
$subNode =& $node->subNode1;
$subNode = 'newNewValue';
$this->assertSame('newNewValue', $node->subNode);
$this->assertSame('newNewValue', $node->subNode1);
// removal
unset($node->subNode);
$this->assertObjectNotHasAttribute('subNode', $node);
unset($node->subNode1);
$this->assertFalse(isset($node->subNode1));
}
/**
@ -305,6 +306,7 @@ PHP;
}
],
"attrGroups": [],
"namespacedName": null,
"attributes": {
"startLine": 4,
"comments": [
@ -334,6 +336,134 @@ PHP;
}
]
JSON;
$expected81 = <<<'JSON'
[
{
"nodeType": "Stmt_Function",
"attributes": {
"startLine": 4,
"comments": [
{
"nodeType": "Comment",
"text": "\/\/ comment",
"line": 2,
"filePos": 6,
"tokenPos": 1,
"endLine": 2,
"endFilePos": 15,
"endTokenPos": 1
},
{
"nodeType": "Comment_Doc",
"text": "\/** doc comment *\/",
"line": 3,
"filePos": 17,
"tokenPos": 3,
"endLine": 3,
"endFilePos": 34,
"endTokenPos": 3
}
],
"endLine": 6
},
"byRef": false,
"name": {
"nodeType": "Identifier",
"attributes": {
"startLine": 4,
"endLine": 4
},
"name": "functionName"
},
"params": [
{
"nodeType": "Param",
"attributes": {
"startLine": 4,
"endLine": 4
},
"type": null,
"byRef": true,
"variadic": false,
"var": {
"nodeType": "Expr_Variable",
"attributes": {
"startLine": 4,
"endLine": 4
},
"name": "a"
},
"default": {
"nodeType": "Scalar_LNumber",
"attributes": {
"startLine": 4,
"endLine": 4,
"kind": 10
},
"value": 0
},
"flags": 0,
"attrGroups": []
},
{
"nodeType": "Param",
"attributes": {
"startLine": 4,
"endLine": 4
},
"type": null,
"byRef": false,
"variadic": false,
"var": {
"nodeType": "Expr_Variable",
"attributes": {
"startLine": 4,
"endLine": 4
},
"name": "b"
},
"default": {
"nodeType": "Scalar_DNumber",
"attributes": {
"startLine": 4,
"endLine": 4
},
"value": 1
},
"flags": 0,
"attrGroups": []
}
],
"returnType": null,
"stmts": [
{
"nodeType": "Stmt_Echo",
"attributes": {
"startLine": 5,
"endLine": 5
},
"exprs": [
{
"nodeType": "Scalar_String",
"attributes": {
"startLine": 5,
"endLine": 5,
"kind": 1
},
"value": "Foo"
}
]
}
],
"attrGroups": [],
"namespacedName": null
}
]
JSON;
if (version_compare(PHP_VERSION, '8.1', '>=')) {
$expected = $expected81;
}
$parser = new Parser\Php7(new Lexer());
$stmts = $parser->parse(canonicalize($code));

View File

@ -204,6 +204,13 @@ class A extends B implements C, D {
interface A extends C, D {
public function a(A $a) : A;
public function b(A|B|int $a): A|B|int;
public function c(A&B $a): A&B;
}
#[X]
enum E: int {
#[X]
case A = 1;
}
#[X]
@ -262,6 +269,13 @@ interface A extends \NS\C, \NS\D
{
public function a(\NS\A $a) : \NS\A;
public function b(\NS\A|\NS\B|int $a) : \NS\A|\NS\B|int;
public function c(\NS\A&\NS\B $a) : \NS\A&\NS\B;
}
#[\NS\X]
enum E : int
{
#[\NS\X]
case A = 1;
}
#[\NS\X]
trait A
@ -327,6 +341,7 @@ EOC;
]),
new Stmt\Trait_('E'),
new Expr\New_(new Stmt\Class_(null)),
new Stmt\Enum_('F'),
];
$traverser = new PhpParser\NodeTraverser;
@ -338,7 +353,8 @@ EOC;
$this->assertSame('NS\\C', (string) $stmts[0]->stmts[2]->namespacedName);
$this->assertSame('NS\\D', (string) $stmts[0]->stmts[3]->consts[0]->namespacedName);
$this->assertSame('NS\\E', (string) $stmts[0]->stmts[4]->namespacedName);
$this->assertObjectNotHasAttribute('namespacedName', $stmts[0]->stmts[5]->class);
$this->assertNull($stmts[0]->stmts[5]->class->namespacedName);
$this->assertSame('NS\\F', (string) $stmts[0]->stmts[6]->namespacedName);
$stmts = $traverser->traverse([new Stmt\Namespace_(null, $nsStmts)]);
$this->assertSame('A', (string) $stmts[0]->stmts[0]->namespacedName);
@ -346,7 +362,8 @@ EOC;
$this->assertSame('C', (string) $stmts[0]->stmts[2]->namespacedName);
$this->assertSame('D', (string) $stmts[0]->stmts[3]->consts[0]->namespacedName);
$this->assertSame('E', (string) $stmts[0]->stmts[4]->namespacedName);
$this->assertObjectNotHasAttribute('namespacedName', $stmts[0]->stmts[5]->class);
$this->assertNull($stmts[0]->stmts[5]->class->namespacedName);
$this->assertSame('F', (string) $stmts[0]->stmts[6]->namespacedName);
}
public function testAddRuntimeResolvedNamespacedName() {

View File

@ -135,6 +135,8 @@ EOC;
['0XF', ['kind' => Scalar\LNumber::KIND_HEX]],
['0b1', ['kind' => Scalar\LNumber::KIND_BIN]],
['0B1', ['kind' => Scalar\LNumber::KIND_BIN]],
['0o7', ['kind' => Scalar\LNumber::KIND_OCT]],
['0O7', ['kind' => Scalar\LNumber::KIND_OCT]],
['[]', ['kind' => Expr\Array_::KIND_SHORT]],
['array()', ['kind' => Expr\Array_::KIND_LONG]],
["'foo'", ['kind' => String_::KIND_SINGLE_QUOTED]],

View File

@ -0,0 +1,106 @@
Inserting array item with comment
-----
<?php
$items = [
'a' => 'foo',
'b' => 'bar',
];
-----
$node = new Expr\ArrayItem(new Scalar\String_('baz'), new Scalar\String_('c'));
$node->setAttribute('comments', [new Comment\Doc(<<<COMMENT
/**
* A doc comment
*/
COMMENT
)]);
$array = $stmts[0]->expr->expr;
$array->items[] = $node;
-----
<?php
$items = [
'a' => 'foo',
'b' => 'bar',
/**
* A doc comment
*/
'c' => 'baz',
];
-----
<?php
$items = [
'a' => 'foo',
'b' => 'bar',
];
-----
$node = new Expr\ArrayItem(new Scalar\String_('baz'), new Scalar\String_('c'));
$node->setAttribute('comments', [new Comment("/* Block comment */")]);
$array = $stmts[0]->expr->expr;
$array->items[] = $node;
-----
<?php
$items = [
'a' => 'foo',
'b' => 'bar',
/* Block comment */
'c' => 'baz',
];
-----
<?php
$items = [
'a' => 'foo',
'b' => 'bar',
];
-----
$node = new Expr\ArrayItem(new Scalar\String_('baz'), new Scalar\String_('c'));
$node->setAttribute('comments', [new Comment("// Line comment")]);
$array = $stmts[0]->expr->expr;
$array->items[] = $node;
-----
<?php
$items = [
'a' => 'foo',
'b' => 'bar',
// Line comment
'c' => 'baz',
];
-----
<?php
$items = [
'a' => 'foo',
];
-----
$node = new Expr\ArrayItem(new Scalar\String_('bar'), new Scalar\String_('b'));
$node->setAttribute('comments', [new Comment("// Line comment")]);
$array = $stmts[0]->expr->expr;
$array->items[] = $node;
-----
<?php
$items = [
'a' => 'foo',
// Line comment
'b' => 'bar',
];
-----
<?php
$items = [];
-----
$node = new Expr\ArrayItem(new Scalar\String_('foo'), new Scalar\String_('a'));
$node->setAttribute('comments', [new Comment("// Line comment")]);
$array = $stmts[0]->expr->expr;
$array->items[] = $node;
-----
<?php
$items = [
// Line comment
'a' => 'foo',
];

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

Some files were not shown because too many files have changed in this diff Show More