Skip to content

Commit

Permalink
support all magic methods (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
seferov authored Oct 21, 2019
1 parent 0cfc76d commit eeb9090
Show file tree
Hide file tree
Showing 13 changed files with 376 additions and 29 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ but suggests typehint everything possible.
- Who works projects using PHP 7.1 and higher.
- Who doesn't want to point out missing type hint and return type declarations in code review process
by using it as part of CI pipeline.
- Who love strict typing
- Who love strict typing and defensive programming.

#### Features

- Respects phpdoc; there are some rare cases mixed or compound types are needed.
If such cases documented in phpdoc, `typhp` doesn't complain. For example: `@return array|bool`, `@param mixed $foo`, etc.
- Takes [magic methods](https://www.php.net/manual/en/language.oop5.magic.php) into account.
- Analyses based on configuration. Include/exclude files and directories to be analysed.
For optional config file, see the [current project example](https://github.com/seferov/typhp/blob/master/.typhp.yml)
- Does NOT modifies your code
Expand Down
142 changes: 131 additions & 11 deletions src/Analyser.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

namespace Seferov\Typhp;

use phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\DocBlockFactory;
use PhpParser\Node;
use PhpParser\Node\Identifier;
use PhpParser\ParserFactory;
use Seferov\Typhp\Issue\UntypedArgumentIssue;
use Seferov\Typhp\Issue\UntypedKnownArgumentIssue;
use Seferov\Typhp\Issue\UntypedKnownReturnIssue;
use Seferov\Typhp\Issue\UntypedReturnIssue;

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

if ($docBlock && $this->docBlockAnalyser->isSuppressedByInheritDoc($docBlock)) {
return;
}

foreach ($functionLike->getParams() as $param) {
if (null === $param->type) {
if ($docBlock && $this->docBlockAnalyser->isParamSuppressedByDocBlock($param->var->name, $docBlock)) {
continue;
}
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'])) {
$this->analyseSpecialMagicMethods($functionLike, $docBlock);
return;
}

$this->analyseParams($functionLike->getParams(), $name, $docBlock);
$this->analyseReturn($functionLike->getReturnType(), $name, $docBlock);
}

$this->issueCollection->add(UntypedArgumentIssue::create($name->name, $name->getStartLine(), $param->var->name));
/**
* @param Node\Param[] $params
*/
private function analyseParams(array $params, Node\Identifier $name, ?DocBlock $docBlock): void
{
foreach ($params as $param) {
if (null !== $param->type) {
continue;
}
}

if (null === $functionLike->getReturnType() && '__construct' !== $name->name) {
if ($docBlock && $this->docBlockAnalyser->isReturnSuppressedByDocBlock($docBlock)) {
return;
if ($docBlock && $this->docBlockAnalyser->isParamSuppressedByDocBlock($param->var->name, $docBlock)) {
continue;
}

$this->issueCollection->add(UntypedReturnIssue::create($name->name, $name->getStartLine()));
$this->issueCollection->add(UntypedArgumentIssue::create($name->name, $name->getStartLine(), $param->var->name));
}
}

/**
* @param null|Identifier|Node\Name|Node\NullableType $returnType
*/
private function analyseReturn($returnType, Node\Identifier $name, ?DocBlock $docBlock): void
{
if (null !== $returnType) {
return;
}

if ($docBlock && $this->docBlockAnalyser->isReturnSuppressedByDocBlock($docBlock)) {
return;
}

$this->issueCollection->add(UntypedReturnIssue::create($name->name, $name->getStartLine()));
}

private function analyseSpecialMagicMethods(Node\Stmt\ClassMethod $classMethod, ?DocBlock $docBlock): void
{
$name = $classMethod->name;
switch ($name->name) {
case '__construct':
case '__invoke':
$this->analyseParams($classMethod->getParams(), $name, $docBlock);
break;
case '__call':
case '__callStatic':
// string $name, array $arguments. ANALYSE
// mixed return type. ANALYSE
$params = $classMethod->getParams();
$firstParam = array_shift($params);
if (null === $firstParam->type) {
$this->issueCollection->add(UntypedKnownArgumentIssue::create($name->name, $name->getStartLine(), $firstParam->var->name, 'string'));
}
$secondParam = array_shift($params);
if (null === $secondParam->type) {
$this->issueCollection->add(UntypedKnownArgumentIssue::create($name->name, $name->getStartLine(), $secondParam->var->name, 'array'));
}
$this->analyseReturn($classMethod->getReturnType(), $name, $docBlock);
break;
case '__get':
$params = $classMethod->getParams();
if (null === $params[0]->type) {
$this->issueCollection->add(UntypedKnownArgumentIssue::create($name->name, $name->getStartLine(), $params[0]->var->name, 'string'));
}
$this->analyseReturn($classMethod->getReturnType(), $name, $docBlock);
break;
case '__set':
$params = $classMethod->getParams();
$firstParam = array_shift($params);
if (null === $firstParam->type) {
$this->issueCollection->add(UntypedKnownArgumentIssue::create($name->name, $name->getStartLine(), $firstParam->var->name, 'string'));
}
$this->analyseParams($params, $name, $docBlock);
$this->analyseReturn($classMethod->getReturnType(), $name, $docBlock);
break;
case '__unset':
$params = $classMethod->getParams();
$firstParam = array_shift($params);
if (null === $firstParam->type) {
$this->issueCollection->add(UntypedKnownArgumentIssue::create($name->name, $name->getStartLine(), $firstParam->var->name, 'string'));
}
$return = $classMethod->getReturnType();
if (null === $return) {
$this->issueCollection->add(UntypedKnownReturnIssue::create($name->name, $name->getStartLine(), 'void'));
}
break;
case '__sleep':
case '__debugInfo':
$return = $classMethod->getReturnType();
if (null === $return) {
$this->issueCollection->add(UntypedKnownReturnIssue::create($name->name, $name->getStartLine(), 'array'));
}
break;
case '__wakeup':
$return = $classMethod->getReturnType();
if (null === $return) {
$this->issueCollection->add(UntypedKnownReturnIssue::create($name->name, $name->getStartLine(), 'void'));
}
break;
case '__toString':
$return = $classMethod->getReturnType();
if (null === $return) {
$this->issueCollection->add(UntypedKnownReturnIssue::create($name->name, $name->getStartLine(), 'string'));
}
break;
case '__isset':
$params = $classMethod->getParams();
if (null === $params[0]->type) {
$this->issueCollection->add(UntypedKnownArgumentIssue::create($name->name, $name->getStartLine(), $params[0]->var->name, 'string'));
}
$return = $classMethod->getReturnType();
if (null === $return) {
$this->issueCollection->add(UntypedKnownReturnIssue::create($name->name, $name->getStartLine(), 'bool'));
}
break;
case '__set_state':
$params = $classMethod->getParams();
if (null === $params[0]->type) {
$this->issueCollection->add(UntypedKnownArgumentIssue::create($name->name, $name->getStartLine(), $params[0]->var->name, 'array'));
}
break;
case '__destruct':
case '__clone':
break;
}
}
}
2 changes: 1 addition & 1 deletion src/Command/AnalyseCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
case 'compact':
/** @var IssueInterface $issue */
foreach ($issueCollection as $issue) {
$output->writeln(implode(';', [$issue->getLine(), $issue->getName(), $issue->getIssueCode()]));
$output->writeln($issue->getIssueCompact());
}
break;
default:
Expand Down
2 changes: 1 addition & 1 deletion src/Issue/IssueInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ public function getLine(): int;

public function getIssue(): string;

public function getIssueCode(): string;
public function getIssueCompact(): string;
}
9 changes: 2 additions & 7 deletions src/Issue/UntypedArgumentIssue.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,13 @@ public static function create(string $name, int $line, string $argumentName): se
return $issue;
}

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

public function getIssue(): string
{
return sprintf('Missing type declaration for argument "%s"', $this->argumentName);
}

public function getArgumentName(): string
{
return $this->argumentName;
}
}
36 changes: 36 additions & 0 deletions src/Issue/UntypedKnownArgumentIssue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Seferov\Typhp\Issue;

