diff --git a/.gitattributes b/.gitattributes index ab732073..a728f976 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,9 @@ .gitattributes export-ignore .gitignore export-ignore .github export-ignore -.travis.yml export-ignore +ncs.* export-ignore +phpstan*.neon export-ignore tests/ export-ignore + *.sh eol=lf +*.php* diff=php linguist-language=PHP diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md deleted file mode 100644 index a4cd1263..00000000 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: "🐛 Bug Report" -about: "If something isn't working as expected 🤔" - ---- - -Version: ?.?.? - -### Bug Description -... A clear and concise description of what the bug is. A good bug report shouldn't leave others needing to chase you up for more information. - -### Steps To Reproduce -... If possible a minimal demo of the problem ... - -### Expected Behavior -... A clear and concise description of what you expected to happen. - -### Possible Solution -... Only if you have suggestions on a fix for the bug diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md deleted file mode 100644 index d2e21948..00000000 --- a/.github/ISSUE_TEMPLATE/Feature_request.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: "🚀 Feature Request" -about: "I have a suggestion (and may want to implement it) 🙂" - ---- - -- Is your feature request related to a problem? Please describe. -- Explain your intentions. -- It's up to you to make a strong case to convince the project's developers of the merits of this feature. diff --git a/.github/ISSUE_TEMPLATE/Support_question.md b/.github/ISSUE_TEMPLATE/Support_question.md deleted file mode 100644 index 75c48b6e..00000000 --- a/.github/ISSUE_TEMPLATE/Support_question.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: "🤗 Support Question" -about: "If you have a question 💬, please check out our forum!" - ---- - ---------------^ Click "Preview" for a nicer view! -We primarily use GitHub as an issue tracker; for usage and support questions, please check out these resources below. Thanks! 😁. - -* Nette Forum: https://forum.nette.org -* Nette Gitter: https://gitter.im/nette/nette -* Slack (czech): https://pehapkari.slack.com/messages/C2R30BLKA diff --git a/.github/ISSUE_TEMPLATE/Support_us.md b/.github/ISSUE_TEMPLATE/Support_us.md deleted file mode 100644 index 92d8a4c3..00000000 --- a/.github/ISSUE_TEMPLATE/Support_us.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -name: "❤️ Support us" -about: "If you would like to support our efforts in maintaining this project 🙌" - ---- - ---------------^ Click "Preview" for a nicer view! - -> https://nette.org/donate - -Help support Nette! - -We develop Nette Framework for more than 14 years. In order to make your life more comfortable. Nette cares about the safety of your sites. Nette saves you time. And gives job opportunities. - -Nette earns you money. And is absolutely free. - -To ensure future development and improving the documentation, we need your donation. - -Whether you are chief of IT company which benefits from Nette, or developer who goes for advice on our forum, if you like Nette, [please make a donation now](https://nette.org/donate). - -Thank you! diff --git a/.github/funding.yml b/.github/funding.yml deleted file mode 100644 index 25adc952..00000000 --- a/.github/funding.yml +++ /dev/null @@ -1,2 +0,0 @@ -github: dg -custom: "https://nette.org/donate" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index f8aa3f40..00000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,11 +0,0 @@ -- bug fix / new feature? -- BC break? yes/no -- doc PR: nette/docs#??? - - diff --git a/.github/workflows/coding-style.yml b/.github/workflows/coding-style.yml index 5b40e406..9e412ba6 100644 --- a/.github/workflows/coding-style.yml +++ b/.github/workflows/coding-style.yml @@ -7,25 +7,25 @@ jobs: name: Nette Code Checker runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: shivammathur/setup-php@v1 + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 with: - php-version: 7.4 + php-version: 8.3 coverage: none - run: composer create-project nette/code-checker temp/code-checker ^3 --no-progress - - run: php temp/code-checker/code-checker --strict-types --no-progress + - run: php temp/code-checker/code-checker --strict-types --no-progress --ignore "tests/*/fixtures" nette_cs: name: Nette Coding Standard runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: shivammathur/setup-php@v1 + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 with: - php-version: 7.4 + php-version: 8.3 coverage: none - - run: composer create-project nette/coding-standard temp/coding-standard ^2 --no-progress - - run: php temp/coding-standard/ecs check src tests --config tests/coding-standard.yml + - run: composer create-project nette/coding-standard temp/coding-standard ^3 --no-progress + - run: php temp/coding-standard/ecs check diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index e129028c..32145249 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -1,21 +1,18 @@ name: Static Analysis (only informative) -on: - push: - branches: - - master +on: [push, pull_request] jobs: phpstan: name: PHPStan runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: shivammathur/setup-php@v1 + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 with: - php-version: 7.4 + php-version: 8.5 coverage: none - run: composer install --no-progress --prefer-dist - - run: composer phpstan + - run: composer phpstan -- --no-progress continue-on-error: true # is only informative diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 611f2de6..8ed81e7e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,14 +7,14 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: ['7.1', '7.2', '7.3', '7.4'] + php: ['8.1', '8.2', '8.3', '8.4', '8.5'] fail-fast: false name: PHP ${{ matrix.php }} tests steps: - - uses: actions/checkout@v2 - - uses: shivammathur/setup-php@v1 + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} coverage: none @@ -22,20 +22,20 @@ jobs: - run: composer install --no-progress --prefer-dist - run: vendor/bin/tester tests -s -C - if: failure() - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: - name: output - path: tests/PhpGenerator/output + name: output-${{ matrix.php }} + path: tests/**/output lowest_dependencies: name: Lowest Dependencies runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: shivammathur/setup-php@v1 + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 with: - php-version: 7.1 + php-version: 8.1 coverage: none - run: composer update --no-progress --prefer-dist --prefer-lowest --prefer-stable @@ -46,13 +46,15 @@ jobs: name: Code Coverage runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: shivammathur/setup-php@v1 + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 with: - php-version: 7.4 + php-version: 8.1 coverage: none - run: composer install --no-progress --prefer-dist - - run: wget https://github.com/satooshi/php-coveralls/releases/download/v1.0.1/coveralls.phar - run: vendor/bin/tester -p phpdbg tests -s -C --coverage ./coverage.xml --coverage-src ./src - - run: php coveralls.phar --verbose --config tests/.coveralls.yml + - run: wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.4.3/php-coveralls.phar + - env: + COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: php php-coveralls.phar --verbose --config tests/.coveralls.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a021af57..00000000 --- a/.travis.yml +++ /dev/null @@ -1,72 +0,0 @@ -language: php -php: - - 7.1 - - 7.2 - - 7.3 - - 7.4 - -before_install: - # turn off XDebug - - phpenv config-rm xdebug.ini || return 0 - -install: - - travis_retry composer install --no-progress --prefer-dist - -script: - - vendor/bin/tester tests -s - -after_failure: - # Print *.actual content - - for i in $(find tests -name \*.actual); do echo "--- $i"; cat $i; echo; echo; done - -jobs: - include: - - name: Lowest Dependencies - install: - - travis_retry composer update --no-progress --prefer-dist --prefer-lowest --prefer-stable - - - - name: Nette Code Checker - php: 7.4 - install: - - travis_retry composer create-project nette/code-checker temp/code-checker ^3 --no-progress - script: - - php temp/code-checker/code-checker --strict-types - - - - name: Nette Coding Standard - php: 7.4 - install: - - travis_retry composer create-project nette/coding-standard temp/coding-standard ^2 --no-progress - script: - - php temp/coding-standard/ecs check src tests --config tests/coding-standard.yml - - - - stage: Static Analysis (informative) - php: 7.4 - script: - - composer run-script phpstan - - - - stage: Code Coverage - php: 7.4 - script: - - vendor/bin/tester -p phpdbg tests -s --coverage ./coverage.xml --coverage-src ./src - after_script: - - wget https://github.com/satooshi/php-coveralls/releases/download/v1.0.1/coveralls.phar - - php coveralls.phar --verbose --config tests/.coveralls.yml - - - allow_failures: - - stage: Static Analysis (informative) - - stage: Code Coverage - - -sudo: false - -cache: - directories: - - $HOME/.composer/cache - -notifications: - email: false diff --git a/composer.json b/composer.json index 30f44ead..4a98ff24 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "nette/php-generator", - "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 7.4 features.", + "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 8.5 features.", "keywords": ["nette", "php", "code", "scaffolding"], "homepage": "https://nette.org", "license": ["BSD-3-Clause", "GPL-2.0-only", "GPL-3.0-only"], @@ -15,29 +15,33 @@ } ], "require": { - "php": ">=7.1", - "nette/utils": "^2.4.2 || ^3.0" + "php": "8.1 - 8.5", + "nette/utils": "^4.0.6" }, "require-dev": { - "nette/tester": "^2.0", - "nikic/php-parser": "^4.4", - "tracy/tracy": "^2.3", - "phpstan/phpstan": "^0.12" + "nette/tester": "^2.4", + "nikic/php-parser": "^5.0", + "tracy/tracy": "^2.8", + "phpstan/phpstan-nette": "^2.0@stable", + "jetbrains/phpstorm-attributes": "^1.2" }, "suggest": { - "nikic/php-parser": "to use ClassType::withBodiesFrom() & GlobalFunction::withBodyFrom()" + "nikic/php-parser": "to use ClassType::from(withBodies: true) & ClassType::fromCode()" }, "autoload": { - "classmap": ["src/"] + "classmap": ["src/"], + "psr-4": { + "Nette\\": "src" + } }, "minimum-stability": "dev", "scripts": { - "phpstan": "phpstan analyse --level 5 --configuration tests/phpstan.neon src", + "phpstan": "phpstan analyse", "tester": "tester tests -s" }, "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "4.2-dev" } } } diff --git a/contributing.md b/contributing.md deleted file mode 100644 index 184152c0..00000000 --- a/contributing.md +++ /dev/null @@ -1,33 +0,0 @@ -How to contribute & use the issue tracker -========================================= - -Nette welcomes your contributions. There are several ways to help out: - -* Create an issue on GitHub, if you have found a bug -* Write test cases for open bug issues -* Write fixes for open bug/feature issues, preferably with test cases included -* Contribute to the [documentation](https://nette.org/en/writing) - -Issues ------- - -Please **do not use the issue tracker to ask questions**. We will be happy to help you -on [Nette forum](https://forum.nette.org) or chat with us on [Gitter](https://gitter.im/nette/nette). - -A good bug report shouldn't leave others needing to chase you up for more -information. Please try to be as detailed as possible in your report. - -**Feature requests** are welcome. But take a moment to find out whether your idea -fits with the scope and aims of the project. It's up to *you* to make a strong -case to convince the project's developers of the merits of this feature. - -Contributing ------------- - -If you'd like to contribute, please take a moment to read [the contributing guide](https://nette.org/en/contributing). - -The best way to propose a feature is to discuss your ideas on [Nette forum](https://forum.nette.org) before implementing them. - -Please do not fix whitespace, format code, or make a purely cosmetic patch. - -Thanks! :heart: diff --git a/ncs.php b/ncs.php new file mode 100644 index 00000000..9c743bdd --- /dev/null +++ b/ncs.php @@ -0,0 +1,13 @@ + false, +]; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 00000000..8a91e267 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,271 @@ +parameters: + ignoreErrors: + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Attribute \.\.\.\$attrs\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/ClassLike.php + + - + message: '#^Parameter \#1 \$class of method Nette\\PhpGenerator\\Printer\:\:printClass\(\) expects Nette\\PhpGenerator\\ClassType\|Nette\\PhpGenerator\\EnumType\|Nette\\PhpGenerator\\InterfaceType\|Nette\\PhpGenerator\\TraitType, \$this\(Nette\\PhpGenerator\\ClassLike\) given\.$#' + identifier: argument.type + count: 1 + path: src/PhpGenerator/ClassLike.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Constant \.\.\.\$consts\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/ClassType.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Method \.\.\.\$methods\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/ClassType.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Property \.\.\.\$props\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/ClassType.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\TraitUse \.\.\.\$traits\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/ClassType.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Attribute \.\.\.\$attrs\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/Closure.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Parameter \.\.\.\$uses\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/Closure.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Parameter \.\.\.\$val\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/Closure.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Attribute \.\.\.\$attrs\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/Constant.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Attribute \.\.\.\$attrs\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/EnumCase.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Constant \.\.\.\$consts\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/EnumType.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\EnumCase \.\.\.\$cases\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/EnumType.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Method \.\.\.\$methods\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/EnumType.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\TraitUse \.\.\.\$traits\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/EnumType.php + + - + message: '#^Call to an undefined method Nette\\PhpGenerator\\ClassLike\:\:addConstant\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/PhpGenerator/Extractor.php + + - + message: '#^Call to an undefined method Nette\\PhpGenerator\\ClassLike\:\:addMethod\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/PhpGenerator/Extractor.php + + - + message: '#^Call to an undefined method Nette\\PhpGenerator\\ClassLike\:\:addProperty\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/PhpGenerator/Extractor.php + + - + message: '#^Call to an undefined method Nette\\PhpGenerator\\ClassLike\:\:addTrait\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/PhpGenerator/Extractor.php + + - + message: '#^Parameter \#1 \$class of method Nette\\PhpGenerator\\Extractor\:\:addEnumCaseToClass\(\) expects Nette\\PhpGenerator\\EnumType, Nette\\PhpGenerator\\ClassLike given\.$#' + identifier: argument.type + count: 1 + path: src/PhpGenerator/Extractor.php + + - + message: '#^Variable \$trait might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: src/PhpGenerator/Extractor.php + + - + message: '#^Method Nette\\PhpGenerator\\Factory\:\:getAttributes\(\) has parameter \$from with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: src/PhpGenerator/Factory.php + + - + message: '#^Variable \$bodies on left side of \?\?\= always exists and is not nullable\.$#' + identifier: nullCoalesce.variable + count: 1 + path: src/PhpGenerator/Factory.php + + - + message: '#^Variable \$cache on left side of \?\?\= always exists and is not nullable\.$#' + identifier: nullCoalesce.variable + count: 1 + path: src/PhpGenerator/Factory.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Attribute \.\.\.\$attrs\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/GlobalFunction.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Parameter \.\.\.\$val\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/GlobalFunction.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Constant \.\.\.\$consts\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/InterfaceType.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Method \.\.\.\$methods\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/InterfaceType.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Property \.\.\.\$props\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/InterfaceType.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Attribute \.\.\.\$attrs\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/Method.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Parameter \.\.\.\$val\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/Method.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Attribute \.\.\.\$attrs\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/Parameter.php + + - + message: '#^Instanceof between Nette\\PhpGenerator\\EnumType and Nette\\PhpGenerator\\EnumType will always evaluate to true\.$#' + identifier: instanceof.alwaysTrue + count: 1 + path: src/PhpGenerator/Printer.php + + - + message: '#^Method Nette\\PhpGenerator\\Printer\:\:printDocComment\(\) has parameter \$commentable with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: src/PhpGenerator/Printer.php + + - + message: '#^Parameter \#2 \$array of function implode expects array\, array\ given\.$#' + identifier: argument.type + count: 2 + path: src/PhpGenerator/Printer.php + + - + message: '#^Result of \|\| is always true\.$#' + identifier: booleanOr.alwaysTrue + count: 1 + path: src/PhpGenerator/Printer.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\PropertyHook \.\.\.\$hooks\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/PromotedParameter.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Attribute \.\.\.\$attrs\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/Property.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\PropertyHook \.\.\.\$hooks\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/Property.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Attribute \.\.\.\$attrs\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/PropertyHook.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Parameter \.\.\.\$val\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/PropertyHook.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Constant \.\.\.\$consts\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/TraitType.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Method \.\.\.\$methods\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/TraitType.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\Property \.\.\.\$props\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/TraitType.php + + - + message: '#^Expression "\(function \(\\Nette\\PhpGenerator\\TraitUse \.\.\.\$traits\) \{…" on a separate line does not do anything\.$#' + identifier: expr.resultUnused + count: 1 + path: src/PhpGenerator/TraitType.php diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 00000000..bc3a10d6 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,8 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 6 + + paths: + - src diff --git a/readme.md b/readme.md index 524fe2f2..bb7fb17a 100644 --- a/readme.md +++ b/readme.md @@ -1,42 +1,50 @@ -Nette PHP Generator -=================== +[![Nette PHP Generator](https://github.com/nette/php-generator/assets/194960/8a2c83bd-daea-475f-994c-9c951de88501)](https://doc.nette.org/en/php-generator) -[![Downloads this Month](https://img.shields.io/packagist/dm/nette/php-generator.svg)](https://packagist.org/packages/nette/php-generator) -[![Build Status](https://travis-ci.org/nette/php-generator.svg?branch=master)](https://travis-ci.org/nette/php-generator) -[![Coverage Status](https://coveralls.io/repos/github/nette/php-generator/badge.svg?branch=master&v=1)](https://coveralls.io/github/nette/php-generator?branch=master) -[![Latest Stable Version](https://poser.pugx.org/nette/php-generator/v/stable)](https://github.com/nette/php-generator/releases) -[![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/nette/php-generator/blob/master/license.md) +[![Latest Stable Version](https://poser.pugx.org/nette/php-generator/v/stable)](https://github.com/nette/php-generator/releases) [![Downloads this Month](https://img.shields.io/packagist/dm/nette/php-generator.svg)](https://packagist.org/packages/nette/php-generator) +  -Introduction ------------- +Are you looking for a tool to generate PHP code for [classes](#classes), [functions](#global-functions), or complete [PHP files](#php-files)? -Generate PHP code, classes, namespaces etc. with a simple programmatical API. +

-Documentation can be found on the [website](https://doc.nette.org/php-generator). +✅ Supports all the latest PHP features like [property hooks](#property-hooks), [enums](#enums), [attributes](#attributes), etc.
+✅ Allows you to easily modify [existing classes](#generating-from-existing-ones)
+✅ Output compliant with [PSR-12 / PER coding style](#printer-and-psr-compliance)
+✅ Highly mature, stable, and widely used library -If you like Nette, **[please make a donation now](https://nette.org/donate)**. Thank you! +

Installation ------------ -The recommended way to install is via Composer: +Download and install the library using the [Composer](https://doc.nette.org/en/best-practices/composer) tool: -``` +```shell composer require nette/php-generator ``` -- PhpGenerator 3.2 – 3.4 is compatible with PHP 7.1 to 7.4 -- PhpGenerator 3.1 is compatible with PHP 7.1 to 7.3 -- PhpGenerator 3.0 is compatible with PHP 7.0 to 7.3 -- PhpGenerator 2.6 is compatible with PHP 5.6 to 7.3 +PhpGenerator 4.2 is compatible with PHP 8.1 to 8.5. Documentation can be found on the [library's website](https://doc.nette.org/php-generator). +  -Usage ------ +[Support Me](https://github.com/sponsors/dg) +-------------------------------------------- + +Do you like PHP Generator? Are you looking forward to the new features? + +[![Buy me a coffee](https://files.nette.org/icons/donation-3.svg)](https://github.com/sponsors/dg) + +Thank you! -Usage is very easy. Let's start with a straightforward example of generating class: +  + +Classes +------- + +Let's start with an example of creating a class using [ClassType](https://api.nette.org/php-generator/master/Nette/PhpGenerator/ClassType.html): ```php $class = new Nette\PhpGenerator\ClassType('Demo'); @@ -45,129 +53,118 @@ $class ->setFinal() ->setExtends(ParentClass::class) ->addImplement(Countable::class) - ->addTrait(Nette\SmartObject::class) - ->addComment("Description of class.\nSecond line\n") + ->addComment("Class description.\nSecond line\n") ->addComment('@property-read Nette\Forms\Form $form'); -// to generate PHP code simply cast to string or use echo: +// generate code simply by typecasting to string or using echo: echo $class; - -// or use printer: -$printer = new Nette\PhpGenerator\Printer; -echo $printer->printClass($class); ``` -It will render this result: +This will return: ```php /** - * Description of class. + * Class description * Second line * * @property-read Nette\Forms\Form $form */ final class Demo extends ParentClass implements Countable { - use Nette\SmartObject; } ``` -We can add constants and properties: +To generate the code, you can also use a so-called printer, which, unlike `echo $class`, can be [further configured](#printer-and-psr-compliance): ```php -$class->addConstant('ID', 123); +$printer = new Nette\PhpGenerator\Printer; +echo $printer->printClass($class); +``` + +You can add constants (class [Constant](https://api.nette.org/php-generator/master/Nette/PhpGenerator/Constant.html)) and properties (class [Property](https://api.nette.org/php-generator/master/Nette/PhpGenerator/Property.html)): + +```php +$class->addConstant('ID', 123) + ->setProtected() // constant visibility + ->setType('int') + ->setFinal(); $class->addProperty('items', [1, 2, 3]) - ->setPrivate() + ->setPrivate() // or setVisibility('private') ->setStatic() ->addComment('@var int[]'); + +$class->addProperty('list') + ->setType('?array') + ->setInitialized(); // outputs '= null' ``` -It generates: +This will generate: ```php -const ID = 123; +final protected const int ID = 123; /** @var int[] */ private static $items = [1, 2, 3]; + +public ?array $list = null; ``` -And we can add methods with parameters: +And you can add [methods](#method-and-function-signatures): ```php $method = $class->addMethod('count') ->addComment('Count it.') - ->addComment('@return int') ->setFinal() ->setProtected() + ->setReturnType('?int') // return types for methods ->setBody('return count($items ?: $this->items);'); $method->addParameter('items', []) // $items = [] - ->setReference() // &$items = [] - ->setType('array'); // array &$items = [] + ->setReference() // &$items = [] + ->setType('array'); // array &$items = [] ``` -It results in: +The result is: ```php /** * Count it. - * @return int */ -final protected function count(array &$items = []) +final protected function count(array &$items = []): ?int { return count($items ?: $this->items); } ``` -If the property, constant, method or parameter already exist, it will be overwritten. - -Members can be removed using `removeProperty()`, `removeConstant()`, `removeMethod()` or `removeParameter()`. - -PHP Generator supports all new PHP 7.3 and 7.4 features: +Promoted parameters introduced in PHP 8.0 can be passed to the constructor: ```php -use Nette\PhpGenerator\Type; - -$class = new Nette\PhpGenerator\ClassType('Demo'); - -$class->addConstant('ID', 123) - ->setPrivate(); // constant visiblity - -$class->addProperty('items') - ->setType(Type::ARRAY) // typed properites - ->setNullable() - ->setInitialized(); - -$method = $class->addMethod('getValue') - ->setReturnType(Type::INT) // method return type - ->setReturnNullable() // nullable return type - ->setBody('return count($this->items);'); +$method = $class->addMethod('__construct'); +$method->addPromotedParameter('name'); +$method->addPromotedParameter('args', []) + ->setPrivate(); +``` -$method->addParameter('id') - ->setType(Type::ARRAY) // scalar type hint - ->setNullable(); // nullable type hint +The result is: -echo $class; +```php +public function __construct( + public $name, + private $args = [], +) { +} ``` -Result: +Readonly properties and classes be marked using the `setReadOnly()` function. -```php -class Demo -{ - private const ID = 123; +------ - public ?array $items = null; +If an added property, constant, method, or parameter already exists, an exception is thrown. - public function getValue(?int $id): ?int - { - return count($this->items); - } -} -``` +Class members can be removed using `removeProperty()`, `removeConstant()`, `removeMethod()`, or `removeParameter()`. -You can also add existing `Method`, `Property` or `Constant` objects to the class: +You can also add existing `Method`, `Property`, or `Constant` objects to the class: ```php $method = new Nette\PhpGenerator\Method('getHandle'); @@ -180,7 +177,7 @@ $class = (new Nette\PhpGenerator\ClassType('Demo')) ->addMember($const); ``` -You can clone existing methods, properties and constants with a different name using `cloneWithName()`: +You can also clone existing methods, properties, and constants under a different name using `cloneWithName()`: ```php $methodCount = $class->getMethod('count'); @@ -188,84 +185,86 @@ $methodRecount = $methodCount->cloneWithName('recount'); $class->addMember($methodRecount); ``` -Tabs versus spaces ------------------- +  -The generated code uses tabs for indentation. If you want to have the output compatible with PSR-2 or PSR-12, use `PsrPrinter`: +Interfaces or Traits +-------------------- -```php -$class = new Nette\PhpGenerator\ClassType('Demo'); -// ... +You can create interfaces and traits (classes [InterfaceType](https://api.nette.org/php-generator/master/Nette/PhpGenerator/InterfaceType.html) and [TraitType](https://api.nette.org/php-generator/master/Nette/PhpGenerator/TraitType.html)): -$printer = new Nette\PhpGenerator\PsrPrinter; -echo $printer->printClass($class); // 4 spaces indentation +```php +$interface = new Nette\PhpGenerator\InterfaceType('MyInterface'); +$trait = new Nette\PhpGenerator\TraitType('MyTrait'); ``` -It can be used also for functions, closures, namespaces etc. - - -Literals --------- - -You can pass any PHP code to property or parameter default values via `Literal`: +Using a trait: ```php -use Nette\PhpGenerator\Literal; - $class = new Nette\PhpGenerator\ClassType('Demo'); - -$class->addProperty('foo', new Literal('Iterator::SELF_FIRST')); - -$class->addMethod('bar') - ->addParameter('id', new Literal('1 + 2')); - +$class->addTrait('SmartObject'); +$class->addTrait('MyTrait') + ->addResolution('sayHello as protected') + ->addComment('@use MyTrait'); echo $class; ``` -Result: +The result is: ```php class Demo { - public $foo = Iterator::SELF_FIRST; - - public function bar($id = 1 + 2) - { + use SmartObject; + /** @use MyTrait */ + use MyTrait { + sayHello as protected; } } ``` -Interface or Trait ------------------- +  -```php -$class = new Nette\PhpGenerator\ClassType('DemoInterface'); -$class->setInterface(); -// or $class->setTrait(); -``` +Enums +----- -Trait Resolutions and Visibility --------------------------------- +You can easily create enums introduced in PHP 8.1 like this (class [EnumType](https://api.nette.org/php-generator/master/Nette/PhpGenerator/EnumType.html)): ```php -$class = new Nette\PhpGenerator\ClassType('Demo'); -$class->addTrait('SmartObject', ['sayHello as protected']); -echo $class; +$enum = new Nette\PhpGenerator\EnumType('Suit'); +$enum->addCase('Clubs'); +$enum->addCase('Diamonds'); +$enum->addCase('Hearts'); +$enum->addCase('Spades'); + +echo $enum; ``` -Result: +The result is: ```php -class Demo +enum Suit { - use SmartObject { - sayHello as protected; - } + case Clubs; + case Diamonds; + case Hearts; + case Spades; } ``` -Anonymous Class ---------------- +You can also define scalar equivalents and create a "backed" enum: + +```php +$enum->addCase('Clubs', '♣'); +$enum->addCase('Diamonds', '♦'); +``` + +For each *case*, you can add a comment or [attributes](#attributes) using `addComment()` or `addAttribute()`. + +  + +Anonymous Classes +----------------- + +Pass `null` as the name, and you have an anonymous class: ```php $class = new Nette\PhpGenerator\ClassType(null); @@ -275,7 +274,7 @@ $class->addMethod('__construct') echo '$obj = new class ($val) ' . $class . ';'; ``` -Result: +The result is: ```php $obj = new class ($val) { @@ -286,10 +285,12 @@ $obj = new class ($val) { }; ``` -Global Function ---------------- +  + +Global Functions +---------------- -Code of function: +The code for functions is generated by the class [GlobalFunction](https://api.nette.org/php-generator/master/Nette/PhpGenerator/GlobalFunction.html): ```php $function = new Nette\PhpGenerator\GlobalFunction('foo'); @@ -298,11 +299,11 @@ $function->addParameter('a'); $function->addParameter('b'); echo $function; -// or use PsrPrinter for output compatible with PSR-2 / PSR-12 +// or use the PsrPrinter for output compliant with PSR-2 / PSR-12 / PER // echo (new Nette\PhpGenerator\PsrPrinter)->printFunction($function); ``` -Result: +The result is: ```php function foo($a, $b) @@ -311,10 +312,12 @@ function foo($a, $b) } ``` -Closure -------- +  + +Anonymous Functions +------------------- -Code of closure: +The code for anonymous functions is generated by the class [Closure](https://api.nette.org/php-generator/master/Nette/PhpGenerator/Closure.html): ```php $closure = new Nette\PhpGenerator\Closure; @@ -325,11 +328,11 @@ $closure->addUse('c') ->setReference(); echo $closure; -// or use PsrPrinter for output compatible with PSR-2 / PSR-12 +// or use the PsrPrinter for output compliant with PSR-2 / PSR-12 / PER // echo (new Nette\PhpGenerator\PsrPrinter)->printClosure($closure); ``` -Result: +The result is: ```php function ($a, $b) use (&$c) { @@ -337,52 +340,115 @@ function ($a, $b) use (&$c) { } ``` -Arrow function --------------- +  + +Short Arrow Functions +--------------------- -You can also print closure as arrow function using printer: +You can also output a short anonymous function using the printer: ```php $closure = new Nette\PhpGenerator\Closure; -$closure->setBody('return $a + $b;'); +$closure->setBody('$a + $b'); $closure->addParameter('a'); $closure->addParameter('b'); -// or use PsrPrinter for output compatible with PSR-2 / PSR-12 echo (new Nette\PhpGenerator\Printer)->printArrowFunction($closure); ``` -Result: +The result is: + +```php +fn($a, $b) => $a + $b +``` + +  + +Method and Function Signatures +------------------------------ + +Methods are represented by the class [Method](https://api.nette.org/php-generator/master/Nette/PhpGenerator/Method.html). You can set visibility, return value, add comments, [attributes](#attributes), etc.: ```php -fn ($a, $b) => $a + $b; +$method = $class->addMethod('count') + ->addComment('Count it.') + ->setFinal() + ->setProtected() + ->setReturnType('?int'); ``` -Method and Function Body Generator ----------------------------------- +Individual parameters are represented by the class [Parameter](https://api.nette.org/php-generator/master/Nette/PhpGenerator/Parameter.html). Again, you can set all conceivable properties: -You can use special placeholders for handy way to generate method or function body. +```php +$method->addParameter('items', []) // $items = [] + ->setReference() // &$items = [] + ->setType('array'); // array &$items = [] + +// function count(&$items = []) +``` -Simple placeholders: +To define the so-called variadics parameters (or also the splat, spread, ellipsis, unpacking or three dots operator), use `setVariadic()`: + +```php +$method = $class->addMethod('count'); +$method->setVariadic(true); +$method->addParameter('items'); +``` + +This generates: + +```php +function count(...$items) +{ +} +``` + +  + +Method and Function Bodies +-------------------------- + +The body can be passed all at once to the `setBody()` method or gradually (line by line) by repeatedly calling `addBody()`: + +```php +$function = new Nette\PhpGenerator\GlobalFunction('foo'); +$function->addBody('$a = rand(10, 20);'); +$function->addBody('return $a;'); +echo $function; +``` + +The result is: + +```php +function foo() +{ + $a = rand(10, 20); + return $a; +} +``` + +You can use special placeholders for easy variable insertion. + +Simple placeholders `?` ```php $str = 'any string'; $num = 3; $function = new Nette\PhpGenerator\GlobalFunction('foo'); -$function->addBody('return strlen(?, ?);', [$str, $num]); +$function->addBody('return substr(?, ?);', [$str, $num]); echo $function; ``` -Result: +The result is: ```php function foo() { - return strlen('any string', 3); + return substr('any string', 3); } ``` -Variadic placeholder: +Placeholder for variadic `...?` ```php $items = [1, 2, 3]; @@ -391,7 +457,7 @@ $function->setBody('myfunc(...?);', [$items]); echo $function; ``` -Result: +The result is: ```php function foo() @@ -400,7 +466,16 @@ function foo() } ``` -Escape placeholder using slash: +You can also use named parameters for PHP 8 with `...?:` + +```php +$items = ['foo' => 1, 'bar' => true]; +$function->setBody('myfunc(...?:);', [$items]); + +// myfunc(foo: 1, bar: true); +``` + +The placeholder is escaped with a backslash `\?` ```php $num = 3; @@ -410,7 +485,7 @@ $function->addBody('return $a \? 10 : ?;', [$num]); echo $function; ``` -Result: +The result is: ```php function foo($a) @@ -419,53 +494,329 @@ function foo($a) } ``` +  + +Printer and PSR Compliance +-------------------------- + +The [Printer](https://api.nette.org/php-generator/master/Nette/PhpGenerator/Printer.html) class is used for generating PHP code: + +```php +$class = new Nette\PhpGenerator\ClassType('Demo'); +// ... + +$printer = new Nette\PhpGenerator\Printer; +echo $printer->printClass($class); // same as: echo $class +``` + +It can generate code for all other elements, offering methods like `printFunction()`, `printNamespace()`, etc. + +There's also the `PsrPrinter` class, which outputs in accordance with PSR-2 / PSR-12 / PER coding style: + +```php +$printer = new Nette\PhpGenerator\PsrPrinter; +echo $printer->printClass($class); +``` + +Need custom behavior? Create your own version by inheriting the `Printer` class. You can reconfigure these variables: + +```php +class MyPrinter extends Nette\PhpGenerator\Printer +{ + // length of the line after which the line will break + public int $wrapLength = 120; + // indentation character, can be replaced with a sequence of spaces + public string $indentation = "\t"; + // number of blank lines between properties + public int $linesBetweenProperties = 0; + // number of blank lines between methods + public int $linesBetweenMethods = 2; + // number of blank lines between 'use statements' groups for classes, functions, and constants + public int $linesBetweenUseTypes = 0; + // position of the opening curly brace for functions and methods + public bool $bracesOnNextLine = true; + // place one parameter on one line, even if it has an attribute or is supported + public bool $singleParameterOnOneLine = false; + // omits namespaces that do not contain any class or function + public bool $omitEmptyNamespaces = true; + // separator between the right parenthesis and return type of functions and methods + public string $returnTypeColon = ': '; +} +``` + +How and why does the standard `Printer` differ from `PsrPrinter`? Why isn't there just one printer, the `PsrPrinter`, in the package? + +The standard `Printer` formats the code as we do throughout Nette. Since Nette was established much earlier than PSR, and also because PSR took years to deliver standards on time, sometimes even several years after introducing a new feature in PHP, it resulted in a [coding standard](https://doc.nette.org/en/contributing/coding-standard) that differs in a few minor aspects. +The major difference is the use of tabs instead of spaces. We know that by using tabs in our projects, we allow for width customization, which is [essential for people with visual impairments](https://doc.nette.org/en/contributing/coding-standard#toc-tabs-instead-of-spaces). +An example of a minor difference is placing the curly brace on a separate line for functions and methods, always. The PSR recommendation seems illogical to us and [leads to reduced code clarity](https://doc.nette.org/en/contributing/coding-standard#toc-wrapping-and-braces). + +  + +Types +----- + +Every type or union/intersection type can be passed as a string; you can also use predefined constants for native types: + +```php +use Nette\PhpGenerator\Type; + +$member->setType('array'); // or Type::Array; +$member->setType('?array'); // or Type::nullable(Type::Array); +$member->setType('array|string'); // or Type::union(Type::Array, Type::String) +$member->setType('Foo&Bar'); // or Type::intersection(Foo::class, Bar::class) +$member->setType(null); // removes the type +``` + +The same applies to the `setReturnType()` method. + +  + +Literals +-------- + +Using `Literal`, you can pass any PHP code, for example, for default property values or parameters, etc: + +```php +use Nette\PhpGenerator\Literal; + +$class = new Nette\PhpGenerator\ClassType('Demo'); + +$class->addProperty('foo', new Literal('Iterator::SELF_FIRST')); + +$class->addMethod('bar') + ->addParameter('id', new Literal('1 + 2')); + +echo $class; +``` + +Result: + +```php +class Demo +{ + public $foo = Iterator::SELF_FIRST; + + public function bar($id = 1 + 2) + { + } +} +``` + +You can also pass parameters to `Literal` and have them formatted into valid PHP code using [placeholders](#method-and-function-bodies): + +```php +new Literal('substr(?, ?)', [$a, $b]); +// generates for example: substr('hello', 5); +``` + +A literal representing the creation of a new object can easily be generated using the `new` method: + +```php +Literal::new(Demo::class, [$a, 'foo' => $b]); +// generates for example: new Demo(10, foo: 20) +``` + +  + +Attributes +---------- + +With PHP 8, you can add attributes to all classes, methods, properties, constants, enum cases, functions, closures, and parameters. You can also use [literals](#literals) as parameter values. + +```php +$class = new Nette\PhpGenerator\ClassType('Demo'); +$class->addAttribute('Table', [ + 'name' => 'user', + 'constraints' => [ + Literal::new('UniqueConstraint', ['name' => 'ean', 'columns' => ['ean']]), + ], +]); + +$class->addProperty('list') + ->addAttribute('Deprecated'); + +$method = $class->addMethod('count') + ->addAttribute('Foo\Cached', ['mode' => true]); + +$method->addParameter('items') + ->addAttribute('Bar'); + +echo $class; +``` + +Result: + +```php +#[Table(name: 'user', constraints: [new UniqueConstraint(name: 'ean', columns: ['ean'])])] +class Demo +{ + #[Deprecated] + public $list; + + + #[Foo\Cached(mode: true)] + public function count( + #[Bar] + $items, + ) { + } +} +``` + +  + +Property Hooks +-------------- + +You can also define property hooks (represented by the class [PropertyHook](https://api.nette.org/php-generator/master/Nette/PhpGenerator/PropertyHook.html)) for get and set operations, a feature introduced in PHP 8.4: + +```php +$class = new Nette\PhpGenerator\ClassType('Demo'); +$prop = $class->addProperty('firstName') + ->setType('string'); + +$prop->addHook('set', 'strtolower($value)') + ->addParameter('value') + ->setType('string'); + +$prop->addHook('get') + ->setBody('return ucfirst($this->firstName);'); + +echo $class; +``` + +This generates: + +```php +class Demo +{ + public string $firstName { + set(string $value) => strtolower($value); + get { + return ucfirst($this->firstName); + } + } +} +``` + +Properties and property hooks can be abstract or final: + +```php +$class->addProperty('id') + ->setType('int') + ->addHook('get') + ->setAbstract(); + +$class->addProperty('role') + ->setType('string') + ->addHook('set', 'strtolower($value)') + ->setFinal(); +``` + +  + +Asymmetric Visibility +--------------------- + +PHP 8.4 introduces asymmetric visibility for properties. You can set different access levels for reading and writing. +The visibility can be set using either the `setVisibility()` method with two parameters, or by using `setPublic()`, `setProtected()`, or `setPrivate()` with the `mode` parameter that specifies whether the visibility applies to getting or setting the property. The default mode is 'get'. + +```php +$class = new Nette\PhpGenerator\ClassType('Demo'); + +$class->addProperty('name') + ->setType('string') + ->setVisibility('public', 'private'); // public for read, private for write + +$class->addProperty('id') + ->setType('int') + ->setProtected('set'); // protected for write + +echo $class; +``` + +This generates: + +```php +class Demo +{ + public private(set) string $name; + + protected(set) int $id; +} +``` + +  + Namespace --------- -Classes, traits and interfaces (hereinafter classes) can be grouped into namespaces: +Classes, traits, interfaces, and enums (hereafter referred to as classes) can be grouped into namespaces represented by the [PhpNamespace](https://api.nette.org/php-generator/master/Nette/PhpGenerator/PhpNamespace.html) class: ```php $namespace = new Nette\PhpGenerator\PhpNamespace('Foo'); +// create new classes in the namespace $class = $namespace->addClass('Task'); $interface = $namespace->addInterface('Countable'); $trait = $namespace->addTrait('NameAware'); -// or +// or insert an existing class into the namespace $class = new Nette\PhpGenerator\ClassType('Task'); $namespace->add($class); ``` -If the class already exists, it will be overwritten. +If the class already exists, an exception is thrown. -You can define use-statements: +You can define use clauses: ```php -$namespace->addUse(Http\Request::class); // use Http\Request; -$namespace->addUse(Http\Request::class, 'HttpReq'); // use Http\Request as HttpReq; +// use Http\Request; +$namespace->addUse(Http\Request::class); +// use Http\Request as HttpReq; +$namespace->addUse(Http\Request::class, 'HttpReq'); +// use function iter\range; +$namespace->addUseFunction('iter\range'); ``` -**IMPORTANT NOTE:** when the class is part of the namespace, it is rendered slightly differently: all types (ie. type hints, return types, parent class name, -implemented interfaces and used traits) are automatically *resolved* (unless you turn it off, see below). -It means that you have to **use full class names** in definitions and they will be replaced -with aliases (according to the use-statements) or fully qualified names in the resulting code: +To simplify a fully qualified class, function, or constant name based on defined aliases, use the `simplifyName` method: + +```php +echo $namespace->simplifyName('Foo\Bar'); // 'Bar', because 'Foo' is the current namespace +echo $namespace->simplifyName('iter\range', $namespace::NameFunction); // 'range', due to the defined use-statement +``` + +Conversely, you can convert a simplified class, function, or constant name back to a fully qualified name using the `resolveName` method: + +```php +echo $namespace->resolveName('Bar'); // 'Foo\Bar' +echo $namespace->resolveName('range', $namespace::NameFunction); // 'iter\range' +``` + +  + +Class Names Resolving +--------------------- + +**When a class is part of a namespace, it's rendered slightly differently:** all types (e.g., type hints, return types, parent class name, implemented interfaces, used traits, and attributes) are automatically *resolved* (unless you turn it off, see below). +This means you must use **fully qualified class names** in definitions, and they will be replaced with aliases (based on use clauses) or fully qualified names in the resulting code: ```php $namespace = new Nette\PhpGenerator\PhpNamespace('Foo'); $namespace->addUse('Bar\AliasedClass'); $class = $namespace->addClass('Demo'); -$class->addImplement('Foo\A') // it will resolve to A - ->addTrait('Bar\AliasedClass'); // it will resolve to AliasedClass +$class->addImplement('Foo\A') // will be simplified to A + ->addTrait('Bar\AliasedClass'); // will be simplified to AliasedClass $method = $class->addMethod('method'); -$method->addComment('@return ' . $namespace->unresolveName('Foo\D')); // in comments resolve manually +$method->addComment('@return ' . $namespace->simplifyType('Foo\D')); // we manually simplify in comments $method->addParameter('arg') - ->setType('Bar\OtherClass'); // it will resolve to \Bar\OtherClass + ->setType('Bar\OtherClass'); // will be translated to \Bar\OtherClass echo $namespace; -// or use PsrPrinter for output compatible with PSR-2 / PSR-12 +// or use the PsrPrinter for output in accordance with PSR-2 / PSR-12 / PER // echo (new Nette\PhpGenerator\PsrPrinter)->printNamespace($namespace); ``` @@ -497,23 +848,29 @@ $printer->setTypeResolving(false); echo $printer->printNamespace($namespace); ``` +  + PHP Files --------- -PHP files can contains multiple classes, namespaces and comments: +Classes, functions, and namespaces can be grouped into PHP files represented by the [PhpFile](https://api.nette.org/php-generator/master/Nette/PhpGenerator/PhpFile.html) class: ```php $file = new Nette\PhpGenerator\PhpFile; $file->addComment('This file is auto-generated.'); $file->setStrictTypes(); // adds declare(strict_types=1) -$namespace = $file->addNamespace('Foo'); -$class = $namespace->addClass('A'); -$class->addMethod('hello'); +$class = $file->addClass('Foo\A'); +$function = $file->addFunction('Foo\foo'); + +// or +// $namespace = $file->addNamespace('Foo'); +// $class = $namespace->addClass('A'); +// $function = $namespace->addFunction('foo'); echo $file; -// or use PsrPrinter for output compatible with PSR-2 / PSR-12 +// or use the PsrPrinter for output in accordance with PSR-2 / PSR-12 / PER // echo (new Nette\PhpGenerator\PsrPrinter)->printFile($file); ``` @@ -532,47 +889,120 @@ namespace Foo; class A { - public function hello() - { - } +} + +function foo() +{ } ``` -Generate using Reflection -------------------------- +**Please note:** No additional code can be added to the files outside of functions and classes. + +  + +Generating from Existing Ones +----------------------------- -Another common use case is to create class or method based on existing ones: +In addition to being able to model classes and functions using the API described above, you can also have them automatically generated using existing ones: ```php +// creates a class identical to the PDO class $class = Nette\PhpGenerator\ClassType::from(PDO::class); +// creates a function identical to the trim() function $function = Nette\PhpGenerator\GlobalFunction::from('trim'); +// creates a closure based on the provided one $closure = Nette\PhpGenerator\Closure::from( - function (stdClass $a, $b = null) {} + function (stdClass $a, $b = null) {}, ); ``` -Method bodies are empty by default. If you want to load them as well, use this way -(it requires `nikic/php-parser` to be installed): +By default, function and method bodies are empty. If you also want to load them, use this method +(requires the `nikic/php-parser` package to be installed): + +```php +$class = Nette\PhpGenerator\ClassType::from(Foo::class, withBodies: true); + +$function = Nette\PhpGenerator\GlobalFunction::from('foo', withBody: true); +``` + +  + +Loading from PHP Files +---------------------- + +You can also load functions, classes, interfaces, and enums directly from a string containing PHP code. For example, to create a `ClassType` object: + +```php +$class = Nette\PhpGenerator\ClassType::fromCode(<< + +Class Manipulator +----------------- + +The [ClassManipulator](https://api.nette.org/php-generator/master/Nette/PhpGenerator/ClassManipulator.html) class provides tools for manipulating classes. ```php -$class = Nette\PhpGenerator\ClassType::withBodiesFrom(MyClass::class); +$class = new Nette\PhpGenerator\ClassType('Demo'); +$manipulator = new Nette\PhpGenerator\ClassManipulator($class); +``` -$function = Nette\PhpGenerator\GlobalFunction::withBodyFrom('dump'); +The `inheritMethod()` method copies a method from a parent class or implemented interface into your class. This allows you to override the method or extend its signature: + +```php +$method = $manipulator->inheritMethod('bar'); +$method->setBody('...'); +``` + +The `inheritProperty()` method copies a property from a parent class into your class. This is useful when you want to have the same property in your class, but possibly with a different default value: + +```php +$property = $manipulator->inheritProperty('foo'); +$property->setValue('new value'); +``` + +The `implement()` method automatically implements all methods and properties from the given interface or abstract class: + +```php +$manipulator->implement(SomeInterface::class); +// Now your class implements SomeInterface and includes all its methods ``` +  -Variables dumper +Variable Dumping ---------------- -The Dumper returns a parsable PHP string representation of a variable. It provides a better function that you can use instead of `var_export()` -with more readable output. +The `Dumper` class converts a variable into parseable PHP code. It provides a better and clearer output than the standard `var_export()` function. ```php $dumper = new Nette\PhpGenerator\Dumper; $var = ['a', 'b', 123]; -echo $dumper->dump($var); // prints ['a', 'b', 123] +echo $dumper->dump($var); // outputs ['a', 'b', 123] ``` diff --git a/src/PhpGenerator/Attribute.php b/src/PhpGenerator/Attribute.php new file mode 100644 index 00000000..896f9473 --- /dev/null +++ b/src/PhpGenerator/Attribute.php @@ -0,0 +1,49 @@ +name = $name; + $this->args = $args; + } + + + public function getName(): string + { + return $this->name; + } + + + /** @return mixed[] */ + public function getArguments(): array + { + return $this->args; + } +} diff --git a/src/PhpGenerator/ClassLike.php b/src/PhpGenerator/ClassLike.php new file mode 100644 index 00000000..e9edcbe0 --- /dev/null +++ b/src/PhpGenerator/ClassLike.php @@ -0,0 +1,148 @@ +fromClassReflection(new \ReflectionClass($class), $withBodies); + + if (!$instance instanceof static) { + $class = is_object($class) ? $class::class : $class; + throw new Nette\InvalidArgumentException("$class cannot be represented with " . static::class . '. Call ' . $instance::class . '::' . __FUNCTION__ . '() or ' . __METHOD__ . '() instead.'); + } + + return $instance; + } + + + public static function fromCode(string $code): static + { + $instance = (new Factory) + ->fromClassCode($code); + + if (!$instance instanceof static) { + throw new Nette\InvalidArgumentException('Provided code cannot be represented with ' . static::class . '. Call ' . $instance::class . '::' . __FUNCTION__ . '() or ' . __METHOD__ . '() instead.'); + } + + return $instance; + } + + + public function __construct(string $name, ?PhpNamespace $namespace = null) + { + $this->setName($name); + $this->namespace = $namespace; + } + + + public function __toString(): string + { + return (new Printer)->printClass($this, $this->namespace); + } + + + /** @deprecated an object can be in multiple namespaces */ + public function getNamespace(): ?PhpNamespace + { + return $this->namespace; + } + + + public function setName(?string $name): static + { + if ($name !== null && (!Helpers::isIdentifier($name) || isset(Helpers::Keywords[strtolower($name)]))) { + throw new Nette\InvalidArgumentException("Value '$name' is not valid class name."); + } + + $this->name = $name; + return $this; + } + + + public function getName(): ?string + { + return $this->name; + } + + + public function isClass(): bool + { + return $this instanceof ClassType; + } + + + public function isInterface(): bool + { + return $this instanceof InterfaceType; + } + + + public function isTrait(): bool + { + return $this instanceof TraitType; + } + + + public function isEnum(): bool + { + return $this instanceof EnumType; + } + + + /** @param string[] $names */ + protected function validateNames(array $names): void + { + foreach ($names as $name) { + if (!Helpers::isNamespaceIdentifier($name, allowLeadingSlash: true)) { + throw new Nette\InvalidArgumentException("Value '$name' is not valid class name."); + } + } + } + + + public function validate(): void + { + } + + + public function __clone(): void + { + $this->attributes = array_map(fn($attr) => clone $attr, $this->attributes); + } +} diff --git a/src/PhpGenerator/ClassManipulator.php b/src/PhpGenerator/ClassManipulator.php new file mode 100644 index 00000000..08ebcaf8 --- /dev/null +++ b/src/PhpGenerator/ClassManipulator.php @@ -0,0 +1,124 @@ +class->hasProperty($name)) { + return $returnIfExists + ? $this->class->getProperty($name) + : throw new Nette\InvalidStateException("Cannot inherit property '$name', because it already exists."); + } + + $parents = [...(array) $this->class->getExtends(), ...$this->class->getImplements()] + ?: throw new Nette\InvalidStateException("Class '{$this->class->getName()}' has neither setExtends() nor setImplements() set."); + + foreach ($parents as $parent) { + try { + $rp = new \ReflectionProperty($parent, $name); + } catch (\ReflectionException) { + continue; + } + return $this->implementProperty($rp); + } + + throw new Nette\InvalidStateException("Property '$name' has not been found in any ancestor: " . implode(', ', $parents)); + } + + + /** + * Inherits method from parent class or interface. + */ + public function inheritMethod(string $name, bool $returnIfExists = false): Method + { + if ($this->class->hasMethod($name)) { + return $returnIfExists + ? $this->class->getMethod($name) + : throw new Nette\InvalidStateException("Cannot inherit method '$name', because it already exists."); + } + + $parents = [...(array) $this->class->getExtends(), ...$this->class->getImplements()] + ?: throw new Nette\InvalidStateException("Class '{$this->class->getName()}' has neither setExtends() nor setImplements() set."); + + foreach ($parents as $parent) { + try { + $rm = new \ReflectionMethod($parent, $name); + } catch (\ReflectionException) { + continue; + } + return $this->implementMethod($rm); + } + + throw new Nette\InvalidStateException("Method '$name' has not been found in any ancestor: " . implode(', ', $parents)); + } + + + /** + * Implements all methods from the given interface or abstract class. + */ + public function implement(string $name): void + { + $definition = new \ReflectionClass($name); + if ($definition->isInterface()) { + $this->class->addImplement($name); + } elseif ($definition->isAbstract()) { + $this->class->setExtends($name); + } else { + throw new Nette\InvalidArgumentException("'$name' is not an interface or abstract class."); + } + + foreach ($definition->getMethods() as $method) { + if (!$this->class->hasMethod($method->getName()) && $method->isAbstract()) { + $this->implementMethod($method); + } + } + + if (PHP_VERSION_ID >= 80400) { + foreach ($definition->getProperties() as $property) { + if (!$this->class->hasProperty($property->getName()) && $property->isAbstract()) { + $this->implementProperty($property); + } + } + } + } + + + private function implementMethod(\ReflectionMethod $rm): Method + { + $method = (new Factory)->fromMethodReflection($rm); + $method->setAbstract(false); + $this->class->addMember($method); + return $method; + } + + + private function implementProperty(\ReflectionProperty $rp): Property + { + $property = (new Factory)->fromPropertyReflection($rp); + $property->setHooks([])->setAbstract(false); + $this->class->addMember($property); + return $property; + } +} diff --git a/src/PhpGenerator/ClassType.php b/src/PhpGenerator/ClassType.php index a1358b02..320c5067 100644 --- a/src/PhpGenerator/ClassType.php +++ b/src/PhpGenerator/ClassType.php @@ -10,187 +10,47 @@ namespace Nette\PhpGenerator; use Nette; +use function array_diff, array_map, strtolower; /** - * Class/Interface/Trait description. - * - * @property Method[] $methods - * @property Property[] $properties + * Definition of a class with properties, methods, constants, traits and PHP attributes. */ -final class ClassType +final class ClassType extends ClassLike { - use Nette\SmartObject; - use Traits\CommentAware; + use Traits\ConstantsAware; + use Traits\MethodsAware; + use Traits\PropertiesAware; + use Traits\TraitsAware; + #[\Deprecated] public const TYPE_CLASS = 'class', TYPE_INTERFACE = 'interface', - TYPE_TRAIT = 'trait'; + TYPE_TRAIT = 'trait', + TYPE_ENUM = 'enum'; - public const - VISIBILITY_PUBLIC = 'public', - VISIBILITY_PROTECTED = 'protected', - VISIBILITY_PRIVATE = 'private'; - - /** @var PhpNamespace|null */ - private $namespace; - - /** @var string|null */ - private $name; - - /** @var string class|interface|trait */ - private $type = self::TYPE_CLASS; - - /** @var bool */ - private $final = false; - - /** @var bool */ - private $abstract = false; - - /** @var string|string[] */ - private $extends = []; + private bool $final = false; + private bool $abstract = false; + private ?string $extends = null; + private bool $readOnly = false; /** @var string[] */ - private $implements = []; - - /** @var array[] */ - private $traits = []; - - /** @var Constant[] name => Constant */ - private $consts = []; - - /** @var Property[] name => Property */ - private $properties = []; - - /** @var Method[] name => Method */ - private $methods = []; - - - /** - * @param string|object $class - */ - public static function from($class): self - { - return (new Factory)->fromClassReflection(new \ReflectionClass($class)); - } - - - /** - * @param string|object $class - */ - public static function withBodiesFrom($class): self - { - return (new Factory)->fromClassReflection(new \ReflectionClass($class), true); - } - - - public function __construct(string $name = null, PhpNamespace $namespace = null) - { - $this->setName($name); - $this->namespace = $namespace; - } + private array $implements = []; - public function __toString(): string + public function __construct(?string $name = null, ?PhpNamespace $namespace = null) { - try { - return (new Printer)->printClass($this, $this->namespace); - } catch (\Throwable $e) { - if (PHP_VERSION_ID >= 70400) { - throw $e; - } - trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR); - return ''; - } - } - - - /** @deprecated an object can be in multiple namespaces */ - public function getNamespace(): ?PhpNamespace - { - return $this->namespace; - } - - - /** @return static */ - public function setName(?string $name): self - { - if ($name !== null && !Helpers::isIdentifier($name)) { - throw new Nette\InvalidArgumentException("Value '$name' is not valid class name."); - } - $this->name = $name; - return $this; - } - - - public function getName(): ?string - { - return $this->name; - } - - - /** @return static */ - public function setClass(): self - { - $this->type = self::TYPE_CLASS; - return $this; - } - - - public function isClass(): bool - { - return $this->type === self::TYPE_CLASS; - } - - - /** @return static */ - public function setInterface(): self - { - $this->type = self::TYPE_INTERFACE; - return $this; - } - - - public function isInterface(): bool - { - return $this->type === self::TYPE_INTERFACE; - } - - - /** @return static */ - public function setTrait(): self - { - $this->type = self::TYPE_TRAIT; - return $this; - } - - - public function isTrait(): bool - { - return $this->type === self::TYPE_TRAIT; - } - - - /** @return static */ - public function setType(string $type): self - { - if (!in_array($type, [self::TYPE_CLASS, self::TYPE_INTERFACE, self::TYPE_TRAIT], true)) { - throw new Nette\InvalidArgumentException('Argument must be class|interface|trait.'); + if ($name === null) { + parent::__construct('foo', $namespace); + $this->setName(null); + } else { + parent::__construct($name, $namespace); } - $this->type = $type; - return $this; - } - - - public function getType(): string - { - return $this->type; } - /** @return static */ - public function setFinal(bool $state = true): self + public function setFinal(bool $state = true): static { $this->final = $state; return $this; @@ -203,8 +63,7 @@ public function isFinal(): bool } - /** @return static */ - public function setAbstract(bool $state = true): self + public function setAbstract(bool $state = true): static { $this->abstract = $state; return $this; @@ -217,327 +76,123 @@ public function isAbstract(): bool } - /** - * @param string|string[] $names - * @return static - */ - public function setExtends($names): self - { - if (!is_string($names) && !is_array($names)) { - throw new Nette\InvalidArgumentException('Argument must be string or string[].'); - } - $this->validateNames((array) $names); - $this->extends = $names; - return $this; - } - - - /** @return string|string[] */ - public function getExtends() - { - return $this->extends; - } - - - /** @return static */ - public function addExtend(string $name): self + public function setReadOnly(bool $state = true): static { - $this->validateNames([$name]); - $this->extends = (array) $this->extends; - $this->extends[] = $name; + $this->readOnly = $state; return $this; } - /** - * @param string[] $names - * @return static - */ - public function setImplements(array $names): self + public function isReadOnly(): bool { - $this->validateNames($names); - $this->implements = $names; - return $this; + return $this->readOnly; } - /** @return string[] */ - public function getImplements(): array + public function setExtends(?string $name): static { - return $this->implements; - } - - - /** @return static */ - public function addImplement(string $name): self - { - $this->validateNames([$name]); - $this->implements[] = $name; + if ($name) { + $this->validateNames([$name]); + } + $this->extends = $name; return $this; } - /** @return static */ - public function removeImplement(string $name): self + public function getExtends(): ?string { - $key = array_search($name, $this->implements, true); - if ($key !== false) { - unset($this->implements[$key]); - } - return $this; + return $this->extends; } /** * @param string[] $names - * @return static */ - public function setTraits(array $names): self + public function setImplements(array $names): static { $this->validateNames($names); - $this->traits = array_fill_keys($names, []); + $this->implements = $names; return $this; } /** @return string[] */ - public function getTraits(): array - { - return array_keys($this->traits); - } - - - /** @internal */ - public function getTraitResolutions(): array + public function getImplements(): array { - return $this->traits; + return $this->implements; } - /** @return static */ - public function addTrait(string $name, array $resolutions = []): self + public function addImplement(string $name): static { $this->validateNames([$name]); - $this->traits[$name] = $resolutions; - return $this; - } - - - /** @return static */ - public function removeTrait(string $name): self - { - unset($this->traits[$name]); - return $this; - } - - - /** - * @param Method|Property|Constant $member - * @return static - */ - public function addMember($member): self - { - if ($member instanceof Method) { - if ($this->isInterface()) { - $member->setBody(null); - } - $this->methods[$member->getName()] = $member; - - } elseif ($member instanceof Property) { - $this->properties[$member->getName()] = $member; - - } elseif ($member instanceof Constant) { - $this->consts[$member->getName()] = $member; - - } else { - throw new Nette\InvalidArgumentException('Argument must be Method|Property|Constant.'); - } - - return $this; - } - - - /** - * @param Constant[]|mixed[] $consts - * @return static - */ - public function setConstants(array $consts): self - { - $this->consts = []; - foreach ($consts as $k => $v) { - $const = $v instanceof Constant ? $v : (new Constant($k))->setValue($v); - $this->consts[$const->getName()] = $const; - } + $this->implements[] = $name; return $this; } - /** @return Constant[] */ - public function getConstants(): array + public function removeImplement(string $name): static { - return $this->consts; - } - - - public function addConstant(string $name, $value): Constant - { - return $this->consts[$name] = (new Constant($name))->setValue($value); - } - - - /** @return static */ - public function removeConstant(string $name): self - { - unset($this->consts[$name]); + $this->implements = array_diff($this->implements, [$name]); return $this; } - /** - * @param Property[] $props - * @return static - */ - public function setProperties(array $props): self + public function addMember(Method|Property|Constant|TraitUse $member, bool $overwrite = false): static { - $this->properties = []; - foreach ($props as $v) { - if (!$v instanceof Property) { - throw new Nette\InvalidArgumentException('Argument must be Nette\PhpGenerator\Property[].'); - } - $this->properties[$v->getName()] = $v; + $name = $member->getName(); + [$type, $n] = match (true) { + $member instanceof Constant => ['consts', $name], + $member instanceof Method => ['methods', strtolower($name)], + $member instanceof Property => ['properties', $name], + $member instanceof TraitUse => ['traits', $name], + }; + if (!$overwrite && isset($this->$type[$n])) { + throw new Nette\InvalidStateException("Cannot add member '$name', because it already exists."); } + $this->$type[$n] = $member; return $this; } - /** @return Property[] */ - public function getProperties(): array - { - return $this->properties; - } - - - public function getProperty(string $name): Property - { - if (!isset($this->properties[$name])) { - throw new Nette\InvalidArgumentException("Property '$name' not found."); - } - return $this->properties[$name]; - } - - - /** - * @param string $name without $ - */ - public function addProperty(string $name, $value = null): Property - { - return $this->properties[$name] = (new Property($name))->setValue($value); - } - - /** - * @param string $name without $ - * @return static + * @deprecated use ClassManipulator::inheritProperty() */ - public function removeProperty(string $name): self + public function inheritProperty(string $name, bool $returnIfExists = false): Property { - unset($this->properties[$name]); - return $this; - } - - - public function hasProperty(string $name): bool - { - return isset($this->properties[$name]); + return (new ClassManipulator($this))->inheritProperty($name, $returnIfExists); } /** - * @param Method[] $methods - * @return static + * @deprecated use ClassManipulator::inheritMethod() */ - public function setMethods(array $methods): self - { - $this->methods = []; - foreach ($methods as $v) { - if (!$v instanceof Method) { - throw new Nette\InvalidArgumentException('Argument must be Nette\PhpGenerator\Method[].'); - } - $this->methods[$v->getName()] = $v; - } - return $this; - } - - - /** @return Method[] */ - public function getMethods(): array - { - return $this->methods; - } - - - public function getMethod(string $name): Method - { - if (!isset($this->methods[$name])) { - throw new Nette\InvalidArgumentException("Method '$name' not found."); - } - return $this->methods[$name]; - } - - - public function addMethod(string $name): Method - { - $method = new Method($name); - if ($this->isInterface()) { - $method->setBody(null); - } else { - $method->setPublic(); - } - return $this->methods[$name] = $method; - } - - - /** @return static */ - public function removeMethod(string $name): self + public function inheritMethod(string $name, bool $returnIfExists = false): Method { - unset($this->methods[$name]); - return $this; - } - - - public function hasMethod(string $name): bool - { - return isset($this->methods[$name]); + return (new ClassManipulator($this))->inheritMethod($name, $returnIfExists); } /** @throws Nette\InvalidStateException */ public function validate(): void { - if ($this->abstract && $this->final) { - throw new Nette\InvalidStateException('Class cannot be abstract and final.'); - - } elseif (!$this->name && ($this->abstract || $this->final)) { + $name = $this->getName(); + if ($name === null && ($this->abstract || $this->final)) { throw new Nette\InvalidStateException('Anonymous class cannot be abstract or final.'); - } - } - - private function validateNames(array $names): void - { - foreach ($names as $name) { - if (!Helpers::isNamespaceIdentifier($name, true)) { - throw new Nette\InvalidArgumentException("Value '$name' is not valid class name."); - } + } elseif ($this->abstract && $this->final) { + throw new Nette\InvalidStateException("Class '$name' cannot be abstract and final at the same time."); } } - public function __clone() + public function __clone(): void { - $clone = function ($item) { return clone $item; }; + parent::__clone(); + $clone = fn($item) => clone $item; $this->consts = array_map($clone, $this->consts); - $this->properties = array_map($clone, $this->properties); $this->methods = array_map($clone, $this->methods); + $this->properties = array_map($clone, $this->properties); + $this->traits = array_map($clone, $this->traits); } } diff --git a/src/PhpGenerator/Closure.php b/src/PhpGenerator/Closure.php index b8fa86b5..49401e62 100644 --- a/src/PhpGenerator/Closure.php +++ b/src/PhpGenerator/Closure.php @@ -9,21 +9,17 @@ namespace Nette\PhpGenerator; -use Nette; - /** - * Closure. - * - * @property string $body + * Definition of a closure. */ final class Closure { - use Nette\SmartObject; use Traits\FunctionLike; + use Traits\AttributeAware; /** @var Parameter[] */ - private $uses = []; + private array $uses = []; public static function from(\Closure $closure): self @@ -34,23 +30,15 @@ public static function from(\Closure $closure): self public function __toString(): string { - try { - return (new Printer)->printClosure($this); - } catch (\Throwable $e) { - if (PHP_VERSION_ID >= 70400) { - throw $e; - } - trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR); - return ''; - } + return (new Printer)->printClosure($this); } /** + * Replaces all uses. * @param Parameter[] $uses - * @return static */ - public function setUses(array $uses): self + public function setUses(array $uses): static { (function (Parameter ...$uses) {})(...$uses); $this->uses = $uses; @@ -58,6 +46,7 @@ public function setUses(array $uses): self } + /** @return Parameter[] */ public function getUses(): array { return $this->uses; @@ -68,4 +57,10 @@ public function addUse(string $name): Parameter { return $this->uses[] = new Parameter($name); } + + + public function __clone(): void + { + $this->parameters = array_map(fn($param) => clone $param, $this->parameters); + } } diff --git a/src/PhpGenerator/Constant.php b/src/PhpGenerator/Constant.php index 7d3a83df..1e89d6e6 100644 --- a/src/PhpGenerator/Constant.php +++ b/src/PhpGenerator/Constant.php @@ -9,33 +9,58 @@ namespace Nette\PhpGenerator; -use Nette; - /** - * Class constant. + * Definition of a class constant. */ final class Constant { - use Nette\SmartObject; use Traits\NameAware; use Traits\VisibilityAware; use Traits\CommentAware; + use Traits\AttributeAware; - /** @var mixed */ - private $value; + private mixed $value; + private bool $final = false; + private ?string $type = null; - /** @return static */ - public function setValue($val): self + public function setValue(mixed $val): static { $this->value = $val; return $this; } - public function getValue() + public function getValue(): mixed { return $this->value; } + + + public function setFinal(bool $state = true): static + { + $this->final = $state; + return $this; + } + + + public function isFinal(): bool + { + return $this->final; + } + + + public function setType(?string $type): static + { + Helpers::validateType($type); + $this->type = $type; + return $this; + } + + + public function getType(): ?string + { + return $this->type; + } } diff --git a/src/PhpGenerator/Dumper.php b/src/PhpGenerator/Dumper.php index 6faa9560..45ffc451 100644 --- a/src/PhpGenerator/Dumper.php +++ b/src/PhpGenerator/Dumper.php @@ -10,37 +10,36 @@ namespace Nette\PhpGenerator; use Nette; +use function addcslashes, array_keys, array_shift, count, dechex, implode, in_array, is_array, is_int, is_object, is_resource, is_string, ltrim, method_exists, ord, preg_match, preg_replace, preg_replace_callback, preg_split, range, serialize, str_contains, str_pad, str_repeat, str_replace, strlen, strrpos, strtoupper, substr, trim, unserialize, var_export; +use const PREG_SPLIT_DELIM_CAPTURE, STR_PAD_LEFT; /** - * PHP code generator utils. + * Generates a PHP representation of a variable. */ final class Dumper { - private const INDENT_LENGTH = 4; + private const IndentLength = 4; - /** @var int */ - public $maxDepth = 50; - - /** @var int */ - public $wrapLength = 120; + public int $maxDepth = 50; + public int $wrapLength = 120; + public string $indentation = "\t"; + public bool $customObjects = true; /** * Returns a PHP representation of a variable. */ - public function dump($var, int $column = 0): string + public function dump(mixed $var, int $column = 0): string { return $this->dumpVar($var, [], 0, $column); } - private function dumpVar(&$var, array $parents = [], int $level = 0, int $column = 0): string + /** @param array $parents */ + private function dumpVar(mixed $var, array $parents = [], int $level = 0, int $column = 0): string { - if ($var instanceof Literal) { - return ltrim(Nette\Utils\Strings::indent(trim((string) $var), $level), "\t"); - - } elseif ($var === null) { + if ($var === null) { return 'null'; } elseif (is_string($var)) { @@ -49,182 +48,241 @@ private function dumpVar(&$var, array $parents = [], int $level = 0, int $column } elseif (is_array($var)) { return $this->dumpArray($var, $parents, $level, $column); + } elseif ($var instanceof Literal) { + return $this->dumpLiteral($var, $level); + } elseif (is_object($var)) { - return $this->dumpObject($var, $parents, $level); + return $this->dumpObject($var, $parents, $level, $column); } elseif (is_resource($var)) { - throw new Nette\InvalidArgumentException('Cannot dump resource.'); + throw new Nette\InvalidStateException('Cannot dump value of type resource.'); } else { - return var_export($var, true); + return var_export($var, return: true); } } - private function dumpString(string $var): string + private function dumpString(string $s): string { - if (preg_match('#[^\x09\x20-\x7E\xA0-\x{10FFFF}]#u', $var) || preg_last_error()) { - static $table; - if ($table === null) { - foreach (array_merge(range("\x00", "\x1F"), range("\x7F", "\xFF")) as $ch) { - $table[$ch] = '\x' . str_pad(dechex(ord($ch)), 2, '0', STR_PAD_LEFT); - } - $table['\\'] = '\\\\'; - $table["\r"] = '\r'; - $table["\n"] = '\n'; - $table["\t"] = '\t'; - $table['$'] = '\$'; - $table['"'] = '\"'; - } - return '"' . strtr($var, $table) . '"'; - } + $special = [ + "\r" => '\r', + "\n" => '\n', + "\t" => '\t', + "\e" => '\e', + '\\' => '\\\\', + ]; + + $utf8 = preg_match('##u', $s); + $escaped = preg_replace_callback( + $utf8 ? '#[\p{C}\\\]#u' : '#[\x00-\x1F\x7F-\xFF\\\]#', + fn($m) => $special[$m[0]] ?? (strlen($m[0]) === 1 + ? '\x' . str_pad(strtoupper(dechex(ord($m[0]))), 2, '0', STR_PAD_LEFT) + : '\u{' . strtoupper(ltrim(dechex(self::utf8Ord($m[0])), '0')) . '}'), + $s, + ); + return $s === str_replace('\\\\', '\\', $escaped) + ? "'" . preg_replace('#\'|\\\(?=[\'\\\]|$)#D', '\\\$0', $s) . "'" + : '"' . addcslashes($escaped, '"$') . '"'; + } + - return "'" . preg_replace('#\'|\\\\(?=[\'\\\\]|$)#D', '\\\\$0', $var) . "'"; + private static function utf8Ord(string $c): int + { + $ord0 = ord($c[0]); + return match (true) { + $ord0 < 0x80 => $ord0, + $ord0 < 0xE0 => ($ord0 << 6) + ord($c[1]) - 0x3080, + $ord0 < 0xF0 => ($ord0 << 12) + (ord($c[1]) << 6) + ord($c[2]) - 0xE2080, + default => ($ord0 << 18) + (ord($c[1]) << 12) + (ord($c[2]) << 6) + ord($c[3]) - 0x3C82080, + }; } - private function dumpArray(array &$var, array $parents, int $level, int $column): string + /** + * @param mixed[] $var + * @param array $parents + */ + private function dumpArray(array $var, array $parents, int $level, int $column): string { if (empty($var)) { return '[]'; - } elseif ($level > $this->maxDepth || in_array($var, $parents ?? [], true)) { - throw new Nette\InvalidArgumentException('Nesting level too deep or recursive dependency.'); + } elseif ($level > $this->maxDepth || in_array($var, $parents, strict: true)) { + throw new Nette\InvalidStateException('Nesting level too deep or recursive dependency.'); } - $space = str_repeat("\t", $level); - $outInline = ''; - $outWrapped = "\n$space"; $parents[] = $var; - $counter = 0; - $hideKeys = is_int(($tmp = array_keys($var))[0]) && $tmp === range($tmp[0], $tmp[0] + count($var) - 1); - - foreach ($var as $k => &$v) { - $keyPart = $hideKeys && $k === $counter ? '' : $this->dumpVar($k) . ' => '; - $counter = is_int($k) ? max($k + 1, $counter) : $counter; - $outInline .= ($outInline === '' ? '' : ', ') . $keyPart; - $outInline .= $this->dumpVar($v, $parents, 0, $column + strlen($outInline)); - $outWrapped .= "\t" - . $keyPart - . $this->dumpVar($v, $parents, $level + 1, strlen($keyPart)) - . ",\n$space"; + $hideKeys = is_int(($keys = array_keys($var))[0]) && $keys === range($keys[0], $keys[0] + count($var) - 1); + $pairs = []; + + foreach ($var as $k => $v) { + $keyPart = $hideKeys && ($k !== $keys[0] || $k === 0) + ? '' + : $this->dumpVar($k) . ' => '; + $pairs[] = $keyPart . $this->dumpVar($v, $parents, $level + 1, strlen($keyPart) + 1); // 1 = comma after item } - array_pop($parents); - $wrap = strpos($outInline, "\n") !== false || $level * self::INDENT_LENGTH + $column + strlen($outInline) + 3 > $this->wrapLength; // 3 = [], - return '[' . ($wrap ? $outWrapped : $outInline) . ']'; + $line = '[' . implode(', ', $pairs) . ']'; + $space = str_repeat($this->indentation, $level); + return !str_contains($line, "\n") && $level * self::IndentLength + $column + strlen($line) <= $this->wrapLength + ? $line + : "[\n$space" . $this->indentation . implode(",\n$space" . $this->indentation, $pairs) . ",\n$space]"; } - private function dumpObject(&$var, array $parents, int $level): string + /** @param array $parents */ + private function dumpObject(object $var, array $parents, int $level, int $column): string { - if ($var instanceof \Serializable) { - return 'unserialize(' . $this->dumpString(serialize($var)) . ')'; - - } elseif ($var instanceof \Closure) { - throw new Nette\InvalidArgumentException('Cannot dump closure.'); + if ($level > $this->maxDepth || in_array($var, $parents, strict: true)) { + throw new Nette\InvalidStateException('Nesting level too deep or recursive dependency.'); + } elseif ((new \ReflectionObject($var))->isAnonymous()) { + throw new Nette\InvalidStateException('Cannot dump an instance of an anonymous class.'); } - $class = get_class($var); - if ((new \ReflectionObject($var))->isAnonymous()) { - throw new Nette\InvalidArgumentException('Cannot dump anonymous class.'); + $class = $var::class; + $parents[] = $var; - } elseif (in_array($class, [\DateTime::class, \DateTimeImmutable::class], true)) { - return $this->format("new \\$class(?, new \\DateTimeZone(?))", $var->format('Y-m-d H:i:s.u'), $var->getTimeZone()->getName()); - } + if ($class === \stdClass::class) { + $var = (array) $var; + return '(object) ' . $this->dumpArray($var, $parents, $level, $column + 10); + + } elseif ($class === \DateTime::class || $class === \DateTimeImmutable::class) { + return $this->format( + "new \\$class(?, new \\DateTimeZone(?))", + $var->format('Y-m-d H:i:s.u'), + $var->getTimeZone()->getName(), + ); + + } elseif ($var instanceof \UnitEnum) { + return '\\' . $var::class . '::' . $var->name; + + } elseif ($var instanceof \Closure) { + $inner = Nette\Utils\Callback::unwrap($var); + if (Nette\Utils\Callback::isStatic($inner)) { + return implode('::', (array) $inner) . '(...)'; + } - $arr = (array) $var; - $space = str_repeat("\t", $level); + throw new Nette\InvalidStateException('Cannot dump object of type Closure.'); - if ($level > $this->maxDepth || in_array($var, $parents ?? [], true)) { - throw new Nette\InvalidArgumentException('Nesting level too deep or recursive dependency.'); + } elseif ($this->customObjects) { + return $this->dumpCustomObject($var, $parents, $level); + + } else { + throw new Nette\InvalidStateException("Cannot dump object of type $class."); } + } + + /** @param array $parents */ + private function dumpCustomObject(object $var, array $parents, int $level): string + { + $class = $var::class; + $space = str_repeat($this->indentation, $level); $out = "\n"; - $parents[] = $var; - if (method_exists($var, '__sleep')) { - foreach ($var->__sleep() as $v) { - $props[$v] = $props["\x00*\x00$v"] = $props["\x00$class\x00$v"] = true; + + if (method_exists($var, '__serialize')) { + $arr = $var->__serialize(); + } else { + $arr = (array) $var; + if (method_exists($var, '__sleep')) { + foreach ($var->__sleep() as $v) { + $props[$v] = $props["\x00*\x00$v"] = $props["\x00$class\x00$v"] = true; + } } } - foreach ($arr as $k => &$v) { + foreach ($arr as $k => $v) { if (!isset($props) || isset($props[$k])) { - $out .= "$space\t" + $out .= $space . $this->indentation . ($keyPart = $this->dumpVar($k) . ' => ') . $this->dumpVar($v, $parents, $level + 1, strlen($keyPart)) . ",\n"; } } - array_pop($parents); - $out .= $space; - return $class === \stdClass::class - ? "(object) [$out]" - : '\\' . __CLASS__ . "::createObject('$class', [$out])"; + return '\\' . self::class . "::createObject(\\$class::class, [$out$space])"; + } + + + private function dumpLiteral(Literal $var, int $level): string + { + $s = $var->formatWith($this); + $s = Nette\Utils\Strings::unixNewLines($s); + $s = Nette\Utils\Strings::indent(trim($s), $level, $this->indentation); + return ltrim($s, $this->indentation); } /** - * Generates PHP statement. + * Generates PHP statement. Supports placeholders: ? \? $? ->? ::? ...? ...?: ?* */ - public function format(string $statement, ...$args): string + public function format(string $statement, mixed ...$args): string { - $tokens = preg_split('#(\.\.\.\?|\$\?|->\?|::\?|\\\\\?|\?\*|\?)#', $statement, -1, PREG_SPLIT_DELIM_CAPTURE); + $tokens = preg_split('#(\.\.\.\?:?|\$\?|->\?|::\?|\\\\\?|\?\*|\?(?!\w))#', $statement, -1, PREG_SPLIT_DELIM_CAPTURE); $res = ''; foreach ($tokens as $n => $token) { if ($n % 2 === 0) { $res .= $token; - } elseif ($token === '\\?') { + } elseif ($token === '\?') { $res .= '?'; } elseif (!$args) { throw new Nette\InvalidArgumentException('Insufficient number of arguments.'); } elseif ($token === '?') { $res .= $this->dump(array_shift($args), strlen($res) - strrpos($res, "\n")); - } elseif ($token === '...?' || $token === '?*') { + } elseif ($token === '...?' || $token === '...?:' || $token === '?*') { $arg = array_shift($args); if (!is_array($arg)) { throw new Nette\InvalidArgumentException('Argument must be an array.'); } - $res .= $this->dumpArguments($arg, strlen($res) - strrpos($res, "\n")); + + $res .= $this->dumpArguments($arg, strlen($res) - strrpos($res, "\n"), $token === '...?:'); } else { // $ -> :: $arg = array_shift($args); if ($arg instanceof Literal || !Helpers::isIdentifier($arg)) { $arg = '{' . $this->dumpVar($arg) . '}'; } + $res .= substr($token, 0, -1) . $arg; } } + if ($args) { throw new Nette\InvalidArgumentException('Insufficient number of placeholders.'); } + return $res; } - private function dumpArguments(array &$var, int $column): string + /** @param mixed[] $args */ + private function dumpArguments(array $args, int $column, bool $named): string { - $outInline = $outWrapped = ''; - - foreach ($var as &$v) { - $outInline .= $outInline === '' ? '' : ', '; - $outInline .= $this->dumpVar($v, [$var], 0, $column + strlen($outInline)); - $outWrapped .= ($outWrapped === '' ? '' : ',') . "\n\t" . $this->dumpVar($v, [$var], 1); + $pairs = []; + foreach ($args as $k => $v) { + $name = $named && !is_int($k) ? $k . ': ' : ''; + $pairs[] = $name . $this->dumpVar($v, [$args], 0, $column + strlen($name) + 1); // 1 = ) after args } - return count($var) > 1 && (strpos($outInline, "\n") !== false || $column + strlen($outInline) > $this->wrapLength) - ? $outWrapped . "\n" - : $outInline; + $line = implode(', ', $pairs); + return count($args) < 2 || (!str_contains($line, "\n") && $column + strlen($line) <= $this->wrapLength) + ? $line + : "\n" . $this->indentation . implode(",\n" . $this->indentation, $pairs) . ",\n"; } /** - * @return object + * @param mixed[] $props * @internal */ - public static function createObject(string $class, array $props) + public static function createObject(string $class, array $props): object { + if (method_exists($class, '__serialize')) { + $obj = (new \ReflectionClass($class))->newInstanceWithoutConstructor(); + $obj->__unserialize($props); + return $obj; + } return unserialize('O' . substr(serialize($class), 1, -1) . substr(serialize($props), 1)); } } diff --git a/src/PhpGenerator/EnumCase.php b/src/PhpGenerator/EnumCase.php new file mode 100644 index 00000000..ee099b93 --- /dev/null +++ b/src/PhpGenerator/EnumCase.php @@ -0,0 +1,36 @@ +value = $val; + return $this; + } + + + public function getValue(): string|int|Literal|null + { + return $this->value; + } +} diff --git a/src/PhpGenerator/EnumType.php b/src/PhpGenerator/EnumType.php new file mode 100644 index 00000000..a4f47243 --- /dev/null +++ b/src/PhpGenerator/EnumType.php @@ -0,0 +1,148 @@ + */ + private array $cases = []; + private ?string $type = null; + + + public function setType(?string $type): static + { + $this->type = $type; + return $this; + } + + + public function getType(): ?string + { + return $this->type; + } + + + /** + * @param string[] $names + */ + public function setImplements(array $names): static + { + $this->validateNames($names); + $this->implements = $names; + return $this; + } + + + /** @return string[] */ + public function getImplements(): array + { + return $this->implements; + } + + + public function addImplement(string $name): static + { + $this->validateNames([$name]); + $this->implements[] = $name; + return $this; + } + + + public function removeImplement(string $name): static + { + $this->implements = array_diff($this->implements, [$name]); + return $this; + } + + + /** + * Sets cases to enum + * @param EnumCase[] $cases + */ + public function setCases(array $cases): static + { + (function (EnumCase ...$cases) {})(...$cases); + $this->cases = []; + foreach ($cases as $case) { + $this->cases[$case->getName()] = $case; + } + + return $this; + } + + + /** @return EnumCase[] */ + public function getCases(): array + { + return $this->cases; + } + + + /** Adds case to enum */ + public function addCase(string $name, string|int|Literal|null $value = null, bool $overwrite = false): EnumCase + { + if (!$overwrite && isset($this->cases[$name])) { + throw new Nette\InvalidStateException("Cannot add cases '$name', because it already exists."); + } + return $this->cases[$name] = (new EnumCase($name)) + ->setValue($value); + } + + + public function removeCase(string $name): static + { + unset($this->cases[$name]); + return $this; + } + + + /** + * Adds a member. If it already exists, throws an exception or overwrites it if $overwrite is true. + */ + public function addMember(Method|Constant|EnumCase|TraitUse $member, bool $overwrite = false): static + { + $name = $member->getName(); + [$type, $n] = match (true) { + $member instanceof Constant => ['consts', $name], + $member instanceof Method => ['methods', strtolower($name)], + $member instanceof TraitUse => ['traits', $name], + $member instanceof EnumCase => ['cases', $name], + }; + if (!$overwrite && isset($this->$type[$n])) { + throw new Nette\InvalidStateException("Cannot add member '$name', because it already exists."); + } + $this->$type[$n] = $member; + return $this; + } + + + public function __clone(): void + { + parent::__clone(); + $clone = fn($item) => clone $item; + $this->consts = array_map($clone, $this->consts); + $this->methods = array_map($clone, $this->methods); + $this->traits = array_map($clone, $this->traits); + $this->cases = array_map($clone, $this->cases); + } +} diff --git a/src/PhpGenerator/Extractor.php b/src/PhpGenerator/Extractor.php new file mode 100644 index 00000000..89eb7e54 --- /dev/null +++ b/src/PhpGenerator/Extractor.php @@ -0,0 +1,595 @@ +printer = new PhpParser\PrettyPrinter\Standard; + $this->parseCode($code); + } + + + private function parseCode(string $code): void + { + if (!str_starts_with($code, 'code = Nette\Utils\Strings::unixNewLines($code); + $parser = (new ParserFactory)->createForNewestSupportedVersion(); + $stmts = $parser->parse($this->code); + + $traverser = new PhpParser\NodeTraverser; + $traverser->addVisitor(new PhpParser\NodeVisitor\ParentConnectingVisitor); + $traverser->addVisitor(new PhpParser\NodeVisitor\NameResolver(null, ['preserveOriginalNames' => true])); + $this->statements = $traverser->traverse($stmts); + } + + + /** @return array */ + public function extractMethodBodies(string $className): array + { + $nodeFinder = new NodeFinder; + $classNode = $nodeFinder->findFirst( + $this->statements, + fn(Node $node) => $node instanceof Node\Stmt\ClassLike && $node->namespacedName->toString() === $className, + ); + + $res = []; + foreach ($nodeFinder->findInstanceOf($classNode, Node\Stmt\ClassMethod::class) as $methodNode) { + if ($methodNode->stmts) { + $res[$methodNode->name->toString()] = $this->getReformattedContents($methodNode->stmts, 2); + } + } + + return $res; + } + + + /** @return array> */ + public function extractPropertyHookBodies(string $className): array + { + if (!class_exists(Node\PropertyHook::class)) { + return []; + } + + $nodeFinder = new NodeFinder; + $classNode = $nodeFinder->findFirst( + $this->statements, + fn(Node $node) => $node instanceof Node\Stmt\ClassLike && $node->namespacedName->toString() === $className, + ); + + $res = []; + foreach ($nodeFinder->findInstanceOf($classNode, Node\Stmt\Property::class) as $propertyNode) { + foreach ($propertyNode->props as $propNode) { + $propName = $propNode->name->toString(); + foreach ($propertyNode->hooks as $hookNode) { + $body = $hookNode->body; + if ($body !== null) { + $contents = $this->getReformattedContents(is_array($body) ? $body : [$body], 3); + $res[$propName][$hookNode->name->toString()] = [$contents, !is_array($body)]; + } + } + } + } + return $res; + } + + + public function extractFunctionBody(string $name): string + { + $functionNode = (new NodeFinder)->findFirst( + $this->statements, + fn(Node $node) => $node instanceof Node\Stmt\Function_ && $node->namespacedName->toString() === $name, + ); + assert($functionNode instanceof Node\Stmt\Function_); + + return $this->getReformattedContents($functionNode->stmts, 1); + } + + + /** @param Node[] $nodes */ + private function getReformattedContents(array $nodes, int $level): string + { + if (!$nodes) { + return ''; + } + $body = $this->getNodeContents(...$nodes); + $body = $this->performReplacements($body, $this->prepareReplacements($nodes, $level)); + return Helpers::unindent($body, $level); + } + + + /** + * @param Node[] $nodes + * @return array + */ + private function prepareReplacements(array $nodes, int $level): array + { + $start = $this->getNodeStartPos($nodes[0]); + $replacements = []; + $indent = "\n" . str_repeat("\t", $level); + (new NodeFinder)->find($nodes, function (Node $node) use (&$replacements, $start, $level, $indent) { + if ($node instanceof Node\Name\FullyQualified) { + if ($node->getAttribute('originalName') instanceof Node\Name) { + $of = match (true) { + $node->getAttribute('parent') instanceof Node\Expr\ConstFetch => PhpNamespace::NameConstant, + $node->getAttribute('parent') instanceof Node\Expr\FuncCall => PhpNamespace::NameFunction, + default => PhpNamespace::NameNormal, + }; + $replacements[] = [ + $node->getStartFilePos() - $start, + $node->getEndFilePos() - $start, + Helpers::tagName($node->toCodeString(), $of), + ]; + } + + } elseif ( + $node instanceof Node\Scalar\String_ + && in_array($node->getAttribute('kind'), [Node\Scalar\String_::KIND_SINGLE_QUOTED, Node\Scalar\String_::KIND_DOUBLE_QUOTED], true) + && str_contains($node->getAttribute('rawValue'), "\n") + ) { // multi-line strings -> single line + $replacements[] = [ + $node->getStartFilePos() - $start, + $node->getEndFilePos() - $start, + '"' . addcslashes($node->value, "\x00..\x1F\"") . '"', + ]; + + } elseif ( + $node instanceof Node\Scalar\String_ + && in_array($node->getAttribute('kind'), [Node\Scalar\String_::KIND_NOWDOC, Node\Scalar\String_::KIND_HEREDOC], true) + && Helpers::unindent($node->getAttribute('docIndentation'), $level) === $node->getAttribute('docIndentation') + ) { // fix indentation of NOWDOW/HEREDOC + $replacements[] = [ + $node->getStartFilePos() - $start, + $node->getEndFilePos() - $start, + str_replace("\n", $indent, $this->getNodeContents($node)), + ]; + + } elseif ( + $node instanceof Node\Scalar\Encapsed + && $node->getAttribute('kind') === Node\Scalar\String_::KIND_DOUBLE_QUOTED + ) { // multi-line strings -> single line + foreach ($node->parts as $part) { + if ($part instanceof Node\Scalar\EncapsedStringPart) { + $replacements[] = [ + $part->getStartFilePos() - $start, + $part->getEndFilePos() - $start, + addcslashes($part->value, "\x00..\x1F\""), + ]; + } + } + } elseif ( + $node instanceof Node\Scalar\Encapsed && $node->getAttribute('kind') === Node\Scalar\String_::KIND_HEREDOC + && Helpers::unindent($node->getAttribute('docIndentation'), $level) === $node->getAttribute('docIndentation') + ) { // fix indentation of HEREDOC + $replacements[] = [ + $tmp = $node->getStartFilePos() - $start + strlen($node->getAttribute('docLabel')) + 3, // <<< + $tmp, + $indent, + ]; + $replacements[] = [ + $tmp = $node->getEndFilePos() - $start - strlen($node->getAttribute('docLabel')), + $tmp, + $indent, + ]; + foreach ($node->parts as $part) { + if ($part instanceof Node\Scalar\EncapsedStringPart) { + $replacements[] = [ + $part->getStartFilePos() - $start, + $part->getEndFilePos() - $start, + str_replace("\n", $indent, $this->getNodeContents($part)), + ]; + } + } + } + }); + return $replacements; + } + + + /** @param array $replacements */ + private function performReplacements(string $s, array $replacements): string + { + usort($replacements, fn($a, $b) => $b[0] <=> $a[0]); + + foreach ($replacements as [$start, $end, $replacement]) { + $s = substr_replace($s, $replacement, $start, $end - $start + 1); + } + + return $s; + } + + + public function extractAll(): PhpFile + { + $phpFile = new PhpFile; + + if ( + $this->statements + && !$this->statements[0] instanceof Node\Stmt\ClassLike + && !$this->statements[0] instanceof Node\Stmt\Function_ + ) { + $this->addCommentAndAttributes($phpFile, $this->statements[0]); + } + + $namespaces = ['' => $this->statements]; + foreach ($this->statements as $node) { + if ($node instanceof Node\Stmt\Declare_ + && $node->declares[0]->key->name === 'strict_types' + && $node->declares[0]->value instanceof Node\Scalar\LNumber + ) { + $phpFile->setStrictTypes((bool) $node->declares[0]->value->value); + + } elseif ($node instanceof Node\Stmt\Namespace_) { + $namespaces[$node->name->toString()] = $node->stmts; + } + } + + foreach ($namespaces as $name => $nodes) { + foreach ($nodes as $node) { + match (true) { + $node instanceof Node\Stmt\Use_ => $this->addUseToNamespace($phpFile->addNamespace($name), $node), + $node instanceof Node\Stmt\ClassLike => $this->addClassLikeToFile($phpFile, $node), + $node instanceof Node\Stmt\Function_ => $this->addFunctionToFile($phpFile, $node), + default => null, + }; + } + } + + return $phpFile; + } + + + private function addUseToNamespace(PhpNamespace $namespace, Node\Stmt\Use_ $node): void + { + $of = [ + $node::TYPE_NORMAL => PhpNamespace::NameNormal, + $node::TYPE_FUNCTION => PhpNamespace::NameFunction, + $node::TYPE_CONSTANT => PhpNamespace::NameConstant, + ][$node->type]; + foreach ($node->uses as $use) { + $namespace->addUse($use->name->toString(), $use->alias?->toString(), $of); + } + } + + + private function addClassLikeToFile(PhpFile $phpFile, Node\Stmt\ClassLike $node): ClassLike + { + if ($node instanceof Node\Stmt\Class_) { + $class = $phpFile->addClass($node->namespacedName->toString()); + $class->setFinal($node->isFinal()); + $class->setAbstract($node->isAbstract()); + $class->setReadOnly($node->isReadonly()); + if ($node->extends) { + $class->setExtends($node->extends->toString()); + } + foreach ($node->implements as $item) { + $class->addImplement($item->toString()); + } + } elseif ($node instanceof Node\Stmt\Interface_) { + $class = $phpFile->addInterface($node->namespacedName->toString()); + foreach ($node->extends as $item) { + $class->addExtend($item->toString()); + } + } elseif ($node instanceof Node\Stmt\Trait_) { + $class = $phpFile->addTrait($node->namespacedName->toString()); + + } elseif ($node instanceof Node\Stmt\Enum_) { + $class = $phpFile->addEnum($node->namespacedName->toString()); + $class->setType($node->scalarType?->toString()); + foreach ($node->implements as $item) { + $class->addImplement($item->toString()); + } + } else { + throw new Nette\ShouldNotHappenException; + } + + $this->addCommentAndAttributes($class, $node); + $this->addClassMembers($class, $node); + return $class; + } + + + private function addClassMembers(ClassLike $class, Node\Stmt\ClassLike $node): void + { + foreach ($node->stmts as $stmt) { + match (true) { + $stmt instanceof Node\Stmt\TraitUse => $this->addTraitToClass($class, $stmt), + $stmt instanceof Node\Stmt\Property => $this->addPropertyToClass($class, $stmt), + $stmt instanceof Node\Stmt\ClassMethod => $this->addMethodToClass($class, $stmt), + $stmt instanceof Node\Stmt\ClassConst => $this->addConstantToClass($class, $stmt), + $stmt instanceof Node\Stmt\EnumCase => $this->addEnumCaseToClass($class, $stmt), + default => null, + }; + } + } + + + private function addTraitToClass(ClassLike $class, Node\Stmt\TraitUse $node): void + { + foreach ($node->traits as $item) { + $trait = $class->addTrait($item->toString()); + } + assert($trait instanceof TraitUse); + + foreach ($node->adaptations as $item) { + $trait->addResolution(rtrim($this->getReformattedContents([$item], 0), ';')); + } + + $this->addCommentAndAttributes($trait, $node); + } + + + private function addPropertyToClass(ClassLike $class, Node\Stmt\Property $node): void + { + foreach ($node->props as $item) { + $prop = $class->addProperty($item->name->toString()); + $prop->setStatic($node->isStatic()); + $prop->setVisibility($this->toVisibility($node->flags), $this->toSetterVisibility($node->flags)); + $prop->setType($node->type ? $this->toPhp($node->type) : null); + if ($item->default) { + $prop->setValue($this->toValue($item->default)); + } + + $prop->setReadOnly($node->isReadonly() || ($class instanceof ClassType && $class->isReadOnly())); + $this->addCommentAndAttributes($prop, $node); + + $prop->setAbstract((bool) ($node->flags & Modifiers::ABSTRACT)); + $prop->setFinal((bool) ($node->flags & Modifiers::FINAL)); + $this->addHooksToProperty($prop, $node); + } + } + + + private function addHooksToProperty(Property|PromotedParameter $prop, Node\Stmt\Property|Node\Param $node): void + { + if (!class_exists(Node\PropertyHook::class)) { + return; + } + + foreach ($node->hooks as $hookNode) { + $hook = $prop->addHook($hookNode->name->toString()); + $hook->setFinal((bool) ($hookNode->flags & Modifiers::FINAL)); + $this->setupFunction($hook, $hookNode); + if ($hookNode->body === null) { + $hook->setAbstract(); + } elseif (!is_array($hookNode->body)) { + $hook->setBody($this->getReformattedContents([$hookNode->body], 1), short: true); + } + } + } + + + private function addMethodToClass(ClassLike $class, Node\Stmt\ClassMethod $node): void + { + $method = $class->addMethod($node->name->toString()); + $method->setAbstract($node->isAbstract()); + $method->setFinal($node->isFinal()); + $method->setStatic($node->isStatic()); + $method->setVisibility($this->toVisibility($node->flags)); + $this->setupFunction($method, $node); + if ($method->getName() === Method::Constructor && $class instanceof ClassType && $class->isReadOnly()) { + array_map(fn($param) => $param instanceof PromotedParameter ? $param->setReadOnly() : $param, $method->getParameters()); + } + } + + + private function addConstantToClass(ClassLike $class, Node\Stmt\ClassConst $node): void + { + foreach ($node->consts as $item) { + $const = $class->addConstant($item->name->toString(), $this->toValue($item->value)); + $const->setVisibility($this->toVisibility($node->flags)); + $const->setFinal($node->isFinal()); + $this->addCommentAndAttributes($const, $node); + } + } + + + private function addEnumCaseToClass(EnumType $class, Node\Stmt\EnumCase $node): void + { + $value = match (true) { + $node->expr === null => null, + $node->expr instanceof Node\Scalar\LNumber, $node->expr instanceof Node\Scalar\String_ => $node->expr->value, + default => $this->toValue($node->expr), + }; + $case = $class->addCase($node->name->toString(), $value); + $this->addCommentAndAttributes($case, $node); + } + + + private function addFunctionToFile(PhpFile $phpFile, Node\Stmt\Function_ $node): void + { + $function = $phpFile->addFunction($node->namespacedName->toString()); + $this->setupFunction($function, $node); + } + + + private function addCommentAndAttributes( + PhpFile|ClassLike|Constant|Property|GlobalFunction|Method|Parameter|EnumCase|TraitUse|PropertyHook $element, + Node $node, + ): void + { + if ($node->getDocComment()) { + $comment = $node->getDocComment()->getReformattedText(); + $comment = Helpers::unformatDocComment($comment); + $element->setComment($comment); + $node->setDocComment(new PhpParser\Comment\Doc('')); + } + + foreach ($node->attrGroups ?? [] as $group) { + foreach ($group->attrs as $attribute) { + $args = []; + foreach ($attribute->args as $arg) { + if ($arg->name) { + $args[$arg->name->toString()] = $this->toValue($arg->value); + } else { + $args[] = $this->toValue($arg->value); + } + } + + $element->addAttribute($attribute->name->toString(), $args); + } + } + } + + + private function setupFunction(GlobalFunction|Method|PropertyHook $function, Node\FunctionLike $node): void + { + $function->setReturnReference($node->returnsByRef()); + if (!$function instanceof PropertyHook) { + $function->setReturnType($node->getReturnType() ? $this->toPhp($node->getReturnType()) : null); + } + + foreach ($node->getParams() as $item) { + $getVisibility = $this->toVisibility($item->flags); + $setVisibility = $this->toSetterVisibility($item->flags); + $final = (bool) ($item->flags & Modifiers::FINAL); + if ($getVisibility || $setVisibility || $final) { + $param = $function->addPromotedParameter($item->var->name) + ->setVisibility($getVisibility, $setVisibility) + ->setReadonly($item->isReadonly()) + ->setFinal($final); + $this->addHooksToProperty($param, $item); + } else { + $param = $function->addParameter($item->var->name); + } + $param->setType($item->type ? $this->toPhp($item->type) : null); + $param->setReference($item->byRef); + if (!$function instanceof PropertyHook) { + $function->setVariadic($item->variadic); + } + if ($item->default) { + $param->setDefaultValue($this->toValue($item->default)); + } + + $this->addCommentAndAttributes($param, $item); + } + + $this->addCommentAndAttributes($function, $node); + if ($node->getStmts()) { + $indent = $function instanceof GlobalFunction ? 1 : 2; + $function->setBody($this->getReformattedContents($node->getStmts(), $indent)); + } + } + + + private function toValue(Node\Expr $node): mixed + { + if ($node instanceof Node\Expr\ConstFetch) { + return match ($node->name->toLowerString()) { + 'null' => null, + 'true' => true, + 'false' => false, + default => new Literal($this->getReformattedContents([$node], 0)), + }; + } elseif ($node instanceof Node\Scalar\LNumber + || $node instanceof Node\Scalar\DNumber + || $node instanceof Node\Scalar\String_ + ) { + return $node->value; + + } elseif ($node instanceof Node\Expr\Array_) { + $res = []; + foreach ($node->items as $item) { + if ($item->unpack) { + return new Literal($this->getReformattedContents([$node], 0)); + + } elseif ($item->key) { + $key = $this->toValue($item->key); + if ($key instanceof Literal) { + return new Literal($this->getReformattedContents([$node], 0)); + } + + $res[$key] = $this->toValue($item->value); + + } else { + $res[] = $this->toValue($item->value); + } + } + return $res; + + } else { + return new Literal($this->getReformattedContents([$node], 0)); + } + } + + + private function toVisibility(int $flags): ?Visibility + { + return match (true) { + (bool) ($flags & Modifiers::PUBLIC) => Visibility::Public, + (bool) ($flags & Modifiers::PROTECTED) => Visibility::Protected, + (bool) ($flags & Modifiers::PRIVATE) => Visibility::Private, + default => null, + }; + } + + + private function toSetterVisibility(int $flags): ?Visibility + { + return match (true) { + !class_exists(Node\PropertyHook::class) => null, + (bool) ($flags & Modifiers::PUBLIC_SET) => Visibility::Public, + (bool) ($flags & Modifiers::PROTECTED_SET) => Visibility::Protected, + (bool) ($flags & Modifiers::PRIVATE_SET) => Visibility::Private, + default => null, + }; + } + + + private function toPhp(Node $value): string + { + $dolly = clone $value; + $dolly->setAttribute('comments', []); + return $this->printer->prettyPrint([$dolly]); + } + + + private function getNodeContents(Node ...$nodes): string + { + $start = $this->getNodeStartPos($nodes[0]); + return substr($this->code, $start, end($nodes)->getEndFilePos() - $start + 1); + } + + + private function getNodeStartPos(Node $node): int + { + return ($comments = $node->getComments()) + ? $comments[0]->getStartFilePos() + : $node->getStartFilePos(); + } +} diff --git a/src/PhpGenerator/Factory.php b/src/PhpGenerator/Factory.php index 5a8f1a44..2f164b17 100644 --- a/src/PhpGenerator/Factory.php +++ b/src/PhpGenerator/Factory.php @@ -10,65 +10,146 @@ namespace Nette\PhpGenerator; use Nette; -use PhpParser; -use PhpParser\Node; -use PhpParser\ParserFactory; +use Nette\Utils\Reflection; +use function array_diff, array_filter, array_key_exists, array_map, count, explode, file_get_contents, implode, is_object, is_subclass_of, method_exists, reset; +use const PHP_VERSION_ID; /** - * Creates a representation based on reflection. + * Creates a representations based on reflection or source code. */ final class Factory { - use Nette\SmartObject; + /** @var string[][] */ + private array $bodyCache = []; - public function fromClassReflection(\ReflectionClass $from, bool $withBodies = false): ClassType + /** @var Extractor[] */ + private array $extractorCache = []; + + + /** @param \ReflectionClass $from */ + public function fromClassReflection( + \ReflectionClass $from, + bool $withBodies = false, + ): ClassLike { - $class = $from->isAnonymous() - ? new ClassType - : new ClassType($from->getShortName(), new PhpNamespace($from->getNamespaceName())); - $class->setType($from->isInterface() ? $class::TYPE_INTERFACE : ($from->isTrait() ? $class::TYPE_TRAIT : $class::TYPE_CLASS)); - $class->setFinal($from->isFinal() && $class->isClass()); - $class->setAbstract($from->isAbstract() && $class->isClass()); + if ($withBodies && ($from->isAnonymous() || $from->isInternal() || $from->isInterface())) { + throw new Nette\NotSupportedException('The $withBodies parameter cannot be used for anonymous or internal classes or interfaces.'); + } + + $enumIface = null; + if ($from->isEnum()) { + $class = new EnumType($from->getShortName(), new PhpNamespace($from->getNamespaceName())); + $from = new \ReflectionEnum($from->getName()); + $enumIface = $from->isBacked() ? \BackedEnum::class : \UnitEnum::class; + } elseif ($from->isAnonymous()) { + $class = new ClassType; + } elseif ($from->isInterface()) { + $class = new InterfaceType($from->getShortName(), new PhpNamespace($from->getNamespaceName())); + } elseif ($from->isTrait()) { + $class = new TraitType($from->getShortName(), new PhpNamespace($from->getNamespaceName())); + } else { + $class = new ClassType($from->getShortName(), new PhpNamespace($from->getNamespaceName())); + $class->setFinal($from->isFinal() && $class->isClass()); + $class->setAbstract($from->isAbstract() && $class->isClass()); + $class->setReadOnly(PHP_VERSION_ID >= 80200 && $from->isReadOnly()); + } $ifaces = $from->getInterfaceNames(); foreach ($ifaces as $iface) { - $ifaces = array_filter($ifaces, function (string $item) use ($iface): bool { - return !is_subclass_of($iface, $item); - }); + $ifaces = array_filter($ifaces, fn(string $item): bool => !is_subclass_of($iface, $item)); + } + + if ($from->isInterface()) { + $class->setExtends($ifaces); + } elseif ($ifaces) { + $ifaces = array_diff($ifaces, [$enumIface]); + $class->setImplements($ifaces); } - $class->setImplements($ifaces); $class->setComment(Helpers::unformatDocComment((string) $from->getDocComment())); + $class->setAttributes($this->getAttributes($from)); if ($from->getParentClass()) { $class->setExtends($from->getParentClass()->name); $class->setImplements(array_diff($class->getImplements(), $from->getParentClass()->getInterfaceNames())); } - $props = $methods = $consts = []; + + $props = []; foreach ($from->getProperties() as $prop) { - if ($prop->isDefault() && $prop->getDeclaringClass()->name === $from->name) { - $props[] = $this->fromPropertyReflection($prop); + $declaringClass = Reflection::getPropertyDeclaringClass($prop); + + if ($prop->isDefault() + && $declaringClass->name === $from->name + && !$prop->isPromoted() + && !$class->isEnum() + ) { + $props[] = $p = $this->fromPropertyReflection($prop); + if ($withBodies) { + $hookBodies ??= $this->getExtractor($declaringClass->getFileName())->extractPropertyHookBodies($declaringClass->name); + foreach ($hookBodies[$prop->getName()] ?? [] as $hookType => [$body, $short]) { + $p->getHook($hookType)->setBody($body, short: $short); + } + } } } - $class->setProperties($props); - $bodies = $withBodies ? $this->loadMethodBodies($from) : []; + if ($props) { + $class->setProperties($props); + } + + $methods = $resolutions = []; foreach ($from->getMethods() as $method) { - if ($method->getDeclaringClass()->name === $from->name) { + $declaringMethod = Reflection::getMethodDeclaringMethod($method); + $declaringClass = $declaringMethod->getDeclaringClass(); + + if ( + $declaringClass->name === $from->name + && (!$enumIface || !method_exists($enumIface, $method->name)) + ) { $methods[] = $m = $this->fromMethodReflection($method); - if (isset($bodies[$method->name])) { - $m->setBody($bodies[$method->name]); + if ($withBodies) { + $bodies = &$this->bodyCache[$declaringClass->name]; + $bodies ??= $this->getExtractor($declaringClass->getFileName())->extractMethodBodies($declaringClass->name); + if (isset($bodies[$declaringMethod->name])) { + $m->setBody($bodies[$declaringMethod->name]); + } } } + + $modifier = $declaringMethod->getModifiers() !== $method->getModifiers() + ? ' ' . $this->getVisibility($method)->value + : null; + $alias = $declaringMethod->name !== $method->name ? ' ' . $method->name : ''; + if ($modifier || $alias) { + $resolutions[] = $declaringMethod->name . ' as' . $modifier . $alias; + } } + $class->setMethods($methods); + foreach ($from->getTraitNames() as $trait) { + $trait = $class->addTrait($trait); + foreach ($resolutions as $resolution) { + $trait->addResolution($resolution); + } + $resolutions = []; + } + + $consts = $cases = []; foreach ($from->getReflectionConstants() as $const) { - if ($const->getDeclaringClass()->name === $from->name) { + if ($class->isEnum() && $from->hasCase($const->name)) { + $cases[] = $this->fromCaseReflection($const); + } elseif ($const->getDeclaringClass()->name === $from->name) { $consts[] = $this->fromConstantReflection($const); } } - $class->setConstants($consts); + + if ($consts) { + $class->setConstants($consts); + } + if ($cases) { + $class->setCases($cases); + } return $class; } @@ -80,26 +161,20 @@ public function fromMethodReflection(\ReflectionMethod $from): Method $method->setParameters(array_map([$this, 'fromParameterReflection'], $from->getParameters())); $method->setStatic($from->isStatic()); $isInterface = $from->getDeclaringClass()->isInterface(); - $method->setVisibility($from->isPrivate() - ? ClassType::VISIBILITY_PRIVATE - : ($from->isProtected() ? ClassType::VISIBILITY_PROTECTED : ($isInterface ? null : ClassType::VISIBILITY_PUBLIC)) - ); + $method->setVisibility($isInterface ? null : $this->getVisibility($from)); $method->setFinal($from->isFinal()); $method->setAbstract($from->isAbstract() && !$isInterface); - $method->setBody($from->isAbstract() ? null : ''); $method->setReturnReference($from->returnsReference()); $method->setVariadic($from->isVariadic()); $method->setComment(Helpers::unformatDocComment((string) $from->getDocComment())); - if ($from->getReturnType() instanceof \ReflectionNamedType) { - $method->setReturnType($from->getReturnType()->getName()); - $method->setReturnNullable($from->getReturnType()->allowsNull()); - } + $method->setAttributes($this->getAttributes($from)); + $method->setReturnType((string) $from->getReturnType()); + return $method; } - /** @return GlobalFunction|Closure */ - public function fromFunctionReflection(\ReflectionFunction $from, bool $withBody = false) + public function fromFunctionReflection(\ReflectionFunction $from, bool $withBody = false): GlobalFunction|Closure { $function = $from->isClosure() ? new Closure : new GlobalFunction($from->name); $function->setParameters(array_map([$this, 'fromParameterReflection'], $from->getParameters())); @@ -108,37 +183,62 @@ public function fromFunctionReflection(\ReflectionFunction $from, bool $withBody if (!$from->isClosure()) { $function->setComment(Helpers::unformatDocComment((string) $from->getDocComment())); } - if ($from->getReturnType() instanceof \ReflectionNamedType) { - $function->setReturnType($from->getReturnType()->getName()); - $function->setReturnNullable($from->getReturnType()->allowsNull()); + + $function->setAttributes($this->getAttributes($from)); + $function->setReturnType((string) $from->getReturnType()); + + if ($withBody) { + if ($from->isClosure() || $from->isInternal()) { + throw new Nette\NotSupportedException('The $withBody parameter cannot be used for closures or internal functions.'); + } + + $function->setBody($this->getExtractor($from->getFileName())->extractFunctionBody($from->name)); } - $function->setBody($withBody ? $this->loadFunctionBody($from) : ''); + return $function; } - /** @return Method|GlobalFunction|Closure */ - public function fromCallable(callable $from) + public function fromCallable(callable $from): Method|GlobalFunction|Closure { $ref = Nette\Utils\Callback::toReflection($from); return $ref instanceof \ReflectionMethod - ? self::fromMethodReflection($ref) - : self::fromFunctionReflection($ref); + ? $this->fromMethodReflection($ref) + : $this->fromFunctionReflection($ref); } public function fromParameterReflection(\ReflectionParameter $from): Parameter { - $param = new Parameter($from->name); + if ($from->isPromoted()) { + $property = $from->getDeclaringClass()->getProperty($from->name); + $param = (new PromotedParameter($from->name)) + ->setVisibility($this->getVisibility($property)) + ->setReadOnly($property->isReadonly()) + ->setFinal(PHP_VERSION_ID >= 80500 && $property->isFinal() && !$property->isPrivateSet()); + $this->addHooks($property, $param); + } else { + $param = new Parameter($from->name); + } $param->setReference($from->isPassedByReference()); - $param->setType($from->getType() instanceof \ReflectionNamedType ? $from->getType()->getName() : null); - $param->setNullable($from->hasType() && $from->getType()->allowsNull()); + $param->setType((string) $from->getType()); + if ($from->isDefaultValueAvailable()) { - $param->setDefaultValue($from->isDefaultValueConstant() - ? new Literal($from->getDefaultValueConstantName()) - : $from->getDefaultValue()); - $param->setNullable($param->isNullable() && $param->getDefaultValue() !== null); + if ($from->isDefaultValueConstant()) { + $parts = explode('::', $from->getDefaultValueConstantName()); + if (count($parts) > 1) { + $parts[0] = Helpers::tagName($parts[0]); + } + + $param->setDefaultValue(new Literal(implode('::', $parts))); + } elseif (is_object($from->getDefaultValue())) { + $param->setDefaultValue($this->fromObject($from->getDefaultValue())); + } else { + $param->setDefaultValue($from->getDefaultValue()); + } } + + $param->setAttributes($this->getAttributes($from)); return $param; } @@ -147,11 +247,20 @@ public function fromConstantReflection(\ReflectionClassConstant $from): Constant { $const = new Constant($from->name); $const->setValue($from->getValue()); - $const->setVisibility($from->isPrivate() - ? ClassType::VISIBILITY_PRIVATE - : ($from->isProtected() ? ClassType::VISIBILITY_PROTECTED : ClassType::VISIBILITY_PUBLIC) - ); + $const->setVisibility($this->getVisibility($from)); + $const->setFinal($from->isFinal()); + $const->setComment(Helpers::unformatDocComment((string) $from->getDocComment())); + $const->setAttributes($this->getAttributes($from)); + return $const; + } + + + public function fromCaseReflection(\ReflectionClassConstant $from): EnumCase + { + $const = new EnumCase($from->name); + $const->setValue($from->getValue()->value ?? null); $const->setComment(Helpers::unformatDocComment((string) $from->getDocComment())); + $const->setAttributes($this->getAttributes($from)); return $const; } @@ -162,156 +271,108 @@ public function fromPropertyReflection(\ReflectionProperty $from): Property $prop = new Property($from->name); $prop->setValue($defaults[$prop->getName()] ?? null); $prop->setStatic($from->isStatic()); - $prop->setVisibility($from->isPrivate() - ? ClassType::VISIBILITY_PRIVATE - : ($from->isProtected() ? ClassType::VISIBILITY_PROTECTED : ClassType::VISIBILITY_PUBLIC) - ); - if (PHP_VERSION_ID >= 70400 && ($from->getType() instanceof \ReflectionNamedType)) { - $prop->setType($from->getType()->getName()); - $prop->setNullable($from->getType()->allowsNull()); - $prop->setInitialized(array_key_exists($prop->getName(), $defaults)); - } + $prop->setVisibility($this->getVisibility($from)); + $prop->setType((string) $from->getType()); + $prop->setInitialized($from->hasType() && array_key_exists($prop->getName(), $defaults)); + $prop->setReadOnly($from->isReadOnly()); $prop->setComment(Helpers::unformatDocComment((string) $from->getDocComment())); + $prop->setAttributes($this->getAttributes($from)); + + if (PHP_VERSION_ID >= 80400) { + $this->addHooks($from, $prop); + $isInterface = $from->getDeclaringClass()->isInterface(); + $prop->setFinal($from->isFinal() && !$prop->isPrivate(PropertyAccessMode::Set)); + $prop->setAbstract($from->isAbstract() && !$isInterface); + } return $prop; } - private function loadMethodBodies(\ReflectionClass $from): array + private function addHooks(\ReflectionProperty $from, Property|PromotedParameter $prop): void { - if ($from->isAnonymous()) { - throw new Nette\NotSupportedException('Anonymous classes are not supported.'); + if (PHP_VERSION_ID < 80400) { + return; + } + + $getV = $this->getVisibility($from); + $setV = $from->isPrivateSet() + ? Visibility::Private + : ($from->isProtectedSet() ? Visibility::Protected : $getV); + $defaultSetV = $from->isReadOnly() && $getV !== Visibility::Private + ? Visibility::Protected + : $getV; + if ($setV !== $defaultSetV) { + $prop->setVisibility($getV === Visibility::Public ? null : $getV, $setV); } - [$code, $stmts] = $this->parse($from); - $nodeFinder = new PhpParser\NodeFinder; - $class = $nodeFinder->findFirst($stmts, function (Node $node) use ($from) { - return ($node instanceof Node\Stmt\Class_ || $node instanceof Node\Stmt\Trait_) && $node->namespacedName->toString() === $from->name; - }); - - $bodies = []; - foreach ($nodeFinder->findInstanceOf($class, Node\Stmt\ClassMethod::class) as $method) { - /** @var Node\Stmt\ClassMethod $method */ - if ($method->stmts) { - $body = $this->extractBody($nodeFinder, $code, $method->stmts); - $bodies[$method->name->toString()] = Helpers::unindent($body, 2); + foreach ($from->getHooks() as $type => $hook) { + $params = $hook->getParameters(); + if ( + count($params) === 1 + && $params[0]->getName() === 'value' + && $params[0]->getType() == $from->getType() // intentionally == + ) { + $params = []; } + $prop->addHook($type) + ->setParameters(array_map([$this, 'fromParameterReflection'], $params)) + ->setAbstract($hook->isAbstract()) + ->setFinal($hook->isFinal()) + ->setReturnReference($hook->returnsReference()) + ->setComment(Helpers::unformatDocComment((string) $hook->getDocComment())) + ->setAttributes($this->getAttributes($hook)); } - return $bodies; } - private function loadFunctionBody(\ReflectionFunction $from): string + public function fromObject(object $obj): Literal { - if ($from->isClosure()) { - throw new Nette\NotSupportedException('Closures are not supported.'); - } - - [$code, $stmts] = $this->parse($from); + return new Literal('new \\' . $obj::class . '(/* unknown */)'); + } - $nodeFinder = new PhpParser\NodeFinder; - /** @var Node\Stmt\Function_ $function */ - $function = $nodeFinder->findFirst($stmts, function (Node $node) use ($from) { - return $node instanceof Node\Stmt\Function_ && $node->namespacedName->toString() === $from->name; - }); - $body = $this->extractBody($nodeFinder, $code, $function->stmts); - return Helpers::unindent($body, 1); + public function fromClassCode(string $code): ClassLike + { + $classes = $this->fromCode($code)->getClasses(); + return reset($classes) ?: throw new Nette\InvalidStateException('The code does not contain any class.'); } - /** - * @param Node[] $statements - */ - private function extractBody(PhpParser\NodeFinder $nodeFinder, string $originalCode, array $statements): string + public function fromCode(string $code): PhpFile { - $start = $statements[0]->getAttribute('startFilePos'); - $body = substr($originalCode, $start, end($statements)->getAttribute('endFilePos') - $start + 1); - - $replacements = []; - // name-nodes => resolved fully-qualified name - foreach ($nodeFinder->findInstanceOf($statements, Node\Name::class) as $node) { - if ($node->hasAttribute('resolvedName') - && $node->getAttribute('resolvedName') instanceof Node\Name\FullyQualified - ) { - $replacements[] = [ - $node->getStartFilePos(), - $node->getEndFilePos(), - $node->getAttribute('resolvedName')->toCodeString(), - ]; - } - } + $reader = new Extractor($code); + return $reader->extractAll(); + } - // multi-line strings => singleline - foreach (array_merge( - $nodeFinder->findInstanceOf($statements, Node\Scalar\String_::class), - $nodeFinder->findInstanceOf($statements, Node\Scalar\EncapsedStringPart::class) - ) as $node) { - $token = substr($body, $node->getStartFilePos() - $start, $node->getEndFilePos() - $node->getStartFilePos() + 1); - if (strpos($token, "\n") !== false) { - $quote = $node instanceof Node\Scalar\String_ ? '"' : ''; - $replacements[] = [ - $node->getStartFilePos(), - $node->getEndFilePos(), - $quote . addcslashes($node->value, "\x00..\x1F") . $quote, - ]; - } - } - // HEREDOC => "string" - foreach ($nodeFinder->findInstanceOf($statements, Node\Scalar\Encapsed::class) as $node) { - if ($node->getAttribute('kind') === Node\Scalar\String_::KIND_HEREDOC) { - $replacements[] = [ - $node->getStartFilePos(), - $node->parts[0]->getStartFilePos() - 1, - '"', - ]; - $replacements[] = [ - end($node->parts)->getEndFilePos() + 1, - $node->getEndFilePos(), - '"', - ]; + /** @return Attribute[] */ + private function getAttributes($from): array + { + return array_map(function ($attr) { + $args = $attr->getArguments(); + foreach ($args as &$arg) { + if (is_object($arg)) { + $arg = $this->fromObject($arg); + } } - } - //sort collected resolved names by position in file - usort($replacements, function ($a, $b) { - return $a[0] <=> $b[0]; - }); - $correctiveOffset = -$start; - //replace changes body length so we need correct offset - foreach ($replacements as [$startPos, $endPos, $replacement]) { - $replacingStringLength = $endPos - $startPos + 1; - $body = substr_replace( - $body, - $replacement, - $correctiveOffset + $startPos, - $replacingStringLength - ); - $correctiveOffset += strlen($replacement) - $replacingStringLength; - } - return $body; + return new Attribute($attr->getName(), $args); + }, $from->getAttributes()); } - private function parse($from): array + private function getVisibility(\ReflectionProperty|\ReflectionMethod|\ReflectionClassConstant $from): Visibility { - $file = $from->getFileName(); - if (!class_exists(ParserFactory::class)) { - throw new Nette\NotSupportedException("PHP-Parser is required to load method bodies, install package 'nikic/php-parser'."); - } elseif (!$file) { - throw new Nette\InvalidStateException("Source code of $from->name not found."); - } - - $lexer = new PhpParser\Lexer(['usedAttributes' => ['startFilePos', 'endFilePos']]); - $parser = (new ParserFactory)->create(ParserFactory::ONLY_PHP7, $lexer); - $code = file_get_contents($file); - $code = str_replace("\r\n", "\n", $code); - $stmts = $parser->parse($code); + return $from->isPrivate() + ? Visibility::Private + : ($from->isProtected() ? Visibility::Protected : Visibility::Public); + } - $traverser = new PhpParser\NodeTraverser; - $traverser->addVisitor(new PhpParser\NodeVisitor\NameResolver(null, ['replaceNodes' => false])); - $stmts = $traverser->traverse($stmts); - return [$code, $stmts]; + private function getExtractor(string $file): Extractor + { + $cache = &$this->extractorCache[$file]; + $cache ??= new Extractor(file_get_contents($file)); + return $cache; } } diff --git a/src/PhpGenerator/GlobalFunction.php b/src/PhpGenerator/GlobalFunction.php index 804259a3..c2ed0ac0 100644 --- a/src/PhpGenerator/GlobalFunction.php +++ b/src/PhpGenerator/GlobalFunction.php @@ -13,39 +13,29 @@ /** - * Global function. - * - * @property string $body + * Definition of a global function. */ final class GlobalFunction { - use Nette\SmartObject; use Traits\FunctionLike; use Traits\NameAware; use Traits\CommentAware; + use Traits\AttributeAware; - public static function from(string $function): self + public static function from(string|\Closure $function, bool $withBody = false): self { - return (new Factory)->fromFunctionReflection(new \ReflectionFunction($function)); + return (new Factory)->fromFunctionReflection(Nette\Utils\Callback::toReflection($function), $withBody); } - public static function withBodyFrom(string $function): self + public function __toString(): string { - return (new Factory)->fromFunctionReflection(new \ReflectionFunction($function), true); + return (new Printer)->printFunction($this); } - public function __toString(): string + public function __clone(): void { - try { - return (new Printer)->printFunction($this); - } catch (\Throwable $e) { - if (PHP_VERSION_ID >= 70400) { - throw $e; - } - trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR); - return ''; - } + $this->parameters = array_map(fn($param) => clone $param, $this->parameters); } } diff --git a/src/PhpGenerator/Helpers.php b/src/PhpGenerator/Helpers.php index 24a8f444..4047933e 100644 --- a/src/PhpGenerator/Helpers.php +++ b/src/PhpGenerator/Helpers.php @@ -10,6 +10,7 @@ namespace Nette\PhpGenerator; use Nette; +use function is_string, preg_match, preg_replace, preg_replace_callback, str_contains, str_repeat, str_replace, strrpos, strtolower, substr, trim; /** @@ -19,39 +20,64 @@ final class Helpers { use Nette\StaticClass; - public const PHP_IDENT = '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*'; - - - /** @deprecated use Nette\PhpGenerator\Dumper::dump() */ - public static function dump($var): string - { - return (new Dumper)->dump($var); - } - - - /** @deprecated use Nette\PhpGenerator\Dumper::format() */ - public static function format(string $statement, ...$args): string + public const ReIdentifier = '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*'; + + public const Keywords = [ + // class keywords + 'bool' => 1, 'false' => 1, 'float' => 1, 'int' => 1, 'iterable' => 1, 'mixed' => 1, 'never' => 1, 'null' => 1, + 'object' => 1, 'parent' => 1, 'self' => 1, 'string' => 1, 'true' => 1, 'void' => 1, + + // PHP keywords + '__halt_compiler' => 1, 'abstract' => 1, 'and' => 1, 'array' => 1, 'as' => 1, 'break' => 1, 'callable' => 1, + 'case' => 1, 'catch' => 1, 'class' => 1, 'clone' => 1, 'const' => 1, 'continue' => 1, 'declare' => 1, 'default' => 1, + 'die' => 1, 'do' => 1, 'echo' => 1, 'else' => 1, 'elseif' => 1, 'empty' => 1, 'enddeclare' => 1, 'endfor' => 1, + 'endforeach' => 1, 'endif' => 1, 'endswitch' => 1, 'endwhile' => 1, 'eval' => 1, 'exit' => 1, 'extends' => 1, + 'final' => 1, 'finally' => 1, 'fn' => 1, 'for' => 1, 'foreach' => 1, 'function' => 1, 'global' => 1, 'goto' => 1, + 'if' => 1, 'implements' => 1, 'include' => 1, 'include_once' => 1, 'instanceof' => 1, 'insteadof' => 1, + 'interface' => 1, 'isset' => 1, 'list' => 1, 'match' => 1, 'namespace' => 1, 'new' => 1, 'or' => 1, 'print' => 1, + 'private' => 1, 'protected' => 1, 'public' => 1, 'readonly' => 1, 'require' => 1, 'require_once' => 1, 'return' => 1, + 'static' => 1, 'switch' => 1, 'throw' => 1, 'trait' => 1, 'try' => 1, 'unset' => 1, 'use' => 1, 'var' => 1, + 'while' => 1, 'xor' => 1, 'yield' => 1, '__CLASS__' => 1, '__DIR__' => 1, '__FILE__' => 1, '__FUNCTION__' => 1, + '__LINE__' => 1, '__METHOD__' => 1, '__NAMESPACE__' => 1, '__PROPERTY__' => 1, '__TRAIT__' => 1, + ]; + + #[\Deprecated] + public const + PHP_IDENT = self::ReIdentifier, + KEYWORDS = self::Keywords; + + + public static function formatDocComment(string $content, bool $forceMultiLine = false): string { - return (new Dumper)->format($statement, ...$args); + $s = trim($content); + $s = str_replace('*/', '* /', $s); + if ($s === '') { + return ''; + } elseif ($forceMultiLine || str_contains($content, "\n")) { + $s = str_replace("\n", "\n * ", "/**\n$s") . "\n */"; + return Nette\Utils\Strings::normalize($s) . "\n"; + } else { + return "/** $s */\n"; + } } - /** @deprecated use Nette\PhpGenerator\Dumper::format() */ - public static function formatArgs(string $statement, array $args): string + public static function tagName(string $name, string $of = PhpNamespace::NameNormal): string { - return (new Dumper)->format($statement, ...$args); + return isset(self::Keywords[strtolower($name)]) + ? $name + : "/*($of*/$name"; } - public static function formatDocComment(string $content): string + public static function simplifyTaggedNames(string $code, ?PhpNamespace $namespace): string { - if (($s = trim($content)) === '') { - return ''; - } elseif (strpos($content, "\n") === false) { - return "/** $s */\n"; - } else { - return str_replace("\n", "\n * ", "/**\n$s") . "\n */\n"; - } + return preg_replace_callback('~/\*\(([ncf])\*/([\w\x7f-\xff\\\]++)~', function ($m) use ($namespace) { + [, $of, $name] = $m; + return $namespace + ? $namespace->simplifyType($name, $of) + : $name; + }, $code); } @@ -63,19 +89,21 @@ public static function unformatDocComment(string $comment): string public static function unindent(string $s, int $level = 1): string { - return preg_replace('#^(\t|\ \ \ \ ){1,' . $level . '}#m', '', $s); + return $level + ? preg_replace('#^(\t| {4}){1,' . $level . '}#m', '', $s) + : $s; } - public static function isIdentifier($value): bool + public static function isIdentifier(mixed $value): bool { - return is_string($value) && preg_match('#^' . self::PHP_IDENT . '$#D', $value); + return is_string($value) && preg_match('#^' . self::ReIdentifier . '$#D', $value); } - public static function isNamespaceIdentifier($value, bool $allowLeadingSlash = false): bool + public static function isNamespaceIdentifier(mixed $value, bool $allowLeadingSlash = false): bool { - $re = '#^' . ($allowLeadingSlash ? '\\\\?' : '') . self::PHP_IDENT . '(\\\\' . self::PHP_IDENT . ')*$#D'; + $re = '#^' . ($allowLeadingSlash ? '\\\?' : '') . self::ReIdentifier . '(\\\\' . self::ReIdentifier . ')*$#D'; return is_string($value) && preg_match($re, $value); } @@ -88,7 +116,9 @@ public static function extractNamespace(string $name): string public static function extractShortName(string $name): string { - return ($pos = strrpos($name, '\\')) === false ? $name : substr($name, $pos + 1); + return ($pos = strrpos($name, '\\')) === false + ? $name + : substr($name, $pos + 1); } @@ -98,9 +128,29 @@ public static function tabsToSpaces(string $s, int $count = 4): string } - /** @internal */ - public static function createObject(string $class, array $props) + /** + * @param mixed[] $props + * @internal + */ + public static function createObject(string $class, array $props): object { return Dumper::createObject($class, $props); } + + + public static function validateType(?string $type, bool &$nullable = false): ?string + { + if ($type === '' || $type === null) { + return null; + } elseif (!Nette\Utils\Validators::isTypeDeclaration($type)) { + throw new Nette\InvalidArgumentException("Value '$type' is not valid type."); + } + + if ($type[0] === '?') { + $nullable = true; + return substr($type, 1); + } + + return $type; + } } diff --git a/src/PhpGenerator/InterfaceType.php b/src/PhpGenerator/InterfaceType.php new file mode 100644 index 00000000..3d179d25 --- /dev/null +++ b/src/PhpGenerator/InterfaceType.php @@ -0,0 +1,95 @@ +validateNames($names); + $this->extends = $names; + return $this; + } + + + /** @return string[] */ + public function getExtends(): array + { + return $this->extends; + } + + + public function addExtend(string $name): static + { + $this->validateNames([$name]); + $this->extends[] = $name; + return $this; + } + + + /** + * Adds a member. If it already exists, throws an exception or overwrites it if $overwrite is true. + */ + public function addMember(Method|Constant|Property $member, bool $overwrite = false): static + { + $name = $member->getName(); + [$type, $n] = match (true) { + $member instanceof Constant => ['consts', $name], + $member instanceof Method => ['methods', strtolower($name)], + $member instanceof Property => ['properties', $name], + }; + if (!$overwrite && isset($this->$type[$n])) { + throw new Nette\InvalidStateException("Cannot add member '$name', because it already exists."); + } + $this->$type[$n] = $member; + return $this; + } + + + /** @throws Nette\InvalidStateException */ + public function validate(): void + { + foreach ($this->getProperties() as $property) { + if ($property->isInitialized()) { + throw new Nette\InvalidStateException("Property {$this->getName()}::\${$property->getName()}: Interface cannot have initialized properties."); + } elseif (!$property->getHooks()) { + throw new Nette\InvalidStateException("Property {$this->getName()}::\${$property->getName()}: Interface cannot have properties without hooks."); + } + } + } + + + public function __clone(): void + { + parent::__clone(); + $clone = fn($item) => clone $item; + $this->consts = array_map($clone, $this->consts); + $this->methods = array_map($clone, $this->methods); + $this->properties = array_map($clone, $this->properties); + } +} diff --git a/src/PhpGenerator/Literal.php b/src/PhpGenerator/Literal.php index 48dbd83e..3171cb9c 100644 --- a/src/PhpGenerator/Literal.php +++ b/src/PhpGenerator/Literal.php @@ -15,18 +15,35 @@ */ class Literal { - /** @var string */ - private $value; + /** + * Creates a literal representing the creation of an object using the new operator. + * @param mixed[] $args + */ + public static function new(string $class, array $args = []): self + { + return new self('new ' . $class . '(...?:)', [$args]); + } - public function __construct(string $value) - { - $this->value = $value; + public function __construct( + private string $value, + /** @var ?mixed[] */ + private ?array $args = null, + ) { } public function __toString(): string { - return $this->value; + return $this->formatWith(new Dumper); + } + + + /** @internal */ + public function formatWith(Dumper $dumper): string + { + return $this->args === null + ? $this->value + : $dumper->format($this->value, ...$this->args); } } diff --git a/src/PhpGenerator/Method.php b/src/PhpGenerator/Method.php index ff00f4d4..5816fbe0 100644 --- a/src/PhpGenerator/Method.php +++ b/src/PhpGenerator/Method.php @@ -10,38 +10,31 @@ namespace Nette\PhpGenerator; use Nette; +use function func_num_args; /** - * Class method. - * - * @property string|null $body + * Definition of a class method. */ final class Method { - use Nette\SmartObject; use Traits\FunctionLike; use Traits\NameAware; use Traits\VisibilityAware; use Traits\CommentAware; + use Traits\AttributeAware; - /** @var string|null */ - private $body = ''; + public const Constructor = '__construct'; - /** @var bool */ - private $static = false; - - /** @var bool */ - private $final = false; - - /** @var bool */ - private $abstract = false; + private bool $static = false; + private bool $final = false; + private bool $abstract = false; /** - * @param string|array $method + * @param string|array{object|string, string}|\Closure $method */ - public static function from($method): self + public static function from(string|array|\Closure $method): static { return (new Factory)->fromMethodReflection(Nette\Utils\Callback::toReflection($method)); } @@ -49,34 +42,11 @@ public static function from($method): self public function __toString(): string { - try { - return (new Printer)->printMethod($this); - } catch (\Throwable $e) { - if (PHP_VERSION_ID >= 70400) { - throw $e; - } - trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR); - return ''; - } - } - - - /** @return static */ - public function setBody(?string $code, array $args = null): self - { - $this->body = $args === null || $code === null ? $code : (new Dumper)->format($code, ...$args); - return $this; - } - - - public function getBody(): ?string - { - return $this->body; + return (new Printer)->printMethod($this); } - /** @return static */ - public function setStatic(bool $state = true): self + public function setStatic(bool $state = true): static { $this->static = $state; return $this; @@ -89,8 +59,7 @@ public function isStatic(): bool } - /** @return static */ - public function setFinal(bool $state = true): self + public function setFinal(bool $state = true): static { $this->final = $state; return $this; @@ -103,8 +72,7 @@ public function isFinal(): bool } - /** @return static */ - public function setAbstract(bool $state = true): self + public function setAbstract(bool $state = true): static { $this->abstract = $state; return $this; @@ -117,11 +85,31 @@ public function isAbstract(): bool } + /** + * @param string $name without $ + */ + public function addPromotedParameter(string $name, mixed $defaultValue = null): PromotedParameter + { + $param = new PromotedParameter($name); + if (func_num_args() > 1) { + $param->setDefaultValue($defaultValue); + } + + return $this->parameters[$name] = $param; + } + + /** @throws Nette\InvalidStateException */ public function validate(): void { - if ($this->abstract && ($this->final || $this->visibility === ClassType::VISIBILITY_PRIVATE)) { - throw new Nette\InvalidStateException('Method cannot be abstract and final or private.'); + if ($this->abstract && ($this->final || $this->visibility === Visibility::Private)) { + throw new Nette\InvalidStateException("Method $this->name() cannot be abstract and final or private at the same time."); } } + + + public function __clone(): void + { + $this->parameters = array_map(fn($param) => clone $param, $this->parameters); + } } diff --git a/src/PhpGenerator/Parameter.php b/src/PhpGenerator/Parameter.php index 63daa1aa..910207ec 100644 --- a/src/PhpGenerator/Parameter.php +++ b/src/PhpGenerator/Parameter.php @@ -9,37 +9,26 @@ namespace Nette\PhpGenerator; -use Nette; +use Nette\Utils\Type; /** - * Method parameter description. - * - * @property mixed $defaultValue + * Definition of a function/method parameter. */ -final class Parameter +class Parameter { - use Nette\SmartObject; use Traits\NameAware; + use Traits\AttributeAware; + use Traits\CommentAware; - /** @var bool */ - private $reference = false; + private bool $reference = false; + private ?string $type = null; + private bool $nullable = false; + private bool $hasDefaultValue = false; + private mixed $defaultValue = null; - /** @var string|null */ - private $type; - /** @var bool */ - private $nullable = false; - - /** @var bool */ - private $hasDefaultValue = false; - - /** @var mixed */ - private $defaultValue; - - - /** @return static */ - public function setReference(bool $state = true): self + public function setReference(bool $state = true): static { $this->reference = $state; return $this; @@ -52,49 +41,23 @@ public function isReference(): bool } - /** @return static */ - public function setType(?string $type): self - { - $this->type = $type; - return $this; - } - - - public function getType(): ?string - { - return $this->type; - } - - - /** @deprecated use setType() */ - public function setTypeHint(?string $type): self + public function setType(?string $type): static { - $this->type = $type; + $this->type = Helpers::validateType($type, $this->nullable); return $this; } - /** @deprecated use getType() */ - public function getTypeHint(): ?string + /** @return ($asObject is true ? ?Type : ?string) */ + public function getType(bool $asObject = false): Type|string|null { - return $this->type; + return $asObject && $this->type + ? Type::fromString($this->type) + : $this->type; } - /** - * @deprecated just use setDefaultValue() - * @return static - */ - public function setOptional(bool $state = true): self - { - trigger_error(__METHOD__ . '() is deprecated, use setDefaultValue()', E_USER_DEPRECATED); - $this->hasDefaultValue = $state; - return $this; - } - - - /** @return static */ - public function setNullable(bool $state = true): self + public function setNullable(bool $state = true): static { $this->nullable = $state; return $this; @@ -103,12 +66,11 @@ public function setNullable(bool $state = true): self public function isNullable(): bool { - return $this->nullable; + return $this->nullable || ($this->hasDefaultValue && $this->defaultValue === null); } - /** @return static */ - public function setDefaultValue($val): self + public function setDefaultValue(mixed $val): static { $this->defaultValue = $val; $this->hasDefaultValue = true; @@ -116,7 +78,7 @@ public function setDefaultValue($val): self } - public function getDefaultValue() + public function getDefaultValue(): mixed { return $this->defaultValue; } @@ -126,4 +88,9 @@ public function hasDefaultValue(): bool { return $this->hasDefaultValue; } + + + public function validate(): void + { + } } diff --git a/src/PhpGenerator/PhpFile.php b/src/PhpGenerator/PhpFile.php index 82613250..d1d81571 100644 --- a/src/PhpGenerator/PhpFile.php +++ b/src/PhpGenerator/PhpFile.php @@ -9,11 +9,11 @@ namespace Nette\PhpGenerator; -use Nette; +use function count; /** - * Instance of PHP file. + * Definition of a PHP file. * * Generates: * - opening tag (fromCode($code); + } + + /** + * Adds a class to the file. If it already exists, throws an exception. + * As a parameter, pass the full name with namespace. + */ public function addClass(string $name): ClassType { return $this @@ -40,7 +47,11 @@ public function addClass(string $name): ClassType } - public function addInterface(string $name): ClassType + /** + * Adds an interface to the file. If it already exists, throws an exception. + * As a parameter, pass the full name with namespace. + */ + public function addInterface(string $name): InterfaceType { return $this ->addNamespace(Helpers::extractNamespace($name)) @@ -48,7 +59,11 @@ public function addInterface(string $name): ClassType } - public function addTrait(string $name): ClassType + /** + * Adds a trait to the file. If it already exists, throws an exception. + * As a parameter, pass the full name with namespace. + */ + public function addTrait(string $name): TraitType { return $this ->addNamespace(Helpers::extractNamespace($name)) @@ -56,15 +71,55 @@ public function addTrait(string $name): ClassType } - public function addNamespace(string $name): PhpNamespace + /** + * Adds an enum to the file. If it already exists, throws an exception. + * As a parameter, pass the full name with namespace. + */ + public function addEnum(string $name): EnumType { - if (!isset($this->namespaces[$name])) { - $this->namespaces[$name] = new PhpNamespace($name); - foreach ($this->namespaces as $namespace) { - $namespace->setBracketedSyntax(count($this->namespaces) > 1 && isset($this->namespaces[''])); - } + return $this + ->addNamespace(Helpers::extractNamespace($name)) + ->addEnum(Helpers::extractShortName($name)); + } + + + /** + * Adds a function to the file. If it already exists, throws an exception. + * As a parameter, pass the full name with namespace. + */ + public function addFunction(string $name): GlobalFunction + { + return $this + ->addNamespace(Helpers::extractNamespace($name)) + ->addFunction(Helpers::extractShortName($name)); + } + + + /** + * Adds a namespace to the file. If it already exists, it returns the existing one. + */ + public function addNamespace(string|PhpNamespace $namespace): PhpNamespace + { + $res = $namespace instanceof PhpNamespace + ? ($this->namespaces[$namespace->getName()] = $namespace) + : ($this->namespaces[$namespace] ??= new PhpNamespace($namespace)); + + foreach ($this->namespaces as $namespace) { + $namespace->setBracketedSyntax(count($this->namespaces) > 1 && isset($this->namespaces[''])); } - return $this->namespaces[$name]; + + return $res; + } + + + /** + * Removes the namespace from the file. + */ + public function removeNamespace(string|PhpNamespace $namespace): static + { + $name = $namespace instanceof PhpNamespace ? $namespace->getName() : $namespace; + unset($this->namespaces[$name]); + return $this; } @@ -75,33 +130,57 @@ public function getNamespaces(): array } - /** @return static */ - public function addUse(string $name, string $alias = null): self + /** @return (ClassType|InterfaceType|TraitType|EnumType)[] */ + public function getClasses(): array { - $this->addNamespace('')->addUse($name, $alias); - return $this; + $classes = []; + foreach ($this->namespaces as $n => $namespace) { + $n .= $n ? '\\' : ''; + foreach ($namespace->getClasses() as $c => $class) { + $classes[$n . $c] = $class; + } + } + + return $classes; + } + + + /** @return GlobalFunction[] */ + public function getFunctions(): array + { + $functions = []; + foreach ($this->namespaces as $n => $namespace) { + $n .= $n ? '\\' : ''; + foreach ($namespace->getFunctions() as $f => $function) { + $functions[$n . $f] = $function; + } + } + + return $functions; } /** - * Adds declare(strict_types=1) to output. - * @return static + * Adds a use statement to the file, to the global namespace. */ - public function setStrictTypes(bool $on = true): self + public function addUse(string $name, ?string $alias = null, string $of = PhpNamespace::NameNormal): static { - $this->strictTypes = $on; + $this->addNamespace('')->addUse($name, $alias, $of); return $this; } - public function hasStrictTypes(): bool + /** + * Adds declare(strict_types=1) to output. + */ + public function setStrictTypes(bool $state = true): static { - return $this->strictTypes; + $this->strictTypes = $state; + return $this; } - /** @deprecated use hasStrictTypes() */ - public function getStrictTypes(): bool + public function hasStrictTypes(): bool { return $this->strictTypes; } @@ -109,14 +188,6 @@ public function getStrictTypes(): bool public function __toString(): string { - try { - return (new Printer)->printFile($this); - } catch (\Throwable $e) { - if (PHP_VERSION_ID >= 70400) { - throw $e; - } - trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR); - return ''; - } + return (new Printer)->printFile($this); } } diff --git a/src/PhpGenerator/PhpLiteral.php b/src/PhpGenerator/PhpLiteral.php index 10fd5a56..460c5c4e 100644 --- a/src/PhpGenerator/PhpLiteral.php +++ b/src/PhpGenerator/PhpLiteral.php @@ -10,6 +10,7 @@ namespace Nette\PhpGenerator; +/** @deprecated use Nette\PhpGenerator\Literal */ class PhpLiteral extends Literal { } diff --git a/src/PhpGenerator/PhpNamespace.php b/src/PhpGenerator/PhpNamespace.php index 1447f6f5..ffc46bd2 100644 --- a/src/PhpGenerator/PhpNamespace.php +++ b/src/PhpGenerator/PhpNamespace.php @@ -11,11 +11,12 @@ use Nette; use Nette\InvalidStateException; -use Nette\Utils\Strings; +use function strlen; +use const ARRAY_FILTER_USE_BOTH; /** - * Namespaced part of a PHP file. + * Definition of a PHP namespace. * * Generates: * - namespace statement @@ -24,25 +25,36 @@ */ final class PhpNamespace { - use Nette\SmartObject; + public const + NameNormal = 'n', + NameFunction = 'f', + NameConstant = 'c'; - private const KEYWORDS = [ - 'string' => 1, 'int' => 1, 'float' => 1, 'bool' => 1, 'array' => 1, 'object' => 1, - 'callable' => 1, 'iterable' => 1, 'void' => 1, 'self' => 1, 'parent' => 1, 'static' => 1, - 'mixed' => 1, 'null' => 1, 'false' => 1, - ]; + #[\Deprecated('use PhpNamespace::NameNormal')] + public const NAME_NORMAL = self::NameNormal; + + #[\Deprecated('use PhpNamespace::NameFunction')] + public const NAME_FUNCTION = self::NameFunction; + + #[\Deprecated('use PhpNamespace::NameConstant')] + public const NAME_CONSTANT = self::NameConstant; - /** @var string */ - private $name; + private string $name; - /** @var bool */ - private $bracketedSyntax = false; + private bool $bracketedSyntax = false; + + /** @var string[][] */ + private array $aliases = [ + self::NameNormal => [], + self::NameFunction => [], + self::NameConstant => [], + ]; - /** @var string[] */ - private $uses = []; + /** @var (ClassType|InterfaceType|TraitType|EnumType)[] */ + private array $classes = []; - /** @var ClassType[] */ - private $classes = []; + /** @var GlobalFunction[] */ + private array $functions = []; public function __construct(string $name) @@ -50,6 +62,7 @@ public function __construct(string $name) if ($name !== '' && !Helpers::isNamespaceIdentifier($name)) { throw new Nette\InvalidArgumentException("Value '$name' is not valid name."); } + $this->name = $name; } @@ -61,10 +74,9 @@ public function getName(): string /** - * @return static * @internal */ - public function setBracketedSyntax(bool $state = true): self + public function setBracketedSyntax(bool $state = true): static { $this->bracketedSyntax = $state; return $this; @@ -77,93 +89,195 @@ public function hasBracketedSyntax(): bool } - /** @deprecated use hasBracketedSyntax() */ - public function getBracketedSyntax(): bool - { - return $this->bracketedSyntax; - } - - /** + * Adds a use statement to the namespace for class, function or constant. * @throws InvalidStateException - * @return static */ - public function addUse(string $name, string $alias = null, string &$aliasOut = null): self + public function addUse(string $name, ?string $alias = null, string $of = self::NameNormal): static { - $name = ltrim($name, '\\'); - if ($alias === null && $this->name === Helpers::extractNamespace($name)) { - $alias = Helpers::extractShortName($name); + if ( + !Helpers::isNamespaceIdentifier($name, allowLeadingSlash: true) + || (Helpers::isIdentifier($name) && isset(Helpers::Keywords[strtolower($name)])) + ) { + throw new Nette\InvalidArgumentException("Value '$name' is not valid class/function/constant name."); + + } elseif ($alias && (!Helpers::isIdentifier($alias) || isset(Helpers::Keywords[strtolower($alias)]))) { + throw new Nette\InvalidArgumentException("Value '$alias' is not valid alias."); } + + $name = ltrim($name, '\\'); + $aliases = array_change_key_case($this->aliases[$of]); + $used = [self::NameNormal => $this->classes, self::NameFunction => $this->functions, self::NameConstant => []][$of]; + if ($alias === null) { - $path = explode('\\', $name); + $base = Helpers::extractShortName($name); $counter = null; do { - if (empty($path)) { - $counter++; - } else { - $alias = array_pop($path) . $alias; - } - } while (isset($this->uses[$alias . $counter]) && $this->uses[$alias . $counter] !== $name); - $alias .= $counter; - - } elseif (isset($this->uses[$alias]) && $this->uses[$alias] !== $name) { - throw new InvalidStateException( - "Alias '$alias' used already for '{$this->uses[$alias]}', cannot use for '{$name}'." - ); + $alias = $base . $counter; + $lower = strtolower($alias); + $counter++; + } while ((isset($aliases[$lower]) && strcasecmp($aliases[$lower], $name) !== 0) || isset($used[$lower])); + } else { + $lower = strtolower($alias); + if (isset($aliases[$lower]) && strcasecmp($aliases[$lower], $name) !== 0) { + throw new InvalidStateException( + "Alias '$alias' used already for '{$aliases[$lower]}', cannot use for '$name'.", + ); + } elseif (isset($used[$lower])) { + throw new Nette\InvalidStateException("Name '$alias' used already for '$this->name\\{$used[$lower]->getName()}'."); + } } - $aliasOut = $alias; - $this->uses[$alias] = $name; - asort($this->uses); + $this->aliases[$of][$alias] = $name; return $this; } + public function removeUse(string $name, string $of = self::NameNormal): void + { + foreach ($this->aliases[$of] as $alias => $item) { + if (strcasecmp($item, $name) === 0) { + unset($this->aliases[$of][$alias]); + } + } + } + + + /** + * Adds a use statement to the namespace for function. + */ + public function addUseFunction(string $name, ?string $alias = null): static + { + return $this->addUse($name, $alias, self::NameFunction); + } + + + /** + * Adds a use statement to the namespace for constant. + */ + public function addUseConstant(string $name, ?string $alias = null): static + { + return $this->addUse($name, $alias, self::NameConstant); + } + + /** @return string[] */ - public function getUses(): array + public function getUses(string $of = self::NameNormal): array { - return $this->uses; + uasort($this->aliases[$of], fn(string $a, string $b): int => strtr($a, '\\', ' ') <=> strtr($b, '\\', ' ')); + return array_filter( + $this->aliases[$of], + fn($name, $alias) => (bool) strcasecmp(($this->name ? $this->name . '\\' : '') . $alias, $name), + ARRAY_FILTER_USE_BOTH, + ); } - public function unresolveName(string $name): string + /** + * Resolves relative name to full name. + */ + public function resolveName(string $name, string $of = self::NameNormal): string { - if (isset(self::KEYWORDS[strtolower($name)]) || $name === '') { + if (isset(Helpers::Keywords[strtolower($name)]) || $name === '') { return $name; + } elseif ($name[0] === '\\') { + return substr($name, 1); } + + $aliases = array_change_key_case($this->aliases[$of]); + if ($of !== self::NameNormal) { + return $aliases[strtolower($name)] + ?? $this->resolveName(Helpers::extractNamespace($name) . '\\') . Helpers::extractShortName($name); + } + + $parts = explode('\\', $name, 2); + return ($res = $aliases[strtolower($parts[0])] ?? null) + ? $res . (isset($parts[1]) ? '\\' . $parts[1] : '') + : $this->name . ($this->name ? '\\' : '') . $name; + } + + + /** + * Simplifies type hint with relative names. + */ + public function simplifyType(string $type, string $of = self::NameNormal): string + { + return preg_replace_callback('~[\w\x7f-\xff\\\]+~', fn($m) => $this->simplifyName($m[0], $of), $type); + } + + + /** + * Simplifies the full name of a class, function, or constant to a relative name. + */ + public function simplifyName(string $name, string $of = self::NameNormal): string + { + if (isset(Helpers::Keywords[strtolower($name)]) || $name === '') { + return $name; + } + $name = ltrim($name, '\\'); - $res = null; - $lower = strtolower($name); - foreach ($this->uses as $alias => $original) { - if (Strings::startsWith($lower . '\\', strtolower($original) . '\\')) { + + if ($of !== self::NameNormal) { + foreach ($this->aliases[$of] as $alias => $original) { + if (strcasecmp($original, $name) === 0) { + return $alias; + } + } + + return $this->simplifyName(Helpers::extractNamespace($name) . '\\') . Helpers::extractShortName($name); + } + + $shortest = null; + $relative = self::startsWith($name, $this->name . '\\') + ? substr($name, strlen($this->name) + 1) + : null; + + foreach ($this->aliases[$of] as $alias => $original) { + if ($relative && self::startsWith($relative . '\\', $alias . '\\')) { + $relative = null; + } + + if (self::startsWith($name . '\\', $original . '\\')) { $short = $alias . substr($name, strlen($original)); - if (!isset($res) || strlen($res) > strlen($short)) { - $res = $short; + if (!isset($shortest) || strlen($shortest) > strlen($short)) { + $shortest = $short; } } } - if (!$res && Strings::startsWith($lower, strtolower($this->name) . '\\')) { - return substr($name, strlen($this->name) + 1); - } else { - return $res ?: ($this->name ? '\\' : '') . $name; + if (isset($shortest, $relative) && strlen($shortest) < strlen($relative)) { + return $shortest; } + + return $relative ?? $shortest ?? ($this->name ? '\\' : '') . $name; } - /** @return static */ - public function add(ClassType $class): self + /** + * Adds a class-like type to the namespace. If it already exists, throws an exception. + */ + public function add(ClassType|InterfaceType|TraitType|EnumType $class): static { $name = $class->getName(); if ($name === null) { throw new Nette\InvalidArgumentException('Class does not have a name.'); } - $this->addUse($this->name . '\\' . $name); - $this->classes[$name] = $class; + + $lower = strtolower($name); + if (isset($this->classes[$lower]) && $this->classes[$lower] !== $class) { + throw new Nette\InvalidStateException("Cannot add '$name', because it already exists."); + } elseif ($orig = array_change_key_case($this->aliases[self::NameNormal])[$lower] ?? null) { + throw new Nette\InvalidStateException("Name '$name' used already as alias for $orig."); + } + + $this->classes[$lower] = $class; return $this; } + /** + * Adds a class to the namespace. If it already exists, throws an exception. + */ public function addClass(string $name): ClassType { $this->add($class = new ClassType($name, $this)); @@ -171,35 +285,128 @@ public function addClass(string $name): ClassType } - public function addInterface(string $name): ClassType + /** + * Adds an interface to the namespace. If it already exists, throws an exception. + */ + public function addInterface(string $name): InterfaceType + { + $this->add($iface = new InterfaceType($name, $this)); + return $iface; + } + + + /** + * Adds a trait to the namespace. If it already exists, throws an exception. + */ + public function addTrait(string $name): TraitType { - return $this->addClass($name)->setInterface(); + $this->add($trait = new TraitType($name, $this)); + return $trait; } - public function addTrait(string $name): ClassType + /** + * Adds an enum to the namespace. If it already exists, throws an exception. + */ + public function addEnum(string $name): EnumType { - return $this->addClass($name)->setTrait(); + $this->add($enum = new EnumType($name, $this)); + return $enum; } - /** @return ClassType[] */ + /** + * Returns a class-like type from the namespace. + */ + public function getClass(string $name): ClassType|InterfaceType|TraitType|EnumType + { + return $this->classes[strtolower($name)] ?? throw new Nette\InvalidArgumentException("Class '$name' not found."); + } + + + /** + * Returns all class-like types in the namespace. + * @return (ClassType|InterfaceType|TraitType|EnumType)[] + */ public function getClasses(): array { - return $this->classes; + $res = []; + foreach ($this->classes as $class) { + $res[$class->getName()] = $class; + } + + return $res; } - public function __toString(): string + /** + * Removes a class-like type from namespace. + */ + public function removeClass(string $name): static { - try { - return (new Printer)->printNamespace($this); - } catch (\Throwable $e) { - if (PHP_VERSION_ID >= 70400) { - throw $e; - } - trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR); - return ''; + unset($this->classes[strtolower($name)]); + return $this; + } + + + /** + * Adds a function to the namespace. If it already exists, throws an exception. + */ + public function addFunction(string $name): GlobalFunction + { + $lower = strtolower($name); + if (isset($this->functions[$lower])) { + throw new Nette\InvalidStateException("Cannot add '$name', because it already exists."); + } elseif ($orig = array_change_key_case($this->aliases[self::NameFunction])[$lower] ?? null) { + throw new Nette\InvalidStateException("Name '$name' used already as alias for $orig."); + } + + return $this->functions[$lower] = new GlobalFunction($name); + } + + + /** + * Returns a function from the namespace. + */ + public function getFunction(string $name): GlobalFunction + { + return $this->functions[strtolower($name)] ?? throw new Nette\InvalidArgumentException("Function '$name' not found."); + } + + + /** + * Returns all functions in the namespace. + * @return GlobalFunction[] + */ + public function getFunctions(): array + { + $res = []; + foreach ($this->functions as $fn) { + $res[$fn->getName()] = $fn; } + + return $res; + } + + + /** + * Removes a function type from namespace. + */ + public function removeFunction(string $name): static + { + unset($this->functions[strtolower($name)]); + return $this; + } + + + private static function startsWith(string $a, string $b): bool + { + return strncasecmp($a, $b, strlen($b)) === 0; + } + + + public function __toString(): string + { + return (new Printer)->printNamespace($this); } } diff --git a/src/PhpGenerator/Printer.php b/src/PhpGenerator/Printer.php index 23dc76ca..3d629907 100644 --- a/src/PhpGenerator/Printer.php +++ b/src/PhpGenerator/Printer.php @@ -11,6 +11,7 @@ use Nette; use Nette\Utils\Strings; +use function array_filter, array_map, count, end, get_debug_type, implode, is_scalar, ltrim, preg_replace, rtrim, str_contains, str_repeat, str_replace, strlen, substr; /** @@ -18,164 +19,253 @@ */ class Printer { - use Nette\SmartObject; - - /** @var string */ - protected $indentation = "\t"; - - /** @var int */ - protected $linesBetweenProperties = 0; - - /** @var int */ - protected $linesBetweenMethods = 2; - - /** @var string */ - protected $returnTypeColon = ': '; - - /** @var bool */ - private $resolveTypes = true; + public int $wrapLength = 120; + public string $indentation = "\t"; + public int $linesBetweenProperties = 0; + public int $linesBetweenMethods = 2; + public int $linesBetweenUseTypes = 0; + public string $returnTypeColon = ': '; + public bool $bracesOnNextLine = true; + public bool $singleParameterOnOneLine = false; + public bool $omitEmptyNamespaces = true; + protected ?PhpNamespace $namespace = null; + protected ?Dumper $dumper; + private bool $resolveTypes = true; + + + public function __construct() + { + $this->dumper = new Dumper; + } - public function printFunction(GlobalFunction $function, PhpNamespace $namespace = null): string + public function printFunction(GlobalFunction $function, ?PhpNamespace $namespace = null): string { - return Helpers::formatDocComment($function->getComment() . "\n") - . 'function ' + $this->namespace = $this->resolveTypes ? $namespace : null; + $line = 'function ' . ($function->getReturnReference() ? '&' : '') - . $function->getName() - . $this->printParameters($function, $namespace) - . $this->printReturnType($function, $namespace) - . "\n{\n" . $this->indent(ltrim(rtrim($function->getBody()) . "\n")) . "}\n"; + . $function->getName(); + $returnType = $this->printReturnType($function); + $params = $this->printParameters($function, strlen($line) + strlen($returnType) + 2); // 2 = parentheses + $body = $this->printFunctionBody($function); + $braceOnNextLine = $this->isBraceOnNextLine(str_contains($params, "\n"), (bool) $returnType); + + return $this->printDocComment($function) + . $this->printAttributes($function->getAttributes()) + . $line + . $params + . $returnType + . ($braceOnNextLine ? "\n" : ' ') + . "{\n" . $this->indent($body) . "}\n"; } - public function printClosure(Closure $closure): string + public function printClosure(Closure $closure, ?PhpNamespace $namespace = null): string { + $this->namespace = $this->resolveTypes ? $namespace : null; $uses = []; foreach ($closure->getUses() as $param) { $uses[] = ($param->isReference() ? '&' : '') . '$' . $param->getName(); } - $useStr = strlen($tmp = implode(', ', $uses)) > (new Dumper)->wrapLength && count($uses) > 1 - ? "\n" . $this->indentation . implode(",\n" . $this->indentation, $uses) . "\n" + + $useStr = strlen($tmp = implode(', ', $uses)) > $this->wrapLength && count($uses) > 1 + ? "\n" . $this->indentation . implode(",\n" . $this->indentation, $uses) . ",\n" : $tmp; + $body = $this->printFunctionBody($closure); - return 'function ' + return $this->printAttributes($closure->getAttributes(), inline: true) + . 'function ' . ($closure->getReturnReference() ? '&' : '') - . $this->printParameters($closure, null) + . $this->printParameters($closure) . ($uses ? " use ($useStr)" : '') - . $this->printReturnType($closure, null) - . " {\n" . $this->indent(ltrim(rtrim($closure->getBody()) . "\n")) . '}'; + . $this->printReturnType($closure) + . " {\n" . $this->indent($body) . '}'; } - public function printArrowFunction(Closure $closure): string + public function printArrowFunction(Closure $closure, ?PhpNamespace $namespace = null): string { + $this->namespace = $this->resolveTypes ? $namespace : null; foreach ($closure->getUses() as $use) { if ($use->isReference()) { throw new Nette\InvalidArgumentException('Arrow function cannot bind variables by-reference.'); } } - return 'fn ' + $body = $this->printFunctionBody($closure); + + return $this->printAttributes($closure->getAttributes()) + . 'fn' . ($closure->getReturnReference() ? '&' : '') - . $this->printParameters($closure, null) - . $this->printReturnType($closure, null) - . ' => ' . trim($closure->getBody()) . ';'; + . $this->printParameters($closure) + . $this->printReturnType($closure) + . ' => ' . rtrim($body, "\n") . ';'; } - public function printMethod(Method $method, PhpNamespace $namespace = null): string + public function printMethod(Method $method, ?PhpNamespace $namespace = null, bool $isInterface = false): string { + $this->namespace = $this->resolveTypes ? $namespace : null; $method->validate(); - return Helpers::formatDocComment($method->getComment() . "\n") - . ($method->isAbstract() ? 'abstract ' : '') + $line = ($method->isAbstract() && !$isInterface ? 'abstract ' : '') . ($method->isFinal() ? 'final ' : '') . ($method->getVisibility() ? $method->getVisibility() . ' ' : '') . ($method->isStatic() ? 'static ' : '') . 'function ' . ($method->getReturnReference() ? '&' : '') - . $method->getName() - . ($params = $this->printParameters($method, $namespace)) - . $this->printReturnType($method, $namespace) - . ($method->isAbstract() || $method->getBody() === null + . $method->getName(); + $returnType = $this->printReturnType($method); + $params = $this->printParameters($method, strlen($line) + strlen($returnType) + strlen($this->indentation) + 2); + $body = $this->printFunctionBody($method); + $braceOnNextLine = $this->isBraceOnNextLine(str_contains($params, "\n"), (bool) $returnType); + + return $this->printDocComment($method) + . $this->printAttributes($method->getAttributes()) + . $line + . $params + . $returnType + . ($method->isAbstract() || $isInterface ? ";\n" - : (strpos($params, "\n") === false ? "\n" : ' ') - . "{\n" - . $this->indent(ltrim(rtrim($method->getBody()) . "\n")) - . "}\n"); + : ($braceOnNextLine ? "\n" : ' ') . "{\n" . $this->indent($body) . "}\n"); } - public function printClass(ClassType $class, PhpNamespace $namespace = null): string + private function printFunctionBody(Closure|GlobalFunction|Method|PropertyHook $function): string { + $code = Helpers::simplifyTaggedNames($function->getBody(), $this->namespace); + $code = Strings::normalize($code); + return ltrim(rtrim($code) . "\n"); + } + + + public function printClass( + ClassType|InterfaceType|TraitType|EnumType $class, + ?PhpNamespace $namespace = null, + ): string + { + $this->namespace = $this->resolveTypes ? $namespace : null; $class->validate(); - $resolver = $this->resolveTypes && $namespace ? [$namespace, 'unresolveName'] : function ($s) { return $s; }; + $resolver = $this->namespace + ? [$namespace, 'simplifyType'] + : fn($s) => $s; $traits = []; - foreach ($class->getTraitResolutions() as $trait => $resolutions) { - $traits[] = 'use ' . $resolver($trait) - . ($resolutions ? " {\n" . $this->indentation . implode(";\n" . $this->indentation, $resolutions) . ";\n}\n" : ";\n"); + if ($class instanceof ClassType || $class instanceof TraitType || $class instanceof EnumType) { + foreach ($class->getTraits() as $trait) { + $resolutions = implode(";\n", $trait->getResolutions()); + $resolutions = Helpers::simplifyTaggedNames($resolutions, $this->namespace); + $traits[] = $this->printDocComment($trait) + . 'use ' . $resolver($trait->getName()) + . ($resolutions + ? " {\n" . $this->indent($resolutions) . ";\n}\n" + : ";\n"); + } } - $consts = []; - foreach ($class->getConstants() as $const) { - $def = ($const->getVisibility() ? $const->getVisibility() . ' ' : '') . 'const ' . $const->getName() . ' = '; - $consts[] = Helpers::formatDocComment((string) $const->getComment()) - . $def - . $this->dump($const->getValue(), strlen($def)) . ";\n"; + $cases = []; + $enumType = null; + if ($class instanceof EnumType) { + $enumType = $class->getType(); + foreach ($class->getCases() as $case) { + $enumType ??= is_scalar($case->getValue()) ? get_debug_type($case->getValue()) : null; + $cases[] = $this->printDocComment($case) + . $this->printAttributes($case->getAttributes()) + . 'case ' . $case->getName() + . ($case->getValue() === null ? '' : ' = ' . $this->dump($case->getValue())) + . ";\n"; + } } - $properties = []; - foreach ($class->getProperties() as $property) { - $type = $property->getType(); - $def = (($property->getVisibility() ?: 'public') . ($property->isStatic() ? ' static' : '') . ' ' - . ltrim($this->printType($type, $property->isNullable(), $namespace) . ' ') - . '$' . $property->getName()); - - $properties[] = Helpers::formatDocComment((string) $property->getComment()) - . $def - . ($property->getValue() === null && !$property->isInitialized() ? '' : ' = ' . $this->dump($property->getValue(), strlen($def) + 3)) // 3 = ' = ' - . ";\n"; + $readOnlyClass = $class instanceof ClassType && $class->isReadOnly(); + $consts = []; + $methods = []; + if ( + $class instanceof ClassType + || $class instanceof InterfaceType + || $class instanceof TraitType + || $class instanceof EnumType + ) { + foreach ($class->getConstants() as $const) { + $consts[] = $this->printConstant($const); + } + + foreach ($class->getMethods() as $method) { + if ($readOnlyClass && $method->getName() === Method::Constructor) { + $method = clone $method; + array_map(fn($param) => $param instanceof PromotedParameter ? $param->setReadOnly(false) : null, $method->getParameters()); + } + $methods[] = $this->printMethod($method, $namespace, $class->isInterface()); + } } - $methods = []; - foreach ($class->getMethods() as $method) { - $methods[] = $this->printMethod($method, $namespace); + $properties = []; + if ($class instanceof ClassType || $class instanceof TraitType || $class instanceof InterfaceType) { + foreach ($class->getProperties() as $property) { + $properties[] = $this->printProperty($property, $readOnlyClass, $class instanceof InterfaceType); + } } $members = array_filter([ implode('', $traits), $this->joinProperties($consts), + $this->joinProperties($cases), $this->joinProperties($properties), ($methods && $properties ? str_repeat("\n", $this->linesBetweenMethods - 1) : '') . implode(str_repeat("\n", $this->linesBetweenMethods), $methods), ]); - return Strings::normalize( - Helpers::formatDocComment($class->getComment() . "\n") - . ($class->isAbstract() ? 'abstract ' : '') - . ($class->isFinal() ? 'final ' : '') - . ($class->getName() ? $class->getType() . ' ' . $class->getName() . ' ' : '') - . ($class->getExtends() ? 'extends ' . implode(', ', array_map($resolver, (array) $class->getExtends())) . ' ' : '') - . ($class->getImplements() ? 'implements ' . implode(', ', array_map($resolver, $class->getImplements())) . ' ' : '') - . ($class->getName() ? "\n" : '') . "{\n" + if ($class instanceof ClassType) { + $line[] = $class->isAbstract() ? 'abstract' : null; + $line[] = $class->isFinal() ? 'final' : null; + $line[] = $class->isReadOnly() ? 'readonly' : null; + } + + $line[] = match (true) { + $class instanceof ClassType => $class->getName() ? 'class ' . $class->getName() : null, + $class instanceof InterfaceType => 'interface ' . $class->getName(), + $class instanceof TraitType => 'trait ' . $class->getName(), + $class instanceof EnumType => 'enum ' . $class->getName() . ($enumType ? $this->returnTypeColon . $enumType : ''), + }; + $line[] = ($class instanceof ClassType || $class instanceof InterfaceType) && $class->getExtends() + ? 'extends ' . implode(', ', array_map($resolver, (array) $class->getExtends())) + : null; + $line[] = ($class instanceof ClassType || $class instanceof EnumType) && $class->getImplements() + ? 'implements ' . implode(', ', array_map($resolver, $class->getImplements())) + : null; + $line[] = $class->getName() ? null : '{'; + + return $this->printDocComment($class) + . $this->printAttributes($class->getAttributes()) + . implode(' ', array_filter($line)) + . ($class->getName() ? "\n{\n" : "\n") . ($members ? $this->indent(implode("\n", $members)) : '') . '}' - ) . ($class->getName() ? "\n" : ''); + . ($class->getName() ? "\n" : ''); } public function printNamespace(PhpNamespace $namespace): string { + $this->namespace = $this->resolveTypes ? $namespace : null; $name = $namespace->getName(); - $uses = $this->printUses($namespace); - - $classes = []; + $uses = [ + $this->printUses($namespace), + $this->printUses($namespace, PhpNamespace::NameFunction), + $this->printUses($namespace, PhpNamespace::NameConstant), + ]; + $uses = implode(str_repeat("\n", $this->linesBetweenUseTypes), array_filter($uses)); + + $items = []; foreach ($namespace->getClasses() as $class) { - $classes[] = $this->printClass($class, $namespace); + $items[] = $this->printClass($class, $namespace); } - $body = ($uses ? $uses . "\n\n" : '') - . implode("\n", $classes); + foreach ($namespace->getFunctions() as $function) { + $items[] = $this->printFunction($function, $namespace); + } + + $body = ($uses ? $uses . "\n" : '') + . implode("\n", $items); if ($namespace->hasBracketedSyntax()) { return 'namespace' . ($name ? " $name" : '') . "\n{\n" @@ -193,103 +283,261 @@ public function printFile(PhpFile $file): string { $namespaces = []; foreach ($file->getNamespaces() as $namespace) { - $namespaces[] = $this->printNamespace($namespace); + if (!$this->omitEmptyNamespaces || $namespace->getClasses() || $namespace->getFunctions()) { + $namespaces[] = $this->printNamespace($namespace); + } } - return Strings::normalize( - "getComment() ? "\n" . Helpers::formatDocComment($file->getComment() . "\n") : '') + return "getComment() ? "\n" . $this->printDocComment($file) : '') . "\n" . ($file->hasStrictTypes() ? "declare(strict_types=1);\n\n" : '') - . implode("\n\n", $namespaces) - ) . "\n"; + . implode("\n\n", $namespaces); } - /** @return static */ - public function setTypeResolving(bool $state = true): self + protected function printUses(PhpNamespace $namespace, string $of = PhpNamespace::NameNormal): string { - $this->resolveTypes = $state; - return $this; + $prefix = [ + PhpNamespace::NameNormal => '', + PhpNamespace::NameFunction => 'function ', + PhpNamespace::NameConstant => 'const ', + ][$of]; + $uses = []; + foreach ($namespace->getUses($of) as $alias => $original) { + $uses[] = Helpers::extractShortName($original) === $alias + ? "use $prefix$original;\n" + : "use $prefix$original as $alias;\n"; + } + + return implode('', $uses); } - protected function indent(string $s): string + protected function printParameters(Closure|GlobalFunction|Method|PropertyHook $function, int $column = 0): string { - $s = str_replace("\t", $this->indentation, $s); - return Strings::indent($s, 1, $this->indentation); + $special = false; + foreach ($function->getParameters() as $param) { + $param->validate(); + $special = $special || $param instanceof PromotedParameter || $param->getAttributes() || $param->getComment(); + } + + if (!$special || ($this->singleParameterOnOneLine && count($function->getParameters()) === 1)) { + $line = $this->formatParameters($function, multiline: false); + if (!str_contains($line, "\n") && strlen($line) + $column <= $this->wrapLength) { + return $line; + } + } + + return $this->formatParameters($function, multiline: true); } - protected function dump($var, int $column = 0): string + private function formatParameters(Closure|GlobalFunction|Method|PropertyHook $function, bool $multiline): string { - return (new Dumper)->dump($var, $column); + $params = $function->getParameters(); + $res = ''; + + foreach ($params as $param) { + $variadic = !$function instanceof PropertyHook && $function->isVariadic() && $param === end($params); + $attrs = $this->printAttributes($param->getAttributes(), inline: true); + $res .= + $this->printDocComment($param) + . ($attrs ? ($multiline ? substr($attrs, 0, -1) . "\n" : $attrs) : '') + . ($param instanceof PromotedParameter + ? ($param->isFinal() ? 'final ' : '') + . $this->printPropertyVisibility($param) + . ($param->isReadOnly() && $param->getType() ? ' readonly' : '') + . ' ' + : '') + . ltrim($this->printType($param->getType(), $param->isNullable()) . ' ') + . ($param->isReference() ? '&' : '') + . ($variadic ? '...' : '') + . '$' . $param->getName() + . ($param->hasDefaultValue() && !$variadic ? ' = ' . $this->dump($param->getDefaultValue()) : '') + . ($param instanceof PromotedParameter ? $this->printHooks($param) : '') + . ($multiline ? ",\n" : ', '); + } + + return $multiline + ? "(\n" . $this->indent($res) . ')' + : '(' . substr($res, 0, -2) . ')'; } - protected function printUses(PhpNamespace $namespace): string + private function printConstant(Constant $const): string { - $name = $namespace->getName(); - $uses = []; - foreach ($namespace->getUses() as $alias => $original) { - if ($original !== ($name ? $name . '\\' . $alias : $alias)) { - if ($alias === $original || substr($original, -(strlen($alias) + 1)) === '\\' . $alias) { - $uses[] = "use $original;"; - } else { - $uses[] = "use $original as $alias;"; - } - } - } - return implode("\n", $uses); + $def = ($const->isFinal() ? 'final ' : '') + . ($const->getVisibility() ? $const->getVisibility() . ' ' : '') + . 'const ' + . ltrim($this->printType($const->getType(), nullable: false) . ' ') + . $const->getName() . ' = '; + + return $this->printDocComment($const) + . $this->printAttributes($const->getAttributes()) + . $def + . $this->dump($const->getValue(), strlen($def)) . ";\n"; } - /** - * @param Closure|GlobalFunction|Method $function - */ - public function printParameters($function, PhpNamespace $namespace = null): string + private function printProperty(Property $property, bool $readOnlyClass = false, bool $isInterface = false): string { - $params = []; - $list = $function->getParameters(); - foreach ($list as $param) { - $variadic = $function->isVariadic() && $param === end($list); - $type = $param->getType(); - $params[] = ltrim($this->printType($type, $param->isNullable(), $namespace) . ' ') - . ($param->isReference() ? '&' : '') - . ($variadic ? '...' : '') - . '$' . $param->getName() - . ($param->hasDefaultValue() && !$variadic ? ' = ' . $this->dump($param->getDefaultValue()) : ''); + $property->validate(); + $type = $property->getType(); + $def = ($property->isAbstract() && !$isInterface ? 'abstract ' : '') + . ($property->isFinal() ? 'final ' : '') + . $this->printPropertyVisibility($property) + . ($property->isStatic() ? ' static' : '') + . (!$readOnlyClass && $property->isReadOnly() && $type ? ' readonly' : '') + . ' ' + . ltrim($this->printType($type, $property->isNullable()) . ' ') + . '$' . $property->getName(); + + $defaultValue = $property->getValue() === null && !$property->isInitialized() + ? '' + : ' = ' . $this->dump($property->getValue(), strlen($def) + 3); // 3 = ' = ' + + return $this->printDocComment($property) + . $this->printAttributes($property->getAttributes()) + . $def + . $defaultValue + . ($this->printHooks($property, $isInterface) ?: ';') + . "\n"; + } + + + private function printPropertyVisibility(Property|PromotedParameter $param): string + { + $get = $param->getVisibility(PropertyAccessMode::Get); + $set = $param->getVisibility(PropertyAccessMode::Set); + return $set + ? ($get ? "$get $set(set)" : "$set(set)") + : $get ?? 'public'; + } + + + protected function printType(?string $type, bool $nullable): string + { + if ($type === null) { + return ''; } - return strlen($tmp = implode(', ', $params)) > (new Dumper)->wrapLength && count($params) > 1 - ? "(\n" . $this->indentation . implode(",\n" . $this->indentation, $params) . "\n)" - : "($tmp)"; + if ($this->namespace) { + $type = $this->namespace->simplifyType($type); + } + + return $nullable + ? Type::nullable($type) + : $type; } - public function printType(?string $type, bool $nullable = false, PhpNamespace $namespace = null): string + protected function printDocComment(/*Traits\CommentAware*/ $commentable): string { - return $type - ? ($nullable ? '?' : '') . ($this->resolveTypes && $namespace ? $namespace->unresolveName($type) : $type) - : ''; + $multiLine = $commentable instanceof GlobalFunction + || $commentable instanceof Method + || $commentable instanceof ClassLike + || $commentable instanceof PhpFile; + return Helpers::formatDocComment((string) $commentable->getComment(), $multiLine); } - /** - * @param Closure|GlobalFunction|Method $function - */ - private function printReturnType($function, ?PhpNamespace $namespace): string + protected function printReturnType(Closure|GlobalFunction|Method $function): string { - return ($tmp = $this->printType($function->getReturnType(), $function->isReturnNullable(), $namespace)) + return ($tmp = $this->printType($function->getReturnType(), $function->isReturnNullable())) ? $this->returnTypeColon . $tmp : ''; } - private function joinProperties(array $props) + /** @param Attribute[] $attrs */ + protected function printAttributes(array $attrs, bool $inline = false): string + { + if (!$attrs) { + return ''; + } + + $this->dumper->indentation = $this->indentation; + $items = []; + foreach ($attrs as $attr) { + $args = $this->dumper->format('...?:', $attr->getArguments()); + $args = Helpers::simplifyTaggedNames($args, $this->namespace); + $items[] = $this->printType($attr->getName(), nullable: false) . ($args === '' ? '' : "($args)"); + $inline = $inline && !str_contains($args, "\n"); + } + + return $inline + ? '#[' . implode(', ', $items) . '] ' + : '#[' . implode("]\n#[", $items) . "]\n"; + } + + + private function printHooks(Property|PromotedParameter $property, bool $isInterface = false): string + { + $hooks = $property->getHooks(); + if (!$hooks) { + return ''; + } + + $simple = true; + foreach ($hooks as $type => $hook) { + $simple = $simple && ($hook->isAbstract() || $isInterface); + $hooks[$type] = $this->printDocComment($hook) + . $this->printAttributes($hook->getAttributes()) + . ($hook->isAbstract() || $isInterface + ? ($hook->getReturnReference() ? '&' : '') + . $type . ';' + : ($hook->isFinal() ? 'final ' : '') + . ($hook->getReturnReference() ? '&' : '') + . $type + . ($hook->getParameters() ? $this->printParameters($hook) : '') + . ' ' + . ($hook->isShort() + ? '=> ' . $hook->getBody() . ';' + : "{\n" . $this->indent($this->printFunctionBody($hook)) . '}')); + } + + return $simple + ? ' { ' . implode(' ', $hooks) . ' }' + : " {\n" . $this->indent(implode("\n", $hooks)) . "\n}"; + } + + + public function setTypeResolving(bool $state = true): static + { + $this->resolveTypes = $state; + return $this; + } + + + protected function indent(string $s): string + { + $s = str_replace("\t", $this->indentation, $s); + return Strings::indent($s, 1, $this->indentation); + } + + + protected function dump(mixed $var, int $column = 0): string + { + $this->dumper->indentation = $this->indentation; + $this->dumper->wrapLength = $this->wrapLength; + $s = $this->dumper->dump($var, $column); + $s = Helpers::simplifyTaggedNames($s, $this->namespace); + return $s; + } + + + /** @param string[] $props */ + private function joinProperties(array $props): string { return $this->linesBetweenProperties ? implode(str_repeat("\n", $this->linesBetweenProperties), $props) : preg_replace('#^(\w.*\n)\n(?=\w.*;)#m', '$1', implode("\n", $props)); } + + + protected function isBraceOnNextLine(bool $multiLine, bool $hasReturnType): bool + { + return $this->bracesOnNextLine && (!$multiLine || $hasReturnType); + } } diff --git a/src/PhpGenerator/PromotedParameter.php b/src/PhpGenerator/PromotedParameter.php new file mode 100644 index 00000000..f81764ae --- /dev/null +++ b/src/PhpGenerator/PromotedParameter.php @@ -0,0 +1,35 @@ +readOnly && !$this->getType()) { + throw new Nette\InvalidStateException("Property \${$this->getName()}: Read-only properties are only supported on typed property."); + } + } + + + public function __clone(): void + { + $this->hooks = array_map(fn($item) => $item ? clone $item : $item, $this->hooks); + } +} diff --git a/src/PhpGenerator/Property.php b/src/PhpGenerator/Property.php index 6731e6b6..0586ee2f 100644 --- a/src/PhpGenerator/Property.php +++ b/src/PhpGenerator/Property.php @@ -10,52 +10,42 @@ namespace Nette\PhpGenerator; use Nette; +use Nette\Utils\Type; /** - * Class property description. - * - * @property mixed $value + * Definition of a class property. */ final class Property { - use Nette\SmartObject; use Traits\NameAware; - use Traits\VisibilityAware; + use Traits\PropertyLike; use Traits\CommentAware; + use Traits\AttributeAware; - /** @var mixed */ - private $value; + private mixed $value = null; + private bool $static = false; + private ?string $type = null; + private bool $nullable = false; + private bool $initialized = false; + private bool $abstract = false; - /** @var bool */ - private $static = false; - /** @var string|null */ - private $type; - - /** @var bool */ - private $nullable = false; - - /** @var bool */ - private $initialized = false; - - - /** @return static */ - public function setValue($val): self + public function setValue(mixed $val): static { $this->value = $val; + $this->initialized = true; return $this; } - public function &getValue() + public function &getValue(): mixed { return $this->value; } - /** @return static */ - public function setStatic(bool $state = true): self + public function setStatic(bool $state = true): static { $this->static = $state; return $this; @@ -68,22 +58,23 @@ public function isStatic(): bool } - /** @return static */ - public function setType(?string $val): self + public function setType(?string $type): static { - $this->type = $val; + $this->type = Helpers::validateType($type, $this->nullable); return $this; } - public function getType(): ?string + /** @return ($asObject is true ? ?Type : ?string) */ + public function getType(bool $asObject = false): Type|string|null { - return $this->type; + return $asObject && $this->type + ? Type::fromString($this->type) + : $this->type; } - /** @return static */ - public function setNullable(bool $state = true): self + public function setNullable(bool $state = true): static { $this->nullable = $state; return $this; @@ -92,12 +83,11 @@ public function setNullable(bool $state = true): self public function isNullable(): bool { - return $this->nullable; + return $this->nullable || ($this->initialized && $this->value === null); } - /** @return static */ - public function setInitialized(bool $state = true): self + public function setInitialized(bool $state = true): static { $this->initialized = $state; return $this; @@ -106,6 +96,43 @@ public function setInitialized(bool $state = true): self public function isInitialized(): bool { - return $this->initialized; + return $this->initialized || $this->value !== null; + } + + + public function setAbstract(bool $state = true): static + { + $this->abstract = $state; + return $this; + } + + + public function isAbstract(): bool + { + return $this->abstract; + } + + + /** @throws Nette\InvalidStateException */ + public function validate(): void + { + if ($this->readOnly && !$this->type) { + throw new Nette\InvalidStateException("Property \$$this->name: Read-only properties are only supported on typed property."); + + } elseif ($this->abstract && $this->final) { + throw new Nette\InvalidStateException("Property \$$this->name cannot be abstract and final at the same time."); + + } elseif ( + $this->abstract + && !Nette\Utils\Arrays::some($this->getHooks(), fn($hook) => $hook->isAbstract()) + ) { + throw new Nette\InvalidStateException("Property \$$this->name: Abstract property must have at least one abstract hook."); + } + } + + + public function __clone(): void + { + $this->hooks = array_map(fn($item) => $item ? clone $item : $item, $this->hooks); } } diff --git a/src/PhpGenerator/PropertyAccessMode.php b/src/PhpGenerator/PropertyAccessMode.php new file mode 100644 index 00000000..54271b2f --- /dev/null +++ b/src/PhpGenerator/PropertyAccessMode.php @@ -0,0 +1,20 @@ +body = $args === null + ? $code + : (new Dumper)->format($code, ...$args); + $this->short = $short; + return $this; + } + + + public function getBody(): string + { + return $this->body; + } + + + public function isShort(): bool + { + return $this->short && trim($this->body) !== ''; + } + + + public function setFinal(bool $state = true): static + { + $this->final = $state; + return $this; + } + + + public function isFinal(): bool + { + return $this->final; + } + + + public function setAbstract(bool $state = true): static + { + $this->abstract = $state; + return $this; + } + + + public function isAbstract(): bool + { + return $this->abstract; + } + + + /** + * @param Parameter[] $val + * @internal + */ + public function setParameters(array $val): static + { + (function (Parameter ...$val) {})(...$val); + $this->parameters = []; + foreach ($val as $v) { + $this->parameters[$v->getName()] = $v; + } + + return $this; + } + + + /** + * @return Parameter[] + * @internal + */ + public function getParameters(): array + { + return $this->parameters; + } + + + /** + * Adds a parameter. If it already exists, it overwrites it. + * @param string $name without $ + */ + public function addParameter(string $name): Parameter + { + return $this->parameters[$name] = new Parameter($name); + } + + + public function setReturnReference(bool $state = true): static + { + $this->returnReference = $state; + return $this; + } + + + public function getReturnReference(): bool + { + return $this->returnReference; + } +} diff --git a/src/PhpGenerator/PropertyHookType.php b/src/PhpGenerator/PropertyHookType.php new file mode 100644 index 00000000..f9224a5d --- /dev/null +++ b/src/PhpGenerator/PropertyHookType.php @@ -0,0 +1,20 @@ +getName(); + [$type, $n] = match (true) { + $member instanceof Constant => ['consts', $name], + $member instanceof Method => ['methods', strtolower($name)], + $member instanceof Property => ['properties', $name], + $member instanceof TraitUse => ['traits', $name], + }; + if (!$overwrite && isset($this->$type[$n])) { + throw new Nette\InvalidStateException("Cannot add member '$name', because it already exists."); + } + $this->$type[$n] = $member; + return $this; + } + + + public function __clone(): void + { + parent::__clone(); + $clone = fn($item) => clone $item; + $this->consts = array_map($clone, $this->consts); + $this->methods = array_map($clone, $this->methods); + $this->properties = array_map($clone, $this->properties); + $this->traits = array_map($clone, $this->traits); + } +} diff --git a/src/PhpGenerator/TraitUse.php b/src/PhpGenerator/TraitUse.php new file mode 100644 index 00000000..f0f6a06b --- /dev/null +++ b/src/PhpGenerator/TraitUse.php @@ -0,0 +1,49 @@ +name = $name; + } + + + public function addResolution(string $resolution): static + { + $this->resolutions[] = $resolution; + return $this; + } + + + /** @return string[] */ + public function getResolutions(): array + { + return $this->resolutions; + } +} diff --git a/src/PhpGenerator/Traits/AttributeAware.php b/src/PhpGenerator/Traits/AttributeAware.php new file mode 100644 index 00000000..9e9d7d44 --- /dev/null +++ b/src/PhpGenerator/Traits/AttributeAware.php @@ -0,0 +1,49 @@ +attributes[] = new Attribute($name, $args); + return $this; + } + + + /** + * Replaces all attributes. + * @param Attribute[] $attrs + */ + public function setAttributes(array $attrs): static + { + (function (Attribute ...$attrs) {})(...$attrs); + $this->attributes = $attrs; + return $this; + } + + + /** @return Attribute[] */ + public function getAttributes(): array + { + return $this->attributes; + } +} diff --git a/src/PhpGenerator/Traits/CommentAware.php b/src/PhpGenerator/Traits/CommentAware.php index b5314056..deff6a5a 100644 --- a/src/PhpGenerator/Traits/CommentAware.php +++ b/src/PhpGenerator/Traits/CommentAware.php @@ -15,12 +15,10 @@ */ trait CommentAware { - /** @var string|null */ - private $comment; + private ?string $comment = null; - /** @return static */ - public function setComment(?string $val): self + public function setComment(?string $val): static { $this->comment = $val; return $this; @@ -33,10 +31,19 @@ public function getComment(): ?string } - /** @return static */ - public function addComment(string $val): self + /** + * Adds a new line to the comment. + */ + public function addComment(string $val): static { $this->comment .= $this->comment ? "\n$val" : $val; return $this; } + + + public function removeComment(): static + { + $this->comment = null; + return $this; + } } diff --git a/src/PhpGenerator/Traits/ConstantsAware.php b/src/PhpGenerator/Traits/ConstantsAware.php new file mode 100644 index 00000000..58a9093d --- /dev/null +++ b/src/PhpGenerator/Traits/ConstantsAware.php @@ -0,0 +1,79 @@ + */ + private array $consts = []; + + + /** + * Replaces all constants. + * @param Constant[] $consts + */ + public function setConstants(array $consts): static + { + (function (Constant ...$consts) {})(...$consts); + $this->consts = []; + foreach ($consts as $const) { + $this->consts[$const->getName()] = $const; + } + + return $this; + } + + + /** @return Constant[] */ + public function getConstants(): array + { + return $this->consts; + } + + + public function getConstant(string $name): Constant + { + return $this->consts[$name] ?? throw new Nette\InvalidArgumentException("Constant '$name' not found."); + } + + + /** + * Adds a constant. If it already exists, throws an exception or overwrites it if $overwrite is true. + */ + public function addConstant(string $name, mixed $value, bool $overwrite = false): Constant + { + if (!$overwrite && isset($this->consts[$name])) { + throw new Nette\InvalidStateException("Cannot add constant '$name', because it already exists."); + } + return $this->consts[$name] = (new Constant($name)) + ->setValue($value) + ->setPublic(); + } + + + public function removeConstant(string $name): static + { + unset($this->consts[$name]); + return $this; + } + + + public function hasConstant(string $name): bool + { + return isset($this->consts[$name]); + } +} diff --git a/src/PhpGenerator/Traits/FunctionLike.php b/src/PhpGenerator/Traits/FunctionLike.php index 73608bf9..8522abfb 100644 --- a/src/PhpGenerator/Traits/FunctionLike.php +++ b/src/PhpGenerator/Traits/FunctionLike.php @@ -9,9 +9,12 @@ namespace Nette\PhpGenerator\Traits; +use JetBrains\PhpStorm\Language; use Nette; use Nette\PhpGenerator\Dumper; use Nette\PhpGenerator\Parameter; +use Nette\Utils\Type; +use function func_num_args; /** @@ -19,29 +22,26 @@ */ trait FunctionLike { - /** @var string */ - private $body = ''; + private string $body = ''; /** @var Parameter[] */ - private $parameters = []; - - /** @var bool */ - private $variadic = false; - - /** @var string|null */ - private $returnType; - - /** @var bool */ - private $returnReference = false; - - /** @var bool */ - private $returnNullable = false; - - - /** @return static */ - public function setBody(string $code, array $args = null): self - { - $this->body = $args === null ? $code : (new Dumper)->format($code, ...$args); + private array $parameters = []; + private bool $variadic = false; + private ?string $returnType = null; + private bool $returnReference = false; + private bool $returnNullable = false; + + + /** @param ?mixed[] $args */ + public function setBody( + #[Language('PHP')] + string $code, + ?array $args = null, + ): static + { + $this->body = $args === null + ? $code + : (new Dumper)->format($code, ...$args); return $this; } @@ -52,8 +52,12 @@ public function getBody(): string } - /** @return static */ - public function addBody(string $code, array $args = null): self + /** @param ?mixed[] $args */ + public function addBody( + #[Language('PHP')] + string $code, + ?array $args = null, + ): static { $this->body .= ($args === null ? $code : (new Dumper)->format($code, ...$args)) . "\n"; return $this; @@ -62,17 +66,15 @@ public function addBody(string $code, array $args = null): self /** * @param Parameter[] $val - * @return static */ - public function setParameters(array $val): self + public function setParameters(array $val): static { + (function (Parameter ...$val) {})(...$val); $this->parameters = []; foreach ($val as $v) { - if (!$v instanceof Parameter) { - throw new Nette\InvalidArgumentException('Argument must be Nette\PhpGenerator\Parameter[].'); - } $this->parameters[$v->getName()] = $v; } + return $this; } @@ -84,32 +86,44 @@ public function getParameters(): array } + public function getParameter(string $name): Parameter + { + return $this->parameters[$name] ?? throw new Nette\InvalidArgumentException("Parameter '$name' not found."); + } + + /** + * Adds a parameter. If it already exists, it overwrites it. * @param string $name without $ */ - public function addParameter(string $name, $defaultValue = null): Parameter + public function addParameter(string $name, mixed $defaultValue = null): Parameter { $param = new Parameter($name); if (func_num_args() > 1) { $param->setDefaultValue($defaultValue); } + return $this->parameters[$name] = $param; } /** * @param string $name without $ - * @return static */ - public function removeParameter(string $name): self + public function removeParameter(string $name): static { unset($this->parameters[$name]); return $this; } - /** @return static */ - public function setVariadic(bool $state = true): self + public function hasParameter(string $name): bool + { + return isset($this->parameters[$name]); + } + + + public function setVariadic(bool $state = true): static { $this->variadic = $state; return $this; @@ -122,22 +136,23 @@ public function isVariadic(): bool } - /** @return static */ - public function setReturnType(?string $val): self + public function setReturnType(?string $type): static { - $this->returnType = $val; + $this->returnType = Nette\PhpGenerator\Helpers::validateType($type, $this->returnNullable); return $this; } - public function getReturnType(): ?string + /** @return ($asObject is true ? ?Type : ?string) */ + public function getReturnType(bool $asObject = false): Type|string|null { - return $this->returnType; + return $asObject && $this->returnType + ? Type::fromString($this->returnType) + : $this->returnType; } - /** @return static */ - public function setReturnReference(bool $state = true): self + public function setReturnReference(bool $state = true): static { $this->returnReference = $state; return $this; @@ -150,8 +165,7 @@ public function getReturnReference(): bool } - /** @return static */ - public function setReturnNullable(bool $state = true): self + public function setReturnNullable(bool $state = true): static { $this->returnNullable = $state; return $this; @@ -162,19 +176,4 @@ public function isReturnNullable(): bool { return $this->returnNullable; } - - - /** @deprecated use isReturnNullable() */ - public function getReturnNullable(): bool - { - return $this->returnNullable; - } - - - /** @deprecated */ - public function setNamespace(Nette\PhpGenerator\PhpNamespace $val = null): self - { - trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED); - return $this; - } } diff --git a/src/PhpGenerator/Traits/MethodsAware.php b/src/PhpGenerator/Traits/MethodsAware.php new file mode 100644 index 00000000..9b38dc85 --- /dev/null +++ b/src/PhpGenerator/Traits/MethodsAware.php @@ -0,0 +1,89 @@ + */ + private array $methods = []; + + + /** + * Replaces all methods. + * @param Method[] $methods + */ + public function setMethods(array $methods): static + { + (function (Method ...$methods) {})(...$methods); + $this->methods = []; + foreach ($methods as $m) { + $this->methods[strtolower($m->getName())] = $m; + } + + return $this; + } + + + /** @return Method[] */ + public function getMethods(): array + { + $res = []; + foreach ($this->methods as $m) { + $res[$m->getName()] = $m; + } + + return $res; + } + + + public function getMethod(string $name): Method + { + return $this->methods[strtolower($name)] ?? throw new Nette\InvalidArgumentException("Method '$name' not found."); + } + + + /** + * Adds a method. If it already exists, throws an exception or overwrites it if $overwrite is true. + */ + public function addMethod(string $name, bool $overwrite = false): Method + { + $lower = strtolower($name); + if (!$overwrite && isset($this->methods[$lower])) { + throw new Nette\InvalidStateException("Cannot add method '$name', because it already exists."); + } + $method = new Method($name); + if (!$this->isInterface()) { + $method->setPublic(); + } + + return $this->methods[$lower] = $method; + } + + + public function removeMethod(string $name): static + { + unset($this->methods[strtolower($name)]); + return $this; + } + + + public function hasMethod(string $name): bool + { + return isset($this->methods[strtolower($name)]); + } +} diff --git a/src/PhpGenerator/Traits/NameAware.php b/src/PhpGenerator/Traits/NameAware.php index 157aa0f8..16de3563 100644 --- a/src/PhpGenerator/Traits/NameAware.php +++ b/src/PhpGenerator/Traits/NameAware.php @@ -17,8 +17,7 @@ */ trait NameAware { - /** @var string */ - private $name; + private string $name; public function __construct(string $name) @@ -26,6 +25,7 @@ public function __construct(string $name) if (!Nette\PhpGenerator\Helpers::isIdentifier($name)) { throw new Nette\InvalidArgumentException("Value '$name' is not valid name."); } + $this->name = $name; } @@ -38,9 +38,8 @@ public function getName(): string /** * Returns clone with a different name. - * @return static */ - public function cloneWithName(string $name): self + public function cloneWithName(string $name): static { $dolly = clone $this; $dolly->__construct($name); diff --git a/src/PhpGenerator/Traits/PropertiesAware.php b/src/PhpGenerator/Traits/PropertiesAware.php new file mode 100644 index 00000000..64418249 --- /dev/null +++ b/src/PhpGenerator/Traits/PropertiesAware.php @@ -0,0 +1,82 @@ + */ + private array $properties = []; + + + /** + * Replaces all properties. + * @param Property[] $props + */ + public function setProperties(array $props): static + { + (function (Property ...$props) {})(...$props); + $this->properties = []; + foreach ($props as $v) { + $this->properties[$v->getName()] = $v; + } + + return $this; + } + + + /** @return Property[] */ + public function getProperties(): array + { + return $this->properties; + } + + + public function getProperty(string $name): Property + { + return $this->properties[$name] ?? throw new Nette\InvalidArgumentException("Property '$name' not found."); + } + + + /** + * Adds a property. If it already exists, throws an exception or overwrites it if $overwrite is true. + * @param string $name without $ + */ + public function addProperty(string $name, mixed $value = null, bool $overwrite = false): Property + { + if (!$overwrite && isset($this->properties[$name])) { + throw new Nette\InvalidStateException("Cannot add property '$name', because it already exists."); + } + return $this->properties[$name] = func_num_args() > 1 + ? (new Property($name))->setValue($value) + : new Property($name); + } + + + /** @param string $name without $ */ + public function removeProperty(string $name): static + { + unset($this->properties[$name]); + return $this; + } + + + public function hasProperty(string $name): bool + { + return isset($this->properties[$name]); + } +} diff --git a/src/PhpGenerator/Traits/PropertyLike.php b/src/PhpGenerator/Traits/PropertyLike.php new file mode 100644 index 00000000..e7997518 --- /dev/null +++ b/src/PhpGenerator/Traits/PropertyLike.php @@ -0,0 +1,160 @@ + null, 'get' => null]; + private bool $final = false; + private bool $readOnly = false; + + /** @var array */ + private array $hooks = ['set' => null, 'get' => null]; + + + public function setVisibility(Visibility|string|null $get, Visibility|string|null $set = null): static + { + $this->visibility = [ + 'set' => $set instanceof Visibility || $set === null ? $set : Visibility::from($set), + 'get' => $get instanceof Visibility || $get === null ? $get : Visibility::from($get), + ]; + return $this; + } + + + public function getVisibility(PropertyAccessMode|string $mode = PropertyAccessMode::Get): ?string + { + $mode = is_string($mode) ? PropertyAccessMode::from($mode) : $mode; + return $this->visibility[$mode->value]?->value; + } + + + public function setPublic(PropertyAccessMode|string $mode = PropertyAccessMode::Get): static + { + $mode = is_string($mode) ? PropertyAccessMode::from($mode) : $mode; + $this->visibility[$mode->value] = Visibility::Public; + return $this; + } + + + public function isPublic(PropertyAccessMode|string $mode = PropertyAccessMode::Get): bool + { + $mode = is_string($mode) ? PropertyAccessMode::from($mode) : $mode; + return in_array($this->visibility[$mode->value], [Visibility::Public, null], true); + } + + + public function setProtected(PropertyAccessMode|string $mode = PropertyAccessMode::Get): static + { + $mode = is_string($mode) ? PropertyAccessMode::from($mode) : $mode; + $this->visibility[$mode->value] = Visibility::Protected; + return $this; + } + + + public function isProtected(PropertyAccessMode|string $mode = PropertyAccessMode::Get): bool + { + $mode = is_string($mode) ? PropertyAccessMode::from($mode) : $mode; + return $this->visibility[$mode->value] === Visibility::Protected; + } + + + public function setPrivate(PropertyAccessMode|string $mode = PropertyAccessMode::Get): static + { + $mode = is_string($mode) ? PropertyAccessMode::from($mode) : $mode; + $this->visibility[$mode->value] = Visibility::Private; + return $this; + } + + + public function isPrivate(PropertyAccessMode|string $mode = PropertyAccessMode::Get): bool + { + $mode = is_string($mode) ? PropertyAccessMode::from($mode) : $mode; + return $this->visibility[$mode->value] === Visibility::Private; + } + + + public function setFinal(bool $state = true): static + { + $this->final = $state; + return $this; + } + + + public function isFinal(): bool + { + return $this->final; + } + + + public function setReadOnly(bool $state = true): static + { + $this->readOnly = $state; + return $this; + } + + + public function isReadOnly(): bool + { + return $this->readOnly; + } + + + /** + * Replaces all hooks. + * @param PropertyHook[] $hooks + */ + public function setHooks(array $hooks): static + { + (function (PropertyHook ...$hooks) {})(...$hooks); + $this->hooks = $hooks; + return $this; + } + + + /** @return array */ + public function getHooks(): array + { + return array_filter($this->hooks); + } + + + public function addHook(PropertyHookType|string $type, string $shortBody = ''): PropertyHook + { + $type = is_string($type) ? PropertyHookType::from($type) : $type; + return $this->hooks[$type->value] = (new PropertyHook) + ->setBody($shortBody, short: true); + } + + + public function getHook(PropertyHookType|string $type): ?PropertyHook + { + $type = is_string($type) ? PropertyHookType::from($type) : $type; + return $this->hooks[$type->value] ?? null; + } + + + public function hasHook(PropertyHookType|string $type): bool + { + $type = is_string($type) ? PropertyHookType::from($type) : $type; + return isset($this->hooks[$type->value]); + } +} diff --git a/src/PhpGenerator/Traits/TraitsAware.php b/src/PhpGenerator/Traits/TraitsAware.php new file mode 100644 index 00000000..8a1938d2 --- /dev/null +++ b/src/PhpGenerator/Traits/TraitsAware.php @@ -0,0 +1,78 @@ + */ + private array $traits = []; + + + /** + * Replaces all traits. + * @param TraitUse[] $traits + */ + public function setTraits(array $traits): static + { + (function (TraitUse ...$traits) {})(...$traits); + $this->traits = []; + foreach ($traits as $trait) { + $this->traits[$trait->getName()] = $trait; + } + + return $this; + } + + + /** @return TraitUse[] */ + public function getTraits(): array + { + return $this->traits; + } + + + /** + * Adds a method. If it already exists, throws an exception. + */ + public function addTrait(string $name): TraitUse + { + if (isset($this->traits[$name])) { + throw new Nette\InvalidStateException("Cannot add trait '$name', because it already exists."); + } + $this->traits[$name] = $trait = new TraitUse($name); + if (func_num_args() > 1 && is_array(func_get_arg(1))) { // back compatibility + trigger_error('Passing second argument to ' . __METHOD__ . '() is deprecated, use addResolution() instead.'); + array_map(fn($item) => $trait->addResolution($item), func_get_arg(1)); + } + + return $trait; + } + + + public function removeTrait(string $name): static + { + unset($this->traits[$name]); + return $this; + } + + + public function hasTrait(string $name): bool + { + return isset($this->traits[$name]); + } +} diff --git a/src/PhpGenerator/Traits/VisibilityAware.php b/src/PhpGenerator/Traits/VisibilityAware.php index 3dd862c7..c3da1819 100644 --- a/src/PhpGenerator/Traits/VisibilityAware.php +++ b/src/PhpGenerator/Traits/VisibilityAware.php @@ -9,8 +9,7 @@ namespace Nette\PhpGenerator\Traits; -use Nette; -use Nette\PhpGenerator\ClassType; +use Nette\PhpGenerator\Visibility; /** @@ -18,68 +17,59 @@ */ trait VisibilityAware { - /** @var string|null public|protected|private */ - private $visibility; + private ?Visibility $visibility = null; - /** - * @param string|null $val public|protected|private - * @return static - */ - public function setVisibility(?string $val): self + public function setVisibility(Visibility|string|null $value): static { - if (!in_array($val, [ClassType::VISIBILITY_PUBLIC, ClassType::VISIBILITY_PROTECTED, ClassType::VISIBILITY_PRIVATE, null], true)) { - throw new Nette\InvalidArgumentException('Argument must be public|protected|private.'); - } - $this->visibility = $val; + $this->visibility = $value instanceof Visibility || $value === null + ? $value + : Visibility::from($value); return $this; } public function getVisibility(): ?string { - return $this->visibility; + return $this->visibility?->value; } - /** @return static */ - public function setPublic(): self + public function setPublic(): static { - $this->visibility = ClassType::VISIBILITY_PUBLIC; + $this->visibility = Visibility::Public; return $this; } public function isPublic(): bool { - return $this->visibility === ClassType::VISIBILITY_PUBLIC || $this->visibility === null; + return $this->visibility === Visibility::Public || $this->visibility === null; } - /** @return static */ - public function setProtected(): self + public function setProtected(): static { - $this->visibility = ClassType::VISIBILITY_PROTECTED; + $this->visibility = Visibility::Protected; return $this; } public function isProtected(): bool { - return $this->visibility === ClassType::VISIBILITY_PROTECTED; + return $this->visibility === Visibility::Protected; } - /** @return static */ - public function setPrivate(): self + public function setPrivate(): static { - $this->visibility = ClassType::VISIBILITY_PRIVATE; + $this->visibility = Visibility::Private; return $this; } public function isPrivate(): bool { - return $this->visibility === ClassType::VISIBILITY_PRIVATE; + return $this->visibility === Visibility::Private; } } diff --git a/src/PhpGenerator/Type.php b/src/PhpGenerator/Type.php index d320bf3e..42cb20b4 100644 --- a/src/PhpGenerator/Type.php +++ b/src/PhpGenerator/Type.php @@ -9,6 +9,9 @@ namespace Nette\PhpGenerator; +use Nette; +use function implode, preg_match, preg_replace, str_contains; + /** * PHP return, property and parameter types. @@ -16,26 +19,94 @@ class Type { public const - STRING = 'string', - INT = 'int', - FLOAT = 'float', - BOOL = 'bool', - ARRAY = 'array', - OBJECT = 'object', - CALLABLE = 'callable', - ITERABLE = 'iterable', - VOID = 'void', - MIXED = 'mixed', - FALSE = 'false', - NULL = 'null', - SELF = 'self', - PARENT = 'parent', - STATIC = 'static'; - - - public static function nullable(string $type, bool $state = true): string + String = 'string', + Int = 'int', + Float = 'float', + Bool = 'bool', + Array = 'array', + Object = 'object', + Callable = 'callable', + Iterable = 'iterable', + Void = 'void', + Never = 'never', + Mixed = 'mixed', + True = 'true', + False = 'false', + Null = 'null', + Self = 'self', + Parent = 'parent', + Static = 'static'; + + #[\Deprecated('use Type::String')] + public const STRING = self::String; + + #[\Deprecated('use Type::Int')] + public const INT = self::Int; + + #[\Deprecated('use Type::Float')] + public const FLOAT = self::Float; + + #[\Deprecated('use Type::Bool')] + public const BOOL = self::Bool; + + #[\Deprecated('use Type::Array')] + public const ARRAY = self::Array; + + #[\Deprecated('use Type::Object')] + public const OBJECT = self::Object; + + #[\Deprecated('use Type::Callable')] + public const CALLABLE = self::Callable; + + #[\Deprecated('use Type::Iterable')] + public const ITERABLE = self::Iterable; + + #[\Deprecated('use Type::Void')] + public const VOID = self::Void; + + #[\Deprecated('use Type::Never')] + public const NEVER = self::Never; + + #[\Deprecated('use Type::Mixed')] + public const MIXED = self::Mixed; + + #[\Deprecated('use Type::False')] + public const FALSE = self::False; + + #[\Deprecated('use Type::Null')] + public const NULL = self::Null; + + #[\Deprecated('use Type::Self')] + public const SELF = self::Self; + + #[\Deprecated('use Type::Parent')] + public const PARENT = self::Parent; + + #[\Deprecated('use Type::Static')] + public const STATIC = self::Static; + + + public static function nullable(string $type, bool $nullable = true): string { - return ($state ? '?' : '') . ltrim($type, '?'); + if (str_contains($type, '&')) { + return $nullable + ? throw new Nette\InvalidArgumentException('Intersection types cannot be nullable.') + : $type; + } + + $nnType = preg_replace('#^\?|^null\||\|null(?=\||$)#i', '', $type); + $always = (bool) preg_match('#^(null|mixed)$#i', $nnType); + if ($nullable) { + return match (true) { + $always, $type !== $nnType => $type, + str_contains($type, '|') => $type . '|null', + default => '?' . $type, + }; + } else { + return $always + ? throw new Nette\InvalidArgumentException("Type $type cannot be not nullable.") + : $nnType; + } } @@ -45,22 +116,8 @@ public static function union(string ...$types): string } - public static function getType($value): ?string + public static function intersection(string ...$types): string { - if (is_object($value)) { - return get_class($value); - } elseif (is_int($value)) { - return self::INT; - } elseif (is_float($value)) { - return self::FLOAT; - } elseif (is_string($value)) { - return self::STRING; - } elseif (is_bool($value)) { - return self::BOOL; - } elseif (is_array($value)) { - return self::ARRAY; - } else { - return null; - } + return implode('&', $types); } } diff --git a/src/PhpGenerator/Visibility.php b/src/PhpGenerator/Visibility.php new file mode 100644 index 00000000..34f39cc0 --- /dev/null +++ b/src/PhpGenerator/Visibility.php @@ -0,0 +1,21 @@ + ClassType::from(Abc\Interface1::class), + Nette\InvalidArgumentException::class, + 'Abc\Interface1 cannot be represented with Nette\PhpGenerator\ClassType. Call Nette\PhpGenerator\InterfaceType::from() or Nette\PhpGenerator\ClassLike::from() instead.', +); + +Assert::exception( + fn() => TraitType::from(Abc\Class1::class), + Nette\InvalidArgumentException::class, + 'Abc\Class1 cannot be represented with Nette\PhpGenerator\TraitType. Call Nette\PhpGenerator\ClassType::from() or Nette\PhpGenerator\ClassLike::from() instead.', +); + +Assert::exception( + fn() => ClassType::fromCode(' InterfaceType::fromCode('implement(TestInterface::class); +Assert::match(<<<'XX' + class TestClass implements TestInterface + { + public array $interfaceProperty; + + + function interfaceMethod() + { + } + } + + XX, (string) $class); + + +// Test abstract class extension +$class = new ClassType('TestClass'); +$manipulator = new ClassManipulator($class); +$manipulator->implement(TestAbstract::class); +Assert::match(<<<'XX' + class TestClass extends TestAbstract + { + public array $abstractProperty; + + + public function abstractMethod() + { + } + } + + XX, (string) $class); + + +// Test exception for regular class +Assert::exception( + fn() => $manipulator->implement(stdClass::class), + InvalidArgumentException::class, + "'stdClass' is not an interface or abstract class." +); diff --git a/tests/PhpGenerator/ClassManipulator.implement.phpt b/tests/PhpGenerator/ClassManipulator.implement.phpt new file mode 100644 index 00000000..e5d85114 --- /dev/null +++ b/tests/PhpGenerator/ClassManipulator.implement.phpt @@ -0,0 +1,75 @@ +implement(TestInterface::class); +Assert::match(<<<'XX' + class TestClass implements TestInterface + { + function interfaceMethod() + { + } + } + + XX, (string) $class); + + +// Test abstract class extension +$class = new ClassType('TestClass'); +$manipulator = new ClassManipulator($class); +$manipulator->implement(TestAbstract::class); +Assert::match(<<<'XX' + class TestClass extends TestAbstract + { + public function abstractMethod() + { + } + } + + XX, (string) $class); + + +// Test exception for regular class +Assert::exception( + fn() => $manipulator->implement(stdClass::class), + InvalidArgumentException::class, + "'stdClass' is not an interface or abstract class.", +); diff --git a/tests/PhpGenerator/ClassManipulator.inheritMethod.phpt b/tests/PhpGenerator/ClassManipulator.inheritMethod.phpt new file mode 100644 index 00000000..4927e69c --- /dev/null +++ b/tests/PhpGenerator/ClassManipulator.inheritMethod.phpt @@ -0,0 +1,55 @@ + $manipulator->inheritMethod('bar'), + Nette\InvalidStateException::class, + "Class 'Test' has neither setExtends() nor setImplements() set.", +); + +$class->setExtends('Unknown1'); +$class->addImplement('Unknown2'); +Assert::exception( + fn() => $manipulator->inheritMethod('bar'), + Nette\InvalidStateException::class, + "Method 'bar' has not been found in any ancestor: Unknown1, Unknown2", +); + + +// implement method +$class = new ClassType('Test'); +$class->setExtends(Foo::class); +$manipulator = new ClassManipulator($class); +$method = $manipulator->inheritMethod('bar'); +Assert::match(<<<'XX' + public function bar(int $a, ...$b): void + { + } + + XX, (string) $method); + +Assert::same($method, $manipulator->inheritMethod('bar', returnIfExists: true)); +Assert::exception( + fn() => $manipulator->inheritMethod('bar', returnIfExists: false), + Nette\InvalidStateException::class, + "Cannot inherit method 'bar', because it already exists.", +); diff --git a/tests/PhpGenerator/ClassManipulator.inheritProperty.phpt b/tests/PhpGenerator/ClassManipulator.inheritProperty.phpt new file mode 100644 index 00000000..95d533aa --- /dev/null +++ b/tests/PhpGenerator/ClassManipulator.inheritProperty.phpt @@ -0,0 +1,53 @@ + $manipulator->inheritProperty('bar'), + Nette\InvalidStateException::class, + "Class 'Test' has neither setExtends() nor setImplements() set.", +); + +$class->setExtends('Unknown'); +Assert::exception( + fn() => $manipulator->inheritProperty('bar'), + Nette\InvalidStateException::class, + "Property 'bar' has not been found in any ancestor: Unknown", +); + + +// implement property +$class = new ClassType('Test'); +$class->setExtends(Foo::class); +$manipulator = new ClassManipulator($class); +$prop = $manipulator->inheritProperty('bar'); +Assert::match(<<<'XX' + class Test extends Foo + { + public array $bar = [123]; + } + + XX, (string) $class); + +Assert::same($prop, $manipulator->inheritProperty('bar', returnIfExists: true)); +Assert::exception( + fn() => $manipulator->inheritProperty('bar', returnIfExists: false), + Nette\InvalidStateException::class, + "Cannot inherit property 'bar', because it already exists.", +); diff --git a/tests/PhpGenerator/ClassType.addMember.phpt b/tests/PhpGenerator/ClassType.addMember.phpt index e1a7905f..9d561b78 100644 --- a/tests/PhpGenerator/ClassType.addMember.phpt +++ b/tests/PhpGenerator/ClassType.addMember.phpt @@ -5,29 +5,30 @@ declare(strict_types=1); use Nette\PhpGenerator\ClassType; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; -Assert::exception(function () { - (new ClassType('Example')) - ->addMember(new stdClass); -}, Nette\InvalidArgumentException::class, 'Argument must be Method|Property|Constant.'); - - $class = (new ClassType('Example')) ->addMember($method = new Nette\PhpGenerator\Method('getHandle')) ->addMember($property = new Nette\PhpGenerator\Property('handle')) - ->addMember($const = new Nette\PhpGenerator\Constant('ROLE')); + ->addMember($const = new Nette\PhpGenerator\Constant('ROLE')) + ->addMember($trait = new Nette\PhpGenerator\TraitUse('Foo\Bar')); Assert::same(['getHandle' => $method], $class->getMethods()); Assert::same(['handle' => $property], $class->getProperties()); Assert::same(['ROLE' => $const], $class->getConstants()); +Assert::same(['Foo\Bar' => $trait], $class->getTraits()); Assert::same('', $method->getBody()); -$class = (new ClassType('Example')) - ->setType('interface') - ->addMember($method = new Nette\PhpGenerator\Method('getHandle')); +// duplicity +$class = new ClassType('Example'); +$class->addMember(new Nette\PhpGenerator\Method('foo')); +Assert::exception( + fn() => $class->addMember(new Nette\PhpGenerator\Method('FOO')), + Nette\InvalidStateException::class, + "Cannot add member 'FOO', because it already exists.", +); -Assert::null($method->getBody()); +$class->addMember($new = new Nette\PhpGenerator\Method('FOO'), overwrite: true); +Assert::same($new, $class->getMethod('FOO')); diff --git a/tests/PhpGenerator/ClassType.attributes.phpt b/tests/PhpGenerator/ClassType.attributes.phpt new file mode 100644 index 00000000..c2dc1c76 --- /dev/null +++ b/tests/PhpGenerator/ClassType.attributes.phpt @@ -0,0 +1,46 @@ +addComment('Description of class.') + ->addAttribute('ExampleAttribute') + ->addAttribute('WithArgument', [new Literal('Foo::BAR')]) + ->addAttribute('Table', [ + 'name' => 'user', + 'constraints' => [ + Literal::new('UniqueConstraint', ['name' => 'ean', 'columns' => ['ean']]), + ], + ]); + +$class->addConstant('FOO', 123) + ->addComment('Commented') + ->addAttribute('ExampleAttribute') + ->addAttribute('WithArguments', [true]); + +$class->addProperty('handle') + ->addComment('@var resource') + ->addAttribute('ExampleAttribute'); + +$method = $class->addMethod('getHandle') + ->addComment('Returns file handle.') + ->addAttribute('ExampleAttribute'); + +$method->addParameter('mode') + ->addComment('comment') + ->addAttribute('ExampleAttribute') + ->addAttribute('WithArguments', [0]); + +sameFile(__DIR__ . '/expected/ClassType.attributes.expect', (string) $class); diff --git a/tests/PhpGenerator/ClassType.clone.phpt b/tests/PhpGenerator/ClassType.clone.phpt index b38f1c9d..0d007eb2 100644 --- a/tests/PhpGenerator/ClassType.clone.phpt +++ b/tests/PhpGenerator/ClassType.clone.phpt @@ -5,18 +5,21 @@ declare(strict_types=1); use Nette\PhpGenerator\ClassType; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; $class = new ClassType('Example'); +$class->addAttribute('Attr'); $class->addConstant('A', 10); $class->addProperty('a'); -$class->addMethod('a'); +$class->addMethod('a') + ->addParameter('foo'); $dolly = clone $class; +Assert::notSame($dolly->getAttributes(), $class->getAttributes()); Assert::notSame($dolly->getConstants(), $class->getConstants()); Assert::notSame($dolly->getProperty('a'), $class->getProperty('a')); Assert::notSame($dolly->getMethod('a'), $class->getMethod('a')); +Assert::notSame($dolly->getMethod('a')->getParameter('foo'), $class->getMethod('a')->getParameter('foo')); diff --git a/tests/PhpGenerator/ClassType.from.74.phpt b/tests/PhpGenerator/ClassType.from.74.phpt deleted file mode 100644 index ba13d539..00000000 --- a/tests/PhpGenerator/ClassType.from.74.phpt +++ /dev/null @@ -1,17 +0,0 @@ - ClassType::from(PDO::class, withBodies: true), + Nette\NotSupportedException::class, + 'The $withBodies parameter cannot be used for anonymous or internal classes or interfaces.', +); -Assert::exception(function () { - ClassType::withBodiesFrom(new class { - }); -}, Nette\NotSupportedException::class, 'Anonymous classes are not supported.'); +Assert::exception( + fn() => ClassType::from(new class { + public function f() + { + } + }, withBodies: true), + Nette\NotSupportedException::class, + 'The $withBodies parameter cannot be used for anonymous or internal classes or interfaces.', +); -$res = ClassType::withBodiesFrom(Abc\Class7::class); +$res = ClassType::from(Abc\Class7::class, withBodies: true); sameFile(__DIR__ . '/expected/ClassType.from.bodies.expect', (string) $res); + + +if (PHP_VERSION_ID >= 80400) { + require __DIR__ . '/fixtures/classes.84.php'; + $res = []; + $res[] = ClassType::from(Abc\PropertyHookSignatures::class, withBodies: true); + $res[] = ClassType::from(Abc\AbstractHookSignatures::class, withBodies: true); + $res[] = ClassType::from(Abc\PropertyHookSignaturesChild::class, withBodies: true); + sameFile(__DIR__ . '/expected/ClassType.from.bodies.84.expect', implode("\n", $res)); +} diff --git a/tests/PhpGenerator/ClassType.from.phpt b/tests/PhpGenerator/ClassType.from.phpt index 3e10b6f5..d4f59140 100644 --- a/tests/PhpGenerator/ClassType.from.phpt +++ b/tests/PhpGenerator/ClassType.from.phpt @@ -8,20 +8,26 @@ declare(strict_types=1); use Nette\PhpGenerator\ClassType; use Nette\PhpGenerator\Factory; - +use Nette\PhpGenerator\InterfaceType; require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/fixtures/classes.php'; -$res[] = ClassType::from(Abc\Interface1::class); -$res[] = ClassType::from(Abc\Interface2::class); +$res[] = InterfaceType::from(Abc\Interface1::class); +$res[] = InterfaceType::from(Abc\Interface2::class); +$res[] = InterfaceType::from(Abc\Interface3::class); +$res[] = InterfaceType::from(Abc\Interface4::class); $res[] = ClassType::from(Abc\Class1::class); $res[] = ClassType::from(new Abc\Class2); $obj = new Abc\Class3; -$obj->prop2 = 1; -$res[] = (new Factory)->fromClassReflection(new \ReflectionObject($obj)); +@$obj->prop2 = 1; // dynamic property +$res[] = (new Factory)->fromClassReflection(new ReflectionObject($obj)); $res[] = ClassType::from(Abc\Class4::class); $res[] = ClassType::from(Abc\Class5::class); $res[] = ClassType::from(Abc\Class6::class); +$res[] = ClassType::from(Abc\Class7::class); +$res[] = ClassType::from(Abc\Class8::class); +$res[] = ClassType::from(Abc\Class9::class); +$res[] = ClassType::from(Abc\Class10::class); sameFile(__DIR__ . '/expected/ClassType.from.expect', implode("\n", $res)); diff --git a/tests/PhpGenerator/ClassType.from.trait.phpt b/tests/PhpGenerator/ClassType.from.trait.phpt index 036b0e17..1f62a4b0 100644 --- a/tests/PhpGenerator/ClassType.from.trait.phpt +++ b/tests/PhpGenerator/ClassType.from.trait.phpt @@ -1,32 +1,29 @@ ClassLike::from($class), $classes); -sameFile(__DIR__ . '/expected/ClassType.from.trait.expect', implode("\n", $res)); +sameFile(__DIR__ . '/expected/ClassType.from.trait-use.expect', implode("\n", $res)); -$res = []; -$res[] = ClassType::withBodiesFrom('Trait1'); -$res[] = ClassType::withBodiesFrom('Trait2'); -$res[] = ClassType::withBodiesFrom('Class1'); -$res[] = ClassType::withBodiesFrom('Class2'); -$res[] = ClassType::withBodiesFrom('Class3'); +$res = array_map(fn($class) => ClassLike::from($class, withBodies: true), $classes); -sameFile(__DIR__ . '/expected/ClassType.from.trait.bodies.expect', implode("\n", $res)); +sameFile(__DIR__ . '/expected/ClassType.from.trait-use.bodies.expect', implode("\n", $res)); diff --git a/tests/PhpGenerator/ClassType.inheritance.phpt b/tests/PhpGenerator/ClassType.inheritance.phpt index fa458d9e..0455dc17 100644 --- a/tests/PhpGenerator/ClassType.inheritance.phpt +++ b/tests/PhpGenerator/ClassType.inheritance.phpt @@ -4,7 +4,6 @@ declare(strict_types=1); use Nette\PhpGenerator\ClassType; - require __DIR__ . '/../bootstrap.php'; @@ -54,4 +53,4 @@ class B extends A implements I3 } -sameFile(__DIR__ . '/expected/ClassType.inheritance.expect', (string) ClassType::from('B')); +sameFile(__DIR__ . '/expected/ClassType.inheritance.expect', (string) ClassType::from(B::class)); diff --git a/tests/PhpGenerator/ClassType.phpt b/tests/PhpGenerator/ClassType.phpt index 2bd8e4f5..38852078 100644 --- a/tests/PhpGenerator/ClassType.phpt +++ b/tests/PhpGenerator/ClassType.phpt @@ -7,11 +7,10 @@ declare(strict_types=1); use Nette\PhpGenerator\ClassType; -use Nette\PhpGenerator\PhpLiteral; +use Nette\PhpGenerator\Literal; use Nette\PhpGenerator\Type; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -22,31 +21,36 @@ Assert::false($class->isAbstract()); Assert::true($class->isClass()); Assert::false($class->isInterface()); Assert::false($class->isTrait()); -Assert::same([], $class->getExtends()); +Assert::same(null, $class->getExtends()); Assert::same([], $class->getTraits()); -Assert::same([], $class->getTraitResolutions()); $class ->setAbstract(true) ->setExtends('ParentClass') ->addImplement('IExample') ->addImplement('IOne') - ->setTraits(['ObjectTrait']) - ->addTrait('AnotherTrait', ['sayHello as protected']) - ->addComment("Description of class.\nThis is example\n") - ->addComment('@property-read Nette\Forms\Form $form') - ->setConstants(['ROLE' => 'admin']) - ->addConstant('ACTIVE', false); + ->addComment("Description of class.\nThis is example\n /**/") + ->addComment('@property-read Nette\Forms\Form $form'); + +$trait1 = $class->addTrait('ObjectTrait'); +$trait2 = $class->addTrait('AnotherTrait') + ->addResolution('sayHello as protected'); + +$class->addConstant('ROLE', 'admin'); +$class->addConstant('ACTIVE', false) + ->setFinal() + ->setType('?bool'); +Assert::true($class->hasConstant('ROLE')); +Assert::false($class->hasConstant('xxx')); Assert::false($class->isFinal()); Assert::true($class->isAbstract()); Assert::same('ParentClass', $class->getExtends()); -Assert::same(['ObjectTrait', 'AnotherTrait'], $class->getTraits()); -Assert::same(['ObjectTrait' => [], 'AnotherTrait' => ['sayHello as protected']], $class->getTraitResolutions()); +Assert::same(['ObjectTrait' => $trait1, 'AnotherTrait' => $trait2], $class->getTraits()); Assert::count(2, $class->getConstants()); -Assert::type(Nette\PhpGenerator\Constant::class, $class->getConstants()['ROLE']); +Assert::type(Nette\PhpGenerator\Constant::class, $class->getConstant('ROLE')); -$class->addConstant('FORCE_ARRAY', new PhpLiteral('Nette\Utils\Json::FORCE_ARRAY')) +$class->addConstant('FORCE_ARRAY', new Literal('Nette\Utils\Json::FORCE_ARRAY')) ->setVisibility('private') ->addComment('Commented'); @@ -55,16 +59,23 @@ $class->addProperty('handle') ->addComment('@var resource orignal file handle'); $class->addProperty('order') - ->setValue(new PhpLiteral('RecursiveIteratorIterator::SELF_FIRST')); + ->setValue(new Literal('RecursiveIteratorIterator::SELF_FIRST')) + ->addComment('foo') + ->removeComment(); $class->addProperty('typed1') - ->setType(Type::ARRAY); + ->setType(Type::Array) + ->setReadOnly(); $class->addProperty('typed2') - ->setType(Type::ARRAY) + ->setType(Type::Array) ->setNullable() ->setInitialized(); +$class->addProperty('typed3') + ->setType(Type::Array) + ->setValue(null); + $p = $class->addProperty('sections', ['first' => true]) ->setStatic(true); @@ -80,7 +91,7 @@ Assert::true($p->isPublic()); $m = $class->addMethod('getHandle') ->addComment('Returns file handle.') ->addComment('@return resource') - ->setFinal(true) + ->setFinal() ->setBody('return $this->?;', ['handle']); Assert::same($m, $class->getMethod('getHandle')); @@ -94,12 +105,12 @@ Assert::same('public', $m->getVisibility()); Assert::same('return $this->handle;', $m->getBody()); $m = $class->addMethod('getSections') - ->setStatic(true) + ->setStatic() ->setVisibility('protected') - ->setReturnReference(true) + ->setReturnReference() ->addBody('$mode = ?;', [123]) ->addBody('return self::$sections;'); -$m->addParameter('mode', new PhpLiteral('self::ORDER')); +$m->addParameter('mode', new Literal('self::ORDER')); Assert::false($m->isFinal()); Assert::true($m->isStatic()); @@ -112,17 +123,24 @@ Assert::true($m->isProtected()); Assert::false($m->isPublic()); $method = $class->addMethod('show') - ->setAbstract(true); + ->setAbstract(); -$method->addParameter('foo'); +$p = $method->addParameter('foo'); +Assert::true($method->hasParameter('foo')); +Assert::same($p, $method->getParameter('foo')); $method->removeParameter('foo'); +Assert::false($method->hasParameter('foo')); -$method->addParameter('item'); +$method->addParameter('item') + ->addComment('comment'); $method->addParameter('res', null) - ->setReference(true) - ->setType(Type::ARRAY); + ->setReference() + ->setType(Type::union(Type::Array, 'null')); +$method->addParameter('bar', null) + ->setNullable() + ->setType('stdClass|string'); $class->addTrait('foo'); $class->removeTrait('foo'); @@ -130,6 +148,12 @@ $class->removeTrait('foo'); $class->addImplement('foo'); $class->removeImplement('foo'); +$class + ->addTrait('ThirdTrait') + ->addResolution('a as private foo') + ->addResolution('b as private bar') + ->addComment('@use Foo'); + sameFile(__DIR__ . '/expected/ClassType.expect', (string) $class); @@ -141,20 +165,72 @@ $class->setMethods(array_values($methods)); Assert::same($methods, $class->getMethods()); $properties = $class->getProperties(); -Assert::count(5, $properties); +Assert::count(6, $properties); $class->setProperties(array_values($properties)); Assert::same($properties, $class->getProperties()); $parameters = $method->getParameters(); -Assert::count(2, $parameters); +Assert::count(3, $parameters); $method->setParameters(array_values($parameters)); Assert::same($parameters, $method->getParameters()); -Assert::exception(function () { - $class = new ClassType; - $class->addMethod('method')->setVisibility('unknown'); -}, Nette\InvalidArgumentException::class, 'Argument must be public|protected|private.'); +Assert::exception( + fn() => (new ClassType)->addMethod('method')->setVisibility('unknown'), + ValueError::class, +); + + +// duplicity +$class = new ClassType('Example'); +$class->addConstant('a', 1); +Assert::exception( + fn() => $class->addConstant('a', 1), + Nette\InvalidStateException::class, + "Cannot add constant 'a', because it already exists.", +); + +$class->addProperty('a'); +Assert::exception( + fn() => $class->addProperty('a'), + Nette\InvalidStateException::class, + "Cannot add property 'a', because it already exists.", +); + +$class->addMethod('a'); +Assert::exception( + fn() => $class->addMethod('a'), + Nette\InvalidStateException::class, + "Cannot add method 'a', because it already exists.", +); + +Assert::exception( + fn() => $class->addMethod('A'), + Nette\InvalidStateException::class, + "Cannot add method 'A', because it already exists.", +); + +$class->addTrait('A'); +Assert::exception( + fn() => $class->addTrait('A'), + Nette\InvalidStateException::class, + "Cannot add trait 'A', because it already exists.", +); + + +// overwrite +$class = new ClassType('Example'); +$class->addConstant('a', 1); +$new = $class->addConstant('a', 1, overwrite: true); +Assert::same($new, $class->getConstant('a')); + +$class->addProperty('a'); +$new = $class->addProperty('a', overwrite: true); +Assert::same($new, $class->getProperty('a')); + +$class->addMethod('a'); +$new = $class->addMethod('a', overwrite: true); +Assert::same($new, $class->getMethod('a')); // remove members @@ -173,6 +249,6 @@ Assert::same(['a'], array_keys($class->getProperties())); $class->addMethod('a'); $class->addMethod('b'); -$class->removeMethod('b')->removeMethod('c'); +$class->removeMethod('B')->removeMethod('c'); Assert::same(['a'], array_keys($class->getMethods())); diff --git a/tests/PhpGenerator/ClassType.promotion.phpt b/tests/PhpGenerator/ClassType.promotion.phpt new file mode 100644 index 00000000..06649810 --- /dev/null +++ b/tests/PhpGenerator/ClassType.promotion.phpt @@ -0,0 +1,25 @@ +addMethod('__construct'); +$method->addParameter('a'); +$method->addPromotedParameter('b'); +$method->addPromotedParameter('c') + ->setPrivate() + ->setType('string') + ->addComment('promo') + ->addAttribute('Example'); + +$method->addPromotedParameter('d', Literal::new('Draft', [10])) + ->setType('Draft') + ->setReadOnly(); + +sameFile(__DIR__ . '/expected/ClassType.promotion.expect', (string) $class); diff --git a/tests/PhpGenerator/ClassType.readonly.phpt b/tests/PhpGenerator/ClassType.readonly.phpt new file mode 100644 index 00000000..33079aa7 --- /dev/null +++ b/tests/PhpGenerator/ClassType.readonly.phpt @@ -0,0 +1,23 @@ +getProperty('foo')->isReadOnly()); +Assert::true($class->getMethod('__construct')->getParameter('bar')->isReadOnly()); + +$file = (new Extractor(file_get_contents(__DIR__ . '/fixtures/classes.82.php')))->extractAll(); +$class = $file->getClasses()[Abc\Class13::class]; +Assert::true($class->getProperty('foo')->isReadOnly()); +Assert::true($class->getMethod('__construct')->getParameter('bar')->isReadOnly()); diff --git a/tests/PhpGenerator/ClassType.validate.phpt b/tests/PhpGenerator/ClassType.validate.phpt index 4dbadf99..874597c1 100644 --- a/tests/PhpGenerator/ClassType.validate.phpt +++ b/tests/PhpGenerator/ClassType.validate.phpt @@ -5,24 +5,23 @@ declare(strict_types=1); use Nette\PhpGenerator\ClassType; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; Assert::exception(function () { $class = new ClassType('A'); - $class->setFinal(true)->setAbstract(true); + $class->setFinal()->setAbstract(); $class->validate(); -}, Nette\InvalidStateException::class, 'Class cannot be abstract and final.'); +}, Nette\InvalidStateException::class, "Class 'A' cannot be abstract and final at the same time."); Assert::exception(function () { $class = new ClassType('A'); - $class->setAbstract(true)->setFinal(true); + $class->setAbstract()->setFinal(); $class->validate(); -}, Nette\InvalidStateException::class, 'Class cannot be abstract and final.'); +}, Nette\InvalidStateException::class, "Class 'A' cannot be abstract and final at the same time."); Assert::exception(function () { $class = new ClassType; - $class->setAbstract(true); + $class->setAbstract(); $class->validate(); }, Nette\InvalidStateException::class, 'Anonymous class cannot be abstract or final.'); diff --git a/tests/PhpGenerator/Closure.from.phpt b/tests/PhpGenerator/Closure.from.phpt new file mode 100644 index 00000000..aab8faba --- /dev/null +++ b/tests/PhpGenerator/Closure.from.phpt @@ -0,0 +1,19 @@ +setReturnReference(true) + ->setReturnReference() ->setBody('return $a + $b;'); $function->addParameter('a'); $function->addParameter('b'); $function->addUse('this'); $function->addUse('vars') - ->setReference(true); + ->setReference(); same( -'function &($a, $b) use ($this, &$vars) { - return $a + $b; -}', (string) $function); + <<<'XX' + function &($a, $b) use ($this, &$vars) { + return $a + $b; + } + XX, + (string) $function, +); $uses = $function->getUses(); @@ -34,9 +37,13 @@ Assert::type(Nette\PhpGenerator\Parameter::class, $uses[1]); $uses = $function->setUses([$uses[0]]); same( -'function &($a, $b) use ($this) { - return $a + $b; -}', (string) $function); + <<<'XX' + function &($a, $b) use ($this) { + return $a + $b; + } + XX, + (string) $function, +); @@ -54,14 +61,46 @@ $function ->addUse('this'); same( -'function () use ($this): array { - return []; -}', (string) $function); + <<<'XX' + function () use ($this): array { + return []; + } + XX, + (string) $function, +); + +$function = new Closure; +$function->setBody('return $a + $b;'); +$function->addAttribute('ExampleAttribute'); + +same( + <<<'XX' + #[ExampleAttribute] function () { + return $a + $b; + } + XX, + (string) $function, +); + + + +$function = new Closure; +$function->setBody('return $a + $b;'); +$function->addAttribute('Foo', ['a', str_repeat('b', 120)]); +$function->addAttribute('Bar'); -$closure = function (stdClass $a, $b = null) {}; -$function = Closure::from($closure); same( -'function (stdClass $a, $b = null) { -}', (string) $function); + <<<'XX' + #[Foo( + 'a', + 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + )] + #[Bar] + function () { + return $a + $b; + } + XX, + (string) $function, +); diff --git a/tests/PhpGenerator/Dumper.dump().enum.phpt b/tests/PhpGenerator/Dumper.dump().enum.phpt new file mode 100644 index 00000000..487bd1e1 --- /dev/null +++ b/tests/PhpGenerator/Dumper.dump().enum.phpt @@ -0,0 +1,24 @@ +dump(Suit::Clubs)); diff --git a/tests/PhpGenerator/Dumper.dump().errors.phpt b/tests/PhpGenerator/Dumper.dump().errors.phpt new file mode 100644 index 00000000..f38820fe --- /dev/null +++ b/tests/PhpGenerator/Dumper.dump().errors.phpt @@ -0,0 +1,28 @@ +dump($rec); +}, Nette\InvalidStateException::class, 'Nesting level too deep or recursive dependency.'); + + +Assert::exception(function () { + $rec = new stdClass; + $rec->x = &$rec; + $dumper = new Dumper; + $dumper->dump($rec); +}, Nette\InvalidStateException::class, 'Nesting level too deep or recursive dependency.'); diff --git a/tests/PhpGenerator/Dumper.dump().indent.phpt b/tests/PhpGenerator/Dumper.dump().indent.phpt index f01c4973..c3e9e970 100644 --- a/tests/PhpGenerator/Dumper.dump().indent.phpt +++ b/tests/PhpGenerator/Dumper.dump().indent.phpt @@ -7,9 +7,9 @@ declare(strict_types=1); use Nette\PhpGenerator\Dumper; +use Nette\PhpGenerator\Literal; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -38,3 +38,21 @@ same('[ 2, 3, ]', $dumper->dump([8 => 1, 2, 3], $dumper->wrapLength - 13)); + + +$dumper = new Dumper; +$dumper->indentation = ' '; +same('[ + 1, + 2, + 3, +]', $dumper->dump([1, 2, 3], $dumper->wrapLength - 8)); + +same( + "[ + 'multi' => [ + 1, + ], +]", + $dumper->dump(['multi' => new Literal("[\n1,\n]\n")]), +); diff --git a/tests/PhpGenerator/Dumper.dump().phpt b/tests/PhpGenerator/Dumper.dump().phpt index 3cb85f45..e0e47933 100644 --- a/tests/PhpGenerator/Dumper.dump().phpt +++ b/tests/PhpGenerator/Dumper.dump().phpt @@ -7,15 +7,16 @@ declare(strict_types=1); use Nette\PhpGenerator\Dumper; -use Nette\PhpGenerator\PhpLiteral; +use Nette\PhpGenerator\Literal; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; ini_set('serialize_precision', '14'); $dumper = new Dumper; + +// scalars Assert::same('0', $dumper->dump(0)); Assert::same('1', $dumper->dump(1)); Assert::same('0.0', $dumper->dump(0.0)); @@ -30,50 +31,56 @@ Assert::same('false', $dumper->dump(false)); Assert::same("''", $dumper->dump('')); Assert::same("'Hello'", $dumper->dump('Hello')); -Assert::same('"\t\n\t"', $dumper->dump("\t\n\t")); +Assert::same('"\t\n\r\e"', $dumper->dump("\t\n\r\e")); +Assert::same('"\u{FEFF}"', $dumper->dump("\xEF\xBB\xBF")); // BOM +Assert::same('\'$"\\\\\'', $dumper->dump('$"\\')); +Assert::same('\'$"\ \x00\'', $dumper->dump('$"\ \x00')); // no escape +Assert::same('"\$\"\\\ \x00"', $dumper->dump("$\"\\ \x00")); Assert::same( "'I\u{F1}t\u{EB}rn\u{E2}ti\u{F4}n\u{E0}liz\u{E6}ti\u{F8}n'", - $dumper->dump("I\u{F1}t\u{EB}rn\u{E2}ti\u{F4}n\u{E0}liz\u{E6}ti\u{F8}n") // Iñtërnâtiônàlizætiøn + $dumper->dump("I\u{F1}t\u{EB}rn\u{E2}ti\u{F4}n\u{E0}liz\u{E6}ti\u{F8}n"), // Iñtërnâtiônàlizætiøn ); Assert::same('"\rHello \$"', $dumper->dump("\rHello $")); Assert::same("'He\\llo'", $dumper->dump('He\llo')); Assert::same('\'He\ll\\\\\o \\\'wor\\\\\\\'ld\\\\\'', $dumper->dump('He\ll\\\o \'wor\\\'ld\\')); -Assert::same('[]', $dumper->dump([])); -Assert::same('[$s]', $dumper->dump([new PhpLiteral('$s')])); +// literal +Assert::same('[$s]', $dumper->dump([new Literal('$s')])); +Assert::same("[strlen('hello')]", $dumper->dump([new Literal('strlen(?)', ['hello'])])); +Assert::same("a\nb", $dumper->dump(new Literal("a\r\nb"))); + + +// Literal::new +Assert::same('new stdClass()', $dumper->dump(Literal::new('stdClass'))); +Assert::same('new stdClass(10, 20)', $dumper->dump(Literal::new('stdClass', [10, 20]))); +Assert::same('new stdClass(10, c: 20)', $dumper->dump(Literal::new('stdClass', [10, 'c' => 20]))); + + +// arrays +Assert::same('[]', $dumper->dump([])); Assert::same('[1, 2, 3]', $dumper->dump([1, 2, 3])); Assert::same("['a']", $dumper->dump(['a'])); Assert::same("[2 => 'a']", $dumper->dump([2 => 'a'])); Assert::same("[2 => 'a', 'b']", $dumper->dump([2 => 'a', 'b'])); -Assert::same("[-2 => 'a', -1 => 'b']", $dumper->dump([-2 => 'a', -1 => 'b'])); -Assert::same("[-2 => 'a', 0 => 'b']", $dumper->dump([-2 => 'a', 'b'])); -Assert::same("[0 => 'a', -2 => 'b', 1 => 'c']", $dumper->dump(['a', -2 => 'b', 'c'])); - -$dumper->wrapLength = 100; -same("[ - [ - 'a', - 'loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong', - ], -]", $dumper->dump([['a', 'loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong']])); +Assert::same("[-2 => 'a', 'b']", $dumper->dump([-2 => 'a', -1 => 'b'])); +Assert::same("[-2 => 'a', 0 => 'b']", $dumper->dump([-2 => 'a', 0 => 'b'])); +Assert::same("[0 => 'a', -2 => 'b', 1 => 'c']", $dumper->dump(['a', -2 => 'b', 1 => 'c'])); -Assert::same( - "['a' => 1, 0 => [\"\\r\" => \"\\r\", 0 => 2], 1 => 3]", - $dumper->dump(['a' => 1, ["\r" => "\r", 2], 3]) -); +// stdClass Assert::same( - "(object) [\n\t'a' => 1,\n\t'b' => 2,\n]", - $dumper->dump((object) ['a' => 1, 'b' => 2]) + "(object) ['a' => 1, 'b' => 2]", + $dumper->dump((object) ['a' => 1, 'b' => 2]), ); Assert::same( - "(object) [\n\t'a' => (object) [\n\t\t'b' => 2,\n\t],\n]", - $dumper->dump((object) ['a' => (object) ['b' => 2]]) + "(object) ['a' => (object) ['b' => 2]]", + $dumper->dump((object) ['a' => (object) ['b' => 2]]), ); +// objects class Test { public $a = 1; @@ -84,8 +91,8 @@ class Test } Assert::same( - "\\Nette\\PhpGenerator\\Dumper::createObject('Test', [\n\t'a' => 1,\n\t\"\\x00*\\x00b\" => 2,\n\t\"\\x00Test\\x00c\" => 3,\n])", - $dumper->dump(new Test) + "\\Nette\\PhpGenerator\\Dumper::createObject(\\Test::class, [\n\t'a' => 1,\n\t\"\\x00*\\x00b\" => 2,\n\t\"\\x00Test\\x00c\" => 3,\n])", + $dumper->dump(new Test), ); Assert::equal(new Test, eval('return ' . $dumper->dump(new Test) . ';')); @@ -109,77 +116,89 @@ class Test2 extends Test } Assert::same( - "\\Nette\\PhpGenerator\\Dumper::createObject('Test2', [\n\t\"\\x00Test2\\x00c\" => 4,\n\t'a' => 1,\n\t\"\\x00*\\x00b\" => 2,\n])", - $dumper->dump(new Test2) + "\\Nette\\PhpGenerator\\Dumper::createObject(\\Test2::class, [\n\t'a' => 1,\n\t\"\\x00*\\x00b\" => 2,\n\t\"\\x00Test2\\x00c\" => 4,\n])", + $dumper->dump(new Test2), ); Assert::equal(new Test2, eval('return ' . $dumper->dump(new Test2) . ';')); -class Test3 implements Serializable -{ - private $a; +Assert::exception(function () { + $dumper = new Dumper; + $dumper->dump(new class { + }); +}, Nette\InvalidStateException::class, 'Cannot dump an instance of an anonymous class.'); + + + +// closures +Assert::same( + 'strlen(...)', + $dumper->dump(Closure::fromCallable('strlen')), +); + +Assert::same( + 'Nette\PhpGenerator\ClassLike::from(...)', + $dumper->dump(Closure::fromCallable([Nette\PhpGenerator\ClassLike::class, 'from'])), +); +Assert::exception(function () { + $dumper = new Dumper; + $dumper->dump(function () {}); +}, Nette\InvalidStateException::class, 'Cannot dump object of type Closure.'); - public function serialize() + + +// __serialize +class TestSer +{ + public function __serialize(): array { - return ''; + return ['a', 'b']; } - public function unserialize($s) + public function __unserialize(array $data): void { } } -Assert::same('unserialize(\'C:5:"Test3":0:{}\')', $dumper->dump(new Test3)); -Assert::equal(new Test3, eval('return ' . $dumper->dump(new Test3) . ';')); -Assert::exception(function () { - $dumper = new Dumper; - $dumper->dump(function () {}); -}, Nette\InvalidArgumentException::class, 'Cannot dump closure.'); +$dumper = new Dumper; +Assert::same("\\Nette\\PhpGenerator\\Dumper::createObject(\\TestSer::class, [\n\t0 => 'a',\n\t1 => 'b',\n])", $dumper->dump(new TestSer)); +Assert::equal(new TestSer, eval('return ' . $dumper->dump(new TestSer) . ';')); +// datetime class TestDateTime extends DateTime { } Assert::same( "new \\DateTime('2016-06-22 20:52:43.123400', new \\DateTimeZone('Europe/Prague'))", - $dumper->dump(new DateTime('2016-06-22 20:52:43.1234', new DateTimeZone('Europe/Prague'))) + $dumper->dump(new DateTime('2016-06-22 20:52:43.1234', new DateTimeZone('Europe/Prague'))), ); Assert::same( "new \\DateTimeImmutable('2016-06-22 20:52:43.123400', new \\DateTimeZone('Europe/Prague'))", - $dumper->dump(new DateTimeImmutable('2016-06-22 20:52:43.1234', new DateTimeZone('Europe/Prague'))) + $dumper->dump(new DateTimeImmutable('2016-06-22 20:52:43.1234', new DateTimeZone('Europe/Prague'))), ); same( - "\\Nette\\PhpGenerator\\Dumper::createObject('TestDateTime', [ - 'date' => '2016-06-22 20:52:43.123400', - 'timezone_type' => 3, - 'timezone' => 'Europe/Prague', -])", - $dumper->dump(new TestDateTime('2016-06-22 20:52:43.1234', new DateTimeZone('Europe/Prague'))) + <<<'XX' + \Nette\PhpGenerator\Dumper::createObject(\TestDateTime::class, [ + 'date' => '2016-06-22 20:52:43.123400', + 'timezone_type' => 3, + 'timezone' => 'Europe/Prague', + ]) + XX, + $dumper->dump(new TestDateTime('2016-06-22 20:52:43.1234', new DateTimeZone('Europe/Prague'))), ); -Assert::exception(function () { - $dumper = new Dumper; - $dumper->dump(new class { - }); -}, Nette\InvalidArgumentException::class, 'Cannot dump anonymous class.'); - - -Assert::exception(function () { - $rec = []; - $rec[] = &$rec; - $dumper = new Dumper; - $dumper->dump($rec); -}, Nette\InvalidArgumentException::class, 'Nesting level too deep or recursive dependency.'); - -Assert::exception(function () { - $rec = new stdClass; - $rec->x = &$rec; - $dumper = new Dumper; - $dumper->dump($rec); -}, Nette\InvalidArgumentException::class, 'Nesting level too deep or recursive dependency.'); +// disallow custom objects +$dumper = new Dumper; +$dumper->customObjects = false; +Assert::exception( + fn() => $dumper->dump(new TestSer), + Nette\InvalidStateException::class, + 'Cannot dump object of type TestSer.', +); diff --git a/tests/PhpGenerator/Dumper.dump().wrap.phpt b/tests/PhpGenerator/Dumper.dump().wrap.phpt index 1e57c75f..35b0c870 100644 --- a/tests/PhpGenerator/Dumper.dump().wrap.phpt +++ b/tests/PhpGenerator/Dumper.dump().wrap.phpt @@ -7,60 +7,70 @@ declare(strict_types=1); use Nette\PhpGenerator\Dumper; -use Nette\PhpGenerator\PhpLiteral; - +use Nette\PhpGenerator\Literal; require __DIR__ . '/../bootstrap.php'; $dumper = new Dumper; -$dumper->wrapLength = 21; -same("[ - 'a' => [1, 2, 3], - 'aaaaaaaaa' => [ - 1, - 2, - 3, - ], -]", +$dumper->wrapLength = 28; +same( + <<<'XX' + [ + 'a' => [1, 2, 3], + 'aaaaaaaaa' => [ + 1, + 2, + 3, + ], + ] + XX, $dumper->dump([ 'a' => [1, 2, 3], 'aaaaaaaaa' => [1, 2, 3], - ]) + ]), ); -same("[ - 'single' => 1 + 2, - 'multi' => [ - 1, - ], -]", +same( + <<<'XX' + [ + 'single' => 1 + 2, + 'multi' => [ + 1, + ], + ] + XX, $dumper->dump([ - 'single' => new PhpLiteral('1 + 2'), - 'multi' => new PhpLiteral("[\n\t1,\n]\n"), - ]) + 'single' => new Literal('1 + 2'), + 'multi' => new Literal("[\n\t1,\n]\n"), + ]), ); -same("(object) [ - 'a' => [1, 2, 3], - 'aaaaaaaaa' => [ - 1, - 2, - 3, - ], -]", +same( + <<<'XX' + (object) [ + 'a' => [1, 2, 3], + 'aaaaaaaaa' => [ + 1, + 2, + 3, + ], + ] + XX, $dumper->dump((object) [ 'a' => [1, 2, 3], 'aaaaaaaaa' => [1, 2, 3], - ]) + ]), ); $dumper = new Dumper; $dumper->wrapLength = 100; -same("[ +same(<<<'XX' [ - 'a', - 'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong', - ], -]", $dumper->dump([['a', 'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong']])); + [ + 'a', + 'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong', + ], + ] + XX, $dumper->dump([['a', 'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong']])); diff --git a/tests/PhpGenerator/Dumper.format().phpt b/tests/PhpGenerator/Dumper.format().phpt index cceb8894..c8b79fde 100644 --- a/tests/PhpGenerator/Dumper.format().phpt +++ b/tests/PhpGenerator/Dumper.format().phpt @@ -9,50 +9,55 @@ declare(strict_types=1); use Nette\PhpGenerator\Dumper; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; $dumper = new Dumper; Assert::same('func', $dumper->format('func')); Assert::same('func(1)', $dumper->format('func(?)', 1)); +Assert::same('fn(?string $x = 1)', $dumper->format('fn(?string $x = ?)', 1)); +Assert::same('fn(?string $x = 1)', $dumper->format('fn(\?string $x = ?)', 1)); Assert::same('func(1 ? 2 : 3)', $dumper->format('func(1 \? 2 : 3)')); Assert::same('func([1, 2])', $dumper->format('func(?)', [1, 2])); Assert::same('func(1, 2)', $dumper->format('func(...?)', [1, 2])); +Assert::same('func(1, 2)', $dumper->format('func(...?)', [1, 'a' => 2])); +Assert::same('func(1, a: 2)', $dumper->format('func(...?:)', [1, 'a' => 2])); // named args Assert::same('func(1, 2)', $dumper->format('func(?*)', [1, 2])); // old way $dumper->wrapLength = 100; same( -'func( - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36 -)', - $dumper->format('func(?*)', range(10, 36)) + <<<'XX' + func( + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + ) + XX, + $dumper->format('func(?*)', range(10, 36)), ); Assert::exception(function () { diff --git a/tests/PhpGenerator/Dumper.format().wrap.phpt b/tests/PhpGenerator/Dumper.format().wrap.phpt index 896e783f..5eb1e38e 100644 --- a/tests/PhpGenerator/Dumper.format().wrap.phpt +++ b/tests/PhpGenerator/Dumper.format().wrap.phpt @@ -9,7 +9,6 @@ declare(strict_types=1); use Nette\PhpGenerator\Dumper; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -18,33 +17,53 @@ $dumper->wrapLength = 100; Assert::same('func([1, 2, 3])', $dumper->format('func(?)', [1, 2, 3])); -same('loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong([ - 1, - 2, - 3, -])', $dumper->format('loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong(?)', [1, 2, 3])); - - -same('looooooooooooooooooooooooooooooooooooooooo([1, 2, 3]) + ooooooooooooooooooooooooooooooooooooooooooooooong([ - 1, - 2, - 3, -])', $dumper->format('looooooooooooooooooooooooooooooooooooooooo(?) + ooooooooooooooooooooooooooooooooooooooooooooooong(?)', [1, 2, 3], [1, 2, 3])); +same( + <<<'XX' + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong([ + 1, + 2, + 3, + ]) + XX, + $dumper->format('loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong(?)', [1, 2, 3]), +); + + +same( + <<<'XX' + looooooooooooooooooooooooooooooooooooooooo([1, 2, 3]) + ooooooooooooooooooooooooooooooooooooooooooooooong([ + 1, + 2, + 3, + ]) + XX, + $dumper->format('looooooooooooooooooooooooooooooooooooooooo(?) + ooooooooooooooooooooooooooooooooooooooooooooooong(?)', [1, 2, 3], [1, 2, 3]), +); // variadics Assert::same('func(1, 2, 3)', $dumper->format('func(...?)', [1, 2, 3])); -same('loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong( - 1, - 2, - 3 -)', $dumper->format('loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong(...?)', [1, 2, 3])); - - -same('looooooooooooooooooooooooooooooooooooooooo(1, 2, 3) + ooooooooooooooooooooooooooooooooooooooooooooooong( - 1, - 2, - 3 -)', $dumper->format('looooooooooooooooooooooooooooooooooooooooo(...?) + ooooooooooooooooooooooooooooooooooooooooooooooong(...?)', [1, 2, 3], [1, 2, 3])); +same( + <<<'XX' + loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong( + 1, + 2, + 3, + ) + XX, + $dumper->format('loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong(...?)', [1, 2, 3]), +); + + +same( + <<<'XX' + looooooooooooooooooooooooooooooooooooooooo(1, 2, 3) + ooooooooooooooooooooooooooooooooooooooooooooooong( + 1, + 2, + 3, + ) + XX, + $dumper->format('looooooooooooooooooooooooooooooooooooooooo(...?) + ooooooooooooooooooooooooooooooooooooooooooooooong(...?)', [1, 2, 3], [1, 2, 3]), +); diff --git a/tests/PhpGenerator/EnumType.from.phpt b/tests/PhpGenerator/EnumType.from.phpt new file mode 100644 index 00000000..977e7c5a --- /dev/null +++ b/tests/PhpGenerator/EnumType.from.phpt @@ -0,0 +1,13 @@ +isEnum()); + +$enum->addComment("Description of class.\nThis is example\n") + ->addAttribute('ExampleAttribute'); + +$enum->addConstant('ACTIVE', false); +$enum->addTrait('ObjectTrait'); + +$enum->addMethod('foo') + ->setBody('return 10;'); + +$enum->addCase('Clubs') + ->addComment('♣') + ->addAttribute('ValueAttribute'); +$enum->addCase('Diamonds') + ->addComment('♦'); +$enum->addCase('Hearts'); +$enum->addCase('Spades'); + +$res[] = $enum; + + +$enum = new EnumType('Method'); +$enum->addImplement('IOne'); + +$enum->addCase('GET', 'get'); +$enum->addCase('POST', 'post'); + +$res[] = $enum; + +sameFile(__DIR__ . '/expected/ClassType.enum.expect', implode("\n", $res)); diff --git a/tests/PhpGenerator/Extractor.extractAll.file.comments.phpt b/tests/PhpGenerator/Extractor.extractAll.file.comments.phpt new file mode 100644 index 00000000..03eafa44 --- /dev/null +++ b/tests/PhpGenerator/Extractor.extractAll.file.comments.phpt @@ -0,0 +1,43 @@ +extractAll(); + +Assert::null($file->getComment()); +Assert::same('doc comment', $file->getClasses()['Class1']->getComment()); + + +$file = (new Extractor(<<<'XX' + extractAll(); + +Assert::same('doc comment', $file->getComment()); + + +$file = (new Extractor(<<<'XX' + extractAll(); + +Assert::null($file->getComment()); diff --git a/tests/PhpGenerator/Extractor.extractAll.phpt b/tests/PhpGenerator/Extractor.extractAll.phpt new file mode 100644 index 00000000..d9a488fe --- /dev/null +++ b/tests/PhpGenerator/Extractor.extractAll.phpt @@ -0,0 +1,44 @@ +extractAll(); +Assert::type(Nette\PhpGenerator\PhpFile::class, $file); +sameFile(__DIR__ . '/expected/Extractor.classes.expect', (string) $file); + +$file = (new Extractor(file_get_contents(__DIR__ . '/fixtures/classes.81.php')))->extractAll(); +sameFile(__DIR__ . '/expected/Extractor.classes.81.expect', (string) $file); + +$file = (new Extractor(file_get_contents(__DIR__ . '/fixtures/classes.82.php')))->extractAll(); +sameFile(__DIR__ . '/expected/Extractor.classes.82.expect', (string) $file); + +if (class_exists(PhpParser\Node\PropertyHook::class)) { + $file = (new Extractor(file_get_contents(__DIR__ . '/fixtures/classes.84.php')))->extractAll(); + sameFile(__DIR__ . '/expected/Extractor.classes.84.expect', (string) $file); + + $file = (new Extractor(file_get_contents(__DIR__ . '/fixtures/classes.85.php')))->extractAll(); + sameFile(__DIR__ . '/expected/Extractor.classes.85.expect', (string) $file); +} + +$file = (new Extractor(file_get_contents(__DIR__ . '/fixtures/enum.php')))->extractAll(); +sameFile(__DIR__ . '/expected/Extractor.enum.expect', (string) $file); + +$file = (new Extractor(file_get_contents(__DIR__ . '/fixtures/traits.php')))->extractAll(); +sameFile(__DIR__ . '/expected/Extractor.traits.expect', (string) $file); + +$file = (new Extractor(file_get_contents(__DIR__ . '/fixtures/bodies.php')))->extractAll(); +sameFile(__DIR__ . '/expected/Extractor.bodies.expect', (string) $file); + +$file = (new Extractor(file_get_contents(__DIR__ . '/fixtures/extractor.php')))->extractAll(); +sameFile(__DIR__ . '/expected/Extractor.expect', (string) $file); + +Assert::exception( + fn() => new Extractor(''), + Nette\InvalidStateException::class, + 'The input string is not a PHP code.', +); diff --git a/tests/PhpGenerator/Extractor.extractAll.resolving.phpt b/tests/PhpGenerator/Extractor.extractAll.resolving.phpt new file mode 100644 index 00000000..20affda6 --- /dev/null +++ b/tests/PhpGenerator/Extractor.extractAll.resolving.phpt @@ -0,0 +1,23 @@ +extractAll(); +$classes = $file->getClasses(); + +$namespace = new PhpNamespace('Nette'); +$namespace->addUse('Abc\a\FOO'); // must not be confused with constant +$namespace->addUse('Abc\a\func'); // must not be confused with func +$namespace->add(reset($classes)); + +$printer = new Printer; +sameFile(__DIR__ . '/expected/Extractor.bodies.resolving.expect', $printer->printNamespace($namespace)); + +$printer->setTypeResolving(false); +sameFile(__DIR__ . '/expected/Extractor.bodies.unresolving.expect', $printer->printNamespace($namespace)); diff --git a/tests/PhpGenerator/Extractor.extractAll.vars.phpt b/tests/PhpGenerator/Extractor.extractAll.vars.phpt new file mode 100644 index 00000000..b5c183b0 --- /dev/null +++ b/tests/PhpGenerator/Extractor.extractAll.vars.phpt @@ -0,0 +1,82 @@ + [3]]]; + public $arraySpec1 = [...self::Foo]; + public $arraySpec2 = [self::class => 1]; + public $concat = 'x' . 'y'; + public $math = 10 * 3; + + public function foo($a = [1, 2, 3], $b = new stdClass(1, 2)) + { + } + } + XX))->extractAll(); + + +$class = $file->getClasses()['Class1']; +Assert::equal( + new Attribute('Attr', [1, 'foo' => 2, 'bar' => new Literal('new /*(n*/\Attr(3)')]), + $class->getAttributes()[0], +); + +Assert::same([1], $class->getConstant('Foo')->getValue()); + +Assert::same(null, $class->getProperty('null')->getValue()); +Assert::same( + [true, false, 1, 1.0, 'hello'], + $class->getProperty('scalar')->getValue(), +); +Assert::equal( + [new Literal('/*(c*/\PHP_VERSION'), new Literal('self::Foo')], + $class->getProperty('const')->getValue(), +); +Assert::equal( + [1, 2, ['x' => [3]]], + $class->getProperty('array')->getValue(), +); +Assert::equal( + new Literal('[...self::Foo]'), + $class->getProperty('arraySpec1')->getValue(), +); +Assert::equal( + new Literal('[self::class => 1]'), + $class->getProperty('arraySpec2')->getValue(), +); +Assert::equal( + new Literal("'x' . 'y'"), + $class->getProperty('concat')->getValue(), +); +Assert::equal( + new Literal('10 * 3'), + $class->getProperty('math')->getValue(), +); + +$method = $class->getMethod('foo'); +Assert::same( + [1, 2, 3], + $method->getParameter('a')->getDefaultValue(), +); +Assert::equal( + new Literal('new /*(n*/\stdClass(1, 2)'), + $method->getParameter('b')->getDefaultValue(), +); diff --git a/tests/PhpGenerator/Extractor.extractFunctionBody.phpt b/tests/PhpGenerator/Extractor.extractFunctionBody.phpt new file mode 100644 index 00000000..ae81dc22 --- /dev/null +++ b/tests/PhpGenerator/Extractor.extractFunctionBody.phpt @@ -0,0 +1,31 @@ +extractFunctionBody('NS\bar1'), +); diff --git a/tests/PhpGenerator/Extractor.extractMethodBodies.phpt b/tests/PhpGenerator/Extractor.extractMethodBodies.phpt new file mode 100644 index 00000000..a499442d --- /dev/null +++ b/tests/PhpGenerator/Extractor.extractMethodBodies.phpt @@ -0,0 +1,64 @@ +name; + } + } + + XX); + +$bodies = $extractor->extractMethodBodies('NS\Undefined'); +Assert::same([], $bodies); + +$bodies = $extractor->extractMethodBodies('NS\Foo'); +Assert::same([ + 'bar1' => "\$a = 10;\necho 123;", + 'bar2' => 'echo "hello";', +], $bodies); + +$bodies = $extractor->extractMethodBodies('NS\Color'); +Assert::same([ + 'getName' => 'return $this->name;', +], $bodies); diff --git a/tests/PhpGenerator/Extractor.extractPropertyHookBodies.phpt b/tests/PhpGenerator/Extractor.extractPropertyHookBodies.phpt new file mode 100644 index 00000000..d7dd0e97 --- /dev/null +++ b/tests/PhpGenerator/Extractor.extractPropertyHookBodies.phpt @@ -0,0 +1,53 @@ + 'x'; + } + + public string $full { + get { + if (true) { + return 'x'; + } else { + return 'y'; + } + } + } + + public string $empty { + set { } + } + + abstract public string $abstract { get; } + } + + XX); + +$bodies = $extractor->extractPropertyHookBodies('NS\Undefined'); +Assert::same([], $bodies); + +$bodies = $extractor->extractPropertyHookBodies('NS\Foo'); +Assert::same([ + 'short' => ['get' => ["'x'", true]], + 'full' => [ + 'get' => ["if (true) {\n return 'x';\n} else {\n return 'y';\n}", false], + ], + 'empty' => ['set' => ['', false]], +], $bodies); diff --git a/tests/PhpGenerator/Extractor.strings.phpt b/tests/PhpGenerator/Extractor.strings.phpt new file mode 100644 index 00000000..c091ce08 --- /dev/null +++ b/tests/PhpGenerator/Extractor.strings.phpt @@ -0,0 +1,158 @@ +extractFunctionBody('quoted'), +); + + +Assert::match( + <<<'XX' + $s1 = <<extractFunctionBody('heredoc'), +); + + +Assert::match( + <<<'XX' + $s1 = <<<'DOC' + a + b + c 'q1' "q2" + DOC; + + $s2 = <<<'DOC' + a + b + c + DOC; + + $s3 = <<<'DOC' + a + b + c + DOC; + XX, + $extractor->extractFunctionBody('nowdoc'), +); diff --git a/tests/PhpGenerator/Factory.fromClassCode.phpt b/tests/PhpGenerator/Factory.fromClassCode.phpt new file mode 100644 index 00000000..2b1ca92b --- /dev/null +++ b/tests/PhpGenerator/Factory.fromClassCode.phpt @@ -0,0 +1,24 @@ +fromClassCode(file_get_contents(__DIR__ . '/fixtures/classes.php')); +Assert::type(Nette\PhpGenerator\InterfaceType::class, $class); +Assert::match(<<<'XX' + /** + * Interface + * @author John Doe + */ + interface Interface1 + { + public function func1(); + } + XX, (string) $class); diff --git a/tests/PhpGenerator/Factory.phpt b/tests/PhpGenerator/Factory.phpt index 49643bbf..0dff2244 100644 --- a/tests/PhpGenerator/Factory.phpt +++ b/tests/PhpGenerator/Factory.phpt @@ -9,7 +9,6 @@ declare(strict_types=1); use Nette\PhpGenerator\Factory; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -26,17 +25,17 @@ Assert::type(Nette\PhpGenerator\ClassType::class, $res); Assert::null($res->getName()); -$res = $factory->fromMethodReflection(new \ReflectionMethod(ReflectionClass::class, 'getName')); +$res = $factory->fromMethodReflection(new ReflectionMethod(ReflectionClass::class, 'getName')); Assert::type(Nette\PhpGenerator\Method::class, $res); Assert::same('getName', $res->getName()); -$res = $factory->fromFunctionReflection(new \ReflectionFunction('trim')); +$res = $factory->fromFunctionReflection(new ReflectionFunction('trim')); Assert::type(Nette\PhpGenerator\GlobalFunction::class, $res); Assert::same('trim', $res->getName()); -$res = $factory->fromFunctionReflection(new \ReflectionFunction(function () {})); +$res = $factory->fromFunctionReflection(new ReflectionFunction(function () {})); Assert::type(Nette\PhpGenerator\Closure::class, $res); diff --git a/tests/PhpGenerator/GlobalFunction.from.81.phpt b/tests/PhpGenerator/GlobalFunction.from.81.phpt new file mode 100644 index 00000000..d7b44419 --- /dev/null +++ b/tests/PhpGenerator/GlobalFunction.from.81.phpt @@ -0,0 +1,32 @@ +setBody('return $a + $b;'); +$function->addAttribute('ExampleAttribute'); +$function->addComment('My Function'); -$function = GlobalFunction::from('func'); same( -'/** - * global - */ -function func(stdClass $a, $b = null) -{ -} -', (string) $function); - - -$function = GlobalFunction::withBodyFrom('func'); -same(<<<'XX' -/** - * global - */ -function func(stdClass $a, $b = null) -{ - echo \sprintf('hello, %s', 'world'); - return 1; -} - -XX -, (string) $function); + <<<'XX' + /** + * My Function + */ + #[ExampleAttribute] + function test() + { + return $a + $b; + } + + XX, + (string) $function, +); diff --git a/tests/PhpGenerator/Helpers.comments.phpt b/tests/PhpGenerator/Helpers.comments.phpt index 94335ae1..2889cd4e 100644 --- a/tests/PhpGenerator/Helpers.comments.phpt +++ b/tests/PhpGenerator/Helpers.comments.phpt @@ -9,14 +9,16 @@ declare(strict_types=1); use Nette\PhpGenerator\Helpers; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; Assert::same('', Helpers::formatDocComment(' ')); Assert::same("/** @var string */\n", Helpers::formatDocComment('@var string')); Assert::same("/**\n * @var string\n */\n", Helpers::formatDocComment("@var string\n")); +Assert::same("/**\n * @var string\n */\n", Helpers::formatDocComment('@var string', forceMultiLine: true)); Assert::same("/**\n * A\n * B\n * C\n */\n", Helpers::formatDocComment("A\nB\nC\n")); +Assert::same("/**\n * @var string\n */\n", Helpers::formatDocComment("@var string \r\n")); +Assert::same("/**\n * A\n *\n * B\n */\n", Helpers::formatDocComment("A\n\nB")); Assert::same('', Helpers::unformatDocComment('')); Assert::same('', Helpers::unformatDocComment("/** */\n\r\t")); diff --git a/tests/PhpGenerator/Helpers.isIdentifier.phpt b/tests/PhpGenerator/Helpers.isIdentifier.phpt index eb9ceda6..70b394dd 100644 --- a/tests/PhpGenerator/Helpers.isIdentifier.phpt +++ b/tests/PhpGenerator/Helpers.isIdentifier.phpt @@ -5,7 +5,6 @@ declare(strict_types=1); use Nette\PhpGenerator\Helpers; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/PhpGenerator/Helpers.isNamespaceIdentifier.phpt b/tests/PhpGenerator/Helpers.isNamespaceIdentifier.phpt index b827ea1c..17704c22 100644 --- a/tests/PhpGenerator/Helpers.isNamespaceIdentifier.phpt +++ b/tests/PhpGenerator/Helpers.isNamespaceIdentifier.phpt @@ -5,7 +5,6 @@ declare(strict_types=1); use Nette\PhpGenerator\Helpers; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -14,9 +13,9 @@ Assert::true(Helpers::isNamespaceIdentifier("\x7F")); Assert::true(Helpers::isNamespaceIdentifier("\x7F\\\x7F")); Assert::false(Helpers::isNamespaceIdentifier('0Item')); Assert::true(Helpers::isNamespaceIdentifier('Item\Item')); -Assert::false(Helpers::isNamespaceIdentifier('Item\\\\Item')); -Assert::false(Helpers::isNamespaceIdentifier('\\Item')); +Assert::false(Helpers::isNamespaceIdentifier('Item\\\Item')); +Assert::false(Helpers::isNamespaceIdentifier('\Item')); Assert::false(Helpers::isNamespaceIdentifier('Item\\')); -Assert::true(Helpers::isNamespaceIdentifier('\\Item', true)); -Assert::false(Helpers::isNamespaceIdentifier('Item\\', true)); +Assert::true(Helpers::isNamespaceIdentifier('\Item', allowLeadingSlash: true)); +Assert::false(Helpers::isNamespaceIdentifier('Item\\', allowLeadingSlash: true)); diff --git a/tests/PhpGenerator/Helpers.tabsToSpaces().phpt b/tests/PhpGenerator/Helpers.tabsToSpaces().phpt index 7b373f6a..be79c31d 100644 --- a/tests/PhpGenerator/Helpers.tabsToSpaces().phpt +++ b/tests/PhpGenerator/Helpers.tabsToSpaces().phpt @@ -5,7 +5,6 @@ declare(strict_types=1); use Nette\PhpGenerator\Helpers; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/PhpGenerator/Helpers.unindent.phpt b/tests/PhpGenerator/Helpers.unindent.phpt index dba75a68..02161321 100644 --- a/tests/PhpGenerator/Helpers.unindent.phpt +++ b/tests/PhpGenerator/Helpers.unindent.phpt @@ -9,7 +9,6 @@ declare(strict_types=1); use Nette\PhpGenerator\Helpers; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/PhpGenerator/Helpers.validateType.phpt b/tests/PhpGenerator/Helpers.validateType.phpt new file mode 100644 index 00000000..850c637f --- /dev/null +++ b/tests/PhpGenerator/Helpers.validateType.phpt @@ -0,0 +1,42 @@ + Helpers::validateType('-', $foo), + Nette\InvalidArgumentException::class, +); + +Assert::exception( + fn() => Helpers::validateType('?Foo|Bar', $foo), + Nette\InvalidArgumentException::class, +); + +Assert::exception( + fn() => Helpers::validateType('(Foo)', $foo), + Nette\InvalidArgumentException::class, +); + +Assert::exception( + fn() => Helpers::validateType('(Foo&Bar)', $foo), + Nette\InvalidArgumentException::class, +); diff --git a/tests/PhpGenerator/InterfaceType.fromCode.phpt b/tests/PhpGenerator/InterfaceType.fromCode.phpt new file mode 100644 index 00000000..a0b672f2 --- /dev/null +++ b/tests/PhpGenerator/InterfaceType.fromCode.phpt @@ -0,0 +1,28 @@ + InterfaceType::fromCode('setInterface() ->addExtend('IOne') ->addExtend('ITwo') ->addComment('Description of interface'); @@ -24,4 +18,4 @@ Assert::same(['IOne', 'ITwo'], $interface->getExtends()); $interface->addMethod('getForm'); -sameFile(__DIR__ . '/expected/ClassType.interface.expect', (string) $interface); +sameFile(__DIR__ . '/expected/InterfaceType.expect', (string) $interface); diff --git a/tests/PhpGenerator/InterfaceType.validate.phpt b/tests/PhpGenerator/InterfaceType.validate.phpt new file mode 100644 index 00000000..b036e425 --- /dev/null +++ b/tests/PhpGenerator/InterfaceType.validate.phpt @@ -0,0 +1,21 @@ +addProperty('first', 123); + $interface->validate(); +}, Nette\InvalidStateException::class, 'Property Demo::$first: Interface cannot have initialized properties.'); + +Assert::exception(function () { + $interface = new InterfaceType('Demo'); + $interface->addProperty('first'); + $interface->validate(); +}, Nette\InvalidStateException::class, 'Property Demo::$first: Interface cannot have properties without hooks.'); diff --git a/tests/PhpGenerator/Method.from.81.phpt b/tests/PhpGenerator/Method.from.81.phpt new file mode 100644 index 00000000..dd5edb53 --- /dev/null +++ b/tests/PhpGenerator/Method.from.81.phpt @@ -0,0 +1,26 @@ +getReturnType()); - $method = Method::from(A::class . '::testScalar'); + $method = Method::from([A::class, 'testScalar']); Assert::same('string', $method->getReturnType()); // generating methods with return type declarations @@ -43,10 +42,14 @@ namespace ->setBody('return new Foo();'); same( - 'function create(): Foo -{ - return new Foo(); -} -', (string) $method); + <<<'XX' + function create(): Foo + { + return new Foo(); + } + + XX, + (string) $method, + ); } diff --git a/tests/PhpGenerator/Method.scalarParameters.phpt b/tests/PhpGenerator/Method.scalarParameters.phpt index 6bea25a7..fc885857 100644 --- a/tests/PhpGenerator/Method.scalarParameters.phpt +++ b/tests/PhpGenerator/Method.scalarParameters.phpt @@ -11,7 +11,6 @@ use Nette\PhpGenerator\Method; use Nette\PhpGenerator\Type; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; // test from @@ -22,16 +21,16 @@ interface Foo function scalars(string $a, bool $b, int $c, float $d); } -$method = Method::from(Foo::class . '::scalars'); +$method = Method::from([Foo::class, 'scalars']); Assert::same('string', $method->getParameters()['a']->getType()); -$method = Method::from(Foo::class . '::scalars'); +$method = Method::from([Foo::class, 'scalars']); Assert::same('bool', $method->getParameters()['b']->getType()); -$method = Method::from(Foo::class . '::scalars'); +$method = Method::from([Foo::class, 'scalars']); Assert::same('int', $method->getParameters()['c']->getType()); -$method = Method::from(Foo::class . '::scalars'); +$method = Method::from([Foo::class, 'scalars']); Assert::same('float', $method->getParameters()['d']->getType()); @@ -39,12 +38,16 @@ Assert::same('float', $method->getParameters()['d']->getType()); $method = (new Method('create')) ->setBody('return null;'); -$method->addParameter('a')->setType(Type::STRING); -$method->addParameter('b')->setType(Type::BOOL); +$method->addParameter('a')->setType(Type::String); +$method->addParameter('b')->setType(Type::Bool); same( -'function create(string $a, bool $b) -{ - return null; -} -', (string) $method); + <<<'XX' + function create(string $a, bool $b) + { + return null; + } + + XX, + (string) $method, +); diff --git a/tests/PhpGenerator/Method.validate.phpt b/tests/PhpGenerator/Method.validate.phpt index 7b4342f7..4af92226 100644 --- a/tests/PhpGenerator/Method.validate.phpt +++ b/tests/PhpGenerator/Method.validate.phpt @@ -5,24 +5,23 @@ declare(strict_types=1); use Nette\PhpGenerator\Method; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; Assert::exception(function () { $method = new Method('foo'); - $method->setFinal(true)->setAbstract(true); + $method->setFinal()->setAbstract(); $method->validate(); -}, Nette\InvalidStateException::class, 'Method cannot be abstract and final or private.'); +}, Nette\InvalidStateException::class, 'Method foo() cannot be abstract and final or private at the same time.'); Assert::exception(function () { $method = new Method('foo'); - $method->setAbstract(true)->setFinal(true); + $method->setAbstract()->setFinal(); $method->validate(); -}, Nette\InvalidStateException::class, 'Method cannot be abstract and final or private.'); +}, Nette\InvalidStateException::class, 'Method foo() cannot be abstract and final or private at the same time.'); Assert::exception(function () { $method = new Method('foo'); - $method->setAbstract(true)->setVisibility('private'); + $method->setAbstract()->setVisibility('private'); $method->validate(); -}, Nette\InvalidStateException::class, 'Method cannot be abstract and final or private.'); +}, Nette\InvalidStateException::class, 'Method foo() cannot be abstract and final or private at the same time.'); diff --git a/tests/PhpGenerator/Method.variadics.phpt b/tests/PhpGenerator/Method.variadics.phpt index 9e48b046..376257ad 100644 --- a/tests/PhpGenerator/Method.variadics.phpt +++ b/tests/PhpGenerator/Method.variadics.phpt @@ -9,7 +9,6 @@ declare(strict_types=1); use Nette\PhpGenerator\Method; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -22,10 +21,10 @@ interface Variadics function bar($foo, array &...$bar); } -$method = Method::from(Variadics::class . '::foo'); +$method = Method::from([Variadics::class, 'foo']); Assert::true($method->isVariadic()); -$method = Method::from(Variadics::class . '::bar'); +$method = Method::from([Variadics::class, 'bar']); Assert::true($method->isVariadic()); Assert::true($method->getParameters()['bar']->isReference()); Assert::same('array', $method->getParameters()['bar']->getType()); @@ -36,70 +35,90 @@ Assert::same('array', $method->getParameters()['bar']->getType()); // parameterless variadic method $method = (new Method('variadic')) - ->setVariadic(true) + ->setVariadic() ->setBody('return 42;'); same( -'function variadic() -{ - return 42; -} -', (string) $method); + <<<'XX' + function variadic() + { + return 42; + } + + XX, + (string) $method, +); // variadic method with one parameter $method = (new Method('variadic')) - ->setVariadic(true) + ->setVariadic() ->setBody('return 42;'); $method->addParameter('foo'); same( -'function variadic(...$foo) -{ - return 42; -} -', (string) $method); + <<<'XX' + function variadic(...$foo) + { + return 42; + } + + XX, + (string) $method, +); // variadic method with multiple parameters $method = (new Method('variadic')) - ->setVariadic(true) + ->setVariadic() ->setBody('return 42;'); $method->addParameter('foo'); $method->addParameter('bar'); $method->addParameter('baz', []); same( -'function variadic($foo, $bar, ...$baz) -{ - return 42; -} -', (string) $method); + <<<'XX' + function variadic($foo, $bar, ...$baz) + { + return 42; + } + + XX, + (string) $method, +); // method with typehinted variadic param $method = (new Method('variadic')) - ->setVariadic(true) + ->setVariadic() ->setBody('return 42;'); $method->addParameter('foo')->setType('array'); same( -'function variadic(array ...$foo) -{ - return 42; -} -', (string) $method); + <<<'XX' + function variadic(array ...$foo) + { + return 42; + } + + XX, + (string) $method, +); // method with typrhinted by-value variadic param $method = (new Method('variadic')) - ->setVariadic(true) + ->setVariadic() ->setBody('return 42;'); -$method->addParameter('foo')->setType('array')->setReference(true); +$method->addParameter('foo')->setType('array')->setReference(); same( -'function variadic(array &...$foo) -{ - return 42; -} -', (string) $method); + <<<'XX' + function variadic(array &...$foo) + { + return 42; + } + + XX, + (string) $method, +); diff --git a/tests/PhpGenerator/NameAware.cloneWithName.phpt b/tests/PhpGenerator/NameAware.cloneWithName.phpt index 0faa5ab5..2a40006b 100644 --- a/tests/PhpGenerator/NameAware.cloneWithName.phpt +++ b/tests/PhpGenerator/NameAware.cloneWithName.phpt @@ -5,7 +5,6 @@ declare(strict_types=1); use Nette\PhpGenerator\ClassType; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/PhpGenerator/PhpFile.addNamespace.phpt b/tests/PhpGenerator/PhpFile.addNamespace.phpt new file mode 100644 index 00000000..6854b7b3 --- /dev/null +++ b/tests/PhpGenerator/PhpFile.addNamespace.phpt @@ -0,0 +1,29 @@ +addClass('Bar'); + +$phpFile = new PhpFile; +$phpFile->addNamespace('Foo'); +$phpFile->addNamespace($namespace); // overwrite + + +same( + <<<'XX' + addComment('This file is auto-generated. DO NOT EDIT!'); $file->addComment('Hey there, I\'m here to document things.'); +$namespace = $file->addNamespace('Deleted'); +$namespace->addClass('Foo'); +$file->removeNamespace('Deleted'); + $namespaceFoo = $file->addNamespace('Foo'); $classA = $namespaceFoo->addClass('A'); @@ -31,10 +34,11 @@ $traitC = $namespaceFoo->addTrait('C'); Assert::same($namespaceFoo, $traitC->getNamespace()); $classA - ->addImplement('Foo\\A') - ->addTrait('Foo\\C') - ->addImplement('Bar\\C') - ->addTrait('Bar\\D'); + ->addImplement('Foo\A') + ->addImplement('Bar\C'); + +$classA->addTrait('Foo\C'); +$classA->addTrait('Bar\D'); $namespaceBar = $file->addNamespace('Bar'); @@ -48,44 +52,89 @@ Assert::same($interfaceC->getNamespace(), $namespaceBar); $traitD = $namespaceBar->addTrait('D'); Assert::same($traitD->getNamespace(), $namespaceBar); +$enumEN = $namespaceBar->addEnum('EN'); +Assert::same($enumEN->getNamespace(), $namespaceBar); + $classB - ->addExtend('Foo\\A') - ->addImplement('Foo\\B') - ->addTrait('Foo\\C'); + ->setExtends('Foo\A') + ->addImplement('Foo\B') + ->addTrait('Foo\C'); -$classE = $file->addClass('Baz\\E'); +$classE = $file->addClass('Baz\E'); Assert::same($file->addNamespace('Baz'), $classE->getNamespace()); -$interfaceF = $file->addInterface('Baz\\F'); +$interfaceF = $file->addInterface('Baz\F'); Assert::same($file->addNamespace('Baz'), $interfaceF->getNamespace()); $interfaceF - ->addExtend('Foo\\B') - ->addExtend('Bar\\C'); + ->addExtend('Foo\B') + ->addExtend('Bar\C'); -$traitG = $file->addTrait('Baz\\G'); +$traitG = $file->addTrait('Baz\G'); Assert::same($file->addNamespace('Baz'), $traitG->getNamespace()); +$file->addFunction('Baz\f2') + ->setReturnType('Foo\B'); + sameFile(__DIR__ . '/expected/PhpFile.regular.expect', (string) $file); $file->addClass('H'); -$file->addClass('FooBar\\I'); +$file->addClass('FooBar\I'); + +$file->addFunction('f1') + ->setBody('return 1;'); sameFile(__DIR__ . '/expected/PhpFile.bracketed.expect', (string) $file); +Assert::same([ + 'Foo', + 'Bar', + 'Baz', + '', + 'FooBar', +], array_keys($file->getNamespaces())); + +Assert::same([ + 'Foo\A', + 'Foo\B', + 'Foo\C', + 'Bar\B', + 'Bar\C', + 'Bar\D', + 'Bar\EN', + 'Baz\E', + 'Baz\F', + 'Baz\G', + 'H', + 'FooBar\I', +], array_keys($file->getClasses())); + +Assert::same(['Baz\f2', 'f1'], array_keys($file->getFunctions())); + + + + $file = new PhpFile; -$file->addClass('A'); +$file->addClass('CA'); $file->addUse('A') ->addUse('B', 'C'); sameFile(__DIR__ . '/expected/PhpFile.globalNamespace.expect', (string) $file); + + $file = new PhpFile; $file->addComment('This file is auto-generated. DO NOT EDIT!'); $file->setStrictTypes(); $file->addClass('A'); sameFile(__DIR__ . '/expected/PhpFile.strictTypes.expect', (string) $file); + + + +$file = PhpFile::fromCode(file_get_contents(__DIR__ . '/fixtures/classes.php')); +Assert::type(PhpFile::class, $file); +sameFile(__DIR__ . '/expected/Extractor.classes.expect', (string) $file); diff --git a/tests/PhpGenerator/PhpNamespace.add.phpt b/tests/PhpGenerator/PhpNamespace.add.phpt index f825ac4c..73f93b29 100644 --- a/tests/PhpGenerator/PhpNamespace.add.phpt +++ b/tests/PhpGenerator/PhpNamespace.add.phpt @@ -5,13 +5,14 @@ declare(strict_types=1); use Nette\PhpGenerator\ClassType; use Nette\PhpGenerator\PhpNamespace; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; -Assert::exception(function () { - (new PhpNamespace('Foo'))->add(new ClassType); -}, Nette\InvalidArgumentException::class, 'Class does not have a name.'); +Assert::exception( + fn() => (new PhpNamespace('Foo'))->add(new ClassType), + Nette\InvalidArgumentException::class, + 'Class does not have a name.', +); $namespace = (new PhpNamespace('Foo')) @@ -19,17 +20,32 @@ $namespace = (new PhpNamespace('Foo')) ->add($classB = new ClassType('B', new PhpNamespace('X'))); -same('namespace Foo; +same( + <<<'XX' + namespace Foo; + + class A + { + } -class A -{ -} + class B + { + } -class B -{ -} -', (string) $namespace); + XX, + (string) $namespace, +); // namespaces are not changed Assert::null($classA->getNamespace()); Assert::same('X', $classB->getNamespace()->getName()); + + +// duplicity +Assert::noError(fn() => $namespace->add($classA)); + +Assert::exception( + fn() => $namespace->add(new ClassType('a')), + Nette\InvalidStateException::class, + "Cannot add 'a', because it already exists.", +); diff --git a/tests/PhpGenerator/PhpNamespace.aliases.phpt b/tests/PhpGenerator/PhpNamespace.aliases.phpt new file mode 100644 index 00000000..d7dd1358 --- /dev/null +++ b/tests/PhpGenerator/PhpNamespace.aliases.phpt @@ -0,0 +1,112 @@ +addUse('Bar\C'); + +Assert::exception( + fn() => $namespace->addTrait('C'), + Nette\InvalidStateException::class, + "Name 'C' used already as alias for Bar\\C.", +); + +Assert::exception( + fn() => $namespace->addTrait('c'), + Nette\InvalidStateException::class, + "Name 'c' used already as alias for Bar\\C.", +); + +$namespace->addClass('B'); +Assert::exception( + fn() => $namespace->addUse('Lorem\B', 'B'), + Nette\InvalidStateException::class, + "Name 'B' used already for 'Foo\\B'.", +); + +Assert::exception( + fn() => $namespace->addUse('lorem\b', 'b'), + Nette\InvalidStateException::class, + "Name 'b' used already for 'Foo\\B'.", +); + +$namespace->addUseFunction('Bar\f1'); +Assert::exception( + fn() => $namespace->addFunction('f1'), + Nette\InvalidStateException::class, + "Name 'f1' used already as alias for Bar\\f1.", +); + +Assert::exception( + fn() => $namespace->addFunction('F1'), + Nette\InvalidStateException::class, + "Name 'F1' used already as alias for Bar\\f1.", +); + +$namespace->addFunction('f2'); +Assert::exception( + fn() => $namespace->addUseFunction('Bar\f2', 'f2'), + Nette\InvalidStateException::class, + "Name 'f2' used already for 'Foo\\f2'.", +); + +Assert::exception( + fn() => $namespace->addUseFunction('Bar\f2', 'F2'), + Nette\InvalidStateException::class, + "Name 'F2' used already for 'Foo\\f2'.", +); + +Assert::same(['C' => 'Bar\C'], $namespace->getUses()); +Assert::same(['f1' => 'Bar\f1'], $namespace->getUses($namespace::NameFunction)); + + +// alias generation +$namespace = new PhpNamespace(''); +$namespace->addUse('C'); +Assert::same('C', $namespace->simplifyName('C')); +$namespace->addUse('Bar\C'); +Assert::same('C1', $namespace->simplifyName('Bar\C')); +$namespace->removeUse('bar\c'); +Assert::same('Bar\C', $namespace->simplifyName('Bar\C')); + +$namespace = new PhpNamespace(''); +$namespace->addUse('Bar\C'); +$namespace->addUse('C'); +Assert::same('C1', $namespace->simplifyName('C')); + +$namespace = new PhpNamespace(''); +$namespace->addUse('A'); +Assert::same('A', $namespace->simplifyName('A')); +$namespace->addUse('Bar\A'); +Assert::same('A1', $namespace->simplifyName('Bar\A')); + +$namespace = new PhpNamespace('Foo'); +$namespace->addUse('C'); +Assert::same('C', $namespace->simplifyName('C')); +$namespace->addUse('Bar\C'); +Assert::same('C1', $namespace->simplifyName('Bar\C')); +Assert::same('\Foo\C', $namespace->simplifyName('Foo\C')); +$namespace->addUse('Foo\C'); +Assert::same('C2', $namespace->simplifyName('Foo\C')); + +$namespace = new PhpNamespace('Foo'); +$namespace->addUse('Bar\C'); +$namespace->addUse('C'); +Assert::same('C1', $namespace->simplifyName('C')); + +$namespace = new PhpNamespace('Foo\Bar'); +$namespace->addUse('Foo\Bar\Baz\Qux'); +Assert::same('Qux', $namespace->simplifyName('Foo\Bar\Baz\Qux')); + +$namespace = new PhpNamespace('Foo'); +$namespace->addUseFunction('Bar\c'); +$namespace->addUseFunction('c'); +Assert::same('c1', $namespace->simplifyName('c', $namespace::NameFunction)); +$namespace->removeUse('c', $namespace::NameFunction); +Assert::same('\c', $namespace->simplifyName('c', $namespace::NameFunction)); diff --git a/tests/PhpGenerator/PhpNamespace.fqn.phpt b/tests/PhpGenerator/PhpNamespace.fqn.phpt index 5473e555..28a8c745 100644 --- a/tests/PhpGenerator/PhpNamespace.fqn.phpt +++ b/tests/PhpGenerator/PhpNamespace.fqn.phpt @@ -9,7 +9,6 @@ declare(strict_types=1); use Nette\PhpGenerator\ClassType; use Nette\PhpGenerator\PhpNamespace; use Nette\PhpGenerator\Printer; - require __DIR__ . '/../bootstrap.php'; @@ -18,9 +17,10 @@ $class = new ClassType('Example'); $class ->setExtends('\ParentClass') ->addImplement('One') - ->addImplement('\Two') - ->addTrait('Three') - ->addTrait('\Four'); + ->addImplement('\Two'); + +$class->addTrait('Three'); +$class->addTrait('\Four'); $class->addMethod('one') ->setReturnType('One'); @@ -45,9 +45,10 @@ $class = new ClassType('Example', new PhpNamespace('')); $class ->setExtends('\ParentClass') ->addImplement('One') - ->addImplement('\Two') - ->addTrait('Three') - ->addTrait('\Four'); + ->addImplement('\Two'); + +$class->addTrait('Three'); +$class->addTrait('\Four'); $class->addMethod('one') ->setReturnType('One'); diff --git a/tests/PhpGenerator/PhpNamespace.phpt b/tests/PhpGenerator/PhpNamespace.phpt index 9a4427d0..3b352de4 100644 --- a/tests/PhpGenerator/PhpNamespace.phpt +++ b/tests/PhpGenerator/PhpNamespace.phpt @@ -1,78 +1,49 @@ getName()); -Assert::same('A', $namespace->unresolveName('A')); -Assert::same('foo\A', $namespace->unresolveName('foo\A')); - -$namespace->addUse('Bar\C'); - -Assert::same('Bar', $namespace->unresolveName('Bar')); -Assert::same('C', $namespace->unresolveName('bar\C')); -Assert::same('C\D', $namespace->unresolveName('Bar\C\D')); - -foreach (['String', 'string', 'int', 'float', 'bool', 'array', 'callable', 'self', 'parent', ''] as $type) { - Assert::same($type, $namespace->unresolveName($type)); -} - $namespace = new PhpNamespace('Foo'); - Assert::same('Foo', $namespace->getName()); -Assert::same('\A', $namespace->unresolveName('\A')); -Assert::same('\A', $namespace->unresolveName('A')); -Assert::same('A', $namespace->unresolveName('foo\A')); - -$namespace->addUse('Bar\C'); -Assert::same(['C' => 'Bar\\C'], $namespace->getUses()); - -Assert::same('\Bar', $namespace->unresolveName('Bar')); -Assert::same('C', $namespace->unresolveName('\bar\C')); -Assert::same('C', $namespace->unresolveName('bar\C')); -Assert::same('C\D', $namespace->unresolveName('Bar\C\D')); - -foreach (['String', 'string', 'int', 'float', 'bool', 'array', 'callable', 'self', 'parent', ''] as $type) { - Assert::same($type, $namespace->unresolveName($type)); -} - $classA = $namespace->addClass('A'); Assert::same($namespace, $classA->getNamespace()); +Assert::exception( + fn() => $namespace->addClass('a'), + Nette\InvalidStateException::class, + "Cannot add 'a', because it already exists.", +); + $interfaceB = $namespace->addInterface('B'); Assert::same($namespace, $interfaceB->getNamespace()); +Assert::same($classA, $namespace->getClass('a')); + Assert::count(2, $namespace->getClasses()); -Assert::type(Nette\PhpGenerator\ClassType::class, $namespace->getClasses()['A']); +Assert::same($classA, $namespace->getClasses()['A']); +$namespace->removeClass('a'); +Assert::count(1, $namespace->getClasses()); + -Assert::exception(function () use ($namespace) { - $traitC = $namespace->addTrait('C'); - Assert::same($namespace, $traitC->getNamespace()); -}, Nette\InvalidStateException::class, "Alias 'C' used already for 'Bar\\C', cannot use for 'Foo\\C'."); +$function = $namespace->addFunction('foo'); -$classA - ->addImplement('Foo\\A') - ->addImplement('Bar\\C') - ->addTrait('Bar\\D'); +Assert::exception( + fn() => $namespace->addFunction('Foo'), + Nette\InvalidStateException::class, + "Cannot add 'Foo', because it already exists.", +); -$method = $classA->addMethod('test'); -$method->addParameter('a')->setType('Bar\C'); -$method->addParameter('b')->setType('self'); -$method->addParameter('c')->setType('parent'); -$method->addParameter('d')->setType('array'); -$method->addParameter('e')->setType('callable'); +Assert::same($function, $namespace->getFunction('foo')); -sameFile(__DIR__ . '/expected/PhpNamespace.expect', (string) $namespace); +Assert::count(1, $namespace->getFunctions()); +Assert::same($function, $namespace->getFunctions()['foo']); +$namespace->removeFunction('FOO'); +Assert::count(0, $namespace->getFunctions()); diff --git a/tests/PhpGenerator/PhpNamespace.print.phpt b/tests/PhpGenerator/PhpNamespace.print.phpt new file mode 100644 index 00000000..a0281710 --- /dev/null +++ b/tests/PhpGenerator/PhpNamespace.print.phpt @@ -0,0 +1,37 @@ +addUse('Foo'); +$namespace->addUse('Bar\C'); +$namespace->addUseFunction('Bar\c'); +$namespace->addUseConstant('Bar\FOO'); + +$classA = $namespace->addClass('A'); +$interfaceB = $namespace->addInterface('B'); + +$classA + ->addImplement('Foo\A') + ->addImplement('Bar\C') + ->addAttribute('Foo\A'); + +$classA->addTrait('Bar\D'); + +$method = $classA->addMethod('test'); +$method->addAttribute('Foo\A'); +$method->setReturnType('static|Foo\A'); + +$method->addParameter('a')->setType('Bar\C')->addAttribute('Bar\D'); +$method->addParameter('b')->setType('self'); +$method->addParameter('c')->setType('parent'); +$method->addParameter('d')->setType('array'); +$method->addParameter('e')->setType('?callable'); +$method->addParameter('f')->setType('Bar\C|string'); + +sameFile(__DIR__ . '/expected/PhpNamespace.expect', (string) $namespace); diff --git a/tests/PhpGenerator/PhpNamespace.resolve.phpt b/tests/PhpGenerator/PhpNamespace.resolve.phpt new file mode 100644 index 00000000..08c82dee --- /dev/null +++ b/tests/PhpGenerator/PhpNamespace.resolve.phpt @@ -0,0 +1,85 @@ +getName()); +Assert::same('', $namespace->resolveName('')); +Assert::same('', $namespace->resolveName('\\')); +Assert::same('A', $namespace->resolveName('A')); +Assert::same('A', $namespace->resolveName('\A')); +Assert::same('foo\A', $namespace->resolveName('foo\A')); + +$namespace->addUse('Bar\C'); + +Assert::same('Bar', $namespace->resolveName('Bar')); +Assert::same('Bar\C', $namespace->resolveName('c')); +Assert::same('Bar\C\D', $namespace->resolveName('C\D')); + +$namespace->addUseFunction('Foo\a'); + +Assert::same('bar\c', $namespace->resolveName('bar\c', $namespace::NameFunction)); +Assert::same('Foo\a', $namespace->resolveName('A', $namespace::NameFunction)); +Assert::same('foo\a\b', $namespace->resolveName('foo\a\b', $namespace::NameFunction)); + +$namespace->addUseFunction('Bar\c'); + +Assert::same('Bar', $namespace->resolveName('Bar', $namespace::NameFunction)); +Assert::same('Bar\c', $namespace->resolveName('C', $namespace::NameFunction)); +Assert::same('Bar\C\d', $namespace->resolveName('c\d', $namespace::NameFunction)); + +$namespace->addUseConstant('Bar\c'); + +Assert::same('Bar', $namespace->resolveName('Bar', $namespace::NameConstant)); +Assert::same('Bar\c', $namespace->resolveName('C', $namespace::NameConstant)); +Assert::same('Bar\C\d', $namespace->resolveName('c\d', $namespace::NameConstant)); + + + +// namespace +$namespace = new PhpNamespace('Foo'); + +foreach (['String', 'string', 'int', 'float', 'bool', 'array', 'callable', 'self', 'parent', ''] as $type) { + Assert::same($type, $namespace->resolveName($type)); +} + +Assert::same('Foo', $namespace->getName()); +Assert::same('', $namespace->resolveName('')); +Assert::same('', $namespace->resolveName('\\')); +Assert::same('A', $namespace->resolveName('\A')); +Assert::same('Foo\A', $namespace->resolveName('A')); + +$namespace->addUse('Foo'); +Assert::same('Foo\B', $namespace->resolveName('B')); + +$namespace->addUse('Bar\C'); +Assert::same('Foo\C', $namespace->resolveName('Foo\C')); + +Assert::same('Bar', $namespace->resolveName('\Bar')); +Assert::same('Bar\C', $namespace->resolveName('C')); +Assert::same('Bar\C', $namespace->resolveName('c')); +Assert::same('Bar\C\D', $namespace->resolveName('c\D')); + +$namespace->addUseFunction('Foo\a'); + +foreach (['String', 'string', 'int', 'float', 'bool', 'array', 'callable', 'self', 'parent', ''] as $type) { + Assert::same($type, $namespace->resolveName($type, $namespace::NameFunction)); +} + +Assert::same('bar\c', $namespace->resolveName('\bar\c', $namespace::NameFunction)); +Assert::same('Foo\a', $namespace->resolveName('A', $namespace::NameFunction)); +Assert::same('Foo\C\b', $namespace->resolveName('foo\C\b', $namespace::NameFunction)); +Assert::same('Foo\A\b', $namespace->resolveName('A\b', $namespace::NameFunction)); + +$namespace->addUseFunction('Bar\c'); + +Assert::same('Bar', $namespace->resolveName('\Bar', $namespace::NameFunction)); +Assert::same('Bar\c', $namespace->resolveName('C', $namespace::NameFunction)); +Assert::same('Bar\C\d', $namespace->resolveName('c\d', $namespace::NameFunction)); diff --git a/tests/PhpGenerator/PhpNamespace.simplify.phpt b/tests/PhpGenerator/PhpNamespace.simplify.phpt new file mode 100644 index 00000000..21a0494f --- /dev/null +++ b/tests/PhpGenerator/PhpNamespace.simplify.phpt @@ -0,0 +1,89 @@ +getName()); +Assert::same('A', $namespace->simplifyName('A')); +Assert::same('foo\A', $namespace->simplifyName('foo\A')); + +$namespace->addUse('Bar\C'); + +Assert::same('Bar', $namespace->simplifyName('Bar')); +Assert::same('C', $namespace->simplifyName('bar\C')); +Assert::same('C\D', $namespace->simplifyName('Bar\C\D')); + +$namespace->addUseFunction('Foo\a'); + +Assert::same('bar\c', $namespace->simplifyName('bar\c', $namespace::NameFunction)); +Assert::same('a', $namespace->simplifyName('foo\A', $namespace::NameFunction)); +Assert::same('foo\a\b', $namespace->simplifyName('foo\a\b', $namespace::NameFunction)); + +$namespace->addUseFunction('Bar\c'); + +Assert::same('Bar', $namespace->simplifyName('Bar', $namespace::NameFunction)); +Assert::same('c', $namespace->simplifyName('bar\c', $namespace::NameFunction)); +Assert::same('C\d', $namespace->simplifyName('Bar\C\d', $namespace::NameFunction)); + +$namespace->addUseConstant('Bar\c'); + +Assert::same('Bar', $namespace->simplifyName('Bar', $namespace::NameConstant)); +Assert::same('c', $namespace->simplifyName('bar\c', $namespace::NameConstant)); +Assert::same('C\d', $namespace->simplifyName('Bar\C\d', $namespace::NameConstant)); + + + +// namespace +$namespace = new PhpNamespace('Foo'); + +foreach (['String', 'string', 'int', 'float', 'bool', 'array', 'callable', 'self', 'parent', ''] as $type) { + Assert::same($type, $namespace->simplifyName($type)); +} + +Assert::same('Foo', $namespace->getName()); +Assert::same('\A', $namespace->simplifyName('\A')); +Assert::same('\A', $namespace->simplifyName('A')); +Assert::same('A', $namespace->simplifyName('foo\A')); + +Assert::same('A', $namespace->simplifyType('foo\A')); +Assert::same('null|A', $namespace->simplifyType('null|foo\A')); +Assert::same('?A', $namespace->simplifyType('?foo\A')); +Assert::same('A&\Countable', $namespace->simplifyType('foo\A&Countable')); +Assert::same('', $namespace->simplifyType('')); + +$namespace->addUse('Foo'); +Assert::same('B', $namespace->simplifyName('Foo\B')); + +$namespace->addUse('Bar\C'); +Assert::same('Foo\C', $namespace->simplifyName('Foo\C')); + +Assert::same('\Bar', $namespace->simplifyName('Bar')); +Assert::same('C', $namespace->simplifyName('\bar\C')); +Assert::same('C', $namespace->simplifyName('bar\C')); +Assert::same('C\D', $namespace->simplifyName('Bar\C\D')); +Assert::same('A', $namespace->simplifyType('foo\A<\bar\C, Bar\C\D>')); +Assert::same('žluťoučký', $namespace->simplifyType('foo\žluťoučký')); + +$namespace->addUseFunction('Foo\a'); + +foreach (['String', 'string', 'int', 'float', 'bool', 'array', 'callable', 'self', 'parent', ''] as $type) { + Assert::same($type, $namespace->simplifyName($type, $namespace::NameFunction)); +} + +Assert::same('\bar\c', $namespace->simplifyName('bar\c', $namespace::NameFunction)); +Assert::same('a', $namespace->simplifyName('foo\A', $namespace::NameFunction)); +Assert::same('Foo\C\b', $namespace->simplifyName('foo\C\b', $namespace::NameFunction)); +Assert::same('a\b', $namespace->simplifyName('foo\a\b', $namespace::NameFunction)); + +$namespace->addUseFunction('Bar\c'); + +Assert::same('\Bar', $namespace->simplifyName('Bar', $namespace::NameFunction)); +Assert::same('c', $namespace->simplifyName('bar\c', $namespace::NameFunction)); +Assert::same('C\d', $namespace->simplifyName('Bar\C\d', $namespace::NameFunction)); diff --git a/tests/PhpGenerator/Printer.arrow.phpt b/tests/PhpGenerator/Printer.arrow.phpt index 06019d79..16328993 100644 --- a/tests/PhpGenerator/Printer.arrow.phpt +++ b/tests/PhpGenerator/Printer.arrow.phpt @@ -6,7 +6,6 @@ use Nette\PhpGenerator\Closure; use Nette\PhpGenerator\Printer; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -18,7 +17,7 @@ $function $function->addParameter('a'); $function->addParameter('b'); -Assert::same('fn &($a, $b) => $a + $b;', (new Printer)->printArrowFunction($function)); +Assert::same('fn&($a, $b) => $a + $b;', (new Printer)->printArrowFunction($function)); @@ -27,7 +26,7 @@ $function ->setReturnType('array') ->setBody('[]'); -Assert::same('fn (): array => [];', (new Printer)->printArrowFunction($function)); +Assert::same('fn(): array => [];', (new Printer)->printArrowFunction($function)); Assert::exception(function () { diff --git a/tests/PhpGenerator/Printer.function.phpt b/tests/PhpGenerator/Printer.function.phpt new file mode 100644 index 00000000..ac1d5ff3 --- /dev/null +++ b/tests/PhpGenerator/Printer.function.phpt @@ -0,0 +1,182 @@ +setReturnType('stdClass') + ->setBody("func(); \r\nreturn 123;") + ->addParameter('var') + ->setType('stdClass'); + +Assert::match(<<<'XX' + function func(stdClass $var): stdClass + { + func(); + return 123; + } + + XX, $printer->printFunction($function)); + + +$function = new GlobalFunction('multi'); +$function->addParameter('foo') + ->addAttribute('Foo'); + +Assert::match(<<<'XX' + function multi( + #[Foo] + $foo, + ) { + } + XX, $printer->printFunction($function)); + + +$function = new GlobalFunction('multiType'); +$function + ->setReturnType('array') + ->addParameter('foo') + ->addAttribute('Foo'); + +Assert::match(<<<'XX' + function multiType( + #[Foo] + $foo, + ): array + { + } + XX, $printer->printFunction($function)); + + +$function = new GlobalFunction('func'); +$function->addAttribute('Foo', [1, 2, 3]); +$function->addAttribute('Bar'); + +same( + <<<'XX' + #[Foo(1, 2, 3)] + #[Bar] + function func() + { + } + + XX, + (string) $function, +); + + +// single +$function = new GlobalFunction('func'); +$function->addAttribute('Foo', [1, 2, 3]); + +Assert::match(<<<'XX' + #[Foo(1, 2, 3)] + function func() + { + } + XX, $printer->printFunction($function)); + + +// multiple +$function = new GlobalFunction('func'); +$function->addAttribute('Foo', [1, 2, 3]); +$function->addAttribute('Bar'); + +Assert::match(<<<'XX' + #[Foo(1, 2, 3)] + #[Bar] + function func() + { + } + XX, $printer->printFunction($function)); + + +// multiline +$function = new GlobalFunction('func'); +$function->addAttribute('Foo', ['a', str_repeat('x', 120)]); + +Assert::match(<<<'XX' + #[Foo( + 'a', + 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + )] + function func() + { + } + XX, $printer->printFunction($function)); + + +// parameter: single +$function = new GlobalFunction('func'); +$param = $function->addParameter('foo'); +$param->addAttribute('Foo', [1, 2, 3]); + +Assert::match(<<<'XX' + function func( + #[Foo(1, 2, 3)] + $foo, + ) { + } + XX, $printer->printFunction($function)); + + +// parameter: multiple +$function = new GlobalFunction('func'); +$param = $function->addParameter('foo'); +$param->addAttribute('Foo', [1, 2, 3]); +$param->addAttribute('Bar'); + +Assert::match(<<<'XX' + function func( + #[Foo(1, 2, 3), Bar] + $foo, + ) { + } + XX, $printer->printFunction($function)); + + +// parameter: multiline +$function = new GlobalFunction('func'); +$param = $function->addParameter('bar'); +$param->addAttribute('Foo'); +$param = $function->addParameter('foo'); +$param->addAttribute('Foo', ['a', str_repeat('x', 120)]); + +Assert::match(<<<'XX' + function func( + #[Foo] + $bar, + #[Foo( + 'a', + 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + )] + $foo, + ) { + } + XX, $printer->printFunction($function)); + + +// parameter: multiple & multiline +$function = new GlobalFunction('func'); +$param = $function->addParameter('foo'); +$param->addAttribute('Bar'); +$param->addAttribute('Foo', ['a', str_repeat('x', 120)]); + +Assert::match(<<<'XX' + function func( + #[Bar] + #[Foo( + 'a', + 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + )] + $foo, + ) { + } + XX, $printer->printFunction($function)); diff --git a/tests/PhpGenerator/Printer.namespace.phpt b/tests/PhpGenerator/Printer.namespace.phpt index eb6401d3..4a69c2a4 100644 --- a/tests/PhpGenerator/Printer.namespace.phpt +++ b/tests/PhpGenerator/Printer.namespace.phpt @@ -4,7 +4,6 @@ declare(strict_types=1); use Nette\PhpGenerator\PhpNamespace; use Nette\PhpGenerator\Printer; - require __DIR__ . '/../bootstrap.php'; @@ -21,9 +20,10 @@ $class = $namespace->addClass('A') ->setExtends('ParentClass') ->addImplement('IExample') ->addImplement('Foo\IOne') - ->setTraits(['Foo\ObjectTrait']) ->addComment("Description of class.\nThis is example\n"); +$class->addTrait('Foo\ObjectTrait'); + $class->addMethod('first') ->addComment('@return resource') ->setFinal(true) diff --git a/tests/PhpGenerator/Printer.phpt b/tests/PhpGenerator/Printer.phpt index 0e77fd24..52b034c1 100644 --- a/tests/PhpGenerator/Printer.phpt +++ b/tests/PhpGenerator/Printer.phpt @@ -3,11 +3,10 @@ declare(strict_types=1); use Nette\PhpGenerator\ClassType; -use Nette\PhpGenerator\PhpLiteral; +use Nette\PhpGenerator\Literal; use Nette\PhpGenerator\Printer; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -15,67 +14,67 @@ $printer = new Printer; $class = (new ClassType('Example')) - ->setFinal(true) + ->setFinal() ->setExtends('ParentClass') ->addImplement('IExample') - ->setTraits(['ObjectTrait']) - ->addTrait('AnotherTrait', ['sayHello as protected']) ->addComment("Description of class.\nThis is example\n"); -$class->addConstant('FORCE_ARRAY', new PhpLiteral('Nette\Utils\Json::FORCE_ARRAY')) +$class->addTrait('ObjectTrait'); +$class->addTrait('AnotherTrait') + ->addResolution('sayHello as protected'); + +$class->addConstant('FORCE_ARRAY', new Literal('Nette\Utils\Json::FORCE_ARRAY')) ->setPrivate() ->addComment('Commented'); $class->addConstant('MULTILINE_LONG', ['aaaaaaaa' => 1, 'bbbbbbbb' => 2, 'cccccccc' => 3, 'dddddddd' => 4, 'eeeeeeee' => 5, 'ffffffff' => 6]); -$class->addConstant('SHORT', ['aaaaaaaa' => 1, 'bbbbbbbb' => 2, 'cccccccc' => 3, 'dddddddd' => 4, 'eeeeeeee' => 5, 'ffffffff' => 6]); +$class->addConstant('SHORT', ['aaaaaaaa' => 1, 'bbbbbbbb' => 2, 'cccccccc' => 3, 'dddddddd' => 4, 'eeeeeeee' => 5]); $class->addProperty('handle') ->setVisibility('private') ->addComment('@var resource orignal file handle'); $class->addProperty('order') - ->setValue(new PhpLiteral('RecursiveIteratorIterator::SELF_FIRST')); + ->setValue(new Literal('RecursiveIteratorIterator::SELF_FIRST')); $class->addProperty('multilineLong', ['aaaaaaaa' => 1, 'bbbbbbbb' => 2, 'cccccccc' => 3, 'dddddddd' => 4, 'eeeeeeee' => 5, 'ffffffff' => 6]); $class->addProperty('short', ['aaaaaaaa' => 1, 'bbbbbbbb' => 2, 'cccccccc' => 3, 'dddddddd' => 4, 'eeeeeeee' => 5, 'ffffffff' => 6]); $class->addMethod('first') ->addComment('@return resource') - ->setFinal(true) + ->setFinal() ->setReturnType('stdClass') - ->setBody("func();\nreturn ?;", [['aaaaaaaaaaaa' => 1, 'bbbbbbbbbbb' => 2, 'cccccccccccccc' => 3, 'dddddddddddd' => 4, 'eeeeeeeeeeee' => 5, 'ffffffff' => 6]]) + ->setBody("func(); \r\nreturn ?;", [['aaaaaaaaaaaa' => 1, 'bbbbbbbbbbb' => 2, 'cccccccccccccc' => 3, 'dddddddddddd' => 4, 'eeeeeeeeeeee' => 5, 'ffffffff' => 6]]) ->addParameter('var') ->setType('stdClass'); $class->addMethod('second'); +$method = $class->addMethod('multi') + ->addParameter('foo') + ->addAttribute('Foo'); + +$method = $class->addMethod('multiType') + ->setReturnType('array') + ->addParameter('foo') + ->addAttribute('Foo'); + sameFile(__DIR__ . '/expected/Printer.class.expect', $printer->printClass($class)); sameFile(__DIR__ . '/expected/Printer.method.expect', $printer->printMethod($class->getMethod('first'))); -Assert::with($printer, function () { - $this->linesBetweenProperties = 1; - $this->linesBetweenMethods = 3; -}); +$printer->linesBetweenProperties = 1; +$printer->linesBetweenMethods = 3; +$printer->bracesOnNextLine = false; sameFile(__DIR__ . '/expected/Printer.class-alt.expect', $printer->printClass($class)); -$function = new Nette\PhpGenerator\GlobalFunction('func'); -$function - ->setReturnType('stdClass') - ->setBody("func();\nreturn 123;") - ->addParameter('var') - ->setType('stdClass'); - -sameFile(__DIR__ . '/expected/Printer.function.expect', $printer->printFunction($function)); - - $closure = new Nette\PhpGenerator\Closure; $closure ->setReturnType('stdClass') - ->setBody("func();\nreturn 123;") + ->setBody("func(); \r\nreturn 123;") ->addParameter('var') ->setType('stdClass'); @@ -85,6 +84,6 @@ sameFile(__DIR__ . '/expected/Printer.closure.expect', $printer->printClosure($c // printer validates class Assert::exception(function () { $class = new ClassType; - $class->setFinal(true)->setAbstract(true); + $class->setFinal()->setAbstract(); (new Printer)->printClass($class); -}, Nette\InvalidStateException::class, 'Class cannot be abstract and final.'); +}, Nette\InvalidStateException::class, 'Anonymous class cannot be abstract or final.'); diff --git a/tests/PhpGenerator/Printer.single.parameter.phpt b/tests/PhpGenerator/Printer.single.parameter.phpt new file mode 100644 index 00000000..b806d798 --- /dev/null +++ b/tests/PhpGenerator/Printer.single.parameter.phpt @@ -0,0 +1,87 @@ +singleParameterOnOneLine = true; + + +$function = new Nette\PhpGenerator\GlobalFunction('singleFunction'); +$function + ->setReturnType('array') + ->addParameter('foo') + ->addAttribute('Foo'); + +Assert::match(<<<'XX' + function singleFunction(#[Foo] $foo): array + { + } + + XX, $printer->printFunction($function)); + + +$method = new Nette\PhpGenerator\Method('singleMethod'); +$method + ->setPublic() + ->setReturnType('array') + ->addParameter('foo') + ->addAttribute('Foo'); + +Assert::match(<<<'XX' + public function singleMethod(#[Foo] $foo): array + { + } + + XX, $printer->printMethod($method)); + + +$method = new Nette\PhpGenerator\Method('singleMethod'); +$method + ->setPublic() + ->setReturnType('array') + ->addPromotedParameter('foo') + ->setPublic(); + +Assert::match(<<<'XX' + public function singleMethod(public $foo): array + { + } + + XX, $printer->printMethod($method)); + + +$method = new Nette\PhpGenerator\Method('singleMethod'); +$method + ->setPublic() + ->setReturnType('array') + ->addPromotedParameter('looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong') + ->setPublic(); + +Assert::match(<<<'XX' + public function singleMethod( + public $looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, + ): array + { + } + + XX, $printer->printMethod($method)); + + +$method = new Nette\PhpGenerator\Method('singleMethod'); +$method->addParameter('foo') + ->addAttribute('Foo', [new Literal("'\n'")]); + +Assert::match(<<<'XX' + function singleMethod( + #[Foo(' + ')] + $foo, + ) { + } + XX, $printer->printMethod($method)); diff --git a/tests/PhpGenerator/Printer.use-order.phpt b/tests/PhpGenerator/Printer.use-order.phpt new file mode 100644 index 00000000..24299d65 --- /dev/null +++ b/tests/PhpGenerator/Printer.use-order.phpt @@ -0,0 +1,27 @@ +addUse('Example\Foo\EmailAlias\Bar'); +$namespace->addUse('Example\Foo\Email\Test'); +$namespace->addUse('Example\Foo\MyClass'); + +Assert::match( + <<<'XX' + namespace Foo; + + use Example\Foo\Email\Test; + use Example\Foo\EmailAlias\Bar; + use Example\Foo\MyClass; + + XX, + $printer->printNamespace($namespace), +); diff --git a/tests/PhpGenerator/Property.abstract-final.phpt b/tests/PhpGenerator/Property.abstract-final.phpt new file mode 100644 index 00000000..11738a80 --- /dev/null +++ b/tests/PhpGenerator/Property.abstract-final.phpt @@ -0,0 +1,48 @@ +setAbstract(); + +$class->addProperty('first') + ->setType('string') + ->setAbstract() + ->addHook('set') + ->setAbstract(); + +$prop = $class->addProperty('second') + ->setType('string') + ->setAbstract(); + +$prop->addHook('set') + ->setAbstract(); + +$prop->addHook('get', '123'); + +$class->addProperty('third') + ->setFinal(); + +same(<<<'XX' + abstract class Demo + { + abstract public string $first { set; } + + abstract public string $second { + set; + get => 123; + } + + final public $third; + } + + XX, (string) $class); diff --git a/tests/PhpGenerator/Property.validate.phpt b/tests/PhpGenerator/Property.validate.phpt new file mode 100644 index 00000000..e12a4125 --- /dev/null +++ b/tests/PhpGenerator/Property.validate.phpt @@ -0,0 +1,21 @@ +setFinal()->setAbstract(); + $property->validate(); +}, Nette\InvalidStateException::class, 'Property $a cannot be abstract and final at the same time.'); + +Assert::exception(function () { + $property = new Property('a'); + $property->setAbstract(); + $property->validate(); +}, Nette\InvalidStateException::class, 'Property $a: Abstract property must have at least one abstract hook.'); diff --git a/tests/PhpGenerator/PropertyLike.asymmetric-visiblity.phpt b/tests/PhpGenerator/PropertyLike.asymmetric-visiblity.phpt new file mode 100644 index 00000000..bc32b071 --- /dev/null +++ b/tests/PhpGenerator/PropertyLike.asymmetric-visiblity.phpt @@ -0,0 +1,80 @@ +addProperty('first') + ->setType('string'); +Assert::true($default->isPublic(PropertyAccessMode::Get)); +Assert::true($default->isPublic(PropertyAccessMode::Set)); +Assert::null($default->getVisibility()); +Assert::null($default->getVisibility('set')); + +// Public with private setter +$restricted = $class->addProperty('second') + ->setType('string') + ->setVisibility(null, 'private'); +Assert::true($restricted->isPublic()); +Assert::false($restricted->isPublic('set')); +Assert::true($restricted->isPrivate('set')); +Assert::null($restricted->getVisibility()); +Assert::same('private', $restricted->getVisibility('set')); + +// Public with protected setter using individual methods +$mixed = $class->addProperty('third') + ->setType('string') + ->setPublic() + ->setProtected('set'); +Assert::true($mixed->isPublic()); +Assert::false($mixed->isPublic('set')); +Assert::true($mixed->isProtected('set')); +Assert::same('public', $mixed->getVisibility()); +Assert::same('protected', $mixed->getVisibility('set')); + +// Protected with private setter +$nested = $class->addProperty('fourth') + ->setType('string') + ->setProtected() + ->setPrivate('set'); +Assert::false($nested->isPublic()); +Assert::true($nested->isProtected()); +Assert::true($nested->isPrivate('set')); +Assert::same('protected', $nested->getVisibility()); +Assert::same('private', $nested->getVisibility('set')); + +// Test invalid getter visibility +Assert::exception( + fn() => $default->setVisibility('invalid', 'public'), + ValueError::class, +); + +// Test invalid setter visibility +Assert::exception( + fn() => $default->setVisibility('public', 'invalid'), + ValueError::class, +); + + +same(<<<'XX' + class Demo + { + public string $first; + private(set) string $second; + public protected(set) string $third; + protected private(set) string $fourth; + } + + XX, (string) $class); diff --git a/tests/PhpGenerator/PropertyLike.hooks.phpt b/tests/PhpGenerator/PropertyLike.hooks.phpt new file mode 100644 index 00000000..0b74a362 --- /dev/null +++ b/tests/PhpGenerator/PropertyLike.hooks.phpt @@ -0,0 +1,133 @@ +addProperty('first') + ->setType('string') + ->setValue('x') + ->setPublic() + ->addHook(PropertyHookType::Set) + ->setBody('$value . ?', ['x'], short: true) + ->addComment('comment') + ->addAttribute('Example') + ->addParameter('value') + ->setType('string'); + +$prop = $class->addProperty('second') + ->setType('string') + ->setPublic(); + +$prop->addHook('get') + ->setBody('return $this->second;') + ->setReturnReference() + ->setFinal(); + +$prop->addHook('set', '$value') + ->addParameter('value') + ->setType('string'); + +same(<<<'XX' + class Demo + { + public string $first = 'x' { + /** comment */ + #[Example] + set(string $value) => $value . 'x'; + } + + public string $second { + set(string $value) => $value; + final &get { + return $this->second; + } + } + } + + XX, (string) $class); + + + +// promoted properties + +$class = new ClassType('Demo'); + +$method = $class->addMethod('__construct'); + +$method->addPromotedParameter('first') + ->setType('string') + ->addHook('get') + ->setBody('return $this->first . "x";') + ->setReturnReference(); + +$method->addPromotedParameter('second') + ->setType('string') + ->addHook('set', '$value') + ->setFinal() + ->addParameter('value') + ->setType('string'); + +$method->addPromotedParameter('third') + ->setPublic() + ->setProtected('set') + ->setFinal() + ->setType('string') + ->addComment('hello') + ->addAttribute('Example'); + +same(<<<'XX' + class Demo + { + public function __construct( + public string $first { + &get { + return $this->first . "x"; + } + }, + public string $second { + final set(string $value) => $value; + }, + /** hello */ + #[Example] + final public protected(set) string $third, + ) { + } + } + + XX, (string) $class); + + +$interface = new InterfaceType('Demo'); + +$interface->addProperty('first') + ->setType('int') + ->setPublic() + ->addHook('get'); + +$prop = $interface->addProperty('second') + ->setType('Value') + ->setPublic(); + +$prop->addHook('get'); +$prop->addHook('set'); + +same(<<<'XX' + interface Demo + { + public int $first { get; } + public Value $second { set; get; } + } + + XX, (string) $interface); diff --git a/tests/PhpGenerator/PropertyLike.visiblity.phpt b/tests/PhpGenerator/PropertyLike.visiblity.phpt new file mode 100644 index 00000000..3d867171 --- /dev/null +++ b/tests/PhpGenerator/PropertyLike.visiblity.phpt @@ -0,0 +1,77 @@ +addProperty('first') + ->setType('string'); +Assert::true($default->isPublic()); +Assert::false($default->isProtected()); +Assert::false($default->isPrivate()); +Assert::null($default->getVisibility()); + +// Explicit public +$public = $class->addProperty('second') + ->setType('string') + ->setPublic(); +Assert::true($public->isPublic()); +Assert::false($public->isProtected()); +Assert::false($public->isPrivate()); +Assert::same('public', $public->getVisibility()); + +// Protected +$protected = $class->addProperty('third') + ->setType('string') + ->setProtected(); +Assert::false($protected->isPublic()); +Assert::true($protected->isProtected()); +Assert::false($protected->isPrivate()); +Assert::same('protected', $protected->getVisibility()); + +// Private +$private = $class->addProperty('fourth') + ->setType('string') + ->setPrivate(); +Assert::false($private->isPublic()); +Assert::false($private->isProtected()); +Assert::true($private->isPrivate()); +Assert::same('private', $private->getVisibility()); + +// Change visibility +$changing = $class->addProperty('fifth') + ->setType('string') + ->setPublic(); +$changing->setVisibility('protected'); +Assert::false($changing->isPublic()); +Assert::true($changing->isProtected()); +Assert::false($changing->isPrivate()); + +// Test invalid visibility +Assert::exception( + fn() => $changing->setVisibility('invalid'), + ValueError::class, +); + +same(<<<'XX' + class Demo + { + public string $first; + public string $second; + protected string $third; + private string $fourth; + protected string $fifth; + } + + XX, (string) $class); diff --git a/tests/PhpGenerator/PsrPrinter.phpt b/tests/PhpGenerator/PsrPrinter.phpt index 8dd5b3c8..a11e16ef 100644 --- a/tests/PhpGenerator/PsrPrinter.phpt +++ b/tests/PhpGenerator/PsrPrinter.phpt @@ -3,10 +3,9 @@ declare(strict_types=1); use Nette\PhpGenerator\ClassType; -use Nette\PhpGenerator\PhpLiteral; +use Nette\PhpGenerator\Literal; use Nette\PhpGenerator\PsrPrinter; - require __DIR__ . '/../bootstrap.php'; @@ -14,39 +13,44 @@ $printer = new PsrPrinter; $class = (new ClassType('Example')) - ->setFinal(true) + ->setFinal() ->setExtends('ParentClass') ->addImplement('IExample') - ->setTraits(['ObjectTrait']) - ->addTrait('AnotherTrait', ['sayHello as protected']) ->addComment("Description of class.\nThis is example\n"); -$class->addConstant('FORCE_ARRAY', new PhpLiteral('Nette\Utils\Json::FORCE_ARRAY')) +$class->addTrait('ObjectTrait'); +$class->addTrait('AnotherTrait') + ->addResolution('sayHello as protected'); + +$class->addConstant('FORCE_ARRAY', new Literal('Nette\Utils\Json::FORCE_ARRAY')) ->setVisibility('private') ->addComment('Commented'); $class->addConstant('MULTILINE_LONG', ['aaaaaaaa' => 1, 'bbbbbbbb' => 2, 'cccccccc' => 3, 'dddddddd' => 4, 'eeeeeeee' => 5, 'ffffffff' => 6]); -$class->addConstant('SHORT', ['aaaaaaaa' => 1, 'bbbbbbbb' => 2, 'cccccccc' => 3, 'dddddddd' => 4, 'eeeeeeee' => 5, 'ffffffff' => 6]); +$class->addConstant('SHORT', ['aaaaaaaa' => 1, 'bbbbbbbb' => 2, 'cccccccc' => 3, 'dddddddd' => 4, 'eeeeeeee' => 5]); $class->addProperty('handle') ->setVisibility('private') ->addComment('@var resource orignal file handle'); $class->addProperty('order') - ->setValue(new PhpLiteral('RecursiveIteratorIterator::SELF_FIRST')); + ->setValue(new Literal('RecursiveIteratorIterator::SELF_FIRST')); $class->addProperty('multilineLong', ['aaaaaaaa' => 1, 'bbbbbbbb' => 2, 'cccccccc' => 3, 'dddddddd' => 4, 'eeeeeeee' => 5, 'ffffffff' => 6]); $class->addProperty('short', ['aaaaaaaa' => 1, 'bbbbbbbb' => 2, 'cccccccc' => 3, 'dddddddd' => 4, 'eeeeeeee' => 5, 'ffffffff' => 6]); $class->addMethod('first') ->addComment('@return resource') - ->setFinal(true) + ->setFinal() ->setReturnType('stdClass') ->setBody("func();\nreturn ?;", [['aaaaaaaaaaaa' => 1, 'bbbbbbbbbbb' => 2, 'cccccccccccccc' => 3, 'dddddddddddd' => 4, 'eeeeeeeeeeee' => 5, 'ffffffff' => 6]]) ->addParameter('var') ->setType('stdClass'); -$class->addMethod('second'); +$class->addMethod('braces1') + ->setReturnType('stdClass') + ->addParameter('var') + ->addAttribute('attr'); sameFile(__DIR__ . '/expected/PsrPrinter.class.expect', $printer->printClass($class)); diff --git a/tests/PhpGenerator/Type.phpt b/tests/PhpGenerator/Type.phpt index d487f18b..a2f54cee 100644 --- a/tests/PhpGenerator/Type.phpt +++ b/tests/PhpGenerator/Type.phpt @@ -4,22 +4,52 @@ declare(strict_types=1); use Nette\PhpGenerator\Type; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; +// Nullable +Assert::same('?int', Type::nullable(Type::Int)); +Assert::same('int', Type::nullable(Type::Int, nullable: false)); + +Assert::same('?int', Type::nullable('?int')); +Assert::same('int', Type::nullable('?int', nullable: false)); + +Assert::same('null', Type::nullable('null')); +Assert::same('NULL', Type::nullable('NULL')); +Assert::exception( + fn() => Type::nullable('null', nullable: false), + Nette\InvalidArgumentException::class, + 'Type null cannot be not nullable.', +); + +Assert::same('mixed', Type::nullable('mixed')); +Assert::exception( + fn() => Type::nullable('mixed', nullable: false), + Nette\InvalidArgumentException::class, + 'Type mixed cannot be not nullable.', +); + +Assert::same('int|float|string|null', Type::nullable('int|float|string')); +Assert::same('int|float|string', Type::nullable('int|float|string', nullable: false)); + +Assert::same('NULL|int|float|string', Type::nullable('NULL|int|float|string')); +Assert::same('int|float|string', Type::nullable('NULL|int|float|string', nullable: false)); + +Assert::same('int|float|string|null', Type::nullable('int|float|string|null')); +Assert::same('int|float|string', Type::nullable('int|float|string|null', nullable: false)); + +Assert::same('int|float|null|string', Type::nullable('int|float|null|string')); +Assert::same('int|float|string', Type::nullable('int|float|null|string', nullable: false)); -Assert::same('A|string', Type::union(A::class, Type::STRING)); +Assert::exception( + fn() => Type::nullable('Foo&Bar'), + Nette\InvalidArgumentException::class, + 'Intersection types cannot be nullable.', +); +Assert::same('Foo&Bar', Type::nullable('Foo&Bar', nullable: false)); -Assert::same('?A', Type::nullable(A::class)); -Assert::same('?A', Type::nullable(A::class, true)); -Assert::same('A', Type::nullable(A::class, false)); -Assert::same('?A', Type::nullable('?A', true)); -Assert::same('A', Type::nullable('?A', false)); +// Union +Assert::same('A|string', Type::union(A::class, Type::String)); -Assert::same(stdClass::class, Type::getType(new stdClass)); -Assert::same(Type::STRING, Type::getType('')); -Assert::same(Type::INT, Type::getType(1)); -Assert::same(Type::FLOAT, Type::getType(1.0)); -Assert::same(Type::ARRAY, Type::getType([])); -Assert::same(null, Type::getType(fopen(__FILE__, 'r'))); +// Intersection +Assert::same('A&string', Type::intersection(A::class, Type::String)); diff --git a/tests/PhpGenerator/expected/ClassType.attributes.expect b/tests/PhpGenerator/expected/ClassType.attributes.expect new file mode 100644 index 00000000..434918ec --- /dev/null +++ b/tests/PhpGenerator/expected/ClassType.attributes.expect @@ -0,0 +1,29 @@ +/** + * Description of class. + */ +#[ExampleAttribute] +#[WithArgument(Foo::BAR)] +#[Table(name: 'user', constraints: [new UniqueConstraint(name: 'ean', columns: ['ean'])])] +class Example +{ + /** Commented */ + #[ExampleAttribute] + #[WithArguments(true)] + public const FOO = 123; + + /** @var resource */ + #[ExampleAttribute] + public $handle; + + + /** + * Returns file handle. + */ + #[ExampleAttribute] + public function getHandle( + /** comment */ + #[ExampleAttribute, WithArguments(0)] + $mode, + ) { + } +} diff --git a/tests/PhpGenerator/expected/ClassType.enum.expect b/tests/PhpGenerator/expected/ClassType.enum.expect new file mode 100644 index 00000000..d9b74983 --- /dev/null +++ b/tests/PhpGenerator/expected/ClassType.enum.expect @@ -0,0 +1,31 @@ +/** + * Description of class. + * This is example + */ +#[ExampleAttribute] +enum Suit +{ + use ObjectTrait; + + public const ACTIVE = false; + + /** ♣ */ + #[ValueAttribute] + case Clubs; + + /** ♦ */ + case Diamonds; + case Hearts; + case Spades; + + public function foo() + { + return 10; + } +} + +enum Method: string implements IOne +{ + case GET = 'get'; + case POST = 'post'; +} diff --git a/tests/PhpGenerator/expected/ClassType.expect b/tests/PhpGenerator/expected/ClassType.expect index b246396c..642dfb17 100644 --- a/tests/PhpGenerator/expected/ClassType.expect +++ b/tests/PhpGenerator/expected/ClassType.expect @@ -1,7 +1,7 @@ /** * Description of class. * This is example - * + * /** / * @property-read Nette\Forms\Form $form */ abstract class Example extends ParentClass implements IExample, IOne @@ -10,9 +10,14 @@ abstract class Example extends ParentClass implements IExample, IOne use AnotherTrait { sayHello as protected; } + /** @use Foo */ + use ThirdTrait { + a as private foo; + b as private bar; + } - const ROLE = 'admin'; - const ACTIVE = false; + public const ROLE = 'admin'; + final public const ?bool ACTIVE = false; /** Commented */ private const FORCE_ARRAY = Nette\Utils\Json::FORCE_ARRAY; @@ -20,8 +25,9 @@ abstract class Example extends ParentClass implements IExample, IOne /** @var resource orignal file handle */ private $handle; public $order = RecursiveIteratorIterator::SELF_FIRST; - public array $typed1; + public readonly array $typed1; public ?array $typed2 = null; + public ?array $typed3 = null; public static $sections = ['first' => true]; @@ -42,5 +48,10 @@ abstract class Example extends ParentClass implements IExample, IOne } - abstract public function show($item, array &$res = null); + abstract public function show( + /** comment */ + $item, + array|null &$res = null, + stdClass|string|null $bar = null, + ); } diff --git a/tests/PhpGenerator/expected/ClassType.from.74.expect b/tests/PhpGenerator/expected/ClassType.from.74.expect deleted file mode 100644 index 24f85e5e..00000000 --- a/tests/PhpGenerator/expected/ClassType.from.74.expect +++ /dev/null @@ -1,7 +0,0 @@ -class Class7 -{ - public \A $a; - public ?\B $b; - public ?\C $c = null; - public ?int $i = 1; -} diff --git a/tests/PhpGenerator/expected/ClassType.from.81.expect b/tests/PhpGenerator/expected/ClassType.from.81.expect new file mode 100644 index 00000000..9d17ea27 --- /dev/null +++ b/tests/PhpGenerator/expected/ClassType.from.81.expect @@ -0,0 +1,34 @@ +#[Attr(new \Abc\Attr(/* unknown */))] +class Class11 +{ + final public const FOO = 10; + + public Foo&Bar $foo; + public readonly array $ro; + + + public function foo(Foo&Bar $c): Foo&Bar + { + } + + + public function bar($c = new \stdClass(/* unknown */)) + { + } +} + +#[\Attribute] +class Attr +{ +} + +class Class12 +{ + private readonly string $bar; + + + public function __construct( + private readonly string $foo, + ) { + } +} diff --git a/tests/PhpGenerator/expected/ClassType.from.82.expect b/tests/PhpGenerator/expected/ClassType.from.82.expect new file mode 100644 index 00000000..fe1c6221 --- /dev/null +++ b/tests/PhpGenerator/expected/ClassType.from.82.expect @@ -0,0 +1,20 @@ +readonly class Class13 +{ + public bool $foo; + + + public function __construct( + private bool $bar = true, + ) { + } + + + public function func(C|(X&D)|null $foo): (A&B)|null + { + } +} + +trait Trait13 +{ + public const FOO = 123; +} diff --git a/tests/PhpGenerator/expected/ClassType.from.84.expect b/tests/PhpGenerator/expected/ClassType.from.84.expect new file mode 100644 index 00000000..e7e8e2a0 --- /dev/null +++ b/tests/PhpGenerator/expected/ClassType.from.84.expect @@ -0,0 +1,167 @@ +class PropertyHookSignatures +{ + public string $basic { + get { + } + } + + public string $fullGet { + get { + } + } + + protected string $refGet { + &get { + } + } + + protected string $finalGet { + final get { + } + } + + public string $basicSet { + set { + } + } + + public string $fullSet { + set { + } + } + + public string $setWithParam { + set(string $foo) { + } + } + + public string $setWithParam2 { + set(string|int $value) { + } + } + + public string $finalSet { + final set { + } + } + + public string $combined { + set { + } + get { + } + } + + final public string $combinedFinal { + /** comment set */ + #[Set] + set { + } + /** comment get */ + #[Get] + get { + } + } + + public string $virtualProp { + set { + } + &get { + } + } +} + +abstract class AbstractHookSignatures +{ + abstract public string $abstractGet { get; } + abstract protected string $abstractSet { set; } + abstract public string $abstractBoth { set; get; } + + abstract public string $mixedGet { + set { + } + get; + } + + abstract public string $mixedSet { + set; + get { + } + } +} + +interface InterfaceHookSignatures +{ + public string $get { get; } + + public string $set { #[Set] + set; } + public string $both { set; get; } + public string $refGet { &get; } +} + +class AsymmetricVisibilitySignatures +{ + private(set) string $first; + protected(set) string $second; + protected private(set) string $third; + private(set) string $fourth; + protected(set) string $fifth; + public readonly string $implicit; + private(set) readonly string $readFirst; + private(set) readonly string $readSecond; + protected readonly string $readThird; + public(set) readonly string $readFourth; + private(set) string $firstFinal; + final protected(set) string $secondFinal; + protected private(set) string $thirdFinal; + private(set) string $fourthFinal; + final protected(set) string $fifthFinal; +} + +class CombinedSignatures +{ + protected(set) string $prop2 { + final set { + } + get { + } + } + + protected private(set) string $prop3 { + set { + } + final get { + } + } +} + +class ConstructorAllSignatures +{ + public function __construct( + private(set) string $prop1, + protected(set) string $prop2, + protected private(set) string $prop3, + private(set) string $prop4, + protected(set) string $prop5, + private(set) readonly string $readProp1, + private(set) readonly string $readProp2, + protected readonly string $readProp3, + public(set) readonly string $readProp4, + public string $hookProp1 { + get { + } + }, + protected(set) string $mixedProp1 { + set { + } + get { + } + }, + ) { + } +} + +class PropertyHookSignaturesChild extends PropertyHookSignatures +{ +} diff --git a/tests/PhpGenerator/expected/ClassType.from.85.expect b/tests/PhpGenerator/expected/ClassType.from.85.expect new file mode 100644 index 00000000..0f46e4c2 --- /dev/null +++ b/tests/PhpGenerator/expected/ClassType.from.85.expect @@ -0,0 +1,10 @@ +class Class85 +{ + private(set) static string $foo; + + + public function __construct( + final public $final, + ) { + } +} diff --git a/tests/PhpGenerator/expected/ClassType.from.bodies.84.expect b/tests/PhpGenerator/expected/ClassType.from.bodies.84.expect new file mode 100644 index 00000000..62891892 --- /dev/null +++ b/tests/PhpGenerator/expected/ClassType.from.bodies.84.expect @@ -0,0 +1,88 @@ +class PropertyHookSignatures +{ + public string $basic { + get => 'x'; + } + + public string $fullGet { + get { + return 'x'; + } + } + + protected string $refGet { + &get { + return 'x'; + } + } + + protected string $finalGet { + final get => 'x'; + } + + public string $basicSet { + set => 'x'; + } + + public string $fullSet { + set { + } + } + + public string $setWithParam { + set(string $foo) { + } + } + + public string $setWithParam2 { + set(string|int $value) => ''; + } + + public string $finalSet { + final set { + } + } + + public string $combined { + set { + } + get => 'x'; + } + + final public string $combinedFinal { + /** comment set */ + #[Set] + set { + } + /** comment get */ + #[Get] + get => 'x'; + } + + public string $virtualProp { + set { + } + &get => 'x'; + } +} + +abstract class AbstractHookSignatures +{ + abstract public string $abstractGet { get; } + abstract protected string $abstractSet { set; } + abstract public string $abstractBoth { set; get; } + + abstract public string $mixedGet { + set => 'x'; + get; + } + + abstract public string $mixedSet { + set; + get => 'x'; + } +} + +class PropertyHookSignaturesChild extends PropertyHookSignatures +{ +} diff --git a/tests/PhpGenerator/expected/ClassType.from.bodies.expect b/tests/PhpGenerator/expected/ClassType.from.bodies.expect index 729902bc..c33ef3d5 100644 --- a/tests/PhpGenerator/expected/ClassType.from.bodies.expect +++ b/tests/PhpGenerator/expected/ClassType.from.bodies.expect @@ -27,7 +27,8 @@ abstract class Class7 public function long() { - if ($member instanceof \Abc\Method) { + // comment + if ($member instanceof Method) { $s = [1, 2, 3]; } /* @@ -37,6 +38,27 @@ abstract class Class7 } + public function resolving($a = Abc\a\FOO, ?self $b = null, $c = self::FOO) + { + // constants + echo FOO; + echo \FOO; + echo a\FOO; + echo \Nette\FOO; + + // functions + func(); + \func(); + a\func(); + \Nette\func(); + + // classes + $x = new MyClass; + $y = new \stdClass; + $z = \Nette\Utils\ArrayHash::class; + } + + public function complex() { echo 1; @@ -57,14 +79,7 @@ abstract class Class7 line comment */ // Alias Method will not be resolved in comment - if ($member instanceof \Abc\Method) { - $s1 = "\na\n\tb\n\t\tc\n"; - $s2 = "\na\n\t{$b}\n\t\t$c\n"; - - $s3 = "a\n\t{$b}\n\t\t$c" - ; - $s3 = "a\n\tb\n\t\tc" - ; + if ($member instanceof Method) { // inline HTML is not supported ?> a diff --git a/tests/PhpGenerator/expected/ClassType.from.expect b/tests/PhpGenerator/expected/ClassType.from.expect index 36215443..bcc1f5b7 100644 --- a/tests/PhpGenerator/expected/ClassType.from.expect +++ b/tests/PhpGenerator/expected/ClassType.from.expect @@ -11,6 +11,14 @@ interface Interface2 { } +interface Interface3 extends Interface1 +{ +} + +interface Interface4 extends Interface3, Interface2 +{ +} + abstract class Class1 implements Interface1 { /** @@ -42,7 +50,12 @@ class Class2 extends Class1 implements Interface2 * Func3 * @return Class1 */ - private function &func3(array $a = [], Class2 $b = null, Unknown $c, \Xyz\Unknown $d, callable $e, $f = Abc\Unknown::ABC, $g) + private function &func3(array $a, Class2 $b, Unknown $c, \Xyz\Unknown $d, ?callable $e, $f) + { + } + + + private function func4(array $a = [], ?Class2 $b = null, $c = Unknown::ABC) { } @@ -64,7 +77,7 @@ class Class4 class Class5 { - public function func1(\A $a, ?\B $b, \C $c = null, \D $d = null, \E $e, ?int $i = 1, ?array $arr = []) + public function func1(\A $a, ?\B $b, ?\C $c = null, ?\D $d = null, ?int $i = 1, ?array $arr = []) { } @@ -85,3 +98,59 @@ class Class6 extends Class4 private const THE_PRIVATE_CONSTANT = 9; public const THE_PUBLIC_CONSTANT = 9; } + +class Class7 +{ + public \A $a; + public ?\B $b; + public ?\C $c = null; + public ?int $i = 1; +} + +class Class8 +{ + public function __construct( + public $a, + private string|int $b = 10, + $c = null, + ) { + } +} + +/** + * Description of class. + */ +#[\ExampleAttribute] +#[NamedArguments(foo: 'bar', bar: [1, 2, 3])] +class Class9 +{ + /** Commented */ + #[ExampleAttribute] + #[WithArguments(true)] + public const FOO = 123; + + /** @var resource */ + #[ExampleAttribute] + public $handle; + + + /** + * Returns file handle + */ + #[ExampleAttribute] + public function getHandle( + #[WithArguments(123)] + $mode, + ) { + } +} + +class Class10 +{ + public string|int $prop; + + + public function test(mixed $param): string|int + { + } +} diff --git a/tests/PhpGenerator/expected/ClassType.from.trait.bodies.expect b/tests/PhpGenerator/expected/ClassType.from.trait-use.bodies.expect similarity index 60% rename from tests/PhpGenerator/expected/ClassType.from.trait.bodies.expect rename to tests/PhpGenerator/expected/ClassType.from.trait-use.bodies.expect index 6a7cab1b..bdd46f66 100644 --- a/tests/PhpGenerator/expected/ClassType.from.trait.bodies.expect +++ b/tests/PhpGenerator/expected/ClassType.from.trait-use.bodies.expect @@ -3,6 +3,7 @@ */ trait Trait1 { + public static $s1; public $x1; @@ -12,73 +13,83 @@ trait Trait1 } } +trait Trait1b +{ + public function f1() + { + echo 'Trait1b::f1'; + } +} + trait Trait2 { + use Trait1; + protected $x2; - public $x1; public function f2() { echo 'Trait2::f2'; } - - - public function f1() - { - } } -class Class1 extends ParentClass +class ParentClass { - protected $x2; + public $x1; public function f1() { + echo 'ParentClass::f1'; } +} - - public function f2() - { - } +class Class1 extends ParentClass +{ + use Trait2; } class Class2 extends ParentClass { - public $x1; - protected $x2; - + use Trait2; public function f1() { echo 'Class2::f1'; } - - - public function f2() - { - } } class Class3 extends ParentClass { + use Trait2 { + f1 as protected aliased; + } + + /** info */ public $x1; - protected $x2; public function f1() { echo 'Class3::f1'; } +} +class Class4 extends ParentClass +{ + use Trait2; - public function f2() + public function aliased() { + echo 'Class4::aliased'; } +} - - public function aliased() - { +class Class5 +{ + use Trait1 { + f1 as private; } + use Trait1b; } diff --git a/tests/PhpGenerator/expected/ClassType.from.trait.expect b/tests/PhpGenerator/expected/ClassType.from.trait-use.expect similarity index 62% rename from tests/PhpGenerator/expected/ClassType.from.trait.expect rename to tests/PhpGenerator/expected/ClassType.from.trait-use.expect index 658908b8..85c990ed 100644 --- a/tests/PhpGenerator/expected/ClassType.from.trait.expect +++ b/tests/PhpGenerator/expected/ClassType.from.trait-use.expect @@ -3,6 +3,7 @@ */ trait Trait1 { + public static $s1; public $x1; @@ -11,30 +12,18 @@ trait Trait1 } } -trait Trait2 +trait Trait1b { - protected $x2; - public $x1; - - - public function f2() - { - } - - public function f1() { } } -class Class1 extends ParentClass +trait Trait2 { - protected $x2; - + use Trait1; - public function f1() - { - } + protected $x2; public function f2() @@ -42,39 +31,58 @@ class Class1 extends ParentClass } } -class Class2 extends ParentClass +class ParentClass { public $x1; - protected $x2; public function f1() { } +} +class Class1 extends ParentClass +{ + use Trait2; +} - public function f2() +class Class2 extends ParentClass +{ + use Trait2; + + public function f1() { } } class Class3 extends ParentClass { + use Trait2 { + f1 as protected aliased; + } + + /** info */ public $x1; - protected $x2; public function f1() { } +} +class Class4 extends ParentClass +{ + use Trait2; - public function f2() + public function aliased() { } +} - - public function aliased() - { +class Class5 +{ + use Trait1 { + f1 as private; } + use Trait1b; } diff --git a/tests/PhpGenerator/expected/ClassType.promotion.expect b/tests/PhpGenerator/expected/ClassType.promotion.expect new file mode 100644 index 00000000..78700ad6 --- /dev/null +++ b/tests/PhpGenerator/expected/ClassType.promotion.expect @@ -0,0 +1,12 @@ +class Example +{ + public function __construct( + $a, + public $b, + /** promo */ + #[Example] + private string $c, + public readonly Draft $d = new Draft(10), + ) { + } +} diff --git a/tests/PhpGenerator/expected/EnumType.from.expect b/tests/PhpGenerator/expected/EnumType.from.expect new file mode 100644 index 00000000..03264a3b --- /dev/null +++ b/tests/PhpGenerator/expected/EnumType.from.expect @@ -0,0 +1,31 @@ +/** + * Description of enum. + */ +#[\ExampleAttribute] +enum Enum1 +{ + public const FOO = 123; + public const BAR = \Abc\Enum1::Clubs; + + /** Commented */ + case Clubs; + + #[ExampleAttribute] + case Diamonds; + case Hearts; + case Spades; + + public function foo($x = self::Diamonds) + { + } +} + +enum Enum2: string implements \Countable +{ + case GET = 'get'; + case POST = 'post'; + + public function count(): int + { + } +} diff --git a/tests/PhpGenerator/expected/Extractor.bodies.expect b/tests/PhpGenerator/expected/Extractor.bodies.expect new file mode 100644 index 00000000..a4bdb604 --- /dev/null +++ b/tests/PhpGenerator/expected/Extractor.bodies.expect @@ -0,0 +1,102 @@ +methods[$member->getName()] = $member; + */ + throw new Nette\InvalidArgumentException('Argument must be Method|Property|Constant.'); + } + + + function resolving($a = a\FOO, ?self $b = null, $c = self::FOO) + { + // constants + echo FOO; + echo \FOO; + echo a\FOO; + echo Nette\FOO; + + // functions + func(); + \func(); + a\func(); + Nette\func(); + + // classes + $x = new MyClass; + $y = new \stdClass; + $z = Nette\Utils\ArrayHash::class; + } + + + function complex() + { + echo 1; + // single line comment + + // spaces - indent + // spaces - indent + + /* multi + line + comment */ + if ( + $a + && $b + $c) + {} + + /** multi + line + comment */ + // Alias Method will not be resolved in comment + if ($member instanceof Method) { + // inline HTML is not supported + ?> + a + b + c + methods[$member->getName()] = $member; + */ + throw new InvalidArgumentException('Argument must be Method|Property|Constant.'); + } + + + function resolving($a = \Abc\a\FOO, ?self $b = null, $c = self::FOO) + { + // constants + echo FOO; + echo \FOO; + echo \Abc\a\FOO; + echo FOO; + + // functions + func(); + \func(); + \Abc\a\func(); + func(); + + // classes + $x = new \Abc\MyClass; + $y = new \stdClass; + $z = Utils\ArrayHash::class; + } + + + function complex() + { + echo 1; + // single line comment + + // spaces - indent + // spaces - indent + + /* multi + line + comment */ + if ( + $a + && $b + $c) + {} + + /** multi + line + comment */ + // Alias Method will not be resolved in comment + if ($member instanceof \Abc\Method) { + // inline HTML is not supported + ?> + a + b + c + methods[$member->getName()] = $member; + */ + throw new \Nette\InvalidArgumentException('Argument must be Method|Property|Constant.'); + } + + + function resolving($a = \Abc\a\FOO, ?self $b = null, $c = self::FOO) + { + // constants + echo FOO; + echo \FOO; + echo \Abc\a\FOO; + echo \Nette\FOO; + + // functions + func(); + \func(); + \Abc\a\func(); + \Nette\func(); + + // classes + $x = new \Abc\MyClass; + $y = new \stdClass; + $z = \Nette\Utils\ArrayHash::class; + } + + + function complex() + { + echo 1; + // single line comment + + // spaces - indent + // spaces - indent + + /* multi + line + comment */ + if ( + $a + && $b + $c) + {} + + /** multi + line + comment */ + // Alias Method will not be resolved in comment + if ($member instanceof \Abc\Method) { + // inline HTML is not supported + ?> + a + b + c + bar = "foobar"; + } +} diff --git a/tests/PhpGenerator/expected/Extractor.classes.82.expect b/tests/PhpGenerator/expected/Extractor.classes.82.expect new file mode 100644 index 00000000..556d21dd --- /dev/null +++ b/tests/PhpGenerator/expected/Extractor.classes.82.expect @@ -0,0 +1,26 @@ + 'x'; + } + + public string $fullGet { + get { + return 'x'; + } + } + + protected string $refGet { + &get { + return 'x'; + } + } + + protected string $finalGet { + final get => 'x'; + } + + public string $basicSet { + set => 'x'; + } + + public string $fullSet { + set { + } + } + + public string $setWithParam { + set(string $foo) { + } + } + + public string $setWithParam2 { + set(string|int $value) => ''; + } + + public string $finalSet { + final set { + } + } + + public string $combined { + set(string $value) { + } + get => 'x'; + } + + final public string $combinedFinal { + /** comment set */ + #[Set] + set { + } + /** comment get */ + #[Get] + get => 'x'; + } + + public string $virtualProp { + set { + } + &get => 'x'; + } +} + +abstract class AbstractHookSignatures +{ + abstract public string $abstractGet { get; } + abstract protected string $abstractSet { set; } + abstract public string $abstractBoth { set; get; } + + abstract public string $mixedGet { + set => 'x'; + get; + } + + abstract public string $mixedSet { + set; + get => 'x'; + } +} + +interface InterfaceHookSignatures +{ + public string $get { get; } + + public string $set { #[Set] + set; } + public string $both { set; get; } + public string $refGet { &get; } +} + +class AsymmetricVisibilitySignatures +{ + public private(set) string $first; + public protected(set) string $second; + protected private(set) string $third; + private(set) string $fourth; + protected(set) string $fifth; + public readonly string $implicit; + public private(set) readonly string $readFirst; + private(set) readonly string $readSecond; + protected protected(set) readonly string $readThird; + public public(set) readonly string $readFourth; + final public private(set) string $firstFinal; + final public protected(set) string $secondFinal; + final protected private(set) string $thirdFinal; + final private(set) string $fourthFinal; + final protected(set) string $fifthFinal; +} + +class CombinedSignatures +{ + public protected(set) string $prop2 { + final set { + } + get { + return 'x'; + } + } + + protected private(set) string $prop3 { + set(string $value) { + } + final get => 'x'; + } +} + +class ConstructorAllSignatures +{ + public function __construct( + public private(set) string $prop1, + public protected(set) string $prop2, + protected private(set) string $prop3, + private(set) string $prop4, + protected(set) string $prop5, + public private(set) readonly string $readProp1, + private(set) readonly string $readProp2, + protected protected(set) readonly string $readProp3, + public public(set) readonly string $readProp4, + public string $hookProp1 { + get => 'x'; + }, + public protected(set) string $mixedProp1 { + set { + } + get { + return 'x'; + } + }, + ) { + } +} + +class PropertyHookSignaturesChild extends PropertyHookSignatures +{ +} diff --git a/tests/PhpGenerator/expected/Extractor.classes.85.expect b/tests/PhpGenerator/expected/Extractor.classes.85.expect new file mode 100644 index 00000000..66acf331 --- /dev/null +++ b/tests/PhpGenerator/expected/Extractor.classes.85.expect @@ -0,0 +1,16 @@ + 1, 'bbbbbbbb' => 2, 'cccccccc' => 3, @@ -21,7 +21,7 @@ final class Example extends ParentClass implements IExample 'ffffffff' => 6, ]; - const SHORT = ['aaaaaaaa' => 1, 'bbbbbbbb' => 2, 'cccccccc' => 3, 'dddddddd' => 4, 'eeeeeeee' => 5, 'ffffffff' => 6]; + public const SHORT = ['aaaaaaaa' => 1, 'bbbbbbbb' => 2, 'cccccccc' => 3, 'dddddddd' => 4, 'eeeeeeee' => 5]; /** @var resource orignal file handle */ private $handle; @@ -44,8 +44,7 @@ final class Example extends ParentClass implements IExample /** * @return resource */ - final public function first(stdClass $var): stdClass - { + final public function first(stdClass $var): stdClass { func(); return [ 'aaaaaaaaaaaa' => 1, @@ -59,7 +58,22 @@ final class Example extends ParentClass implements IExample - public function second() - { + public function second() { + } + + + + public function multi( + #[Foo] + $foo, + ) { + } + + + + public function multiType( + #[Foo] + $foo, + ): array { } } diff --git a/tests/PhpGenerator/expected/Printer.class.expect b/tests/PhpGenerator/expected/Printer.class.expect index 899d6ec6..810494c7 100644 --- a/tests/PhpGenerator/expected/Printer.class.expect +++ b/tests/PhpGenerator/expected/Printer.class.expect @@ -12,7 +12,7 @@ final class Example extends ParentClass implements IExample /** Commented */ private const FORCE_ARRAY = Nette\Utils\Json::FORCE_ARRAY; - const MULTILINE_LONG = [ + public const MULTILINE_LONG = [ 'aaaaaaaa' => 1, 'bbbbbbbb' => 2, 'cccccccc' => 3, @@ -21,7 +21,7 @@ final class Example extends ParentClass implements IExample 'ffffffff' => 6, ]; - const SHORT = ['aaaaaaaa' => 1, 'bbbbbbbb' => 2, 'cccccccc' => 3, 'dddddddd' => 4, 'eeeeeeee' => 5, 'ffffffff' => 6]; + public const SHORT = ['aaaaaaaa' => 1, 'bbbbbbbb' => 2, 'cccccccc' => 3, 'dddddddd' => 4, 'eeeeeeee' => 5]; /** @var resource orignal file handle */ private $handle; @@ -59,4 +59,19 @@ final class Example extends ParentClass implements IExample public function second() { } + + + public function multi( + #[Foo] + $foo, + ) { + } + + + public function multiType( + #[Foo] + $foo, + ): array + { + } } diff --git a/tests/PhpGenerator/expected/Printer.function.expect b/tests/PhpGenerator/expected/Printer.function.expect deleted file mode 100644 index 45cade76..00000000 --- a/tests/PhpGenerator/expected/Printer.function.expect +++ /dev/null @@ -1,5 +0,0 @@ -function func(stdClass $var): stdClass -{ - func(); - return 123; -} diff --git a/tests/PhpGenerator/expected/PsrPrinter.class.expect b/tests/PhpGenerator/expected/PsrPrinter.class.expect index ec60b708..f4e5e464 100644 --- a/tests/PhpGenerator/expected/PsrPrinter.class.expect +++ b/tests/PhpGenerator/expected/PsrPrinter.class.expect @@ -12,7 +12,7 @@ final class Example extends ParentClass implements IExample /** Commented */ private const FORCE_ARRAY = Nette\Utils\Json::FORCE_ARRAY; - const MULTILINE_LONG = [ + public const MULTILINE_LONG = [ 'aaaaaaaa' => 1, 'bbbbbbbb' => 2, 'cccccccc' => 3, @@ -21,7 +21,7 @@ final class Example extends ParentClass implements IExample 'ffffffff' => 6, ]; - const SHORT = ['aaaaaaaa' => 1, 'bbbbbbbb' => 2, 'cccccccc' => 3, 'dddddddd' => 4, 'eeeeeeee' => 5, 'ffffffff' => 6]; + public const SHORT = ['aaaaaaaa' => 1, 'bbbbbbbb' => 2, 'cccccccc' => 3, 'dddddddd' => 4, 'eeeeeeee' => 5]; /** @var resource orignal file handle */ private $handle; @@ -54,7 +54,9 @@ final class Example extends ParentClass implements IExample ]; } - public function second() - { + public function braces1( + #[attr] + $var, + ): stdClass { } } diff --git a/tests/PhpGenerator/fixtures/class-body.phpf b/tests/PhpGenerator/fixtures/bodies.php similarity index 72% rename from tests/PhpGenerator/fixtures/class-body.phpf rename to tests/PhpGenerator/fixtures/bodies.php index 5b710d56..b13e695a 100644 --- a/tests/PhpGenerator/fixtures/class-body.phpf +++ b/tests/PhpGenerator/fixtures/bodies.php @@ -5,6 +5,8 @@ namespace Abc; use Nette; +use function substr; +use const BAR; abstract class Class7 { @@ -34,6 +36,26 @@ function long() throw new Nette\InvalidArgumentException('Argument must be Method|Property|Constant.'); } + function resolving($a = a\FOO, ?self $b = null, $c = self::FOO) + { + // constants + echo FOO; + echo \FOO; + echo a\FOO; + echo \Nette\FOO; + + // functions + func(); + \func(); + a\func(); + \Nette\func(); + + // classes + $x = new MyClass; + $y = new \stdClass; + $z = Nette\Utils\ArrayHash::class; + } + function complex() { echo 1; @@ -55,29 +77,6 @@ function complex() comment */ // Alias Method will not be resolved in comment if ($member instanceof Method) { - $s1 = ' -a - b - c -'; - $s2 = " -a - {$b} - $c -"; - - $s3 = << a diff --git a/tests/PhpGenerator/fixtures/classes.81.php b/tests/PhpGenerator/fixtures/classes.81.php new file mode 100644 index 00000000..f283a3f7 --- /dev/null +++ b/tests/PhpGenerator/fixtures/classes.81.php @@ -0,0 +1,40 @@ +bar = "foobar"; + } +} diff --git a/tests/PhpGenerator/fixtures/classes.82.php b/tests/PhpGenerator/fixtures/classes.82.php new file mode 100644 index 00000000..c4a70d13 --- /dev/null +++ b/tests/PhpGenerator/fixtures/classes.82.php @@ -0,0 +1,26 @@ + 'x'; + } + + public string $fullGet { + get { return 'x'; } + } + + protected string $refGet { + &get { return 'x'; } + } + + protected string $finalGet { + final get => 'x'; + } + + // Set variants + public string $basicSet { + set => 'x'; + } + + public string $fullSet { + set { } + } + + public string $setWithParam { + set(string $foo) { } + } + + public string $setWithParam2 { + set(string|int $value) => ''; + } + + public string $finalSet { + final set { } + } + + // Combinations + public string $combined { + set(string $value) { } + get => 'x'; + } + + final public string $combinedFinal { + /** comment set */ + #[Set] + set { } + /** comment get */ + #[Get] + get => 'x'; + } + + public string $virtualProp { + set { } + &get => 'x'; + } +} + +// Abstract hooks +abstract class AbstractHookSignatures +{ + // Abstract variants + abstract public string $abstractGet { get; } + + abstract protected string $abstractSet { set; } + + abstract public string $abstractBoth { set; get; } + // Combination of abstract/concrete + abstract public string $mixedGet { + set => 'x'; + get; + } + + abstract public string $mixedSet { + set; + get => 'x'; + } +} + +// Interface with hooks +interface InterfaceHookSignatures +{ + public string $get { get; } + + public string $set { #[Set] set; } + + public string $both { set; get; } + + // Get can be forced as reference + public string $refGet { &get; } +} + +// Asymmetric visibility - all valid combinations +class AsymmetricVisibilitySignatures +{ + // Basic variants + public private(set) string $first; + public protected(set) string $second; + protected private(set) string $third; + private(set) string $fourth; + protected(set) string $fifth; + + // With readonly + public readonly string $implicit; + public private(set) readonly string $readFirst; + private(set) readonly string $readSecond; + protected protected(set) readonly string $readThird; + public public(set) readonly string $readFourth; + + // With final + final public private(set) string $firstFinal; + final public protected(set) string $secondFinal; + final protected private(set) string $thirdFinal; + final private(set) string $fourthFinal; + final protected(set) string $fifthFinal; +} + +// Combination of hooks and asymmetric visibility +class CombinedSignatures +{ + public protected(set) string $prop2 { + final set { } + get { return 'x'; } + } + + protected private(set) string $prop3 { + set(string $value) { } + final get => 'x'; + } +} + +// Constructor property promotion with asymmetric visibility +class ConstructorAllSignatures +{ + public function __construct( + // Basic asymmetric visibility + public private(set) string $prop1, + public protected(set) string $prop2, + protected private(set) string $prop3, + private(set) string $prop4, + protected(set) string $prop5, + + // With readonly + public private(set) readonly string $readProp1, + private(set) readonly string $readProp2, + protected protected(set) readonly string $readProp3, + public public(set) readonly string $readProp4, + + // With hooks + public string $hookProp1 { + get => 'x'; + }, + + // Combination of hooks and asymmetric visibility + public protected(set) string $mixedProp1 { + set { } + get { return 'x'; } + }, + ) {} +} + +class PropertyHookSignaturesChild extends PropertyHookSignatures +{ +} diff --git a/tests/PhpGenerator/fixtures/classes.85.php b/tests/PhpGenerator/fixtures/classes.85.php new file mode 100644 index 00000000..9460b244 --- /dev/null +++ b/tests/PhpGenerator/fixtures/classes.85.php @@ -0,0 +1,15 @@ + new Nette\PhpGenerator\PhpNamespace('')); +Assert::noError(fn() => new Nette\PhpGenerator\PhpNamespace('Iñtërnâti\ônàlizætiøn')); -Assert::exception(function () { - new Nette\PhpGenerator\ClassType('*'); -}, Nette\InvalidArgumentException::class); +Assert::exception( + fn() => new Nette\PhpGenerator\PhpNamespace(null), + TypeError::class, +); -Assert::exception(function () { - new Nette\PhpGenerator\ClassType('abc abc'); -}, Nette\InvalidArgumentException::class); +Assert::exception( + fn() => new Nette\PhpGenerator\PhpNamespace('*'), + Nette\InvalidArgumentException::class, +); -Assert::exception(function () { - new Nette\PhpGenerator\ClassType('abc\\abc'); -}, Nette\InvalidArgumentException::class); +Assert::exception( + fn() => new Nette\PhpGenerator\PhpNamespace('abc abc'), + Nette\InvalidArgumentException::class, +); -Assert::exception(function () { - new Nette\PhpGenerator\ClassType('\\abc'); -}, Nette\InvalidArgumentException::class); +Assert::exception( + fn() => new Nette\PhpGenerator\PhpNamespace('abc\\'), + Nette\InvalidArgumentException::class, +); +Assert::exception( + fn() => new Nette\PhpGenerator\PhpNamespace('\abc'), + Nette\InvalidArgumentException::class, +); -$class = new Nette\PhpGenerator\ClassType('Abc'); -Assert::exception(function () use ($class) { - $class->setExtends('*'); -}, Nette\InvalidArgumentException::class, "Value '*' is not valid class name."); - -Assert::exception(function () use ($class) { - $class->setExtends(['A', '*']); -}, Nette\InvalidArgumentException::class, "Value '*' is not valid class name."); - -Assert::exception(function () use ($class) { - $class->addExtend('*'); -}, Nette\InvalidArgumentException::class, "Value '*' is not valid class name."); - -Assert::exception(function () use ($class) { - $class->setImplements(['A', '*']); -}, Nette\InvalidArgumentException::class, "Value '*' is not valid class name."); - -Assert::exception(function () use ($class) { - $class->addImplement('*'); -}, Nette\InvalidArgumentException::class, "Value '*' is not valid class name."); +Assert::exception( + fn() => (new Nette\PhpGenerator\PhpNamespace('Abc'))->addUse(''), + Nette\InvalidArgumentException::class, +); -Assert::exception(function () use ($class) { - $class->setTraits(['A', '*']); -}, Nette\InvalidArgumentException::class, "Value '*' is not valid class name."); +Assert::exception( + fn() => (new Nette\PhpGenerator\PhpNamespace('Abc'))->addUse('Foo', 'a b'), + Nette\InvalidArgumentException::class, +); -Assert::exception(function () use ($class) { - $class->addTrait('*'); -}, Nette\InvalidArgumentException::class, "Value '*' is not valid class name."); +Assert::exception( + fn() => (new Nette\PhpGenerator\PhpNamespace('Abc'))->addUse('true'), + Nette\InvalidArgumentException::class, +); +Assert::exception( + fn() => (new Nette\PhpGenerator\PhpNamespace('Abc'))->addUse('aaa', 'true'), + Nette\InvalidArgumentException::class, +); -Assert::noError(function () { - new Nette\PhpGenerator\Property('Iñtërnâtiônàlizætiøn'); -}); -Assert::exception(function () { - new Nette\PhpGenerator\Property(null); -}, TypeError::class); +Assert::noError(fn() => new Nette\PhpGenerator\ClassType(null)); +Assert::noError(fn() => new Nette\PhpGenerator\ClassType('Iñtërnâtiônàlizætiøn')); -Assert::exception(function () { - new Nette\PhpGenerator\Property(''); -}, Nette\InvalidArgumentException::class); +Assert::exception( + fn() => new Nette\PhpGenerator\ClassType(''), + Nette\InvalidArgumentException::class, +); -Assert::exception(function () { - new Nette\PhpGenerator\Property('*'); -}, Nette\InvalidArgumentException::class); +Assert::exception( + fn() => new Nette\PhpGenerator\ClassType('*'), + Nette\InvalidArgumentException::class, +); +Assert::exception( + fn() => new Nette\PhpGenerator\ClassType('abc abc'), + Nette\InvalidArgumentException::class, +); -Assert::noError(function () { - new Nette\PhpGenerator\Parameter('Iñtërnâtiônàlizætiøn'); -}); +Assert::exception( + fn() => new Nette\PhpGenerator\ClassType('abc\abc'), + Nette\InvalidArgumentException::class, +); -Assert::exception(function () { - new Nette\PhpGenerator\Parameter(''); -}, Nette\InvalidArgumentException::class); +Assert::exception( + fn() => new Nette\PhpGenerator\ClassType('\abc'), + Nette\InvalidArgumentException::class, +); -Assert::exception(function () { - new Nette\PhpGenerator\Parameter(null); -}, TypeError::class); +Assert::exception( + fn() => new Nette\PhpGenerator\ClassType('bool'), + Nette\InvalidArgumentException::class, +); -Assert::exception(function () { - new Nette\PhpGenerator\Parameter('*'); -}, Nette\InvalidArgumentException::class); -Assert::exception(function () { - new Nette\PhpGenerator\Parameter('$test'); -}, Nette\InvalidArgumentException::class); - - -Assert::noError(function () { - new Nette\PhpGenerator\Method('Iñtërnâtiônàlizætiøn'); -}); - -Assert::exception(function () { - new Nette\PhpGenerator\Method(''); -}, Nette\InvalidArgumentException::class); +$class = new Nette\PhpGenerator\ClassType('Abc'); +Assert::exception( + fn() => $class->setExtends('*'), + Nette\InvalidArgumentException::class, + "Value '*' is not valid class name.", +); + +Assert::exception( + fn() => $class->setImplements(['A', '*']), + Nette\InvalidArgumentException::class, + "Value '*' is not valid class name.", +); + +Assert::exception( + fn() => $class->addImplement('*'), + Nette\InvalidArgumentException::class, + "Value '*' is not valid class name.", +); + +Assert::exception( + fn() => $class->addTrait('*'), + Nette\InvalidArgumentException::class, + "Value '*' is not valid trait name.", +); + + +$iface = new Nette\PhpGenerator\InterfaceType('Abc'); +Assert::exception( + fn() => $iface->setExtends(['A', '*']), + Nette\InvalidArgumentException::class, + "Value '*' is not valid class name.", +); + +Assert::exception( + fn() => $iface->addExtend('*'), + Nette\InvalidArgumentException::class, + "Value '*' is not valid class name.", +); -Assert::exception(function () { - new Nette\PhpGenerator\Method(null); -}, TypeError::class); -Assert::exception(function () { - new Nette\PhpGenerator\Method('*'); -}, Nette\InvalidArgumentException::class); +Assert::noError(fn() => new Nette\PhpGenerator\Property('Iñtërnâtiônàlizætiøn')); + +Assert::exception( + fn() => new Nette\PhpGenerator\Property(null), + TypeError::class, +); + +Assert::exception( + fn() => new Nette\PhpGenerator\Property(''), + Nette\InvalidArgumentException::class, +); + +Assert::exception( + fn() => new Nette\PhpGenerator\Property('*'), + Nette\InvalidArgumentException::class, +); + +Assert::exception( + fn() => (new Nette\PhpGenerator\Property('foo'))->setType('a b'), + Nette\InvalidArgumentException::class, +); + + +Assert::noError(fn() => new Nette\PhpGenerator\Parameter('Iñtërnâtiônàlizætiøn')); + +Assert::exception( + fn() => new Nette\PhpGenerator\Parameter(''), + Nette\InvalidArgumentException::class, +); + +Assert::exception( + fn() => new Nette\PhpGenerator\Parameter(null), + TypeError::class, +); + +Assert::exception( + fn() => new Nette\PhpGenerator\Parameter('*'), + Nette\InvalidArgumentException::class, +); + +Assert::exception( + fn() => new Nette\PhpGenerator\Parameter('$test'), + Nette\InvalidArgumentException::class, +); + +Assert::exception( + fn() => (new Nette\PhpGenerator\Parameter('foo'))->setType('a b'), + Nette\InvalidArgumentException::class, +); + + +Assert::noError(fn() => new Nette\PhpGenerator\Method('Iñtërnâtiônàlizætiøn')); + +Assert::exception( + fn() => new Nette\PhpGenerator\Method(''), + Nette\InvalidArgumentException::class, +); + +Assert::exception( + fn() => new Nette\PhpGenerator\Method(null), + TypeError::class, +); + +Assert::exception( + fn() => new Nette\PhpGenerator\Method('*'), + Nette\InvalidArgumentException::class, +); + +Assert::exception( + fn() => (new Nette\PhpGenerator\Method('foo'))->setReturnType('a b'), + Nette\InvalidArgumentException::class, +); -Assert::noError(function () { - new Nette\PhpGenerator\GlobalFunction('Iñtërnâtiônàlizætiøn'); -}); +Assert::noError(fn() => new Nette\PhpGenerator\GlobalFunction('Iñtërnâtiônàlizætiøn')); -Assert::exception(function () { - new Nette\PhpGenerator\GlobalFunction(''); -}, Nette\InvalidArgumentException::class); +Assert::exception( + fn() => new Nette\PhpGenerator\GlobalFunction(''), + Nette\InvalidArgumentException::class, +); -Assert::exception(function () { - new Nette\PhpGenerator\GlobalFunction(null); -}, TypeError::class); +Assert::exception( + fn() => new Nette\PhpGenerator\GlobalFunction(null), + TypeError::class, +); -Assert::exception(function () { - new Nette\PhpGenerator\GlobalFunction('*'); -}, Nette\InvalidArgumentException::class); +Assert::exception( + fn() => new Nette\PhpGenerator\GlobalFunction('*'), + Nette\InvalidArgumentException::class, +); -Assert::noError(function () { - new Nette\PhpGenerator\Constant('Iñtërnâtiônàlizætiøn'); -}); +Assert::noError(fn() => new Nette\PhpGenerator\Constant('Iñtërnâtiônàlizætiøn')); -Assert::exception(function () { - new Nette\PhpGenerator\Constant(null); -}, TypeError::class); +Assert::exception( + fn() => new Nette\PhpGenerator\Constant(null), + TypeError::class, +); -Assert::exception(function () { - new Nette\PhpGenerator\Constant(''); -}, Nette\InvalidArgumentException::class); +Assert::exception( + fn() => new Nette\PhpGenerator\Constant(''), + Nette\InvalidArgumentException::class, +); -Assert::exception(function () { - new Nette\PhpGenerator\Constant('*'); -}, Nette\InvalidArgumentException::class); +Assert::exception( + fn() => new Nette\PhpGenerator\Constant('*'), + Nette\InvalidArgumentException::class, +); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index a826bf40..9754ccbe 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -20,7 +20,12 @@ function same(string $expected, $actual): void function sameFile(string $file, $actual): void { - same(file_get_contents($file), $actual); + try { + same(file_get_contents($file), $actual); + } catch (Tester\AssertException $e) { + $e->outputName = basename($file, '.expect'); + throw $e; + } } diff --git a/tests/coding-standard.yml b/tests/coding-standard.yml deleted file mode 100644 index 1e35732e..00000000 --- a/tests/coding-standard.yml +++ /dev/null @@ -1,7 +0,0 @@ -imports: - - { resource: '../temp/coding-standard/coding-standard-php71.yml' } - -parameters: - skip: - PhpCsFixer\Fixer\Casing\LowercaseConstantsFixer: - - src/PhpGenerator/Type.php # constant NULL diff --git a/tests/phpstan.neon b/tests/phpstan.neon deleted file mode 100644 index c551b84f..00000000 --- a/tests/phpstan.neon +++ /dev/null @@ -1,2 +0,0 @@ -parameters: - treatPhpDocTypesAsCertain: false