From e212e9f9bdb0712edd5164b2522c4abddfa1ae5f Mon Sep 17 00:00:00 2001 From: Adam Halfar Date: Tue, 5 Mar 2024 15:14:09 +0100 Subject: [PATCH] Backport support for more complex array annotations (#344) and for multiline annotation (#345) to Cake4. --- config/app.example.php | 1 + src/Annotator/AbstractAnnotator.php | 31 +++++++- .../Task/ControllerEventsTask.php | 78 +++++++++++++------ .../Annotator/TemplateAnnotatorTest.php | 54 +++++++++++++ .../Task/ControllerEventsTaskTest.php | 46 +++++++++-- tests/test_app/templates/Foos/array.php | 22 ++++++ tests/test_app/templates/Foos/multiline.php | 34 ++++++++ tests/test_files/templates/array.php | 23 ++++++ tests/test_files/templates/multiline.php | 36 +++++++++ 9 files changed, 292 insertions(+), 33 deletions(-) create mode 100644 tests/test_app/templates/Foos/array.php create mode 100644 tests/test_app/templates/Foos/multiline.php create mode 100644 tests/test_files/templates/array.php create mode 100644 tests/test_files/templates/multiline.php diff --git a/config/app.example.php b/config/app.example.php index 58d87d1c..7bc80543 100644 --- a/config/app.example.php +++ b/config/app.example.php @@ -51,5 +51,6 @@ ], // If a custom directory should be used, defaults to TMP otherwise 'codeCompletionPath' => null, + 'codeCompletionReturnType' => null, // Auto-detect based on controller/component, set to true/false to force one mode. ], ]; diff --git a/src/Annotator/AbstractAnnotator.php b/src/Annotator/AbstractAnnotator.php index 0587003b..83e88bf8 100644 --- a/src/Annotator/AbstractAnnotator.php +++ b/src/Annotator/AbstractAnnotator.php @@ -485,14 +485,41 @@ protected function parseExistingAnnotations(File $file, int $closeTagIndex, arra /** @var \PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode|\PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode $valueNode */ $valueNode = static::getValueNode($tokens[$i]['content'], $content); if ($valueNode instanceof InvalidTagValueNode) { - continue; + $multilineFixed = false; + for ($p = $i + 3; $p < $closeTagIndex; $p++) { + if ($tokens[$p]['type'] === 'T_DOC_COMMENT_TAG') { + break; + } + + if ($tokens[$p]['type'] !== 'T_DOC_COMMENT_STRING') { + continue; + } + + $content .= $tokens[$p]['content']; + /** @var \PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode|\PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode $valueNode */ + $valueNode = static::getValueNode($tokens[$i]['content'], $content); + if (!($valueNode instanceof InvalidTagValueNode)) { + $multilineFixed = true; + + break; + } + } + + if (!$multilineFixed || $valueNode instanceof InvalidTagValueNode) { + continue; + } } $returnTypes = $this->valueNodeParts($valueNode); $typeString = $this->renderUnionTypes($returnTypes); $tag = $tokens[$i]['content']; - $content = mb_substr($content, mb_strlen($typeString) + 1); + $variablePos = strpos($content, ' $'); + if ($tag === VariableAnnotation::TAG && $variablePos) { + $content = mb_substr($content, $variablePos + 1); + } else { + $content = mb_substr($content, mb_strlen($typeString) + 1); + } $annotation = AnnotationFactory::createOrFail($tag, $typeString, $content, $classNameIndex); if ($this->getConfig(static::CONFIG_REMOVE) && $tag === VariableAnnotation::TAG && $this->varInUse($tokens, $closeTagIndex, $content)) { diff --git a/src/CodeCompletion/Task/ControllerEventsTask.php b/src/CodeCompletion/Task/ControllerEventsTask.php index c9087069..d86388d3 100644 --- a/src/CodeCompletion/Task/ControllerEventsTask.php +++ b/src/CodeCompletion/Task/ControllerEventsTask.php @@ -2,6 +2,8 @@ namespace IdeHelper\CodeCompletion\Task; +use Cake\Core\Configure; + class ControllerEventsTask implements TaskInterface { /** @@ -20,43 +22,73 @@ public function type(): string { * @return string */ public function create(): string { - $events = <<<'TXT' - public function startup(EventInterface $event) { + /** @var bool|null $returnType */ + $returnType = Configure::read('IdeHelper.codeCompletionReturnType'); + + $controllerEvents = $this->events($returnType ?? false); + $componentEvents = $this->events($returnType ?? true); + + return <<assertTextContains(' -> 1 annotation added.', $output); } + /** + * Tests that a docblock with arrays in different types, e.g. shape. + * + * @return void + */ + public function testAnnotateWithShapedArray() { + $annotator = $this->_getAnnotatorMock([]); + + $expectedContent = str_replace("\r\n", "\n", file_get_contents(TEST_FILES . 'templates/array.php')); + $callback = function($value) use ($expectedContent) { + $value = str_replace(["\r\n", "\r"], "\n", $value); + if ($value !== $expectedContent) { + $this->_displayDiff($expectedContent, $value); + } + + return $value === $expectedContent; + }; + $annotator->expects($this->once())->method('storeFile')->with($this->anything(), $this->callback($callback)); + + $path = TEST_ROOT . 'templates/Foos/array.php'; + $annotator->annotate($path); + + $output = $this->out->output(); + + $this->assertTextContains(' -> 1 annotation added.', $output); + } + + /** + * Tests that a multiline array is parsed completly. + * + * @return void + */ + public function testAnnotateWithMultilineArray() { + $annotator = $this->_getAnnotatorMock([]); + + $expectedContent = str_replace("\r\n", "\n", file_get_contents(TEST_FILES . 'templates/multiline.php')); + $callback = function($value) use ($expectedContent) { + $value = str_replace(["\r\n", "\r"], "\n", $value); + if ($value !== $expectedContent) { + $this->_displayDiff($expectedContent, $value); + } + + return $value === $expectedContent; + }; + $annotator->expects($this->once())->method('storeFile')->with($this->anything(), $this->callback($callback)); + + $path = TEST_ROOT . 'templates/Foos/multiline.php'; + $annotator->annotate($path); + + $output = $this->out->output(); + + $this->assertTextContains(' -> 1 annotation added.', $output); + } + /** * @param array $params * @return \IdeHelper\Annotator\TemplateAnnotator|\PHPUnit\Framework\MockObject\MockObject diff --git a/tests/TestCase/CodeCompletion/Task/ControllerEventsTaskTest.php b/tests/TestCase/CodeCompletion/Task/ControllerEventsTaskTest.php index 5e70c2be..762b4e27 100644 --- a/tests/TestCase/CodeCompletion/Task/ControllerEventsTaskTest.php +++ b/tests/TestCase/CodeCompletion/Task/ControllerEventsTaskTest.php @@ -33,44 +33,74 @@ public function testCollect() { use Cake\Http\Response; if (false) { - abstract class Controller { + class Controller { + /** + * @param \Cake\Event\EventInterface $event + * + * @return \Cake\Http\Response|null|void + */ public function startup(EventInterface $event) { return null; } + /** + * @param \Cake\Event\EventInterface $event + * + * @return \Cake\Http\Response|null|void + */ public function beforeFilter(EventInterface $event) { return null; } + /** + * @param \Cake\Event\EventInterface $event + * + * @return \Cake\Http\Response|null|void + */ public function beforeRender(EventInterface $event) { return null; } + /** + * @param \Cake\Event\EventInterface $event + * + * @return \Cake\Http\Response|null|void + */ public function afterFilter(EventInterface $event) { return null; } + /** + * @param \Cake\Event\EventInterface $event + * + * @return \Cake\Http\Response|null|void + */ public function shutdown(EventInterface $event) { return null; } + /** + * @param \Cake\Event\EventInterface $event + * + * @return \Cake\Http\Response|null|void + */ public function beforeRedirect(EventInterface $event, $url, Response $response) { return null; } } - abstract class Component { - public function startup(EventInterface $event) { + class Component { + public function startup(EventInterface $event): \Cake\Http\Response|null { return null; } - public function beforeFilter(EventInterface $event) { + public function beforeFilter(EventInterface $event): \Cake\Http\Response|null { return null; } - public function beforeRender(EventInterface $event) { + public function beforeRender(EventInterface $event): \Cake\Http\Response|null { return null; } - public function afterFilter(EventInterface $event) { + public function afterFilter(EventInterface $event): \Cake\Http\Response|null { return null; } - public function shutdown(EventInterface $event) { + public function shutdown(EventInterface $event): \Cake\Http\Response|null { return null; } - public function beforeRedirect(EventInterface $event, $url, Response $response) { + public function beforeRedirect(EventInterface $event, $url, Response $response): \Cake\Http\Response|null { return null; } } diff --git a/tests/test_app/templates/Foos/array.php b/tests/test_app/templates/Foos/array.php new file mode 100644 index 00000000..27decc7a --- /dev/null +++ b/tests/test_app/templates/Foos/array.php @@ -0,0 +1,22 @@ + $ints + * @var array{a: int, b: string|null}|null $shaped + */ + foreach ($x as $y) { + echo $y; + } + foreach ($foo as $int) { + echo $int; + } +?> +
+ + +
diff --git a/tests/test_app/templates/Foos/multiline.php b/tests/test_app/templates/Foos/multiline.php new file mode 100644 index 00000000..ad5bf042 --- /dev/null +++ b/tests/test_app/templates/Foos/multiline.php @@ -0,0 +1,34 @@ + $ints + * @var array{ + * a: int, + * b: string|null + * }|null $shaped + * @var array{ + * c: array{ + * d: int|string, + * e: string|null + * } + * } $nested + */ + foreach ($x as $y) { + echo $y; + } + foreach ($foo as $int) { + echo $int; + } +?> +
+ + + +
diff --git a/tests/test_files/templates/array.php b/tests/test_files/templates/array.php new file mode 100644 index 00000000..3e739997 --- /dev/null +++ b/tests/test_files/templates/array.php @@ -0,0 +1,23 @@ + $ints + * @var array{a: int, b: string|null}|null $shaped + * @var mixed $foo + */ + foreach ($x as $y) { + echo $y; + } + foreach ($foo as $int) { + echo $int; + } +?> +
+ + +
diff --git a/tests/test_files/templates/multiline.php b/tests/test_files/templates/multiline.php new file mode 100644 index 00000000..e2dd4473 --- /dev/null +++ b/tests/test_files/templates/multiline.php @@ -0,0 +1,36 @@ + $ints + * @var array{ + * a: int, + * b: string|null + * }|null $shaped + * @var array{ + * c: array{ + * d: int|string, + * e: string|null + * } + * } $nested + * + * @var mixed $foo + */ + foreach ($x as $y) { + echo $y; + } + foreach ($foo as $int) { + echo $int; + } +?> +
+ + + +