class UntypedKnownArgumentIssue extends AbstractIssue
{
/**
* @var string
*/
private $argumentName;
/**
* @var string
*/
private $type;

public static function create(string $name, int $line, string $argumentName, string $type): parent
{
$issue = new self;
$issue->name = $name;
$issue->line = $line;
$issue->argumentName = $argumentName;
$issue->type = $type;

return $issue;
}

public function getIssueCompact(): string
{
return implode(';', [$this->getLine(), $this->getName(), 'untyped-known-argument', $this->argumentName]);
}

public function getIssue(): string
{
return sprintf('Missing type declaration for argument "%s". Type must be "%s"', $this->argumentName, $this->type);
}
}
31 changes: 31 additions & 0 deletions src/Issue/UntypedKnownReturnIssue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Seferov\Typhp\Issue;

class UntypedKnownReturnIssue extends AbstractIssue
{
/**
* @var string
*/
private $type;

public static function create(string $name, int $line, string $type): self
{
$issue = new self;
$issue->name = $name;
$issue->line = $line;
$issue->type = $type;

return $issue;
}

public function getIssueCompact(): string
{
return implode(';', [$this->getLine(), $this->getName(), 'untyped-known-return']);
}

public function getIssue(): string
{
return sprintf('Missing return type. Type must be "%s"', $this->type);
}
}
4 changes: 2 additions & 2 deletions src/Issue/UntypedReturnIssue.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ public static function create(string $name, int $line): self
return $issue;
}

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

