From f3e16ed40d56ea5b8a03218934cef7078b17da66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gytis=20=C5=A0k=C4=97ma?= Date: Mon, 11 Dec 2023 17:39:48 +0200 Subject: [PATCH] fix: parsing of `new` initializers in promoted constructor properties --- CHANGELOG.md | 5 ++++ src/Core/Func/FunctionSignatureParser.php | 27 +++++++++++++------ src/Core/TokenHelper.php | 21 +++++++++++++++ .../Sniffs/CompositeCodeElementSniffTest.php | 11 ++++++++ tests/Sniffs/fixtures/TestClass13.php | 2 +- tests/Sniffs/fixtures/TestClass15.php | 23 ++++++++++++++++ 6 files changed, 80 insertions(+), 9 deletions(-) create mode 100644 tests/Sniffs/fixtures/TestClass15.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b22c72..532bb5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to `phpcs-type-sniff` will be documented in this file. Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. +## 82.2.1 - 2023-12-11 +### Fixed +- Fixed parsing of `new` initializers in promoted constructor properties +- Sniff options parsing + ## 82.2.0 - 2023-04-07 ### Changed - Remove upper bound PHP requirement so package can be installed on higher PHP versions even if locked. diff --git a/src/Core/Func/FunctionSignatureParser.php b/src/Core/Func/FunctionSignatureParser.php index b005203..763df4e 100644 --- a/src/Core/Func/FunctionSignatureParser.php +++ b/src/Core/Func/FunctionSignatureParser.php @@ -136,20 +136,29 @@ public static function fromTokens(File $file, int $fnPtr): FunctionSignature } break; case T_NEW: - $raw['new'] = 0; + $raw['new'] = true; break; case T_OPEN_PARENTHESIS: - if (0 === ($raw['new'] ?? null)) { - $raw['new'] = 1; + if ($raw['new'] ?? false) { + // new constructor args may contain tokens arrays, object constructors, etc. + // so we must skip to next param or to end of function signature + $closingParenthesisPtr = TokenHelper::findClosingParenthesis($file, $ptr); + // Some dumbass may write = new Obj, but it's already a standard warning. This won't crash + if (null !== $closingParenthesisPtr) { + if (!empty($raw)) { + $params[] = static::createParam($raw); + $raw = []; + } + $ptr = $closingParenthesisPtr; + break; // skip to whatever is next + } } else { $raw['type'] = ($raw['type'] ?? '') . $token['content']; // intersection type } break; case T_CLOSE_PARENTHESIS: - if (1 === ($raw['new'] ?? null)) { - $raw['new'] = 2; - break; - } + // end of function signature, close parenthesis for new (hopefully) have been skipped + // also possible closing DNF type // type detected e.g '(int', variable still not detected, means this is before var name: intersection type if (isset($raw['type']) && !isset($raw['name'])) { @@ -239,11 +248,13 @@ protected static function createParam(array $raw): FunctionParam { $rawValueType = $raw['default'] ?? ''; if (str_contains($rawValueType, '::')) { - $valueType = null; // a constant is used, need reflection :( + $valueType = null; // a constant is used, need reflection or imported class parsing } else { $valueType = TypeFactory::fromRawType($raw['default'] ?? ''); } + // Try when global constant (parsed as FqcnType). + // 'new' flag indicated new constructor - would need to parse imported classes. if ($valueType instanceof FqcnType && !($raw['new'] ?? false)) { if (defined($valueType->toString())) { // eg PHP_INT_MAX $valueType = TypeFactory::fromValue(constant($valueType->toString())); diff --git a/src/Core/TokenHelper.php b/src/Core/TokenHelper.php index aa3cda9..01e9074 100644 --- a/src/Core/TokenHelper.php +++ b/src/Core/TokenHelper.php @@ -453,4 +453,25 @@ public static function isClassExtended(File $file, int $classPtr): bool return T_EXTENDS === $extendsCode; } + + public static function findClosingParenthesis(File $file, int $openParenthesisPtr): ?int + { + $tokens = $file->getTokens(); + + $ptr = $openParenthesisPtr; + $openedScopeCount = 1; + while (isset($tokens[++$ptr])) { + $tokenCode = $tokens[$ptr]['code'] ?? null; + if (T_OPEN_PARENTHESIS === $tokenCode) { + $openedScopeCount++; + } elseif (T_CLOSE_PARENTHESIS === $tokenCode) { + $openedScopeCount--; + } + if (0 === $openedScopeCount) { + return $ptr; + } + } + + return null; + } } diff --git a/tests/Sniffs/CompositeCodeElementSniffTest.php b/tests/Sniffs/CompositeCodeElementSniffTest.php index ae3035d..cf17174 100644 --- a/tests/Sniffs/CompositeCodeElementSniffTest.php +++ b/tests/Sniffs/CompositeCodeElementSniffTest.php @@ -552,6 +552,17 @@ public function dataProcess(): array ], ]; + // #24 + $dataSets[] = [ + [ + 'addViolationId' => false, + 'useReflection' => false, + ], + __DIR__ . '/fixtures/TestClass15.php', + [ + ], + ]; + return $dataSets; } diff --git a/tests/Sniffs/fixtures/TestClass13.php b/tests/Sniffs/fixtures/TestClass13.php index 5941e7c..7b8d815 100644 --- a/tests/Sniffs/fixtures/TestClass13.php +++ b/tests/Sniffs/fixtures/TestClass13.php @@ -77,7 +77,7 @@ public function create10(): static public function method1( Acme|null $param1 = null, - Acme|string|null $param1 = null + Acme|string|null $param2 = null ): int|null { } } diff --git a/tests/Sniffs/fixtures/TestClass15.php b/tests/Sniffs/fixtures/TestClass15.php new file mode 100644 index 0000000..4c470cb --- /dev/null +++ b/tests/Sniffs/fixtures/TestClass15.php @@ -0,0 +1,23 @@ + new \stdClass(), 'b' => []]), + private readonly Headers $headers2 = new Headers([]) + ) { + } +} + +class EmailRequest2 +{ + public function __construct( + private readonly Headers $headers = new Headers, + private readonly Headers $headers2 = new Headers + ) { + } +}