Skip to content

Commit eeb9090

Browse files
authored
support all magic methods (#6)
1 parent 0cfc76d commit eeb9090

13 files changed

Lines changed: 376 additions & 29 deletions

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ but suggests typehint everything possible.
1010
- Who works projects using PHP 7.1 and higher.
1111
- Who doesn't want to point out missing type hint and return type declarations in code review process
1212
by using it as part of CI pipeline.
13-
- Who love strict typing
13+
- Who love strict typing and defensive programming.
1414

1515
#### Features
1616

1717
- Respects phpdoc; there are some rare cases mixed or compound types are needed.
1818
If such cases documented in phpdoc, `typhp` doesn't complain. For example: `@return array|bool`, `@param mixed $foo`, etc.
19+
- Takes [magic methods](https://www.php.net/manual/en/language.oop5.magic.php) into account.
1920
- Analyses based on configuration. Include/exclude files and directories to be analysed.
2021
For optional config file, see the [current project example](https://github.com/seferov/typhp/blob/master/.typhp.yml)
2122
- Does NOT modifies your code

src/Analyser.php

Lines changed: 131 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22

33
namespace Seferov\Typhp;
44

5+
use phpDocumentor\Reflection\DocBlock;
56
use phpDocumentor\Reflection\DocBlockFactory;
67
use PhpParser\Node;
8+
use PhpParser\Node\Identifier;
79
use PhpParser\ParserFactory;
810
use Seferov\Typhp\Issue\UntypedArgumentIssue;
11+
use Seferov\Typhp\Issue\UntypedKnownArgumentIssue;
12+
use Seferov\Typhp\Issue\UntypedKnownReturnIssue;
913
use Seferov\Typhp\Issue\UntypedReturnIssue;
1014

1115
class Analyser
@@ -81,29 +85,145 @@ private function analyseFunctionLike(Node\FunctionLike $functionLike): void
8185
try {
8286
$docBlock = $this->docBlockFactory->create($functionLike->getDocComment()->getText());
8387
} catch (\Exception $e) {
88+
// Invalid phpdoc case; continue analyzing without phpdoc info.
8489
}
8590
}
8691

8792
if ($docBlock && $this->docBlockAnalyser->isSuppressedByInheritDoc($docBlock)) {
8893
return;
8994
}
9095

91-
foreach ($functionLike->getParams() as $param) {
92-
if (null === $param->type) {
93-
if ($docBlock && $this->docBlockAnalyser->isParamSuppressedByDocBlock($param->var->name, $docBlock)) {
94-
continue;
95-
}
96+
if ($functionLike instanceof Node\Stmt\ClassMethod && in_array($name->name, ['__construct', '__destruct', '__call', '__callStatic', '__get', '__set', '__isset', '__unset', '__sleep', '__wakeup', '__toString', '__invoke', '__set_state', '__clone', '__debugInfo'])) {
97+
$this->analyseSpecialMagicMethods($functionLike, $docBlock);
98+
return;
99+
}
100+
101+
$this->analyseParams($functionLike->getParams(), $name, $docBlock);
102+
$this->analyseReturn($functionLike->getReturnType(), $name, $docBlock);
103+
}
96104

97-
$this->issueCollection->add(UntypedArgumentIssue::create($name->name, $name->getStartLine(), $param->var->name));
105+
/**
106+
* @param Node\Param[] $params
107+
*/
108+
private function analyseParams(array $params, Node\Identifier $name, ?DocBlock $docBlock): void
109+
{
110+
foreach ($params as $param) {
111+
if (null !== $param->type) {
112+
continue;
98113
}
99-
}
100114

101-
if (null === $functionLike->getReturnType() && '__construct' !== $name->name) {
102-
if ($docBlock && $this->docBlockAnalyser->isReturnSuppressedByDocBlock($docBlock)) {
103-
return;
115+
if ($docBlock && $this->docBlockAnalyser->isParamSuppressedByDocBlock($param->var->name, $docBlock)) {
116+
continue;
104117
}
105118

106-
$this->issueCollection->add(UntypedReturnIssue::create($name->name, $name->getStartLine()));
119+
$this->issueCollection->add(UntypedArgumentIssue::create($name->name, $name->getStartLine(), $param->var->name));
120+
}
121+
}
122+
123+
/**
124+
* @param null|Identifier|Node\Name|Node\NullableType $returnType
125+
*/
126+
private function analyseReturn($returnType, Node\Identifier $name, ?DocBlock $docBlock): void
127+
{
128+
if (null !== $returnType) {
129+
return;
130+
}
131+
132+
if ($docBlock && $this->docBlockAnalyser->isReturnSuppressedByDocBlock($docBlock)) {
133+
return;
134+
}
135+
136+
$this->issueCollection->add(UntypedReturnIssue::create($name->name, $name->getStartLine()));
137+
}
138+
139+
private function analyseSpecialMagicMethods(Node\Stmt\ClassMethod $classMethod, ?DocBlock $docBlock): void
140+
{
141+
$name = $classMethod->name;
142+
switch ($name->name) {
143+
case '__construct':
144+
case '__invoke':
145+
$this->analyseParams($classMethod->getParams(), $name, $docBlock);
146+
break;
147+
case '__call':
148+
case '__callStatic':
149+
// string $name, array $arguments. ANALYSE
150+
// mixed return type. ANALYSE
151+
$params = $classMethod->getParams();
152+
$firstParam = array_shift($params);
153+
if (null === $firstParam->type) {
154+
$this->issueCollection->add(UntypedKnownArgumentIssue::create($name->name, $name->getStartLine(), $firstParam->var->name, 'string'));
155+
}
156+
$secondParam = array_shift($params);
157+
if (null === $secondParam->type) {
158+
$this->issueCollection->add(UntypedKnownArgumentIssue::create($name->name, $name->getStartLine(), $secondParam->var->name, 'array'));
159+
}
160+
$this->analyseReturn($classMethod->getReturnType(), $name, $docBlock);
161+
break;
162+
case '__get':
163+
$params = $classMethod->getParams();
164+
if (null === $params[0]->type) {
165+
$this->issueCollection->add(UntypedKnownArgumentIssue::create($name->name, $name->getStartLine(), $params[0]->var->name, 'string'));
166+
}
167+
$this->analyseReturn($classMethod->getReturnType(), $name, $docBlock);
168+
break;
169+
case '__set':
170+
$params = $classMethod->getParams();
171+
$firstParam = array_shift($params);
172+
if (null === $firstParam->type) {
173+
$this->issueCollection->add(UntypedKnownArgumentIssue::create($name->name, $name->getStartLine(), $firstParam->var->name, 'string'));
174+
}
175+
$this->analyseParams($params, $name, $docBlock);
176+
$this->analyseReturn($classMethod->getReturnType(), $name, $docBlock);
177+
break;
178+
case '__unset':
179+
$params = $classMethod->getParams();
180+
$firstParam = array_shift($params);
181+
if (null === $firstParam->type) {
182+
$this->issueCollection->add(UntypedKnownArgumentIssue::create($name->name, $name->getStartLine(), $firstParam->var->name, 'string'));
183+
}
184+
$return = $classMethod->getReturnType();
185+
if (null === $return) {
186+
$this->issueCollection->add(UntypedKnownReturnIssue::create($name->name, $name->getStartLine(), 'void'));
187+
}
188+
break;
189+
case '__sleep':
190+
case '__debugInfo':
191+
$return = $classMethod->getReturnType();
192+
if (null === $return) {
193+
$this->issueCollection->add(UntypedKnownReturnIssue::create($name->name, $name->getStartLine(), 'array'));
194+
}
195+
break;
196+
case '__wakeup':
197+
$return = $classMethod->getReturnType();
198+
if (null === $return) {
199+
$this->issueCollection->add(UntypedKnownReturnIssue::create($name->name, $name->getStartLine(), 'void'));
200+
}
201+
break;
202+
case '__toString':
203+
$return = $classMethod->getReturnType();
204+
if (null === $return) {
205+
$this->issueCollection->add(UntypedKnownReturnIssue::create($name->name, $name->getStartLine(), 'string'));
206+
}
207+
break;
208+
case '__isset':
209+
$params = $classMethod->getParams();
210+
if (null === $params[0]->type) {
211+
$this->issueCollection->add(UntypedKnownArgumentIssue::create($name->name, $name->getStartLine(), $params[0]->var->name, 'string'));
212+
}
213+
$return = $classMethod->getReturnType();
214+
if (null === $return) {
215+
$this->issueCollection->add(UntypedKnownReturnIssue::create($name->name, $name->getStartLine(), 'bool'));
216+
}
217+
break;
218+
case '__set_state':
219+
$params = $classMethod->getParams();
220+
if (null === $params[0]->type) {
221+
$this->issueCollection->add(UntypedKnownArgumentIssue::create($name->name, $name->getStartLine(), $params[0]->var->name, 'array'));
222+
}
223+
break;
224+
case '__destruct':
225+
case '__clone':
226+
break;
107227
}
108228
}
109229
}

src/Command/AnalyseCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
8686
case 'compact':
8787
/** @var IssueInterface $issue */
8888
foreach ($issueCollection as $issue) {
89-
$output->writeln(implode(';', [$issue->getLine(), $issue->getName(), $issue->getIssueCode()]));
89+
$output->writeln($issue->getIssueCompact());
9090
}
9191
break;
9292
default:

src/Issue/IssueInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ public function getLine(): int;
1010

1111
public function getIssue(): string;
1212

13-
public function getIssueCode(): string;
13+
public function getIssueCompact(): string;
1414
}

src/Issue/UntypedArgumentIssue.php

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,13 @@ public static function create(string $name, int $line, string $argumentName): se
1919
return $issue;
2020
}
2121

22-
public function getIssueCode(): string
22+
public function getIssueCompact(): string
2323
{
24-
return 'untyped-argument';
24+
return implode(';', [$this->getLine(), $this->getName(), 'untyped-argument', $this->argumentName]);
2525
}
2626

2727
public function getIssue(): string
2828
{
2929
return sprintf('Missing type declaration for argument "%s"', $this->argumentName);
3030
}
31-
32-
public function getArgumentName(): string
33-
{
34-
return $this->argumentName;
35-
}
3631
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace Seferov\Typhp\Issue;
4+
5+
class UntypedKnownArgumentIssue extends AbstractIssue
6+
{
7+
/**
8+
* @var string
9+
*/
10+
private $argumentName;
11+
/**
12+
* @var string
13+
*/
14+
private $type;
15+
16+
public static function create(string $name, int $line, string $argumentName, string $type): parent
17+
{
18+
$issue = new self;
19+
$issue->name = $name;
20+
$issue->line = $line;
21+
$issue->argumentName = $argumentName;
22+
$issue->type = $type;
23+
24+
return $issue;
25+
}
26+
27+
public function getIssueCompact(): string
28+
{
29+
return implode(';', [$this->getLine(), $this->getName(), 'untyped-known-argument', $this->argumentName]);
30+
}
31+
32+
public function getIssue(): string
33+
{
34+
return sprintf('Missing type declaration for argument "%s". Type must be "%s"', $this->argumentName, $this->type);
35+
}
36+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace Seferov\Typhp\Issue;
4+
5+
class UntypedKnownReturnIssue extends AbstractIssue
6+
{
7+
/**
8+
* @var string
9+
*/
10+
private $type;
11+
12+
public static function create(string $name, int $line, string $type): self
13+
{
14+
$issue = new self;
15+
$issue->name = $name;
16+
$issue->line = $line;
17+
$issue->type = $type;
18+
19+
return $issue;
20+
}
21+
22+
public function getIssueCompact(): string
23+
{
24+
return implode(';', [$this->getLine(), $this->getName(), 'untyped-known-return']);
25+
}
26+
27+
public function getIssue(): string
28+
{
29+
return sprintf('Missing return type. Type must be "%s"', $this->type);
30+
}
31+
}

src/Issue/UntypedReturnIssue.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ public static function create(string $name, int $line): self
1313
return $issue;
1414
}
1515

16-
public function getIssueCode(): string
16+
public function getIssueCompact(): string
1717
{
18-
return 'untyped-return';
18+
return implode(';', [$this->getLine(), $this->getName(), 'untyped-return']);
1919
}
2020

2121
public function getIssue(): string

tests/Functional/Scenarios/ClassMethodsTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ public function testConstruct(): void
1111
$output = $this->process('tests/Functional/Scenarios/Fixtures/ClassConstruct.php');
1212
$outputLines = $output->getLines();
1313
$this->assertCount(2, $outputLines);
14-
$this->assertSame('12;__construct;untyped-argument', $outputLines[0]);
15-
$this->assertSame('17;__construct;untyped-argument', $outputLines[1]);
14+
$this->assertSame('12;__construct;untyped-argument;a', $outputLines[0]);
15+
$this->assertSame('17;__construct;untyped-argument;a', $outputLines[1]);
1616
$this->assertSame(4, $output->getExitCode());
1717
}
1818