public function getIssue(): string
Expand Down
8 changes: 4 additions & 4 deletions tests/Functional/Scenarios/ClassMethodsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ public function testConstruct(): void
$output = $this->process('tests/Functional/Scenarios/Fixtures/ClassConstruct.php');
$outputLines = $output->getLines();
$this->assertCount(2, $outputLines);
$this->assertSame('12;__construct;untyped-argument', $outputLines[0]);
$this->assertSame('17;__construct;untyped-argument', $outputLines[1]);
$this->assertSame('12;__construct;untyped-argument;a', $outputLines[0]);
$this->assertSame('17;__construct;untyped-argument;a', $outputLines[1]);
$this->assertSame(4, $output->getExitCode());
}

Expand All @@ -22,9 +22,9 @@ public function testMethods(): void
$outputLines = $output->getLines();
$this->assertCount(5, $outputLines);
$this->assertSame('7;foo;untyped-return', $outputLines[0]);
$this->assertSame('9;bar;untyped-argument', $outputLines[1]);
$this->assertSame('9;bar;untyped-argument;a', $outputLines[1]);
$this->assertSame('9;bar;untyped-return', $outputLines[2]);
$this->assertSame('11;a;untyped-argument', $outputLines[3]);
$this->assertSame('11;a;untyped-argument;b', $outputLines[3]);
$this->assertSame('11;a;untyped-return', $outputLines[4]);
$this->assertSame(4, $output->getExitCode());
}
Expand Down
60 changes: 60 additions & 0 deletions tests/Functional/Scenarios/Fixtures/MagicMethodsNoIssue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace Seferov\Typhp\Tests\Functional\Scenarios\Fixtures;

class MagicMethodsNoIssue
{
public function __construct(string $name) {}

public function __destruct() {}

public function __call(string $name, array $arguments): string
{
return $name;
}

public static function __callStatic(string $name, array $arguments): string
{
return $name;
}

public function __get(string $name): bool
{
return false;
}

/**
* @param mixed $value
*/
public function __set(string $name, $value): void {}

public function __isset(string $name): bool
{
return false;
}

public function __unset(string $name): void {}

public function __sleep(): array
{
return [];
}

public function __wakeup(): void {}

public function __toString(): string
{
return 'string';
}
public function __invoke(bool $flag) {
return 'invoked';
}
public static function __set_state(array $properties) {
return new self;
}
public function __clone() {}
public function __debugInfo(): array
{
return [];
}
}
Loading

0 comments on commit eeb9090

Please sign in to comment.