From 8c0e83702119485abf475140abe63fb84914d74c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Wed, 10 Apr 2024 11:56:10 +0200 Subject: [PATCH 1/3] WIP: fix polyfill declaration Closes #965. --- .makefile/e2e.file | 18 +++++++++++++ fixtures/set040-polyfills/bootstrap.php | 15 +++++++++++ fixtures/set040-polyfills/composer.json | 11 ++++++++ fixtures/set040-polyfills/composer.lock | 18 +++++++++++++ fixtures/set040-polyfills/expected-output | 1 + fixtures/set040-polyfills/index.php | 27 +++++++++++++++++++ fixtures/set040-polyfills/scoper.inc.php | 14 ++++++++++ fixtures/set040-polyfills/src/Php20.php | 17 ++++++++++++ .../set040-polyfills/stubs/NewPhp20Class.php | 7 +++++ .../stubs/NewPhp20Interface.php | 7 +++++ 10 files changed, 135 insertions(+) create mode 100644 fixtures/set040-polyfills/bootstrap.php create mode 100644 fixtures/set040-polyfills/composer.json create mode 100644 fixtures/set040-polyfills/composer.lock create mode 100644 fixtures/set040-polyfills/expected-output create mode 100644 fixtures/set040-polyfills/index.php create mode 100644 fixtures/set040-polyfills/scoper.inc.php create mode 100644 fixtures/set040-polyfills/src/Php20.php create mode 100644 fixtures/set040-polyfills/stubs/NewPhp20Class.php create mode 100644 fixtures/set040-polyfills/stubs/NewPhp20Interface.php diff --git a/.makefile/e2e.file b/.makefile/e2e.file index a19531a4..d6f5e7db 100644 --- a/.makefile/e2e.file +++ b/.makefile/e2e.file @@ -431,6 +431,24 @@ e2e_039: $(PHP_SCOPER_PHAR_BIN) > build/set039/output || true diff fixtures/set039-min-php-version/expected-output build/set039/output +.PHONY: e2e_040 +e2e_040: # Runs end-to-end tests for the fixture set e2e_040 — Codebase using a polyfill +e2e_040: $(PHP_SCOPER_PHAR_BIN) + rm -rf fixtures/set040-polyfills/vendor || true + composer --working-dir=fixtures/set040-polyfills dump-autoload + + $(PHP_SCOPER_PHAR) add-prefix . \ + --working-dir=fixtures/set040-polyfills \ + --output-dir=../../build/set040 \ + --force \ + --no-interaction \ + --stop-on-failure + composer --working-dir=build/set040 dump-autoload + + php build/set040/index.php > build/set040/output || true + + diff fixtures/set040-polyfills/expected-output build/set040/output + # # Rules from files diff --git a/fixtures/set040-polyfills/bootstrap.php b/fixtures/set040-polyfills/bootstrap.php new file mode 100644 index 00000000..d2ef0fb5 --- /dev/null +++ b/fixtures/set040-polyfills/bootstrap.php @@ -0,0 +1,15 @@ += 200_000) { + return; +} + +if (!defined('NEW_PHP20_CONSTANT')) { + define('NEW_PHP20_CONSTANT', 42); +} + +if (!function_exists('new_php20_function')) { + function new_php20_function(bool $echo = false): void { Php20::new_php20_function($echo); } +} diff --git a/fixtures/set040-polyfills/composer.json b/fixtures/set040-polyfills/composer.json new file mode 100644 index 00000000..674008b2 --- /dev/null +++ b/fixtures/set040-polyfills/composer.json @@ -0,0 +1,11 @@ +{ + "bin": "index.php", + "autoload": { + "files": ["bootstrap.php"], + "classmap": ["stubs"], + "psr-4": { + "Set040\\Polyfill\\": "src/", + "Set040\\": "src/" + } + } +} diff --git a/fixtures/set040-polyfills/composer.lock b/fixtures/set040-polyfills/composer.lock new file mode 100644 index 00000000..ba6fe47d --- /dev/null +++ b/fixtures/set040-polyfills/composer.lock @@ -0,0 +1,18 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "d751713988987e9331980363e24189ce", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/fixtures/set040-polyfills/expected-output b/fixtures/set040-polyfills/expected-output new file mode 100644 index 00000000..d5c32f4a --- /dev/null +++ b/fixtures/set040-polyfills/expected-output @@ -0,0 +1 @@ +OK. diff --git a/fixtures/set040-polyfills/index.php b/fixtures/set040-polyfills/index.php new file mode 100644 index 00000000..69df1326 --- /dev/null +++ b/fixtures/set040-polyfills/index.php @@ -0,0 +1,27 @@ + [ + 'NewPhp20Interface', + 'NewPhp20Class', + ], + 'exclude-functions' => [ + 'new_php20_function', + ], + 'exclude-constants' => [ + 'NEW_PHP20_CONSTANT', + ], +]; diff --git a/fixtures/set040-polyfills/src/Php20.php b/fixtures/set040-polyfills/src/Php20.php new file mode 100644 index 00000000..fc794e0d --- /dev/null +++ b/fixtures/set040-polyfills/src/Php20.php @@ -0,0 +1,17 @@ + Date: Wed, 10 Apr 2024 22:38:58 +0200 Subject: [PATCH 2/3] more fixes --- specs/class/conditional.php | 2 + specs/class/interface.php | 48 +++ specs/class/nested-declarations.php | 295 ++++++++++++++++++ specs/class/regular.php | 31 ++ .../NodeVisitor/ClassAliasStmtAppender.php | 72 ++++- 5 files changed, 433 insertions(+), 15 deletions(-) create mode 100644 specs/class/nested-declarations.php diff --git a/specs/class/conditional.php b/specs/class/conditional.php index 1ebebfeb..6eadff76 100644 --- a/specs/class/conditional.php +++ b/specs/class/conditional.php @@ -74,6 +74,7 @@ class A {} class A { } + \class_alias('Humbug\\A', 'A', \false); } PHP, @@ -122,6 +123,7 @@ class A {} class A { } + \class_alias('Humbug\\Foo\\A', 'Foo\\A', \false); } PHP, diff --git a/specs/class/interface.php b/specs/class/interface.php index a4393b93..f9dfeae9 100644 --- a/specs/class/interface.php +++ b/specs/class/interface.php @@ -62,6 +62,54 @@ public function a(); PHP, + 'Declaration of an internal interface' => [ + 'exclude-classes' => ['NewPhp20Interface'], + 'expected-recorded-classes' => [ + ['NewPhp20Interface', 'Humbug\NewPhp20Interface'], + ], + 'payload' => <<<'PHP' + [ + 'exclude-classes' => ['NewPhp20Interface'], + 'expected-recorded-classes' => [ + ['NewPhp20Interface', 'Humbug\NewPhp20Interface'], + ], + 'payload' => <<<'PHP' + [ 'expose-global-classes' => true, 'expected-recorded-classes' => [ diff --git a/specs/class/nested-declarations.php b/specs/class/nested-declarations.php new file mode 100644 index 00000000..284bca45 --- /dev/null +++ b/specs/class/nested-declarations.php @@ -0,0 +1,295 @@ +, + * Pádraic Brady + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return [ + 'meta' => [ + 'title' => 'Exposed class declaration', + // Default values. If not specified will be the one used + 'prefix' => 'Humbug', + + 'expose-global-constants' => false, + 'expose-global-classes' => false, + 'expose-global-functions' => false, + 'expose-namespaces' => [], + 'expose-constants' => [], + 'expose-classes' => ['A'], + 'expose-functions' => [], + + 'exclude-namespaces' => [], + 'exclude-constants' => [], + 'exclude-classes' => [], + 'exclude-functions' => [], + + 'expected-recorded-classes' => [ + ['A', 'Humbug\A'], + ], + 'expected-recorded-functions' => [], + ], + + 'Exposed class within an if block' => <<<'PHP' + <<<'PHP' + <<<'PHP' + <<<'PHP' + <<<'PHP' + <<<'PHP' + <<<'PHP' + <<<'PHP' + <<<'PHP' + [ + 'expose-global-classes' => true, + 'expected-recorded-classes' => [ + ['A', 'Humbug\A'], + ], + 'payload' => <<<'PHP' + [ 'expected-recorded-classes' => [ ['Normalizer', 'Humbug\Normalizer'], diff --git a/src/PhpParser/NodeVisitor/ClassAliasStmtAppender.php b/src/PhpParser/NodeVisitor/ClassAliasStmtAppender.php index 1c994feb..bb8d02a3 100644 --- a/src/PhpParser/NodeVisitor/ClassAliasStmtAppender.php +++ b/src/PhpParser/NodeVisitor/ClassAliasStmtAppender.php @@ -27,7 +27,9 @@ use PhpParser\Node\Stmt\Interface_; use PhpParser\Node\Stmt\Namespace_; use PhpParser\NodeVisitorAbstract; +use function array_map; use function array_reduce; +use function in_array; /** * Appends a `class_alias` statement to the exposed classes. @@ -64,28 +66,44 @@ public function __construct( public function afterTraverse(array $nodes): array { - $newNodes = []; + $this->traverseNodes($nodes); + return $nodes; + } + + /** + * @param Node[] $nodes + */ + private function traverseNodes(array $nodes): void + { foreach ($nodes as $node) { - if ($node instanceof Namespace_) { - $node = $this->appendToNamespaceStmt($node); + if (self::isNodeAStatementWithStatements($node)) { + $this->updateStatements($node); } - - $newNodes[] = $node; } + } - return $newNodes; + private static function isNodeAStatementWithStatements(Node $node): bool + { + return $node instanceof Stmt && in_array('stmts', $node->getSubNodeNames(), true); } - private function appendToNamespaceStmt(Namespace_ $namespace): Namespace_ + /** + * @template T of Stmt + * + * @param T|null $statement + */ + private function updateStatements(?Stmt $statement): void { - $namespace->stmts = array_reduce( - $namespace->stmts, - fn (array $stmts, Stmt $stmt) => $this->createNamespaceStmts($stmts, $stmt), + if (null === $statement || null === $statement->stmts) { + return; + } + + $statement->stmts = array_reduce( + $statement->stmts, + fn (array $stmts, Stmt $stmt) => $this->appendClassAliasStmtIfApplicable($stmts, $stmt), [], ); - - return $namespace; } /** @@ -93,16 +111,40 @@ private function appendToNamespaceStmt(Namespace_ $namespace): Namespace_ * * @return Stmt[] */ - private function createNamespaceStmts(array $stmts, Stmt $stmt): array + private function appendClassAliasStmtIfApplicable(array $stmts, Stmt $stmt): array { $stmts[] = $stmt; $isClassOrInterface = $stmt instanceof Class_ || $stmt instanceof Interface_; - if (!$isClassOrInterface) { - return $stmts; + if ($isClassOrInterface) { + return $this->appendClassAliasStmtIfNecessary($stmts, $stmt); + } + + if (self::isNodeAStatementWithStatements($stmt)) { + $this->updateStatements($stmt); } + if ($stmt instanceof Stmt\If_) { + $this->updateStatements($stmt->else); + $this->traverseNodes($stmt->elseifs); + } elseif ($stmt instanceof Stmt\Switch_) { + $this->traverseNodes($stmt->cases); + } elseif ($stmt instanceof Stmt\TryCatch) { + $this->traverseNodes($stmt->catches ?? []); + $this->updateStatements($stmt->finally); + } + + return $stmts; + } + + /** + * @param Stmt[] $stmts + * + * @return Stmt[] + */ + private function appendClassAliasStmtIfNecessary(array $stmts, Class_|Interface_ $stmt): array + { $name = $stmt->name; if (null === $name) { From 5ffa3a26238d5943c1c91de21eb4a5720335b819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Thu, 11 Apr 2024 01:13:52 +0200 Subject: [PATCH 3/3] fix --- .github/workflows/e2e-tests.yaml | 1 + Makefile | 3 ++- phpstan.neon.dist | 2 ++ .../NodeVisitor/ClassAliasStmtAppender.php | 16 ++++++++++------ 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/e2e-tests.yaml index 3aca4599..b336886c 100644 --- a/.github/workflows/e2e-tests.yaml +++ b/.github/workflows/e2e-tests.yaml @@ -87,6 +87,7 @@ jobs: - 'e2e_035' - 'e2e_036' - 'e2e_037' + - 'e2e_040' php: - '8.2' - '8.3' diff --git a/Makefile b/Makefile index 7255f9af..1e3aa14b 100644 --- a/Makefile +++ b/Makefile @@ -168,7 +168,8 @@ e2e: e2e_004 \ e2e_036 \ e2e_037 \ e2e_038 \ - e2e_039 + e2e_039 \ + e2e_040 .PHONY: blackfire blackfire: ## Runs Blackfire profiling diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 5a2aa3f6..a517e336 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -60,3 +60,5 @@ parameters: path: 'tests/Console/Command/AddInspectCommandIntegrationTest.php' - message: '#unserialize#' path: 'src/Symbol/SymbolsRegistry.php' + - message: '#Stmt\:\:\$stmts#' + path: 'src/PhpParser/NodeVisitor/ClassAliasStmtAppender.php' diff --git a/src/PhpParser/NodeVisitor/ClassAliasStmtAppender.php b/src/PhpParser/NodeVisitor/ClassAliasStmtAppender.php index bb8d02a3..531e0651 100644 --- a/src/PhpParser/NodeVisitor/ClassAliasStmtAppender.php +++ b/src/PhpParser/NodeVisitor/ClassAliasStmtAppender.php @@ -24,10 +24,11 @@ use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\Expression; +use PhpParser\Node\Stmt\If_; use PhpParser\Node\Stmt\Interface_; -use PhpParser\Node\Stmt\Namespace_; +use PhpParser\Node\Stmt\Switch_; +use PhpParser\Node\Stmt\TryCatch; use PhpParser\NodeVisitorAbstract; -use function array_map; use function array_reduce; use function in_array; @@ -83,6 +84,9 @@ private function traverseNodes(array $nodes): void } } + /** + * @phpstan-assert-if-true Stmt $node + */ private static function isNodeAStatementWithStatements(Node $node): bool { return $node instanceof Stmt && in_array('stmts', $node->getSubNodeNames(), true); @@ -125,13 +129,13 @@ private function appendClassAliasStmtIfApplicable(array $stmts, Stmt $stmt): arr $this->updateStatements($stmt); } - if ($stmt instanceof Stmt\If_) { + if ($stmt instanceof If_) { $this->updateStatements($stmt->else); $this->traverseNodes($stmt->elseifs); - } elseif ($stmt instanceof Stmt\Switch_) { + } elseif ($stmt instanceof Switch_) { $this->traverseNodes($stmt->cases); - } elseif ($stmt instanceof Stmt\TryCatch) { - $this->traverseNodes($stmt->catches ?? []); + } elseif ($stmt instanceof TryCatch) { + $this->traverseNodes($stmt->catches); $this->updateStatements($stmt->finally); }