diff --git a/composer.json b/composer.json
index 80c92b017..0017d1195 100644
--- a/composer.json
+++ b/composer.json
@@ -24,12 +24,12 @@
"nette/robot-loader": "^4.1",
"phpecs/phpecs": "^2.2",
"phpstan/extension-installer": "^1.4.3",
- "phpstan/phpstan": "^2.1.32",
+ "phpstan/phpstan": "^2.1.33",
"phpstan/phpstan-webmozart-assert": "^2.0",
- "phpunit/phpunit": "^12.4",
+ "phpunit/phpunit": "^12.5",
"rector/jack": "^0.4",
"rector/swiss-knife": "^2.3.3",
- "tomasvotruba/class-leak": "^2.0.5"
+ "tomasvotruba/class-leak": "^2.1"
},
"autoload": {
"psr-4": {
diff --git a/composer.lock b/composer.lock
index ea1625aa5..5f1412b47 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "b87d1cb1266b6be20605dd3ec29d8151",
+ "content-hash": "a967275501c77c1e7789c741c9c25676",
"packages": [
{
"name": "brick/math",
@@ -633,31 +633,31 @@
},
{
"name": "fruitcake/php-cors",
- "version": "v1.3.0",
+ "version": "v1.4.0",
"source": {
"type": "git",
"url": "https://github.com/fruitcake/php-cors.git",
- "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b"
+ "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b",
- "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b",
+ "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379",
+ "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379",
"shasum": ""
},
"require": {
- "php": "^7.4|^8.0",
- "symfony/http-foundation": "^4.4|^5.4|^6|^7"
+ "php": "^8.1",
+ "symfony/http-foundation": "^5.4|^6.4|^7.3|^8"
},
"require-dev": {
- "phpstan/phpstan": "^1.4",
+ "phpstan/phpstan": "^2",
"phpunit/phpunit": "^9",
- "squizlabs/php_codesniffer": "^3.5"
+ "squizlabs/php_codesniffer": "^4"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.2-dev"
+ "dev-master": "1.3-dev"
}
},
"autoload": {
@@ -688,7 +688,7 @@
],
"support": {
"issues": "https://github.com/fruitcake/php-cors/issues",
- "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0"
+ "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0"
},
"funding": [
{
@@ -700,7 +700,7 @@
"type": "github"
}
],
- "time": "2023-10-12T05:21:21+00:00"
+ "time": "2025-12-03T09:33:47+00:00"
},
{
"name": "graham-campbell/result-type",
@@ -1288,16 +1288,16 @@
},
{
"name": "laravel/framework",
- "version": "v12.40.2",
+ "version": "v12.41.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "1ccd99220b474500e672b373f32bd709ec38de50"
+ "reference": "3e229b05935fd0300c632fb1f718c73046d664fc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/1ccd99220b474500e672b373f32bd709ec38de50",
- "reference": "1ccd99220b474500e672b373f32bd709ec38de50",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/3e229b05935fd0300c632fb1f718c73046d664fc",
+ "reference": "3e229b05935fd0300c632fb1f718c73046d664fc",
"shasum": ""
},
"require": {
@@ -1503,7 +1503,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-11-26T19:24:25+00:00"
+ "time": "2025-12-03T01:02:13+00:00"
},
{
"name": "laravel/prompts",
@@ -2186,16 +2186,16 @@
},
{
"name": "livewire/livewire",
- "version": "v3.7.0",
+ "version": "v3.7.1",
"source": {
"type": "git",
"url": "https://github.com/livewire/livewire.git",
- "reference": "f5f9efe6d5a7059116bd695a89d95ceedf33f3cb"
+ "reference": "214da8f3a1199a88b56ab2fe901d4a607f784805"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/livewire/livewire/zipball/f5f9efe6d5a7059116bd695a89d95ceedf33f3cb",
- "reference": "f5f9efe6d5a7059116bd695a89d95ceedf33f3cb",
+ "url": "https://api.github.com/repos/livewire/livewire/zipball/214da8f3a1199a88b56ab2fe901d4a607f784805",
+ "reference": "214da8f3a1199a88b56ab2fe901d4a607f784805",
"shasum": ""
},
"require": {
@@ -2250,7 +2250,7 @@
"description": "A front-end framework for Laravel.",
"support": {
"issues": "https://github.com/livewire/livewire/issues",
- "source": "https://github.com/livewire/livewire/tree/v3.7.0"
+ "source": "https://github.com/livewire/livewire/tree/v3.7.1"
},
"funding": [
{
@@ -2258,7 +2258,7 @@
"type": "github"
}
],
- "time": "2025-11-12T17:58:16+00:00"
+ "time": "2025-12-03T22:41:13+00:00"
},
{
"name": "monolog/monolog",
@@ -2365,16 +2365,16 @@
},
{
"name": "nesbot/carbon",
- "version": "3.10.3",
+ "version": "3.11.0",
"source": {
"type": "git",
"url": "https://github.com/CarbonPHP/carbon.git",
- "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f"
+ "reference": "bdb375400dcd162624531666db4799b36b64e4a1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f",
- "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f",
+ "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/bdb375400dcd162624531666db4799b36b64e4a1",
+ "reference": "bdb375400dcd162624531666db4799b36b64e4a1",
"shasum": ""
},
"require": {
@@ -2382,9 +2382,9 @@
"ext-json": "*",
"php": "^8.1",
"psr/clock": "^1.0",
- "symfony/clock": "^6.3.12 || ^7.0",
+ "symfony/clock": "^6.3.12 || ^7.0 || ^8.0",
"symfony/polyfill-mbstring": "^1.0",
- "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0"
+ "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0"
},
"provide": {
"psr/clock-implementation": "1.0"
@@ -2466,7 +2466,7 @@
"type": "tidelift"
}
],
- "time": "2025-09-06T13:39:36+00:00"
+ "time": "2025-12-02T21:04:28+00:00"
},
{
"name": "nette/schema",
@@ -2535,20 +2535,20 @@
},
{
"name": "nette/utils",
- "version": "v4.0.9",
+ "version": "v4.1.0",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
- "reference": "505a30ad386daa5211f08a318e47015b501cad30"
+ "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nette/utils/zipball/505a30ad386daa5211f08a318e47015b501cad30",
- "reference": "505a30ad386daa5211f08a318e47015b501cad30",
+ "url": "https://api.github.com/repos/nette/utils/zipball/fa1f0b8261ed150447979eb22e373b7b7ad5a8e0",
+ "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0",
"shasum": ""
},
"require": {
- "php": "8.0 - 8.5"
+ "php": "8.2 - 8.5"
},
"conflict": {
"nette/finder": "<3",
@@ -2571,7 +2571,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "4.0-dev"
+ "dev-master": "4.1-dev"
}
},
"autoload": {
@@ -2618,9 +2618,9 @@
],
"support": {
"issues": "https://github.com/nette/utils/issues",
- "source": "https://github.com/nette/utils/tree/v4.0.9"
+ "source": "https://github.com/nette/utils/tree/v4.1.0"
},
- "time": "2025-10-31T00:45:47+00:00"
+ "time": "2025-12-01T17:49:23+00:00"
},
{
"name": "nikic/php-parser",
@@ -3568,16 +3568,16 @@
},
{
"name": "samsonasik/array-lookup",
- "version": "2.0.3",
+ "version": "2.1.0",
"source": {
"type": "git",
"url": "https://github.com/samsonasik/ArrayLookup.git",
- "reference": "3b815db0c4d1847e900ca2548b1e9566c5f808f5"
+ "reference": "e61c61c732eeb4607768ecc8e212ecf830e0f401"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/samsonasik/ArrayLookup/zipball/3b815db0c4d1847e900ca2548b1e9566c5f808f5",
- "reference": "3b815db0c4d1847e900ca2548b1e9566c5f808f5",
+ "url": "https://api.github.com/repos/samsonasik/ArrayLookup/zipball/e61c61c732eeb4607768ecc8e212ecf830e0f401",
+ "reference": "e61c61c732eeb4607768ecc8e212ecf830e0f401",
"shasum": ""
},
"require": {
@@ -3622,7 +3622,7 @@
],
"support": {
"issues": "https://github.com/samsonasik/ArrayLookup/issues",
- "source": "https://github.com/samsonasik/ArrayLookup/tree/2.0.3"
+ "source": "https://github.com/samsonasik/ArrayLookup/tree/2.1.0"
},
"funding": [
{
@@ -3630,26 +3630,25 @@
"type": "github"
}
],
- "time": "2025-06-25T02:38:52+00:00"
+ "time": "2025-12-04T22:51:39+00:00"
},
{
"name": "symfony/clock",
- "version": "v7.4.0",
+ "version": "v8.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/clock.git",
- "reference": "9169f24776edde469914c1e7a1442a50f7a4e110"
+ "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/clock/zipball/9169f24776edde469914c1e7a1442a50f7a4e110",
- "reference": "9169f24776edde469914c1e7a1442a50f7a4e110",
+ "url": "https://api.github.com/repos/symfony/clock/zipball/832119f9b8dbc6c8e6f65f30c5969eca1e88764f",
+ "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f",
"shasum": ""
},
"require": {
- "php": ">=8.2",
- "psr/clock": "^1.0",
- "symfony/polyfill-php83": "^1.28"
+ "php": ">=8.4",
+ "psr/clock": "^1.0"
},
"provide": {
"psr/clock-implementation": "1.0"
@@ -3688,7 +3687,7 @@
"time"
],
"support": {
- "source": "https://github.com/symfony/clock/tree/v7.4.0"
+ "source": "https://github.com/symfony/clock/tree/v8.0.0"
},
"funding": [
{
@@ -3708,7 +3707,7 @@
"type": "tidelift"
}
],
- "time": "2025-11-12T15:39:26+00:00"
+ "time": "2025-11-12T15:46:48+00:00"
},
{
"name": "symfony/console",
@@ -5857,34 +5856,27 @@
},
{
"name": "symfony/translation",
- "version": "v7.4.0",
+ "version": "v8.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
- "reference": "2d01ca0da3f092f91eeedb46f24aa30d2fca8f68"
+ "reference": "82ab368a6fca6358d995b6dd5c41590fb42c03e6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/translation/zipball/2d01ca0da3f092f91eeedb46f24aa30d2fca8f68",
- "reference": "2d01ca0da3f092f91eeedb46f24aa30d2fca8f68",
+ "url": "https://api.github.com/repos/symfony/translation/zipball/82ab368a6fca6358d995b6dd5c41590fb42c03e6",
+ "reference": "82ab368a6fca6358d995b6dd5c41590fb42c03e6",
"shasum": ""
},
"require": {
- "php": ">=8.2",
- "symfony/deprecation-contracts": "^2.5|^3",
- "symfony/polyfill-mbstring": "~1.0",
- "symfony/translation-contracts": "^2.5.3|^3.3"
+ "php": ">=8.4",
+ "symfony/polyfill-mbstring": "^1.0",
+ "symfony/translation-contracts": "^3.6.1"
},
"conflict": {
"nikic/php-parser": "<5.0",
- "symfony/config": "<6.4",
- "symfony/console": "<6.4",
- "symfony/dependency-injection": "<6.4",
"symfony/http-client-contracts": "<2.5",
- "symfony/http-kernel": "<6.4",
- "symfony/service-contracts": "<2.5",
- "symfony/twig-bundle": "<6.4",
- "symfony/yaml": "<6.4"
+ "symfony/service-contracts": "<2.5"
},
"provide": {
"symfony/translation-implementation": "2.3|3.0"
@@ -5892,17 +5884,17 @@
"require-dev": {
"nikic/php-parser": "^5.0",
"psr/log": "^1|^2|^3",
- "symfony/config": "^6.4|^7.0|^8.0",
- "symfony/console": "^6.4|^7.0|^8.0",
- "symfony/dependency-injection": "^6.4|^7.0|^8.0",
- "symfony/finder": "^6.4|^7.0|^8.0",
+ "symfony/config": "^7.4|^8.0",
+ "symfony/console": "^7.4|^8.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/finder": "^7.4|^8.0",
"symfony/http-client-contracts": "^2.5|^3.0",
- "symfony/http-kernel": "^6.4|^7.0|^8.0",
- "symfony/intl": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/intl": "^7.4|^8.0",
"symfony/polyfill-intl-icu": "^1.21",
- "symfony/routing": "^6.4|^7.0|^8.0",
+ "symfony/routing": "^7.4|^8.0",
"symfony/service-contracts": "^2.5|^3",
- "symfony/yaml": "^6.4|^7.0|^8.0"
+ "symfony/yaml": "^7.4|^8.0"
},
"type": "library",
"autoload": {
@@ -5933,7 +5925,7 @@
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/translation/tree/v7.4.0"
+ "source": "https://github.com/symfony/translation/tree/v8.0.0"
},
"funding": [
{
@@ -5953,7 +5945,7 @@
"type": "tidelift"
}
],
- "time": "2025-11-27T13:27:24+00:00"
+ "time": "2025-11-27T08:09:45+00:00"
},
{
"name": "symfony/translation-contracts",
@@ -7300,23 +7292,23 @@
},
{
"name": "phpunit/php-code-coverage",
- "version": "12.4.0",
+ "version": "12.5.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "67e8aed88f93d0e6e1cb7effe1a2dfc2fee6022c"
+ "reference": "bca180c050dd3ae15f87c26d25cabb34fe1a0a5a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/67e8aed88f93d0e6e1cb7effe1a2dfc2fee6022c",
- "reference": "67e8aed88f93d0e6e1cb7effe1a2dfc2fee6022c",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/bca180c050dd3ae15f87c26d25cabb34fe1a0a5a",
+ "reference": "bca180c050dd3ae15f87c26d25cabb34fe1a0a5a",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"ext-xmlwriter": "*",
- "nikic/php-parser": "^5.6.1",
+ "nikic/php-parser": "^5.6.2",
"php": ">=8.3",
"phpunit/php-file-iterator": "^6.0",
"phpunit/php-text-template": "^5.0",
@@ -7324,10 +7316,10 @@
"sebastian/environment": "^8.0.3",
"sebastian/lines-of-code": "^4.0",
"sebastian/version": "^6.0",
- "theseer/tokenizer": "^1.2.3"
+ "theseer/tokenizer": "^1.3.1"
},
"require-dev": {
- "phpunit/phpunit": "^12.3.7"
+ "phpunit/phpunit": "^12.4.4"
},
"suggest": {
"ext-pcov": "PHP extension that provides line coverage",
@@ -7336,7 +7328,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "12.4.x-dev"
+ "dev-main": "12.5.x-dev"
}
},
"autoload": {
@@ -7365,7 +7357,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
- "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.4.0"
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.0"
},
"funding": [
{
@@ -7385,7 +7377,7 @@
"type": "tidelift"
}
],
- "time": "2025-09-24T13:44:41+00:00"
+ "time": "2025-11-29T07:15:54+00:00"
},
{
"name": "phpunit/php-file-iterator",
@@ -7634,16 +7626,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "12.4.4",
+ "version": "12.5.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "9253ec75a672e39fcc9d85bdb61448215b8162c7"
+ "reference": "e33a5132ea24119400f6ce5bce6665922e968bad"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9253ec75a672e39fcc9d85bdb61448215b8162c7",
- "reference": "9253ec75a672e39fcc9d85bdb61448215b8162c7",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e33a5132ea24119400f6ce5bce6665922e968bad",
+ "reference": "e33a5132ea24119400f6ce5bce6665922e968bad",
"shasum": ""
},
"require": {
@@ -7657,7 +7649,7 @@
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.3",
- "phpunit/php-code-coverage": "^12.4.0",
+ "phpunit/php-code-coverage": "^12.5.0",
"phpunit/php-file-iterator": "^6.0.0",
"phpunit/php-invoker": "^6.0.0",
"phpunit/php-text-template": "^5.0.0",
@@ -7679,7 +7671,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "12.4-dev"
+ "dev-main": "12.5-dev"
}
},
"autoload": {
@@ -7711,7 +7703,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/12.4.4"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.1"
},
"funding": [
{
@@ -7735,7 +7727,7 @@
"type": "tidelift"
}
],
- "time": "2025-11-21T07:39:11+00:00"
+ "time": "2025-12-06T12:19:17+00:00"
},
{
"name": "rector/jack",
@@ -8958,5 +8950,5 @@
"php": "^8.4"
},
"platform-dev": {},
- "plugin-api-version": "2.9.0"
+ "plugin-api-version": "2.6.0"
}
diff --git a/resources/docs/creating-a-node-visitor.md b/resources/docs/creating-a-node-visitor.md
index 5465af60d..c012a32bf 100644
--- a/resources/docs/creating-a-node-visitor.md
+++ b/resources/docs/creating-a-node-visitor.md
@@ -1,17 +1,10 @@
-You can add new NodeVisitors to process nodes in the `rector.php` config, make sure you tag it correctly so dependency injection knows where to look:
+You can add new NodeVisitors to decorate nodes with attributes before being used by one or more rules. This is useful if you need to add metadata to nodes, that are known only in higher nodes. E.g. if `Expr` is part of assign:
```php
-registerService(HelloVisitor::class, null, ScopeResolverNodeVisitorInterface::class);
+$value = 100;
```
-## A simple node visitor
+First we create a node visitor, that implements `Rector\Contract\PhpParser\DecoratingNodeVisitorInterface`:
```php
setAttribute(self::HELLO_ATTRIBUTE, 'i was here');
- return $node;
+ $node->var->setAttribute(self::IS_PART_OF_ASSIGN, true);
+ $node->expr->setAttribute(self::IS_PART_OF_ASSIGN, true);
+
+ return null;
+ }
+}
+```
+
+Never modify any nodes in the visitor as it might break node tree traversal. Only add attributes to nodes.
+
+
+
+### Register in Rector
+
+Then, register in the `rector.php` config:
+
+```php
+registerDecoratingNodeVisitor(ScopeResolverNodeVisitorInterface::class);
+```
+
+
+
+If you're adding similar node visitor to Rector codebase, add class to `Rector\DependencyInjection\LazyContainerFactory::DECORATING_NODE_VISITOR_CLASSES` to register it.
+
+
+
+Then, you can check the attribute in any Rector rule:
+
+```php
+fianl class SomeRector extends AbstractRector
+{
+ public function refactor(Node $node): ?Node
+ {
+ if ($node->getAttribute(AssignAwareNodeVisitor::IS_PART_OF_ASSIGN)) {
+ // we know node is part of assign
+ }
+
+ // ...
+
+ return null;
}
}
```
+
+
+
+### Use Existing Attribute Keys
+
+Rector provides couple such node visitors out of the box. You can use these attributes already.
+
+To find out list of attributes you can use, see `IS_*` constants in
+[`AttributeKey` class](https://github.com/rectorphp/rector-src/blob/main/src/NodeTypeResolver/Node/AttributeKey.php).
diff --git a/src/Documentation/DocumentationMenuFactory.php b/src/Documentation/DocumentationMenuFactory.php
index d83b822c3..34031c33c 100644
--- a/src/Documentation/DocumentationMenuFactory.php
+++ b/src/Documentation/DocumentationMenuFactory.php
@@ -72,7 +72,11 @@ public function create(): array
'writing-tests-for-custom-rule',
'Writing Tests For Custom Rule'
),
- $this->documentationMenuItemFactory->createSection('creating-a-node-visitor', 'Creating Node Visitor'),
+ $this->documentationMenuItemFactory->createSection(
+ 'creating-a-node-visitor',
+ 'Creating Node Visitor',
+ true
+ ),
$this->documentationMenuItemFactory->createSection('how-to-run-on-php-53', 'Run on PHP 5.3'),
$this->documentationMenuItemFactory->createSection(
'https://leanpub.com/rector-the-power-of-automated-refactoring',