diff --git a/.gitattributes b/.gitattributes index 3aa6270a..a728f976 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,9 +1,8 @@ .gitattributes export-ignore .gitignore export-ignore .github export-ignore -.travis.yml export-ignore -ecs.php export-ignore -phpstan.neon export-ignore +ncs.* export-ignore +phpstan*.neon export-ignore tests/ export-ignore *.sh eol=lf diff --git a/.github/workflows/coding-style.yml b/.github/workflows/coding-style.yml index d415d813..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.1 + 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: 8.0 + php-version: 8.3 coverage: none - - run: composer create-project nette/coding-standard temp/coding-standard ^3 --no-progress --ignore-platform-reqs + - 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 b0692d71..32145249 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -1,19 +1,16 @@ 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: 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 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 73516285..8ed81e7e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,13 +7,13 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: ['7.1', '7.2', '7.3', '7.4', '8.0'] + 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: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} @@ -22,9 +22,9 @@ jobs: - run: composer install --no-progress --prefer-dist - run: vendor/bin/tester tests -s -C - if: failure() - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: - name: output + name: output-${{ matrix.php }} path: tests/**/output @@ -32,10 +32,10 @@ jobs: name: Lowest Dependencies runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - 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,10 +46,10 @@ jobs: name: Code Coverage runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - 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 diff --git a/composer.json b/composer.json index 75c72f4c..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 8.0 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,20 +15,24 @@ } ], "require": { - "php": ">=7.1", - "nette/utils": "^3.1.2" + "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": { @@ -37,7 +41,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.5-dev" + "dev-master": "4.2-dev" } } } diff --git a/ecs.php b/ecs.php deleted file mode 100644 index 40851e2a..00000000 --- a/ecs.php +++ /dev/null @@ -1,24 +0,0 @@ -import(PRESET_DIR . '/php71.php'); - - $parameters = $containerConfigurator->parameters(); - - $parameters->set('skip', [ - 'fixtures*/*', - - // constant NULL, FALSE - PhpCsFixer\Fixer\Casing\LowercaseConstantsFixer::class => [ - 'src/PhpGenerator/Type.php', - ], - ]); -}; 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 index d7ffa1b5..bc3a10d6 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,7 +1,8 @@ +includes: + - phpstan-baseline.neon + parameters: - level: 5 + level: 6 paths: - src - - treatPhpDocTypesAsCertain: false diff --git a/readme.md b/readme.md index 04861b82..bb7fb17a 100644 --- a/readme.md +++ b/readme.md @@ -1,49 +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) -[![Tests](https://github.com/nette/php-generator/workflows/Tests/badge.svg?branch=master)](https://github.com/nette/php-generator/actions) -[![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 ------------- - -Do you need to generate PHP code of classes, functions, namespaces, etc.? This library with a friendly API will help you. +Are you looking for a tool to generate PHP code for [classes](#classes), [functions](#global-functions), or complete [PHP files](#php-files)? -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 -[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! +

Installation ------------ +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.5 is compatible with PHP 7.1 to 8.0 -- 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). +  +[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! + +  Classes ------- -Let's start with a straightforward example of generating class using [ClassType](https://api.nette.org/3.0/Nette/PhpGenerator/ClassType.html): +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'); @@ -52,41 +53,41 @@ $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; ``` -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 also use a printer to generate the code, which, unlike `echo $class`, we will be able to further configure: +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 $printer = new Nette\PhpGenerator\Printer; echo $printer->printClass($class); ``` -We can add constants ([Constant](https://api.nette.org/3.0/Nette/PhpGenerator/Constant.html)) and properties ([Property](https://api.nette.org/3.0/Nette/PhpGenerator/Property.html)): +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) - ->setPrivate(); // constant visiblity + ->setProtected() // constant visibility + ->setType('int') + ->setFinal(); $class->addProperty('items', [1, 2, 3]) ->setPrivate() // or setVisibility('private') @@ -94,15 +95,14 @@ $class->addProperty('items', [1, 2, 3]) ->addComment('@var int[]'); $class->addProperty('list') - ->setType('array') - ->setNullable() - ->setInitialized(); // prints '= null' + ->setType('?array') + ->setInitialized(); // outputs '= null' ``` -It generates: +This will generate: ```php -private const ID = 123; +final protected const int ID = 123; /** @var int[] */ private static $items = [1, 2, 3]; @@ -110,29 +110,26 @@ 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') // method return type - ->setReturnNullable() // nullable return type + ->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 = []): ?int { @@ -140,16 +137,16 @@ final protected function count(array &$items = []): ?int } ``` -Promoted parameters introduced by PHP 8.0 can be passed to the constructor: +Promoted parameters introduced in PHP 8.0 can be passed to the constructor: ```php $method = $class->addMethod('__construct'); $method->addPromotedParameter('name'); $method->addPromotedParameter('args', []) - ->setPrivate(); + ->setPrivate(); ``` -It results in: +The result is: ```php public function __construct( @@ -159,11 +156,15 @@ public function __construct( } ``` -If the property, constant, method or parameter already exist, it will be overwritten. +Readonly properties and classes be marked using the `setReadOnly()` function. + +------ -Members can be removed using `removeProperty()`, `removeConstant()`, `removeMethod()` or `removeParameter()`. +If an added property, constant, method, or parameter already exists, an exception is thrown. -You can also add existing `Method`, `Property` or `Constant` objects to the class: +Class members can be removed using `removeProperty()`, `removeConstant()`, `removeMethod()`, or `removeParameter()`. + +You can also add existing `Method`, `Property`, or `Constant` objects to the class: ```php $method = new Nette\PhpGenerator\Method('getHandle'); @@ -176,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'); @@ -184,105 +185,86 @@ $methodRecount = $methodCount->cloneWithName('recount'); $class->addMember($methodRecount); ``` -Types ------ +  -Each type or union type can be passed as a string, you can also use predefined constants for native types: +Interfaces or Traits +-------------------- -```php -use Nette\PhpGenerator\Type; +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)): -$member->setType('array'); -$member->setType(Type::ARRAY); -$member->setType('array|string'); -$member->setType(null); // removes type +```php +$interface = new Nette\PhpGenerator\InterfaceType('MyInterface'); +$trait = new Nette\PhpGenerator\TraitType('MyTrait'); ``` -The same applies to the method `setReturnType()`. - - -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`: +Using a trait: ```php $class = new Nette\PhpGenerator\ClassType('Demo'); -// ... - -$printer = new Nette\PhpGenerator\PsrPrinter; -echo $printer->printClass($class); // 4 spaces indentation +$class->addTrait('SmartObject'); +$class->addTrait('MyTrait') + ->addResolution('sayHello as protected') + ->addComment('@use MyTrait'); +echo $class; ``` - -Interface or Trait ------------------- - -You can create interfaces and traits in a similar way, just change the type: +The result is: ```php -$class = new Nette\PhpGenerator\ClassType('DemoInterface'); -$class->setInterface(); -// or $class->setTrait(); +class Demo +{ + use SmartObject; + /** @use MyTrait */ + use MyTrait { + sayHello as protected; + } +} ``` -Literals --------- - -You can pass any PHP code to property or parameter default values via `Literal`: +  -```php -use Nette\PhpGenerator\Literal; - -$class = new Nette\PhpGenerator\ClassType('Demo'); +Enums +----- -$class->addProperty('foo', new Literal('Iterator::SELF_FIRST')); +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)): -$class->addMethod('bar') - ->addParameter('id', new Literal('1 + 2')); +```php +$enum = new Nette\PhpGenerator\EnumType('Suit'); +$enum->addCase('Clubs'); +$enum->addCase('Diamonds'); +$enum->addCase('Hearts'); +$enum->addCase('Spades'); -echo $class; +echo $enum; ``` -Result: +The result is: ```php -class Demo +enum Suit { - public $foo = Iterator::SELF_FIRST; - - public function bar($id = 1 + 2) - { - } + case Clubs; + case Diamonds; + case Hearts; + case Spades; } ``` -Using Traits ------------- +You can also define scalar equivalents and create a "backed" enum: ```php -$class = new Nette\PhpGenerator\ClassType('Demo'); -$class->addTrait('SmartObject'); -$class->addTrait('MyTrait', ['sayHello as protected']); -echo $class; +$enum->addCase('Clubs', '♣'); +$enum->addCase('Diamonds', '♦'); ``` -Result: +For each *case*, you can add a comment or [attributes](#attributes) using `addComment()` or `addAttribute()`. -```php -class Demo -{ - use SmartObject; - use MyTrait { - sayHello as protected; - } -} -``` +  -Anonymous Class ---------------- +Anonymous Classes +----------------- -Give `null` as the name and you have an anonymous class: +Pass `null` as the name, and you have an anonymous class: ```php $class = new Nette\PhpGenerator\ClassType(null); @@ -292,7 +274,7 @@ $class->addMethod('__construct') echo '$obj = new class ($val) ' . $class . ';'; ``` -Result: +The result is: ```php $obj = new class ($val) { @@ -303,10 +285,12 @@ $obj = new class ($val) { }; ``` -Global Function ---------------- +  -Code of functions will generate class [GlobalFunction](https://api.nette.org/3.0/Nette/PhpGenerator/GlobalFunction.html): +Global Functions +---------------- + +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'); @@ -315,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) @@ -328,10 +312,12 @@ function foo($a, $b) } ``` -Closure -------- +  + +Anonymous Functions +------------------- -Code of closures will generate class [Closure](https://api.nette.org/3.0/Nette/PhpGenerator/Closure.html): +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; @@ -342,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) { @@ -354,69 +340,75 @@ 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 ``` -Attributes ----------- +  -You can add PHP 8 attributes to all classes, methods, properties, constants, functions, closures and parameters. +Method and Function Signatures +------------------------------ -```php -$class = new Nette\PhpGenerator\ClassType('Demo'); -$class->addAttribute('Deprecated'); - -$class->addProperty('list') - ->addAttribute('WithArguments', [1, 2]); +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 $method = $class->addMethod('count') - ->addAttribute('Foo\Cached', ['mode' => true]); + ->addComment('Count it.') + ->setFinal() + ->setProtected() + ->setReturnType('?int'); +``` -$method->addParameter('items') - ->addAttribute('Bar'); +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: -echo $class; +```php +$method->addParameter('items', []) // $items = [] + ->setReference() // &$items = [] + ->setType('array'); // array &$items = [] + +// function count(&$items = []) ``` -Result: +To define the so-called variadics parameters (or also the splat, spread, ellipsis, unpacking or three dots operator), use `setVariadic()`: ```php -#[Deprecated] -class Demo -{ - #[WithArguments(1, 2)] - public $list; +$method = $class->addMethod('count'); +$method->setVariadic(true); +$method->addParameter('items'); +``` +This generates: - #[Foo\Cached(mode: true)] - public function count(#[Bar] $items) - { - } +```php +function count(...$items) +{ } ``` -Method and Function Body Generator ----------------------------------- +  + +Method and Function Bodies +-------------------------- -The body can be passed to the `setBody()` method at once or sequentially (line by line) by repeatedly calling `addBody()`: +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'); @@ -425,7 +417,7 @@ $function->addBody('return $a;'); echo $function; ``` -Result +The result is: ```php function foo() @@ -435,7 +427,7 @@ function foo() } ``` -You can use special placeholders for handy way to inject variables. +You can use special placeholders for easy variable insertion. Simple placeholders `?` @@ -443,20 +435,20 @@ Simple placeholders `?` $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]; @@ -465,7 +457,7 @@ $function->setBody('myfunc(...?);', [$items]); echo $function; ``` -Result: +The result is: ```php function foo() @@ -474,7 +466,7 @@ function foo() } ``` -You can also use PHP 8 named parameters using placeholder `...?:` +You can also use named parameters for PHP 8 with `...?:` ```php $items = ['foo' => 1, 'bar' => true]; @@ -483,7 +475,7 @@ $function->setBody('myfunc(...?:);', [$items]); // myfunc(foo: 1, bar: true); ``` -Escape placeholder using slash `\?` +The placeholder is escaped with a backslash `\?` ```php $num = 3; @@ -493,7 +485,7 @@ $function->addBody('return $a \? 10 : ?;', [$num]); echo $function; ``` -Result: +The result is: ```php function foo($a) @@ -502,10 +494,264 @@ 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 ([PhpNamespace](https://api.nette.org/3.0/Nette/PhpGenerator/PhpNamespace.html)): +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'); @@ -520,41 +766,57 @@ $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 // 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'); +``` + +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 the class is part of the namespace, it is rendered slightly differently**: all types (ie. type hints, return types, parent class name, -implemented interfaces, used traits and attributes) 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: +**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); ``` @@ -586,26 +848,29 @@ $printer->setTypeResolving(false); echo $printer->printNamespace($namespace); ``` +  + PHP Files --------- -Classes and namespaces can be grouped into PHP files represented by the class [PhpFile](https://api.nette.org/3.0/Nette/PhpGenerator/PhpFile.html): +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 insert an existing namespace into the file -// $file->addNamespace(new Nette\PhpGenerator\PhpNamespace('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); ``` @@ -624,61 +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::withBodiesFrom(MyClass::class); +$class = Nette\PhpGenerator\ClassType::from(Foo::class, withBodies: true); -$function = Nette\PhpGenerator\GlobalFunction::withBodyFrom('dump'); +$function = Nette\PhpGenerator\GlobalFunction::from('foo', withBody: true); ``` +  -Variables Dumper ----------------- +Loading from PHP Files +---------------------- -The Dumper returns a parsable PHP string representation of a variable. Provides better and clearer output that native functon `var_export()`. +You can also load functions, classes, interfaces, and enums directly from a string containing PHP code. For example, to create a `ClassType` object: ```php -$dumper = new Nette\PhpGenerator\Dumper; +$class = Nette\PhpGenerator\ClassType::fromCode(<<dump($var); // prints ['a', 'b', 123] +When loading classes from PHP code, single-line comments outside method bodies are ignored (e.g., for properties, etc.), as this library doesn't have an API to work with them. + +You can also directly load an entire PHP file, which can contain any number of classes, functions, or even namespaces: + +```php +$file = Nette\PhpGenerator\PhpFile::fromCode(file_get_contents('classes.php')); ``` -Custom Printer --------------- +The file's initial comment and `strict_types` declaration are also loaded. However, all other global code is ignored. + +It requires `nikic/php-parser` to be installed. -Need to customize printer behavior? Create your own by inheriting the `Printer` class. You can reconfigure these variables: +*(If you need to manipulate global code in files or individual statements in method bodies, it's better to use the `nikic/php-parser` library directly.)* + +  + +Class Manipulator +----------------- + +The [ClassManipulator](https://api.nette.org/php-generator/master/Nette/PhpGenerator/ClassManipulator.html) class provides tools for manipulating classes. ```php -class MyPrinter extends Nette\PhpGenerator\Printer -{ - protected $indentation = "\t"; - protected $linesBetweenProperties = 0; - protected $linesBetweenMethods = 1; - protected $returnTypeColon = ': '; -} +$class = new Nette\PhpGenerator\ClassType('Demo'); +$manipulator = new Nette\PhpGenerator\ClassManipulator($class); +``` + +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 +``` + +  + +Variable Dumping +---------------- + +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); // outputs ['a', 'b', 123] ``` diff --git a/src/PhpGenerator/Attribute.php b/src/PhpGenerator/Attribute.php index 2c0f9a3b..896f9473 100644 --- a/src/PhpGenerator/Attribute.php +++ b/src/PhpGenerator/Attribute.php @@ -13,24 +13,23 @@ /** - * PHP Attribute. + * Definition of a PHP attribute. */ final class Attribute { - use Nette\SmartObject; + private string $name; - /** @var string */ - private $name; - - /** @var array */ - private $args; + /** @var mixed[] */ + private array $args; + /** @param mixed[] $args */ public function __construct(string $name, array $args) { if (!Helpers::isNamespaceIdentifier($name)) { throw new Nette\InvalidArgumentException("Value '$name' is not valid attribute name."); } + $this->name = $name; $this->args = $args; } @@ -42,6 +41,7 @@ public function getName(): string } + /** @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 5d3b719f..320c5067 100644 --- a/src/PhpGenerator/ClassType.php +++ b/src/PhpGenerator/ClassType.php @@ -10,188 +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\AttributeAware; + 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; @@ -204,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; @@ -218,331 +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] = func_num_args() > 1 - ? (new Property($name))->setValue($value) - : new Property($name); - } - - /** - * @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 f504a738..49401e62 100644 --- a/src/PhpGenerator/Closure.php +++ b/src/PhpGenerator/Closure.php @@ -9,22 +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 @@ -35,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; @@ -59,6 +46,7 @@ public function setUses(array $uses): self } + /** @return Parameter[] */ public function getUses(): array { return $this->uses; @@ -69,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 4f046256..1e89d6e6 100644 --- a/src/PhpGenerator/Constant.php +++ b/src/PhpGenerator/Constant.php @@ -9,34 +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 c9fa9261..45ffc451 100644 --- a/src/PhpGenerator/Dumper.php +++ b/src/PhpGenerator/Dumper.php @@ -10,32 +10,34 @@ 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 === null) { return 'null'; @@ -46,138 +48,183 @@ 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)) { - if ($var instanceof Literal || $var instanceof Closure) { - return ltrim(Nette\Utils\Strings::indent(trim((string) $var), $level), "\t"); - } - 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); + $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 === $counter + foreach ($var as $k => $v) { + $keyPart = $hideKeys && ($k !== $keys[0] || $k === 0) ? '' : $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"; + $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]" - : '\\' . self::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.'); @@ -188,6 +235,7 @@ public function format(string $statement, ...$args): string if (!is_array($arg)) { throw new Nette\InvalidArgumentException('Argument must be an array.'); } + $res .= $this->dumpArguments($arg, strlen($res) - strrpos($res, "\n"), $token === '...?:'); } else { // $ -> :: @@ -195,39 +243,46 @@ public function format(string $statement, ...$args): string 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, bool $named): string + /** @param mixed[] $args */ + private function dumpArguments(array $args, int $column, bool $named): string { - $outInline = $outWrapped = ''; - - foreach ($var as $k => &$v) { - $k = !$named || is_int($k) ? '' : $k . ': '; - $outInline .= $outInline === '' ? '' : ', '; - $outInline .= $k . $this->dumpVar($v, [$var], 0, $column + strlen($outInline)); - $outWrapped .= ($outWrapped === '' ? '' : ',') . "\n\t" . $k . $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 c87f4d02..2f164b17 100644 --- a/src/PhpGenerator/Factory.php +++ b/src/PhpGenerator/Factory.php @@ -10,74 +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(self::getAttributes($from)); + $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) { + $declaringClass = Reflection::getPropertyDeclaringClass($prop); + if ($prop->isDefault() - && $prop->getDeclaringClass()->name === $from->name - && (PHP_VERSION_ID < 80000 || !$prop->isPromoted()) + && $declaringClass->name === $from->name + && !$prop->isPromoted() + && !$class->isEnum() ) { - $props[] = $this->fromPropertyReflection($prop); + $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 = []; + 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 ($withBodies) { - $srcMethod = Nette\Utils\Reflection::getMethodDeclaringMethod($method); - $srcClass = $srcMethod->getDeclaringClass()->name; - $b = $bodies[$srcClass] = $bodies[$srcClass] ?? $this->loadMethodBodies($srcMethod->getDeclaringClass()); - if (isset($b[$srcMethod->name])) { - $m->setBody($b[$srcMethod->name]); + $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; } @@ -89,30 +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())); - $method->setAttributes(self::getAttributes($from)); - if ($from->getReturnType() instanceof \ReflectionNamedType) { - $method->setReturnType($from->getReturnType()->getName()); - $method->setReturnNullable($from->getReturnType()->allowsNull()); - } elseif ($from->getReturnType() instanceof \ReflectionUnionType) { - $method->setReturnType((string) $from->getReturnType()); - } + $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())); @@ -121,47 +183,62 @@ public function fromFunctionReflection(\ReflectionFunction $from, bool $withBody if (!$from->isClosure()) { $function->setComment(Helpers::unformatDocComment((string) $from->getDocComment())); } - $function->setAttributes(self::getAttributes($from)); - if ($from->getReturnType() instanceof \ReflectionNamedType) { - $function->setReturnType($from->getReturnType()->getName()); - $function->setReturnNullable($from->getReturnType()->allowsNull()); - } elseif ($from->getReturnType() instanceof \ReflectionUnionType) { - $function->setReturnType((string) $from->getReturnType()); + + $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 = PHP_VERSION_ID >= 80000 && $from->isPromoted() - ? new PromotedParameter($from->name) - : new Parameter($from->name); - $param->setReference($from->isPassedByReference()); - if ($from->getType() instanceof \ReflectionNamedType) { - $param->setType($from->getType()->getName()); - $param->setNullable($from->getType()->allowsNull()); - } elseif ($from->getType() instanceof \ReflectionUnionType) { - $param->setType((string) $from->getType()); + 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((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(self::getAttributes($from)); + + $param->setAttributes($this->getAttributes($from)); return $param; } @@ -170,13 +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(self::getAttributes($from)); + $const->setAttributes($this->getAttributes($from)); return $const; } @@ -187,179 +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) { - if ($from->getType() instanceof \ReflectionNamedType) { - $prop->setType($from->getType()->getName()); - $prop->setNullable($from->getType()->allowsNull()); - } elseif ($from->getType() instanceof \ReflectionUnionType) { - $prop->setType((string) $from->getType()); - } - $prop->setInitialized($from->hasType() && array_key_exists($prop->getName(), $defaults)); - } else { - $prop->setInitialized(false); - } + $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(self::getAttributes($from)); + $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) { - /** @var Node\Scalar\String_|Node\Scalar\EncapsedStringPart $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) { - /** @var Node\Scalar\Encapsed $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); - - $traverser = new PhpParser\NodeTraverser; - $traverser->addVisitor(new PhpParser\NodeVisitor\NameResolver(null, ['replaceNodes' => false])); - $stmts = $traverser->traverse($stmts); - - return [$code, $stmts]; + return $from->isPrivate() + ? Visibility::Private + : ($from->isProtected() ? Visibility::Protected : Visibility::Public); } - private function getAttributes($from): array + private function getExtractor(string $file): Extractor { - if (PHP_VERSION_ID < 80000) { - return []; - } - $res = []; - foreach ($from->getAttributes() as $attr) { - $res[] = new Attribute($attr->getName(), $attr->getArguments()); - } - return $res; + $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 0d40bae7..c2ed0ac0 100644 --- a/src/PhpGenerator/GlobalFunction.php +++ b/src/PhpGenerator/GlobalFunction.php @@ -13,40 +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 82ede77a..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 + 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)->dump($var); + $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 format(string $statement, ...$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"; } - /** @deprecated use Nette\PhpGenerator\Dumper::format() */ - public static function formatArgs(string $statement, array $args): string + public static function simplifyTaggedNames(string $code, ?PhpNamespace $namespace): string { - return (new Dumper)->format($statement, ...$args); - } - - - public static function formatDocComment(string $content): 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); } @@ -100,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 0eab60d1..5816fbe0 100644 --- a/src/PhpGenerator/Method.php +++ b/src/PhpGenerator/Method.php @@ -10,39 +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)); } @@ -50,36 +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 (new Printer)->printMethod($this); } - /** @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 static */ - public function setStatic(bool $state = true): self + public function setStatic(bool $state = true): static { $this->static = $state; return $this; @@ -92,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; @@ -106,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; @@ -123,12 +88,13 @@ public function isAbstract(): bool /** * @param string $name without $ */ - public function addPromotedParameter(string $name, $defaultValue = null): PromotedParameter + 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; } @@ -136,8 +102,14 @@ public function addPromotedParameter(string $name, $defaultValue = null): Promot /** @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 167325d9..910207ec 100644 --- a/src/PhpGenerator/Parameter.php +++ b/src/PhpGenerator/Parameter.php @@ -9,38 +9,26 @@ namespace Nette\PhpGenerator; -use Nette; +use Nette\Utils\Type; /** - * Function/Method parameter description. - * - * @property mixed $defaultValue + * Definition of a function/method 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; @@ -53,53 +41,23 @@ public function isReference(): bool } - /** @return static */ - public function setType(?string $type): self - { - if ($type && $type[0] === '?') { - $type = substr($type, 1); - $this->nullable = true; - } - $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; @@ -108,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; @@ -121,7 +78,7 @@ public function setDefaultValue($val): self } - public function getDefaultValue() + public function getDefaultValue(): mixed { return $this->defaultValue; } @@ -131,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 3310550e..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,26 +71,58 @@ public function addTrait(string $name): ClassType } - /** @param string|PhpNamespace $namespace */ - public function addNamespace($namespace): 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 + { + 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 { - if ($namespace instanceof PhpNamespace) { - $res = $this->namespaces[$namespace->getName()] = $namespace; + return $this + ->addNamespace(Helpers::extractNamespace($name)) + ->addFunction(Helpers::extractShortName($name)); + } - } elseif (is_string($namespace)) { - $res = $this->namespaces[$namespace] = $this->namespaces[$namespace] ?? new PhpNamespace($namespace); - } else { - throw new Nette\InvalidArgumentException('Argument must be string|PhpNamespace.'); - } + /** + * 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 $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; + } + + /** @return PhpNamespace[] */ public function getNamespaces(): array { @@ -83,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; } @@ -117,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 8ef230a6..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,99 +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 unresolveUnionType(string $type): string + /** + * Resolves relative name to full name. + */ + public function resolveName(string $name, string $of = self::NameNormal): string { - return implode('|', array_map([$this, 'unresolveName'], explode('|', $type))); + 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); } - public function unresolveName(string $name): string + /** + * 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(self::KEYWORDS[strtolower($name)]) || $name === '') { + 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)); @@ -177,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 + { + $this->add($trait = new TraitType($name, $this)); + return $trait; + } + + + /** + * Adds an enum to the namespace. If it already exists, throws an exception. + */ + public function addEnum(string $name): EnumType { - return $this->addClass($name)->setInterface(); + $this->add($enum = new EnumType($name, $this)); + return $enum; } - public function addTrait(string $name): ClassType + /** + * Returns a class-like type from the namespace. + */ + public function getClass(string $name): ClassType|InterfaceType|TraitType|EnumType { - return $this->addClass($name)->setTrait(); + return $this->classes[strtolower($name)] ?? throw new Nette\InvalidArgumentException("Class '$name' not found."); } - /** @return ClassType[] */ + /** + * 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 e78c43cb..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,179 +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 { + $this->namespace = $this->resolveTypes ? $namespace : null; $line = 'function ' . ($function->getReturnReference() ? '&' : '') . $function->getName(); - $returnType = $this->printReturnType($function, $namespace); + $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 Helpers::formatDocComment($function->getComment() . "\n") - . self::printAttributes($function->getAttributes(), $namespace) + return $this->printDocComment($function) + . $this->printAttributes($function->getAttributes()) . $line - . $this->printParameters($function, $namespace, strlen($line) + strlen($returnType) + 2) // 2 = parentheses + . $params . $returnType - . "\n{\n" . $this->indent(ltrim(rtrim($function->getBody()) . "\n")) . "}\n"; + . ($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 self::printAttributes($closure->getAttributes(), null, true) + 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 self::printAttributes($closure->getAttributes(), null) + $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(); - $line = ($method->isAbstract() ? 'abstract ' : '') + $line = ($method->isAbstract() && !$isInterface ? 'abstract ' : '') . ($method->isFinal() ? 'final ' : '') . ($method->getVisibility() ? $method->getVisibility() . ' ' : '') . ($method->isStatic() ? 'static ' : '') . 'function ' . ($method->getReturnReference() ? '&' : '') . $method->getName(); - $returnType = $this->printReturnType($method, $namespace); + $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 Helpers::formatDocComment($method->getComment() . "\n") - . self::printAttributes($method->getAttributes(), $namespace) + return $this->printDocComment($method) + . $this->printAttributes($method->getAttributes()) . $line - . ($params = $this->printParameters($method, $namespace, strlen($line) + strlen($returnType) + strlen($this->indentation) + 2)) // 2 = parentheses + . $params . $returnType - . ($method->isAbstract() || $method->getBody() === null + . ($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"); + } + + + 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 $class, PhpNamespace $namespace = null): string + 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, 'unresolveUnionType'] - : 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()) - . self::printAttributes($const->getAttributes(), $namespace) - . $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()) - . self::printAttributes($property->getAttributes(), $namespace) - . $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") - . self::printAttributes($class->getAttributes(), $namespace) - . ($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); + $uses = [ + $this->printUses($namespace), + $this->printUses($namespace, PhpNamespace::NameFunction), + $this->printUses($namespace, PhpNamespace::NameConstant), + ]; + $uses = implode(str_repeat("\n", $this->linesBetweenUseTypes), array_filter($uses)); - $classes = []; + $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" @@ -208,137 +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; + } + } - protected function dump($var, int $column = 0): string - { - return (new Dumper)->dump($var, $column); + return $this->formatParameters($function, multiline: true); } - protected function printUses(PhpNamespace $namespace): string + private function formatParameters(Closure|GlobalFunction|Method|PropertyHook $function, bool $multiline): string { - $name = $namespace->getName(); - $uses = []; - foreach ($namespace->getUses() as $alias => $original) { - if ($original !== ($name ? $name . '\\' . $alias : $alias)) { - $uses[] = $alias === $original || substr($original, -(strlen($alias) + 1)) === '\\' . $alias - ? "use $original;" - : "use $original as $alias;"; - } + $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 implode("\n", $uses); + + return $multiline + ? "(\n" . $this->indent($res) . ')' + : '(' . substr($res, 0, -2) . ')'; } - /** - * @param Closure|GlobalFunction|Method $function - */ - public function printParameters($function, PhpNamespace $namespace = null, int $column = 0): string + private function printConstant(Constant $const): string { - $params = []; - $list = $function->getParameters(); - $special = false; + $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"; + } - foreach ($list as $param) { - $variadic = $function->isVariadic() && $param === end($list); - $type = $param->getType(); - $promoted = $param instanceof PromotedParameter ? $param : null; - $params[] = - ($promoted ? Helpers::formatDocComment((string) $promoted->getComment()) : '') - . ($attrs = self::printAttributes($param->getAttributes(), $namespace, true)) - . ($promoted ? ($promoted->getVisibility() ?: 'public') . ' ' : '') - . ltrim($this->printType($type, $param->isNullable(), $namespace) . ' ') - . ($param->isReference() ? '&' : '') - . ($variadic ? '...' : '') - . '$' . $param->getName() - . ($param->hasDefaultValue() && !$variadic ? ' = ' . $this->dump($param->getDefaultValue()) : ''); - $special = $special || $promoted || $attrs; - } + private function printProperty(Property $property, bool $readOnlyClass = false, bool $isInterface = false): string + { + $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"; + } - $line = implode(', ', $params); - return count($params) > 1 && ($special || strlen($line) + $column > (new Dumper)->wrapLength) - ? "(\n" . $this->indent(implode(",\n", $params)) . ($special ? ',' : '') . "\n)" - : "($line)"; + 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'; } - public function printType(?string $type, bool $nullable = false, PhpNamespace $namespace = null): string + protected function printType(?string $type, bool $nullable): string { if ($type === null) { return ''; } - if ($this->resolveTypes && $namespace) { - $type = $namespace->unresolveUnionType($type); - } - if ($nullable && strcasecmp($type, 'mixed')) { - $type = strpos($type, '|') === false - ? '?' . $type - : $type . '|null'; + + if ($this->namespace) { + $type = $this->namespace->simplifyType($type); } - return $type; + + return $nullable + ? Type::nullable($type) + : $type; + } + + + protected function printDocComment(/*Traits\CommentAware*/ $commentable): string + { + $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 printAttributes(array $attrs, ?PhpNamespace $namespace, bool $inline = false): string + /** @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 = (new Dumper)->format('...?:', $attr->getArguments()); - $items[] = $this->printType($attr->getName(), false, $namespace) . ($args ? "($args)" : ''); + $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 joinProperties(array $props) + 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 index 00004831..f81764ae 100644 --- a/src/PhpGenerator/PromotedParameter.php +++ b/src/PhpGenerator/PromotedParameter.php @@ -9,12 +9,27 @@ namespace Nette\PhpGenerator; +use Nette; + /** - * Promoted parameter in constructor. + * Definition of a promoted constructor parameter. */ final class PromotedParameter extends Parameter { - use Traits\VisibilityAware; - use Traits\CommentAware; + use Traits\PropertyLike; + + /** @throws Nette\InvalidStateException */ + public function validate(): void + { + if ($this->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 7d128c82..0586ee2f 100644 --- a/src/PhpGenerator/Property.php +++ b/src/PhpGenerator/Property.php @@ -10,39 +10,28 @@ 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; @@ -50,14 +39,13 @@ public function setValue($val): self } - 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; @@ -70,26 +58,23 @@ public function isStatic(): bool } - /** @return static */ - public function setType(?string $type): self + public function setType(?string $type): static { - if ($type && $type[0] === '?') { - $type = substr($type, 1); - $this->nullable = true; - } - $this->type = $type; + $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; @@ -98,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; @@ -114,4 +98,41 @@ public function isInitialized(): bool { 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 index d0195eb1..9e9d7d44 100644 --- a/src/PhpGenerator/Traits/AttributeAware.php +++ b/src/PhpGenerator/Traits/AttributeAware.php @@ -18,11 +18,11 @@ trait AttributeAware { /** @var Attribute[] */ - private $attributes = []; + private array $attributes = []; - /** @return static */ - public function addAttribute(string $name, array $args = []): self + /** @param mixed[] $args */ + public function addAttribute(string $name, array $args = []): static { $this->attributes[] = new Attribute($name, $args); return $this; @@ -30,10 +30,10 @@ public function addAttribute(string $name, array $args = []): self /** + * Replaces all attributes. * @param Attribute[] $attrs - * @return static */ - public function setAttributes(array $attrs): self + public function setAttributes(array $attrs): static { (function (Attribute ...$attrs) {})(...$attrs); $this->attributes = $attrs; 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 7697c823..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,27 +22,22 @@ */ trait FunctionLike { - /** @var string */ - private $body = ''; + private string $body = ''; /** @var Parameter[] */ - private $parameters = []; + private array $parameters = []; + private bool $variadic = false; + private ?string $returnType = null; + private bool $returnReference = false; + private bool $returnNullable = false; - /** @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 + /** @param ?mixed[] $args */ + public function setBody( + #[Language('PHP')] + string $code, + ?array $args = null, + ): static { $this->body = $args === null ? $code @@ -54,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; @@ -64,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; } @@ -86,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; @@ -124,26 +136,23 @@ public function isVariadic(): bool } - /** @return static */ - public function setReturnType(?string $type): self + public function setReturnType(?string $type): static { - if ($type && $type[0] === '?') { - $type = substr($type, 1); - $this->returnNullable = true; - } - $this->returnType = $type; + $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; @@ -156,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; @@ -168,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 index 42d2991f..c2dc1c76 100644 --- a/tests/PhpGenerator/ClassType.attributes.phpt +++ b/tests/PhpGenerator/ClassType.attributes.phpt @@ -7,8 +7,7 @@ declare(strict_types=1); use Nette\PhpGenerator\ClassType; -use Nette\PhpGenerator\PhpLiteral; - +use Nette\PhpGenerator\Literal; require __DIR__ . '/../bootstrap.php'; @@ -18,8 +17,13 @@ $class = new ClassType('Example'); $class ->addComment('Description of class.') ->addAttribute('ExampleAttribute') - ->addAttribute('WithArgument', [new PhpLiteral('Foo::BAR')]) - ->addAttribute('NamedArguments', ['foo' => 'bar', 'bar' => [1, 2, 3]]); + ->addAttribute('WithArgument', [new Literal('Foo::BAR')]) + ->addAttribute('Table', [ + 'name' => 'user', + 'constraints' => [ + Literal::new('UniqueConstraint', ['name' => 'ean', 'columns' => ['ean']]), + ], + ]); $class->addConstant('FOO', 123) ->addComment('Commented') @@ -35,7 +39,8 @@ $method = $class->addMethod('getHandle') ->addAttribute('ExampleAttribute'); $method->addParameter('mode') + ->addComment('comment') ->addAttribute('ExampleAttribute') - ->addAttribute('WithArguments', [123]); + ->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 { +Assert::exception( + fn() => ClassType::from(new class { public function f() { } - }); -}, Nette\NotSupportedException::class, 'Anonymous classes are not supported.'); + }, 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 2daa5b60..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'; // triggers error 'Required parameter $c follows optional parameter $a' +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 7db53c93..1f62a4b0 100644 --- a/tests/PhpGenerator/ClassType.from.trait.phpt +++ b/tests/PhpGenerator/ClassType.from.trait.phpt @@ -1,36 +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[] = ClassType::withBodiesFrom('Class4'); -$res[] = ClassType::withBodiesFrom('Class5'); +$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 53379b23..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,18 +59,21 @@ $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) + ->setType(Type::Array) ->setValue(null); $p = $class->addProperty('sections', ['first' => true]) @@ -84,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')); @@ -98,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()); @@ -116,20 +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::union(Type::ARRAY, 'null')); + ->setReference() + ->setType(Type::union(Type::Array, 'null')); $method->addParameter('bar', null) - ->setType('stdClass|string') - ->setNullable(true); + ->setNullable() + ->setType('stdClass|string'); $class->addTrait('foo'); $class->removeTrait('foo'); @@ -137,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); @@ -158,10 +175,62 @@ $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 @@ -180,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 index be063b1e..06649810 100644 --- a/tests/PhpGenerator/ClassType.promotion.phpt +++ b/tests/PhpGenerator/ClassType.promotion.phpt @@ -3,7 +3,7 @@ declare(strict_types=1); use Nette\PhpGenerator\ClassType; - +use Nette\PhpGenerator\Literal; require __DIR__ . '/../bootstrap.php'; @@ -18,4 +18,8 @@ $method->addPromotedParameter('c') ->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.attributes.80.phpt b/tests/PhpGenerator/Closure.from.phpt similarity index 66% rename from tests/PhpGenerator/Closure.attributes.80.phpt rename to tests/PhpGenerator/Closure.from.phpt index 918d9b9b..aab8faba 100644 --- a/tests/PhpGenerator/Closure.attributes.80.phpt +++ b/tests/PhpGenerator/Closure.from.phpt @@ -1,14 +1,9 @@ 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, ); @@ -36,10 +37,12 @@ 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, ); @@ -58,10 +61,12 @@ $function ->addUse('this'); same( - 'function () use ($this): array { - return []; -}', - (string) $function + <<<'XX' + function () use ($this): array { + return []; + } + XX, + (string) $function, ); @@ -71,18 +76,31 @@ $function->setBody('return $a + $b;'); $function->addAttribute('ExampleAttribute'); same( - '#[ExampleAttribute] function () { - return $a + $b; -}', - (string) $function + <<<'XX' + #[ExampleAttribute] function () { + return $a + $b; + } + XX, + (string) $function, ); -$closure = function (stdClass $a, $b = null) {}; -$function = Closure::from($closure); +$function = new Closure; +$function->setBody('return $a + $b;'); +$function->addAttribute('Foo', ['a', str_repeat('b', 120)]); +$function->addAttribute('Bar'); + 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 ede092ca..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,56 +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([])); -// internal classes -Assert::same('[$s]', $dumper->dump([new PhpLiteral('$s')])); -same('[ - function () { - return 1; - }, -]', $dumper->dump([(new Nette\PhpGenerator\Closure)->setBody('return 1;')])); +// 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', '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'])); -$dumper->wrapLength = 100; -same("[ - [ - 'a', - 'loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong', - ], -]", $dumper->dump([['a', 'loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong']])); - -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; @@ -90,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) . ';')); @@ -115,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'])), +); - public function serialize() +Assert::exception(function () { + $dumper = new Dumper; + $dumper->dump(function () {}); +}, Nette\InvalidStateException::class, 'Cannot dump object of type Closure.'); + + + +// __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 9d9ffb14..35b0c870 100644 --- a/tests/PhpGenerator/Dumper.dump().wrap.phpt +++ b/tests/PhpGenerator/Dumper.dump().wrap.phpt @@ -7,63 +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; +$dumper->wrapLength = 28; same( - "[ - 'a' => [1, 2, 3], - 'aaaaaaaaa' => [ - 1, - 2, - 3, - ], -]", + <<<'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, - ], -]", + <<<'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, - ], -]", + <<<'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 2ab3df99..c8b79fde 100644 --- a/tests/PhpGenerator/Dumper.format().phpt +++ b/tests/PhpGenerator/Dumper.format().phpt @@ -9,13 +9,14 @@ 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])); @@ -25,36 +26,38 @@ 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.attributes.80.phpt b/tests/PhpGenerator/GlobalFunction.attributes.80.phpt deleted file mode 100644 index 9fc459ff..00000000 --- a/tests/PhpGenerator/GlobalFunction.attributes.80.phpt +++ /dev/null @@ -1,32 +0,0 @@ -addAttribute('ExampleAttribute'); $function->addComment('My Function'); same( - '/** - * My Function - */ -#[ExampleAttribute] -function test() -{ - return $a + $b; -} -', - (string) $function -); - - -/** global */ -function func(stdClass $a, $b = null) -{ - echo sprintf('hello, %s', 'world'); - return 1; -} - - -$function = GlobalFunction::from('func'); -same( - '/** - * global - */ -function func(stdClass $a, $b = null) -{ -} -', - (string) $function + <<<'XX' + /** + * My Function + */ + #[ExampleAttribute] + function test() + { + return $a + $b; + } + + XX, + (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); 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,12 +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 ec2deef4..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,14 +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 3de0c581..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,80 +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 index 16d46663..6854b7b3 100644 --- a/tests/PhpGenerator/PhpFile.addNamespace.phpt +++ b/tests/PhpGenerator/PhpFile.addNamespace.phpt @@ -4,7 +4,6 @@ declare(strict_types=1); use Nette\PhpGenerator\PhpFile; use Nette\PhpGenerator\PhpNamespace; - require __DIR__ . '/../bootstrap.php'; $namespace = new PhpNamespace('Foo'); @@ -15,11 +14,16 @@ $phpFile->addNamespace('Foo'); $phpFile->addNamespace($namespace); // overwrite -same('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 6228a38e..3b352de4 100644 --- a/tests/PhpGenerator/PhpNamespace.phpt +++ b/tests/PhpGenerator/PhpNamespace.phpt @@ -1,87 +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')); - -Assert::same('A', $namespace->unresolveUnionType('foo\A')); -Assert::same('null|A', $namespace->unresolveUnionType('null|foo\A')); -Assert::same('', $namespace->unresolveUnionType('')); - -$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'."); -$classA - ->addImplement('Foo\\A') - ->addImplement('Bar\\C') - ->addTrait('Bar\\D') - ->addAttribute('Foo\\A'); +$function = $namespace->addFunction('foo'); -$method = $classA->addMethod('test'); -$method->addAttribute('Foo\\A'); -$method->setReturnType('static|Foo\\A'); +Assert::exception( + fn() => $namespace->addFunction('Foo'), + Nette\InvalidStateException::class, + "Cannot add 'Foo', because it already exists.", +); -$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'); +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 f7ddcccb..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'; 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 index e3b5e82a..434918ec 100644 --- a/tests/PhpGenerator/expected/ClassType.attributes.expect +++ b/tests/PhpGenerator/expected/ClassType.attributes.expect @@ -3,13 +3,13 @@ */ #[ExampleAttribute] #[WithArgument(Foo::BAR)] -#[NamedArguments(foo: 'bar', bar: [1, 2, 3])] +#[Table(name: 'user', constraints: [new UniqueConstraint(name: 'ean', columns: ['ean'])])] class Example { /** Commented */ #[ExampleAttribute] #[WithArguments(true)] - const FOO = 123; + public const FOO = 123; /** @var resource */ #[ExampleAttribute] @@ -20,7 +20,10 @@ class Example * Returns file handle. */ #[ExampleAttribute] - public function getHandle(#[ExampleAttribute, WithArguments(123)] $mode) - { + 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 5f837efa..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,9 +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 ?array $typed3 = null; public static $sections = ['first' => true]; @@ -43,5 +48,10 @@ abstract class Example extends ParentClass implements IExample, IOne } - abstract public function show($item, array|null &$res = null, stdClass|string|null $bar = 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.80.expect b/tests/PhpGenerator/expected/ClassType.from.80.expect deleted file mode 100644 index 06afaec1..00000000 --- a/tests/PhpGenerator/expected/ClassType.from.80.expect +++ /dev/null @@ -1,45 +0,0 @@ -class Class8 -{ - public function __construct( - public $a, - public 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.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 8aeb3f8d..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,15 +50,13 @@ 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) + { } @@ -71,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 = []) { } @@ -92,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-use.bodies.expect b/tests/PhpGenerator/expected/ClassType.from.trait-use.bodies.expect new file mode 100644 index 00000000..bdd46f66 --- /dev/null +++ b/tests/PhpGenerator/expected/ClassType.from.trait-use.bodies.expect @@ -0,0 +1,95 @@ +/** + * Trait1 + */ +trait Trait1 +{ + public static $s1; + public $x1; + + + public function f1() + { + echo 'Trait1::f1'; + } +} + +trait Trait1b +{ + public function f1() + { + echo 'Trait1b::f1'; + } +} + +trait Trait2 +{ + use Trait1; + + protected $x2; + + + public function f2() + { + echo 'Trait2::f2'; + } +} + +class ParentClass +{ + public $x1; + + + public function f1() + { + echo 'ParentClass::f1'; + } +} + +class Class1 extends ParentClass +{ + use Trait2; +} + +class Class2 extends ParentClass +{ + use Trait2; + + public function f1() + { + echo 'Class2::f1'; + } +} + +class Class3 extends ParentClass +{ + use Trait2 { + f1 as protected aliased; + } + + /** info */ + public $x1; + + + public function f1() + { + echo 'Class3::f1'; + } +} + +class Class4 extends ParentClass +{ + use Trait2; + + public function aliased() + { + echo 'Class4::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 ea2c3cd0..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,69 +31,58 @@ class Class1 extends ParentClass } } -class Class2 extends ParentClass +class ParentClass { public $x1; - protected $x2; public function f1() { } - - - public function f2() - { - } } -class Class3 extends ParentClass +class Class1 extends ParentClass { - public $x1; - protected $x2; + use Trait2; +} +class Class2 extends ParentClass +{ + use Trait2; public function f1() { } +} - - public function f2() - { +class Class3 extends ParentClass +{ + use Trait2 { + f1 as protected aliased; } + /** info */ + public $x1; - public function aliased() + + public function f1() { } } class Class4 extends ParentClass { - protected $x2; - + use Trait2; public function aliased() { } - - - public function f1() - { - } - - - public function f2() - { - } } class Class5 { - public $x1; - - - public function f1() - { + use Trait1 { + f1 as private; } + use Trait1b; } diff --git a/tests/PhpGenerator/expected/ClassType.promotion.expect b/tests/PhpGenerator/expected/ClassType.promotion.expect index b4a90616..78700ad6 100644 --- a/tests/PhpGenerator/expected/ClassType.promotion.expect +++ b/tests/PhpGenerator/expected/ClassType.promotion.expect @@ -4,7 +4,9 @@ class Example $a, public $b, /** promo */ - #[Example] private string $c, + #[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; + } }