From c7c260918756f2d7d06879a01a8fb75dd9684623 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Dec 2022 01:28:15 +0000 Subject: [PATCH 01/59] Update dessant/lock-threads action to v4 --- .github/workflows/lock-closed-issues.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock-closed-issues.yml b/.github/workflows/lock-closed-issues.yml index a05d4173..4c7990df 100644 --- a/.github/workflows/lock-closed-issues.yml +++ b/.github/workflows/lock-closed-issues.yml @@ -8,7 +8,7 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v3 + - uses: dessant/lock-threads@v4 with: github-token: ${{ github.token }} issue-inactive-days: '31' From 2a4686e97458586321f8ffa133aaa967552b7200 Mon Sep 17 00:00:00 2001 From: Tomohito YABU Date: Thu, 8 Dec 2022 01:09:23 +0900 Subject: [PATCH 02/59] Add generics support to `@method` definitions --- src/Ast/PhpDoc/MethodTagValueNode.php | 10 +- src/Parser/PhpDocParser.php | 21 +++- tests/PHPStan/Parser/PhpDocParserTest.php | 133 ++++++++++++++++++++++ 3 files changed, 157 insertions(+), 7 deletions(-) diff --git a/src/Ast/PhpDoc/MethodTagValueNode.php b/src/Ast/PhpDoc/MethodTagValueNode.php index 155897bb..075cec04 100644 --- a/src/Ast/PhpDoc/MethodTagValueNode.php +++ b/src/Ast/PhpDoc/MethodTagValueNode.php @@ -4,6 +4,7 @@ use PHPStan\PhpDocParser\Ast\NodeAttributes; use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use function count; use function implode; class MethodTagValueNode implements PhpDocTagValueNode @@ -20,19 +21,23 @@ class MethodTagValueNode implements PhpDocTagValueNode /** @var string */ public $methodName; + /** @var TemplateTagValueNode[] */ + public $templateTypes; + /** @var MethodTagValueParameterNode[] */ public $parameters; /** @var string (may be empty) */ public $description; - public function __construct(bool $isStatic, ?TypeNode $returnType, string $methodName, array $parameters, string $description) + public function __construct(bool $isStatic, ?TypeNode $returnType, string $methodName, array $parameters, string $description, array $templateTypes = []) { $this->isStatic = $isStatic; $this->returnType = $returnType; $this->methodName = $methodName; $this->parameters = $parameters; $this->description = $description; + $this->templateTypes = $templateTypes; } @@ -42,7 +47,8 @@ public function __toString(): string $returnType = $this->returnType !== null ? "{$this->returnType} " : ''; $parameters = implode(', ', $this->parameters); $description = $this->description !== '' ? " {$this->description}" : ''; - return "{$static}{$returnType}{$this->methodName}({$parameters}){$description}"; + $templateTypes = count($this->templateTypes) > 0 ? '<' . implode(', ', $this->templateTypes) . '>' : ''; + return "{$static}{$returnType}{$this->methodName}{$templateTypes}({$parameters}){$description}"; } } diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index 9badbe61..d9942b3d 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -182,7 +182,7 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph case '@template-contravariant': case '@phpstan-template-contravariant': case '@psalm-template-contravariant': - $tagValue = $this->parseTemplateTagValue($tokens); + $tagValue = $this->parseTemplateTagValue($tokens, true); break; case '@extends': @@ -346,6 +346,14 @@ private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTa exit; } + $templateTypes = []; + if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { + do { + $templateTypes[] = $this->parseTemplateTagValue($tokens, false); + } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)); + $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); + } + $parameters = []; $tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES); if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) { @@ -357,10 +365,9 @@ private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTa $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES); $description = $this->parseOptionalDescription($tokens); - return new Ast\PhpDoc\MethodTagValueNode($isStatic, $returnType, $methodName, $parameters, $description); + return new Ast\PhpDoc\MethodTagValueNode($isStatic, $returnType, $methodName, $parameters, $description, $templateTypes); } - private function parseMethodTagValueParameter(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueParameterNode { switch ($tokens->currentTokenType()) { @@ -390,7 +397,7 @@ private function parseMethodTagValueParameter(TokenIterator $tokens): Ast\PhpDoc return new Ast\PhpDoc\MethodTagValueParameterNode($parameterType, $isReference, $isVariadic, $parameterName, $defaultValue); } - private function parseTemplateTagValue(TokenIterator $tokens): Ast\PhpDoc\TemplateTagValueNode + private function parseTemplateTagValue(TokenIterator $tokens, bool $parseDescription): Ast\PhpDoc\TemplateTagValueNode { $name = $tokens->currentTokenValue(); $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); @@ -408,7 +415,11 @@ private function parseTemplateTagValue(TokenIterator $tokens): Ast\PhpDoc\Templa $default = null; } - $description = $this->parseOptionalDescription($tokens); + if ($parseDescription) { + $description = $this->parseOptionalDescription($tokens); + } else { + $description = ''; + } return new Ast\PhpDoc\TemplateTagValueNode($name, $bound, $description, $default); } diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index d7a45c4a..0672e3b7 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -3,6 +3,7 @@ namespace PHPStan\PhpDocParser\Parser; use Iterator; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayItemNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; @@ -44,6 +45,7 @@ use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode; use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; use PHPStan\PhpDocParser\Lexer\Lexer; @@ -2201,6 +2203,98 @@ public function provideMethodTagsData(): Iterator ), ]), ]; + + yield [ + 'OK non-static, with return type and parameter with generic type', + '/** @method ?T randomElement(array $array = [\'a\', \'b\']) */', + new PhpDocNode([ + new PhpDocTagNode( + '@method', + new MethodTagValueNode( + false, + new NullableTypeNode(new IdentifierTypeNode('T')), + 'randomElement', + [ + new MethodTagValueParameterNode( + new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('array-key'), + new IdentifierTypeNode('T'), + ] + ), + false, + false, + '$array', + new ConstExprArrayNode([ + new ConstExprArrayItemNode( + null, + new ConstExprStringNode('\'a\'') + ), + new ConstExprArrayItemNode( + null, + new ConstExprStringNode('\'b\'') + ), + ]) + ), + ], + '', + [ + new TemplateTagValueNode( + 'T', + null, + '', + new IdentifierTypeNode('string') + ), + ] + ) + ), + ]), + ]; + + yield [ + 'OK static, with return type and multiple parameters with generic type', + '/** @method static bool compare(T1 $t1, T2 $t2, T3 $t3) */', + new PhpDocNode([ + new PhpDocTagNode( + '@method', + new MethodTagValueNode( + true, + new IdentifierTypeNode('bool'), + 'compare', + [ + new MethodTagValueParameterNode( + new IdentifierTypeNode('T1'), + false, + false, + '$t1', + null + ), + new MethodTagValueParameterNode( + new IdentifierTypeNode('T2'), + false, + false, + '$t2', + null + ), + new MethodTagValueParameterNode( + new IdentifierTypeNode('T3'), + false, + false, + '$t3', + null + ), + ], + '', + [ + new TemplateTagValueNode('T1', null, ''), + new TemplateTagValueNode('T2', new IdentifierTypeNode('Bar'), ''), + new TemplateTagValueNode('T3', new IdentifierTypeNode('Baz'), ''), + ] + ) + ), + ]), + ]; } @@ -3078,6 +3172,45 @@ public function provideMultiLinePhpDocData(): array ), ]), ], + [ + 'OK with template method', + '/** + * @template TKey as array-key + * @template TValue + * @method TKey|null find(TValue $v) find index of $v + */', + new PhpDocNode([ + new PhpDocTagNode( + '@template', + new TemplateTagValueNode('TKey', new IdentifierTypeNode('array-key'), '') + ), + new PhpDocTagNode( + '@template', + new TemplateTagValueNode('TValue', null, '') + ), + new PhpDocTagNode( + '@method', + new MethodTagValueNode( + false, + new UnionTypeNode([ + new IdentifierTypeNode('TKey'), + new IdentifierTypeNode('null'), + ]), + 'find', + [ + new MethodTagValueParameterNode( + new IdentifierTypeNode('TValue'), + false, + false, + '$v', + null + ), + ], + 'find index of $v' + ) + ), + ]), + ], [ 'OK with multiline conditional return type', '/** From 6ff970a7101acfe99b3048e4bbfbc094e55c5b04 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 7 Dec 2022 17:12:39 +0100 Subject: [PATCH 03/59] Fix tests --- tests/PHPStan/Parser/PhpDocParserTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 0672e3b7..aa06b5e7 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -2221,6 +2221,10 @@ public function provideMethodTagsData(): Iterator [ new IdentifierTypeNode('array-key'), new IdentifierTypeNode('T'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, ] ), false, From 950bddf2c4203fa08ad27b8349bba1f32a8a7ebe Mon Sep 17 00:00:00 2001 From: Jeremiasz Major Date: Wed, 30 Nov 2022 19:39:18 +0100 Subject: [PATCH 04/59] Support unsealed array shapes --- src/Ast/Type/ArrayShapeNode.php | 14 ++++- src/Parser/TypeParser.php | 23 +++++---- tests/PHPStan/Parser/TypeParserTest.php | 69 +++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 12 deletions(-) diff --git a/src/Ast/Type/ArrayShapeNode.php b/src/Ast/Type/ArrayShapeNode.php index 38d64dd6..b0683351 100644 --- a/src/Ast/Type/ArrayShapeNode.php +++ b/src/Ast/Type/ArrayShapeNode.php @@ -13,15 +13,25 @@ class ArrayShapeNode implements TypeNode /** @var ArrayShapeItemNode[] */ public $items; - public function __construct(array $items) + /** @var bool */ + public $sealed; + + public function __construct(array $items, bool $sealed = true) { $this->items = $items; + $this->sealed = $sealed; } public function __toString(): string { - return 'array{' . implode(', ', $this->items) . '}'; + $items = $this->items; + + if ($this->sealed) { + $items[] = '...'; + } + + return 'array{' . implode(', ', $items) . '}'; } } diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index ef0fbc90..f45aafdc 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -503,29 +503,32 @@ private function tryParseArrayOrOffsetAccess(TokenIterator $tokens, Ast\Type\Typ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\ArrayShapeNode { $tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET); - if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) { - return new Ast\Type\ArrayShapeNode([]); - } - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); - $items = [$this->parseArrayShapeItem($tokens)]; + $items = []; + $sealed = true; - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); - while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { + do { $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) { - // trailing comma case return new Ast\Type\ArrayShapeNode($items); } + if ($tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC)) { + $sealed = false; + $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA); + break; + } + $items[] = $this->parseArrayShapeItem($tokens); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); - } + } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)); $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET); - return new Ast\Type\ArrayShapeNode($items); + return new Ast\Type\ArrayShapeNode($items, $sealed); } diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 75b26fe7..484823a8 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -599,6 +599,75 @@ public function provideParseData(): array ), ]), ], + [ + 'array{a: int, b: int, ...}', + new ArrayShapeNode([ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new IdentifierTypeNode('b'), + false, + new IdentifierTypeNode('int') + ), + ], false), + ], + [ + 'array{int, string, ...}', + new ArrayShapeNode([ + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('string') + ), + ], false), + ], + [ + 'array{...}', + new ArrayShapeNode([], false), + ], + [ + 'array{ + * a: int, + * ... + *}', + new ArrayShapeNode([ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + ], false), + ], + [ + 'array{ + a: int, + ..., + }', + new ArrayShapeNode([ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + ], false), + ], + [ + 'array{int, ..., string}', + new ParserException( + 'string', + Lexer::TOKEN_IDENTIFIER, + 16, + Lexer::TOKEN_CLOSE_CURLY_BRACKET + ), + ], [ 'callable(): Foo', new CallableTypeNode( From 5941477f100993652218928039d530b75a13a9ca Mon Sep 17 00:00:00 2001 From: Jeremiasz Major Date: Fri, 16 Dec 2022 07:42:48 +0100 Subject: [PATCH 05/59] Fix printing nodes * Add `toString()` tests for all nodes * Fix printing array shapes * Improve callable type printing * Add missing function import --- src/Ast/Type/ArrayShapeNode.php | 2 +- src/Ast/Type/CallableTypeParameterNode.php | 3 +- tests/PHPStan/Ast/PhpDoc/NodePrintTest.php | 46 --- .../Ast/ToString/ConstExprToStringTest.php | 52 +++ .../Ast/ToString/PhpDocToStringTest.php | 332 ++++++++++++++++++ .../PHPStan/Ast/ToString/TypeToStringTest.php | 221 ++++++++++++ 6 files changed, 608 insertions(+), 48 deletions(-) delete mode 100644 tests/PHPStan/Ast/PhpDoc/NodePrintTest.php create mode 100644 tests/PHPStan/Ast/ToString/ConstExprToStringTest.php create mode 100644 tests/PHPStan/Ast/ToString/PhpDocToStringTest.php create mode 100644 tests/PHPStan/Ast/ToString/TypeToStringTest.php diff --git a/src/Ast/Type/ArrayShapeNode.php b/src/Ast/Type/ArrayShapeNode.php index b0683351..3778436d 100644 --- a/src/Ast/Type/ArrayShapeNode.php +++ b/src/Ast/Type/ArrayShapeNode.php @@ -27,7 +27,7 @@ public function __toString(): string { $items = $this->items; - if ($this->sealed) { + if (! $this->sealed) { $items[] = '...'; } diff --git a/src/Ast/Type/CallableTypeParameterNode.php b/src/Ast/Type/CallableTypeParameterNode.php index 2badb7c2..7ab2d7e3 100644 --- a/src/Ast/Type/CallableTypeParameterNode.php +++ b/src/Ast/Type/CallableTypeParameterNode.php @@ -4,6 +4,7 @@ use PHPStan\PhpDocParser\Ast\Node; use PHPStan\PhpDocParser\Ast\NodeAttributes; +use function trim; class CallableTypeParameterNode implements Node { @@ -41,7 +42,7 @@ public function __toString(): string $isReference = $this->isReference ? '&' : ''; $isVariadic = $this->isVariadic ? '...' : ''; $default = $this->isOptional ? ' = default' : ''; - return "{$type}{$isReference}{$isVariadic}{$this->parameterName}{$default}"; + return trim("{$type}{$isReference}{$isVariadic}{$this->parameterName}") . $default; } } diff --git a/tests/PHPStan/Ast/PhpDoc/NodePrintTest.php b/tests/PHPStan/Ast/PhpDoc/NodePrintTest.php deleted file mode 100644 index db39bf3b..00000000 --- a/tests/PHPStan/Ast/PhpDoc/NodePrintTest.php +++ /dev/null @@ -1,46 +0,0 @@ -assertSame($expectedPrinted, (string) $node); - } - - - public function providePhpDocData(): Iterator - { - yield [ - new PhpDocNode([ - new PhpDocTextNode('It works'), - ]), - '/** - * It works - */', - ]; - - yield [ - new PhpDocNode([ - new PhpDocTextNode('It works'), - new PhpDocTextNode(''), - new PhpDocTextNode('with empty lines'), - ]), - '/** - * It works - * - * with empty lines - */', - ]; - } - -} diff --git a/tests/PHPStan/Ast/ToString/ConstExprToStringTest.php b/tests/PHPStan/Ast/ToString/ConstExprToStringTest.php new file mode 100644 index 00000000..7ca62550 --- /dev/null +++ b/tests/PHPStan/Ast/ToString/ConstExprToStringTest.php @@ -0,0 +1,52 @@ +assertSame($expected, (string) $node); + } + + public static function provideConstExprCases(): Generator + { + yield from [ + ['null', new ConstExprNullNode()], + ['true', new ConstExprTrueNode()], + ['false', new ConstExprFalseNode()], + ['8', new ConstExprIntegerNode('8')], + ['21.37', new ConstExprFloatNode('21.37')], + ['foo', new ConstExprStringNode('foo')], + ['FooBar', new ConstFetchNode('', 'FooBar')], + ['Foo\\Bar::Baz', new ConstFetchNode('Foo\\Bar', 'Baz')], + ['[]', new ConstExprArrayNode([])], + [ + '[foo, 4 => foo, bar => baz]', + new ConstExprArrayNode([ + new ConstExprArrayItemNode(null, new ConstExprStringNode('foo')), + new ConstExprArrayItemNode(new ConstExprIntegerNode('4'), new ConstExprStringNode('foo')), + new ConstExprArrayItemNode(new ConstExprStringNode('bar'), new ConstExprStringNode('baz')), + ]), + ], + ]; + } + +} diff --git a/tests/PHPStan/Ast/ToString/PhpDocToStringTest.php b/tests/PHPStan/Ast/ToString/PhpDocToStringTest.php new file mode 100644 index 00000000..4d40c410 --- /dev/null +++ b/tests/PHPStan/Ast/ToString/PhpDocToStringTest.php @@ -0,0 +1,332 @@ +assertSame($expected, (string) $node); + } + + /** + * @dataProvider provideOtherCases + * @dataProvider provideMethodCases + * @dataProvider provideClassCases + * @dataProvider provideAssertionCases + */ + public function testTagValueNodeToString(string $expected, Node $node): void + { + $this->assertSame($expected, (string) $node); + } + + public static function provideFullPhpDocCases(): Generator + { + yield [ + "/**\n *\n */", + new PhpDocNode([]), + ]; + + yield [ + "/**\n * It works\n */", + new PhpDocNode([ + new PhpDocTextNode('It works'), + ]), + ]; + + yield [ + "/**\n * It works\n *\n * with empty lines\n */", + new PhpDocNode([ + new PhpDocTextNode('It works'), + new PhpDocTextNode(''), + new PhpDocTextNode('with empty lines'), + ]), + ]; + + yield [ + "/**\n * Foo\n *\n * @deprecated Because of reasons.\n */", + new PhpDocNode([ + new PhpDocTextNode('Foo'), + new PhpDocTextNode(''), + new PhpDocTagNode('@deprecated', new DeprecatedTagValueNode('Because of reasons.')), + ]), + ]; + } + + public static function provideOtherCases(): Generator + { + $string = new IdentifierTypeNode('string'); + + yield from [ + ['', new GenericTagValueNode('')], + ['Foo bar', new GenericTagValueNode('Foo bar')], + ]; + + yield [ + '#desc', + new InvalidTagValueNode( + '#desc', + new ParserException('#desc', Lexer::TOKEN_OTHER, 11, Lexer::TOKEN_IDENTIFIER) + ), + ]; + + yield from [ + ['', new DeprecatedTagValueNode('')], + ['Because of reasons.', new DeprecatedTagValueNode('Because of reasons.')], + ]; + + yield from [ + ['string $foo', new VarTagValueNode($string, '$foo', '')], + ['string $foo Description.', new VarTagValueNode($string, '$foo', 'Description.')], + ]; + + $bar = new IdentifierTypeNode('Foo\\Bar'); + $baz = new IdentifierTypeNode('Foo\\Baz'); + + yield from [ + ['TValue', new TemplateTagValueNode('TValue', null, '', null)], + ['TValue of Foo\\Bar', new TemplateTagValueNode('TValue', $bar, '', null)], + ['TValue = Foo\\Bar', new TemplateTagValueNode('TValue', null, '', $bar)], + ['TValue of Foo\\Bar = Foo\\Baz', new TemplateTagValueNode('TValue', $bar, '', $baz)], + ['TValue Description.', new TemplateTagValueNode('TValue', null, 'Description.', null)], + ['TValue of Foo\\Bar = Foo\\Baz Description.', new TemplateTagValueNode('TValue', $bar, 'Description.', $baz)], + ]; + } + + public static function provideMethodCases(): Generator + { + $string = new IdentifierTypeNode('string'); + $foo = new IdentifierTypeNode('Foo\\Foo'); + + yield from [ + ['string $foo', new ParamOutTagValueNode($string, '$foo', '')], + ['string $foo Description.', new ParamOutTagValueNode($string, '$foo', 'Description.')], + ]; + + yield from [ + ['Foo\\Foo', new ReturnTagValueNode($foo, '')], + ['string Description.', new ReturnTagValueNode($string, 'Description.')], + ]; + + yield from [ + ['string', new SelfOutTagValueNode($string, '')], + ['string Description.', new SelfOutTagValueNode($string, 'Description.')], + ]; + + yield from [ + ['Foo\\Foo', new ThrowsTagValueNode($foo, '')], + ['Foo\\Foo Description.', new ThrowsTagValueNode($foo, 'Description.')], + ]; + + yield from [ + ['string $foo', new ParamTagValueNode($string, false, '$foo', '', false)], + ['string &$foo', new ParamTagValueNode($string, false, '$foo', '', true)], + ['string ...$foo', new ParamTagValueNode($string, true, '$foo', '', false)], + ['string &...$foo', new ParamTagValueNode($string, true, '$foo', '', true)], + ['string $foo Description.', new ParamTagValueNode($string, false, '$foo', 'Description.', false)], + ['string &...$foo Description.', new ParamTagValueNode($string, true, '$foo', 'Description.', true)], + ['$foo', new TypelessParamTagValueNode(false, '$foo', '', false)], + ['&$foo', new TypelessParamTagValueNode(false, '$foo', '', true)], + ['&...$foo', new TypelessParamTagValueNode(true, '$foo', '', true)], + ['$foo Description.', new TypelessParamTagValueNode(false, '$foo', 'Description.', false)], + ['&...$foo Description.', new TypelessParamTagValueNode(true, '$foo', 'Description.', true)], + ]; + } + + public static function provideClassCases(): Generator + { + $string = new IdentifierTypeNode('string'); + $bar = new IdentifierTypeNode('Foo\\Bar'); + $arrayOfStrings = new GenericTypeNode(new IdentifierTypeNode('array'), [$string]); + + yield from [ + ['PHPUnit\\TestCase', new MixinTagValueNode(new IdentifierTypeNode('PHPUnit\\TestCase'), '')], + ['Foo\\Bar Baz', new MixinTagValueNode(new IdentifierTypeNode('Foo\\Bar'), 'Baz')], + ]; + + yield from [ + ['Foo array', new TypeAliasTagValueNode('Foo', $arrayOfStrings)], + ['Test from Foo\Bar', new TypeAliasImportTagValueNode('Test', $bar, null)], + ['Test from Foo\Bar as Foo', new TypeAliasImportTagValueNode('Test', $bar, 'Foo')], + ]; + + yield from [ + [ + 'array', + new ExtendsTagValueNode($arrayOfStrings, ''), + ], + [ + 'array How did we manage to extend an array?', + new ExtendsTagValueNode($arrayOfStrings, 'How did we manage to extend an array?'), + ], + [ + 'array', + new ImplementsTagValueNode($arrayOfStrings, ''), + ], + [ + 'array How did we manage to implement an array?', + new ImplementsTagValueNode($arrayOfStrings, 'How did we manage to implement an array?'), + ], + [ + 'array', + new UsesTagValueNode($arrayOfStrings, ''), + ], + [ + 'array How did we manage to use an array?', + new UsesTagValueNode($arrayOfStrings, 'How did we manage to use an array?'), + ], + ]; + + yield from [ + ['string $foo', new PropertyTagValueNode($string, '$foo', '')], + ['string $foo Description.', new PropertyTagValueNode($string, '$foo', 'Description.')], + ]; + + yield from [ + [ + 'foo', + new MethodTagValueParameterNode(null, false, false, 'foo', null), + ], + [ + 'string foo', + new MethodTagValueParameterNode($string, false, false, 'foo', null), + ], + [ + '&foo', + new MethodTagValueParameterNode(null, true, false, 'foo', null), + ], + [ + 'string &foo', + new MethodTagValueParameterNode($string, true, false, 'foo', null), + ], + [ + 'string &foo = bar', + new MethodTagValueParameterNode($string, true, false, 'foo', new ConstExprStringNode('bar')), + ], + [ + '&...foo', + new MethodTagValueParameterNode(null, true, true, 'foo', null), + ], + [ + 'string ...foo', + new MethodTagValueParameterNode($string, false, true, 'foo', null), + ], + [ + 'string foo()', + new MethodTagValueNode(false, $string, 'foo', [], '', []), + ], + [ + 'static string bar() Description', + new MethodTagValueNode(true, $string, 'bar', [], 'Description', []), + ], + [ + 'baz(string &foo, string ...foo)', + new MethodTagValueNode(false, null, 'baz', [ + new MethodTagValueParameterNode($string, true, false, 'foo', null), + new MethodTagValueParameterNode($string, false, true, 'foo', null), + ], '', []), + ], + ]; + } + + public static function provideAssertionCases(): Generator + { + $string = new IdentifierTypeNode('string'); + + yield from [ + [ + 'string $foo->bar() description', + new AssertTagMethodValueNode($string, '$foo', 'bar', false, 'description', false), + ], + [ + '=string $foo->bar()', + new AssertTagMethodValueNode($string, '$foo', 'bar', false, '', true), + ], + [ + '!string $foo->bar() foobar', + new AssertTagMethodValueNode($string, '$foo', 'bar', true, 'foobar', false), + ], + [ + '!=string $foo->bar()', + new AssertTagMethodValueNode($string, '$foo', 'bar', true, '', true), + ], + [ + 'string $foo->bar description', + new AssertTagPropertyValueNode($string, '$foo', 'bar', false, 'description', false), + ], + [ + '=string $foo->bar', + new AssertTagPropertyValueNode($string, '$foo', 'bar', false, '', true), + ], + [ + '!string $foo->bar foobar', + new AssertTagPropertyValueNode($string, '$foo', 'bar', true, 'foobar', false), + ], + [ + '!=string $foo->bar', + new AssertTagPropertyValueNode($string, '$foo', 'bar', true, '', true), + ], + [ + 'string $foo description', + new AssertTagValueNode($string, '$foo', false, 'description', false), + ], + [ + '=string $foo', + new AssertTagValueNode($string, '$foo', false, '', true), + ], + [ + '!string $foo foobar', + new AssertTagValueNode($string, '$foo', true, 'foobar', false), + ], + [ + '!=string $foo', + new AssertTagValueNode($string, '$foo', true, '', true), + ], + ]; + + yield from [ + ['string $foo', new ParamOutTagValueNode($string, '$foo', '')], + ['string $foo Description.', new ParamOutTagValueNode($string, '$foo', 'Description.')], + ]; + } + +} diff --git a/tests/PHPStan/Ast/ToString/TypeToStringTest.php b/tests/PHPStan/Ast/ToString/TypeToStringTest.php new file mode 100644 index 00000000..a6849006 --- /dev/null +++ b/tests/PHPStan/Ast/ToString/TypeToStringTest.php @@ -0,0 +1,221 @@ +assertSame($expected, (string) $node); + } + + public static function provideSimpleCases(): Generator + { + yield from [ + ['string', new IdentifierTypeNode('string')], + ['Foo\\Bar', new IdentifierTypeNode('Foo\\Bar')], + ['null', new ConstTypeNode(new ConstExprNullNode())], + ['$this', new ThisTypeNode()], + ]; + } + + public static function provideArrayCases(): Generator + { + yield from [ + ['$this[]', new ArrayTypeNode(new ThisTypeNode())], + ['array[int]', new OffsetAccessTypeNode(new IdentifierTypeNode('array'), new IdentifierTypeNode('int'))], + ]; + + yield from [ + ['array{}', new ArrayShapeNode([])], + ['array{...}', new ArrayShapeNode([], false)], + [ + 'array{string, int, ...}', + new ArrayShapeNode([ + new ArrayShapeItemNode(null, false, new IdentifierTypeNode('string')), + new ArrayShapeItemNode(null, false, new IdentifierTypeNode('int')), + ], false), + ], + [ + 'array{foo: Foo, bar?: Bar, 1: Baz}', + new ArrayShapeNode([ + new ArrayShapeItemNode(new ConstExprStringNode('foo'), false, new IdentifierTypeNode('Foo')), + new ArrayShapeItemNode(new ConstExprStringNode('bar'), true, new IdentifierTypeNode('Bar')), + new ArrayShapeItemNode(new ConstExprIntegerNode('1'), false, new IdentifierTypeNode('Baz')), + ]), + ], + ]; + } + + public static function provideCallableCases(): Generator + { + yield from [ + [ + '\\Closure(): string', + new CallableTypeNode(new IdentifierTypeNode('\Closure'), [], new IdentifierTypeNode('string')), + ], + [ + 'callable(int, int $foo): void', + new CallableTypeNode(new IdentifierTypeNode('callable'), [ + new CallableTypeParameterNode(new IdentifierTypeNode('int'), false, false, '', false), + new CallableTypeParameterNode(new IdentifierTypeNode('int'), false, false, '$foo', false), + ], new IdentifierTypeNode('void')), + ], + [ + 'callable(int = default, int $foo = default): void', + new CallableTypeNode(new IdentifierTypeNode('callable'), [ + new CallableTypeParameterNode(new IdentifierTypeNode('int'), false, false, '', true), + new CallableTypeParameterNode(new IdentifierTypeNode('int'), false, false, '$foo', true), + ], new IdentifierTypeNode('void')), + ], + [ + 'callable(int &, int &$foo): void', + new CallableTypeNode(new IdentifierTypeNode('callable'), [ + new CallableTypeParameterNode(new IdentifierTypeNode('int'), true, false, '', false), + new CallableTypeParameterNode(new IdentifierTypeNode('int'), true, false, '$foo', false), + ], new IdentifierTypeNode('void')), + ], + [ + 'callable(string ...$foo): void', + new CallableTypeNode(new IdentifierTypeNode('callable'), [ + new CallableTypeParameterNode(new IdentifierTypeNode('string'), false, true, '$foo', false), + ], new IdentifierTypeNode('void')), + ], + ]; + } + + public static function provideGenericCases(): Generator + { + yield from [ + [ + 'array', + new GenericTypeNode(new IdentifierTypeNode('array'), [new IdentifierTypeNode('string')]), + ], + [ + 'array', + new GenericTypeNode( + new IdentifierTypeNode('array'), + [new IdentifierTypeNode('string'), new IdentifierTypeNode('int')], + [GenericTypeNode::VARIANCE_INVARIANT, GenericTypeNode::VARIANCE_BIVARIANT] + ), + ], + [ + 'Foo\Bar', + new GenericTypeNode( + new IdentifierTypeNode('Foo\\Bar'), + [new IdentifierTypeNode('string'), new IdentifierTypeNode('int')], + [GenericTypeNode::VARIANCE_COVARIANT, GenericTypeNode::VARIANCE_CONTRAVARIANT] + ), + ], + ]; + } + + public static function provideConditionalCases(): Generator + { + yield from [ + [ + '(TKey is int ? list : list)', + new ConditionalTypeNode( + new IdentifierTypeNode('TKey'), + new IdentifierTypeNode('int'), + new GenericTypeNode(new IdentifierTypeNode('list'), [new IdentifierTypeNode('int')]), + new GenericTypeNode(new IdentifierTypeNode('list'), [new IdentifierTypeNode('string')]), + false + ), + ], + [ + '(TValue is not array ? int : int[])', + new ConditionalTypeNode( + new IdentifierTypeNode('TValue'), + new IdentifierTypeNode('array'), + new IdentifierTypeNode('int'), + new ArrayTypeNode(new IdentifierTypeNode('int')), + true + ), + ], + [ + '($foo is Exception ? never : string)', + new ConditionalTypeForParameterNode( + '$foo', + new IdentifierTypeNode('Exception'), + new IdentifierTypeNode('never'), + new IdentifierTypeNode('string'), + false + ), + ], + [ + '($foo is not Exception ? string : never)', + new ConditionalTypeForParameterNode( + '$foo', + new IdentifierTypeNode('Exception'), + new IdentifierTypeNode('string'), + new IdentifierTypeNode('never'), + true + ), + ], + ]; + } + + public static function provideCombinedCases(): Generator + { + yield from [ + ['?string', new NullableTypeNode(new IdentifierTypeNode('string'))], + [ + '(Foo & Bar)', + new IntersectionTypeNode([ + new IdentifierTypeNode('Foo'), + new IdentifierTypeNode('Bar'), + ]), + ], + [ + '(Foo | Bar)', + new UnionTypeNode([ + new IdentifierTypeNode('Foo'), + new IdentifierTypeNode('Bar'), + ]), + ], + [ + '((Foo & Bar) | Baz)', + new UnionTypeNode([ + new IntersectionTypeNode([ + new IdentifierTypeNode('Foo'), + new IdentifierTypeNode('Bar'), + ]), + new IdentifierTypeNode('Baz'), + ]), + ], + ]; + } + +} From 57918d9fd58727a9eab4440c84bfb9789f5a7f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Mon, 19 Dec 2022 13:25:01 +0100 Subject: [PATCH 06/59] Create release-toot.yml --- .github/workflows/release-toot.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/release-toot.yml diff --git a/.github/workflows/release-toot.yml b/.github/workflows/release-toot.yml new file mode 100644 index 00000000..2af0f176 --- /dev/null +++ b/.github/workflows/release-toot.yml @@ -0,0 +1,21 @@ +name: Toot release + +# More triggers +# https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#release +on: + release: + types: [published] + +jobs: + toot: + runs-on: ubuntu-latest + steps: + - uses: cbrgm/mastodon-github-action@v1 + if: ${{ !github.event.repository.private }} + with: + # GitHub event payload + # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#release + message: "New release: ${{ github.event.repository.name }} ${{ github.event.release.tag_name }} ${{ github.event.release.html_url }} #phpstan" + env: + MASTODON_URL: phpc.social + MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} From 61800f71a5526081d1b5633766aa88341f1ade76 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 20 Dec 2022 21:51:49 +0100 Subject: [PATCH 07/59] Fix ConstExprArrayItemNode::__toString() --- src/Ast/ConstExpr/ConstExprArrayItemNode.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Ast/ConstExpr/ConstExprArrayItemNode.php b/src/Ast/ConstExpr/ConstExprArrayItemNode.php index d0695f82..ef144521 100644 --- a/src/Ast/ConstExpr/ConstExprArrayItemNode.php +++ b/src/Ast/ConstExpr/ConstExprArrayItemNode.php @@ -3,6 +3,7 @@ namespace PHPStan\PhpDocParser\Ast\ConstExpr; use PHPStan\PhpDocParser\Ast\NodeAttributes; +use function sprintf; class ConstExprArrayItemNode implements ConstExprNode { @@ -25,11 +26,11 @@ public function __construct(?ConstExprNode $key, ConstExprNode $value) public function __toString(): string { if ($this->key !== null) { - return "{$this->key} => {$this->value}"; + return sprintf('%s => %s', $this->key, $this->value); } - return "{$this->value}"; + return (string) $this->value; } } From a6e53116eb575403aab7e9f31abfdbed1ab49b32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Tue, 20 Dec 2022 22:04:59 +0100 Subject: [PATCH 08/59] Update release-toot.yml --- .github/workflows/release-toot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-toot.yml b/.github/workflows/release-toot.yml index 2af0f176..6a1c8156 100644 --- a/.github/workflows/release-toot.yml +++ b/.github/workflows/release-toot.yml @@ -17,5 +17,5 @@ jobs: # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#release message: "New release: ${{ github.event.repository.name }} ${{ github.event.release.tag_name }} ${{ github.event.release.html_url }} #phpstan" env: - MASTODON_URL: phpc.social + MASTODON_URL: https://phpc.social MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} From b06595655ec8db5c31751185219829afefcdd04e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 6 Jan 2023 14:32:51 +0100 Subject: [PATCH 09/59] New workflow - test Slevomat CS against latest parser --- .../test-slevomat-coding-standard.yml | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/workflows/test-slevomat-coding-standard.yml diff --git a/.github/workflows/test-slevomat-coding-standard.yml b/.github/workflows/test-slevomat-coding-standard.yml new file mode 100644 index 00000000..b7d81438 --- /dev/null +++ b/.github/workflows/test-slevomat-coding-standard.yml @@ -0,0 +1,67 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Test Slevomat Coding Standard" + +on: + pull_request: + push: + branches: + - "1.9.x" + +jobs: + tests: + name: "Tests" + runs-on: "ubuntu-latest" + + strategy: + fail-fast: false + matrix: + php-version: + - "7.3" + - "7.4" + - "8.0" + - "8.1" + - "8.2" + + steps: + - name: "Checkout" + uses: actions/checkout@v3 + + - name: "Checkout Slevomat Coding Standard" + uses: actions/checkout@v3 + with: + repository: slevomat/coding-standard + path: slevomat-cs + ref: 8.7.1 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + + - name: "Install dependencies" + working-directory: slevomat-cs + run: "composer install --no-interaction --no-progress" + + - name: "Remove stable phpdoc-parser" + working-directory: slevomat-cs + run: "rm -r vendor/phpstan/phpdoc-parser/src" + + - name: "Remove top-level phpcs.xml" + run: "rm phpcs.xml" + + - name: "Copy phpdoc-parser" + run: "cp -r src/ slevomat-cs/vendor/phpstan/phpdoc-parser/src" + + - name: "Tests" + working-directory: slevomat-cs + run: "bin/phpunit" + + - name: "PHPStan" + working-directory: slevomat-cs + run: "bin/phpstan analyse -c build/PHPStan/phpstan.neon" + + - name: "PHPStan in tests" + working-directory: slevomat-cs + run: "bin/phpstan analyse -c build/PHPStan/phpstan.tests.neon" From 57090cfccbfaa639e703c007486d605a6e80f56d Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Sun, 29 Jan 2023 23:41:23 +0900 Subject: [PATCH 10/59] Add support for list shapes --- src/Ast/Type/ArrayShapeNode.php | 14 ++++++++++++-- src/Parser/TypeParser.php | 18 +++++++++++------- tests/PHPStan/Parser/TypeParserTest.php | 22 ++++++++++++++++++++++ 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/Ast/Type/ArrayShapeNode.php b/src/Ast/Type/ArrayShapeNode.php index 3778436d..41941f80 100644 --- a/src/Ast/Type/ArrayShapeNode.php +++ b/src/Ast/Type/ArrayShapeNode.php @@ -8,6 +8,9 @@ class ArrayShapeNode implements TypeNode { + public const KIND_ARRAY = 'array'; + public const KIND_LIST = 'list'; + use NodeAttributes; /** @var ArrayShapeItemNode[] */ @@ -16,10 +19,17 @@ class ArrayShapeNode implements TypeNode /** @var bool */ public $sealed; - public function __construct(array $items, bool $sealed = true) + /** @var self::KIND_* */ + public $kind; + + /** + * @param self::KIND_* $kind + */ + public function __construct(array $items, bool $sealed = true, string $kind = self::KIND_ARRAY) { $this->items = $items; $this->sealed = $sealed; + $this->kind = $kind; } @@ -31,7 +41,7 @@ public function __toString(): string $items[] = '...'; } - return 'array{' . implode(', ', $items) . '}'; + return $this->kind . '{' . implode(', ', $items) . '}'; } } diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index f45aafdc..993bf873 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -5,6 +5,7 @@ use LogicException; use PHPStan\PhpDocParser\Ast; use PHPStan\PhpDocParser\Lexer\Lexer; +use function in_array; use function strpos; use function trim; @@ -123,8 +124,8 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); - } elseif ($type->name === 'array' && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { - $type = $this->parseArrayShape($tokens, $type); + } elseif (in_array($type->name, ['array', 'list'], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { + $type = $this->parseArrayShape($tokens, $type, $type->name); if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); @@ -439,8 +440,8 @@ private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNo if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { $type = $this->parseGeneric($tokens, $type); - } elseif ($type->name === 'array' && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { - $type = $this->parseArrayShape($tokens, $type); + } elseif (in_array($type->name, ['array', 'list'], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { + $type = $this->parseArrayShape($tokens, $type, $type->name); } } @@ -499,8 +500,11 @@ private function tryParseArrayOrOffsetAccess(TokenIterator $tokens, Ast\Type\Typ } - /** @phpstan-impure */ - private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\ArrayShapeNode + /** + * @phpstan-impure + * @param Ast\Type\ArrayShapeNode::KIND_* $kind + */ + private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, string $kind): Ast\Type\ArrayShapeNode { $tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET); @@ -528,7 +532,7 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type) $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET); - return new Ast\Type\ArrayShapeNode($items, $sealed); + return new Ast\Type\ArrayShapeNode($items, $sealed, $kind); } diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 484823a8..1b7dc067 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -668,6 +668,28 @@ public function provideParseData(): array Lexer::TOKEN_CLOSE_CURLY_BRACKET ), ], + [ + 'list{ + int, + string + }', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('string') + ), + ], + true, + ArrayShapeNode::KIND_LIST + ), + ], [ 'callable(): Foo', new CallableTypeNode( From e27e92d939e2e3636f0a1f0afaba59692c0bf571 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Wed, 8 Feb 2023 02:37:26 +0900 Subject: [PATCH 11/59] Fix list{} parsing --- src/Parser/TypeParser.php | 2 +- tests/PHPStan/Ast/ToString/TypeToStringTest.php | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 993bf873..d196eb94 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -515,7 +515,7 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) { - return new Ast\Type\ArrayShapeNode($items); + return new Ast\Type\ArrayShapeNode($items, true, $kind); } if ($tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC)) { diff --git a/tests/PHPStan/Ast/ToString/TypeToStringTest.php b/tests/PHPStan/Ast/ToString/TypeToStringTest.php index a6849006..a32b0a09 100644 --- a/tests/PHPStan/Ast/ToString/TypeToStringTest.php +++ b/tests/PHPStan/Ast/ToString/TypeToStringTest.php @@ -75,6 +75,15 @@ public static function provideArrayCases(): Generator new ArrayShapeItemNode(new ConstExprIntegerNode('1'), false, new IdentifierTypeNode('Baz')), ]), ], + ['list{}', new ArrayShapeNode([], true, 'list')], + ['list{...}', new ArrayShapeNode([], false, 'list')], + [ + 'list{string, int, ...}', + new ArrayShapeNode([ + new ArrayShapeItemNode(null, false, new IdentifierTypeNode('string')), + new ArrayShapeItemNode(null, false, new IdentifierTypeNode('int')), + ], false, 'list'), + ], ]; } From 888c7c18c2d2937c9f2d5258e2add9d6f96aa947 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 20 Feb 2023 22:28:55 +0100 Subject: [PATCH 12/59] Send PRs against PHPStan 1.10.x --- .github/workflows/send-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/send-pr.yml b/.github/workflows/send-pr.yml index 8be2ba3c..9a5c0973 100644 --- a/.github/workflows/send-pr.yml +++ b/.github/workflows/send-pr.yml @@ -23,7 +23,7 @@ jobs: repository: phpstan/phpstan-src path: phpstan-src token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - ref: 1.9.x + ref: 1.10.x - name: "Install dependencies" working-directory: ./phpstan-src From bcba2e8b5155b8260099fe2011c54e2cc1804273 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Mar 2023 00:15:20 +0000 Subject: [PATCH 13/59] Update metcalfc/changelog-generator action to v4.1.0 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bac4a006..92b72547 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Generate changelog id: changelog - uses: metcalfc/changelog-generator@v4.0.1 + uses: metcalfc/changelog-generator@v4.1.0 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} From 36f37196c26278174dad8a8fbc9aaead36fefca2 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 22 Mar 2023 09:22:35 +0100 Subject: [PATCH 14/59] Update PHPCS --- build-cs/composer.json | 10 ++-- build-cs/composer.lock | 121 ++++++++++++++++++++++------------------- 2 files changed, 68 insertions(+), 63 deletions(-) diff --git a/build-cs/composer.json b/build-cs/composer.json index d10b56a9..16a240bc 100644 --- a/build-cs/composer.json +++ b/build-cs/composer.json @@ -1,11 +1,9 @@ { - "require": { - "php": "^7.4 || ^8.0" - }, "require-dev": { - "consistence-community/coding-standard": "^3.11", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.1", - "slevomat/coding-standard": "^7.0.0" + "consistence-community/coding-standard": "^3.11.0", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", + "slevomat/coding-standard": "^8.8.0", + "squizlabs/php_codesniffer": "^3.5.3" }, "config": { "allow-plugins": { diff --git a/build-cs/composer.lock b/build-cs/composer.lock index 8026676e..c25a151a 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -4,35 +4,35 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e864762ae0c59b5c172d16fa0bea7f70", + "content-hash": "e69c1916405a7e3c8001c1b609a0ee61", "packages": [], "packages-dev": [ { "name": "consistence-community/coding-standard", - "version": "3.11.1", + "version": "3.11.2", "source": { "type": "git", "url": "https://github.com/consistence-community/coding-standard.git", - "reference": "4632fead8c9ee8f50044fcbce9f66c797b34c0df" + "reference": "adb4be482e76990552bf624309d2acc8754ba1bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consistence-community/coding-standard/zipball/4632fead8c9ee8f50044fcbce9f66c797b34c0df", - "reference": "4632fead8c9ee8f50044fcbce9f66c797b34c0df", + "url": "https://api.github.com/repos/consistence-community/coding-standard/zipball/adb4be482e76990552bf624309d2acc8754ba1bd", + "reference": "adb4be482e76990552bf624309d2acc8754ba1bd", "shasum": "" }, "require": { - "php": ">=7.4", - "slevomat/coding-standard": "~7.0", - "squizlabs/php_codesniffer": "~3.6.0" + "php": "~8.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "~3.7.0" }, "replace": { "consistence/coding-standard": "3.10.*" }, "require-dev": { - "phing/phing": "2.16.4", - "php-parallel-lint/php-parallel-lint": "1.3.0", - "phpunit/phpunit": "9.5.4" + "phing/phing": "2.17.0", + "php-parallel-lint/php-parallel-lint": "1.3.1", + "phpunit/phpunit": "9.5.10" }, "type": "library", "autoload": { @@ -70,41 +70,44 @@ ], "support": { "issues": "https://github.com/consistence-community/coding-standard/issues", - "source": "https://github.com/consistence-community/coding-standard/tree/3.11.1" + "source": "https://github.com/consistence-community/coding-standard/tree/3.11.2" }, - "time": "2021-05-03T18:13:22+00:00" + "time": "2022-06-21T08:36:36+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v0.7.2", + "version": "v1.0.0", "source": { "type": "git", - "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", - "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db" + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "4be43904336affa5c2f70744a348312336afd0da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", - "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", + "reference": "4be43904336affa5c2f70744a348312336afd0da", "shasum": "" }, "require": { "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.3", + "php": ">=5.4", "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" }, "require-dev": { "composer/composer": "*", + "ext-json": "*", + "ext-zip": "*", "php-parallel-lint/php-parallel-lint": "^1.3.1", - "phpcompatibility/php-compatibility": "^9.0" + "phpcompatibility/php-compatibility": "^9.0", + "yoast/phpunit-polyfills": "^1.0" }, "type": "composer-plugin", "extra": { - "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" }, "autoload": { "psr-4": { - "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -120,7 +123,7 @@ }, { "name": "Contributors", - "homepage": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer/graphs/contributors" + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" } ], "description": "PHP_CodeSniffer Standards Composer Installer Plugin", @@ -144,23 +147,23 @@ "tests" ], "support": { - "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", - "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "source": "https://github.com/PHPCSStandards/composer-installer" }, - "time": "2022-02-04T12:51:07+00:00" + "time": "2023-01-05T11:28:13+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.5.1", + "version": "1.15.3", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "981cc368a216c988e862a75e526b6076987d1b50" + "reference": "61800f71a5526081d1b5633766aa88341f1ade76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/981cc368a216c988e862a75e526b6076987d1b50", - "reference": "981cc368a216c988e862a75e526b6076987d1b50", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/61800f71a5526081d1b5633766aa88341f1ade76", + "reference": "61800f71a5526081d1b5633766aa88341f1ade76", "shasum": "" }, "require": { @@ -170,6 +173,7 @@ "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", "phpstan/phpstan-strict-rules": "^1.0", "phpunit/phpunit": "^9.5", "symfony/process": "^5.2" @@ -189,43 +193,43 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.5.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.15.3" }, - "time": "2022-05-05T11:32:40+00:00" + "time": "2022-12-20T20:56:55+00:00" }, { "name": "slevomat/coding-standard", - "version": "7.2.1", + "version": "8.8.0", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "aff06ae7a84e4534bf6f821dc982a93a5d477c90" + "reference": "59e25146a4ef0a7b194c5bc55b32dd414345db89" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/aff06ae7a84e4534bf6f821dc982a93a5d477c90", - "reference": "aff06ae7a84e4534bf6f821dc982a93a5d477c90", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/59e25146a4ef0a7b194c5bc55b32dd414345db89", + "reference": "59e25146a4ef0a7b194c5bc55b32dd414345db89", "shasum": "" }, "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7", + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": "^1.5.1", - "squizlabs/php_codesniffer": "^3.6.2" + "phpstan/phpdoc-parser": ">=1.15.2 <1.16.0", + "squizlabs/php_codesniffer": "^3.7.1" }, "require-dev": { - "phing/phing": "2.17.3", + "phing/phing": "2.17.4", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.10|1.7.1", - "phpstan/phpstan-deprecation-rules": "1.0.0", - "phpstan/phpstan-phpunit": "1.0.0|1.1.1", - "phpstan/phpstan-strict-rules": "1.2.3", - "phpunit/phpunit": "7.5.20|8.5.21|9.5.20" + "phpstan/phpstan": "1.4.10|1.9.6", + "phpstan/phpstan-deprecation-rules": "1.1.1", + "phpstan/phpstan-phpunit": "1.0.0|1.3.3", + "phpstan/phpstan-strict-rules": "1.4.4", + "phpunit/phpunit": "7.5.20|8.5.21|9.5.27" }, "type": "phpcodesniffer-standard", "extra": { "branch-alias": { - "dev-master": "7.x-dev" + "dev-master": "8.x-dev" } }, "autoload": { @@ -238,9 +242,13 @@ "MIT" ], "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "keywords": [ + "dev", + "phpcs" + ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/7.2.1" + "source": "https://github.com/slevomat/coding-standard/tree/8.8.0" }, "funding": [ { @@ -252,20 +260,20 @@ "type": "tidelift" } ], - "time": "2022-05-25T10:58:12+00:00" + "time": "2023-01-09T10:46:13+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.6.2", + "version": "3.7.2", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a" + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/5e4e71592f69da17871dba6e80dd51bce74a351a", - "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", "shasum": "" }, "require": { @@ -301,14 +309,15 @@ "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", "keywords": [ "phpcs", - "standards" + "standards", + "static analysis" ], "support": { "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", "source": "https://github.com/squizlabs/PHP_CodeSniffer", "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" }, - "time": "2021-12-12T21:44:58+00:00" + "time": "2023-02-22T23:07:41+00:00" } ], "aliases": [], @@ -316,9 +325,7 @@ "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, - "platform": { - "php": "^7.4 || ^8.0" - }, + "platform": [], "platform-dev": [], "plugin-api-version": "2.3.0" } From afb728a61ab24df0ff4f77f63fa843c9287e878a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 13:25:52 +0000 Subject: [PATCH 15/59] Update dependency slevomat/coding-standard to v8.9.1 --- build-cs/composer.lock | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index c25a151a..1d103c79 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -154,16 +154,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.15.3", + "version": "1.16.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "61800f71a5526081d1b5633766aa88341f1ade76" + "reference": "e27e92d939e2e3636f0a1f0afaba59692c0bf571" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/61800f71a5526081d1b5633766aa88341f1ade76", - "reference": "61800f71a5526081d1b5633766aa88341f1ade76", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/e27e92d939e2e3636f0a1f0afaba59692c0bf571", + "reference": "e27e92d939e2e3636f0a1f0afaba59692c0bf571", "shasum": "" }, "require": { @@ -193,38 +193,38 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.15.3" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.16.1" }, - "time": "2022-12-20T20:56:55+00:00" + "time": "2023-02-07T18:11:17+00:00" }, { "name": "slevomat/coding-standard", - "version": "8.8.0", + "version": "8.9.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "59e25146a4ef0a7b194c5bc55b32dd414345db89" + "reference": "3d4fe0c803ae15829ef72d90d3d4eee3dd9f79b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/59e25146a4ef0a7b194c5bc55b32dd414345db89", - "reference": "59e25146a4ef0a7b194c5bc55b32dd414345db89", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/3d4fe0c803ae15829ef72d90d3d4eee3dd9f79b2", + "reference": "3d4fe0c803ae15829ef72d90d3d4eee3dd9f79b2", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": ">=1.15.2 <1.16.0", + "phpstan/phpdoc-parser": ">=1.16.0 <1.17.0", "squizlabs/php_codesniffer": "^3.7.1" }, "require-dev": { "phing/phing": "2.17.4", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.10|1.9.6", - "phpstan/phpstan-deprecation-rules": "1.1.1", - "phpstan/phpstan-phpunit": "1.0.0|1.3.3", - "phpstan/phpstan-strict-rules": "1.4.4", - "phpunit/phpunit": "7.5.20|8.5.21|9.5.27" + "phpstan/phpstan": "1.4.10|1.10.8", + "phpstan/phpstan-deprecation-rules": "1.1.3", + "phpstan/phpstan-phpunit": "1.0.0|1.3.10", + "phpstan/phpstan-strict-rules": "1.5.0", + "phpunit/phpunit": "7.5.20|8.5.21|9.6.5" }, "type": "phpcodesniffer-standard", "extra": { @@ -234,7 +234,7 @@ }, "autoload": { "psr-4": { - "SlevomatCodingStandard\\": "SlevomatCodingStandard" + "SlevomatCodingStandard\\": "SlevomatCodingStandard/" } }, "notification-url": "https://packagist.org/downloads/", @@ -248,7 +248,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.8.0" + "source": "https://github.com/slevomat/coding-standard/tree/8.9.1" }, "funding": [ { @@ -260,7 +260,7 @@ "type": "tidelift" } ], - "time": "2023-01-09T10:46:13+00:00" + "time": "2023-03-27T11:00:16+00:00" }, { "name": "squizlabs/php_codesniffer", From bfec8729f7e23c40670f98e27e694cfdb13fc12a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 4 Apr 2023 10:05:53 +0200 Subject: [PATCH 16/59] PhpDocParser - option to preserve type alias with parse error --- src/Ast/Type/InvalidTypeNode.php | 37 +++++++ src/Parser/PhpDocParser.php | 16 ++- tests/PHPStan/Parser/PhpDocParserTest.php | 120 +++++++++++++++++++++- 3 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 src/Ast/Type/InvalidTypeNode.php diff --git a/src/Ast/Type/InvalidTypeNode.php b/src/Ast/Type/InvalidTypeNode.php new file mode 100644 index 00000000..af3d3d76 --- /dev/null +++ b/src/Ast/Type/InvalidTypeNode.php @@ -0,0 +1,37 @@ +exceptionArgs = [ + $exception->getCurrentTokenValue(), + $exception->getCurrentTokenType(), + $exception->getCurrentOffset(), + $exception->getExpectedTokenType(), + $exception->getExpectedTokenValue(), + ]; + } + + public function getException(): ParserException + { + return new ParserException(...$this->exceptionArgs); + } + + public function __toString(): string + { + return '*Invalid type*'; + } + +} diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index d9942b3d..9d4564ea 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -28,11 +28,15 @@ class PhpDocParser /** @var bool */ private $requireWhitespaceBeforeDescription; - public function __construct(TypeParser $typeParser, ConstExprParser $constantExprParser, bool $requireWhitespaceBeforeDescription = false) + /** @var bool */ + private $preserveTypeAliasesWithInvalidTypes; + + public function __construct(TypeParser $typeParser, ConstExprParser $constantExprParser, bool $requireWhitespaceBeforeDescription = false, bool $preserveTypeAliasesWithInvalidTypes = false) { $this->typeParser = $typeParser; $this->constantExprParser = $constantExprParser; $this->requireWhitespaceBeforeDescription = $requireWhitespaceBeforeDescription; + $this->preserveTypeAliasesWithInvalidTypes = $preserveTypeAliasesWithInvalidTypes; } @@ -453,6 +457,16 @@ private function parseTypeAliasTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeA // support psalm-type syntax $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL); + if ($this->preserveTypeAliasesWithInvalidTypes) { + try { + $type = $this->typeParser->parse($tokens); + + return new Ast\PhpDoc\TypeAliasTagValueNode($alias, $type); + } catch (ParserException $e) { + return new Ast\PhpDoc\TypeAliasTagValueNode($alias, new Ast\Type\InvalidTypeNode($e)); + } + } + $type = $this->typeParser->parse($tokens); return new Ast\PhpDoc\TypeAliasTagValueNode($alias, $type); diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index aa06b5e7..d70bbd70 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -45,6 +45,7 @@ use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\InvalidTypeNode; use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode; use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; @@ -64,6 +65,9 @@ class PhpDocParserTest extends TestCase /** @var PhpDocParser */ private $phpDocParserWithRequiredWhitespaceBeforeDescription; + /** @var PhpDocParser */ + private $phpDocParserWithPreserveTypeAliasesWithInvalidTypes; + protected function setUp(): void { parent::setUp(); @@ -72,6 +76,7 @@ protected function setUp(): void $typeParser = new TypeParser($constExprParser); $this->phpDocParser = new PhpDocParser($typeParser, $constExprParser); $this->phpDocParserWithRequiredWhitespaceBeforeDescription = new PhpDocParser($typeParser, $constExprParser, true); + $this->phpDocParserWithPreserveTypeAliasesWithInvalidTypes = new PhpDocParser($typeParser, $constExprParser, true, true); } @@ -104,7 +109,8 @@ public function testParse( string $label, string $input, PhpDocNode $expectedPhpDocNode, - ?PhpDocNode $withRequiredWhitespaceBeforeDescriptionExpectedPhpDocNode = null + ?PhpDocNode $withRequiredWhitespaceBeforeDescriptionExpectedPhpDocNode = null, + ?PhpDocNode $withPreserveTypeAliasesWithInvalidTypesExpectedPhpDocNode = null ): void { $this->executeTestParse( @@ -120,6 +126,13 @@ public function testParse( $input, $withRequiredWhitespaceBeforeDescriptionExpectedPhpDocNode ?? $expectedPhpDocNode ); + + $this->executeTestParse( + $this->phpDocParserWithPreserveTypeAliasesWithInvalidTypes, + $label, + $input, + $withPreserveTypeAliasesWithInvalidTypesExpectedPhpDocNode ?? $withRequiredWhitespaceBeforeDescriptionExpectedPhpDocNode ?? $expectedPhpDocNode + ); } @@ -3834,6 +3847,111 @@ public function provideTypeAliasTagsData(): Iterator ) ), ]), + null, + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-type', + new TypeAliasTagValueNode( + 'TypeAlias', + new InvalidTypeNode(new ParserException( + '*/', + Lexer::TOKEN_CLOSE_PHPDOC, + 28, + Lexer::TOKEN_IDENTIFIER, + null + )) + ) + ), + ]), + ]; + + yield [ + 'invalid without type with newline', + '/** + * @phpstan-type TypeAlias + */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-type', + new InvalidTagValueNode( + 'TypeAlias', + new ParserException( + "\n\t\t\t ", + Lexer::TOKEN_PHPDOC_EOL, + 34, + Lexer::TOKEN_IDENTIFIER + ) + ) + ), + ]), + null, + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-type', + new TypeAliasTagValueNode( + 'TypeAlias', + new InvalidTypeNode(new ParserException( + "\n\t\t\t ", + Lexer::TOKEN_PHPDOC_EOL, + 34, + Lexer::TOKEN_IDENTIFIER, + null + )) + ) + ), + ]), + ]; + + yield [ + 'invalid without type but valid tag below', + '/** + * @phpstan-type TypeAlias + * @mixin T + */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-type', + new InvalidTagValueNode( + 'TypeAlias', + new ParserException( + "\n\t\t\t * ", + Lexer::TOKEN_PHPDOC_EOL, + 34, + Lexer::TOKEN_IDENTIFIER + ) + ) + ), + new PhpDocTagNode( + '@mixin', + new MixinTagValueNode( + new IdentifierTypeNode('T'), + '' + ) + ), + ]), + null, + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-type', + new TypeAliasTagValueNode( + 'TypeAlias', + new InvalidTypeNode(new ParserException( + "\n\t\t\t * ", + Lexer::TOKEN_PHPDOC_EOL, + 34, + Lexer::TOKEN_IDENTIFIER, + null + )) + ) + ), + new PhpDocTagNode( + '@mixin', + new MixinTagValueNode( + new IdentifierTypeNode('T'), + '' + ) + ), + ]), ]; yield [ From d3753fcb3abc6f78f5de6f72153d4b9c99c72dee Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 4 Apr 2023 12:02:55 +0200 Subject: [PATCH 17/59] Fix parsing invalid types in type aliases --- src/Parser/PhpDocParser.php | 11 ++ tests/PHPStan/Parser/PhpDocParserTest.php | 118 ++++++++++++++++++++++ 2 files changed, 129 insertions(+) diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index 9d4564ea..cee8042d 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -460,9 +460,20 @@ private function parseTypeAliasTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeA if ($this->preserveTypeAliasesWithInvalidTypes) { try { $type = $this->typeParser->parse($tokens); + if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) { + if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) { + throw new ParserException( + $tokens->currentTokenValue(), + $tokens->currentTokenType(), + $tokens->currentTokenOffset(), + Lexer::TOKEN_PHPDOC_EOL + ); + } + } return new Ast\PhpDoc\TypeAliasTagValueNode($alias, $type); } catch (ParserException $e) { + $this->parseOptionalDescription($tokens); return new Ast\PhpDoc\TypeAliasTagValueNode($alias, new Ast\Type\InvalidTypeNode($e)); } } diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index d70bbd70..507832a5 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -916,6 +916,24 @@ public function provideVarTagsData(): Iterator ) ), ]), + null, + new PhpDocNode([ + new PhpDocTagNode( + '@psalm-type', + new TypeAliasTagValueNode( + 'PARTSTRUCTURE_PARAM', + new InvalidTypeNode( + new ParserException( + '{', + Lexer::TOKEN_OPEN_CURLY_BRACKET, + 44, + Lexer::TOKEN_PHPDOC_EOL, + null + ) + ) + ) + ), + ]), ]; } @@ -3954,6 +3972,106 @@ public function provideTypeAliasTagsData(): Iterator ]), ]; + yield [ + 'invalid type that should be an error', + '/** + * @phpstan-type Foo array{} + * @phpstan-type InvalidFoo what{} + */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-type', + new InvalidTagValueNode( + "Unexpected token \"{\", expected '*/' at offset 65", + new ParserException( + '{', + Lexer::TOKEN_OPEN_CURLY_BRACKET, + 65, + Lexer::TOKEN_CLOSE_PHPDOC, + null + ) + ) + ), + ]), + null, + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-type', + new TypeAliasTagValueNode( + 'Foo', + new ArrayShapeNode([]) + ) + ), + new PhpDocTagNode( + '@phpstan-type', + new TypeAliasTagValueNode( + 'InvalidFoo', + new InvalidTypeNode(new ParserException( + '{', + Lexer::TOKEN_OPEN_CURLY_BRACKET, + 65, + Lexer::TOKEN_PHPDOC_EOL, + null + )) + ) + ), + ]), + ]; + + yield [ + 'invalid type that should be an error followed by valid again', + '/** + * @phpstan-type Foo array{} + * @phpstan-type InvalidFoo what{} + * @phpstan-type Bar array{} + */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-type', + new InvalidTagValueNode( + "Unexpected token \"{\", expected '*/' at offset 65", + new ParserException( + '{', + Lexer::TOKEN_OPEN_CURLY_BRACKET, + 65, + Lexer::TOKEN_CLOSE_PHPDOC, + null + ) + ) + ), + ]), + null, + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-type', + new TypeAliasTagValueNode( + 'Foo', + new ArrayShapeNode([]) + ) + ), + new PhpDocTagNode( + '@phpstan-type', + new TypeAliasTagValueNode( + 'InvalidFoo', + new InvalidTypeNode(new ParserException( + '{', + Lexer::TOKEN_OPEN_CURLY_BRACKET, + 65, + Lexer::TOKEN_PHPDOC_EOL, + null + )) + ) + ), + new PhpDocTagNode( + '@phpstan-type', + new TypeAliasTagValueNode( + 'Bar', + new ArrayShapeNode([]) + ) + ), + ]), + ]; + yield [ 'invalid empty', '/** @phpstan-type */', From 882eabc9b6a12e25c27091a261397f9c8792e722 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 6 Apr 2023 09:00:21 +0200 Subject: [PATCH 18/59] Support for object shapes --- src/Ast/Type/ObjectShapeItemNode.php | 48 ++++ src/Ast/Type/ObjectShapeNode.php | 28 ++ src/Parser/TypeParser.php | 68 ++++- tests/PHPStan/Parser/PhpDocParserTest.php | 8 +- tests/PHPStan/Parser/TypeParserTest.php | 321 ++++++++++++++++++++++ 5 files changed, 467 insertions(+), 6 deletions(-) create mode 100644 src/Ast/Type/ObjectShapeItemNode.php create mode 100644 src/Ast/Type/ObjectShapeNode.php diff --git a/src/Ast/Type/ObjectShapeItemNode.php b/src/Ast/Type/ObjectShapeItemNode.php new file mode 100644 index 00000000..2f012406 --- /dev/null +++ b/src/Ast/Type/ObjectShapeItemNode.php @@ -0,0 +1,48 @@ +keyName = $keyName; + $this->optional = $optional; + $this->valueType = $valueType; + } + + + public function __toString(): string + { + if ($this->keyName !== null) { + return sprintf( + '%s%s: %s', + (string) $this->keyName, + $this->optional ? '?' : '', + (string) $this->valueType + ); + } + + return (string) $this->valueType; + } + +} diff --git a/src/Ast/Type/ObjectShapeNode.php b/src/Ast/Type/ObjectShapeNode.php new file mode 100644 index 00000000..1ec2dca4 --- /dev/null +++ b/src/Ast/Type/ObjectShapeNode.php @@ -0,0 +1,28 @@ +items = $items; + } + + public function __toString(): string + { + $items = $this->items; + + return 'object{' . implode(', ', $items) . '}'; + } + +} diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index d196eb94..7a67b84d 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -124,8 +124,12 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); - } elseif (in_array($type->name, ['array', 'list'], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { - $type = $this->parseArrayShape($tokens, $type, $type->name); + } elseif (in_array($type->name, ['array', 'list', 'object'], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { + if ($type->name === 'object') { + $type = $this->parseObjectShape($tokens); + } else { + $type = $this->parseArrayShape($tokens, $type, $type->name); + } if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); @@ -582,4 +586,64 @@ private function parseArrayShapeKey(TokenIterator $tokens) return $key; } + /** + * @phpstan-impure + */ + private function parseObjectShape(TokenIterator $tokens): Ast\Type\ObjectShapeNode + { + $tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET); + + $items = []; + + do { + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + + if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) { + return new Ast\Type\ObjectShapeNode($items); + } + + $items[] = $this->parseObjectShapeItem($tokens); + + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)); + + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET); + + return new Ast\Type\ObjectShapeNode($items); + } + + /** @phpstan-impure */ + private function parseObjectShapeItem(TokenIterator $tokens): Ast\Type\ObjectShapeItemNode + { + $key = $this->parseObjectShapeKey($tokens); + $optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE); + $tokens->consumeTokenType(Lexer::TOKEN_COLON); + $value = $this->parse($tokens); + + return new Ast\Type\ObjectShapeItemNode($key, $optional, $value); + } + + /** + * @phpstan-impure + * @return Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode + */ + private function parseObjectShapeKey(TokenIterator $tokens) + { + if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) { + $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'")); + $tokens->next(); + + } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) { + $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"')); + $tokens->next(); + + } else { + $key = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue()); + $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); + } + + return $key; + } + } diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 507832a5..8c1f17c6 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -901,16 +901,16 @@ public function provideVarTagsData(): Iterator yield [ 'invalid object shape', - '/** @psalm-type PARTSTRUCTURE_PARAM = object{attribute:string, value?:string} */', + '/** @psalm-type PARTSTRUCTURE_PARAM = objecttt{attribute:string, value?:string} */', new PhpDocNode([ new PhpDocTagNode( '@psalm-type', new InvalidTagValueNode( - 'Unexpected token "{", expected \'*/\' at offset 44', + 'Unexpected token "{", expected \'*/\' at offset 46', new ParserException( '{', Lexer::TOKEN_OPEN_CURLY_BRACKET, - 44, + 46, Lexer::TOKEN_CLOSE_PHPDOC ) ) @@ -926,7 +926,7 @@ public function provideVarTagsData(): Iterator new ParserException( '{', Lexer::TOKEN_OPEN_CURLY_BRACKET, - 44, + 46, Lexer::TOKEN_PHPDOC_EOL, null ) diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 1b7dc067..ed5d725a 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -19,6 +19,8 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode; use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode; +use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode; +use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode; use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode; use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; @@ -1539,6 +1541,325 @@ public function provideParseData(): array ] ), ], + [ + 'object{a: int}', + new ObjectShapeNode([ + new ObjectShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + ]), + ], + [ + 'object{a: ?int}', + new ObjectShapeNode([ + new ObjectShapeItemNode( + new IdentifierTypeNode('a'), + false, + new NullableTypeNode( + new IdentifierTypeNode('int') + ) + ), + ]), + ], + [ + 'object{a?: ?int}', + new ObjectShapeNode([ + new ObjectShapeItemNode( + new IdentifierTypeNode('a'), + true, + new NullableTypeNode( + new IdentifierTypeNode('int') + ) + ), + ]), + ], + [ + 'object{a: int, b: string}', + new ObjectShapeNode([ + new ObjectShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + new ObjectShapeItemNode( + new IdentifierTypeNode('b'), + false, + new IdentifierTypeNode('string') + ), + ]), + ], + [ + 'object{a: int, b: array{c: callable(): int}}', + new ObjectShapeNode([ + new ObjectShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + new ObjectShapeItemNode( + new IdentifierTypeNode('b'), + false, + new ArrayShapeNode([ + new ArrayShapeItemNode( + new IdentifierTypeNode('c'), + false, + new CallableTypeNode( + new IdentifierTypeNode('callable'), + [], + new IdentifierTypeNode('int') + ) + ), + ]) + ), + ]), + ], + [ + 'object{a: int, b: object{c: callable(): int}}', + new ObjectShapeNode([ + new ObjectShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + new ObjectShapeItemNode( + new IdentifierTypeNode('b'), + false, + new ObjectShapeNode([ + new ObjectShapeItemNode( + new IdentifierTypeNode('c'), + false, + new CallableTypeNode( + new IdentifierTypeNode('callable'), + [], + new IdentifierTypeNode('int') + ) + ), + ]) + ), + ]), + ], + [ + '?object{a: int}', + new NullableTypeNode( + new ObjectShapeNode([ + new ObjectShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + ]) + ), + ], + [ + 'object{', + new ParserException( + '', + Lexer::TOKEN_END, + 7, + Lexer::TOKEN_IDENTIFIER + ), + ], + [ + 'object{a => int}', + new ParserException( + '=>', + Lexer::TOKEN_OTHER, + 9, + Lexer::TOKEN_COLON + ), + ], + [ + 'object{int}', + new ParserException( + '}', + Lexer::TOKEN_CLOSE_CURLY_BRACKET, + 10, + Lexer::TOKEN_COLON + ), + ], + [ + 'object{0: int}', + new ParserException( + '0', + Lexer::TOKEN_END, + 7, + Lexer::TOKEN_IDENTIFIER + ), + ], + [ + 'object{0?: int}', + new ParserException( + '0', + Lexer::TOKEN_END, + 7, + Lexer::TOKEN_IDENTIFIER + ), + ], + [ + 'object{"a": int}', + new ObjectShapeNode([ + new ObjectShapeItemNode( + new ConstExprStringNode('a'), + false, + new IdentifierTypeNode('int') + ), + ]), + ], + [ + 'object{\'a\': int}', + new ObjectShapeNode([ + new ObjectShapeItemNode( + new ConstExprStringNode('a'), + false, + new IdentifierTypeNode('int') + ), + ]), + ], + [ + 'object{\'$ref\': int}', + new ObjectShapeNode([ + new ObjectShapeItemNode( + new ConstExprStringNode('$ref'), + false, + new IdentifierTypeNode('int') + ), + ]), + ], + [ + 'object{"$ref": int}', + new ObjectShapeNode([ + new ObjectShapeItemNode( + new ConstExprStringNode('$ref'), + false, + new IdentifierTypeNode('int') + ), + ]), + ], + [ + 'object{ + * a: int + *}', + new ObjectShapeNode([ + new ObjectShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + ]), + ], + [ + 'object{ + a: int, + }', + new ObjectShapeNode([ + new ObjectShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + ]), + ], + [ + 'object{ + a: int, + b: string, + }', + new ObjectShapeNode([ + new ObjectShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + new ObjectShapeItemNode( + new IdentifierTypeNode('b'), + false, + new IdentifierTypeNode('string') + ), + ]), + ], + [ + 'object{ + a: int + , b: string + , c: string + }', + new ObjectShapeNode([ + new ObjectShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + new ObjectShapeItemNode( + new IdentifierTypeNode('b'), + false, + new IdentifierTypeNode('string') + ), + new ObjectShapeItemNode( + new IdentifierTypeNode('c'), + false, + new IdentifierTypeNode('string') + ), + ]), + ], + [ + 'object{ + a: int, + b: string + }', + new ObjectShapeNode([ + new ObjectShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + new ObjectShapeItemNode( + new IdentifierTypeNode('b'), + false, + new IdentifierTypeNode('string') + ), + ]), + ], + [ + 'object{foo: int}[]', + new ArrayTypeNode( + new ObjectShapeNode([ + new ObjectShapeItemNode( + new IdentifierTypeNode('foo'), + false, + new IdentifierTypeNode('int') + ), + ]) + ), + ], + [ + 'int | object{foo: int}[]', + new UnionTypeNode([ + new IdentifierTypeNode('int'), + new ArrayTypeNode( + new ObjectShapeNode([ + new ObjectShapeItemNode( + new IdentifierTypeNode('foo'), + false, + new IdentifierTypeNode('int') + ), + ]) + ), + ]), + ], + [ + 'object{}', + new ObjectShapeNode([]), + ], + [ + 'object{}|int', + new UnionTypeNode([new ObjectShapeNode([]), new IdentifierTypeNode('int')]), + ], + [ + 'int|object{}', + new UnionTypeNode([new IdentifierTypeNode('int'), new ObjectShapeNode([])]), + ], ]; } From bada68af7cb6efe88961c4bc6b33367d13c86ca4 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 6 Apr 2023 09:33:22 +0200 Subject: [PATCH 19/59] Object shapes - test without space after colon --- tests/PHPStan/Parser/TypeParserTest.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index ed5d725a..0549ec4a 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -1860,6 +1860,21 @@ public function provideParseData(): array 'int|object{}', new UnionTypeNode([new IdentifierTypeNode('int'), new ObjectShapeNode([])]), ], + [ + 'object{attribute:string, value?:string}', + new ObjectShapeNode([ + new ObjectShapeItemNode( + new IdentifierTypeNode('attribute'), + false, + new IdentifierTypeNode('string') + ), + new ObjectShapeItemNode( + new IdentifierTypeNode('value'), + true, + new IdentifierTypeNode('string') + ), + ]), + ], ]; } From 22dcdfd725ddf99583bfe398fc624ad6c5004a0f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 7 Apr 2023 13:51:11 +0200 Subject: [PATCH 20/59] Do not crash on invalid UTF-8 --- src/Parser/ParserException.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Parser/ParserException.php b/src/Parser/ParserException.php index badcdcbb..f281254f 100644 --- a/src/Parser/ParserException.php +++ b/src/Parser/ParserException.php @@ -7,6 +7,7 @@ use function assert; use function json_encode; use function sprintf; +use const JSON_INVALID_UTF8_SUBSTITUTE; use const JSON_UNESCAPED_SLASHES; use const JSON_UNESCAPED_UNICODE; @@ -84,7 +85,7 @@ public function getExpectedTokenValue(): ?string private function formatValue(string $value): string { - $json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + $json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE); assert($json !== false); return $json; From 4e3ca1e8b029fd5767aeaf0dc05dcd3dfa5301ab Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 00:52:42 +0000 Subject: [PATCH 21/59] Update peter-evans/create-pull-request action to v5 --- .github/workflows/send-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/send-pr.yml b/.github/workflows/send-pr.yml index 9a5c0973..bc305f97 100644 --- a/.github/workflows/send-pr.yml +++ b/.github/workflows/send-pr.yml @@ -35,7 +35,7 @@ jobs: - name: "Create Pull Request" id: create-pr - uses: peter-evans/create-pull-request@v4 + uses: peter-evans/create-pull-request@v5 with: token: ${{ secrets.PHPSTAN_BOT_TOKEN }} path: ./phpstan-src From 80e5c87d922b9d4712a962708cc21f6ad507b306 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 09:53:42 +0000 Subject: [PATCH 22/59] Update build-cs --- build-cs/composer.lock | 46 +++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index 1d103c79..2eafc159 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -9,16 +9,16 @@ "packages-dev": [ { "name": "consistence-community/coding-standard", - "version": "3.11.2", + "version": "3.11.3", "source": { "type": "git", "url": "https://github.com/consistence-community/coding-standard.git", - "reference": "adb4be482e76990552bf624309d2acc8754ba1bd" + "reference": "f38e06327d5bf80ff5ff523a2c05e623b5e8d8b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consistence-community/coding-standard/zipball/adb4be482e76990552bf624309d2acc8754ba1bd", - "reference": "adb4be482e76990552bf624309d2acc8754ba1bd", + "url": "https://api.github.com/repos/consistence-community/coding-standard/zipball/f38e06327d5bf80ff5ff523a2c05e623b5e8d8b1", + "reference": "f38e06327d5bf80ff5ff523a2c05e623b5e8d8b1", "shasum": "" }, "require": { @@ -70,9 +70,9 @@ ], "support": { "issues": "https://github.com/consistence-community/coding-standard/issues", - "source": "https://github.com/consistence-community/coding-standard/tree/3.11.2" + "source": "https://github.com/consistence-community/coding-standard/tree/3.11.3" }, - "time": "2022-06-21T08:36:36+00:00" + "time": "2023-03-27T14:55:41+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", @@ -154,16 +154,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.16.1", + "version": "1.18.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "e27e92d939e2e3636f0a1f0afaba59692c0bf571" + "reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/e27e92d939e2e3636f0a1f0afaba59692c0bf571", - "reference": "e27e92d939e2e3636f0a1f0afaba59692c0bf571", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/22dcdfd725ddf99583bfe398fc624ad6c5004a0f", + "reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f", "shasum": "" }, "require": { @@ -193,38 +193,38 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.16.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.18.1" }, - "time": "2023-02-07T18:11:17+00:00" + "time": "2023-04-07T11:51:11+00:00" }, { "name": "slevomat/coding-standard", - "version": "8.9.1", + "version": "8.10.0", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "3d4fe0c803ae15829ef72d90d3d4eee3dd9f79b2" + "reference": "c4e213e6e57f741451a08e68ef838802eec92287" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/3d4fe0c803ae15829ef72d90d3d4eee3dd9f79b2", - "reference": "3d4fe0c803ae15829ef72d90d3d4eee3dd9f79b2", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/c4e213e6e57f741451a08e68ef838802eec92287", + "reference": "c4e213e6e57f741451a08e68ef838802eec92287", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": ">=1.16.0 <1.17.0", + "phpstan/phpdoc-parser": ">=1.18.0 <1.19.0", "squizlabs/php_codesniffer": "^3.7.1" }, "require-dev": { "phing/phing": "2.17.4", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.10|1.10.8", + "phpstan/phpstan": "1.4.10|1.10.11", "phpstan/phpstan-deprecation-rules": "1.1.3", - "phpstan/phpstan-phpunit": "1.0.0|1.3.10", - "phpstan/phpstan-strict-rules": "1.5.0", - "phpunit/phpunit": "7.5.20|8.5.21|9.6.5" + "phpstan/phpstan-phpunit": "1.0.0|1.3.11", + "phpstan/phpstan-strict-rules": "1.5.1", + "phpunit/phpunit": "7.5.20|8.5.21|9.6.6|10.0.19" }, "type": "phpcodesniffer-standard", "extra": { @@ -248,7 +248,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.9.1" + "source": "https://github.com/slevomat/coding-standard/tree/8.10.0" }, "funding": [ { @@ -260,7 +260,7 @@ "type": "tidelift" } ], - "time": "2023-03-27T11:00:16+00:00" + "time": "2023-04-10T07:39:29+00:00" }, { "name": "squizlabs/php_codesniffer", From 5e2f2e0930c3c9e11fb38b9ced09d38020f7371b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 17 Apr 2023 14:38:02 +0200 Subject: [PATCH 23/59] QuoteAwareConstExprStringNode with correct quotes and escaping in `__toString()` Currently opt-in with new ConstExprParser and TypeParser constructor argument --- .../QuoteAwareConstExprStringNode.php | 78 +++++++++++++++++++ src/Ast/Type/ArrayShapeItemNode.php | 5 +- src/Ast/Type/ObjectShapeItemNode.php | 5 +- src/Parser/ConstExprParser.php | 17 +++- src/Parser/TypeParser.php | 35 +++++++-- 5 files changed, 128 insertions(+), 12 deletions(-) create mode 100644 src/Ast/ConstExpr/QuoteAwareConstExprStringNode.php diff --git a/src/Ast/ConstExpr/QuoteAwareConstExprStringNode.php b/src/Ast/ConstExpr/QuoteAwareConstExprStringNode.php new file mode 100644 index 00000000..9905ea38 --- /dev/null +++ b/src/Ast/ConstExpr/QuoteAwareConstExprStringNode.php @@ -0,0 +1,78 @@ +value = $value; + $this->quoteType = $quoteType; + } + + + public function __toString(): string + { + if ($this->quoteType === self::SINGLE_QUOTED) { + // from https://github.com/nikic/PHP-Parser/blob/0ffddce52d816f72d0efc4d9b02e276d3309ef01/lib/PhpParser/PrettyPrinter/Standard.php#L1007 + return sprintf("'%s'", addcslashes($this->value, '\'\\')); + } + + // from https://github.com/nikic/PHP-Parser/blob/0ffddce52d816f72d0efc4d9b02e276d3309ef01/lib/PhpParser/PrettyPrinter/Standard.php#L1010-L1040 + return sprintf('"%s"', $this->escapeDoubleQuotedString()); + } + + private function escapeDoubleQuotedString() { + $quote = '"'; + $escaped = addcslashes($this->value, "\n\r\t\f\v$" . $quote . "\\"); + + // Escape control characters and non-UTF-8 characters. + // Regex based on https://stackoverflow.com/a/11709412/385378. + $regex = '/( + [\x00-\x08\x0E-\x1F] # Control characters + | [\xC0-\xC1] # Invalid UTF-8 Bytes + | [\xF5-\xFF] # Invalid UTF-8 Bytes + | \xE0(?=[\x80-\x9F]) # Overlong encoding of prior code point + | \xF0(?=[\x80-\x8F]) # Overlong encoding of prior code point + | [\xC2-\xDF](?![\x80-\xBF]) # Invalid UTF-8 Sequence Start + | [\xE0-\xEF](?![\x80-\xBF]{2}) # Invalid UTF-8 Sequence Start + | [\xF0-\xF4](?![\x80-\xBF]{3}) # Invalid UTF-8 Sequence Start + | (?<=[\x00-\x7F\xF5-\xFF])[\x80-\xBF] # Invalid UTF-8 Sequence Middle + | (?unescapeStrings = $unescapeStrings; + $this->quoteAwareConstExprString = $quoteAwareConstExprString; } public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\ConstExpr\ConstExprNode @@ -49,6 +53,7 @@ public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\Con if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING, Lexer::TOKEN_DOUBLE_QUOTED_STRING)) { $value = $tokens->currentTokenValue(); + $type = $tokens->currentTokenType(); if ($trimStrings) { if ($this->unescapeStrings) { $value = self::unescapeString($value); @@ -57,6 +62,16 @@ public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\Con } } $tokens->next(); + + if ($this->quoteAwareConstExprString) { + return new Ast\ConstExpr\QuoteAwareConstExprStringNode( + $value, + $type === Lexer::TOKEN_SINGLE_QUOTED_STRING + ? Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED + : Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED + ); + } + return new Ast\ConstExpr\ConstExprStringNode($value); } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) { diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 7a67b84d..caf7ed16 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -15,9 +15,13 @@ class TypeParser /** @var ConstExprParser|null */ private $constExprParser; - public function __construct(?ConstExprParser $constExprParser = null) + /** @var bool */ + private $quoteAwareConstExprString; + + public function __construct(?ConstExprParser $constExprParser = null, bool $quoteAwareConstExprString = false) { $this->constExprParser = $constExprParser; + $this->quoteAwareConstExprString = $quoteAwareConstExprString; } /** @phpstan-impure */ @@ -562,7 +566,7 @@ private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShape /** * @phpstan-impure - * @return Ast\ConstExpr\ConstExprIntegerNode|Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode + * @return Ast\ConstExpr\ConstExprIntegerNode|Ast\ConstExpr\QuoteAwareConstExprStringNode|Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode */ private function parseArrayShapeKey(TokenIterator $tokens) { @@ -571,11 +575,20 @@ private function parseArrayShapeKey(TokenIterator $tokens) $tokens->next(); } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) { - $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'")); + if ($this->quoteAwareConstExprString) { + $key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(trim($tokens->currentTokenValue(), "'"), Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED); + } else { + $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'")); + } $tokens->next(); } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) { - $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"')); + if ($this->quoteAwareConstExprString) { + $key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(trim($tokens->currentTokenValue(), '"'), Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED); + } else { + $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"')); + } + $tokens->next(); } else { @@ -626,16 +639,24 @@ private function parseObjectShapeItem(TokenIterator $tokens): Ast\Type\ObjectSha /** * @phpstan-impure - * @return Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode + * @return Ast\ConstExpr\QuoteAwareConstExprStringNode|Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode */ private function parseObjectShapeKey(TokenIterator $tokens) { if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) { - $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'")); + if ($this->quoteAwareConstExprString) { + $key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(trim($tokens->currentTokenValue(), "'"), Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED); + } else { + $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'")); + } $tokens->next(); } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) { - $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"')); + if ($this->quoteAwareConstExprString) { + $key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(trim($tokens->currentTokenValue(), '"'), Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED); + } else { + $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"')); + } $tokens->next(); } else { From 376023a642a805b207d7f17775ebb96392a78c57 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 17 Apr 2023 14:51:33 +0200 Subject: [PATCH 24/59] Extract StringUnescaper from ConstExprParser --- .../QuoteAwareConstExprStringNode.php | 13 ++- src/Parser/ConstExprParser.php | 88 +---------------- src/Parser/StringUnescaper.php | 96 +++++++++++++++++++ 3 files changed, 105 insertions(+), 92 deletions(-) create mode 100644 src/Parser/StringUnescaper.php diff --git a/src/Ast/ConstExpr/QuoteAwareConstExprStringNode.php b/src/Ast/ConstExpr/QuoteAwareConstExprStringNode.php index 9905ea38..04cc35b8 100644 --- a/src/Ast/ConstExpr/QuoteAwareConstExprStringNode.php +++ b/src/Ast/ConstExpr/QuoteAwareConstExprStringNode.php @@ -11,6 +11,7 @@ use function sprintf; use function str_pad; use function strlen; +use const STR_PAD_LEFT; class QuoteAwareConstExprStringNode implements ConstExprNode { @@ -47,9 +48,10 @@ public function __toString(): string return sprintf('"%s"', $this->escapeDoubleQuotedString()); } - private function escapeDoubleQuotedString() { + private function escapeDoubleQuotedString() + { $quote = '"'; - $escaped = addcslashes($this->value, "\n\r\t\f\v$" . $quote . "\\"); + $escaped = addcslashes($this->value, "\n\r\t\f\v$" . $quote . '\\'); // Escape control characters and non-UTF-8 characters. // Regex based on https://stackoverflow.com/a/11709412/385378. @@ -68,10 +70,11 @@ private function escapeDoubleQuotedString() { | (?<=[\xF0-\xF4])[\x80-\xBF](?![\x80-\xBF]{2}) # Short 4 byte sequence | (?<=[\xF0-\xF4][\x80-\xBF])[\x80-\xBF](?![\x80-\xBF]) # Short 4 byte sequence (2) )/x'; - return preg_replace_callback($regex, function ($matches) { + return preg_replace_callback($regex, static function ($matches) { assert(strlen($matches[0]) === 1); - $hex = dechex(ord($matches[0]));; - return '\\x' . str_pad($hex, 2, '0', \STR_PAD_LEFT); + $hex = dechex(ord($matches[0])); + + return '\\x' . str_pad($hex, 2, '0', STR_PAD_LEFT); }, $escaped); } diff --git a/src/Parser/ConstExprParser.php b/src/Parser/ConstExprParser.php index c982f6f7..3cb536d0 100644 --- a/src/Parser/ConstExprParser.php +++ b/src/Parser/ConstExprParser.php @@ -4,27 +4,12 @@ use PHPStan\PhpDocParser\Ast; use PHPStan\PhpDocParser\Lexer\Lexer; -use function chr; -use function hexdec; -use function octdec; -use function preg_replace_callback; -use function str_replace; use function strtolower; use function substr; class ConstExprParser { - private const REPLACEMENTS = [ - '\\' => '\\', - 'n' => "\n", - 'r' => "\r", - 't' => "\t", - 'f' => "\f", - 'v' => "\v", - 'e' => "\x1B", - ]; - /** @var bool */ private $unescapeStrings; @@ -56,7 +41,7 @@ public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\Con $type = $tokens->currentTokenType(); if ($trimStrings) { if ($this->unescapeStrings) { - $value = self::unescapeString($value); + $value = StringUnescaper::unescapeString($value); } else { $value = substr($value, 1, -1); } @@ -171,75 +156,4 @@ private function parseArrayItem(TokenIterator $tokens): Ast\ConstExpr\ConstExprA return new Ast\ConstExpr\ConstExprArrayItemNode($key, $value); } - private static function unescapeString(string $string): string - { - $quote = $string[0]; - - if ($quote === '\'') { - return str_replace( - ['\\\\', '\\\''], - ['\\', '\''], - substr($string, 1, -1) - ); - } - - return self::parseEscapeSequences(substr($string, 1, -1), '"'); - } - - /** - * Implementation based on https://github.com/nikic/PHP-Parser/blob/b0edd4c41111042d43bb45c6c657b2e0db367d9e/lib/PhpParser/Node/Scalar/String_.php#L90-L130 - */ - private static function parseEscapeSequences(string $str, string $quote): string - { - $str = str_replace('\\' . $quote, $quote, $str); - - return preg_replace_callback( - '~\\\\([\\\\nrtfve]|[xX][0-9a-fA-F]{1,2}|[0-7]{1,3}|u\{([0-9a-fA-F]+)\})~', - static function ($matches) { - $str = $matches[1]; - - if (isset(self::REPLACEMENTS[$str])) { - return self::REPLACEMENTS[$str]; - } - if ($str[0] === 'x' || $str[0] === 'X') { - return chr(hexdec(substr($str, 1))); - } - if ($str[0] === 'u') { - return self::codePointToUtf8(hexdec($matches[2])); - } - - return chr(octdec($str)); - }, - $str - ); - } - - /** - * Implementation based on https://github.com/nikic/PHP-Parser/blob/b0edd4c41111042d43bb45c6c657b2e0db367d9e/lib/PhpParser/Node/Scalar/String_.php#L132-L154 - */ - private static function codePointToUtf8(int $num): string - { - if ($num <= 0x7F) { - return chr($num); - } - if ($num <= 0x7FF) { - return chr(($num >> 6) + 0xC0) - . chr(($num & 0x3F) + 0x80); - } - if ($num <= 0xFFFF) { - return chr(($num >> 12) + 0xE0) - . chr((($num >> 6) & 0x3F) + 0x80) - . chr(($num & 0x3F) + 0x80); - } - if ($num <= 0x1FFFFF) { - return chr(($num >> 18) + 0xF0) - . chr((($num >> 12) & 0x3F) + 0x80) - . chr((($num >> 6) & 0x3F) + 0x80) - . chr(($num & 0x3F) + 0x80); - } - - // Invalid UTF-8 codepoint escape sequence: Codepoint too large - return "\xef\xbf\xbd"; - } - } diff --git a/src/Parser/StringUnescaper.php b/src/Parser/StringUnescaper.php new file mode 100644 index 00000000..93186ce3 --- /dev/null +++ b/src/Parser/StringUnescaper.php @@ -0,0 +1,96 @@ + '\\', + 'n' => "\n", + 'r' => "\r", + 't' => "\t", + 'f' => "\f", + 'v' => "\v", + 'e' => "\x1B", + ]; + + public static function unescapeString(string $string): string + { + $quote = $string[0]; + + if ($quote === '\'') { + return str_replace( + ['\\\\', '\\\''], + ['\\', '\''], + substr($string, 1, -1) + ); + } + + return self::parseEscapeSequences(substr($string, 1, -1), '"'); + } + + /** + * Implementation based on https://github.com/nikic/PHP-Parser/blob/b0edd4c41111042d43bb45c6c657b2e0db367d9e/lib/PhpParser/Node/Scalar/String_.php#L90-L130 + */ + private static function parseEscapeSequences(string $str, string $quote): string + { + $str = str_replace('\\' . $quote, $quote, $str); + + return preg_replace_callback( + '~\\\\([\\\\nrtfve]|[xX][0-9a-fA-F]{1,2}|[0-7]{1,3}|u\{([0-9a-fA-F]+)\})~', + static function ($matches) { + $str = $matches[1]; + + if (isset(self::REPLACEMENTS[$str])) { + return self::REPLACEMENTS[$str]; + } + if ($str[0] === 'x' || $str[0] === 'X') { + return chr(hexdec(substr($str, 1))); + } + if ($str[0] === 'u') { + return self::codePointToUtf8(hexdec($matches[2])); + } + + return chr(octdec($str)); + }, + $str + ); + } + + /** + * Implementation based on https://github.com/nikic/PHP-Parser/blob/b0edd4c41111042d43bb45c6c657b2e0db367d9e/lib/PhpParser/Node/Scalar/String_.php#L132-L154 + */ + private static function codePointToUtf8(int $num): string + { + if ($num <= 0x7F) { + return chr($num); + } + if ($num <= 0x7FF) { + return chr(($num >> 6) + 0xC0) + . chr(($num & 0x3F) + 0x80); + } + if ($num <= 0xFFFF) { + return chr(($num >> 12) + 0xE0) + . chr((($num >> 6) & 0x3F) + 0x80) + . chr(($num & 0x3F) + 0x80); + } + if ($num <= 0x1FFFFF) { + return chr(($num >> 18) + 0xF0) + . chr((($num >> 12) & 0x3F) + 0x80) + . chr((($num >> 6) & 0x3F) + 0x80) + . chr(($num & 0x3F) + 0x80); + } + + // Invalid UTF-8 codepoint escape sequence: Codepoint too large + return "\xef\xbf\xbd"; + } + +} From ae5be8123a74e6798b8c8e79e0ac96bf14b0d9f4 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 17 Apr 2023 14:53:59 +0200 Subject: [PATCH 25/59] Use StringUnescaper when quoteAwareConstExprString=true --- src/Parser/TypeParser.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index caf7ed16..48cba895 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -576,7 +576,7 @@ private function parseArrayShapeKey(TokenIterator $tokens) } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) { if ($this->quoteAwareConstExprString) { - $key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(trim($tokens->currentTokenValue(), "'"), Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED); + $key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED); } else { $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'")); } @@ -584,7 +584,7 @@ private function parseArrayShapeKey(TokenIterator $tokens) } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) { if ($this->quoteAwareConstExprString) { - $key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(trim($tokens->currentTokenValue(), '"'), Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED); + $key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED); } else { $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"')); } @@ -645,7 +645,7 @@ private function parseObjectShapeKey(TokenIterator $tokens) { if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) { if ($this->quoteAwareConstExprString) { - $key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(trim($tokens->currentTokenValue(), "'"), Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED); + $key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED); } else { $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'")); } @@ -653,7 +653,7 @@ private function parseObjectShapeKey(TokenIterator $tokens) } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) { if ($this->quoteAwareConstExprString) { - $key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(trim($tokens->currentTokenValue(), '"'), Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED); + $key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED); } else { $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"')); } From 829bd2e3585446a4262719b4a2ee7c52c87cab24 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 17 Apr 2023 14:57:07 +0200 Subject: [PATCH 26/59] TypeParserTest - use modern feature toggles --- tests/PHPStan/Parser/TypeParserTest.php | 28 ++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 0549ec4a..a90a58aa 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -5,8 +5,8 @@ use Exception; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; -use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; +use PHPStan\PhpDocParser\Ast\ConstExpr\QuoteAwareConstExprStringNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; @@ -43,7 +43,7 @@ protected function setUp(): void { parent::setUp(); $this->lexer = new Lexer(); - $this->typeParser = new TypeParser(new ConstExprParser()); + $this->typeParser = new TypeParser(new ConstExprParser(true, true), true); } @@ -481,7 +481,7 @@ public function provideParseData(): array 'array{"a": int}', new ArrayShapeNode([ new ArrayShapeItemNode( - new ConstExprStringNode('a'), + new QuoteAwareConstExprStringNode('a', QuoteAwareConstExprStringNode::DOUBLE_QUOTED), false, new IdentifierTypeNode('int') ), @@ -491,7 +491,7 @@ public function provideParseData(): array 'array{\'a\': int}', new ArrayShapeNode([ new ArrayShapeItemNode( - new ConstExprStringNode('a'), + new QuoteAwareConstExprStringNode('a', QuoteAwareConstExprStringNode::SINGLE_QUOTED), false, new IdentifierTypeNode('int') ), @@ -501,7 +501,7 @@ public function provideParseData(): array 'array{\'$ref\': int}', new ArrayShapeNode([ new ArrayShapeItemNode( - new ConstExprStringNode('$ref'), + new QuoteAwareConstExprStringNode('$ref', QuoteAwareConstExprStringNode::SINGLE_QUOTED), false, new IdentifierTypeNode('int') ), @@ -511,7 +511,7 @@ public function provideParseData(): array 'array{"$ref": int}', new ArrayShapeNode([ new ArrayShapeItemNode( - new ConstExprStringNode('$ref'), + new QuoteAwareConstExprStringNode('$ref', QuoteAwareConstExprStringNode::DOUBLE_QUOTED), false, new IdentifierTypeNode('int') ), @@ -979,8 +979,8 @@ public function provideParseData(): array [ "'foo'|'bar'", new UnionTypeNode([ - new ConstTypeNode(new ConstExprStringNode('foo')), - new ConstTypeNode(new ConstExprStringNode('bar')), + new ConstTypeNode(new QuoteAwareConstExprStringNode('foo', QuoteAwareConstExprStringNode::SINGLE_QUOTED)), + new ConstTypeNode(new QuoteAwareConstExprStringNode('bar', QuoteAwareConstExprStringNode::SINGLE_QUOTED)), ]), ], [ @@ -997,7 +997,7 @@ public function provideParseData(): array ], [ '"bar"', - new ConstTypeNode(new ConstExprStringNode('bar')), + new ConstTypeNode(new QuoteAwareConstExprStringNode('bar', QuoteAwareConstExprStringNode::DOUBLE_QUOTED)), ], [ 'Foo::FOO_*', @@ -1035,7 +1035,7 @@ public function provideParseData(): array [ '( "foo" | Foo::FOO_* )', new UnionTypeNode([ - new ConstTypeNode(new ConstExprStringNode('foo')), + new ConstTypeNode(new QuoteAwareConstExprStringNode('foo', QuoteAwareConstExprStringNode::DOUBLE_QUOTED)), new ConstTypeNode(new ConstFetchNode('Foo', 'FOO_*')), ]), ], @@ -1701,7 +1701,7 @@ public function provideParseData(): array 'object{"a": int}', new ObjectShapeNode([ new ObjectShapeItemNode( - new ConstExprStringNode('a'), + new QuoteAwareConstExprStringNode('a', QuoteAwareConstExprStringNode::DOUBLE_QUOTED), false, new IdentifierTypeNode('int') ), @@ -1711,7 +1711,7 @@ public function provideParseData(): array 'object{\'a\': int}', new ObjectShapeNode([ new ObjectShapeItemNode( - new ConstExprStringNode('a'), + new QuoteAwareConstExprStringNode('a', QuoteAwareConstExprStringNode::SINGLE_QUOTED), false, new IdentifierTypeNode('int') ), @@ -1721,7 +1721,7 @@ public function provideParseData(): array 'object{\'$ref\': int}', new ObjectShapeNode([ new ObjectShapeItemNode( - new ConstExprStringNode('$ref'), + new QuoteAwareConstExprStringNode('$ref', QuoteAwareConstExprStringNode::SINGLE_QUOTED), false, new IdentifierTypeNode('int') ), @@ -1731,7 +1731,7 @@ public function provideParseData(): array 'object{"$ref": int}', new ObjectShapeNode([ new ObjectShapeItemNode( - new ConstExprStringNode('$ref'), + new QuoteAwareConstExprStringNode('$ref', QuoteAwareConstExprStringNode::DOUBLE_QUOTED), false, new IdentifierTypeNode('int') ), From f373259f520e3db0bceff2870d18a4f04af6cd92 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 17 Apr 2023 15:10:31 +0200 Subject: [PATCH 27/59] Fix unparseable string representation of CallableTypeParameterNode --- src/Ast/Type/CallableTypeParameterNode.php | 4 ++-- tests/PHPStan/Ast/ToString/TypeToStringTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Ast/Type/CallableTypeParameterNode.php b/src/Ast/Type/CallableTypeParameterNode.php index 7ab2d7e3..c78d4c7b 100644 --- a/src/Ast/Type/CallableTypeParameterNode.php +++ b/src/Ast/Type/CallableTypeParameterNode.php @@ -41,8 +41,8 @@ public function __toString(): string $type = "{$this->type} "; $isReference = $this->isReference ? '&' : ''; $isVariadic = $this->isVariadic ? '...' : ''; - $default = $this->isOptional ? ' = default' : ''; - return trim("{$type}{$isReference}{$isVariadic}{$this->parameterName}") . $default; + $isOptional = $this->isOptional ? '=' : ''; + return trim("{$type}{$isReference}{$isVariadic}{$this->parameterName}") . $isOptional; } } diff --git a/tests/PHPStan/Ast/ToString/TypeToStringTest.php b/tests/PHPStan/Ast/ToString/TypeToStringTest.php index a32b0a09..9cf7ebf6 100644 --- a/tests/PHPStan/Ast/ToString/TypeToStringTest.php +++ b/tests/PHPStan/Ast/ToString/TypeToStringTest.php @@ -102,7 +102,7 @@ public static function provideCallableCases(): Generator ], new IdentifierTypeNode('void')), ], [ - 'callable(int = default, int $foo = default): void', + 'callable(int=, int $foo=): void', new CallableTypeNode(new IdentifierTypeNode('callable'), [ new CallableTypeParameterNode(new IdentifierTypeNode('int'), false, false, '', true), new CallableTypeParameterNode(new IdentifierTypeNode('int'), false, false, '$foo', true), From 5194589df107839656f1fe76c8035c9d84054abf Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 17 Apr 2023 15:13:04 +0200 Subject: [PATCH 28/59] TypeParserTest - test parsing string type representation again --- tests/PHPStan/Parser/TypeParserTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index a90a58aa..e2682793 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -28,6 +28,7 @@ use PHPStan\PhpDocParser\Lexer\Lexer; use PHPUnit\Framework\TestCase; use function get_class; +use function strpos; use const PHP_EOL; class TypeParserTest extends TestCase @@ -65,6 +66,15 @@ public function testParse(string $input, $expectedResult, int $nextTokenType = L $this->assertInstanceOf(get_class($expectedResult), $typeNode); $this->assertEquals($expectedResult, $typeNode); $this->assertSame($nextTokenType, $tokens->currentTokenType()); + + if (strpos((string) $expectedResult, '$ref') !== false) { + // weird case with $ref inside double-quoted string - not really possible in PHP + return; + } + + $typeNodeTokens = new TokenIterator($this->lexer->tokenize((string) $typeNode)); + $parsedAgainTypeNode = $this->typeParser->parse($typeNodeTokens); + $this->assertSame((string) $typeNode, (string) $parsedAgainTypeNode); } From 178b33aa1c8b8d7725f0abee618ef47337e607ce Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 17 Apr 2023 15:16:52 +0200 Subject: [PATCH 29/59] Backward compatibility - QuoteAwareConstExprStringNode should extend ConstExprStringNode --- src/Ast/ConstExpr/QuoteAwareConstExprStringNode.php | 7 ++----- src/Ast/Type/ArrayShapeItemNode.php | 5 ++--- src/Ast/Type/ObjectShapeItemNode.php | 5 ++--- src/Parser/TypeParser.php | 4 ++-- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/Ast/ConstExpr/QuoteAwareConstExprStringNode.php b/src/Ast/ConstExpr/QuoteAwareConstExprStringNode.php index 04cc35b8..0b9360cc 100644 --- a/src/Ast/ConstExpr/QuoteAwareConstExprStringNode.php +++ b/src/Ast/ConstExpr/QuoteAwareConstExprStringNode.php @@ -13,7 +13,7 @@ use function strlen; use const STR_PAD_LEFT; -class QuoteAwareConstExprStringNode implements ConstExprNode +class QuoteAwareConstExprStringNode extends ConstExprStringNode implements ConstExprNode { public const SINGLE_QUOTED = 1; @@ -21,9 +21,6 @@ class QuoteAwareConstExprStringNode implements ConstExprNode use NodeAttributes; - /** @var string */ - public $value; - /** @var self::SINGLE_QUOTED|self::DOUBLE_QUOTED */ public $quoteType; @@ -32,7 +29,7 @@ class QuoteAwareConstExprStringNode implements ConstExprNode */ public function __construct(string $value, int $quoteType) { - $this->value = $value; + parent::__construct($value); $this->quoteType = $quoteType; } diff --git a/src/Ast/Type/ArrayShapeItemNode.php b/src/Ast/Type/ArrayShapeItemNode.php index bf257a11..660c6c9d 100644 --- a/src/Ast/Type/ArrayShapeItemNode.php +++ b/src/Ast/Type/ArrayShapeItemNode.php @@ -4,7 +4,6 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; -use PHPStan\PhpDocParser\Ast\ConstExpr\QuoteAwareConstExprStringNode; use PHPStan\PhpDocParser\Ast\NodeAttributes; use function sprintf; @@ -13,7 +12,7 @@ class ArrayShapeItemNode implements TypeNode use NodeAttributes; - /** @var ConstExprIntegerNode|QuoteAwareConstExprStringNode|ConstExprStringNode|IdentifierTypeNode|null */ + /** @var ConstExprIntegerNode|ConstExprStringNode|IdentifierTypeNode|null */ public $keyName; /** @var bool */ @@ -23,7 +22,7 @@ class ArrayShapeItemNode implements TypeNode public $valueType; /** - * @param ConstExprIntegerNode|QuoteAwareConstExprStringNode|ConstExprStringNode|IdentifierTypeNode|null $keyName + * @param ConstExprIntegerNode|ConstExprStringNode|IdentifierTypeNode|null $keyName */ public function __construct($keyName, bool $optional, TypeNode $valueType) { diff --git a/src/Ast/Type/ObjectShapeItemNode.php b/src/Ast/Type/ObjectShapeItemNode.php index f26ccc08..2f012406 100644 --- a/src/Ast/Type/ObjectShapeItemNode.php +++ b/src/Ast/Type/ObjectShapeItemNode.php @@ -3,7 +3,6 @@ namespace PHPStan\PhpDocParser\Ast\Type; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; -use PHPStan\PhpDocParser\Ast\ConstExpr\QuoteAwareConstExprStringNode; use PHPStan\PhpDocParser\Ast\NodeAttributes; use function sprintf; @@ -12,7 +11,7 @@ class ObjectShapeItemNode implements TypeNode use NodeAttributes; - /** @var QuoteAwareConstExprStringNode|ConstExprStringNode|IdentifierTypeNode */ + /** @var ConstExprStringNode|IdentifierTypeNode */ public $keyName; /** @var bool */ @@ -22,7 +21,7 @@ class ObjectShapeItemNode implements TypeNode public $valueType; /** - * @param QuoteAwareConstExprStringNode|ConstExprStringNode|IdentifierTypeNode $keyName + * @param ConstExprStringNode|IdentifierTypeNode $keyName */ public function __construct($keyName, bool $optional, TypeNode $valueType) { diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 48cba895..661bccfa 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -566,7 +566,7 @@ private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShape /** * @phpstan-impure - * @return Ast\ConstExpr\ConstExprIntegerNode|Ast\ConstExpr\QuoteAwareConstExprStringNode|Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode + * @return Ast\ConstExpr\ConstExprIntegerNode|Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode */ private function parseArrayShapeKey(TokenIterator $tokens) { @@ -639,7 +639,7 @@ private function parseObjectShapeItem(TokenIterator $tokens): Ast\Type\ObjectSha /** * @phpstan-impure - * @return Ast\ConstExpr\QuoteAwareConstExprStringNode|Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode + * @return Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode */ private function parseObjectShapeKey(TokenIterator $tokens) { From f545fc30978190a056832aa7ed995e36a66267f3 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 18 Apr 2023 13:30:27 +0200 Subject: [PATCH 30/59] When callable returns callable the return type needs to be put in parentheses --- src/Ast/Type/CallableTypeNode.php | 6 +++++- tests/PHPStan/Parser/TypeParserTest.php | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Ast/Type/CallableTypeNode.php b/src/Ast/Type/CallableTypeNode.php index 83ade94c..ba400851 100644 --- a/src/Ast/Type/CallableTypeNode.php +++ b/src/Ast/Type/CallableTypeNode.php @@ -29,8 +29,12 @@ public function __construct(IdentifierTypeNode $identifier, array $parameters, T public function __toString(): string { + $returnType = $this->returnType; + if ($returnType instanceof self) { + $returnType = "({$returnType})"; + } $parameters = implode(', ', $this->parameters); - return "{$this->identifier}({$parameters}): {$this->returnType}"; + return "{$this->identifier}({$parameters}): {$returnType}"; } } diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index e2682793..9416d72e 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -1885,6 +1885,22 @@ public function provideParseData(): array ), ]), ], + [ + 'Closure(Foo): (Closure(Foo): Bar)', + new CallableTypeNode( + new IdentifierTypeNode('Closure'), + [ + new CallableTypeParameterNode(new IdentifierTypeNode('Foo'), false, false, '', false), + ], + new CallableTypeNode( + new IdentifierTypeNode('Closure'), + [ + new CallableTypeParameterNode(new IdentifierTypeNode('Foo'), false, false, '', false), + ], + new IdentifierTypeNode('Bar') + ) + ), + ], ]; } From b5080064eefa38ea9954fe31a5a68af0135aa2e8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 18 Apr 2023 16:11:25 +0200 Subject: [PATCH 31/59] Document token array format --- src/Lexer/Lexer.php | 3 +++ src/Parser/TokenIterator.php | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Lexer/Lexer.php b/src/Lexer/Lexer.php index 1b98839d..16a8f0fd 100644 --- a/src/Lexer/Lexer.php +++ b/src/Lexer/Lexer.php @@ -92,6 +92,9 @@ class Lexer /** @var string|null */ private $regexp; + /** + * @return list + */ public function tokenize(string $s): array { if ($this->regexp === null) { diff --git a/src/Parser/TokenIterator.php b/src/Parser/TokenIterator.php index 569a9321..ab4a35de 100644 --- a/src/Parser/TokenIterator.php +++ b/src/Parser/TokenIterator.php @@ -12,7 +12,7 @@ class TokenIterator { - /** @var mixed[][] */ + /** @var list */ private $tokens; /** @var int */ @@ -21,6 +21,9 @@ class TokenIterator /** @var int[] */ private $savePoints = []; + /** + * @param list $tokens + */ public function __construct(array $tokens, int $index = 0) { $this->tokens = $tokens; From f9ecd17403d6fa66a57cf2b6022049790b522d78 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 18 Apr 2023 16:13:42 +0200 Subject: [PATCH 32/59] Set start and end lines to node attributes --- src/Ast/Attribute.php | 11 + src/Ast/PhpDoc/InvalidTagValueNode.php | 1 + src/Ast/Type/InvalidTypeNode.php | 1 + src/Lexer/Lexer.php | 14 +- src/Parser/ConstExprParser.php | 4 +- src/Parser/ParserException.php | 18 +- src/Parser/PhpDocParser.php | 49 ++- src/Parser/TokenIterator.php | 13 +- src/Parser/TypeParser.php | 24 +- tests/PHPStan/Parser/PhpDocParserTest.php | 345 +++++++++++++++++----- tests/PHPStan/Parser/TypeParserTest.php | 33 +++ 11 files changed, 429 insertions(+), 84 deletions(-) create mode 100644 src/Ast/Attribute.php diff --git a/src/Ast/Attribute.php b/src/Ast/Attribute.php new file mode 100644 index 00000000..c5a7adf2 --- /dev/null +++ b/src/Ast/Attribute.php @@ -0,0 +1,11 @@ +getCurrentOffset(), $exception->getExpectedTokenType(), $exception->getExpectedTokenValue(), + $exception->getCurrentTokenLine(), ]; } diff --git a/src/Ast/Type/InvalidTypeNode.php b/src/Ast/Type/InvalidTypeNode.php index af3d3d76..1ec47cf6 100644 --- a/src/Ast/Type/InvalidTypeNode.php +++ b/src/Ast/Type/InvalidTypeNode.php @@ -21,6 +21,7 @@ public function __construct(ParserException $exception) $exception->getCurrentOffset(), $exception->getExpectedTokenType(), $exception->getExpectedTokenValue(), + $exception->getCurrentTokenLine(), ]; } diff --git a/src/Lexer/Lexer.php b/src/Lexer/Lexer.php index 16a8f0fd..90d6b500 100644 --- a/src/Lexer/Lexer.php +++ b/src/Lexer/Lexer.php @@ -88,12 +88,13 @@ class Lexer public const VALUE_OFFSET = 0; public const TYPE_OFFSET = 1; + public const LINE_OFFSET = 2; /** @var string|null */ private $regexp; /** - * @return list + * @return list */ public function tokenize(string $s): array { @@ -104,11 +105,18 @@ public function tokenize(string $s): array preg_match_all($this->regexp, $s, $matches, PREG_SET_ORDER); $tokens = []; + $line = 1; foreach ($matches as $match) { - $tokens[] = [$match[0], (int) $match['MARK']]; + $type = (int) $match['MARK']; + $tokens[] = [$match[0], $type, $line]; + if ($type !== self::TOKEN_PHPDOC_EOL) { + continue; + } + + $line++; } - $tokens[] = ['', self::TOKEN_END]; + $tokens[] = ['', self::TOKEN_END, $line]; return $tokens; } diff --git a/src/Parser/ConstExprParser.php b/src/Parser/ConstExprParser.php index 3cb536d0..c4767ee8 100644 --- a/src/Parser/ConstExprParser.php +++ b/src/Parser/ConstExprParser.php @@ -120,7 +120,9 @@ public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\Con $tokens->currentTokenValue(), $tokens->currentTokenType(), $tokens->currentTokenOffset(), - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + $tokens->currentTokenLine() ); } diff --git a/src/Parser/ParserException.php b/src/Parser/ParserException.php index f281254f..6ab5cc07 100644 --- a/src/Parser/ParserException.php +++ b/src/Parser/ParserException.php @@ -29,12 +29,16 @@ class ParserException extends Exception /** @var string|null */ private $expectedTokenValue; + /** @var int|null */ + private $currentTokenLine; + public function __construct( string $currentTokenValue, int $currentTokenType, int $currentOffset, int $expectedTokenType, - ?string $expectedTokenValue = null + ?string $expectedTokenValue = null, + ?int $currentTokenLine = null ) { $this->currentTokenValue = $currentTokenValue; @@ -42,13 +46,15 @@ public function __construct( $this->currentOffset = $currentOffset; $this->expectedTokenType = $expectedTokenType; $this->expectedTokenValue = $expectedTokenValue; + $this->currentTokenLine = $currentTokenLine; parent::__construct(sprintf( - 'Unexpected token %s, expected %s%s at offset %d', + 'Unexpected token %s, expected %s%s at offset %d%s', $this->formatValue($currentTokenValue), Lexer::TOKEN_LABELS[$expectedTokenType], $expectedTokenValue !== null ? sprintf(' (%s)', $this->formatValue($expectedTokenValue)) : '', - $currentOffset + $currentOffset, + $currentTokenLine === null ? '' : sprintf(' on line %d', $currentTokenLine) )); } @@ -83,6 +89,12 @@ public function getExpectedTokenValue(): ?string } + public function getCurrentTokenLine(): ?int + { + return $this->currentTokenLine; + } + + private function formatValue(string $value): string { $json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE); diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index cee8042d..33b63ccc 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -31,12 +31,25 @@ class PhpDocParser /** @var bool */ private $preserveTypeAliasesWithInvalidTypes; - public function __construct(TypeParser $typeParser, ConstExprParser $constantExprParser, bool $requireWhitespaceBeforeDescription = false, bool $preserveTypeAliasesWithInvalidTypes = false) + /** @var bool */ + private $useLinesAttributes; + + /** + * @param array{lines?: bool} $usedAttributes + */ + public function __construct( + TypeParser $typeParser, + ConstExprParser $constantExprParser, + bool $requireWhitespaceBeforeDescription = false, + bool $preserveTypeAliasesWithInvalidTypes = false, + array $usedAttributes = [] + ) { $this->typeParser = $typeParser; $this->constantExprParser = $constantExprParser; $this->requireWhitespaceBeforeDescription = $requireWhitespaceBeforeDescription; $this->preserveTypeAliasesWithInvalidTypes = $preserveTypeAliasesWithInvalidTypes; + $this->useLinesAttributes = $usedAttributes['lines'] ?? false; } @@ -77,11 +90,28 @@ public function parse(TokenIterator $tokens): Ast\PhpDoc\PhpDocNode private function parseChild(TokenIterator $tokens): Ast\PhpDoc\PhpDocChildNode { if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG)) { - return $this->parseTag($tokens); + $startLine = $tokens->currentTokenLine(); + $tag = $this->parseTag($tokens); + $endLine = $tokens->currentTokenLine(); + if ($this->useLinesAttributes) { + $tag->setAttribute(Ast\Attribute::START_LINE, $startLine); + $tag->setAttribute(Ast\Attribute::END_LINE, $endLine); + } + + return $tag; } - return $this->parseText($tokens); + $startLine = $tokens->currentTokenLine(); + $text = $this->parseText($tokens); + $endLine = $tokens->currentTokenLine(); + + if ($this->useLinesAttributes) { + $text->setAttribute(Ast\Attribute::START_LINE, $startLine); + $text->setAttribute(Ast\Attribute::END_LINE, $endLine); + } + + return $text; } @@ -124,6 +154,8 @@ public function parseTag(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagNode public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\PhpDocTagValueNode { + $startLine = $tokens->currentTokenLine(); + try { $tokens->pushSavePoint(); @@ -251,6 +283,13 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph $tagValue = new Ast\PhpDoc\InvalidTagValueNode($this->parseOptionalDescription($tokens), $e); } + $endLine = $tokens->currentTokenLine(); + + if ($this->useLinesAttributes) { + $tagValue->setAttribute(Ast\Attribute::START_LINE, $startLine); + $tagValue->setAttribute(Ast\Attribute::END_LINE, $endLine); + } + return $tagValue; } @@ -466,7 +505,9 @@ private function parseTypeAliasTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeA $tokens->currentTokenValue(), $tokens->currentTokenType(), $tokens->currentTokenOffset(), - Lexer::TOKEN_PHPDOC_EOL + Lexer::TOKEN_PHPDOC_EOL, + null, + $tokens->currentTokenLine() ); } } diff --git a/src/Parser/TokenIterator.php b/src/Parser/TokenIterator.php index ab4a35de..866f5356 100644 --- a/src/Parser/TokenIterator.php +++ b/src/Parser/TokenIterator.php @@ -12,7 +12,7 @@ class TokenIterator { - /** @var list */ + /** @var list */ private $tokens; /** @var int */ @@ -22,7 +22,7 @@ class TokenIterator private $savePoints = []; /** - * @param list $tokens + * @param list $tokens */ public function __construct(array $tokens, int $index = 0) { @@ -60,6 +60,12 @@ public function currentTokenOffset(): int } + public function currentTokenLine(): int + { + return $this->tokens[$this->index][Lexer::LINE_OFFSET]; + } + + public function isCurrentTokenValue(string $tokenValue): bool { return $this->tokens[$this->index][Lexer::VALUE_OFFSET] === $tokenValue; @@ -220,7 +226,8 @@ private function throwError(int $expectedTokenType, ?string $expectedTokenValue $this->currentTokenType(), $this->currentTokenOffset(), $expectedTokenType, - $expectedTokenValue + $expectedTokenValue, + $this->currentTokenLine() ); } diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 661bccfa..cf99b16e 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -18,15 +18,27 @@ class TypeParser /** @var bool */ private $quoteAwareConstExprString; - public function __construct(?ConstExprParser $constExprParser = null, bool $quoteAwareConstExprString = false) + /** @var bool */ + private $useLinesAttributes; + + /** + * @param array{lines?: bool} $usedAttributes + */ + public function __construct( + ?ConstExprParser $constExprParser = null, + bool $quoteAwareConstExprString = false, + array $usedAttributes = [] + ) { $this->constExprParser = $constExprParser; $this->quoteAwareConstExprString = $quoteAwareConstExprString; + $this->useLinesAttributes = $usedAttributes['lines'] ?? false; } /** @phpstan-impure */ public function parse(TokenIterator $tokens): Ast\Type\TypeNode { + $startLine = $tokens->currentTokenLine(); if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) { $type = $this->parseNullable($tokens); @@ -40,6 +52,12 @@ public function parse(TokenIterator $tokens): Ast\Type\TypeNode $type = $this->parseIntersection($tokens, $type); } } + $endLine = $tokens->currentTokenLine(); + + if ($this->useLinesAttributes) { + $type->setAttribute(Ast\Attribute::START_LINE, $startLine); + $type->setAttribute(Ast\Attribute::END_LINE, $endLine); + } return $type; } @@ -152,7 +170,9 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode $tokens->currentTokenValue(), $tokens->currentTokenType(), $tokens->currentTokenOffset(), - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + $tokens->currentTokenLine() ); if ($this->constExprParser === null) { diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 8c1f17c6..64e2543d 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -3,6 +3,7 @@ namespace PHPStan\PhpDocParser\Parser; use Iterator; +use PHPStan\PhpDocParser\Ast\Attribute; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayItemNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; @@ -51,6 +52,7 @@ use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPUnit\Framework\TestCase; +use function count; use const PHP_EOL; class PhpDocParserTest extends TestCase @@ -309,7 +311,9 @@ public function provideParamTagsData(): Iterator '*/', Lexer::TOKEN_CLOSE_PHPDOC, 11, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ) ), @@ -328,7 +332,9 @@ public function provideParamTagsData(): Iterator '#desc', Lexer::TOKEN_OTHER, 11, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ) ), @@ -347,7 +353,9 @@ public function provideParamTagsData(): Iterator '*/', Lexer::TOKEN_CLOSE_PHPDOC, 16, - Lexer::TOKEN_CLOSE_PARENTHESES + Lexer::TOKEN_CLOSE_PARENTHESES, + null, + 1 ) ) ), @@ -366,7 +374,9 @@ public function provideParamTagsData(): Iterator '$foo', Lexer::TOKEN_VARIABLE, 16, - Lexer::TOKEN_CLOSE_PARENTHESES + Lexer::TOKEN_CLOSE_PARENTHESES, + null, + 1 ) ) ), @@ -385,7 +395,9 @@ public function provideParamTagsData(): Iterator '$foo', Lexer::TOKEN_VARIABLE, 19, - Lexer::TOKEN_CLOSE_ANGLE_BRACKET + Lexer::TOKEN_CLOSE_ANGLE_BRACKET, + null, + 1 ) ) ), @@ -404,7 +416,9 @@ public function provideParamTagsData(): Iterator '$foo', Lexer::TOKEN_VARIABLE, 16, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ) ), @@ -423,7 +437,32 @@ public function provideParamTagsData(): Iterator '*/', Lexer::TOKEN_CLOSE_PHPDOC, 15, - Lexer::TOKEN_VARIABLE + Lexer::TOKEN_VARIABLE, + null, + 1 + ) + ) + ), + ]), + ]; + + yield [ + 'invalid without parameter name and description - multiline', + '/** + * @param Foo + */', + new PhpDocNode([ + new PhpDocTagNode( + '@param', + new InvalidTagValueNode( + 'Foo', + new ParserException( + "\n\t\t\t ", + Lexer::TOKEN_PHPDOC_EOL, + 21, + Lexer::TOKEN_VARIABLE, + null, + 2 ) ) ), @@ -442,7 +481,9 @@ public function provideParamTagsData(): Iterator 'optional', Lexer::TOKEN_IDENTIFIER, 15, - Lexer::TOKEN_VARIABLE + Lexer::TOKEN_VARIABLE, + null, + 1 ) ) ), @@ -797,7 +838,9 @@ public function provideVarTagsData(): Iterator '*/', Lexer::TOKEN_CLOSE_PHPDOC, 9, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ) ), @@ -816,7 +859,9 @@ public function provideVarTagsData(): Iterator '#desc', Lexer::TOKEN_OTHER, 9, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ) ), @@ -835,7 +880,9 @@ public function provideVarTagsData(): Iterator '*/', Lexer::TOKEN_CLOSE_PHPDOC, 14, - Lexer::TOKEN_CLOSE_PARENTHESES + Lexer::TOKEN_CLOSE_PARENTHESES, + null, + 1 ) ) ), @@ -854,7 +901,9 @@ public function provideVarTagsData(): Iterator '$foo', Lexer::TOKEN_VARIABLE, 14, - Lexer::TOKEN_CLOSE_PARENTHESES + Lexer::TOKEN_CLOSE_PARENTHESES, + null, + 1 ) ) ), @@ -873,7 +922,9 @@ public function provideVarTagsData(): Iterator '$foo', Lexer::TOKEN_VARIABLE, 17, - Lexer::TOKEN_CLOSE_ANGLE_BRACKET + Lexer::TOKEN_CLOSE_ANGLE_BRACKET, + null, + 1 ) ) ), @@ -892,7 +943,9 @@ public function provideVarTagsData(): Iterator '$foo', Lexer::TOKEN_VARIABLE, 14, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ) ), @@ -906,12 +959,14 @@ public function provideVarTagsData(): Iterator new PhpDocTagNode( '@psalm-type', new InvalidTagValueNode( - 'Unexpected token "{", expected \'*/\' at offset 46', + 'Unexpected token "{", expected \'*/\' at offset 46 on line 1', new ParserException( '{', Lexer::TOKEN_OPEN_CURLY_BRACKET, 46, - Lexer::TOKEN_CLOSE_PHPDOC + Lexer::TOKEN_CLOSE_PHPDOC, + null, + 1 ) ) ), @@ -928,7 +983,8 @@ public function provideVarTagsData(): Iterator Lexer::TOKEN_OPEN_CURLY_BRACKET, 46, Lexer::TOKEN_PHPDOC_EOL, - null + null, + 1 ) ) ) @@ -982,7 +1038,9 @@ public function providePropertyTagsData(): Iterator '*/', Lexer::TOKEN_CLOSE_PHPDOC, 14, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ) ), @@ -1001,7 +1059,9 @@ public function providePropertyTagsData(): Iterator '#desc', Lexer::TOKEN_OTHER, 14, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ) ), @@ -1020,7 +1080,9 @@ public function providePropertyTagsData(): Iterator '*/', Lexer::TOKEN_CLOSE_PHPDOC, 19, - Lexer::TOKEN_CLOSE_PARENTHESES + Lexer::TOKEN_CLOSE_PARENTHESES, + null, + 1 ) ) ), @@ -1039,7 +1101,9 @@ public function providePropertyTagsData(): Iterator '$foo', Lexer::TOKEN_VARIABLE, 19, - Lexer::TOKEN_CLOSE_PARENTHESES + Lexer::TOKEN_CLOSE_PARENTHESES, + null, + 1 ) ) ), @@ -1058,7 +1122,9 @@ public function providePropertyTagsData(): Iterator '$foo', Lexer::TOKEN_VARIABLE, 22, - Lexer::TOKEN_CLOSE_ANGLE_BRACKET + Lexer::TOKEN_CLOSE_ANGLE_BRACKET, + null, + 1 ) ) ), @@ -1077,7 +1143,9 @@ public function providePropertyTagsData(): Iterator '$foo', Lexer::TOKEN_VARIABLE, 19, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ) ), @@ -1096,7 +1164,9 @@ public function providePropertyTagsData(): Iterator '*/', Lexer::TOKEN_CLOSE_PHPDOC, 18, - Lexer::TOKEN_VARIABLE + Lexer::TOKEN_VARIABLE, + null, + 1 ) ) ), @@ -1115,7 +1185,9 @@ public function providePropertyTagsData(): Iterator 'optional', Lexer::TOKEN_IDENTIFIER, 18, - Lexer::TOKEN_VARIABLE + Lexer::TOKEN_VARIABLE, + null, + 1 ) ) ), @@ -1197,7 +1269,9 @@ public function provideReturnTagsData(): Iterator '*/', Lexer::TOKEN_CLOSE_PHPDOC, 12, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ) ), @@ -1216,7 +1290,9 @@ public function provideReturnTagsData(): Iterator '[', Lexer::TOKEN_OPEN_SQUARE_BRACKET, 12, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ) ), @@ -1235,7 +1311,9 @@ public function provideReturnTagsData(): Iterator '#desc', Lexer::TOKEN_OTHER, 18, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ) ), @@ -1254,7 +1332,9 @@ public function provideReturnTagsData(): Iterator '|', Lexer::TOKEN_UNION, 18, - Lexer::TOKEN_OTHER + Lexer::TOKEN_OTHER, + null, + 1 ) ) ), @@ -1273,7 +1353,9 @@ public function provideReturnTagsData(): Iterator '&', Lexer::TOKEN_INTERSECTION, 18, - Lexer::TOKEN_OTHER + Lexer::TOKEN_OTHER, + null, + 1 ) ) ), @@ -1292,7 +1374,9 @@ public function provideReturnTagsData(): Iterator '*/', Lexer::TOKEN_CLOSE_PHPDOC, 24, - Lexer::TOKEN_CLOSE_ANGLE_BRACKET + Lexer::TOKEN_CLOSE_ANGLE_BRACKET, + null, + 1 ) ) ), @@ -1500,7 +1584,9 @@ public function provideReturnTagsData(): Iterator '$foo', Lexer::TOKEN_VARIABLE, 12, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ) ), @@ -1561,7 +1647,9 @@ public function provideReturnTagsData(): Iterator '(', Lexer::TOKEN_OPEN_PARENTHESES, 20, - Lexer::TOKEN_HORIZONTAL_WS + Lexer::TOKEN_HORIZONTAL_WS, + null, + 1 ) ) ), @@ -1626,7 +1714,9 @@ public function provideThrowsTagsData(): Iterator '*/', Lexer::TOKEN_CLOSE_PHPDOC, 12, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ) ), @@ -1645,7 +1735,9 @@ public function provideThrowsTagsData(): Iterator '#desc', Lexer::TOKEN_OTHER, 18, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ) ), @@ -1709,7 +1801,9 @@ public function provideMixinTagsData(): Iterator '*/', Lexer::TOKEN_CLOSE_PHPDOC, 11, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ) ), @@ -1728,7 +1822,9 @@ public function provideMixinTagsData(): Iterator '#desc', Lexer::TOKEN_OTHER, 17, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ) ), @@ -2190,7 +2286,9 @@ public function provideMethodTagsData(): Iterator '*/', Lexer::TOKEN_CLOSE_PHPDOC, 16, - Lexer::TOKEN_OPEN_PARENTHESES + Lexer::TOKEN_OPEN_PARENTHESES, + null, + 1 ) ) ), @@ -2209,7 +2307,9 @@ public function provideMethodTagsData(): Iterator '*/', Lexer::TOKEN_CLOSE_PHPDOC, 23, - Lexer::TOKEN_OPEN_PARENTHESES + Lexer::TOKEN_OPEN_PARENTHESES, + null, + 1 ) ) ), @@ -2228,7 +2328,9 @@ public function provideMethodTagsData(): Iterator ')', Lexer::TOKEN_CLOSE_PARENTHESES, 17, - Lexer::TOKEN_VARIABLE + Lexer::TOKEN_VARIABLE, + null, + 1 ) ) ), @@ -2475,7 +2577,9 @@ public function provideSingleLinePhpDocData(): Iterator '(', Lexer::TOKEN_OPEN_PARENTHESES, 17, - Lexer::TOKEN_HORIZONTAL_WS + Lexer::TOKEN_HORIZONTAL_WS, + null, + 1 ) ) ), @@ -3494,7 +3598,9 @@ public function provideTemplateTagsData(): Iterator '*/', Lexer::TOKEN_CLOSE_PHPDOC, 14, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ) ), @@ -3513,7 +3619,9 @@ public function provideTemplateTagsData(): Iterator '#desc', Lexer::TOKEN_OTHER, 14, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ) ), @@ -3725,7 +3833,9 @@ public function provideExtendsTagsData(): Iterator '*/', Lexer::TOKEN_CLOSE_PHPDOC, 13, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ) ), @@ -3744,7 +3854,9 @@ public function provideExtendsTagsData(): Iterator '*/', Lexer::TOKEN_CLOSE_PHPDOC, 17, - Lexer::TOKEN_OPEN_ANGLE_BRACKET + Lexer::TOKEN_OPEN_ANGLE_BRACKET, + null, + 1 ) ) ), @@ -3860,7 +3972,9 @@ public function provideTypeAliasTagsData(): Iterator '*/', Lexer::TOKEN_CLOSE_PHPDOC, 28, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ) ), @@ -3876,7 +3990,8 @@ public function provideTypeAliasTagsData(): Iterator Lexer::TOKEN_CLOSE_PHPDOC, 28, Lexer::TOKEN_IDENTIFIER, - null + null, + 1 )) ) ), @@ -3897,7 +4012,9 @@ public function provideTypeAliasTagsData(): Iterator "\n\t\t\t ", Lexer::TOKEN_PHPDOC_EOL, 34, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 2 ) ) ), @@ -3913,7 +4030,8 @@ public function provideTypeAliasTagsData(): Iterator Lexer::TOKEN_PHPDOC_EOL, 34, Lexer::TOKEN_IDENTIFIER, - null + null, + 2 )) ) ), @@ -3935,7 +4053,9 @@ public function provideTypeAliasTagsData(): Iterator "\n\t\t\t * ", Lexer::TOKEN_PHPDOC_EOL, 34, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 2 ) ) ), @@ -3958,7 +4078,8 @@ public function provideTypeAliasTagsData(): Iterator Lexer::TOKEN_PHPDOC_EOL, 34, Lexer::TOKEN_IDENTIFIER, - null + null, + 2 )) ) ), @@ -3982,13 +4103,14 @@ public function provideTypeAliasTagsData(): Iterator new PhpDocTagNode( '@phpstan-type', new InvalidTagValueNode( - "Unexpected token \"{\", expected '*/' at offset 65", + "Unexpected token \"{\", expected '*/' at offset 65 on line 3", new ParserException( '{', Lexer::TOKEN_OPEN_CURLY_BRACKET, 65, Lexer::TOKEN_CLOSE_PHPDOC, - null + null, + 3 ) ) ), @@ -4011,7 +4133,8 @@ public function provideTypeAliasTagsData(): Iterator Lexer::TOKEN_OPEN_CURLY_BRACKET, 65, Lexer::TOKEN_PHPDOC_EOL, - null + null, + 3 )) ) ), @@ -4029,13 +4152,14 @@ public function provideTypeAliasTagsData(): Iterator new PhpDocTagNode( '@phpstan-type', new InvalidTagValueNode( - "Unexpected token \"{\", expected '*/' at offset 65", + "Unexpected token \"{\", expected '*/' at offset 65 on line 3", new ParserException( '{', Lexer::TOKEN_OPEN_CURLY_BRACKET, 65, Lexer::TOKEN_CLOSE_PHPDOC, - null + null, + 3 ) ) ), @@ -4058,7 +4182,8 @@ public function provideTypeAliasTagsData(): Iterator Lexer::TOKEN_OPEN_CURLY_BRACKET, 65, Lexer::TOKEN_PHPDOC_EOL, - null + null, + 3 )) ) ), @@ -4084,7 +4209,9 @@ public function provideTypeAliasTagsData(): Iterator '*/', Lexer::TOKEN_CLOSE_PHPDOC, 18, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ) ), @@ -4136,7 +4263,9 @@ public function provideTypeAliasImportTagsData(): Iterator '42', Lexer::TOKEN_INTEGER, 40, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ) ), @@ -4150,12 +4279,14 @@ public function provideTypeAliasImportTagsData(): Iterator new PhpDocTagNode( '@phpstan-import-type', new InvalidTagValueNode( - 'Unexpected token "[", expected \'*/\' at offset 52', + 'Unexpected token "[", expected \'*/\' at offset 52 on line 1', new ParserException( '[', Lexer::TOKEN_OPEN_SQUARE_BRACKET, 52, - Lexer::TOKEN_CLOSE_PHPDOC + Lexer::TOKEN_CLOSE_PHPDOC, + null, + 1 ) ) ), @@ -4175,7 +4306,8 @@ public function provideTypeAliasImportTagsData(): Iterator Lexer::TOKEN_CLOSE_PHPDOC, 35, Lexer::TOKEN_IDENTIFIER, - 'from' + 'from', + 1 ) ) ), @@ -4195,7 +4327,8 @@ public function provideTypeAliasImportTagsData(): Iterator Lexer::TOKEN_IDENTIFIER, 35, Lexer::TOKEN_IDENTIFIER, - 'from' + 'from', + 1 ) ) ), @@ -4214,7 +4347,9 @@ public function provideTypeAliasImportTagsData(): Iterator '*/', Lexer::TOKEN_CLOSE_PHPDOC, 25, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ) ), @@ -4337,7 +4472,9 @@ public function provideAssertTagsData(): Iterator '*/', Lexer::TOKEN_CLOSE_PHPDOC, 31, - Lexer::TOKEN_ARROW + Lexer::TOKEN_ARROW, + null, + 1 ) ) ), @@ -4680,7 +4817,7 @@ public function provideRealWorldExampleData(): Iterator 'malformed const fetch', '/** @param Foo::** $a */', new PhpDocNode([ - new PhpDocTagNode('@param', new InvalidTagValueNode('Foo::** $a', new ParserException('*', Lexer::TOKEN_WILDCARD, 17, Lexer::TOKEN_VARIABLE))), + new PhpDocTagNode('@param', new InvalidTagValueNode('Foo::** $a', new ParserException('*', Lexer::TOKEN_WILDCARD, 17, Lexer::TOKEN_VARIABLE, null, 1))), ]), ]; @@ -5128,7 +5265,9 @@ public function provideDescriptionWithOrWithoutHtml(): Iterator 'Important', Lexer::TOKEN_IDENTIFIER, 27, - Lexer::TOKEN_HORIZONTAL_WS + Lexer::TOKEN_HORIZONTAL_WS, + null, + 2 ) ) ), @@ -5158,7 +5297,9 @@ public function dataParseTagValue(): array '$foo', Lexer::TOKEN_VARIABLE, 0, - Lexer::TOKEN_IDENTIFIER + Lexer::TOKEN_IDENTIFIER, + null, + 1 ) ), ], @@ -5376,4 +5517,72 @@ public function testNegatedAssertionToString(): void $this->assertSame('@phpstan-assert !Type $param', $assertNode->__toString()); } + public function dataLines(): iterable + { + yield [ + '/** @param Foo $a */', + [ + [1, 1], + ], + ]; + + yield [ + '/** + * @param Foo $foo 1st multi world description + * @param Bar $bar 2nd multi world description + */', + [ + [2, 2], + [3, 3], + ], + ]; + + yield [ + '/** + * @template TRandKey as array-key + * @template TRandVal + * @template TRandList as array|XIterator|Traversable + * + * @param TRandList $list + * + * @return ( + * TRandList is array ? array : ( + * TRandList is XIterator ? XIterator : + * IteratorIterator|LimitIterator + * )) + */', + [ + [2, 2], + [3, 3], + [4, 4], + [5, 5], + [6, 6], + [7, 7], + [8, 12], + ], + ]; + } + + /** + * @dataProvider dataLines + * @param list $childrenLines + */ + public function testLines(string $phpDoc, array $childrenLines): void + { + $tokens = new TokenIterator($this->lexer->tokenize($phpDoc)); + $constExprParser = new ConstExprParser(true, true); + $usedAttributes = [ + 'lines' => true, + ]; + $typeParser = new TypeParser($constExprParser, true, $usedAttributes); + $phpDocParser = new PhpDocParser($typeParser, $constExprParser, true, true, $usedAttributes); + $phpDocNode = $phpDocParser->parse($tokens); + $children = $phpDocNode->children; + $this->assertCount(count($childrenLines), $children); + foreach ($children as $i => $child) { + $this->assertSame($childrenLines[$i][0], $child->getAttribute(Attribute::START_LINE)); + $this->assertSame($childrenLines[$i][1], $child->getAttribute(Attribute::END_LINE)); + } + } + } diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 9416d72e..9af2be32 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -3,6 +3,7 @@ namespace PHPStan\PhpDocParser\Parser; use Exception; +use PHPStan\PhpDocParser\Ast\Attribute; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; @@ -1904,4 +1905,36 @@ public function provideParseData(): array ]; } + public function dataLines(): iterable + { + yield [ + 'int | object{foo: int}[]', + 1, + 1, + ]; + + yield [ + 'array{ + a: int, + b: string + }', + 1, + 4, + ]; + } + + /** + * @dataProvider dataLines + */ + public function testLines(string $input, int $startLine, int $endLine): void + { + $tokens = new TokenIterator($this->lexer->tokenize($input)); + $typeParser = new TypeParser(new ConstExprParser(true, true), true, [ + 'lines' => true, + ]); + $typeNode = $typeParser->parse($tokens); + $this->assertSame($startLine, $typeNode->getAttribute(Attribute::START_LINE)); + $this->assertSame($endLine, $typeNode->getAttribute(Attribute::END_LINE)); + } + } From ddc8c7aa8971ddae3611be0dbf2317c841e67c28 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 18 Apr 2023 17:08:57 +0200 Subject: [PATCH 33/59] Set start and end indexes to node attributes --- src/Ast/Attribute.php | 3 +++ src/Parser/PhpDocParser.php | 27 ++++++++++++++++++++++- src/Parser/TokenIterator.php | 6 +++++ src/Parser/TypeParser.php | 13 ++++++++++- tests/PHPStan/Parser/PhpDocParserTest.php | 27 +++++++++++++---------- tests/PHPStan/Parser/TypeParserTest.php | 9 +++++++- 6 files changed, 70 insertions(+), 15 deletions(-) diff --git a/src/Ast/Attribute.php b/src/Ast/Attribute.php index c5a7adf2..8beccb79 100644 --- a/src/Ast/Attribute.php +++ b/src/Ast/Attribute.php @@ -8,4 +8,7 @@ final class Attribute public const START_LINE = 'startLine'; public const END_LINE = 'endLine'; + public const START_INDEX = 'startIndex'; + public const END_INDEX = 'endIndex'; + } diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index 33b63ccc..ccde2ae4 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -34,8 +34,11 @@ class PhpDocParser /** @var bool */ private $useLinesAttributes; + /** @var bool */ + private $useIndexAttributes; + /** - * @param array{lines?: bool} $usedAttributes + * @param array{lines?: bool, indexes?: bool} $usedAttributes */ public function __construct( TypeParser $typeParser, @@ -50,6 +53,7 @@ public function __construct( $this->requireWhitespaceBeforeDescription = $requireWhitespaceBeforeDescription; $this->preserveTypeAliasesWithInvalidTypes = $preserveTypeAliasesWithInvalidTypes; $this->useLinesAttributes = $usedAttributes['lines'] ?? false; + $this->useIndexAttributes = $usedAttributes['indexes'] ?? false; } @@ -91,26 +95,40 @@ private function parseChild(TokenIterator $tokens): Ast\PhpDoc\PhpDocChildNode { if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG)) { $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); $tag = $this->parseTag($tokens); $endLine = $tokens->currentTokenLine(); + $endIndex = $tokens->currentTokenIndex(); if ($this->useLinesAttributes) { $tag->setAttribute(Ast\Attribute::START_LINE, $startLine); $tag->setAttribute(Ast\Attribute::END_LINE, $endLine); } + if ($this->useIndexAttributes) { + $tag->setAttribute(Ast\Attribute::START_INDEX, $startIndex); + $tag->setAttribute(Ast\Attribute::END_INDEX, $endIndex); + } + return $tag; } $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); $text = $this->parseText($tokens); $endLine = $tokens->currentTokenLine(); + $endIndex = $tokens->currentTokenIndex(); if ($this->useLinesAttributes) { $text->setAttribute(Ast\Attribute::START_LINE, $startLine); $text->setAttribute(Ast\Attribute::END_LINE, $endLine); } + if ($this->useIndexAttributes) { + $text->setAttribute(Ast\Attribute::START_INDEX, $startIndex); + $text->setAttribute(Ast\Attribute::END_INDEX, $endIndex); + } + return $text; } @@ -155,6 +173,7 @@ public function parseTag(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagNode public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\PhpDocTagValueNode { $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); try { $tokens->pushSavePoint(); @@ -284,12 +303,18 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph } $endLine = $tokens->currentTokenLine(); + $endIndex = $tokens->currentTokenIndex(); if ($this->useLinesAttributes) { $tagValue->setAttribute(Ast\Attribute::START_LINE, $startLine); $tagValue->setAttribute(Ast\Attribute::END_LINE, $endLine); } + if ($this->useIndexAttributes) { + $tagValue->setAttribute(Ast\Attribute::START_INDEX, $startIndex); + $tagValue->setAttribute(Ast\Attribute::END_INDEX, $endIndex); + } + return $tagValue; } diff --git a/src/Parser/TokenIterator.php b/src/Parser/TokenIterator.php index 866f5356..f01200ac 100644 --- a/src/Parser/TokenIterator.php +++ b/src/Parser/TokenIterator.php @@ -66,6 +66,12 @@ public function currentTokenLine(): int } + public function currentTokenIndex(): int + { + return $this->index; + } + + public function isCurrentTokenValue(string $tokenValue): bool { return $this->tokens[$this->index][Lexer::VALUE_OFFSET] === $tokenValue; diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index cf99b16e..a5a67dab 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -21,8 +21,11 @@ class TypeParser /** @var bool */ private $useLinesAttributes; + /** @var bool */ + private $useIndexAttributes; + /** - * @param array{lines?: bool} $usedAttributes + * @param array{lines?: bool, indexes?: bool} $usedAttributes */ public function __construct( ?ConstExprParser $constExprParser = null, @@ -33,12 +36,14 @@ public function __construct( $this->constExprParser = $constExprParser; $this->quoteAwareConstExprString = $quoteAwareConstExprString; $this->useLinesAttributes = $usedAttributes['lines'] ?? false; + $this->useIndexAttributes = $usedAttributes['indexes'] ?? false; } /** @phpstan-impure */ public function parse(TokenIterator $tokens): Ast\Type\TypeNode { $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) { $type = $this->parseNullable($tokens); @@ -53,12 +58,18 @@ public function parse(TokenIterator $tokens): Ast\Type\TypeNode } } $endLine = $tokens->currentTokenLine(); + $endIndex = $tokens->currentTokenIndex(); if ($this->useLinesAttributes) { $type->setAttribute(Ast\Attribute::START_LINE, $startLine); $type->setAttribute(Ast\Attribute::END_LINE, $endLine); } + if ($this->useIndexAttributes) { + $type->setAttribute(Ast\Attribute::START_INDEX, $startIndex); + $type->setAttribute(Ast\Attribute::END_INDEX, $endIndex); + } + return $type; } diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 64e2543d..253d9bdf 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -5522,7 +5522,7 @@ public function dataLines(): iterable yield [ '/** @param Foo $a */', [ - [1, 1], + [1, 1, 1, 7], ], ]; @@ -5532,8 +5532,8 @@ public function dataLines(): iterable * @param Bar $bar 2nd multi world description */', [ - [2, 2], - [3, 3], + [2, 2, 2, 16], + [3, 3, 17, 31], ], ]; @@ -5552,27 +5552,28 @@ public function dataLines(): iterable * )) */', [ - [2, 2], - [3, 3], - [4, 4], - [5, 5], - [6, 6], - [7, 7], - [8, 12], + [2, 2, 2, 9], + [3, 3, 10, 13], + [4, 4, 14, 43], + [5, 5, 44, 44], + [6, 6, 45, 50], + [7, 7, 51, 51], + [8, 12, 52, 115], ], ]; } /** * @dataProvider dataLines - * @param list $childrenLines + * @param list $childrenLines */ - public function testLines(string $phpDoc, array $childrenLines): void + public function testLinesAndIndexes(string $phpDoc, array $childrenLines): void { $tokens = new TokenIterator($this->lexer->tokenize($phpDoc)); $constExprParser = new ConstExprParser(true, true); $usedAttributes = [ 'lines' => true, + 'indexes' => true, ]; $typeParser = new TypeParser($constExprParser, true, $usedAttributes); $phpDocParser = new PhpDocParser($typeParser, $constExprParser, true, true, $usedAttributes); @@ -5582,6 +5583,8 @@ public function testLines(string $phpDoc, array $childrenLines): void foreach ($children as $i => $child) { $this->assertSame($childrenLines[$i][0], $child->getAttribute(Attribute::START_LINE)); $this->assertSame($childrenLines[$i][1], $child->getAttribute(Attribute::END_LINE)); + $this->assertSame($childrenLines[$i][2], $child->getAttribute(Attribute::START_INDEX)); + $this->assertSame($childrenLines[$i][3], $child->getAttribute(Attribute::END_INDEX)); } } diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 9af2be32..e63289e8 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -1911,6 +1911,8 @@ public function dataLines(): iterable 'int | object{foo: int}[]', 1, 1, + 0, + 13, ]; yield [ @@ -1920,21 +1922,26 @@ public function dataLines(): iterable }', 1, 4, + 0, + 15, ]; } /** * @dataProvider dataLines */ - public function testLines(string $input, int $startLine, int $endLine): void + public function testLines(string $input, int $startLine, int $endLine, int $startIndex, int $endIndex): void { $tokens = new TokenIterator($this->lexer->tokenize($input)); $typeParser = new TypeParser(new ConstExprParser(true, true), true, [ 'lines' => true, + 'indexes' => true, ]); $typeNode = $typeParser->parse($tokens); $this->assertSame($startLine, $typeNode->getAttribute(Attribute::START_LINE)); $this->assertSame($endLine, $typeNode->getAttribute(Attribute::END_LINE)); + $this->assertSame($startIndex, $typeNode->getAttribute(Attribute::START_INDEX)); + $this->assertSame($endIndex, $typeNode->getAttribute(Attribute::END_INDEX)); } } From 4255dd3d360576b8ecc16ad951b40263b1899a67 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 18 Apr 2023 17:22:48 +0200 Subject: [PATCH 34/59] Rename tests --- tests/PHPStan/Parser/PhpDocParserTest.php | 4 ++-- tests/PHPStan/Parser/TypeParserTest.php | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 253d9bdf..0555031b 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -5517,7 +5517,7 @@ public function testNegatedAssertionToString(): void $this->assertSame('@phpstan-assert !Type $param', $assertNode->__toString()); } - public function dataLines(): iterable + public function dataLinesAndIndexes(): iterable { yield [ '/** @param Foo $a */', @@ -5564,7 +5564,7 @@ public function dataLines(): iterable } /** - * @dataProvider dataLines + * @dataProvider dataLinesAndIndexes * @param list $childrenLines */ public function testLinesAndIndexes(string $phpDoc, array $childrenLines): void diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index e63289e8..f5ec3f28 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -1905,7 +1905,7 @@ public function provideParseData(): array ]; } - public function dataLines(): iterable + public function dataLinesAndIndexes(): iterable { yield [ 'int | object{foo: int}[]', @@ -1928,9 +1928,9 @@ public function dataLines(): iterable } /** - * @dataProvider dataLines + * @dataProvider dataLinesAndIndexes */ - public function testLines(string $input, int $startLine, int $endLine, int $startIndex, int $endIndex): void + public function testLinesAndIndexes(string $input, int $startLine, int $endLine, int $startIndex, int $endIndex): void { $tokens = new TokenIterator($this->lexer->tokenize($input)); $typeParser = new TypeParser(new ConstExprParser(true, true), true, [ From 7ff42f5d4a95db0b228f0feff5f6fc48a80cfb7b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 19 Apr 2023 14:34:34 +0200 Subject: [PATCH 35/59] TypeParser - give all types the line and index attributes --- src/Parser/TypeParser.php | 28 +++++++---- tests/PHPStan/Parser/TypeParserTest.php | 63 ++++++++++++++++++++----- 2 files changed, 70 insertions(+), 21 deletions(-) diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index a5a67dab..5d005a63 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -57,6 +57,12 @@ public function parse(TokenIterator $tokens): Ast\Type\TypeNode $type = $this->parseIntersection($tokens, $type); } } + + return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); + } + + private function enrichWithAttributes(TokenIterator $tokens, Ast\Type\TypeNode $type, int $startLine, int $startIndex): Ast\Type\TypeNode + { $endLine = $tokens->currentTokenLine(); $endIndex = $tokens->currentTokenIndex(); @@ -76,6 +82,9 @@ public function parse(TokenIterator $tokens): Ast\Type\TypeNode /** @phpstan-impure */ private function subParse(TokenIterator $tokens): Ast\Type\TypeNode { + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); + if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) { $type = $this->parseNullable($tokens); @@ -99,13 +108,16 @@ private function subParse(TokenIterator $tokens): Ast\Type\TypeNode } } - return $type; + return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); } /** @phpstan-impure */ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode { + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); + if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) { $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); $type = $this->subParse($tokens); @@ -114,20 +126,20 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES); if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { - return $this->tryParseArrayOrOffsetAccess($tokens, $type); + $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); } - return $type; + return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); } if ($tokens->tryConsumeTokenType(Lexer::TOKEN_THIS_VARIABLE)) { $type = new Ast\Type\ThisTypeNode(); if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { - return $this->tryParseArrayOrOffsetAccess($tokens, $type); + $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); } - return $type; + return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); } $currentTokenValue = $tokens->currentTokenValue(); @@ -143,7 +155,7 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode $isHtml = $this->isHtml($tokens); $tokens->rollback(); if ($isHtml) { - return $type; + return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); } $type = $this->parseGeneric($tokens, $type); @@ -169,7 +181,7 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode } } - return $type; + return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); } else { $tokens->rollback(); // because of ConstFetchNode } @@ -196,7 +208,7 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode throw $exception; } - return new Ast\Type\ConstTypeNode($constExpr); + return $this->enrichWithAttributes($tokens, new Ast\Type\ConstTypeNode($constExpr), $startLine, $startIndex); } catch (LogicException $e) { throw $exception; } diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index f5ec3f28..2e2c75e8 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -1909,10 +1909,35 @@ public function dataLinesAndIndexes(): iterable { yield [ 'int | object{foo: int}[]', - 1, - 1, - 0, - 13, + [ + [ + static function (TypeNode $typeNode): TypeNode { + return $typeNode; + }, + 1, + 1, + 0, + 13, + ], + [ + static function (UnionTypeNode $typeNode): TypeNode { + return $typeNode->types[0]; + }, + 1, + 1, + 0, + 2, + ], + [ + static function (UnionTypeNode $typeNode): TypeNode { + return $typeNode->types[1]; + }, + 1, + 1, + 4, + 13, + ], + ], ]; yield [ @@ -1920,17 +1945,25 @@ public function dataLinesAndIndexes(): iterable a: int, b: string }', - 1, - 4, - 0, - 15, + [ + [ + static function (TypeNode $typeNode): TypeNode { + return $typeNode; + }, + 1, + 4, + 0, + 15, + ], + ], ]; } /** * @dataProvider dataLinesAndIndexes + * @param list $assertions */ - public function testLinesAndIndexes(string $input, int $startLine, int $endLine, int $startIndex, int $endIndex): void + public function testLinesAndIndexes(string $input, array $assertions): void { $tokens = new TokenIterator($this->lexer->tokenize($input)); $typeParser = new TypeParser(new ConstExprParser(true, true), true, [ @@ -1938,10 +1971,14 @@ public function testLinesAndIndexes(string $input, int $startLine, int $endLine, 'indexes' => true, ]); $typeNode = $typeParser->parse($tokens); - $this->assertSame($startLine, $typeNode->getAttribute(Attribute::START_LINE)); - $this->assertSame($endLine, $typeNode->getAttribute(Attribute::END_LINE)); - $this->assertSame($startIndex, $typeNode->getAttribute(Attribute::START_INDEX)); - $this->assertSame($endIndex, $typeNode->getAttribute(Attribute::END_INDEX)); + + foreach ($assertions as [$callable, $startLine, $endLine, $startIndex, $endIndex]) { + $typeToAssert = $callable($typeNode); + $this->assertSame($startLine, $typeToAssert->getAttribute(Attribute::START_LINE)); + $this->assertSame($endLine, $typeToAssert->getAttribute(Attribute::END_LINE)); + $this->assertSame($startIndex, $typeToAssert->getAttribute(Attribute::START_INDEX)); + $this->assertSame($endIndex, $typeToAssert->getAttribute(Attribute::END_INDEX)); + } } } From 0138dd9483e56106bd0776b8d7bf055f5880803f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 19 Apr 2023 15:03:03 +0200 Subject: [PATCH 36/59] Give even invalid nodes line and index attributes --- src/Parser/PhpDocParser.php | 63 ++++++++++------------- tests/PHPStan/Parser/PhpDocParserTest.php | 21 ++++++++ 2 files changed, 47 insertions(+), 37 deletions(-) diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index ccde2ae4..085282c0 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -75,16 +75,21 @@ public function parse(TokenIterator $tokens): Ast\PhpDoc\PhpDocNode $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PHPDOC); } catch (ParserException $e) { $name = ''; + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); if (count($children) > 0) { $lastChild = $children[count($children) - 1]; if ($lastChild instanceof Ast\PhpDoc\PhpDocTagNode) { $name = $lastChild->name; + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); } } + $tokens->forwardToTheEnd(); - return new Ast\PhpDoc\PhpDocNode([ - new Ast\PhpDoc\PhpDocTagNode($name, new Ast\PhpDoc\InvalidTagValueNode($e->getMessage(), $e)), - ]); + $tag = new Ast\PhpDoc\PhpDocTagNode($name, new Ast\PhpDoc\InvalidTagValueNode($e->getMessage(), $e)); + + return new Ast\PhpDoc\PhpDocNode([$this->enrichWithAttributes($tokens, $tag, $startLine, $startIndex)]); } return new Ast\PhpDoc\PhpDocNode(array_values($children)); @@ -96,40 +101,37 @@ private function parseChild(TokenIterator $tokens): Ast\PhpDoc\PhpDocChildNode if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG)) { $startLine = $tokens->currentTokenLine(); $startIndex = $tokens->currentTokenIndex(); - $tag = $this->parseTag($tokens); - $endLine = $tokens->currentTokenLine(); - $endIndex = $tokens->currentTokenIndex(); - - if ($this->useLinesAttributes) { - $tag->setAttribute(Ast\Attribute::START_LINE, $startLine); - $tag->setAttribute(Ast\Attribute::END_LINE, $endLine); - } - - if ($this->useIndexAttributes) { - $tag->setAttribute(Ast\Attribute::START_INDEX, $startIndex); - $tag->setAttribute(Ast\Attribute::END_INDEX, $endIndex); - } - - return $tag; + return $this->enrichWithAttributes($tokens, $this->parseTag($tokens), $startLine, $startIndex); } $startLine = $tokens->currentTokenLine(); $startIndex = $tokens->currentTokenIndex(); $text = $this->parseText($tokens); + + return $this->enrichWithAttributes($tokens, $text, $startLine, $startIndex); + } + + /** + * @template T of Ast\Node + * @param T $tag + * @return T + */ + private function enrichWithAttributes(TokenIterator $tokens, Ast\Node $tag, int $startLine, int $startIndex): Ast\Node + { $endLine = $tokens->currentTokenLine(); $endIndex = $tokens->currentTokenIndex(); if ($this->useLinesAttributes) { - $text->setAttribute(Ast\Attribute::START_LINE, $startLine); - $text->setAttribute(Ast\Attribute::END_LINE, $endLine); + $tag->setAttribute(Ast\Attribute::START_LINE, $startLine); + $tag->setAttribute(Ast\Attribute::END_LINE, $endLine); } if ($this->useIndexAttributes) { - $text->setAttribute(Ast\Attribute::START_INDEX, $startIndex); - $text->setAttribute(Ast\Attribute::END_INDEX, $endIndex); + $tag->setAttribute(Ast\Attribute::START_INDEX, $startIndex); + $tag->setAttribute(Ast\Attribute::END_INDEX, $endIndex); } - return $text; + return $tag; } @@ -302,20 +304,7 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph $tagValue = new Ast\PhpDoc\InvalidTagValueNode($this->parseOptionalDescription($tokens), $e); } - $endLine = $tokens->currentTokenLine(); - $endIndex = $tokens->currentTokenIndex(); - - if ($this->useLinesAttributes) { - $tagValue->setAttribute(Ast\Attribute::START_LINE, $startLine); - $tagValue->setAttribute(Ast\Attribute::END_LINE, $endLine); - } - - if ($this->useIndexAttributes) { - $tagValue->setAttribute(Ast\Attribute::START_INDEX, $startIndex); - $tagValue->setAttribute(Ast\Attribute::END_INDEX, $endIndex); - } - - return $tagValue; + return $this->enrichWithAttributes($tokens, $tagValue, $startLine, $startIndex); } diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 0555031b..444b7ecf 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -5561,6 +5561,27 @@ public function dataLinesAndIndexes(): iterable [8, 12, 52, 115], ], ]; + + yield [ + '/** @param Foo( */', + [ + [1, 1, 1, 6], + ], + ]; + + yield [ + '/** @phpstan-import-type TypeAlias from AnotherClass[] */', + [ + [1, 1, 8, 12], + ], + ]; + + yield [ + '/** @param Foo::** $a */', + [ + [1, 1, 1, 10], + ], + ]; } /** From 1ae8d74428e229264bbf0a4184f65d6211376204 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 19 Apr 2023 15:12:49 +0200 Subject: [PATCH 37/59] Add missing typehints --- src/Ast/ConstExpr/QuoteAwareConstExprStringNode.php | 2 +- src/Ast/PhpDoc/InvalidTagValueNode.php | 2 +- src/Ast/PhpDoc/MethodTagValueNode.php | 4 ++++ src/Ast/Type/ArrayShapeNode.php | 1 + src/Ast/Type/CallableTypeNode.php | 3 +++ src/Ast/Type/GenericTypeNode.php | 4 ++++ src/Ast/Type/IntersectionTypeNode.php | 3 +++ src/Ast/Type/ObjectShapeNode.php | 3 +++ src/Ast/Type/UnionTypeNode.php | 3 +++ tests/PHPStan/Parser/PhpDocParserTest.php | 10 +++++++++- tests/PHPStan/Parser/TypeParserTest.php | 6 ++++++ 11 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/Ast/ConstExpr/QuoteAwareConstExprStringNode.php b/src/Ast/ConstExpr/QuoteAwareConstExprStringNode.php index 0b9360cc..f2792b1b 100644 --- a/src/Ast/ConstExpr/QuoteAwareConstExprStringNode.php +++ b/src/Ast/ConstExpr/QuoteAwareConstExprStringNode.php @@ -45,7 +45,7 @@ public function __toString(): string return sprintf('"%s"', $this->escapeDoubleQuotedString()); } - private function escapeDoubleQuotedString() + private function escapeDoubleQuotedString(): string { $quote = '"'; $escaped = addcslashes($this->value, "\n\r\t\f\v$" . $quote . '\\'); diff --git a/src/Ast/PhpDoc/InvalidTagValueNode.php b/src/Ast/PhpDoc/InvalidTagValueNode.php index 3226afd4..ca7b4f20 100644 --- a/src/Ast/PhpDoc/InvalidTagValueNode.php +++ b/src/Ast/PhpDoc/InvalidTagValueNode.php @@ -35,7 +35,7 @@ public function __construct(string $value, ParserException $exception) ]; } - public function __get(string $name) + public function __get(string $name): ?ParserException { if ($name !== 'exception') { trigger_error(sprintf('Undefined property: %s::$%s', self::class, $name), E_USER_WARNING); diff --git a/src/Ast/PhpDoc/MethodTagValueNode.php b/src/Ast/PhpDoc/MethodTagValueNode.php index 075cec04..211510be 100644 --- a/src/Ast/PhpDoc/MethodTagValueNode.php +++ b/src/Ast/PhpDoc/MethodTagValueNode.php @@ -30,6 +30,10 @@ class MethodTagValueNode implements PhpDocTagValueNode /** @var string (may be empty) */ public $description; + /** + * @param MethodTagValueParameterNode[] $parameters + * @param TemplateTagValueNode[] $templateTypes + */ public function __construct(bool $isStatic, ?TypeNode $returnType, string $methodName, array $parameters, string $description, array $templateTypes = []) { $this->isStatic = $isStatic; diff --git a/src/Ast/Type/ArrayShapeNode.php b/src/Ast/Type/ArrayShapeNode.php index 41941f80..806783f9 100644 --- a/src/Ast/Type/ArrayShapeNode.php +++ b/src/Ast/Type/ArrayShapeNode.php @@ -23,6 +23,7 @@ class ArrayShapeNode implements TypeNode public $kind; /** + * @param ArrayShapeItemNode[] $items * @param self::KIND_* $kind */ public function __construct(array $items, bool $sealed = true, string $kind = self::KIND_ARRAY) diff --git a/src/Ast/Type/CallableTypeNode.php b/src/Ast/Type/CallableTypeNode.php index ba400851..e57e5f82 100644 --- a/src/Ast/Type/CallableTypeNode.php +++ b/src/Ast/Type/CallableTypeNode.php @@ -19,6 +19,9 @@ class CallableTypeNode implements TypeNode /** @var TypeNode */ public $returnType; + /** + * @param CallableTypeParameterNode[] $parameters + */ public function __construct(IdentifierTypeNode $identifier, array $parameters, TypeNode $returnType) { $this->identifier = $identifier; diff --git a/src/Ast/Type/GenericTypeNode.php b/src/Ast/Type/GenericTypeNode.php index 179de55a..44e1d16d 100644 --- a/src/Ast/Type/GenericTypeNode.php +++ b/src/Ast/Type/GenericTypeNode.php @@ -25,6 +25,10 @@ class GenericTypeNode implements TypeNode /** @var (self::VARIANCE_*)[] */ public $variances; + /** + * @param TypeNode[] $genericTypes + * @param (self::VARIANCE_*)[] $variances + */ public function __construct(IdentifierTypeNode $type, array $genericTypes, array $variances = []) { $this->type = $type; diff --git a/src/Ast/Type/IntersectionTypeNode.php b/src/Ast/Type/IntersectionTypeNode.php index 7f9aff33..2ca35e53 100644 --- a/src/Ast/Type/IntersectionTypeNode.php +++ b/src/Ast/Type/IntersectionTypeNode.php @@ -13,6 +13,9 @@ class IntersectionTypeNode implements TypeNode /** @var TypeNode[] */ public $types; + /** + * @param TypeNode[] $types + */ public function __construct(array $types) { $this->types = $types; diff --git a/src/Ast/Type/ObjectShapeNode.php b/src/Ast/Type/ObjectShapeNode.php index 1ec2dca4..f418bc30 100644 --- a/src/Ast/Type/ObjectShapeNode.php +++ b/src/Ast/Type/ObjectShapeNode.php @@ -13,6 +13,9 @@ class ObjectShapeNode implements TypeNode /** @var ObjectShapeItemNode[] */ public $items; + /** + * @param ObjectShapeItemNode[] $items + */ public function __construct(array $items) { $this->items = $items; diff --git a/src/Ast/Type/UnionTypeNode.php b/src/Ast/Type/UnionTypeNode.php index 08acf56c..092aec18 100644 --- a/src/Ast/Type/UnionTypeNode.php +++ b/src/Ast/Type/UnionTypeNode.php @@ -13,6 +13,9 @@ class UnionTypeNode implements TypeNode /** @var TypeNode[] */ public $types; + /** + * @param TypeNode[] $types + */ public function __construct(array $types) { $this->types = $types; diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 444b7ecf..7f51e9b8 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -2587,7 +2587,9 @@ public function provideSingleLinePhpDocData(): Iterator ]; } - + /** + * @return array + */ public function provideMultiLinePhpDocData(): array { return [ @@ -5275,6 +5277,9 @@ public function provideDescriptionWithOrWithoutHtml(): Iterator ]; } + /** + * @return array + */ public function dataParseTagValue(): array { return [ @@ -5517,6 +5522,9 @@ public function testNegatedAssertionToString(): void $this->assertSame('@phpstan-assert !Type $param', $assertNode->__toString()); } + /** + * @return array + */ public function dataLinesAndIndexes(): iterable { yield [ diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 2e2c75e8..5b28dc50 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -79,6 +79,9 @@ public function testParse(string $input, $expectedResult, int $nextTokenType = L } + /** + * @return array + */ public function provideParseData(): array { return [ @@ -1905,6 +1908,9 @@ public function provideParseData(): array ]; } + /** + * @return array + */ public function dataLinesAndIndexes(): iterable { yield [ From 874bf2562d15a901aba58c81a1c6299bcfba0adc Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 19 Apr 2023 15:15:34 +0200 Subject: [PATCH 38/59] Increase PHPStan level --- Makefile | 4 ++++ phpstan-baseline.neon | 11 +++++++++++ phpstan.neon | 5 ++++- src/Parser/StringUnescaper.php | 6 +++--- tests/PHPStan/Parser/FuzzyTest.php | 16 +++++++++++++--- 5 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 phpstan-baseline.neon diff --git a/Makefile b/Makefile index 40e75187..bc3de3e4 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,10 @@ cs-fix: phpstan: php vendor/bin/phpstan +.PHONY: phpstan-generate-baseline +phpstan-generate-baseline: + php vendor/bin/phpstan --generate-baseline + .PHONY: build-abnfgen build-abnfgen: ./build-abnfgen.sh diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 00000000..2ef06c82 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,11 @@ +parameters: + ignoreErrors: + - + message: "#^Method PHPStan\\\\PhpDocParser\\\\Ast\\\\ConstExpr\\\\QuoteAwareConstExprStringNode\\:\\:escapeDoubleQuotedString\\(\\) should return string but returns string\\|null\\.$#" + count: 1 + path: src/Ast/ConstExpr/QuoteAwareConstExprStringNode.php + + - + message: "#^Method PHPStan\\\\PhpDocParser\\\\Parser\\\\StringUnescaper\\:\\:parseEscapeSequences\\(\\) should return string but returns string\\|null\\.$#" + count: 1 + path: src/Parser/StringUnescaper.php diff --git a/phpstan.neon b/phpstan.neon index 550caee1..645cb1d1 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,7 +1,10 @@ +includes: + - phpstan-baseline.neon + parameters: paths: - src - tests - level: 5 + level: 8 ignoreErrors: - '#^Dynamic call to static method PHPUnit\\Framework\\Assert#' diff --git a/src/Parser/StringUnescaper.php b/src/Parser/StringUnescaper.php index 93186ce3..70524055 100644 --- a/src/Parser/StringUnescaper.php +++ b/src/Parser/StringUnescaper.php @@ -53,13 +53,13 @@ static function ($matches) { return self::REPLACEMENTS[$str]; } if ($str[0] === 'x' || $str[0] === 'X') { - return chr(hexdec(substr($str, 1))); + return chr((int) hexdec(substr($str, 1))); } if ($str[0] === 'u') { - return self::codePointToUtf8(hexdec($matches[2])); + return self::codePointToUtf8((int) hexdec($matches[2])); } - return chr(octdec($str)); + return chr((int) octdec($str)); }, $str ); diff --git a/tests/PHPStan/Parser/FuzzyTest.php b/tests/PHPStan/Parser/FuzzyTest.php index d2e7a459..e0deaab6 100644 --- a/tests/PHPStan/Parser/FuzzyTest.php +++ b/tests/PHPStan/Parser/FuzzyTest.php @@ -78,8 +78,12 @@ private function provideFuzzyInputsData(string $startSymbol): Iterator $inputsDirectory = sprintf('%s/fuzzy/%s', __DIR__ . '/../../../temp', $startSymbol); if (is_dir($inputsDirectory)) { - foreach (glob(sprintf('%s/*.tst', $inputsDirectory)) as $file) { - unlink($file); + $glob = glob(sprintf('%s/*.tst', $inputsDirectory)); + + if ($glob !== false) { + foreach ($glob as $file) { + unlink($file); + } } } else { @@ -100,7 +104,13 @@ private function provideFuzzyInputsData(string $startSymbol): Iterator $process->mustRun(); - foreach (glob(sprintf('%s/*.tst', $inputsDirectory)) as $file) { + $glob = glob(sprintf('%s/*.tst', $inputsDirectory)); + + if ($glob === false) { + return; + } + + foreach ($glob as $file) { $input = file_get_contents($file); yield [$input]; } From 00d0fcd9e691ebfde865ec6a76d4c062ba614713 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 20 Apr 2023 10:21:53 +0200 Subject: [PATCH 39/59] NodeTraverser inspired by nikic/php-parser --- phpcs.xml | 1 + phpstan-baseline.neon | 15 + src/Ast/AbstractNodeVisitor.php | 34 ++ src/Ast/NodeTraverser.php | 312 +++++++++++++++ src/Ast/NodeVisitor.php | 87 +++++ tests/PHPStan/Ast/NodeTraverserTest.php | 411 ++++++++++++++++++++ tests/PHPStan/Ast/NodeVisitorForTesting.php | 79 ++++ 7 files changed, 939 insertions(+) create mode 100644 src/Ast/AbstractNodeVisitor.php create mode 100644 src/Ast/NodeTraverser.php create mode 100644 src/Ast/NodeVisitor.php create mode 100644 tests/PHPStan/Ast/NodeTraverserTest.php create mode 100644 tests/PHPStan/Ast/NodeVisitorForTesting.php diff --git a/phpcs.xml b/phpcs.xml index ad529969..fe2ed9ed 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -15,6 +15,7 @@ + diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 2ef06c82..1dc71f89 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -5,6 +5,21 @@ parameters: count: 1 path: src/Ast/ConstExpr/QuoteAwareConstExprStringNode.php + - + message: "#^Cannot use array destructuring on array\\\\|int\\|string\\>\\|null\\.$#" + count: 1 + path: src/Ast/NodeTraverser.php + + - + message: "#^Strict comparison using \\=\\=\\= between 2 and 2 will always evaluate to true\\.$#" + count: 2 + path: src/Ast/NodeTraverser.php + + - + message: "#^Variable property access on PHPStan\\\\PhpDocParser\\\\Ast\\\\Node\\.$#" + count: 1 + path: src/Ast/NodeTraverser.php + - message: "#^Method PHPStan\\\\PhpDocParser\\\\Parser\\\\StringUnescaper\\:\\:parseEscapeSequences\\(\\) should return string but returns string\\|null\\.$#" count: 1 diff --git a/src/Ast/AbstractNodeVisitor.php b/src/Ast/AbstractNodeVisitor.php new file mode 100644 index 00000000..32d1a04a --- /dev/null +++ b/src/Ast/AbstractNodeVisitor.php @@ -0,0 +1,34 @@ + Visitors */ + private $visitors = []; + + /** @var bool Whether traversal should be stopped */ + private $stopTraversal; + + /** + * @param list $visitors + */ + public function __construct(array $visitors) + { + $this->visitors = $visitors; + } + + /** + * Traverses an array of nodes using the registered visitors. + * + * @param Node[] $nodes Array of nodes + * + * @return Node[] Traversed array of nodes + */ + public function traverse(array $nodes): array + { + $this->stopTraversal = false; + + foreach ($this->visitors as $visitor) { + $return = $visitor->beforeTraverse($nodes); + if ($return === null) { + continue; + } + + $nodes = $return; + } + + $nodes = $this->traverseArray($nodes); + + foreach ($this->visitors as $visitor) { + $return = $visitor->afterTraverse($nodes); + if ($return === null) { + continue; + } + + $nodes = $return; + } + + return $nodes; + } + + /** + * Recursively traverse a node. + * + * @param Node $node Node to traverse. + * + * @return Node Result of traversal (may be original node or new one) + */ + private function traverseNode(Node $node): Node + { + $subNodeNames = array_keys(get_object_vars($node)); + foreach ($subNodeNames as $name) { + $subNode =& $node->$name; + + if (is_array($subNode)) { + $subNode = $this->traverseArray($subNode); + if ($this->stopTraversal) { + break; + } + } elseif ($subNode instanceof Node) { + $traverseChildren = true; + $breakVisitorIndex = null; + + foreach ($this->visitors as $visitorIndex => $visitor) { + $return = $visitor->enterNode($subNode); + if ($return === null) { + continue; + } + + if ($return instanceof Node) { + $this->ensureReplacementReasonable($subNode, $return); + $subNode = $return; + } elseif ($return === self::DONT_TRAVERSE_CHILDREN) { + $traverseChildren = false; + } elseif ($return === self::DONT_TRAVERSE_CURRENT_AND_CHILDREN) { + $traverseChildren = false; + $breakVisitorIndex = $visitorIndex; + break; + } elseif ($return === self::STOP_TRAVERSAL) { + $this->stopTraversal = true; + break 2; + } else { + throw new LogicException( + 'enterNode() returned invalid value of type ' . gettype($return) + ); + } + } + + if ($traverseChildren) { + $subNode = $this->traverseNode($subNode); + if ($this->stopTraversal) { + break; + } + } + + foreach ($this->visitors as $visitorIndex => $visitor) { + $return = $visitor->leaveNode($subNode); + + if ($return !== null) { + if ($return instanceof Node) { + $this->ensureReplacementReasonable($subNode, $return); + $subNode = $return; + } elseif ($return === self::STOP_TRAVERSAL) { + $this->stopTraversal = true; + break 2; + } elseif (is_array($return)) { + throw new LogicException( + 'leaveNode() may only return an array ' . + 'if the parent structure is an array' + ); + } else { + throw new LogicException( + 'leaveNode() returned invalid value of type ' . gettype($return) + ); + } + } + + if ($breakVisitorIndex === $visitorIndex) { + break; + } + } + } + } + + return $node; + } + + /** + * Recursively traverse array (usually of nodes). + * + * @param mixed[] $nodes Array to traverse + * + * @return mixed[] Result of traversal (may be original array or changed one) + */ + private function traverseArray(array $nodes): array + { + $doNodes = []; + + foreach ($nodes as $i => &$node) { + if ($node instanceof Node) { + $traverseChildren = true; + $breakVisitorIndex = null; + + foreach ($this->visitors as $visitorIndex => $visitor) { + $return = $visitor->enterNode($node); + if ($return === null) { + continue; + } + + if ($return instanceof Node) { + $this->ensureReplacementReasonable($node, $return); + $node = $return; + } elseif (is_array($return)) { + $doNodes[] = [$i, $return]; + continue 2; + } elseif ($return === self::REMOVE_NODE) { + $doNodes[] = [$i, []]; + continue 2; + } elseif ($return === self::DONT_TRAVERSE_CHILDREN) { + $traverseChildren = false; + } elseif ($return === self::DONT_TRAVERSE_CURRENT_AND_CHILDREN) { + $traverseChildren = false; + $breakVisitorIndex = $visitorIndex; + break; + } elseif ($return === self::STOP_TRAVERSAL) { + $this->stopTraversal = true; + break 2; + } else { + throw new LogicException( + 'enterNode() returned invalid value of type ' . gettype($return) + ); + } + } + + if ($traverseChildren) { + $node = $this->traverseNode($node); + if ($this->stopTraversal) { + break; + } + } + + foreach ($this->visitors as $visitorIndex => $visitor) { + $return = $visitor->leaveNode($node); + + if ($return !== null) { + if ($return instanceof Node) { + $this->ensureReplacementReasonable($node, $return); + $node = $return; + } elseif (is_array($return)) { + $doNodes[] = [$i, $return]; + break; + } elseif ($return === self::REMOVE_NODE) { + $doNodes[] = [$i, []]; + break; + } elseif ($return === self::STOP_TRAVERSAL) { + $this->stopTraversal = true; + break 2; + } else { + throw new LogicException( + 'leaveNode() returned invalid value of type ' . gettype($return) + ); + } + } + + if ($breakVisitorIndex === $visitorIndex) { + break; + } + } + } elseif (is_array($node)) { + throw new LogicException('Invalid node structure: Contains nested arrays'); + } + } + + if (count($doNodes) > 0) { + while ([$i, $replace] = array_pop($doNodes)) { + array_splice($nodes, $i, 1, $replace); + } + } + + return $nodes; + } + + private function ensureReplacementReasonable(Node $old, Node $new): void + { + if ($old instanceof TypeNode && !$new instanceof TypeNode) { + throw new LogicException(sprintf('Trying to replace TypeNode with %s', get_class($new))); + } + + if ($old instanceof ConstExprNode && !$new instanceof ConstExprNode) { + throw new LogicException(sprintf('Trying to replace ConstExprNode with %s', get_class($new))); + } + + if ($old instanceof PhpDocChildNode && !$new instanceof PhpDocChildNode) { + throw new LogicException(sprintf('Trying to replace PhpDocChildNode with %s', get_class($new))); + } + + if ($old instanceof PhpDocTagValueNode && !$new instanceof PhpDocTagValueNode) { + throw new LogicException(sprintf('Trying to replace PhpDocTagValueNode with %s', get_class($new))); + } + } + +} diff --git a/src/Ast/NodeVisitor.php b/src/Ast/NodeVisitor.php new file mode 100644 index 00000000..bf7d784e --- /dev/null +++ b/src/Ast/NodeVisitor.php @@ -0,0 +1,87 @@ + $node stays as-is + * * array (of Nodes) + * => The return value is merged into the parent array (at the position of the $node) + * * NodeTraverser::REMOVE_NODE + * => $node is removed from the parent array + * * NodeTraverser::DONT_TRAVERSE_CHILDREN + * => Children of $node are not traversed. $node stays as-is + * * NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN + * => Further visitors for the current node are skipped, and its children are not + * traversed. $node stays as-is. + * * NodeTraverser::STOP_TRAVERSAL + * => Traversal is aborted. $node stays as-is + * * otherwise + * => $node is set to the return value + * + * @param Node $node Node + * + * @return Node|Node[]|NodeTraverser::*|null Replacement node (or special return value) + */ + public function enterNode(Node $node); + + /** + * Called when leaving a node. + * + * Return value semantics: + * * null + * => $node stays as-is + * * NodeTraverser::REMOVE_NODE + * => $node is removed from the parent array + * * NodeTraverser::STOP_TRAVERSAL + * => Traversal is aborted. $node stays as-is + * * array (of Nodes) + * => The return value is merged into the parent array (at the position of the $node) + * * otherwise + * => $node is set to the return value + * + * @param Node $node Node + * + * @return Node|Node[]|NodeTraverser::REMOVE_NODE|NodeTraverser::STOP_TRAVERSAL|null Replacement node (or special return value) + */ + public function leaveNode(Node $node); + + /** + * Called once after traversal. + * + * Return value semantics: + * * null: $nodes stays as-is + * * otherwise: $nodes is set to the return value + * + * @param Node[] $nodes Array of nodes + * + * @return Node[]|null Array of nodes + */ + public function afterTraverse(array $nodes): ?array; + +} diff --git a/tests/PHPStan/Ast/NodeTraverserTest.php b/tests/PHPStan/Ast/NodeTraverserTest.php new file mode 100644 index 00000000..74089ed2 --- /dev/null +++ b/tests/PHPStan/Ast/NodeTraverserTest.php @@ -0,0 +1,411 @@ +assertEquals($nodes, $traverser->traverse($nodes)); + $this->assertEquals([ + ['beforeTraverse', $nodes], + ['enterNode', $echoNode], + ['enterNode', $str1Node], + ['leaveNode', $str1Node], + ['enterNode', $str2Node], + ['leaveNode', $str2Node], + ['leaveNode', $echoNode], + ['afterTraverse', $nodes], + ], $visitor->trace); + } + + public function testModifying(): void + { + $str1Node = new IdentifierTypeNode('Foo'); + $str2Node = new IdentifierTypeNode('Bar'); + $printNode = new NullableTypeNode($str1Node); + + // first visitor changes the node, second verifies the change + $visitor1 = new NodeVisitorForTesting([ + ['beforeTraverse', [], [$str1Node]], + ['enterNode', $str1Node, $printNode], + ['enterNode', $str1Node, $str2Node], + ['leaveNode', $str2Node, $str1Node], + ['leaveNode', $printNode, $str1Node], + ['afterTraverse', [$str1Node], []], + ]); + $visitor2 = new NodeVisitorForTesting(); + + $traverser = new NodeTraverser([$visitor1, $visitor2]); + + // as all operations are reversed we end where we start + $this->assertEquals([], $traverser->traverse([])); + $this->assertEquals([ + ['beforeTraverse', [$str1Node]], + ['enterNode', $printNode], + ['enterNode', $str2Node], + ['leaveNode', $str1Node], + ['leaveNode', $str1Node], + ['afterTraverse', []], + ], $visitor2->trace); + } + + public function testRemoveFromLeave(): void + { + $str1Node = new IdentifierTypeNode('Foo'); + $str2Node = new IdentifierTypeNode('Bar'); + + $visitor = new NodeVisitorForTesting([ + ['leaveNode', $str1Node, NodeTraverser::REMOVE_NODE], + ]); + $visitor2 = new NodeVisitorForTesting(); + + $traverser = new NodeTraverser([$visitor, $visitor2]); + + $nodes = [$str1Node, $str2Node]; + $this->assertEquals([$str2Node], $traverser->traverse($nodes)); + $this->assertEquals([ + ['beforeTraverse', $nodes], + ['enterNode', $str1Node], + ['enterNode', $str2Node], + ['leaveNode', $str2Node], + ['afterTraverse', [$str2Node]], + ], $visitor2->trace); + } + + public function testRemoveFromEnter(): void + { + $str1Node = new IdentifierTypeNode('Foo'); + $str2Node = new IdentifierTypeNode('Bar'); + + $visitor = new NodeVisitorForTesting([ + ['enterNode', $str1Node, NodeTraverser::REMOVE_NODE], + ]); + $visitor2 = new NodeVisitorForTesting(); + + $traverser = new NodeTraverser([$visitor, $visitor2]); + + $nodes = [$str1Node, $str2Node]; + $this->assertEquals([$str2Node], $traverser->traverse($nodes)); + $this->assertEquals([ + ['beforeTraverse', $nodes], + ['enterNode', $str2Node], + ['leaveNode', $str2Node], + ['afterTraverse', [$str2Node]], + ], $visitor2->trace); + } + + public function testReturnArrayFromEnter(): void + { + $str1Node = new IdentifierTypeNode('Str1'); + $str2Node = new IdentifierTypeNode('Str2'); + $str3Node = new IdentifierTypeNode('Str3'); + $str4Node = new IdentifierTypeNode('Str4'); + + $visitor = new NodeVisitorForTesting([ + ['enterNode', $str1Node, [$str3Node, $str4Node]], + ]); + $visitor2 = new NodeVisitorForTesting(); + + $traverser = new NodeTraverser([$visitor, $visitor2]); + + $nodes = [$str1Node, $str2Node]; + $this->assertEquals([$str3Node, $str4Node, $str2Node], $traverser->traverse($nodes)); + $this->assertEquals([ + ['beforeTraverse', $nodes], + ['enterNode', $str2Node], + ['leaveNode', $str2Node], + ['afterTraverse', [$str3Node, $str4Node, $str2Node]], + ], $visitor2->trace); + } + + public function testMerge(): void + { + $strStart = new IdentifierTypeNode('Start'); + $strMiddle = new IdentifierTypeNode('End'); + $strEnd = new IdentifierTypeNode('Middle'); + $strR1 = new IdentifierTypeNode('Replacement 1'); + $strR2 = new IdentifierTypeNode('Replacement 2'); + + $visitor = new NodeVisitorForTesting([ + ['leaveNode', $strMiddle, [$strR1, $strR2]], + ]); + + $traverser = new NodeTraverser([$visitor]); + + $this->assertEquals( + [$strStart, $strR1, $strR2, $strEnd], + $traverser->traverse([$strStart, $strMiddle, $strEnd]) + ); + } + + public function testInvalidDeepArray(): void + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Invalid node structure: Contains nested arrays'); + $strNode = new IdentifierTypeNode('Foo'); + $nodes = [[[$strNode]]]; + + $traverser = new NodeTraverser([]); + + // @phpstan-ignore-next-line + $this->assertEquals($nodes, $traverser->traverse($nodes)); + } + + public function testDontTraverseChildren(): void + { + $strNode = new IdentifierTypeNode('str'); + $printNode = new NullableTypeNode($strNode); + $varNode = new ThisTypeNode(); + $mulNode = new UnionTypeNode([$varNode, $varNode]); + $negNode = new NullableTypeNode($mulNode); + $nodes = [$printNode, $negNode]; + + $visitor1 = new NodeVisitorForTesting([ + ['enterNode', $printNode, NodeTraverser::DONT_TRAVERSE_CHILDREN], + ]); + $visitor2 = new NodeVisitorForTesting([ + ['enterNode', $mulNode, NodeTraverser::DONT_TRAVERSE_CHILDREN], + ]); + + $expectedTrace = [ + ['beforeTraverse', $nodes], + ['enterNode', $printNode], + ['leaveNode', $printNode], + ['enterNode', $negNode], + ['enterNode', $mulNode], + ['leaveNode', $mulNode], + ['leaveNode', $negNode], + ['afterTraverse', $nodes], + ]; + + $traverser = new NodeTraverser([$visitor1, $visitor2]); + + $this->assertEquals($nodes, $traverser->traverse($nodes)); + $this->assertEquals($expectedTrace, $visitor1->trace); + $this->assertEquals($expectedTrace, $visitor2->trace); + } + + public function testDontTraverseCurrentAndChildren(): void + { + $strNode = new IdentifierTypeNode('str'); + $printNode = new NullableTypeNode($strNode); + $varNode = new IdentifierTypeNode('foo'); + $mulNode = new UnionTypeNode([$varNode, $varNode]); + $divNode = new IntersectionTypeNode([$varNode, $varNode]); + $negNode = new NullableTypeNode($mulNode); + $nodes = [$printNode, $negNode]; + + $visitor1 = new NodeVisitorForTesting([ + ['enterNode', $printNode, NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN], + ['enterNode', $mulNode, NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN], + ['leaveNode', $mulNode, $divNode], + ]); + $visitor2 = new NodeVisitorForTesting(); + + $traverser = new NodeTraverser([$visitor1, $visitor2]); + + $resultNodes = $traverser->traverse($nodes); + $this->assertInstanceOf(NullableTypeNode::class, $resultNodes[1]); + $this->assertInstanceOf(IntersectionTypeNode::class, $resultNodes[1]->type); + + $this->assertEquals([ + ['beforeTraverse', $nodes], + ['enterNode', $printNode], + ['leaveNode', $printNode], + ['enterNode', $negNode], + ['enterNode', $mulNode], + ['leaveNode', $mulNode], + ['leaveNode', $negNode], + ['afterTraverse', $resultNodes], + ], $visitor1->trace); + $this->assertEquals([ + ['beforeTraverse', $nodes], + ['enterNode', $negNode], + ['leaveNode', $negNode], + ['afterTraverse', $resultNodes], + ], $visitor2->trace); + } + + public function testStopTraversal(): void + { + $varNode1 = new IdentifierTypeNode('a'); + $varNode2 = new IdentifierTypeNode('b'); + $varNode3 = new IdentifierTypeNode('c'); + $mulNode = new UnionTypeNode([$varNode1, $varNode2]); + $printNode = new NullableTypeNode($varNode3); + $nodes = [$mulNode, $printNode]; + + // From enterNode() with array parent + $visitor = new NodeVisitorForTesting([ + ['enterNode', $mulNode, NodeTraverser::STOP_TRAVERSAL], + ]); + $traverser = new NodeTraverser([$visitor]); + $this->assertEquals($nodes, $traverser->traverse($nodes)); + $this->assertEquals([ + ['beforeTraverse', $nodes], + ['enterNode', $mulNode], + ['afterTraverse', $nodes], + ], $visitor->trace); + + // From enterNode with Node parent + $visitor = new NodeVisitorForTesting([ + ['enterNode', $varNode1, NodeTraverser::STOP_TRAVERSAL], + ]); + $traverser = new NodeTraverser([$visitor]); + $this->assertEquals($nodes, $traverser->traverse($nodes)); + $this->assertEquals([ + ['beforeTraverse', $nodes], + ['enterNode', $mulNode], + ['enterNode', $varNode1], + ['afterTraverse', $nodes], + ], $visitor->trace); + + // From leaveNode with Node parent + $visitor = new NodeVisitorForTesting([ + ['leaveNode', $varNode1, NodeTraverser::STOP_TRAVERSAL], + ]); + $traverser = new NodeTraverser([$visitor]); + $this->assertEquals($nodes, $traverser->traverse($nodes)); + $this->assertEquals([ + ['beforeTraverse', $nodes], + ['enterNode', $mulNode], + ['enterNode', $varNode1], + ['leaveNode', $varNode1], + ['afterTraverse', $nodes], + ], $visitor->trace); + + // From leaveNode with array parent + $visitor = new NodeVisitorForTesting([ + ['leaveNode', $mulNode, NodeTraverser::STOP_TRAVERSAL], + ]); + $traverser = new NodeTraverser([$visitor]); + $this->assertEquals($nodes, $traverser->traverse($nodes)); + $this->assertEquals([ + ['beforeTraverse', $nodes], + ['enterNode', $mulNode], + ['enterNode', $varNode1], + ['leaveNode', $varNode1], + ['enterNode', $varNode2], + ['leaveNode', $varNode2], + ['leaveNode', $mulNode], + ['afterTraverse', $nodes], + ], $visitor->trace); + + // Check that pending array modifications are still carried out + $visitor = new NodeVisitorForTesting([ + ['leaveNode', $mulNode, NodeTraverser::REMOVE_NODE], + ['enterNode', $printNode, NodeTraverser::STOP_TRAVERSAL], + ]); + $traverser = new NodeTraverser([$visitor]); + $this->assertEquals([$printNode], $traverser->traverse($nodes)); + $this->assertEquals([ + ['beforeTraverse', $nodes], + ['enterNode', $mulNode], + ['enterNode', $varNode1], + ['leaveNode', $varNode1], + ['enterNode', $varNode2], + ['leaveNode', $varNode2], + ['leaveNode', $mulNode], + ['enterNode', $printNode], + ['afterTraverse', [$printNode]], + ], $visitor->trace); + } + + public function testNoCloneNodes(): void + { + $nodes = [new UnionTypeNode([new IdentifierTypeNode('Foo'), new IdentifierTypeNode('Bar')])]; + + $traverser = new NodeTraverser([]); + + $this->assertSame($nodes, $traverser->traverse($nodes)); + } + + /** + * @dataProvider provideTestInvalidReturn + * @param Node[] $nodes + */ + public function testInvalidReturn(array $nodes, NodeVisitor $visitor, string $message): void + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage($message); + + $traverser = new NodeTraverser([$visitor]); + $traverser->traverse($nodes); + } + + /** + * @return list> + */ + public function provideTestInvalidReturn(): array + { + $num = new ConstExprIntegerNode('42'); + $expr = new ConstTypeNode($num); + $nodes = [$expr]; + + $visitor1 = new NodeVisitorForTesting([ + ['enterNode', $expr, 'foobar'], + ]); + $visitor2 = new NodeVisitorForTesting([ + ['enterNode', $num, 'foobar'], + ]); + $visitor3 = new NodeVisitorForTesting([ + ['leaveNode', $num, 'foobar'], + ]); + $visitor4 = new NodeVisitorForTesting([ + ['leaveNode', $expr, 'foobar'], + ]); + $visitor5 = new NodeVisitorForTesting([ + ['leaveNode', $num, [new ConstExprFloatNode('42.0')]], + ]); + $visitor6 = new NodeVisitorForTesting([ + ['leaveNode', $expr, false], + ]); + $visitor7 = new NodeVisitorForTesting([ + ['enterNode', $expr, new ConstExprIntegerNode('42')], + ]); + $visitor8 = new NodeVisitorForTesting([ + ['enterNode', $num, new ReturnTagValueNode(new ConstTypeNode(new ConstExprStringNode('foo')), '')], + ]); + + return [ + [$nodes, $visitor1, 'enterNode() returned invalid value of type string'], + [$nodes, $visitor2, 'enterNode() returned invalid value of type string'], + [$nodes, $visitor3, 'leaveNode() returned invalid value of type string'], + [$nodes, $visitor4, 'leaveNode() returned invalid value of type string'], + [$nodes, $visitor5, 'leaveNode() may only return an array if the parent structure is an array'], + [$nodes, $visitor6, 'leaveNode() returned invalid value of type bool'], + [$nodes, $visitor7, 'Trying to replace TypeNode with PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode'], + [$nodes, $visitor8, 'Trying to replace ConstExprNode with PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode'], + ]; + } + +} diff --git a/tests/PHPStan/Ast/NodeVisitorForTesting.php b/tests/PHPStan/Ast/NodeVisitorForTesting.php new file mode 100644 index 00000000..f3ca0ac0 --- /dev/null +++ b/tests/PHPStan/Ast/NodeVisitorForTesting.php @@ -0,0 +1,79 @@ + */ + public $trace = []; + + /** @var list> */ + private $returns; + + /** @var int */ + private $returnsPos; + + /** + * @param list> $returns + */ + public function __construct(array $returns = []) + { + $this->returns = $returns; + $this->returnsPos = 0; + } + + public function beforeTraverse(array $nodes): ?array + { + return $this->traceEvent('beforeTraverse', $nodes); + } + + public function enterNode(Node $node) + { + return $this->traceEvent('enterNode', $node); + } + + public function leaveNode(Node $node) + { + return $this->traceEvent('leaveNode', $node); + } + + public function afterTraverse(array $nodes): ?array + { + return $this->traceEvent('afterTraverse', $nodes); + } + + /** + * @param Node|Node[] $param + * @return mixed + */ + private function traceEvent(string $method, $param) + { + $this->trace[] = [$method, $param]; + if ($this->returnsPos < count($this->returns)) { + $currentReturn = $this->returns[$this->returnsPos]; + if ($currentReturn[0] === $method && $currentReturn[1] === $param) { + $this->returnsPos++; + return $currentReturn[2]; + } + } + return null; + } + + public function __destruct() + { + if ($this->returnsPos !== count($this->returns)) { + throw new Exception('Expected event did not occur'); + } + } + +} From 10553ab3f0337ff1a71433c3417d7eb2a3eec1fd Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 20 Apr 2023 13:12:21 +0200 Subject: [PATCH 40/59] CloningVisitor --- src/Ast/Attribute.php | 2 ++ src/Ast/NodeVisitor/CloningVisitor.php | 20 ++++++++++++ .../Ast/NodeVisitor/CloningVisitorTest.php | 32 +++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 src/Ast/NodeVisitor/CloningVisitor.php create mode 100644 tests/PHPStan/Ast/NodeVisitor/CloningVisitorTest.php diff --git a/src/Ast/Attribute.php b/src/Ast/Attribute.php index 8beccb79..cd3a0a29 100644 --- a/src/Ast/Attribute.php +++ b/src/Ast/Attribute.php @@ -11,4 +11,6 @@ final class Attribute public const START_INDEX = 'startIndex'; public const END_INDEX = 'endIndex'; + public const ORIGINAL_NODE = 'originalNode'; + } diff --git a/src/Ast/NodeVisitor/CloningVisitor.php b/src/Ast/NodeVisitor/CloningVisitor.php new file mode 100644 index 00000000..7200f3af --- /dev/null +++ b/src/Ast/NodeVisitor/CloningVisitor.php @@ -0,0 +1,20 @@ +setAttribute(Attribute::ORIGINAL_NODE, $originalNode); + + return $node; + } + +} diff --git a/tests/PHPStan/Ast/NodeVisitor/CloningVisitorTest.php b/tests/PHPStan/Ast/NodeVisitor/CloningVisitorTest.php new file mode 100644 index 00000000..e63f3425 --- /dev/null +++ b/tests/PHPStan/Ast/NodeVisitor/CloningVisitorTest.php @@ -0,0 +1,32 @@ +traverse([$node]); + $this->assertCount(1, $newNodes); + $this->assertInstanceOf(NullableTypeNode::class, $newNodes[0]); + $this->assertNotSame($node, $newNodes[0]); + $this->assertSame($node, $newNodes[0]->getAttribute(Attribute::ORIGINAL_NODE)); + + $this->assertInstanceOf(IdentifierTypeNode::class, $newNodes[0]->type); + $this->assertNotSame($identifier, $newNodes[0]->type); + $this->assertSame($identifier, $newNodes[0]->type->getAttribute(Attribute::ORIGINAL_NODE)); + } + +} From 900bd69a93e9bd51a11a0bdc6915c4bd813b25c3 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 22 Apr 2023 10:49:15 +0200 Subject: [PATCH 41/59] Do not include TOKEN_CLOSE_PHPDOC in end index with one-line tags --- src/Parser/PhpDocParser.php | 8 +++ src/Parser/TokenIterator.php | 9 +++ src/Parser/TypeParser.php | 8 +++ tests/PHPStan/Parser/PhpDocParserTest.php | 70 ++++++++++++++++++++++- 4 files changed, 92 insertions(+), 3 deletions(-) diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index 085282c0..bf5b18a3 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -127,6 +127,14 @@ private function enrichWithAttributes(TokenIterator $tokens, Ast\Node $tag, int } if ($this->useIndexAttributes) { + $tokensArray = $tokens->getTokens(); + if ($tokensArray[$endIndex][Lexer::TYPE_OFFSET] === Lexer::TOKEN_CLOSE_PHPDOC) { + $endIndex--; + if ($tokensArray[$endIndex][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) { + $endIndex--; + } + } + $tag->setAttribute(Ast\Attribute::START_INDEX, $startIndex); $tag->setAttribute(Ast\Attribute::END_INDEX, $endIndex); } diff --git a/src/Parser/TokenIterator.php b/src/Parser/TokenIterator.php index f01200ac..bb197e6d 100644 --- a/src/Parser/TokenIterator.php +++ b/src/Parser/TokenIterator.php @@ -37,6 +37,15 @@ public function __construct(array $tokens, int $index = 0) } + /** + * @return list + */ + public function getTokens(): array + { + return $this->tokens; + } + + public function currentTokenValue(): string { return $this->tokens[$this->index][Lexer::VALUE_OFFSET]; diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 5d005a63..3016406c 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -72,6 +72,14 @@ private function enrichWithAttributes(TokenIterator $tokens, Ast\Type\TypeNode $ } if ($this->useIndexAttributes) { + $tokensArray = $tokens->getTokens(); + if ($tokensArray[$endIndex][Lexer::TYPE_OFFSET] === Lexer::TOKEN_CLOSE_PHPDOC) { + $endIndex--; + if ($tokensArray[$endIndex][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) { + $endIndex--; + } + } + $type->setAttribute(Ast\Attribute::START_INDEX, $startIndex); $type->setAttribute(Ast\Attribute::END_INDEX, $endIndex); } diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 7f51e9b8..7798828d 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -5530,7 +5530,7 @@ public function dataLinesAndIndexes(): iterable yield [ '/** @param Foo $a */', [ - [1, 1, 1, 7], + [1, 1, 1, 5], ], ]; @@ -5573,7 +5573,7 @@ public function dataLinesAndIndexes(): iterable yield [ '/** @param Foo( */', [ - [1, 1, 1, 6], + [1, 1, 1, 4], ], ]; @@ -5587,7 +5587,28 @@ public function dataLinesAndIndexes(): iterable yield [ '/** @param Foo::** $a */', [ - [1, 1, 1, 10], + [1, 1, 1, 8], + ], + ]; + + yield [ + '/** @param Foo::** $a*/', + [ + [1, 1, 1, 8], + ], + ]; + + yield [ + '/** @return Foo */', + [ + [1, 1, 1, 3], + ], + ]; + + yield [ + '/** @return Foo*/', + [ + [1, 1, 1, 3], ], ]; } @@ -5617,4 +5638,47 @@ public function testLinesAndIndexes(string $phpDoc, array $childrenLines): void } } + /** + * @return array + */ + public function dataTypeLinesAndIndexes(): iterable + { + yield [ + '/** @return Foo */', + [1, 1, 3, 3], + ]; + + yield [ + '/** @return Foo*/', + [1, 1, 3, 3], + ]; + } + + /** + * @dataProvider dataTypeLinesAndIndexes + * @param array{int, int, int, int} $lines + */ + public function testTypeLinesAndIndexes(string $phpDoc, array $lines): void + { + $tokens = new TokenIterator($this->lexer->tokenize($phpDoc)); + $constExprParser = new ConstExprParser(true, true); + $usedAttributes = [ + 'lines' => true, + 'indexes' => true, + ]; + $typeParser = new TypeParser($constExprParser, true, $usedAttributes); + $phpDocParser = new PhpDocParser($typeParser, $constExprParser, true, true, $usedAttributes); + $phpDocNode = $phpDocParser->parse($tokens); + $this->assertInstanceOf(PhpDocTagNode::class, $phpDocNode->children[0]); + $this->assertInstanceOf(ReturnTagValueNode::class, $phpDocNode->children[0]->value); + + $type = $phpDocNode->children[0]->value->type; + $this->assertInstanceOf(IdentifierTypeNode::class, $type); + + $this->assertSame($lines[0], $type->getAttribute(Attribute::START_LINE)); + $this->assertSame($lines[1], $type->getAttribute(Attribute::END_LINE)); + $this->assertSame($lines[2], $type->getAttribute(Attribute::START_INDEX)); + $this->assertSame($lines[3], $type->getAttribute(Attribute::END_INDEX)); + } + } From 57f6787f0bb6431905a18aa7caea25dcd2bd59e0 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 22 Apr 2023 10:55:53 +0200 Subject: [PATCH 42/59] Update slevomat/coding-standard --- .github/workflows/test-slevomat-coding-standard.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-slevomat-coding-standard.yml b/.github/workflows/test-slevomat-coding-standard.yml index b7d81438..cc9e1b0b 100644 --- a/.github/workflows/test-slevomat-coding-standard.yml +++ b/.github/workflows/test-slevomat-coding-standard.yml @@ -32,7 +32,7 @@ jobs: with: repository: slevomat/coding-standard path: slevomat-cs - ref: 8.7.1 + ref: 710c256bf3f0f696ec8d4f9d2218321c3eb0f7d2 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -40,6 +40,10 @@ jobs: coverage: "none" php-version: "${{ matrix.php-version }}" + - name: "Unset platform" + working-directory: slevomat-cs + run: "composer config --unset platform" + - name: "Install dependencies" working-directory: slevomat-cs run: "composer install --no-interaction --no-progress" @@ -56,12 +60,14 @@ jobs: - name: "Tests" working-directory: slevomat-cs - run: "bin/phpunit" + run: "bin/phpunit --no-coverage" - name: "PHPStan" + if: matrix.php-version == '8.0' || matrix.php-version == '8.1' || matrix.php-version == '8.2' working-directory: slevomat-cs run: "bin/phpstan analyse -c build/PHPStan/phpstan.neon" - name: "PHPStan in tests" + if: matrix.php-version == '8.0' || matrix.php-version == '8.1' || matrix.php-version == '8.2' working-directory: slevomat-cs run: "bin/phpstan analyse -c build/PHPStan/phpstan.tests.neon" From 08ccb8d27dfe0528e1b05fe9e3502e05babdd1af Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 22 Apr 2023 13:10:05 +0200 Subject: [PATCH 43/59] More precise type indexes --- src/Parser/TypeParser.php | 6 ++-- tests/PHPStan/Parser/PhpDocParserTest.php | 42 +++++++++++++++++++---- tests/PHPStan/Parser/TypeParserTest.php | 41 +++++++++++++++++++--- 3 files changed, 74 insertions(+), 15 deletions(-) diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 3016406c..f84f769b 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -73,11 +73,9 @@ private function enrichWithAttributes(TokenIterator $tokens, Ast\Type\TypeNode $ if ($this->useIndexAttributes) { $tokensArray = $tokens->getTokens(); - if ($tokensArray[$endIndex][Lexer::TYPE_OFFSET] === Lexer::TOKEN_CLOSE_PHPDOC) { + $endIndex--; + if ($tokensArray[$endIndex][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) { $endIndex--; - if ($tokensArray[$endIndex][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) { - $endIndex--; - } } $type->setAttribute(Ast\Attribute::START_INDEX, $startIndex); diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 7798828d..94bc7359 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -5641,7 +5641,7 @@ public function testLinesAndIndexes(string $phpDoc, array $childrenLines): void /** * @return array */ - public function dataTypeLinesAndIndexes(): iterable + public function dataReturnTypeLinesAndIndexes(): iterable { yield [ '/** @return Foo */', @@ -5652,13 +5652,43 @@ public function dataTypeLinesAndIndexes(): iterable '/** @return Foo*/', [1, 1, 3, 3], ]; + + yield [ + '/** + * @param Foo $foo + * @return Foo + */', + [3, 3, 10, 10], + ]; + + yield [ + '/** + * @return Foo + * @param Foo $foo + */', + [2, 2, 4, 4], + ]; + + yield [ + '/** + * @param Foo $foo + * @return Foo */', + [3, 3, 10, 10], + ]; + + yield [ + '/** + * @param Foo $foo + * @return Foo*/', + [3, 3, 10, 10], + ]; } /** - * @dataProvider dataTypeLinesAndIndexes + * @dataProvider dataReturnTypeLinesAndIndexes * @param array{int, int, int, int} $lines */ - public function testTypeLinesAndIndexes(string $phpDoc, array $lines): void + public function testReturnTypeLinesAndIndexes(string $phpDoc, array $lines): void { $tokens = new TokenIterator($this->lexer->tokenize($phpDoc)); $constExprParser = new ConstExprParser(true, true); @@ -5669,10 +5699,8 @@ public function testTypeLinesAndIndexes(string $phpDoc, array $lines): void $typeParser = new TypeParser($constExprParser, true, $usedAttributes); $phpDocParser = new PhpDocParser($typeParser, $constExprParser, true, true, $usedAttributes); $phpDocNode = $phpDocParser->parse($tokens); - $this->assertInstanceOf(PhpDocTagNode::class, $phpDocNode->children[0]); - $this->assertInstanceOf(ReturnTagValueNode::class, $phpDocNode->children[0]->value); - - $type = $phpDocNode->children[0]->value->type; + $returnTag = $phpDocNode->getReturnTagValues()[0]; + $type = $returnTag->type; $this->assertInstanceOf(IdentifierTypeNode::class, $type); $this->assertSame($lines[0], $type->getAttribute(Attribute::START_LINE)); diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 5b28dc50..b680f4d8 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -1923,7 +1923,7 @@ static function (TypeNode $typeNode): TypeNode { 1, 1, 0, - 13, + 12, ], [ static function (UnionTypeNode $typeNode): TypeNode { @@ -1932,7 +1932,7 @@ static function (UnionTypeNode $typeNode): TypeNode { 1, 1, 0, - 2, + 0, ], [ static function (UnionTypeNode $typeNode): TypeNode { @@ -1941,7 +1941,40 @@ static function (UnionTypeNode $typeNode): TypeNode { 1, 1, 4, - 13, + 12, + ], + ], + ]; + + yield [ + 'int | object{foo: int}[] ', + [ + [ + static function (TypeNode $typeNode): TypeNode { + return $typeNode; + }, + 1, + 1, + 0, + 12, + ], + [ + static function (UnionTypeNode $typeNode): TypeNode { + return $typeNode->types[0]; + }, + 1, + 1, + 0, + 0, + ], + [ + static function (UnionTypeNode $typeNode): TypeNode { + return $typeNode->types[1]; + }, + 1, + 1, + 4, + 12, ], ], ]; @@ -1959,7 +1992,7 @@ static function (TypeNode $typeNode): TypeNode { 1, 4, 0, - 15, + 14, ], ], ]; From 90490bd8fd8530a272043c4950c180b6d0cf5f81 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 22 Apr 2023 14:54:28 +0200 Subject: [PATCH 44/59] TypeParserTest - verify indexes by concatenating token values --- tests/PHPStan/Parser/TypeParserTest.php | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index b680f4d8..81cf75ae 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -1920,6 +1920,7 @@ public function dataLinesAndIndexes(): iterable static function (TypeNode $typeNode): TypeNode { return $typeNode; }, + 'int | object{foo: int}[]', 1, 1, 0, @@ -1929,6 +1930,7 @@ static function (TypeNode $typeNode): TypeNode { static function (UnionTypeNode $typeNode): TypeNode { return $typeNode->types[0]; }, + 'int', 1, 1, 0, @@ -1938,6 +1940,7 @@ static function (UnionTypeNode $typeNode): TypeNode { static function (UnionTypeNode $typeNode): TypeNode { return $typeNode->types[1]; }, + 'object{foo: int}[]', 1, 1, 4, @@ -1953,6 +1956,7 @@ static function (UnionTypeNode $typeNode): TypeNode { static function (TypeNode $typeNode): TypeNode { return $typeNode; }, + 'int | object{foo: int}[]', 1, 1, 0, @@ -1962,6 +1966,7 @@ static function (TypeNode $typeNode): TypeNode { static function (UnionTypeNode $typeNode): TypeNode { return $typeNode->types[0]; }, + 'int', 1, 1, 0, @@ -1971,6 +1976,7 @@ static function (UnionTypeNode $typeNode): TypeNode { static function (UnionTypeNode $typeNode): TypeNode { return $typeNode->types[1]; }, + 'object{foo: int}[]', 1, 1, 4, @@ -1989,6 +1995,10 @@ static function (UnionTypeNode $typeNode): TypeNode { static function (TypeNode $typeNode): TypeNode { return $typeNode; }, + 'array{ + a: int, + b: string + }', 1, 4, 0, @@ -2000,19 +2010,27 @@ static function (TypeNode $typeNode): TypeNode { /** * @dataProvider dataLinesAndIndexes - * @param list $assertions + * @param list $assertions */ public function testLinesAndIndexes(string $input, array $assertions): void { - $tokens = new TokenIterator($this->lexer->tokenize($input)); + $tokensArray = $this->lexer->tokenize($input); + $tokens = new TokenIterator($tokensArray); $typeParser = new TypeParser(new ConstExprParser(true, true), true, [ 'lines' => true, 'indexes' => true, ]); $typeNode = $typeParser->parse($tokens); - foreach ($assertions as [$callable, $startLine, $endLine, $startIndex, $endIndex]) { + foreach ($assertions as [$callable, $expectedContent, $startLine, $endLine, $startIndex, $endIndex]) { $typeToAssert = $callable($typeNode); + + $content = ''; + for ($i = $startIndex; $i <= $endIndex; $i++) { + $content .= $tokensArray[$i][Lexer::VALUE_OFFSET]; + } + + $this->assertSame($expectedContent, $content); $this->assertSame($startLine, $typeToAssert->getAttribute(Attribute::START_LINE)); $this->assertSame($endLine, $typeToAssert->getAttribute(Attribute::END_LINE)); $this->assertSame($startIndex, $typeToAssert->getAttribute(Attribute::START_INDEX)); From 39e496629d46cd7f5d602fd11091655e11a78b4e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 01:13:29 +0000 Subject: [PATCH 45/59] Update dependency slevomat/coding-standard to v8.11.0 --- build-cs/composer.lock | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index 2eafc159..0cea3f9e 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -154,16 +154,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.18.1", + "version": "1.20.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f" + "reference": "90490bd8fd8530a272043c4950c180b6d0cf5f81" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/22dcdfd725ddf99583bfe398fc624ad6c5004a0f", - "reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/90490bd8fd8530a272043c4950c180b6d0cf5f81", + "reference": "90490bd8fd8530a272043c4950c180b6d0cf5f81", "shasum": "" }, "require": { @@ -193,38 +193,38 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.18.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.20.2" }, - "time": "2023-04-07T11:51:11+00:00" + "time": "2023-04-22T12:59:35+00:00" }, { "name": "slevomat/coding-standard", - "version": "8.10.0", + "version": "8.11.0", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "c4e213e6e57f741451a08e68ef838802eec92287" + "reference": "91428d5bcf7db93a842bcf97f465edf62527f3ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/c4e213e6e57f741451a08e68ef838802eec92287", - "reference": "c4e213e6e57f741451a08e68ef838802eec92287", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/91428d5bcf7db93a842bcf97f465edf62527f3ea", + "reference": "91428d5bcf7db93a842bcf97f465edf62527f3ea", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": ">=1.18.0 <1.19.0", + "phpstan/phpdoc-parser": ">=1.20.0 <1.21.0", "squizlabs/php_codesniffer": "^3.7.1" }, "require-dev": { "phing/phing": "2.17.4", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.10|1.10.11", + "phpstan/phpstan": "1.10.14", "phpstan/phpstan-deprecation-rules": "1.1.3", - "phpstan/phpstan-phpunit": "1.0.0|1.3.11", + "phpstan/phpstan-phpunit": "1.3.11", "phpstan/phpstan-strict-rules": "1.5.1", - "phpunit/phpunit": "7.5.20|8.5.21|9.6.6|10.0.19" + "phpunit/phpunit": "7.5.20|8.5.21|9.6.6|10.1.1" }, "type": "phpcodesniffer-standard", "extra": { @@ -248,7 +248,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.10.0" + "source": "https://github.com/slevomat/coding-standard/tree/8.11.0" }, "funding": [ { @@ -260,7 +260,7 @@ "type": "tidelift" } ], - "time": "2023-04-10T07:39:29+00:00" + "time": "2023-04-21T15:51:44+00:00" }, { "name": "squizlabs/php_codesniffer", From ecb7789d6a43b0dbfa6d74d8fb898d2286b73dd7 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 24 Apr 2023 15:16:27 +0200 Subject: [PATCH 46/59] Do not include TOKEN_PHPDOC_EOL in node tokens --- src/Parser/PhpDocParser.php | 2 ++ tests/PHPStan/Parser/PhpDocParserTest.php | 18 +++++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index bf5b18a3..a3d43212 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -133,6 +133,8 @@ private function enrichWithAttributes(TokenIterator $tokens, Ast\Node $tag, int if ($tokensArray[$endIndex][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) { $endIndex--; } + } elseif ($tokensArray[$endIndex][Lexer::TYPE_OFFSET] === Lexer::TOKEN_PHPDOC_EOL) { + $endIndex--; } $tag->setAttribute(Ast\Attribute::START_INDEX, $startIndex); diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 94bc7359..6d089d56 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -5540,8 +5540,8 @@ public function dataLinesAndIndexes(): iterable * @param Bar $bar 2nd multi world description */', [ - [2, 2, 2, 16], - [3, 3, 17, 31], + [2, 2, 2, 15], + [3, 3, 17, 30], ], ]; @@ -5560,13 +5560,13 @@ public function dataLinesAndIndexes(): iterable * )) */', [ - [2, 2, 2, 9], - [3, 3, 10, 13], - [4, 4, 14, 43], - [5, 5, 44, 44], - [6, 6, 45, 50], - [7, 7, 51, 51], - [8, 12, 52, 115], + [2, 2, 2, 8], + [3, 3, 10, 12], + [4, 4, 14, 42], + [5, 5, 44, 43], + [6, 6, 45, 49], + [7, 7, 51, 50], + [8, 12, 52, 114], ], ]; From 3416dc6441a072a6c43b8da91558ad930b79098f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 24 Apr 2023 15:52:41 +0200 Subject: [PATCH 47/59] Callable parameter and return type have attributes --- src/Parser/TypeParser.php | 25 +++++++-- tests/PHPStan/Parser/TypeParserTest.php | 75 +++++++++++++++++++++++-- 2 files changed, 89 insertions(+), 11 deletions(-) diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index f84f769b..3e7f48fd 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -61,7 +61,12 @@ public function parse(TokenIterator $tokens): Ast\Type\TypeNode return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); } - private function enrichWithAttributes(TokenIterator $tokens, Ast\Type\TypeNode $type, int $startLine, int $startIndex): Ast\Type\TypeNode + /** + * @template T of Ast\Node + * @param T $type + * @return T + */ + private function enrichWithAttributes(TokenIterator $tokens, Ast\Node $type, int $startLine, int $startIndex): Ast\Node { $endLine = $tokens->currentTokenLine(); $endIndex = $tokens->currentTokenIndex(); @@ -139,7 +144,7 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode } if ($tokens->tryConsumeTokenType(Lexer::TOKEN_THIS_VARIABLE)) { - $type = new Ast\Type\ThisTypeNode(); + $type = $this->enrichWithAttributes($tokens, new Ast\Type\ThisTypeNode(), $startLine, $startIndex); if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); @@ -151,7 +156,7 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode $currentTokenValue = $tokens->currentTokenValue(); $tokens->pushSavePoint(); // because of ConstFetchNode if ($tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) { - $type = new Ast\Type\IdentifierTypeNode($currentTokenValue); + $type = $this->enrichWithAttributes($tokens, new Ast\Type\IdentifierTypeNode($currentTokenValue), $startLine, $startIndex); if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) { $tokens->dropSavePoint(); // because of ConstFetchNode @@ -454,7 +459,10 @@ private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNod $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES); $tokens->consumeTokenType(Lexer::TOKEN_COLON); - $returnType = $this->parseCallableReturnType($tokens); + + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); + $returnType = $this->enrichWithAttributes($tokens, $this->parseCallableReturnType($tokens), $startLine, $startIndex); return new Ast\Type\CallableTypeNode($identifier, $parameters, $returnType); } @@ -463,6 +471,8 @@ private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNod /** @phpstan-impure */ private function parseCallableParameter(TokenIterator $tokens): Ast\Type\CallableTypeParameterNode { + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); $type = $this->parse($tokens); $isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE); $isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC); @@ -476,7 +486,12 @@ private function parseCallableParameter(TokenIterator $tokens): Ast\Type\Callabl } $isOptional = $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL); - return new Ast\Type\CallableTypeParameterNode($type, $isReference, $isVariadic, $parameterName, $isOptional); + return $this->enrichWithAttributes( + $tokens, + new Ast\Type\CallableTypeParameterNode($type, $isReference, $isVariadic, $parameterName, $isOptional), + $startLine, + $startIndex + ); } diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 81cf75ae..2b29c4a4 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -8,6 +8,7 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; use PHPStan\PhpDocParser\Ast\ConstExpr\QuoteAwareConstExprStringNode; +use PHPStan\PhpDocParser\Ast\Node; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; @@ -2006,11 +2007,73 @@ static function (TypeNode $typeNode): TypeNode { ], ], ]; + + yield [ + 'callable(Foo, Bar): void', + [ + [ + static function (TypeNode $typeNode): TypeNode { + return $typeNode; + }, + 'callable(Foo, Bar): void', + 1, + 1, + 0, + 9, + ], + [ + static function (CallableTypeNode $typeNode): TypeNode { + return $typeNode->identifier; + }, + 'callable', + 1, + 1, + 0, + 0, + ], + [ + static function (CallableTypeNode $typeNode): Node { + return $typeNode->parameters[0]; + }, + 'Foo', + 1, + 1, + 2, + 2, + ], + [ + static function (CallableTypeNode $typeNode): TypeNode { + return $typeNode->returnType; + }, + 'void', + 1, + 1, + 9, + 9, + ], + ], + ]; + + yield [ + '$this', + [ + [ + static function (TypeNode $typeNode): TypeNode { + return $typeNode; + }, + '$this', + 1, + 1, + 0, + 0, + ], + ], + ]; } /** * @dataProvider dataLinesAndIndexes - * @param list $assertions + * @param list $assertions */ public function testLinesAndIndexes(string $input, array $assertions): void { @@ -2025,16 +2088,16 @@ public function testLinesAndIndexes(string $input, array $assertions): void foreach ($assertions as [$callable, $expectedContent, $startLine, $endLine, $startIndex, $endIndex]) { $typeToAssert = $callable($typeNode); + $this->assertSame($startLine, $typeToAssert->getAttribute(Attribute::START_LINE)); + $this->assertSame($endLine, $typeToAssert->getAttribute(Attribute::END_LINE)); + $this->assertSame($startIndex, $typeToAssert->getAttribute(Attribute::START_INDEX)); + $this->assertSame($endIndex, $typeToAssert->getAttribute(Attribute::END_INDEX)); + $content = ''; for ($i = $startIndex; $i <= $endIndex; $i++) { $content .= $tokensArray[$i][Lexer::VALUE_OFFSET]; } - $this->assertSame($expectedContent, $content); - $this->assertSame($startLine, $typeToAssert->getAttribute(Attribute::START_LINE)); - $this->assertSame($endLine, $typeToAssert->getAttribute(Attribute::END_LINE)); - $this->assertSame($startIndex, $typeToAssert->getAttribute(Attribute::START_INDEX)); - $this->assertSame($endIndex, $typeToAssert->getAttribute(Attribute::END_INDEX)); } } From cff97e916b8e3f0b1731046242f6074721e43de7 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 24 Apr 2023 17:06:14 +0200 Subject: [PATCH 48/59] Missing attributes for array shapes --- src/Parser/TypeParser.php | 26 +++++++++++++-- tests/PHPStan/Parser/TypeParserTest.php | 42 +++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 3e7f48fd..127f7c47 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -611,6 +611,8 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, /** @phpstan-impure */ private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShapeItemNode { + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); try { $tokens->pushSavePoint(); $key = $this->parseArrayShapeKey($tokens); @@ -619,12 +621,22 @@ private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShape $value = $this->parse($tokens); $tokens->dropSavePoint(); - return new Ast\Type\ArrayShapeItemNode($key, $optional, $value); + return $this->enrichWithAttributes( + $tokens, + new Ast\Type\ArrayShapeItemNode($key, $optional, $value), + $startLine, + $startIndex + ); } catch (ParserException $e) { $tokens->rollback(); $value = $this->parse($tokens); - return new Ast\Type\ArrayShapeItemNode(null, false, $value); + return $this->enrichWithAttributes( + $tokens, + new Ast\Type\ArrayShapeItemNode(null, false, $value), + $startLine, + $startIndex + ); } } @@ -634,6 +646,9 @@ private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShape */ private function parseArrayShapeKey(TokenIterator $tokens) { + $startIndex = $tokens->currentTokenIndex(); + $startLine = $tokens->currentTokenLine(); + if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) { $key = new Ast\ConstExpr\ConstExprIntegerNode($tokens->currentTokenValue()); $tokens->next(); @@ -660,7 +675,12 @@ private function parseArrayShapeKey(TokenIterator $tokens) $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); } - return $key; + return $this->enrichWithAttributes( + $tokens, + $key, + $startLine, + $startIndex + ); } /** diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 2b29c4a4..663daf38 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -2069,6 +2069,48 @@ static function (TypeNode $typeNode): TypeNode { ], ], ]; + + yield [ + 'array{foo: int}', + [ + [ + static function (TypeNode $typeNode): TypeNode { + return $typeNode; + }, + 'array{foo: int}', + 1, + 1, + 0, + 6, + ], + [ + static function (ArrayShapeNode $typeNode): TypeNode { + return $typeNode->items[0]; + }, + 'foo: int', + 1, + 1, + 2, + 5, + ], + ], + ]; + + yield [ + 'array{}', + [ + [ + static function (TypeNode $typeNode): TypeNode { + return $typeNode; + }, + 'array{}', + 1, + 1, + 0, + 2, + ], + ], + ]; } /** From b5fede32e89162775d8d95dec5ee783c95c5cf98 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 24 Apr 2023 17:08:49 +0200 Subject: [PATCH 49/59] Missing attributes for object shapes --- src/Parser/TypeParser.php | 10 ++++- tests/PHPStan/Parser/TypeParserTest.php | 58 +++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 127f7c47..6507aed2 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -713,12 +713,15 @@ private function parseObjectShape(TokenIterator $tokens): Ast\Type\ObjectShapeNo /** @phpstan-impure */ private function parseObjectShapeItem(TokenIterator $tokens): Ast\Type\ObjectShapeItemNode { + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); + $key = $this->parseObjectShapeKey($tokens); $optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE); $tokens->consumeTokenType(Lexer::TOKEN_COLON); $value = $this->parse($tokens); - return new Ast\Type\ObjectShapeItemNode($key, $optional, $value); + return $this->enrichWithAttributes($tokens, new Ast\Type\ObjectShapeItemNode($key, $optional, $value), $startLine, $startIndex); } /** @@ -727,6 +730,9 @@ private function parseObjectShapeItem(TokenIterator $tokens): Ast\Type\ObjectSha */ private function parseObjectShapeKey(TokenIterator $tokens) { + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); + if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) { if ($this->quoteAwareConstExprString) { $key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED); @@ -748,7 +754,7 @@ private function parseObjectShapeKey(TokenIterator $tokens) $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); } - return $key; + return $this->enrichWithAttributes($tokens, $key, $startLine, $startIndex); } } diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 663daf38..9c876048 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -2111,6 +2111,64 @@ static function (TypeNode $typeNode): TypeNode { ], ], ]; + + yield [ + 'object{foo: int}', + [ + [ + static function (TypeNode $typeNode): TypeNode { + return $typeNode; + }, + 'object{foo: int}', + 1, + 1, + 0, + 6, + ], + [ + static function (ObjectShapeNode $typeNode): TypeNode { + return $typeNode->items[0]; + }, + 'foo: int', + 1, + 1, + 2, + 5, + ], + ], + ]; + + yield [ + 'object{}', + [ + [ + static function (TypeNode $typeNode): TypeNode { + return $typeNode; + }, + 'object{}', + 1, + 1, + 0, + 2, + ], + ], + ]; + + yield [ + 'object{}[]', + [ + [ + static function (TypeNode $typeNode): TypeNode { + return $typeNode; + }, + 'object{}[]', + 1, + 1, + 0, + 4, + ], + ], + ]; } /** From 6220c55d89daa3a126e2f7a8c537e5a5c1c51f7c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 24 Apr 2023 21:16:34 +0200 Subject: [PATCH 50/59] ConstExprParser - attributes --- src/Parser/ConstExprParser.php | 140 ++++++++++++++++-- tests/PHPStan/Parser/ConstExprParserTest.php | 34 +++++ .../PHPStan/Parser/NodeCollectingVisitor.php | 21 +++ tests/PHPStan/Parser/PhpDocParserTest.php | 4 +- tests/PHPStan/Parser/TypeParserTest.php | 5 +- 5 files changed, 184 insertions(+), 20 deletions(-) create mode 100644 tests/PHPStan/Parser/NodeCollectingVisitor.php diff --git a/src/Parser/ConstExprParser.php b/src/Parser/ConstExprParser.php index c4767ee8..f8cfcb59 100644 --- a/src/Parser/ConstExprParser.php +++ b/src/Parser/ConstExprParser.php @@ -16,24 +16,53 @@ class ConstExprParser /** @var bool */ private $quoteAwareConstExprString; - public function __construct(bool $unescapeStrings = false, bool $quoteAwareConstExprString = false) + /** @var bool */ + private $useLinesAttributes; + + /** @var bool */ + private $useIndexAttributes; + + /** + * @param array{lines?: bool, indexes?: bool} $usedAttributes + */ + public function __construct( + bool $unescapeStrings = false, + bool $quoteAwareConstExprString = false, + array $usedAttributes = [] + ) { $this->unescapeStrings = $unescapeStrings; $this->quoteAwareConstExprString = $quoteAwareConstExprString; + $this->useLinesAttributes = $usedAttributes['lines'] ?? false; + $this->useIndexAttributes = $usedAttributes['indexes'] ?? false; } public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\ConstExpr\ConstExprNode { + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); if ($tokens->isCurrentTokenType(Lexer::TOKEN_FLOAT)) { $value = $tokens->currentTokenValue(); $tokens->next(); - return new Ast\ConstExpr\ConstExprFloatNode($value); + + return $this->enrichWithAttributes( + $tokens, + new Ast\ConstExpr\ConstExprFloatNode($value), + $startLine, + $startIndex + ); } if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) { $value = $tokens->currentTokenValue(); $tokens->next(); - return new Ast\ConstExpr\ConstExprIntegerNode($value); + + return $this->enrichWithAttributes( + $tokens, + new Ast\ConstExpr\ConstExprIntegerNode($value), + $startLine, + $startIndex + ); } if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING, Lexer::TOKEN_DOUBLE_QUOTED_STRING)) { @@ -49,15 +78,25 @@ public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\Con $tokens->next(); if ($this->quoteAwareConstExprString) { - return new Ast\ConstExpr\QuoteAwareConstExprStringNode( - $value, - $type === Lexer::TOKEN_SINGLE_QUOTED_STRING - ? Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED - : Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED + return $this->enrichWithAttributes( + $tokens, + new Ast\ConstExpr\QuoteAwareConstExprStringNode( + $value, + $type === Lexer::TOKEN_SINGLE_QUOTED_STRING + ? Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED + : Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED + ), + $startLine, + $startIndex ); } - return new Ast\ConstExpr\ConstExprStringNode($value); + return $this->enrichWithAttributes( + $tokens, + new Ast\ConstExpr\ConstExprStringNode($value), + $startLine, + $startIndex + ); } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) { $identifier = $tokens->currentTokenValue(); @@ -65,11 +104,26 @@ public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\Con switch (strtolower($identifier)) { case 'true': - return new Ast\ConstExpr\ConstExprTrueNode(); + return $this->enrichWithAttributes( + $tokens, + new Ast\ConstExpr\ConstExprTrueNode(), + $startLine, + $startIndex + ); case 'false': - return new Ast\ConstExpr\ConstExprFalseNode(); + return $this->enrichWithAttributes( + $tokens, + new Ast\ConstExpr\ConstExprFalseNode(), + $startLine, + $startIndex + ); case 'null': - return new Ast\ConstExpr\ConstExprNullNode(); + return $this->enrichWithAttributes( + $tokens, + new Ast\ConstExpr\ConstExprNullNode(), + $startLine, + $startIndex + ); case 'array': $tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES); return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_PARENTHESES); @@ -106,11 +160,21 @@ public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\Con break; } - return new Ast\ConstExpr\ConstFetchNode($identifier, $classConstantName); + return $this->enrichWithAttributes( + $tokens, + new Ast\ConstExpr\ConstFetchNode($identifier, $classConstantName), + $startLine, + $startIndex + ); } - return new Ast\ConstExpr\ConstFetchNode('', $identifier); + return $this->enrichWithAttributes( + $tokens, + new Ast\ConstExpr\ConstFetchNode('', $identifier), + $startLine, + $startIndex + ); } elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_SQUARE_BRACKET); @@ -131,6 +195,9 @@ private function parseArray(TokenIterator $tokens, int $endToken): Ast\ConstExpr { $items = []; + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); + if (!$tokens->tryConsumeTokenType($endToken)) { do { $items[] = $this->parseArrayItem($tokens); @@ -138,12 +205,20 @@ private function parseArray(TokenIterator $tokens, int $endToken): Ast\ConstExpr $tokens->consumeTokenType($endToken); } - return new Ast\ConstExpr\ConstExprArrayNode($items); + return $this->enrichWithAttributes( + $tokens, + new Ast\ConstExpr\ConstExprArrayNode($items), + $startLine, + $startIndex + ); } private function parseArrayItem(TokenIterator $tokens): Ast\ConstExpr\ConstExprArrayItemNode { + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); + $expr = $this->parse($tokens); if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_ARROW)) { @@ -155,7 +230,40 @@ private function parseArrayItem(TokenIterator $tokens): Ast\ConstExpr\ConstExprA $value = $expr; } - return new Ast\ConstExpr\ConstExprArrayItemNode($key, $value); + return $this->enrichWithAttributes( + $tokens, + new Ast\ConstExpr\ConstExprArrayItemNode($key, $value), + $startLine, + $startIndex + ); + } + + /** + * @template T of Ast\ConstExpr\ConstExprNode + * @param T $node + * @return T + */ + private function enrichWithAttributes(TokenIterator $tokens, Ast\ConstExpr\ConstExprNode $node, int $startLine, int $startIndex): Ast\ConstExpr\ConstExprNode + { + $endLine = $tokens->currentTokenLine(); + $endIndex = $tokens->currentTokenIndex(); + if ($this->useLinesAttributes) { + $node->setAttribute(Ast\Attribute::START_LINE, $startLine); + $node->setAttribute(Ast\Attribute::END_LINE, $endLine); + } + + if ($this->useIndexAttributes) { + $tokensArray = $tokens->getTokens(); + $endIndex--; + if ($tokensArray[$endIndex][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) { + $endIndex--; + } + + $node->setAttribute(Ast\Attribute::START_INDEX, $startIndex); + $node->setAttribute(Ast\Attribute::END_INDEX, $endIndex); + } + + return $node; } } diff --git a/tests/PHPStan/Parser/ConstExprParserTest.php b/tests/PHPStan/Parser/ConstExprParserTest.php index 7483a800..1fac87d9 100644 --- a/tests/PHPStan/Parser/ConstExprParserTest.php +++ b/tests/PHPStan/Parser/ConstExprParserTest.php @@ -3,6 +3,7 @@ namespace PHPStan\PhpDocParser\Parser; use Iterator; +use PHPStan\PhpDocParser\Ast\Attribute; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayItemNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode; @@ -13,6 +14,7 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprTrueNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; +use PHPStan\PhpDocParser\Ast\NodeTraverser; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPUnit\Framework\TestCase; @@ -54,6 +56,38 @@ public function testParse(string $input, ConstExprNode $expectedExpr, int $nextT } + /** + * @dataProvider provideTrueNodeParseData + * @dataProvider provideFalseNodeParseData + * @dataProvider provideNullNodeParseData + * @dataProvider provideIntegerNodeParseData + * @dataProvider provideFloatNodeParseData + * @dataProvider provideStringNodeParseData + * @dataProvider provideArrayNodeParseData + * @dataProvider provideFetchNodeParseData + * + * @dataProvider provideWithTrimStringsStringNodeParseData + */ + public function testVerifyAttributes(string $input): void + { + $tokens = new TokenIterator($this->lexer->tokenize($input)); + $constExprParser = new ConstExprParser(true, true, [ + 'lines' => true, + 'indexes' => true, + ]); + $visitor = new NodeCollectingVisitor(); + $traverser = new NodeTraverser([$visitor]); + $traverser->traverse([$constExprParser->parse($tokens)]); + + foreach ($visitor->nodes as $node) { + $this->assertNotNull($node->getAttribute(Attribute::START_LINE), (string) $node); + $this->assertNotNull($node->getAttribute(Attribute::END_LINE), (string) $node); + $this->assertNotNull($node->getAttribute(Attribute::START_INDEX), (string) $node); + $this->assertNotNull($node->getAttribute(Attribute::END_INDEX), (string) $node); + } + } + + public function provideTrueNodeParseData(): Iterator { yield [ diff --git a/tests/PHPStan/Parser/NodeCollectingVisitor.php b/tests/PHPStan/Parser/NodeCollectingVisitor.php new file mode 100644 index 00000000..aa01e559 --- /dev/null +++ b/tests/PHPStan/Parser/NodeCollectingVisitor.php @@ -0,0 +1,21 @@ + */ + public $nodes = []; + + public function enterNode(Node $node) + { + $this->nodes[] = $node; + + return null; + } + +} diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 6d089d56..41dd549f 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -5620,11 +5620,11 @@ public function dataLinesAndIndexes(): iterable public function testLinesAndIndexes(string $phpDoc, array $childrenLines): void { $tokens = new TokenIterator($this->lexer->tokenize($phpDoc)); - $constExprParser = new ConstExprParser(true, true); $usedAttributes = [ 'lines' => true, 'indexes' => true, ]; + $constExprParser = new ConstExprParser(true, true, $usedAttributes); $typeParser = new TypeParser($constExprParser, true, $usedAttributes); $phpDocParser = new PhpDocParser($typeParser, $constExprParser, true, true, $usedAttributes); $phpDocNode = $phpDocParser->parse($tokens); @@ -5691,11 +5691,11 @@ public function dataReturnTypeLinesAndIndexes(): iterable public function testReturnTypeLinesAndIndexes(string $phpDoc, array $lines): void { $tokens = new TokenIterator($this->lexer->tokenize($phpDoc)); - $constExprParser = new ConstExprParser(true, true); $usedAttributes = [ 'lines' => true, 'indexes' => true, ]; + $constExprParser = new ConstExprParser(true, true, $usedAttributes); $typeParser = new TypeParser($constExprParser, true, $usedAttributes); $phpDocParser = new PhpDocParser($typeParser, $constExprParser, true, true, $usedAttributes); $phpDocNode = $phpDocParser->parse($tokens); diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 9c876048..bc16e658 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -2179,10 +2179,11 @@ public function testLinesAndIndexes(string $input, array $assertions): void { $tokensArray = $this->lexer->tokenize($input); $tokens = new TokenIterator($tokensArray); - $typeParser = new TypeParser(new ConstExprParser(true, true), true, [ + $usedAttributes = [ 'lines' => true, 'indexes' => true, - ]); + ]; + $typeParser = new TypeParser(new ConstExprParser(true, true), true, $usedAttributes); $typeNode = $typeParser->parse($tokens); foreach ($assertions as [$callable, $expectedContent, $startLine, $endLine, $startIndex, $endIndex]) { From 142198e6e21b56bddca046e7fdbc07850bf610ea Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 24 Apr 2023 21:21:19 +0200 Subject: [PATCH 51/59] TypeParserTest - verify all nodes have attributes --- tests/PHPStan/Parser/TypeParserTest.php | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index bc16e658..9d94c6de 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -9,6 +9,7 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; use PHPStan\PhpDocParser\Ast\ConstExpr\QuoteAwareConstExprStringNode; use PHPStan\PhpDocParser\Ast\Node; +use PHPStan\PhpDocParser\Ast\NodeTraverser; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; @@ -80,6 +81,34 @@ public function testParse(string $input, $expectedResult, int $nextTokenType = L } + /** + * @dataProvider provideParseData + * @param TypeNode|Exception $expectedResult + */ + public function testVerifyAttributes(string $input, $expectedResult): void + { + if ($expectedResult instanceof Exception) { + $this->expectException(get_class($expectedResult)); + $this->expectExceptionMessage($expectedResult->getMessage()); + } + + $usedAttributes = ['lines' => true, 'indexes' => true]; + $typeParser = new TypeParser(new ConstExprParser(true, true, $usedAttributes), true, $usedAttributes); + $tokens = new TokenIterator($this->lexer->tokenize($input)); + + $visitor = new NodeCollectingVisitor(); + $traverser = new NodeTraverser([$visitor]); + $traverser->traverse([$typeParser->parse($tokens)]); + + foreach ($visitor->nodes as $node) { + $this->assertNotNull($node->getAttribute(Attribute::START_LINE), (string) $node); + $this->assertNotNull($node->getAttribute(Attribute::END_LINE), (string) $node); + $this->assertNotNull($node->getAttribute(Attribute::START_INDEX), (string) $node); + $this->assertNotNull($node->getAttribute(Attribute::END_INDEX), (string) $node); + } + } + + /** * @return array */ From ffaba4f196b4e1f4b07149ede53ad3709a409c2c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 24 Apr 2023 21:26:18 +0200 Subject: [PATCH 52/59] Fix attributes for more types --- src/Parser/PhpDocParser.php | 7 ++- src/Parser/TypeParser.php | 74 +++++++++++++++++++++---- tests/PHPStan/Parser/TypeParserTest.php | 4 ++ 3 files changed, 73 insertions(+), 12 deletions(-) diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index a3d43212..322fb5ed 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -493,10 +493,15 @@ private function parseTemplateTagValue(TokenIterator $tokens, bool $parseDescrip private function parseExtendsTagValue(string $tagName, TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode { + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); $baseType = new IdentifierTypeNode($tokens->currentTokenValue()); $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); - $type = $this->typeParser->parseGeneric($tokens, $baseType); + $type = $this->typeParser->parseGeneric( + $tokens, + $this->typeParser->enrichWithAttributes($tokens, $baseType, $startLine, $startIndex) + ); $description = $this->parseOptionalDescription($tokens); diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 6507aed2..7ea6d506 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -62,11 +62,12 @@ public function parse(TokenIterator $tokens): Ast\Type\TypeNode } /** + * @internal * @template T of Ast\Node * @param T $type * @return T */ - private function enrichWithAttributes(TokenIterator $tokens, Ast\Node $type, int $startLine, int $startIndex): Ast\Node + public function enrichWithAttributes(TokenIterator $tokens, Ast\Node $type, int $startLine, int $startIndex): Ast\Node { $endLine = $tokens->currentTokenLine(); $endIndex = $tokens->currentTokenIndex(); @@ -166,7 +167,7 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode $isHtml = $this->isHtml($tokens); $tokens->rollback(); if ($isHtml) { - return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); + return $type; } $type = $this->parseGeneric($tokens, $type); @@ -188,7 +189,10 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode } if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { - $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); + $type = $this->tryParseArrayOrOffsetAccess( + $tokens, + $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex) + ); } } @@ -398,7 +402,14 @@ public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) { // trailing comma case - return new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances); + $type = new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances); + $startLine = $baseType->getAttribute(Ast\Attribute::START_LINE); + $startIndex = $baseType->getAttribute(Ast\Attribute::START_INDEX); + if ($startLine !== null && $startIndex !== null) { + $type = $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); + } + + return $type; } [$genericTypes[], $variances[]] = $this->parseGenericTypeArgument($tokens); $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); @@ -407,7 +418,14 @@ public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); - return new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances); + $type = new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances); + $startLine = $baseType->getAttribute(Ast\Attribute::START_LINE); + $startIndex = $baseType->getAttribute(Ast\Attribute::START_INDEX); + if ($startLine !== null && $startIndex !== null) { + $type = $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); + } + + return $type; } @@ -417,9 +435,11 @@ public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode */ public function parseGenericTypeArgument(TokenIterator $tokens): array { + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); if ($tokens->tryConsumeTokenType(Lexer::TOKEN_WILDCARD)) { return [ - new Ast\Type\IdentifierTypeNode('mixed'), + $this->enrichWithAttributes($tokens, new Ast\Type\IdentifierTypeNode('mixed'), $startLine, $startIndex), Ast\Type\GenericTypeNode::VARIANCE_BIVARIANT, ]; } @@ -498,6 +518,8 @@ private function parseCallableParameter(TokenIterator $tokens): Ast\Type\Callabl /** @phpstan-impure */ private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNode { + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) { $type = $this->parseNullable($tokens); @@ -510,15 +532,33 @@ private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNo $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { - $type = $this->parseGeneric($tokens, $type); + $type = $this->parseGeneric( + $tokens, + $this->enrichWithAttributes( + $tokens, + $type, + $startLine, + $startIndex + ) + ); } elseif (in_array($type->name, ['array', 'list'], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { - $type = $this->parseArrayShape($tokens, $type, $type->name); + $type = $this->parseArrayShape($tokens, $this->enrichWithAttributes( + $tokens, + $type, + $startLine, + $startIndex + ), $type->name); } } if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { - $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); + $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes( + $tokens, + $type, + $startLine, + $startIndex + )); } return $type; @@ -545,6 +585,8 @@ private function tryParseCallable(TokenIterator $tokens, Ast\Type\IdentifierType /** @phpstan-impure */ private function tryParseArrayOrOffsetAccess(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode { + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); try { while ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { $tokens->pushSavePoint(); @@ -556,11 +598,21 @@ private function tryParseArrayOrOffsetAccess(TokenIterator $tokens, Ast\Type\Typ $offset = $this->parse($tokens); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET); $tokens->dropSavePoint(); - $type = new Ast\Type\OffsetAccessTypeNode($type, $offset); + $type = $this->enrichWithAttributes( + $tokens, + new Ast\Type\OffsetAccessTypeNode($type, $offset), + $startLine, + $startIndex + ); } else { $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET); $tokens->dropSavePoint(); - $type = new Ast\Type\ArrayTypeNode($type); + $type = $this->enrichWithAttributes( + $tokens, + new Ast\Type\ArrayTypeNode($type), + $startLine, + $startIndex + ); } } diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 9d94c6de..ad287989 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -1935,6 +1935,10 @@ public function provideParseData(): array ) ), ], + [ + 'callable(): ?int', + new CallableTypeNode(new IdentifierTypeNode('callable'), [], new NullableTypeNode(new IdentifierTypeNode('int'))), + ], ]; } From 69432fa188c12392f5cd6f44dde95728141873b4 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 25 Apr 2023 10:15:14 +0200 Subject: [PATCH 53/59] PhpDocParserTest - verify all nodes have attributes --- tests/PHPStan/Parser/PhpDocParserTest.php | 47 +++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 41dd549f..47fd303a 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -10,6 +10,7 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; use PHPStan\PhpDocParser\Ast\Node; +use PHPStan\PhpDocParser\Ast\NodeTraverser; use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagMethodValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagPropertyValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagValueNode; @@ -53,6 +54,7 @@ use PHPStan\PhpDocParser\Lexer\Lexer; use PHPUnit\Framework\TestCase; use function count; +use function sprintf; use const PHP_EOL; class PhpDocParserTest extends TestCase @@ -5709,4 +5711,49 @@ public function testReturnTypeLinesAndIndexes(string $phpDoc, array $lines): voi $this->assertSame($lines[3], $type->getAttribute(Attribute::END_INDEX)); } + /** + * @dataProvider provideTagsWithNumbers + * @dataProvider provideSpecializedTags + * @dataProvider provideParamTagsData + * @dataProvider provideTypelessParamTagsData + * @dataProvider provideVarTagsData + * @dataProvider provideReturnTagsData + * @dataProvider provideThrowsTagsData + * @dataProvider provideMixinTagsData + * @dataProvider provideDeprecatedTagsData + * @dataProvider providePropertyTagsData + * @dataProvider provideMethodTagsData + * @dataProvider provideSingleLinePhpDocData + * @dataProvider provideMultiLinePhpDocData + * @dataProvider provideTemplateTagsData + * @dataProvider provideExtendsTagsData + * @dataProvider provideTypeAliasTagsData + * @dataProvider provideTypeAliasImportTagsData + * @dataProvider provideAssertTagsData + * @dataProvider provideRealWorldExampleData + * @dataProvider provideDescriptionWithOrWithoutHtml + * @dataProvider provideTagsWithBackslash + * @dataProvider provideSelfOutTagsData + * @dataProvider provideParamOutTagsData + */ + public function testVerifyAttributes(string $label, string $input): void + { + $usedAttributes = ['lines' => true, 'indexes' => true]; + $constExprParser = new ConstExprParser(true, true, $usedAttributes); + $typeParser = new TypeParser($constExprParser, true, $usedAttributes); + $phpDocParser = new PhpDocParser($typeParser, $constExprParser, true, true, $usedAttributes); + $tokens = new TokenIterator($this->lexer->tokenize($input)); + + $visitor = new NodeCollectingVisitor(); + $traverser = new NodeTraverser([$visitor]); + $traverser->traverse([$phpDocParser->parse($tokens)]); + + foreach ($visitor->nodes as $node) { + $this->assertNotNull($node->getAttribute(Attribute::START_LINE), sprintf('%s: %s', $label, $node)); + $this->assertNotNull($node->getAttribute(Attribute::END_LINE), sprintf('%s: %s', $label, $node)); + $this->assertNotNull($node->getAttribute(Attribute::START_INDEX), sprintf('%s: %s', $label, $node)); + $this->assertNotNull($node->getAttribute(Attribute::END_INDEX), sprintf('%s: %s', $label, $node)); + } + } + } From d985f890ad6c2220258019913c86c4f01264d1e3 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 25 Apr 2023 10:17:30 +0200 Subject: [PATCH 54/59] Fix attributes for more tags --- src/Parser/PhpDocParser.php | 53 +++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index 322fb5ed..7f0e4a44 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -86,13 +86,22 @@ public function parse(TokenIterator $tokens): Ast\PhpDoc\PhpDocNode } } + $tag = new Ast\PhpDoc\PhpDocTagNode( + $name, + $this->enrichWithAttributes( + $tokens, + new Ast\PhpDoc\InvalidTagValueNode($e->getMessage(), $e), + $startLine, + $startIndex + ) + ); + $tokens->forwardToTheEnd(); - $tag = new Ast\PhpDoc\PhpDocTagNode($name, new Ast\PhpDoc\InvalidTagValueNode($e->getMessage(), $e)); - return new Ast\PhpDoc\PhpDocNode([$this->enrichWithAttributes($tokens, $tag, $startLine, $startIndex)]); + return $this->enrichWithAttributes($tokens, new Ast\PhpDoc\PhpDocNode([$this->enrichWithAttributes($tokens, $tag, $startLine, $startIndex)]), 1, 0); } - return new Ast\PhpDoc\PhpDocNode(array_values($children)); + return $this->enrichWithAttributes($tokens, new Ast\PhpDoc\PhpDocNode(array_values($children)), 1, 0); } @@ -396,6 +405,8 @@ private function parsePropertyTagValue(TokenIterator $tokens): Ast\PhpDoc\Proper private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueNode { $isStatic = $tokens->tryConsumeTokenValue('static'); + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); $returnTypeOrMethodName = $this->typeParser->parse($tokens); if ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) { @@ -404,7 +415,9 @@ private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTa $tokens->next(); } elseif ($returnTypeOrMethodName instanceof Ast\Type\IdentifierTypeNode) { - $returnType = $isStatic ? new Ast\Type\IdentifierTypeNode('static') : null; + $returnType = $isStatic + ? $this->typeParser->enrichWithAttributes($tokens, new Ast\Type\IdentifierTypeNode('static'), $startLine, $startIndex) + : null; $methodName = $returnTypeOrMethodName->name; $isStatic = false; @@ -414,9 +427,12 @@ private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTa } $templateTypes = []; + if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { do { - $templateTypes[] = $this->parseTemplateTagValue($tokens, false); + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); + $templateTypes[] = $this->enrichWithAttributes($tokens, $this->parseTemplateTagValue($tokens, false), $startLine, $startIndex); } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); } @@ -437,6 +453,9 @@ private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTa private function parseMethodTagValueParameter(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueParameterNode { + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); + switch ($tokens->currentTokenType()) { case Lexer::TOKEN_IDENTIFIER: case Lexer::TOKEN_OPEN_PARENTHESES: @@ -461,7 +480,12 @@ private function parseMethodTagValueParameter(TokenIterator $tokens): Ast\PhpDoc $defaultValue = null; } - return new Ast\PhpDoc\MethodTagValueParameterNode($parameterType, $isReference, $isVariadic, $parameterName, $defaultValue); + return $this->enrichWithAttributes( + $tokens, + new Ast\PhpDoc\MethodTagValueParameterNode($parameterType, $isReference, $isVariadic, $parameterName, $defaultValue), + $startLine, + $startIndex + ); } private function parseTemplateTagValue(TokenIterator $tokens, bool $parseDescription): Ast\PhpDoc\TemplateTagValueNode @@ -526,6 +550,8 @@ private function parseTypeAliasTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeA $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL); if ($this->preserveTypeAliasesWithInvalidTypes) { + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); try { $type = $this->typeParser->parse($tokens); if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) { @@ -544,7 +570,10 @@ private function parseTypeAliasTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeA return new Ast\PhpDoc\TypeAliasTagValueNode($alias, $type); } catch (ParserException $e) { $this->parseOptionalDescription($tokens); - return new Ast\PhpDoc\TypeAliasTagValueNode($alias, new Ast\Type\InvalidTypeNode($e)); + return new Ast\PhpDoc\TypeAliasTagValueNode( + $alias, + $this->enrichWithAttributes($tokens, new Ast\Type\InvalidTypeNode($e), $startLine, $startIndex) + ); } } @@ -560,8 +589,16 @@ private function parseTypeAliasImportTagValue(TokenIterator $tokens): Ast\PhpDoc $tokens->consumeTokenValue(Lexer::TOKEN_IDENTIFIER, 'from'); + $identifierStartLine = $tokens->currentTokenLine(); + $identifierStartIndex = $tokens->currentTokenIndex(); $importedFrom = $tokens->currentTokenValue(); $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); + $importedFromType = $this->enrichWithAttributes( + $tokens, + new IdentifierTypeNode($importedFrom), + $identifierStartLine, + $identifierStartIndex + ); $importedAs = null; if ($tokens->tryConsumeTokenValue('as')) { @@ -569,7 +606,7 @@ private function parseTypeAliasImportTagValue(TokenIterator $tokens): Ast\PhpDoc $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); } - return new Ast\PhpDoc\TypeAliasImportTagValueNode($importedAlias, new IdentifierTypeNode($importedFrom), $importedAs); + return new Ast\PhpDoc\TypeAliasImportTagValueNode($importedAlias, $importedFromType, $importedAs); } /** From 6c04009f6cae6eda2f040745b6b846080ef069c2 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 25 Apr 2023 10:53:35 +0200 Subject: [PATCH 55/59] Fix ArrayTypeNode indexes --- src/Parser/TypeParser.php | 36 +++--- tests/PHPStan/Parser/TypeParserTest.php | 155 ++++++++++++++++++++++++ 2 files changed, 177 insertions(+), 14 deletions(-) diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 7ea6d506..d59d0f1f 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -585,8 +585,8 @@ private function tryParseCallable(TokenIterator $tokens, Ast\Type\IdentifierType /** @phpstan-impure */ private function tryParseArrayOrOffsetAccess(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode { - $startLine = $tokens->currentTokenLine(); - $startIndex = $tokens->currentTokenIndex(); + $startLine = $type->getAttribute(Ast\Attribute::START_LINE); + $startIndex = $type->getAttribute(Ast\Attribute::START_INDEX); try { while ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { $tokens->pushSavePoint(); @@ -598,21 +598,29 @@ private function tryParseArrayOrOffsetAccess(TokenIterator $tokens, Ast\Type\Typ $offset = $this->parse($tokens); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET); $tokens->dropSavePoint(); - $type = $this->enrichWithAttributes( - $tokens, - new Ast\Type\OffsetAccessTypeNode($type, $offset), - $startLine, - $startIndex - ); + $type = new Ast\Type\OffsetAccessTypeNode($type, $offset); + + if ($startLine !== null && $startIndex !== null) { + $type = $this->enrichWithAttributes( + $tokens, + $type, + $startLine, + $startIndex + ); + } } else { $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET); $tokens->dropSavePoint(); - $type = $this->enrichWithAttributes( - $tokens, - new Ast\Type\ArrayTypeNode($type), - $startLine, - $startIndex - ); + $type = new Ast\Type\ArrayTypeNode($type); + + if ($startLine !== null && $startIndex !== null) { + $type = $this->enrichWithAttributes( + $tokens, + $type, + $startLine, + $startIndex + ); + } } } diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index ad287989..09edabb3 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -2202,6 +2202,161 @@ static function (TypeNode $typeNode): TypeNode { ], ], ]; + + yield [ + 'int[][][]', + [ + [ + static function (TypeNode $typeNode): TypeNode { + return $typeNode; + }, + 'int[][][]', + 1, + 1, + 0, + 6, + ], + [ + static function (ArrayTypeNode $typeNode): TypeNode { + return $typeNode->type; + }, + 'int[][]', + 1, + 1, + 0, + 4, + ], + [ + static function (ArrayTypeNode $typeNode): TypeNode { + if (!$typeNode->type instanceof ArrayTypeNode) { + throw new Exception(); + } + + return $typeNode->type->type; + }, + 'int[]', + 1, + 1, + 0, + 2, + ], + [ + static function (ArrayTypeNode $typeNode): TypeNode { + if (!$typeNode->type instanceof ArrayTypeNode) { + throw new Exception(); + } + if (!$typeNode->type->type instanceof ArrayTypeNode) { + throw new Exception(); + } + + return $typeNode->type->type->type; + }, + 'int', + 1, + 1, + 0, + 0, + ], + ], + ]; + + yield [ + 'int[foo][bar][baz]', + [ + [ + static function (TypeNode $typeNode): TypeNode { + return $typeNode; + }, + 'int[foo][bar][baz]', + 1, + 1, + 0, + 9, + ], + [ + static function (OffsetAccessTypeNode $typeNode): TypeNode { + return $typeNode->type; + }, + 'int[foo][bar]', + 1, + 1, + 0, + 6, + ], + [ + static function (OffsetAccessTypeNode $typeNode): TypeNode { + return $typeNode->offset; + }, + 'baz', + 1, + 1, + 8, + 8, + ], + [ + static function (OffsetAccessTypeNode $typeNode): TypeNode { + if (!$typeNode->type instanceof OffsetAccessTypeNode) { + throw new Exception(); + } + + return $typeNode->type->type; + }, + 'int[foo]', + 1, + 1, + 0, + 3, + ], + [ + static function (OffsetAccessTypeNode $typeNode): TypeNode { + if (!$typeNode->type instanceof OffsetAccessTypeNode) { + throw new Exception(); + } + + return $typeNode->type->offset; + }, + 'bar', + 1, + 1, + 5, + 5, + ], + [ + static function (OffsetAccessTypeNode $typeNode): TypeNode { + if (!$typeNode->type instanceof OffsetAccessTypeNode) { + throw new Exception(); + } + if (!$typeNode->type->type instanceof OffsetAccessTypeNode) { + throw new Exception(); + } + + return $typeNode->type->type->type; + }, + 'int', + 1, + 1, + 0, + 0, + ], + [ + static function (OffsetAccessTypeNode $typeNode): TypeNode { + if (!$typeNode->type instanceof OffsetAccessTypeNode) { + throw new Exception(); + } + if (!$typeNode->type->type instanceof OffsetAccessTypeNode) { + throw new Exception(); + } + + return $typeNode->type->type->offset; + }, + 'foo', + 1, + 1, + 2, + 2, + ], + ], + ]; } /** From 308c57c96db3237c6648ca266a72516d2be82f0a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 22 Apr 2023 10:50:50 +0200 Subject: [PATCH 56/59] Format-preserving printer --- composer.json | 1 + phpstan-baseline.neon | 5 + phpstan.neon | 2 + src/Parser/ConstExprParser.php | 7 +- src/Parser/TokenIterator.php | 22 + src/Printer/DiffElem.php | 44 + src/Printer/Differ.php | 196 +++ src/Printer/Printer.php | 379 ++++++ tests/PHPStan/Printer/DifferTest.php | 95 ++ .../IntegrationPrinterWithPhpParserTest.php | 146 ++ tests/PHPStan/Printer/PhpPrinter.php | 60 + .../PhpPrinterIndentationDetectorVisitor.php | 83 ++ tests/PHPStan/Printer/PrinterTest.php | 1179 +++++++++++++++++ .../Printer/data/printer-1-spaces-after.php | 26 + .../Printer/data/printer-1-spaces-before.php | 24 + .../Printer/data/printer-1-tabs-after.php | 26 + .../Printer/data/printer-1-tabs-before.php | 24 + 17 files changed, 2315 insertions(+), 4 deletions(-) create mode 100644 src/Printer/DiffElem.php create mode 100644 src/Printer/Differ.php create mode 100644 src/Printer/Printer.php create mode 100644 tests/PHPStan/Printer/DifferTest.php create mode 100644 tests/PHPStan/Printer/IntegrationPrinterWithPhpParserTest.php create mode 100644 tests/PHPStan/Printer/PhpPrinter.php create mode 100644 tests/PHPStan/Printer/PhpPrinterIndentationDetectorVisitor.php create mode 100644 tests/PHPStan/Printer/PrinterTest.php create mode 100644 tests/PHPStan/Printer/data/printer-1-spaces-after.php create mode 100644 tests/PHPStan/Printer/data/printer-1-spaces-before.php create mode 100644 tests/PHPStan/Printer/data/printer-1-tabs-after.php create mode 100644 tests/PHPStan/Printer/data/printer-1-tabs-before.php diff --git a/composer.json b/composer.json index 3b902ae2..30b879b7 100644 --- a/composer.json +++ b/composer.json @@ -6,6 +6,7 @@ "php": "^7.2 || ^8.0" }, "require-dev": { + "nikic/php-parser": "^4.15", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", "phpstan/phpstan": "^1.5", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 1dc71f89..04100fcd 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -24,3 +24,8 @@ parameters: message: "#^Method PHPStan\\\\PhpDocParser\\\\Parser\\\\StringUnescaper\\:\\:parseEscapeSequences\\(\\) should return string but returns string\\|null\\.$#" count: 1 path: src/Parser/StringUnescaper.php + + - + message: "#^Variable property access on PHPStan\\\\PhpDocParser\\\\Ast\\\\Node\\.$#" + count: 2 + path: src/Printer/Printer.php diff --git a/phpstan.neon b/phpstan.neon index 645cb1d1..0c336514 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,6 +5,8 @@ parameters: paths: - src - tests + excludePaths: + - tests/PHPStan/*/data/* level: 8 ignoreErrors: - '#^Dynamic call to static method PHPUnit\\Framework\\Assert#' diff --git a/src/Parser/ConstExprParser.php b/src/Parser/ConstExprParser.php index f8cfcb59..cc05ee3b 100644 --- a/src/Parser/ConstExprParser.php +++ b/src/Parser/ConstExprParser.php @@ -126,7 +126,7 @@ public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\Con ); case 'array': $tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES); - return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_PARENTHESES); + return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_PARENTHESES, $startIndex); } if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_COLON)) { @@ -177,7 +177,7 @@ public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\Con ); } elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { - return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_SQUARE_BRACKET); + return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_SQUARE_BRACKET, $startIndex); } throw new ParserException( @@ -191,12 +191,11 @@ public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\Con } - private function parseArray(TokenIterator $tokens, int $endToken): Ast\ConstExpr\ConstExprArrayNode + private function parseArray(TokenIterator $tokens, int $endToken, int $startIndex): Ast\ConstExpr\ConstExprArrayNode { $items = []; $startLine = $tokens->currentTokenLine(); - $startIndex = $tokens->currentTokenIndex(); if (!$tokens->tryConsumeTokenType($endToken)) { do { diff --git a/src/Parser/TokenIterator.php b/src/Parser/TokenIterator.php index bb197e6d..a2f076f9 100644 --- a/src/Parser/TokenIterator.php +++ b/src/Parser/TokenIterator.php @@ -2,6 +2,7 @@ namespace PHPStan\PhpDocParser\Parser; +use LogicException; use PHPStan\PhpDocParser\Lexer\Lexer; use function array_pop; use function assert; @@ -46,6 +47,27 @@ public function getTokens(): array } + public function getContentBetween(int $startPos, int $endPos): string + { + if ($startPos < 0 || $endPos > count($this->tokens)) { + throw new LogicException(); + } + + $content = ''; + for ($i = $startPos; $i < $endPos; $i++) { + $content .= $this->tokens[$i][Lexer::VALUE_OFFSET]; + } + + return $content; + } + + + public function getTokenCount(): int + { + return count($this->tokens); + } + + public function currentTokenValue(): string { return $this->tokens[$this->index][Lexer::VALUE_OFFSET]; diff --git a/src/Printer/DiffElem.php b/src/Printer/DiffElem.php new file mode 100644 index 00000000..2684dfc7 --- /dev/null +++ b/src/Printer/DiffElem.php @@ -0,0 +1,44 @@ +type = $type; + $this->old = $old; + $this->new = $new; + } + +} diff --git a/src/Printer/Differ.php b/src/Printer/Differ.php new file mode 100644 index 00000000..ab10be59 --- /dev/null +++ b/src/Printer/Differ.php @@ -0,0 +1,196 @@ +isEqual = $isEqual; + } + + /** + * Calculate diff (edit script) from $old to $new. + * + * @param T[] $old Original array + * @param T[] $new New array + * + * @return DiffElem[] Diff (edit script) + */ + public function diff(array $old, array $new): array + { + [$trace, $x, $y] = $this->calculateTrace($old, $new); + return $this->extractDiff($trace, $x, $y, $old, $new); + } + + /** + * Calculate diff, including "replace" operations. + * + * If a sequence of remove operations is followed by the same number of add operations, these + * will be coalesced into replace operations. + * + * @param T[] $old Original array + * @param T[] $new New array + * + * @return DiffElem[] Diff (edit script), including replace operations + */ + public function diffWithReplacements(array $old, array $new): array + { + return $this->coalesceReplacements($this->diff($old, $new)); + } + + /** + * @param T[] $old + * @param T[] $new + * @return array{array>, int, int} + */ + private function calculateTrace(array $old, array $new): array + { + $n = count($old); + $m = count($new); + $max = $n + $m; + $v = [1 => 0]; + $trace = []; + for ($d = 0; $d <= $max; $d++) { + $trace[] = $v; + for ($k = -$d; $k <= $d; $k += 2) { + if ($k === -$d || ($k !== $d && $v[$k - 1] < $v[$k + 1])) { + $x = $v[$k + 1]; + } else { + $x = $v[$k - 1] + 1; + } + + $y = $x - $k; + while ($x < $n && $y < $m && ($this->isEqual)($old[$x], $new[$y])) { + $x++; + $y++; + } + + $v[$k] = $x; + if ($x >= $n && $y >= $m) { + return [$trace, $x, $y]; + } + } + } + throw new Exception('Should not happen'); + } + + /** + * @param array> $trace + * @param T[] $old + * @param T[] $new + * @return DiffElem[] + */ + private function extractDiff(array $trace, int $x, int $y, array $old, array $new): array + { + $result = []; + for ($d = count($trace) - 1; $d >= 0; $d--) { + $v = $trace[$d]; + $k = $x - $y; + + if ($k === -$d || ($k !== $d && $v[$k - 1] < $v[$k + 1])) { + $prevK = $k + 1; + } else { + $prevK = $k - 1; + } + + $prevX = $v[$prevK]; + $prevY = $prevX - $prevK; + + while ($x > $prevX && $y > $prevY) { + $result[] = new DiffElem(DiffElem::TYPE_KEEP, $old[$x - 1], $new[$y - 1]); + $x--; + $y--; + } + + if ($d === 0) { + break; + } + + while ($x > $prevX) { + $result[] = new DiffElem(DiffElem::TYPE_REMOVE, $old[$x - 1], null); + $x--; + } + + while ($y > $prevY) { + $result[] = new DiffElem(DiffElem::TYPE_ADD, null, $new[$y - 1]); + $y--; + } + } + return array_reverse($result); + } + + /** + * Coalesce equal-length sequences of remove+add into a replace operation. + * + * @param DiffElem[] $diff + * @return DiffElem[] + */ + private function coalesceReplacements(array $diff): array + { + $newDiff = []; + $c = count($diff); + for ($i = 0; $i < $c; $i++) { + $diffType = $diff[$i]->type; + if ($diffType !== DiffElem::TYPE_REMOVE) { + $newDiff[] = $diff[$i]; + continue; + } + + $j = $i; + while ($j < $c && $diff[$j]->type === DiffElem::TYPE_REMOVE) { + $j++; + } + + $k = $j; + while ($k < $c && $diff[$k]->type === DiffElem::TYPE_ADD) { + $k++; + } + + if ($j - $i === $k - $j) { + $len = $j - $i; + for ($n = 0; $n < $len; $n++) { + $newDiff[] = new DiffElem( + DiffElem::TYPE_REPLACE, + $diff[$i + $n]->old, + $diff[$j + $n]->new + ); + } + } else { + for (; $i < $k; $i++) { + $newDiff[] = $diff[$i]; + } + } + $i = $k - 1; + } + return $newDiff; + } + +} diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php new file mode 100644 index 00000000..b4d44826 --- /dev/null +++ b/src/Printer/Printer.php @@ -0,0 +1,379 @@ + */ + private $differ; + + /** + * Map From "{$class}->{$subNode}" to string that should be inserted + * between elements of this list subnode + * + * @var array + */ + private $listInsertionMap = [ + PhpDocNode::class . '->children' => "\n * ", + UnionTypeNode::class . '->types' => '|', + IntersectionTypeNode::class . '->types' => '&', + ArrayShapeNode::class . '->items' => ', ', + ObjectShapeNode::class . '->items' => ', ', + CallableTypeNode::class . '->parameters' => ', ', + GenericTypeNode::class . '->genericTypes' => ', ', + ConstExprArrayNode::class . '->items' => ', ', + MethodTagValueNode::class . '->parameters' => ', ', + ]; + + /** + * [$find, $extraLeft, $extraRight] + * + * @var array + */ + protected $emptyListInsertionMap = [ + CallableTypeNode::class . '->parameters' => ['(', '', ''], + ArrayShapeNode::class . '->items' => ['{', '', ''], + ObjectShapeNode::class . '->items' => ['{', '', ''], + ]; + + public function printFormatPreserving(PhpDocNode $node, PhpDocNode $originalNode, TokenIterator $originalTokens): string + { + $this->differ = new Differ(static function ($a, $b) { + if ($a instanceof Node && $b instanceof Node) { + return $a === $b->getAttribute(Attribute::ORIGINAL_NODE); + } + + return false; + }); + + $tokenIndex = 0; + $result = $this->printArrayFormatPreserving( + $node->children, + $originalNode->children, + $originalTokens, + $tokenIndex, + PhpDocNode::class, + 'children' + ); + if ($result !== null) { + return $result . $originalTokens->getContentBetween($tokenIndex, $originalTokens->getTokenCount()); + } + + return $this->print($node); + } + + public function print(Node $node): string + { + return (string) $node; + } + + /** + * @param Node[] $nodes + * @param Node[] $originalNodes + */ + private function printArrayFormatPreserving(array $nodes, array $originalNodes, TokenIterator $originalTokens, int &$tokenIndex, string $parentNodeClass, string $subNodeName): ?string + { + $diff = $this->differ->diffWithReplacements($originalNodes, $nodes); + $mapKey = $parentNodeClass . '->' . $subNodeName; + $insertStr = $this->listInsertionMap[$mapKey] ?? null; + $result = ''; + $beforeFirstKeepOrReplace = true; + $delayedAdd = []; + + $insertNewline = false; + [$isMultiline, $beforeAsteriskIndent, $afterAsteriskIndent] = $this->isMultiline($tokenIndex, $originalNodes, $originalTokens); + + if ($insertStr === "\n * ") { + $insertStr = sprintf("\n%s*%s", $beforeAsteriskIndent, $afterAsteriskIndent); + } + + foreach ($diff as $i => $diffElem) { + $diffType = $diffElem->type; + $newNode = $diffElem->new; + $originalNode = $diffElem->old; + if ($diffType === DiffElem::TYPE_KEEP || $diffType === DiffElem::TYPE_REPLACE) { + $beforeFirstKeepOrReplace = false; + if (!$newNode instanceof Node || !$originalNode instanceof Node) { + return null; + } + $itemStartPos = $originalNode->getAttribute(Attribute::START_INDEX); + $itemEndPos = $originalNode->getAttribute(Attribute::END_INDEX); + if ($itemStartPos < 0 || $itemEndPos < 0 || $itemStartPos < $tokenIndex) { + throw new LogicException(); + } + + $result .= $originalTokens->getContentBetween($tokenIndex, $itemStartPos); + + if (count($delayedAdd) > 0) { + foreach ($delayedAdd as $delayedAddNode) { + $result .= $this->printNodeFormatPreserving($delayedAddNode, $originalTokens); + + if ($insertNewline) { + $result .= $insertStr . sprintf("\n%s*%s", $beforeAsteriskIndent, $afterAsteriskIndent); + } else { + $result .= $insertStr; + } + } + + $delayedAdd = []; + } + } elseif ($diffType === DiffElem::TYPE_ADD) { + if ($insertStr === null) { + return null; + } + if (!$newNode instanceof Node) { + return null; + } + + if ($insertStr === ', ' && $isMultiline) { + $insertStr = ','; + $insertNewline = true; + } + + if ($beforeFirstKeepOrReplace) { + // Will be inserted at the next "replace" or "keep" element + $delayedAdd[] = $newNode; + continue; + } + + $itemEndPos = $tokenIndex - 1; + if ($insertNewline) { + $result .= $insertStr . sprintf("\n%s*%s", $beforeAsteriskIndent, $afterAsteriskIndent); + } else { + $result .= $insertStr; + } + + } elseif ($diffType === DiffElem::TYPE_REMOVE) { + if (!$originalNode instanceof Node) { + return null; + } + + $itemStartPos = $originalNode->getAttribute(Attribute::START_INDEX); + $itemEndPos = $originalNode->getAttribute(Attribute::END_INDEX); + if ($itemStartPos < 0 || $itemEndPos < 0) { + throw new LogicException(); + } + + if ($i === 0) { + // If we're removing from the start, keep the tokens before the node and drop those after it, + // instead of the other way around. + $originalTokensArray = $originalTokens->getTokens(); + for ($j = $tokenIndex; $j < $itemStartPos; $j++) { + if ($originalTokensArray[$j][Lexer::TYPE_OFFSET] === Lexer::TOKEN_PHPDOC_EOL) { + break; + } + $result .= $originalTokensArray[$j][Lexer::VALUE_OFFSET]; + } + } + + $tokenIndex = $itemEndPos + 1; + continue; + } + + $result .= $this->printNodeFormatPreserving($newNode, $originalTokens); + $tokenIndex = $itemEndPos + 1; + } + + if (count($delayedAdd) > 0) { + if (!isset($this->emptyListInsertionMap[$mapKey])) { + return null; + } + + [$findToken, $extraLeft, $extraRight] = $this->emptyListInsertionMap[$mapKey]; + if ($findToken !== null) { + $originalTokensArray = $originalTokens->getTokens(); + for (; $tokenIndex < count($originalTokensArray); $tokenIndex++) { + $result .= $originalTokensArray[$tokenIndex][Lexer::VALUE_OFFSET]; + if ($originalTokensArray[$tokenIndex][Lexer::VALUE_OFFSET] !== $findToken) { + continue; + } + + $tokenIndex++; + break; + } + } + $first = true; + $result .= $extraLeft; + foreach ($delayedAdd as $delayedAddNode) { + if (!$first) { + $result .= $insertStr; + if ($insertNewline) { + $result .= sprintf("\n%s*%s", $beforeAsteriskIndent, $afterAsteriskIndent); + } + } + $result .= $this->printNodeFormatPreserving($delayedAddNode, $originalTokens); + $first = false; + } + $result .= $extraRight; + } + + return $result; + } + + /** + * @param Node[] $nodes + * @return array{bool, string, string} + */ + private function isMultiline(int $initialIndex, array $nodes, TokenIterator $originalTokens): array + { + $isMultiline = count($nodes) > 1; + $pos = $initialIndex; + $allText = ''; + /** @var Node|null $node */ + foreach ($nodes as $node) { + if (!$node instanceof Node) { + continue; + } + + $endPos = $node->getAttribute(Attribute::END_INDEX) + 1; + $text = $originalTokens->getContentBetween($pos, $endPos); + $allText .= $text; + if (strpos($text, "\n") === false) { + // We require that a newline is present between *every* item. If the formatting + // is inconsistent, with only some items having newlines, we don't consider it + // as multiline + $isMultiline = false; + } + $pos = $endPos; + } + + $c = preg_match_all('~\n(?[\\x09\\x20]*)\*(?\\x20*)~', $allText, $matches, PREG_SET_ORDER); + if ($c === 0) { + return [$isMultiline, '', '']; + } + + $before = ''; + $after = ''; + foreach ($matches as $match) { + if (strlen($match['before']) > strlen($before)) { + $before = $match['before']; + } + if (strlen($match['after']) <= strlen($after)) { + continue; + } + + $after = $match['after']; + } + + return [$isMultiline, $before, $after]; + } + + private function printNodeFormatPreserving(Node $node, TokenIterator $originalTokens): string + { + /** @var Node|null $originalNode */ + $originalNode = $node->getAttribute(Attribute::ORIGINAL_NODE); + if ($originalNode === null) { + return $this->print($node); + } + + $class = get_class($node); + if ($class !== get_class($originalNode)) { + throw new LogicException(); + } + + $startPos = $originalNode->getAttribute(Attribute::START_INDEX); + $endPos = $originalNode->getAttribute(Attribute::END_INDEX); + if ($startPos < 0 || $endPos < 0) { + throw new LogicException(); + } + + $result = ''; + $pos = $startPos; + $subNodeNames = array_keys(get_object_vars($node)); + foreach ($subNodeNames as $subNodeName) { + $subNode = $node->$subNodeName; + $origSubNode = $originalNode->$subNodeName; + + if ( + (!$subNode instanceof Node && $subNode !== null) + || (!$origSubNode instanceof Node && $origSubNode !== null) + ) { + if ($subNode === $origSubNode) { + // Unchanged, can reuse old code + continue; + } + + if (is_array($subNode) && is_array($origSubNode)) { + // Array subnode changed, we might be able to reconstruct it + $listResult = $this->printArrayFormatPreserving( + $subNode, + $origSubNode, + $originalTokens, + $pos, + $class, + $subNodeName + ); + + if ($listResult === null) { + return $this->print($node); + } + + $result .= $listResult; + continue; + } + + return $this->print($node); + } + + if ($origSubNode === null) { + if ($subNode === null) { + // Both null, nothing to do + continue; + } + + return $this->print($node); + } + + $subStartPos = $origSubNode->getAttribute(Attribute::START_INDEX); + $subEndPos = $origSubNode->getAttribute(Attribute::END_INDEX); + if ($subStartPos < 0 || $subEndPos < 0) { + throw new LogicException(); + } + + if ($subNode === null) { + return $this->print($node); + } + + $result .= $originalTokens->getContentBetween($pos, $subStartPos); + $result .= $this->printNodeFormatPreserving($subNode, $originalTokens); + $pos = $subEndPos + 1; + } + + $result .= $originalTokens->getContentBetween($pos, $endPos + 1); + + return $result; + } + +} diff --git a/tests/PHPStan/Printer/DifferTest.php b/tests/PHPStan/Printer/DifferTest.php new file mode 100644 index 00000000..b06f1692 --- /dev/null +++ b/tests/PHPStan/Printer/DifferTest.php @@ -0,0 +1,95 @@ +type) { + case DiffElem::TYPE_KEEP: + $diffStr .= $diffElem->old; + break; + case DiffElem::TYPE_REMOVE: + $diffStr .= '-' . $diffElem->old; + break; + case DiffElem::TYPE_ADD: + $diffStr .= '+' . $diffElem->new; + break; + case DiffElem::TYPE_REPLACE: + $diffStr .= '/' . $diffElem->old . $diffElem->new; + break; + } + } + return $diffStr; + } + + /** + * @dataProvider provideTestDiff + */ + public function testDiff(string $oldStr, string $newStr, string $expectedDiffStr): void + { + $differ = new Differ(static function ($a, $b) { + return $a === $b; + }); + $diff = $differ->diff(str_split($oldStr), str_split($newStr)); + $this->assertSame($expectedDiffStr, $this->formatDiffString($diff)); + } + + /** + * @return list + */ + public function provideTestDiff(): array + { + return [ + ['abc', 'abc', 'abc'], + ['abc', 'abcdef', 'abc+d+e+f'], + ['abcdef', 'abc', 'abc-d-e-f'], + ['abcdef', 'abcxyzdef', 'abc+x+y+zdef'], + ['axyzb', 'ab', 'a-x-y-zb'], + ['abcdef', 'abxyef', 'ab-c-d+x+yef'], + ['abcdef', 'cdefab', '-a-bcdef+a+b'], + ]; + } + + /** + * @dataProvider provideTestDiffWithReplacements + */ + public function testDiffWithReplacements(string $oldStr, string $newStr, string $expectedDiffStr): void + { + $differ = new Differ(static function ($a, $b) { + return $a === $b; + }); + $diff = $differ->diffWithReplacements(str_split($oldStr), str_split($newStr)); + $this->assertSame($expectedDiffStr, $this->formatDiffString($diff)); + } + + /** + * @return list + */ + public function provideTestDiffWithReplacements(): array + { + return [ + ['abcde', 'axyze', 'a/bx/cy/dze'], + ['abcde', 'xbcdy', '/axbcd/ey'], + ['abcde', 'axye', 'a-b-c-d+x+ye'], + ['abcde', 'axyzue', 'a-b-c-d+x+y+z+ue'], + ]; + } + +} diff --git a/tests/PHPStan/Printer/IntegrationPrinterWithPhpParserTest.php b/tests/PHPStan/Printer/IntegrationPrinterWithPhpParserTest.php new file mode 100644 index 00000000..74d05072 --- /dev/null +++ b/tests/PHPStan/Printer/IntegrationPrinterWithPhpParserTest.php @@ -0,0 +1,146 @@ + + */ + public function dataPrint(): iterable + { + $insertParameter = new class () extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof PhpDocNode) { + $node->children[] = new PhpDocTagNode('@param', new ParamTagValueNode( + new IdentifierTypeNode('Bar'), + false, + '$b', + '' + )); + } + return $node; + } + + }; + yield [ + __DIR__ . '/data/printer-1-tabs-before.php', + __DIR__ . '/data/printer-1-tabs-after.php', + $insertParameter, + ]; + yield [ + __DIR__ . '/data/printer-1-spaces-before.php', + __DIR__ . '/data/printer-1-spaces-after.php', + $insertParameter, + ]; + } + + /** + * @dataProvider dataPrint + */ + public function testPrint(string $file, string $expectedFile, NodeVisitor $visitor): void + { + $lexer = new Emulative([ + 'usedAttributes' => [ + 'comments', + 'startLine', 'endLine', + 'startTokenPos', 'endTokenPos', + ], + ]); + $phpParser = new Php7($lexer); + $phpTraverser = new PhpParserNodeTraverser(); + $phpTraverser->addVisitor(new PhpParserCloningVisitor()); + + $printer = new PhpPrinter(); + $fileContents = file_get_contents($file); + if ($fileContents === false) { + $this->fail('Could not read ' . $file); + } + + /** @var PhpNode[] $oldStmts */ + $oldStmts = $phpParser->parse($fileContents); + $oldTokens = $lexer->getTokens(); + + $phpTraverser2 = new PhpParserNodeTraverser(); + $phpTraverser2->addVisitor(new class ($visitor) extends NodeVisitorAbstract { + + /** @var NodeVisitor */ + private $visitor; + + public function __construct(NodeVisitor $visitor) + { + $this->visitor = $visitor; + } + + public function enterNode(PhpNode $phpNode) + { + if ($phpNode->getDocComment() === null) { + return null; + } + + $phpDoc = $phpNode->getDocComment()->getText(); + + $usedAttributes = ['lines' => true, 'indexes' => true]; + $constExprParser = new ConstExprParser(true, true, $usedAttributes); + $phpDocParser = new PhpDocParser( + new TypeParser($constExprParser, true, $usedAttributes), + $constExprParser, + true, + true, + $usedAttributes + ); + $lexer = new Lexer(); + $tokens = new TokenIterator($lexer->tokenize($phpDoc)); + $phpDocNode = $phpDocParser->parse($tokens); + $cloningTraverser = new NodeTraverser([new NodeVisitor\CloningVisitor()]); + $newNodes = $cloningTraverser->traverse([$phpDocNode]); + + $changingTraverser = new NodeTraverser([$this->visitor]); + + /** @var PhpDocNode $newNode */ + [$newNode] = $changingTraverser->traverse($newNodes); + + $printer = new Printer(); + $newPhpDoc = $printer->printFormatPreserving($newNode, $phpDocNode, $tokens); + $phpNode->setDocComment(new Doc($newPhpDoc)); + + return $phpNode; + } + + }); + + /** @var PhpNode[] $newStmts */ + $newStmts = $phpTraverser->traverse($oldStmts); + $newStmts = $phpTraverser2->traverse($newStmts); + + $newCode = $printer->printFormatPreserving($newStmts, $oldStmts, $oldTokens); + $this->assertStringEqualsFile($expectedFile, $newCode); + } + +} diff --git a/tests/PHPStan/Printer/PhpPrinter.php b/tests/PHPStan/Printer/PhpPrinter.php new file mode 100644 index 00000000..d691a981 --- /dev/null +++ b/tests/PHPStan/Printer/PhpPrinter.php @@ -0,0 +1,60 @@ +indentCharacter = ' '; + $this->indentSize = 4; + } + + protected function preprocessNodes(array $nodes): void + { + parent::preprocessNodes($nodes); + if ($this->origTokens === null) { + return; + } + + $traverser = new NodeTraverser(); + + $visitor = new PhpPrinterIndentationDetectorVisitor($this->origTokens); + $traverser->addVisitor($visitor); + $traverser->traverse($nodes); + + $this->indentCharacter = $visitor->indentCharacter; + $this->indentSize = $visitor->indentSize; + } + + protected function setIndentLevel(int $level): void + { + $this->indentLevel = $level; + $this->nl = "\n" . str_repeat($this->indentCharacter, $level); + } + + protected function indent(): void + { + $this->indentLevel += $this->indentSize; + $this->nl = "\n" . str_repeat($this->indentCharacter, $this->indentLevel); + } + + protected function outdent(): void + { + $this->indentLevel -= $this->indentSize; + $this->nl = "\n" . str_repeat($this->indentCharacter, $this->indentLevel); + } + +} diff --git a/tests/PHPStan/Printer/PhpPrinterIndentationDetectorVisitor.php b/tests/PHPStan/Printer/PhpPrinterIndentationDetectorVisitor.php new file mode 100644 index 00000000..9e2b9248 --- /dev/null +++ b/tests/PHPStan/Printer/PhpPrinterIndentationDetectorVisitor.php @@ -0,0 +1,83 @@ +origTokens = $origTokens; + } + + public function enterNode(Node $node) + { + if ($node instanceof Node\Stmt\Namespace_ || $node instanceof Node\Stmt\Declare_) { + return null; + } + if (!property_exists($node, 'stmts')) { + return null; + } + + if (count($node->stmts) === 0) { + return null; + } + + $firstStmt = $node->stmts[0]; + $text = $this->origTokens->getTokenCode($node->getStartTokenPos(), $firstStmt->getStartTokenPos(), 0); + + $c = preg_match_all('~\n([\\x09\\x20]*)~', $text, $matches, PREG_SET_ORDER); + if ($c === 0 || $c === false) { + return null; + } + + $char = ''; + $size = 0; + foreach ($matches as $match) { + $l = strlen($match[1]); + if ($l === 0) { + continue; + } + + $char = $match[1]; + $size = $l; + break; + } + + if ($size > 0) { + $d = preg_match('~^(\\x20+)$~', $char); + if ($d !== false && $d > 0) { + $size = strlen($char); + $char = ' '; + } + + $this->indentCharacter = $char; + $this->indentSize = $size; + + return NodeTraverser::STOP_TRAVERSAL; + } + + return null; + } + +} diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php new file mode 100644 index 00000000..69acfc55 --- /dev/null +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -0,0 +1,1179 @@ + + */ + public function dataPrintFormatPreserving(): iterable + { + $noopVisitor = new class extends AbstractNodeVisitor { + + }; + yield ['/** */', '/** */', $noopVisitor]; + yield ['/** + */', '/** + */', $noopVisitor]; + yield [ + '/** @param Foo $foo */', + '/** @param Foo $foo */', + $noopVisitor, + ]; + yield [ + '/** + * @param Foo $foo + */', + '/** + * @param Foo $foo + */', + $noopVisitor, + ]; + + $removeFirst = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof PhpDocNode) { + unset($node->children[0]); + + $node->children = array_values($node->children); + return $node; + } + + return null; + } + + }; + yield [ + '/** @param Foo $foo */', + '/** */', + $removeFirst, + ]; + yield [ + '/** @param Foo $foo*/', + '/** */', + $removeFirst, + ]; + + yield [ + '/** @return Foo */', + '/** */', + $removeFirst, + ]; + yield [ + '/** @return Foo*/', + '/** */', + $removeFirst, + ]; + + yield [ + '/** + * @param Foo $foo + */', + '/** + */', + $removeFirst, + ]; + + yield [ + '/** + * @param Foo $foo + * @param Bar $bar + */', + '/** + * @param Bar $bar + */', + $removeFirst, + ]; + + yield [ + '/** + * @param Foo $foo + * @param Bar $bar + */', + '/** + * @param Bar $bar + */', + $removeFirst, + ]; + + $removeLast = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof PhpDocNode) { + array_pop($node->children); + + return $node; + } + + return null; + } + + }; + + yield [ + '/** + * @param Foo $foo + * @param Bar $bar + */', + '/** + * @param Foo $foo + */', + $removeLast, + ]; + + $removeSecond = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof PhpDocNode) { + unset($node->children[1]); + $node->children = array_values($node->children); + + return $node; + } + + return null; + } + + }; + + yield [ + '/** + * @param Foo $foo + * @param Bar $bar + */', + '/** + * @param Foo $foo + */', + $removeSecond, + ]; + + yield [ + '/** + * @param Foo $foo + * @param Bar $bar + * @param Baz $baz + */', + '/** + * @param Foo $foo + * @param Baz $baz + */', + $removeSecond, + ]; + + yield [ + '/** + * @param Foo $foo + * @param Bar $bar + * @param Baz $baz + */', + '/** + * @param Foo $foo + * @param Baz $baz + */', + $removeSecond, + ]; + + $changeReturnType = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ReturnTagValueNode) { + $node->type = new IdentifierTypeNode('Bar'); + + return $node; + } + + return $node; + } + + }; + + yield [ + '/** @return Foo */', + '/** @return Bar */', + $changeReturnType, + ]; + + yield [ + '/** @return Foo*/', + '/** @return Bar*/', + $changeReturnType, + ]; + + yield [ + '/** +* @return Foo +* @param Foo $foo +* @param Bar $bar +*/', + '/** +* @return Bar +* @param Foo $foo +* @param Bar $bar +*/', + $changeReturnType, + ]; + + yield [ + '/** +* @param Foo $foo +* @return Foo +* @param Bar $bar +*/', + '/** +* @param Foo $foo +* @return Bar +* @param Bar $bar +*/', + $changeReturnType, + ]; + + yield [ + '/** +* @return Foo +* @param Foo $foo +* @param Bar $bar +*/', + '/** +* @return Bar +* @param Foo $foo +* @param Bar $bar +*/', + $changeReturnType, + ]; + + yield [ + '/** +* @param Foo $foo Foo description +* @return Foo Foo return description +* @param Bar $bar Bar description +*/', + '/** +* @param Foo $foo Foo description +* @return Bar Foo return description +* @param Bar $bar Bar description +*/', + $changeReturnType, + ]; + + $replaceFirst = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof PhpDocNode) { + $node->children[0] = new PhpDocTagNode('@param', new ParamTagValueNode(new IdentifierTypeNode('Baz'), false, '$a', '')); + return $node; + } + + return $node; + } + + }; + + yield [ + '/** @param Foo $foo */', + '/** @param Baz $a */', + $replaceFirst, + ]; + + yield [ + '/** + * @param Foo $foo + */', + '/** + * @param Baz $a + */', + $replaceFirst, + ]; + + yield [ + '/** + * @param Foo $foo + */', + '/** + * @param Baz $a + */', + $replaceFirst, + ]; + + $insertFirst = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof PhpDocNode) { + array_unshift($node->children, new PhpDocTagNode('@param', new ParamTagValueNode(new IdentifierTypeNode('Baz'), false, '$a', ''))); + + return $node; + } + + return $node; + } + + }; + + yield [ + '/** + * @param Foo $foo + */', + '/** + * @param Baz $a + * @param Foo $foo + */', + $insertFirst, + ]; + + yield [ + '/** + * @param Foo $foo + */', + '/** + * @param Baz $a + * @param Foo $foo + */', + $insertFirst, + ]; + + $insertSecond = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof PhpDocNode) { + array_splice($node->children, 1, 0, [ + new PhpDocTagNode('@param', new ParamTagValueNode(new IdentifierTypeNode('Baz'), false, '$a', '')), + ]); + + return $node; + } + + return $node; + } + + }; + + yield [ + '/** + * @param Foo $foo + */', + '/** + * @param Foo $foo + * @param Baz $a + */', + $insertSecond, + ]; + + yield [ + '/** + * @param Foo $foo + * @param Bar $bar + */', + '/** + * @param Foo $foo + * @param Baz $a + * @param Bar $bar + */', + $insertSecond, + ]; + + yield [ + '/** + * @param Foo $foo + * @param Bar $bar + */', + '/** + * @param Foo $foo + * @param Baz $a + * @param Bar $bar + */', + $insertSecond, + ]; + + yield [ + '/** + * @param Foo $foo + * @param Bar $bar + */', + '/** + * @param Foo $foo + * @param Baz $a + * @param Bar $bar + */', + $insertSecond, + ]; + + $replaceLast = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof PhpDocNode) { + $node->children[count($node->children) - 1] = new PhpDocTagNode('@param', new ParamTagValueNode(new IdentifierTypeNode('Baz'), false, '$a', '')); + return $node; + } + + return $node; + } + + }; + + yield [ + '/** + * @param Foo $foo + */', + '/** + * @param Baz $a + */', + $replaceLast, + ]; + + yield [ + '/** + * @param Foo $foo + * @param Bar $bar + */', + '/** + * @param Foo $foo + * @param Baz $a + */', + $replaceLast, + ]; + + $insertFirstTypeInUnionType = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof UnionTypeNode) { + array_unshift($node->types, new IdentifierTypeNode('Foo')); + } + + return $node; + } + + }; + + yield [ + '/** + * @param Bar|Baz $foo + */', + '/** + * @param Foo|Bar|Baz $foo + */', + $insertFirstTypeInUnionType, + ]; + + yield [ + '/** + * @param Bar|Baz $foo + * @param Foo $bar + */', + '/** + * @param Foo|Bar|Baz $foo + * @param Foo $bar + */', + $insertFirstTypeInUnionType, + ]; + + $replaceTypesInUnionType = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof UnionTypeNode) { + $node->types = [ + new IdentifierTypeNode('Lorem'), + new IdentifierTypeNode('Ipsum'), + ]; + } + + return $node; + } + + }; + + yield [ + '/** + * @param Foo|Bar $bar + */', + '/** + * @param Lorem|Ipsum $bar + */', + $replaceTypesInUnionType, + ]; + + $replaceParametersInCallableType = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof CallableTypeNode) { + $node->parameters = [ + new CallableTypeParameterNode(new IdentifierTypeNode('Foo'), false, false, '$foo', false), + new CallableTypeParameterNode(new IdentifierTypeNode('Bar'), false, false, '$bar', false), + ]; + } + + return $node; + } + + }; + + yield [ + '/** + * @param callable(): void $cb + */', + '/** + * @param callable(Foo $foo, Bar $bar): void $cb + */', + $replaceParametersInCallableType, + ]; + + $removeParametersInCallableType = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof CallableTypeNode) { + $node->parameters = []; + } + + return $node; + } + + }; + + yield [ + '/** + * @param callable(Foo $foo, Bar $bar): void $cb + */', + '/** + * @param callable(): void $cb + */', + $removeParametersInCallableType, + ]; + + $changeCallableTypeIdentifier = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof CallableTypeNode) { + $node->identifier = new IdentifierTypeNode('Closure'); + } + + return $node; + } + + }; + + yield [ + '/** + * @param callable(Foo $foo, Bar $bar): void $cb + * @param callable(): void $cb2 + */', + '/** + * @param Closure(Foo $foo, Bar $bar): void $cb + * @param Closure(): void $cb2 + */', + $changeCallableTypeIdentifier, + ]; + + $addItemsToArrayShape = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ArrayShapeNode) { + array_splice($node->items, 1, 0, [ + new ArrayShapeItemNode(null, false, new IdentifierTypeNode('int')), + ]); + $node->items[] = new ArrayShapeItemNode(null, false, new IdentifierTypeNode('string')); + } + + return $node; + } + + }; + + yield [ + '/** + * @return array{float} + */', + '/** + * @return array{float, int, string} + */', + $addItemsToArrayShape, + ]; + + yield [ + '/** + * @return array{float, Foo} + */', + '/** + * @return array{float, int, Foo, string} + */', + $addItemsToArrayShape, + ]; + + yield [ + '/** + * @return array{ + * float, + * Foo, + * } + */', + '/** + * @return array{ + * float, + * int, + * Foo, + * string, + * } + */', + $addItemsToArrayShape, + ]; + + yield [ + '/** + * @return array{ + * float, + * Foo + * } + */', + '/** + * @return array{ + * float, + * int, + * Foo, + * string + * } + */', + $addItemsToArrayShape, + ]; + + yield [ + '/** + * @return array{ + * float, + * Foo + * } + */', + '/** + * @return array{ + * float, + * int, + * Foo, + * string + * } + */', + $addItemsToArrayShape, + ]; + + yield [ + '/** + * @return array{ + * float, + * Foo, + * } + */', + '/** + * @return array{ + * float, + * int, + * Foo, + * string, + * } + */', + $addItemsToArrayShape, + ]; + + yield [ + '/** + * @return array{ + * float, + * Foo + * } + */', + '/** + * @return array{ + * float, + * int, + * Foo, + * string + * } + */', + $addItemsToArrayShape, + ]; + + yield [ + '/** + * @return array{ + * float, + * Foo + * } + */', + '/** + * @return array{ + * float, + * int, + * Foo, + * string + * } + */', + $addItemsToArrayShape, + ]; + + $addItemsToObjectShape = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ObjectShapeNode) { + $node->items[] = new ObjectShapeItemNode(new IdentifierTypeNode('foo'), false, new IdentifierTypeNode('int')); + } + + return $node; + } + + }; + + yield [ + '/** + * @return object{} + */', + '/** + * @return object{foo: int} + */', + $addItemsToObjectShape, + ]; + + yield [ + '/** + * @return object{bar: string} + */', + '/** + * @return object{bar: string, foo: int} + */', + $addItemsToObjectShape, + ]; + + $addItemsToConstExprArray = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ConstExprArrayNode) { + $node->items[] = new ConstExprArrayItemNode(null, new ConstExprIntegerNode('123')); + } + + return $node; + } + + }; + + yield [ + '/** @method int doFoo(array $foo = []) */', + '/** @method int doFoo(array $foo = [123]) */', + $addItemsToConstExprArray, + ]; + + yield [ + '/** @method int doFoo(array $foo = [420]) */', + '/** @method int doFoo(array $foo = [420, 123]) */', + $addItemsToConstExprArray, + ]; + + $removeKeyFromConstExprArrayItem = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ConstExprArrayNode) { + $node->items[0]->key = null; + } + + return $node; + } + + }; + + yield [ + '/** @method int doFoo(array $foo = [123 => 456]) */', + '/** @method int doFoo(array $foo = [456]) */', + $removeKeyFromConstExprArrayItem, + ]; + + $addKeyToConstExprArrayItem = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ConstExprArrayNode) { + $node->items[0]->key = new ConstExprIntegerNode('123'); + } + + return $node; + } + + }; + + yield [ + '/** @method int doFoo(array $foo = [456]) */', + '/** @method int doFoo(array $foo = [123 => 456]) */', + $addKeyToConstExprArrayItem, + ]; + + $addTemplateTagBound = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof TemplateTagValueNode) { + $node->bound = new IdentifierTypeNode('int'); + } + + return $node; + } + + }; + + yield [ + '/** @template T */', + '/** @template T of int */', + $addTemplateTagBound, + ]; + + yield [ + '/** @template T of string */', + '/** @template T of int */', + $addTemplateTagBound, + ]; + + $removeTemplateTagBound = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof TemplateTagValueNode) { + $node->bound = null; + } + + return $node; + } + + }; + + yield [ + '/** @template T of int */', + '/** @template T */', + $removeTemplateTagBound, + ]; + + $addKeyNameToArrayShapeItemNode = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ArrayShapeItemNode) { + $node->keyName = new QuoteAwareConstExprStringNode('test', QuoteAwareConstExprStringNode::SINGLE_QUOTED); + } + + return $node; + } + + }; + + yield [ + '/** @return array{Foo} */', + "/** @return array{'test': Foo} */", + $addKeyNameToArrayShapeItemNode, + ]; + + $removeKeyNameToArrayShapeItemNode = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ArrayShapeItemNode) { + $node->keyName = null; + } + + return $node; + } + + }; + + yield [ + "/** @return array{'test': Foo} */", + '/** @return array{Foo} */', + $removeKeyNameToArrayShapeItemNode, + ]; + + $changeArrayShapeKind = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ArrayShapeNode) { + $node->kind = ArrayShapeNode::KIND_LIST; + } + + return $node; + } + + }; + + yield [ + '/** @return array{Foo, Bar} */', + '/** @return list{Foo, Bar} */', + $changeArrayShapeKind, + ]; + + $changeParameterName = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ParamTagValueNode) { + $node->parameterName = '$bz'; + } + + return $node; + } + + }; + + yield [ + '/** @param int $a */', + '/** @param int $bz */', + $changeParameterName, + ]; + + yield [ + '/** + * @param int $a + */', + '/** + * @param int $bz + */', + $changeParameterName, + ]; + + yield [ + '/** + * @param int $a + * @return string + */', + '/** + * @param int $bz + * @return string + */', + $changeParameterName, + ]; + + yield [ + '/** + * @param int $a haha description + * @return string + */', + '/** + * @param int $bz haha description + * @return string + */', + $changeParameterName, + ]; + + $changeParameterDescription = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ParamTagValueNode) { + $node->description = 'hehe'; + } + + return $node; + } + + }; + + yield [ + '/** @param int $a */', + '/** @param int $a hehe */', + $changeParameterDescription, + ]; + + yield [ + '/** @param int $a haha */', + '/** @param int $a hehe */', + $changeParameterDescription, + ]; + + yield [ + '/** @param int $a */', + '/** @param int $a hehe */', + $changeParameterDescription, + ]; + + yield [ + '/** + * @param int $a haha + */', + '/** + * @param int $a hehe + */', + $changeParameterDescription, + ]; + + $changeOffsetAccess = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof OffsetAccessTypeNode) { + $node->offset = new IdentifierTypeNode('baz'); + } + + return $node; + } + + }; + + yield [ + '/** + * @param Foo[awesome] $a haha + */', + '/** + * @param Foo[baz] $a haha + */', + $changeOffsetAccess, + ]; + + $changeTypeAliasImportAs = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof TypeAliasImportTagValueNode) { + $node->importedAs = 'Ciao'; + } + + return $node; + } + + }; + + yield [ + '/** + * @phpstan-import-type TypeAlias from AnotherClass as DifferentAlias + */', + '/** + * @phpstan-import-type TypeAlias from AnotherClass as Ciao + */', + $changeTypeAliasImportAs, + ]; + + $removeImportAs = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof TypeAliasImportTagValueNode) { + $node->importedAs = null; + } + + return $node; + } + + }; + + yield [ + '/** + * @phpstan-import-type TypeAlias from AnotherClass as DifferentAlias + */', + '/** + * @phpstan-import-type TypeAlias from AnotherClass + */', + $removeImportAs, + ]; + + $addMethodTemplateType = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof MethodTagValueNode) { + $node->templateTypes[] = new TemplateTagValueNode( + 'T', + new IdentifierTypeNode('int'), + '' + ); + } + + return $node; + } + + }; + + yield [ + '/** @method int doFoo() */', + '/** @method int doFoo() */', + $addMethodTemplateType, + ]; + + yield [ + '/** @method int doFoo() */', + '/** @method int doFoo() */', + $addMethodTemplateType, + ]; + } + + /** + * @dataProvider dataPrintFormatPreserving + */ + public function testPrintFormatPreserving(string $phpDoc, string $expectedResult, NodeVisitor $visitor): void + { + $usedAttributes = ['lines' => true, 'indexes' => true]; + $constExprParser = new ConstExprParser(true, true, $usedAttributes); + $phpDocParser = new PhpDocParser( + new TypeParser($constExprParser, true, $usedAttributes), + $constExprParser, + true, + true, + $usedAttributes + ); + $lexer = new Lexer(); + $tokens = new TokenIterator($lexer->tokenize($phpDoc)); + $phpDocNode = $phpDocParser->parse($tokens); + $cloningTraverser = new NodeTraverser([new NodeVisitor\CloningVisitor()]); + $newNodes = $cloningTraverser->traverse([$phpDocNode]); + + $changingTraverser = new NodeTraverser([$visitor]); + + /** @var PhpDocNode $newNode */ + [$newNode] = $changingTraverser->traverse($newNodes); + + $printer = new Printer(); + $this->assertSame($expectedResult, $printer->printFormatPreserving($newNode, $phpDocNode, $tokens)); + } + +} diff --git a/tests/PHPStan/Printer/data/printer-1-spaces-after.php b/tests/PHPStan/Printer/data/printer-1-spaces-after.php new file mode 100644 index 00000000..953b08c0 --- /dev/null +++ b/tests/PHPStan/Printer/data/printer-1-spaces-after.php @@ -0,0 +1,26 @@ + Date: Thu, 20 Apr 2023 16:55:49 +0200 Subject: [PATCH 57/59] ConstExprParser: support numeric literal separator --- doc/grammars/type.abnf | 16 ++++++++-------- src/Lexer/Lexer.php | 4 ++-- src/Parser/ConstExprParser.php | 5 +++-- src/Parser/TypeParser.php | 3 ++- tests/PHPStan/Parser/ConstExprParserTest.php | 20 ++++++++++++++++++++ tests/PHPStan/Parser/TypeParserTest.php | 17 +++++++++++++++++ 6 files changed, 52 insertions(+), 13 deletions(-) diff --git a/doc/grammars/type.abnf b/doc/grammars/type.abnf index f00a191c..f4bd3c6a 100644 --- a/doc/grammars/type.abnf +++ b/doc/grammars/type.abnf @@ -85,18 +85,18 @@ ConstantExpr / ConstantFetch *ByteHorizontalWs ConstantFloat - = ["-"] 1*ByteDecDigit "." *ByteDecDigit [ConstantFloatExp] - / ["-"] 1*ByteDecDigit ConstantFloatExp - / ["-"] "." 1*ByteDecDigit [ConstantFloatExp] + = ["-"] 1*ByteDecDigit *("_" 1*ByteDecDigit) "." [1*ByteDecDigit *("_" 1*ByteDecDigit)] [ConstantFloatExp] + / ["-"] 1*ByteDecDigit *("_" 1*ByteDecDigit) ConstantFloatExp + / ["-"] "." 1*ByteDecDigit *("_" 1*ByteDecDigit) [ConstantFloatExp] ConstantFloatExp - = "e" ["-"] 1*ByteDecDigit + = "e" ["-"] 1*ByteDecDigit *("_" 1*ByteDecDigit) ConstantInt - = ["-"] "0b" 1*ByteBinDigit - / ["-"] "0o" 1*ByteOctDigit - / ["-"] "0x" 1*ByteHexDigit - / ["-"] 1*ByteDecDigit + = ["-"] "0b" 1*ByteBinDigit *("_" 1*ByteBinDigit) + / ["-"] "0o" 1*ByteOctDigit *("_" 1*ByteOctDigit) + / ["-"] "0x" 1*ByteHexDigit *("_" 1*ByteHexDigit) + / ["-"] 1*ByteDecDigit *("_" 1*ByteDecDigit) ConstantTrue = "true" diff --git a/src/Lexer/Lexer.php b/src/Lexer/Lexer.php index 90d6b500..ccae6bef 100644 --- a/src/Lexer/Lexer.php +++ b/src/Lexer/Lexer.php @@ -160,8 +160,8 @@ private function generateRegexp(): string self::TOKEN_PHPDOC_TAG => '@(?:[a-z][a-z0-9-\\\\]+:)?[a-z][a-z0-9-\\\\]*+', self::TOKEN_PHPDOC_EOL => '\\r?+\\n[\\x09\\x20]*+(?:\\*(?!/)\\x20?+)?', - self::TOKEN_FLOAT => '(?:-?[0-9]++\\.[0-9]*+(?:e-?[0-9]++)?)|(?:-?[0-9]*+\\.[0-9]++(?:e-?[0-9]++)?)|(?:-?[0-9]++e-?[0-9]++)', - self::TOKEN_INTEGER => '-?(?:(?:0b[0-1]++)|(?:0o[0-7]++)|(?:0x[0-9a-f]++)|(?:[0-9]++))', + self::TOKEN_FLOAT => '(?:-?[0-9]++(_[0-9]++)*\\.[0-9]*(_[0-9]++)*+(?:e-?[0-9]++(_[0-9]++)*)?)|(?:-?[0-9]*+(_[0-9]++)*\\.[0-9]++(_[0-9]++)*(?:e-?[0-9]++(_[0-9]++)*)?)|(?:-?[0-9]++(_[0-9]++)*e-?[0-9]++(_[0-9]++)*)', + self::TOKEN_INTEGER => '-?(?:(?:0b[0-1]++(_[0-1]++)*)|(?:0o[0-7]++(_[0-7]++)*)|(?:0x[0-9a-f]++(_[0-9a-f]++)*)|(?:[0-9]++(_[0-9]++)*))', self::TOKEN_SINGLE_QUOTED_STRING => '\'(?:\\\\[^\\r\\n]|[^\'\\r\\n\\\\])*+\'', self::TOKEN_DOUBLE_QUOTED_STRING => '"(?:\\\\[^\\r\\n]|[^"\\r\\n\\\\])*+"', diff --git a/src/Parser/ConstExprParser.php b/src/Parser/ConstExprParser.php index cc05ee3b..b6db8a2c 100644 --- a/src/Parser/ConstExprParser.php +++ b/src/Parser/ConstExprParser.php @@ -4,6 +4,7 @@ use PHPStan\PhpDocParser\Ast; use PHPStan\PhpDocParser\Lexer\Lexer; +use function str_replace; use function strtolower; use function substr; @@ -47,7 +48,7 @@ public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\Con return $this->enrichWithAttributes( $tokens, - new Ast\ConstExpr\ConstExprFloatNode($value), + new Ast\ConstExpr\ConstExprFloatNode(str_replace('_', '', $value)), $startLine, $startIndex ); @@ -59,7 +60,7 @@ public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\Con return $this->enrichWithAttributes( $tokens, - new Ast\ConstExpr\ConstExprIntegerNode($value), + new Ast\ConstExpr\ConstExprIntegerNode(str_replace('_', '', $value)), $startLine, $startIndex ); diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index d59d0f1f..ff62fca3 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -6,6 +6,7 @@ use PHPStan\PhpDocParser\Ast; use PHPStan\PhpDocParser\Lexer\Lexer; use function in_array; +use function str_replace; use function strpos; use function trim; @@ -710,7 +711,7 @@ private function parseArrayShapeKey(TokenIterator $tokens) $startLine = $tokens->currentTokenLine(); if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) { - $key = new Ast\ConstExpr\ConstExprIntegerNode($tokens->currentTokenValue()); + $key = new Ast\ConstExpr\ConstExprIntegerNode(str_replace('_', '', $tokens->currentTokenValue())); $tokens->next(); } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) { diff --git a/tests/PHPStan/Parser/ConstExprParserTest.php b/tests/PHPStan/Parser/ConstExprParserTest.php index 1fac87d9..a8fb1b52 100644 --- a/tests/PHPStan/Parser/ConstExprParserTest.php +++ b/tests/PHPStan/Parser/ConstExprParserTest.php @@ -176,6 +176,21 @@ public function provideIntegerNodeParseData(): Iterator '-0X7Fb4', new ConstExprIntegerNode('-0X7Fb4'), ]; + + yield [ + '123_456', + new ConstExprIntegerNode('123456'), + ]; + + yield [ + '0b01_01_01', + new ConstExprIntegerNode('0b010101'), + ]; + + yield [ + '-0X7_Fb_4', + new ConstExprIntegerNode('-0X7Fb4'), + ]; } @@ -240,6 +255,11 @@ public function provideFloatNodeParseData(): Iterator '-12.3e-4', new ConstExprFloatNode('-12.3e-4'), ]; + + yield [ + '-1_2.3_4e5_6', + new ConstExprFloatNode('-12.34e56'), + ]; } diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 09edabb3..2af4e1d4 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -1035,10 +1035,27 @@ public function provideParseData(): array '123', new ConstTypeNode(new ConstExprIntegerNode('123')), ], + [ + '123_456', + new ConstTypeNode(new ConstExprIntegerNode('123456')), + ], + [ + '_123', + new IdentifierTypeNode('_123'), + ], + [ + '123_', + new ConstTypeNode(new ConstExprIntegerNode('123')), + Lexer::TOKEN_IDENTIFIER, + ], [ '123.2', new ConstTypeNode(new ConstExprFloatNode('123.2')), ], + [ + '123_456.789_012', + new ConstTypeNode(new ConstExprFloatNode('123456.789012')), + ], [ '"bar"', new ConstTypeNode(new QuoteAwareConstExprStringNode('bar', QuoteAwareConstExprStringNode::DOUBLE_QUOTED)), From cf694fda69b63cfa5ee61e9e657b6ab7efdf5229 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 28 Apr 2023 09:50:38 +0200 Subject: [PATCH 58/59] PrinterTest - parse the new printed node again and verify the AST is the same --- tests/PHPStan/Printer/PrinterTest.php | 33 ++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index 69acfc55..b689b440 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -3,6 +3,7 @@ namespace PHPStan\PhpDocParser\Printer; use PHPStan\PhpDocParser\Ast\AbstractNodeVisitor; +use PHPStan\PhpDocParser\Ast\Attribute; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayItemNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; @@ -1173,7 +1174,37 @@ public function testPrintFormatPreserving(string $phpDoc, string $expectedResult [$newNode] = $changingTraverser->traverse($newNodes); $printer = new Printer(); - $this->assertSame($expectedResult, $printer->printFormatPreserving($newNode, $phpDocNode, $tokens)); + $newPhpDoc = $printer->printFormatPreserving($newNode, $phpDocNode, $tokens); + $this->assertSame($expectedResult, $newPhpDoc); + + $newTokens = new TokenIterator($lexer->tokenize($newPhpDoc)); + $this->assertEquals( + $this->unsetAttributes($newNode), + $this->unsetAttributes($phpDocParser->parse($newTokens)) + ); + } + + private function unsetAttributes(PhpDocNode $node): PhpDocNode + { + $visitor = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + $node->setAttribute(Attribute::START_LINE, null); + $node->setAttribute(Attribute::END_LINE, null); + $node->setAttribute(Attribute::START_INDEX, null); + $node->setAttribute(Attribute::END_INDEX, null); + $node->setAttribute(Attribute::ORIGINAL_NODE, null); + + return $node; + } + + }; + + $traverser = new NodeTraverser([$visitor]); + + /** @var PhpDocNode */ + return $traverser->traverse([$node])[0]; } } From 31a38be2912b6eded9e9c6967f0c72a3f3310c34 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 02:00:04 +0000 Subject: [PATCH 59/59] Update dependency slevomat/coding-standard to v8.11.1 --- build-cs/composer.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index 0cea3f9e..c59ffc5f 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -154,16 +154,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.20.2", + "version": "1.20.3", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "90490bd8fd8530a272043c4950c180b6d0cf5f81" + "reference": "6c04009f6cae6eda2f040745b6b846080ef069c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/90490bd8fd8530a272043c4950c180b6d0cf5f81", - "reference": "90490bd8fd8530a272043c4950c180b6d0cf5f81", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/6c04009f6cae6eda2f040745b6b846080ef069c2", + "reference": "6c04009f6cae6eda2f040745b6b846080ef069c2", "shasum": "" }, "require": { @@ -193,22 +193,22 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.20.2" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.20.3" }, - "time": "2023-04-22T12:59:35+00:00" + "time": "2023-04-25T09:01:03+00:00" }, { "name": "slevomat/coding-standard", - "version": "8.11.0", + "version": "8.11.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "91428d5bcf7db93a842bcf97f465edf62527f3ea" + "reference": "af87461316b257e46e15bb041dca6fca3796d822" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/91428d5bcf7db93a842bcf97f465edf62527f3ea", - "reference": "91428d5bcf7db93a842bcf97f465edf62527f3ea", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/af87461316b257e46e15bb041dca6fca3796d822", + "reference": "af87461316b257e46e15bb041dca6fca3796d822", "shasum": "" }, "require": { @@ -248,7 +248,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.11.0" + "source": "https://github.com/slevomat/coding-standard/tree/8.11.1" }, "funding": [ { @@ -260,7 +260,7 @@ "type": "tidelift" } ], - "time": "2023-04-21T15:51:44+00:00" + "time": "2023-04-24T08:19:01+00:00" }, { "name": "squizlabs/php_codesniffer",