From 9bd1a830fd172b187ac5d2249b659c90bb1c3a59 Mon Sep 17 00:00:00 2001 From: Simon Bigelmayr Date: Wed, 18 Feb 2026 15:13:26 +0100 Subject: [PATCH 1/6] feat(phpstan): add OneThingPerLineRule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Detects method chains where multiple ->method() or ?->method() calls appear on the same line. Each call in a chain must be on its own line. - Registers on CallLike, handles both MethodCall and NullsafeMethodCall - Compares method name start lines to correctly handle multiline expressions - Reports errors at the method name line via RuleErrorBuilder::line() - Not registered in rules.neon — enabled per-module in consumer projects Co-Authored-By: Claude Opus 4.6 --- src/PHPStan/Rules/OneThingPerLineRule.php | 47 ++++++++++ tests/PHPStan/OneThingPerLineRuleTest.php | 88 +++++++++++++++++++ tests/PHPStan/data/method-chain-correct.php | 80 +++++++++++++++++ .../PHPStan/data/method-chain-violations.php | 62 +++++++++++++ .../phpstan-one-thing-per-line-test.neon | 5 ++ 5 files changed, 282 insertions(+) create mode 100644 src/PHPStan/Rules/OneThingPerLineRule.php create mode 100644 tests/PHPStan/OneThingPerLineRuleTest.php create mode 100644 tests/PHPStan/data/method-chain-correct.php create mode 100644 tests/PHPStan/data/method-chain-violations.php create mode 100644 tests/PHPStan/phpstan-one-thing-per-line-test.neon diff --git a/src/PHPStan/Rules/OneThingPerLineRule.php b/src/PHPStan/Rules/OneThingPerLineRule.php new file mode 100644 index 00000000..9b8f274d --- /dev/null +++ b/src/PHPStan/Rules/OneThingPerLineRule.php @@ -0,0 +1,47 @@ + */ +final class OneThingPerLineRule implements Rule +{ + public function getNodeType(): string + { + return CallLike::class; + } + + /** @return list */ + public function processNode(Node $node, Scope $scope): array + { + if (! $node instanceof MethodCall + && ! $node instanceof NullsafeMethodCall) { + return []; + } + + $var = $node->var; + if (! $var instanceof MethodCall + && ! $var instanceof NullsafeMethodCall) { + return []; + } + + if ($node->name->getStartLine() !== $var->name->getStartLine()) { + return []; + } + + return [ + RuleErrorBuilder::message('Method chain calls must each be on their own line.') + ->identifier('mll.oneThingPerLine') + ->line($node->name->getStartLine()) + ->build(), + ]; + } +} diff --git a/tests/PHPStan/OneThingPerLineRuleTest.php b/tests/PHPStan/OneThingPerLineRuleTest.php new file mode 100644 index 00000000..5f668bed --- /dev/null +++ b/tests/PHPStan/OneThingPerLineRuleTest.php @@ -0,0 +1,88 @@ +>}> */ + public static function dataIntegrationTests(): iterable + { + self::getContainer(); + + yield [__DIR__ . '/data/method-chain-violations.php', [ + 26 => [self::ERROR_MESSAGE], + 31 => [self::ERROR_MESSAGE, self::ERROR_MESSAGE], + 36 => [self::ERROR_MESSAGE], + 41 => [self::ERROR_MESSAGE], + 46 => [self::ERROR_MESSAGE], + 53 => [self::ERROR_MESSAGE], + 60 => [self::ERROR_MESSAGE], + ]]; + + yield [__DIR__ . '/data/method-chain-correct.php', []]; + } + + /** + * @param array> $expectedErrors + * + * @dataProvider dataIntegrationTests + */ + #[DataProvider('dataIntegrationTests')] + public function testIntegration(string $file, array $expectedErrors): void + { + $errors = $this->runAnalyse($file); + + $ourErrors = array_filter( + $errors, + static fn (Error $error): bool => str_contains($error->getMessage(), self::ERROR_MESSAGE), + ); + + if ($expectedErrors === []) { + self::assertEmpty($ourErrors, 'Should not report errors for correct code'); + } else { + self::assertNotEmpty($ourErrors, 'Should detect method chain violations'); + $this->assertSameErrorMessages($expectedErrors, $ourErrors); + } + } + + /** @return array */ + private function runAnalyse(string $file): array + { + $file = self::getFileHelper()->normalizePath($file); + + /** @var Analyser $analyser */ + $analyser = self::getContainer()->getByType(Analyser::class); + + return $analyser->analyse([$file])->getErrors(); + } + + /** + * @param array> $expectedErrors + * @param array $errors + */ + private function assertSameErrorMessages(array $expectedErrors, array $errors): void + { + foreach ($errors as $error) { + $errorLine = $error->getLine() ?? 0; + $errorMessage = $error->getMessage(); + + self::assertArrayHasKey($errorLine, $expectedErrors, "Unexpected error at line {$errorLine}: {$errorMessage}"); + self::assertContains($errorMessage, $expectedErrors[$errorLine]); + } + } + + /** @return array */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/phpstan-one-thing-per-line-test.neon', + ]; + } +} diff --git a/tests/PHPStan/data/method-chain-correct.php b/tests/PHPStan/data/method-chain-correct.php new file mode 100644 index 00000000..04431fa2 --- /dev/null +++ b/tests/PHPStan/data/method-chain-correct.php @@ -0,0 +1,80 @@ +foo(); + } + + public function staticPlusSingleMethod(): void + { + self::create()->foo(); + } + + public function propertyAccessPlusMethod(): void + { + $this->relation->foo(); + } + + public function properlySplitChain(): void + { + $this->foo() + ->bar(); + } + + public function properlySplitThreeChain(): void + { + $this->foo() + ->bar() + ->baz(); + } + + public function newExpressionPlusSingleMethod(): void + { + (new self())->baz(); + } + + public function multilineArgsWithSplitContinuation(): void + { + $this->foo( + 1 + )->bar(); + } + + public function properlySplitNullSafeChain(?self $nullable): void + { + $nullable?->foo() + ?->bar(); + } + + public function properlySplitChainInClosure(): void + { + /** @var list $items */ + $items = []; + array_map(fn (self $x): self => $x->foo() + ->bar(), $items); + } +} diff --git a/tests/PHPStan/data/method-chain-violations.php b/tests/PHPStan/data/method-chain-violations.php new file mode 100644 index 00000000..d683fbf5 --- /dev/null +++ b/tests/PHPStan/data/method-chain-violations.php @@ -0,0 +1,62 @@ +foo()->bar(); + } + + public function threeCallsOnSameLine(): void + { + $this->foo()->bar()->baz(); + } + + public function staticPlusTwoMethodCallsOnSameLine(): void + { + self::create()->foo()->bar(); + } + + public function nullSafeChain(?self $nullable): void + { + $nullable?->foo()?->bar(); + } + + public function mixedNullSafeAndRegularChain(?self $nullable): void + { + $nullable?->foo()->bar(); + } + + public function arrowFunctionInternalChain(): void + { + /** @var list $items */ + $items = []; + array_map(fn (self $x): self => $x->foo()->bar(), $items); + } + + public function closureInternalChain(): void + { + /** @var list $items */ + $items = []; + array_map(fn (self $x): int => spl_object_id($x->foo()->bar()), $items); + } +} diff --git a/tests/PHPStan/phpstan-one-thing-per-line-test.neon b/tests/PHPStan/phpstan-one-thing-per-line-test.neon new file mode 100644 index 00000000..ef40518d --- /dev/null +++ b/tests/PHPStan/phpstan-one-thing-per-line-test.neon @@ -0,0 +1,5 @@ +parameters: + customRulesetUsed: true + +rules: + - MLL\Utils\PHPStan\Rules\OneThingPerLineRule From abb3c82271cf8a314091da208d203f7015317af8 Mon Sep 17 00:00:00 2001 From: simbig <26680884+simbig@users.noreply.github.com> Date: Wed, 18 Feb 2026 14:14:10 +0000 Subject: [PATCH 2/6] Apply php-cs-fixer changes --- src/PHPStan/Rules/OneThingPerLineRule.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PHPStan/Rules/OneThingPerLineRule.php b/src/PHPStan/Rules/OneThingPerLineRule.php index 9b8f274d..0388e877 100644 --- a/src/PHPStan/Rules/OneThingPerLineRule.php +++ b/src/PHPStan/Rules/OneThingPerLineRule.php @@ -11,7 +11,9 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -/** @implements Rule */ +/** + * @implements Rule + */ final class OneThingPerLineRule implements Rule { public function getNodeType(): string From 73a5a423abd91516863abe230bc8ad0df0587ecc Mon Sep 17 00:00:00 2001 From: Simon Bigelmayr Date: Wed, 18 Feb 2026 15:25:05 +0100 Subject: [PATCH 3/6] fix(phpstan): support PHP 7.4/8.0 in OneThingPerLineRule CI Split nullsafe operator (?->) test fixtures into separate files conditionally loaded on PHP >= 8.0, since PHP 7.4 cannot parse that syntax. Add return type ignore for PHPStan 1.x compatibility. Co-Authored-By: Claude Opus 4.6 --- phpstan/include-by-php-version.php | 5 ++++ phpstan/php-below-8.0.neon | 4 +++ phpstan/php-below-8.1.neon | 1 + tests/PHPStan/OneThingPerLineRuleTest.php | 15 ++++++++--- tests/PHPStan/data/method-chain-correct.php | 6 ----- .../data/method-chain-nullsafe-correct.php | 22 ++++++++++++++++ .../data/method-chain-nullsafe-violations.php | 26 +++++++++++++++++++ .../PHPStan/data/method-chain-violations.php | 10 ------- 8 files changed, 69 insertions(+), 20 deletions(-) create mode 100644 phpstan/php-below-8.0.neon create mode 100644 tests/PHPStan/data/method-chain-nullsafe-correct.php create mode 100644 tests/PHPStan/data/method-chain-nullsafe-violations.php diff --git a/phpstan/include-by-php-version.php b/phpstan/include-by-php-version.php index 6038baf5..e2abf011 100644 --- a/phpstan/include-by-php-version.php +++ b/phpstan/include-by-php-version.php @@ -8,6 +8,11 @@ $includes[] = __DIR__ . '/../rules.neon'; } +// PHP < 8.0: exclude test fixtures using nullsafe operator syntax +if (version_compare(PHP_VERSION, '8.0', '<')) { + $includes[] = __DIR__ . '/php-below-8.0.neon'; +} + // PHP < 8.1: exclude enums, add ignores for older PHPStan versions if (version_compare(PHP_VERSION, '8.1', '<')) { $includes[] = __DIR__ . '/php-below-8.1.neon'; diff --git a/phpstan/php-below-8.0.neon b/phpstan/php-below-8.0.neon new file mode 100644 index 00000000..b293c9f5 --- /dev/null +++ b/phpstan/php-below-8.0.neon @@ -0,0 +1,4 @@ +parameters: + excludePaths: + - ../tests/PHPStan/data/method-chain-nullsafe-violations.php + - ../tests/PHPStan/data/method-chain-nullsafe-correct.php diff --git a/phpstan/php-below-8.1.neon b/phpstan/php-below-8.1.neon index a2f36593..5c8f46f7 100644 --- a/phpstan/php-below-8.1.neon +++ b/phpstan/php-below-8.1.neon @@ -30,6 +30,7 @@ parameters: # Return type differences in older PHPStan rule interfaces - '#Method MLL\\Utils\\PHPStan\\Rules\\MissingClosureParameterTypehintRule::processNode\(\) should return array but returns array\.#' + - '#Method MLL\\Utils\\PHPStan\\Rules\\OneThingPerLineRule::processNode\(\) should return array but returns array\.#' # Existing code with @phpstan-ignore that older versions don't understand - message: '#Cannot access property \$name on SimpleXMLElement\|null\.#' diff --git a/tests/PHPStan/OneThingPerLineRuleTest.php b/tests/PHPStan/OneThingPerLineRuleTest.php index 5f668bed..2cab894d 100644 --- a/tests/PHPStan/OneThingPerLineRuleTest.php +++ b/tests/PHPStan/OneThingPerLineRuleTest.php @@ -20,13 +20,20 @@ public static function dataIntegrationTests(): iterable 26 => [self::ERROR_MESSAGE], 31 => [self::ERROR_MESSAGE, self::ERROR_MESSAGE], 36 => [self::ERROR_MESSAGE], - 41 => [self::ERROR_MESSAGE], - 46 => [self::ERROR_MESSAGE], - 53 => [self::ERROR_MESSAGE], - 60 => [self::ERROR_MESSAGE], + 43 => [self::ERROR_MESSAGE], + 50 => [self::ERROR_MESSAGE], ]]; yield [__DIR__ . '/data/method-chain-correct.php', []]; + + if (PHP_VERSION_ID >= 80000) { + yield [__DIR__ . '/data/method-chain-nullsafe-violations.php', [ + 19 => [self::ERROR_MESSAGE], + 24 => [self::ERROR_MESSAGE], + ]]; + + yield [__DIR__ . '/data/method-chain-nullsafe-correct.php', []]; + } } /** diff --git a/tests/PHPStan/data/method-chain-correct.php b/tests/PHPStan/data/method-chain-correct.php index 04431fa2..9ba71c65 100644 --- a/tests/PHPStan/data/method-chain-correct.php +++ b/tests/PHPStan/data/method-chain-correct.php @@ -64,12 +64,6 @@ public function multilineArgsWithSplitContinuation(): void )->bar(); } - public function properlySplitNullSafeChain(?self $nullable): void - { - $nullable?->foo() - ?->bar(); - } - public function properlySplitChainInClosure(): void { /** @var list $items */ diff --git a/tests/PHPStan/data/method-chain-nullsafe-correct.php b/tests/PHPStan/data/method-chain-nullsafe-correct.php new file mode 100644 index 00000000..8bfae6b1 --- /dev/null +++ b/tests/PHPStan/data/method-chain-nullsafe-correct.php @@ -0,0 +1,22 @@ +foo() + ?->bar(); + } +} diff --git a/tests/PHPStan/data/method-chain-nullsafe-violations.php b/tests/PHPStan/data/method-chain-nullsafe-violations.php new file mode 100644 index 00000000..6279b862 --- /dev/null +++ b/tests/PHPStan/data/method-chain-nullsafe-violations.php @@ -0,0 +1,26 @@ +foo()?->bar(); + } + + public function mixedNullSafeAndRegularChain(?self $nullable): void + { + $nullable?->foo()->bar(); + } +} diff --git a/tests/PHPStan/data/method-chain-violations.php b/tests/PHPStan/data/method-chain-violations.php index d683fbf5..fd7bc5cf 100644 --- a/tests/PHPStan/data/method-chain-violations.php +++ b/tests/PHPStan/data/method-chain-violations.php @@ -36,16 +36,6 @@ public function staticPlusTwoMethodCallsOnSameLine(): void self::create()->foo()->bar(); } - public function nullSafeChain(?self $nullable): void - { - $nullable?->foo()?->bar(); - } - - public function mixedNullSafeAndRegularChain(?self $nullable): void - { - $nullable?->foo()->bar(); - } - public function arrowFunctionInternalChain(): void { /** @var list $items */ From 94480f84b08d7cd2636b32561511a604af53ae7f Mon Sep 17 00:00:00 2001 From: Simon Bigelmayr Date: Thu, 19 Feb 2026 15:42:26 +0100 Subject: [PATCH 4/6] feat(phpstan): handle string interpolation in OneThingPerLineRule Allow method chains inside string interpolation, skipping error detection for such cases. Adds caching of file contents for efficient parsing and extends the node type handling in the rule. --- src/PHPStan/Rules/OneThingPerLineRule.php | 32 +++++++++++++++++++++ tests/PHPStan/data/method-chain-correct.php | 10 +++++++ 2 files changed, 42 insertions(+) diff --git a/src/PHPStan/Rules/OneThingPerLineRule.php b/src/PHPStan/Rules/OneThingPerLineRule.php index 0388e877..6a42f8dd 100644 --- a/src/PHPStan/Rules/OneThingPerLineRule.php +++ b/src/PHPStan/Rules/OneThingPerLineRule.php @@ -3,19 +3,27 @@ namespace MLL\Utils\PHPStan\Rules; use PhpParser\Node; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\CallLike; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\NullsafeMethodCall; +use PhpParser\Node\Expr\NullsafePropertyFetch; +use PhpParser\Node\Expr\PropertyFetch; use PHPStan\Analyser\Scope; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function Safe\file_get_contents; + /** * @implements Rule */ final class OneThingPerLineRule implements Rule { + /** @var array */ + private array $fileContentsCache = []; + public function getNodeType(): string { return CallLike::class; @@ -39,6 +47,10 @@ public function processNode(Node $node, Scope $scope): array return []; } + if ($this->isInsideStringInterpolation($scope->getFile(), $node)) { + return []; + } + return [ RuleErrorBuilder::message('Method chain calls must each be on their own line.') ->identifier('mll.oneThingPerLine') @@ -46,4 +58,24 @@ public function processNode(Node $node, Scope $scope): array ->build(), ]; } + + private function isInsideStringInterpolation(string $file, Expr $node): bool + { + $root = $node; + while ($root instanceof MethodCall + || $root instanceof NullsafeMethodCall + || $root instanceof PropertyFetch + || $root instanceof NullsafePropertyFetch) { + $root = $root->var; + } + + $startPos = $root->getStartFilePos(); + if ($startPos <= 0) { + return false; + } + + $this->fileContentsCache[$file] ??= file_get_contents($file); + + return $this->fileContentsCache[$file][$startPos - 1] === '{'; + } } diff --git a/tests/PHPStan/data/method-chain-correct.php b/tests/PHPStan/data/method-chain-correct.php index 9ba71c65..aebb4c5f 100644 --- a/tests/PHPStan/data/method-chain-correct.php +++ b/tests/PHPStan/data/method-chain-correct.php @@ -16,6 +16,11 @@ public function bar(): self public function baz(): void {} + public function name(): string + { + return 'test'; + } + public static function create(): self { return new self(); @@ -64,6 +69,11 @@ public function multilineArgsWithSplitContinuation(): void )->bar(); } + public function chainInsideStringInterpolation(): string + { + return "value: {$this->foo()->name()}"; + } + public function properlySplitChainInClosure(): void { /** @var list $items */ From 9175bf94741c000c967512a01a0cce5875a5580d Mon Sep 17 00:00:00 2001 From: Simon Bigelmayr Date: Mon, 23 Feb 2026 14:06:13 +0100 Subject: [PATCH 5/6] feat(phpstan): treat no-arg method calls as accessors in OneThingPerLineRule A method call without arguments (e.g. ->relation()) is treated like a property access and does not count as "a thing". This allows patterns like $item->magnaPure()->associate($x) on a single line. Also excludes method chains inside string interpolation. Co-Authored-By: Claude Opus 4.6 --- phpstan/include-by-php-version.php | 5 --- phpstan/php-below-8.1.neon | 1 - src/PHPStan/Rules/OneThingPerLineRule.php | 4 ++ tests/PHPStan/OneThingPerLineRuleTest.php | 7 ++-- tests/PHPStan/data/method-chain-correct.php | 39 +++++++++++++++++++ .../data/method-chain-nullsafe-correct.php | 10 +++++ .../data/method-chain-nullsafe-violations.php | 12 +++--- .../PHPStan/data/method-chain-violations.php | 25 +++++------- 8 files changed, 72 insertions(+), 31 deletions(-) diff --git a/phpstan/include-by-php-version.php b/phpstan/include-by-php-version.php index e2abf011..6038baf5 100644 --- a/phpstan/include-by-php-version.php +++ b/phpstan/include-by-php-version.php @@ -8,11 +8,6 @@ $includes[] = __DIR__ . '/../rules.neon'; } -// PHP < 8.0: exclude test fixtures using nullsafe operator syntax -if (version_compare(PHP_VERSION, '8.0', '<')) { - $includes[] = __DIR__ . '/php-below-8.0.neon'; -} - // PHP < 8.1: exclude enums, add ignores for older PHPStan versions if (version_compare(PHP_VERSION, '8.1', '<')) { $includes[] = __DIR__ . '/php-below-8.1.neon'; diff --git a/phpstan/php-below-8.1.neon b/phpstan/php-below-8.1.neon index 5c8f46f7..a2f36593 100644 --- a/phpstan/php-below-8.1.neon +++ b/phpstan/php-below-8.1.neon @@ -30,7 +30,6 @@ parameters: # Return type differences in older PHPStan rule interfaces - '#Method MLL\\Utils\\PHPStan\\Rules\\MissingClosureParameterTypehintRule::processNode\(\) should return array but returns array\.#' - - '#Method MLL\\Utils\\PHPStan\\Rules\\OneThingPerLineRule::processNode\(\) should return array but returns array\.#' # Existing code with @phpstan-ignore that older versions don't understand - message: '#Cannot access property \$name on SimpleXMLElement\|null\.#' diff --git a/src/PHPStan/Rules/OneThingPerLineRule.php b/src/PHPStan/Rules/OneThingPerLineRule.php index 6a42f8dd..f58bda56 100644 --- a/src/PHPStan/Rules/OneThingPerLineRule.php +++ b/src/PHPStan/Rules/OneThingPerLineRule.php @@ -47,6 +47,10 @@ public function processNode(Node $node, Scope $scope): array return []; } + if ($var->getArgs() === []) { + return []; + } + if ($this->isInsideStringInterpolation($scope->getFile(), $node)) { return []; } diff --git a/tests/PHPStan/OneThingPerLineRuleTest.php b/tests/PHPStan/OneThingPerLineRuleTest.php index 2cab894d..0e427407 100644 --- a/tests/PHPStan/OneThingPerLineRuleTest.php +++ b/tests/PHPStan/OneThingPerLineRuleTest.php @@ -18,10 +18,9 @@ public static function dataIntegrationTests(): iterable yield [__DIR__ . '/data/method-chain-violations.php', [ 26 => [self::ERROR_MESSAGE], - 31 => [self::ERROR_MESSAGE, self::ERROR_MESSAGE], - 36 => [self::ERROR_MESSAGE], - 43 => [self::ERROR_MESSAGE], - 50 => [self::ERROR_MESSAGE], + 31 => [self::ERROR_MESSAGE], + 38 => [self::ERROR_MESSAGE], + 45 => [self::ERROR_MESSAGE], ]]; yield [__DIR__ . '/data/method-chain-correct.php', []]; diff --git a/tests/PHPStan/data/method-chain-correct.php b/tests/PHPStan/data/method-chain-correct.php index aebb4c5f..4b2a4698 100644 --- a/tests/PHPStan/data/method-chain-correct.php +++ b/tests/PHPStan/data/method-chain-correct.php @@ -69,6 +69,45 @@ public function multilineArgsWithSplitContinuation(): void )->bar(); } + public function noArgAccessorThenAction(): void + { + $this->foo()->baz(); + } + + public function noArgAccessorThenActionWithArgs(): void + { + $this->foo()->foo(1); + } + + public function noArgChainAllAccessors(): void + { + $this->foo()->bar()->baz(); + } + + public function staticPlusNoArgChain(): void + { + self::create()->foo()->bar(); + } + + public function propertyThenNoArgAccessorThenAction(): void + { + $this->relation->foo()->baz(); + } + + public function noArgAccessorChainInArrowFunction(): void + { + /** @var list $items */ + $items = []; + array_map(fn (self $x): self => $x->foo()->bar(), $items); + } + + public function noArgAccessorChainInClosure(): void + { + /** @var list $items */ + $items = []; + array_map(fn (self $x): int => spl_object_id($x->foo()->bar()), $items); + } + public function chainInsideStringInterpolation(): string { return "value: {$this->foo()->name()}"; diff --git a/tests/PHPStan/data/method-chain-nullsafe-correct.php b/tests/PHPStan/data/method-chain-nullsafe-correct.php index 8bfae6b1..71d2e19e 100644 --- a/tests/PHPStan/data/method-chain-nullsafe-correct.php +++ b/tests/PHPStan/data/method-chain-nullsafe-correct.php @@ -19,4 +19,14 @@ public function properlySplitNullSafeChain(?self $nullable): void $nullable?->foo() ?->bar(); } + + public function noArgNullSafeChain(?self $nullable): void + { + $nullable?->foo()?->bar(); + } + + public function mixedNoArgNullSafeChain(?self $nullable): void + { + $nullable?->foo()->bar(); + } } diff --git a/tests/PHPStan/data/method-chain-nullsafe-violations.php b/tests/PHPStan/data/method-chain-nullsafe-violations.php index 6279b862..134f30b7 100644 --- a/tests/PHPStan/data/method-chain-nullsafe-violations.php +++ b/tests/PHPStan/data/method-chain-nullsafe-violations.php @@ -4,23 +4,23 @@ class MethodChainNullsafeViolations { - public function foo(): self + public function foo(int $arg = 0): self { return $this; } - public function bar(): self + public function bar(int $arg = 0): self { return $this; } - public function nullSafeChain(?self $nullable): void + public function nullSafeChainWithArgs(?self $nullable): void { - $nullable?->foo()?->bar(); + $nullable?->foo(1)?->bar(2); } - public function mixedNullSafeAndRegularChain(?self $nullable): void + public function mixedNullSafeChainWithArgs(?self $nullable): void { - $nullable?->foo()->bar(); + $nullable?->foo(1)->bar(2); } } diff --git a/tests/PHPStan/data/method-chain-violations.php b/tests/PHPStan/data/method-chain-violations.php index fd7bc5cf..44dac234 100644 --- a/tests/PHPStan/data/method-chain-violations.php +++ b/tests/PHPStan/data/method-chain-violations.php @@ -4,12 +4,12 @@ class MethodChainViolations { - public function foo(): self + public function foo(int $arg = 0): self { return $this; } - public function bar(): self + public function bar(int $arg = 0): self { return $this; } @@ -21,32 +21,27 @@ public static function create(): self return new self(); } - public function twoCallsOnSameLine(): void + public function twoCallsWithArgs(): void { - $this->foo()->bar(); + $this->foo(1)->bar(2); } - public function threeCallsOnSameLine(): void + public function threeCallsFirstHasArgs(): void { - $this->foo()->bar()->baz(); + $this->foo(1)->bar()->baz(); } - public function staticPlusTwoMethodCallsOnSameLine(): void - { - self::create()->foo()->bar(); - } - - public function arrowFunctionInternalChain(): void + public function arrowFunctionChainWithArgs(): void { /** @var list $items */ $items = []; - array_map(fn (self $x): self => $x->foo()->bar(), $items); + array_map(fn (self $x): self => $x->foo(1)->bar(2), $items); } - public function closureInternalChain(): void + public function closureChainWithArgs(): void { /** @var list $items */ $items = []; - array_map(fn (self $x): int => spl_object_id($x->foo()->bar()), $items); + array_map(fn (self $x): int => spl_object_id($x->foo(1)->bar(2)), $items); } } From 1b18fe9b590a7531a6078a08c15bf388423ce064 Mon Sep 17 00:00:00 2001 From: Simon Bigelmayr Date: Mon, 23 Feb 2026 14:09:56 +0100 Subject: [PATCH 6/6] fix(phpstan): restore PHP 7.4/8.0 compatibility config The previous cleanup accidentally removed the php-below-8.0.neon inclusion and the OneThingPerLineRule return type ignore for older PHPStan versions. Co-Authored-By: Claude Opus 4.6 --- phpstan/include-by-php-version.php | 5 +++++ phpstan/php-below-8.1.neon | 1 + 2 files changed, 6 insertions(+) diff --git a/phpstan/include-by-php-version.php b/phpstan/include-by-php-version.php index 6038baf5..e2abf011 100644 --- a/phpstan/include-by-php-version.php +++ b/phpstan/include-by-php-version.php @@ -8,6 +8,11 @@ $includes[] = __DIR__ . '/../rules.neon'; } +// PHP < 8.0: exclude test fixtures using nullsafe operator syntax +if (version_compare(PHP_VERSION, '8.0', '<')) { + $includes[] = __DIR__ . '/php-below-8.0.neon'; +} + // PHP < 8.1: exclude enums, add ignores for older PHPStan versions if (version_compare(PHP_VERSION, '8.1', '<')) { $includes[] = __DIR__ . '/php-below-8.1.neon'; diff --git a/phpstan/php-below-8.1.neon b/phpstan/php-below-8.1.neon index a2f36593..5c8f46f7 100644 --- a/phpstan/php-below-8.1.neon +++ b/phpstan/php-below-8.1.neon @@ -30,6 +30,7 @@ parameters: # Return type differences in older PHPStan rule interfaces - '#Method MLL\\Utils\\PHPStan\\Rules\\MissingClosureParameterTypehintRule::processNode\(\) should return array but returns array\.#' + - '#Method MLL\\Utils\\PHPStan\\Rules\\OneThingPerLineRule::processNode\(\) should return array but returns array\.#' # Existing code with @phpstan-ignore that older versions don't understand - message: '#Cannot access property \$name on SimpleXMLElement\|null\.#'