Skip to content

Commit aaa362a

Browse files
authored
Merge pull request #348 from Harfusha/cake4
Backported support for more complex array annotations (#344) and for multiline annotation (#345) to Cake4.
2 parents f4c3c8e + e212e9f commit aaa362a

File tree

9 files changed

+292
-33
lines changed

9 files changed

+292
-33
lines changed

config/app.example.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,6 @@
5151
],
5252
// If a custom directory should be used, defaults to TMP otherwise
5353
'codeCompletionPath' => null,
54+
'codeCompletionReturnType' => null, // Auto-detect based on controller/component, set to true/false to force one mode.
5455
],
5556
];

src/Annotator/AbstractAnnotator.php

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -485,14 +485,41 @@ protected function parseExistingAnnotations(File $file, int $closeTagIndex, arra
485485
/** @var \PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode|\PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode $valueNode */
486486
$valueNode = static::getValueNode($tokens[$i]['content'], $content);
487487
if ($valueNode instanceof InvalidTagValueNode) {
488-
continue;
488+
$multilineFixed = false;
489+
for ($p = $i + 3; $p < $closeTagIndex; $p++) {
490+
if ($tokens[$p]['type'] === 'T_DOC_COMMENT_TAG') {
491+
break;
492+
}
493+
494+
if ($tokens[$p]['type'] !== 'T_DOC_COMMENT_STRING') {
495+
continue;
496+
}
497+
498+
$content .= $tokens[$p]['content'];
499+
/** @var \PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode|\PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode $valueNode */
500+
$valueNode = static::getValueNode($tokens[$i]['content'], $content);
501+
if (!($valueNode instanceof InvalidTagValueNode)) {
502+
$multilineFixed = true;
503+
504+
break;
505+
}
506+
}
507+
508+
if (!$multilineFixed || $valueNode instanceof InvalidTagValueNode) {
509+
continue;
510+
}
489511
}
490512

491513
$returnTypes = $this->valueNodeParts($valueNode);
492514
$typeString = $this->renderUnionTypes($returnTypes);
493515

494516
$tag = $tokens[$i]['content'];
495-
$content = mb_substr($content, mb_strlen($typeString) + 1);
517+
$variablePos = strpos($content, ' $');
518+
if ($tag === VariableAnnotation::TAG && $variablePos) {
519+
$content = mb_substr($content, $variablePos + 1);
520+
} else {
521+
$content = mb_substr($content, mb_strlen($typeString) + 1);
522+
}
496523

497524
$annotation = AnnotationFactory::createOrFail($tag, $typeString, $content, $classNameIndex);
498525
if ($this->getConfig(static::CONFIG_REMOVE) && $tag === VariableAnnotation::TAG && $this->varInUse($tokens, $closeTagIndex, $content)) {

src/CodeCompletion/Task/ControllerEventsTask.php

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace IdeHelper\CodeCompletion\Task;
44

5+
use Cake\Core\Configure;
6+
57
class ControllerEventsTask implements TaskInterface {
68

79
/**
@@ -20,43 +22,73 @@ public function type(): string {
2022
* @return string
2123
*/
2224
public function create(): string {
23-
$events = <<<'TXT'
24-
public function startup(EventInterface $event) {
25+
/** @var bool|null $returnType */
26+
$returnType = Configure::read('IdeHelper.codeCompletionReturnType');
27+
28+
$controllerEvents = $this->events($returnType ?? false);
29+
$componentEvents = $this->events($returnType ?? true);
30+
31+
return <<<CODE
32+
33+
use Cake\Event\EventInterface;
34+
use Cake\Http\Response;
35+
36+
if (false) {
37+
class Controller {
38+
$controllerEvents
39+
}
40+
41+
class Component {
42+
$componentEvents
43+
}
44+
}
45+
46+
CODE;
47+
}
48+
49+
/**
50+
* @param bool $returnType
51+
*
52+
* @return string
53+
*/
54+
protected function events(bool $returnType): string {
55+
$type = null;
56+
$docBlock = null;
57+
if ($returnType) {
58+
$type = ': ' . '\Cake\Http\Response|null';
59+
} else {
60+
$docBlock = <<<TXT
61+
/**
62+
* @param \Cake\Event\EventInterface \$event
63+
*
64+
* @return \Cake\Http\Response|null|void
65+
*/
66+
TXT;
67+
$docBlock = trim($docBlock) . PHP_EOL . str_repeat("\t", 2);
68+
}
69+
70+
$events = <<<TXT
71+
{$docBlock}public function startup(EventInterface \$event)$type {
2572
return null;
2673
}
27-
public function beforeFilter(EventInterface $event) {
74+
{$docBlock}public function beforeFilter(EventInterface \$event)$type {
2875
return null;
2976
}
30-
public function beforeRender(EventInterface $event) {
77+
{$docBlock}public function beforeRender(EventInterface \$event)$type {
3178
return null;
3279
}
33-
public function afterFilter(EventInterface $event) {
80+
{$docBlock}public function afterFilter(EventInterface \$event)$type {
3481
return null;
3582
}
36-
public function shutdown(EventInterface $event) {
83+
{$docBlock}public function shutdown(EventInterface \$event)$type {
3784
return null;
3885
}
39-
public function beforeRedirect(EventInterface $event, $url, Response $response) {
86+
{$docBlock}public function beforeRedirect(EventInterface \$event, \$url, Response \$response)$type {
4087
return null;
4188
}
4289
TXT;
4390

44-
return <<<CODE
45-
46-
use Cake\Event\EventInterface;
47-
use Cake\Http\Response;
48-
49-
if (false) {
50-
abstract class Controller {
51-
$events
52-
}
53-
54-
abstract class Component {
55-
$events
56-
}
57-
}
58-
59-
CODE;
91+
return $events;
6092
}
6193

6294
}

tests/TestCase/Annotator/TemplateAnnotatorTest.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,60 @@ public function testAnnotateWithFollowingInline() {
420420
$this->assertTextContains(' -> 1 annotation added.', $output);
421421
}
422422

423+
/**
424+
* Tests that a docblock with arrays in different types, e.g. shape.
425+
*
426+
* @return void
427+
*/
428+
public function testAnnotateWithShapedArray() {
429+
$annotator = $this->_getAnnotatorMock([]);
430+
431+
$expectedContent = str_replace("\r\n", "\n", file_get_contents(TEST_FILES . 'templates/array.php'));
432+
$callback = function($value) use ($expectedContent) {
433+
$value = str_replace(["\r\n", "\r"], "\n", $value);
434+
if ($value !== $expectedContent) {
435+
$this->_displayDiff($expectedContent, $value);
436+
}
437+
438+
return $value === $expectedContent;
439+
};
440+
$annotator->expects($this->once())->method('storeFile')->with($this->anything(), $this->callback($callback));
441+
442+
$path = TEST_ROOT . 'templates/Foos/array.php';
443+
$annotator->annotate($path);
444+
445+
$output = $this->out->output();
446+
447+
$this->assertTextContains(' -> 1 annotation added.', $output);
448+
}
449+
450+
/**
451+
* Tests that a multiline array is parsed completly.
452+
*
453+
* @return void
454+
*/
455+
public function testAnnotateWithMultilineArray() {
456+
$annotator = $this->_getAnnotatorMock([]);
457+
458+
$expectedContent = str_replace("\r\n", "\n", file_get_contents(TEST_FILES . 'templates/multiline.php'));
459+
$callback = function($value) use ($expectedContent) {
460+
$value = str_replace(["\r\n", "\r"], "\n", $value);
461+
if ($value !== $expectedContent) {
462+
$this->_displayDiff($expectedContent, $value);
463+
}
464+
465+
return $value === $expectedContent;
466+
};
467+
$annotator->expects($this->once())->method('storeFile')->with($this->anything(), $this->callback($callback));
468+
469+
$path = TEST_ROOT . 'templates/Foos/multiline.php';
470+
$annotator->annotate($path);
471+
472+
$output = $this->out->output();
473+
474+
$this->assertTextContains(' -> 1 annotation added.', $output);
475+
}
476+
423477
/**
424478
* @param array $params
425479
* @return \IdeHelper\Annotator\TemplateAnnotator|\PHPUnit\Framework\MockObject\MockObject

tests/TestCase/CodeCompletion/Task/ControllerEventsTaskTest.php

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,44 +33,74 @@ public function testCollect() {
3333
use Cake\Http\Response;
3434
3535
if (false) {
36-
abstract class Controller {
36+
class Controller {
37+
/**
38+
* @param \Cake\Event\EventInterface $event
39+
*
40+
* @return \Cake\Http\Response|null|void
41+
*/
3742
public function startup(EventInterface $event) {
3843
return null;
3944
}
45+
/**
46+
* @param \Cake\Event\EventInterface $event
47+
*
48+
* @return \Cake\Http\Response|null|void
49+
*/
4050
public function beforeFilter(EventInterface $event) {
4151
return null;
4252
}
53+
/**
54+
* @param \Cake\Event\EventInterface $event
55+
*
56+
* @return \Cake\Http\Response|null|void
57+
*/
4358
public function beforeRender(EventInterface $event) {
4459
return null;
4560
}
61+
/**
62+
* @param \Cake\Event\EventInterface $event
63+
*
64+
* @return \Cake\Http\Response|null|void
65+
*/
4666
public function afterFilter(EventInterface $event) {
4767
return null;
4868
}
69+
/**
70+
* @param \Cake\Event\EventInterface $event
71+
*
72+
* @return \Cake\Http\Response|null|void
73+
*/
4974
public function shutdown(EventInterface $event) {
5075
return null;
5176
}
77+
/**
78+
* @param \Cake\Event\EventInterface $event
79+
*
80+
* @return \Cake\Http\Response|null|void
81+
*/
5282
public function beforeRedirect(EventInterface $event, $url, Response $response) {
5383
return null;
5484
}
5585
}
5686
57-
abstract class Component {
58-
public function startup(EventInterface $event) {
87+
class Component {
88+
public function startup(EventInterface $event): \Cake\Http\Response|null {
5989
return null;
6090
}
61-
public function beforeFilter(EventInterface $event) {
91+
public function beforeFilter(EventInterface $event): \Cake\Http\Response|null {
6292
return null;
6393
}
64-
public function beforeRender(EventInterface $event) {
94+
public function beforeRender(EventInterface $event): \Cake\Http\Response|null {
6595
return null;
6696
}
67-
public function afterFilter(EventInterface $event) {
97+
public function afterFilter(EventInterface $event): \Cake\Http\Response|null {
6898
return null;
6999
}
70-
public function shutdown(EventInterface $event) {
100+
public function shutdown(EventInterface $event): \Cake\Http\Response|null {
71101
return null;
72102
}
73-
public function beforeRedirect(EventInterface $event, $url, Response $response) {
103+
public function beforeRedirect(EventInterface $event, $url, Response $response): \Cake\Http\Response|null {
74104
return null;
75105
}
76106
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
/**
3+
* @var \TestApp\View\AppView $this
4+
* @var array $x
5+
* @var array<int> $ints
6+
* @var array{a: int, b: string|null}|null $shaped
7+
*/
8+
foreach ($x as $y) {
9+
echo $y;
10+
}
11+
foreach ($foo as $int) {
12+
echo $int;
13+
}
14+
?>
15+
<div>
16+
<?php foreach ($ints as $int) {
17+
echo $int;
18+
} ?>
19+
<?php foreach ($shaped as $x) {
20+
echo h($x);
21+
} ?>
22+
</div>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
/**
3+
* @var \TestApp\View\AppView $this
4+
* @var array $x
5+
* @var array<int> $ints
6+
* @var array{
7+
* a: int,
8+
* b: string|null
9+
* }|null $shaped
10+
* @var array{
11+
* c: array{
12+
* d: int|string,
13+
* e: string|null
14+
* }
15+
* } $nested
16+
*/
17+
foreach ($x as $y) {
18+
echo $y;
19+
}
20+
foreach ($foo as $int) {
21+
echo $int;
22+
}
23+
?>
24+
<div>
25+
<?php foreach ($ints as $int) {
26+
echo $int;
27+
} ?>
28+
<?php foreach ($shaped as $x) {
29+
echo h($x);
30+
} ?>
31+
<?php foreach ($nested['c'] as $subArray) {
32+
echo h($x);
33+
} ?>
34+
</div>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
/**
3+
* @var \TestApp\View\AppView $this
4+
* @var array $x
5+
* @var array<int> $ints
6+
* @var array{a: int, b: string|null}|null $shaped
7+
* @var mixed $foo
8+
*/
9+
foreach ($x as $y) {
10+
echo $y;
11+
}
12+
foreach ($foo as $int) {
13+
echo $int;
14+
}
15+
?>
16+
<div>
17+
<?php foreach ($ints as $int) {
18+
echo $int;
19+
} ?>
20+
<?php foreach ($shaped as $x) {
21+
echo h($x);
22+
} ?>
23+
</div>

0 commit comments

Comments
 (0)