@@ -22,9 +22,9 @@ public function testMethods(): void
2222
$outputLines = $output->getLines();
2323
$this->assertCount(5, $outputLines);
2424
$this->assertSame('7;foo;untyped-return', $outputLines[0]);
25-
$this->assertSame('9;bar;untyped-argument', $outputLines[1]);
25+
$this->assertSame('9;bar;untyped-argument;a', $outputLines[1]);
2626
$this->assertSame('9;bar;untyped-return', $outputLines[2]);
27-
$this->assertSame('11;a;untyped-argument', $outputLines[3]);
27+
$this->assertSame('11;a;untyped-argument;b', $outputLines[3]);
2828
$this->assertSame('11;a;untyped-return', $outputLines[4]);
2929
$this->assertSame(4, $output->getExitCode());
3030
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
namespace Seferov\Typhp\Tests\Functional\Scenarios\Fixtures;
4+
5+
class MagicMethodsNoIssue
6+
{
7+
public function __construct(string $name) {}
8+
9+
public function __destruct() {}
10+
11+
public function __call(string $name, array $arguments): string
12+
{
13+
return $name;
14+
}
15+
16+
public static function __callStatic(string $name, array $arguments): string
17+
{
18+
return $name;
19+
}
20+
21+
public function __get(string $name): bool
22+
{
23+
return false;
24+
}
25+
26+
/**
27+
* @param mixed $value
28+
*/
29+
public function __set(string $name, $value): void {}
30+
31+
public function __isset(string $name): bool
32+
{
33+
return false;
34+
}
35+
36+
public function __unset(string $name): void {}
37+
38+
public function __sleep(): array
39+
{
40+
return [];
41+
}
42+
43+
public function __wakeup(): void {}
44+
45+
public function __toString(): string
46+
{
47+
return 'string';
48+
}
49+
public function __invoke(bool $flag) {
50+
return 'invoked';
51+
}
52+
public static function __set_state(array $properties) {
53+
return new self;
54+
}
55+
public function __clone() {}
56+
public function __debugInfo(): array
57+
{
58+
return [];
59+
}
60+
}

0 commit comments

Comments
 (0)