-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #36 from CakeDC/feature/ExpressionTypeResolverExte…
…nsion Feature/expression type resolver extension
- Loading branch information
Showing
14 changed files
with
437 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,3 +6,4 @@ rules: | |
parameters: | ||
level: max | ||
checkGenericClassInNonGenericObjectType: false | ||
treatPhpDocTypesAsCertain: false |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace CakeDC\PHPStan\Constraint; | ||
|
||
use PHPUnit\Framework\Constraint\Constraint; | ||
|
||
class ArrayOfStringStartsWith extends Constraint | ||
{ | ||
/** | ||
* @var array<string> | ||
*/ | ||
private readonly array $actual; | ||
/** | ||
* @var array<array{expected: string, type: string, actual: string|null}> | ||
*/ | ||
private array $result = []; | ||
/** | ||
* @var array<string> | ||
*/ | ||
private array $notExpected = []; | ||
|
||
/** | ||
* @param array<string> $actual | ||
*/ | ||
public function __construct(array $actual) | ||
{ | ||
$this->actual = $actual; | ||
} | ||
|
||
/** | ||
* @return string | ||
*/ | ||
public function toString(): string | ||
{ | ||
return 'a list of errors'; | ||
} | ||
|
||
/** | ||
* @param mixed $other | ||
* @return bool | ||
*/ | ||
protected function matches(mixed $other): bool | ||
{ | ||
$result = true; | ||
$this->notExpected = $this->actual; | ||
assert(is_array($other)); | ||
foreach ($other as $key => $error) { | ||
if (!isset($this->actual[$key])) { | ||
$this->result[$key] = ['expected' => $error, 'type' => 'missing', 'actual' => null]; | ||
$result = false; | ||
continue; | ||
} | ||
unset($this->notExpected[$key]); | ||
if (!str_starts_with($this->actual[$key], $error)) { | ||
$this->result[$key] = ['expected' => $error, 'type' => 'not-equal', 'actual' => $this->actual[$key]]; | ||
$result = false; | ||
} | ||
} | ||
|
||
return $result && empty($this->notExpected); | ||
} | ||
|
||
/** | ||
* @param mixed $other | ||
* @return string | ||
*/ | ||
protected function failureDescription(mixed $other): string | ||
{ | ||
$text = "\n"; | ||
foreach ($this->result as $item) { | ||
if ($item['type'] === 'not-equal') { | ||
$text .= sprintf(" -%s \n +%s \n", $item['expected'], $item['actual']); | ||
} | ||
if ($item['type'] === 'missing') { | ||
$text .= sprintf(" -%s \n", $item['expected']); | ||
} | ||
} | ||
|
||
foreach ($this->notExpected as $item) { | ||
$text .= sprintf(" \n +%s", $item); | ||
} | ||
|
||
return $text; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace CakeDC\PHPStan\Rule\Traits; | ||
|
||
use CakeDC\PHPStan\Constraint\ArrayOfStringStartsWith; | ||
use PHPStan\Analyser\Error; | ||
|
||
/** | ||
* @mixin \PHPStan\Testing\RuleTestCase; | ||
*/ | ||
trait AnalyseCheckLineStartsWithTrait | ||
{ | ||
/** | ||
* @param array $files | ||
* @param array $expected | ||
* @return void | ||
*/ | ||
public function analyseCheckLineStartsWith(array $files, array $expected): void | ||
{ | ||
$actualErrors = $this->gatherAnalyserErrors($files); | ||
$messageText = static function (int $line, string $message): string { | ||
return sprintf('%02d: %s', $line, $message); | ||
}; | ||
$actualErrors = array_map(static function (Error $error) use ($messageText): string { | ||
$line = $error->getLine(); | ||
if ($line === null) { | ||
return $messageText(-1, $error->getMessage()); | ||
} | ||
|
||
return $messageText($line, $error->getMessage()); | ||
}, $actualErrors); | ||
|
||
$expected = array_map(static function (array $item) use ($messageText): string { | ||
return $messageText($item[1], $item[0]); | ||
}, $expected); | ||
$this->assertThat($expected, new ArrayOfStringStartsWith($actualErrors)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
/** | ||
* Copyright 2024, Cake Development Corporation (https://www.cakedc.com) | ||
* | ||
* Licensed under The MIT License | ||
* Redistributions of files must retain the above copyright notice. | ||
* | ||
* @copyright Copyright 2024, Cake Development Corporation (https://www.cakedc.com) | ||
* @license MIT License (http://www.opensource.org/licenses/mit-license.php) | ||
*/ | ||
|
||
namespace CakeDC\PHPStan\Type; | ||
|
||
use CakeDC\PHPStan\Utility\CakeNameRegistry; | ||
use PhpParser\Node\Expr; | ||
use PhpParser\Node\Identifier; | ||
use PhpParser\Node\Scalar\String_; | ||
use PHPStan\Analyser\Scope; | ||
use PHPStan\Reflection\ClassReflection; | ||
use PHPStan\Type\ExpressionTypeResolverExtension; | ||
use PHPStan\Type\ObjectType; | ||
use PHPStan\Type\ThisType; | ||
use PHPStan\Type\Type; | ||
use ReflectionException; | ||
|
||
class BaseTraitExpressionTypeResolverExtension implements ExpressionTypeResolverExtension | ||
{ | ||
/** | ||
* TableLocatorDynamicReturnTypeExtension constructor. | ||
* | ||
* @param string $targetTrait The target trait. | ||
* @param string $methodName The dynamic method to handle. | ||
* @param string $namespaceFormat The resolve namespace format. | ||
* @param string|null $propertyDefaultValue A property name for default classname, used when no args in method call. | ||
*/ | ||
public function __construct( | ||
protected string $targetTrait, | ||
protected string $methodName, | ||
protected string $namespaceFormat, | ||
protected ?string $propertyDefaultValue = null | ||
) { | ||
} | ||
|
||
/** | ||
* @param \PhpParser\Node\Expr $expr | ||
* @param \PHPStan\Analyser\Scope $scope | ||
* @return \PHPStan\Type\Type|null | ||
*/ | ||
public function getType(Expr $expr, Scope $scope): ?Type | ||
{ | ||
if ( | ||
!$expr instanceof Expr\MethodCall | ||
|| !$expr->name instanceof Identifier | ||
|| $expr->name->toString() !== $this->methodName | ||
) { | ||
return null; | ||
} | ||
|
||
$callerType = $scope->getType($expr->var); | ||
if (!$callerType instanceof ThisType && !$callerType instanceof ObjectType) { | ||
return null; | ||
} | ||
$reflection = $callerType->getClassReflection(); | ||
if ($reflection === null || !$this->isFromTargetTrait($reflection)) { | ||
return null; | ||
} | ||
|
||
$value = $expr->getArgs()[0]->value ?? null; | ||
$baseName = $this->getBaseName($value, $reflection); | ||
if ($baseName === null) { | ||
return null; | ||
} | ||
$className = CakeNameRegistry::getClassName($baseName, $this->namespaceFormat); | ||
if ($className !== null) { | ||
return new ObjectType($className); | ||
} | ||
|
||
return null; | ||
} | ||
|
||
/** | ||
* @param \PHPStan\Reflection\ClassReflection $reflection | ||
* @return bool | ||
*/ | ||
protected function isFromTargetTrait(ClassReflection $reflection): bool | ||
{ | ||
foreach ($reflection->getTraits() as $trait) { | ||
if ($trait->getName() === $this->targetTrait) { | ||
return true; | ||
} | ||
} | ||
foreach ($reflection->getParents() as $parent) { | ||
if ($this->isFromTargetTrait($parent)) { | ||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
/** | ||
* @param \PhpParser\Node\Expr|null $value | ||
* @param \PHPStan\Reflection\ClassReflection $reflection | ||
* @return string|null | ||
*/ | ||
protected function getBaseName(?Expr $value, ClassReflection $reflection): ?string | ||
{ | ||
if ($value instanceof String_) { | ||
return $value->value; | ||
} | ||
|
||
try { | ||
if ($value === null && $this->propertyDefaultValue) { | ||
$value = $reflection->getNativeReflection() | ||
->getProperty($this->propertyDefaultValue) | ||
->getDefaultValue(); | ||
|
||
return is_string($value) ? $value : null; | ||
} | ||
} catch (ReflectionException) { | ||
return null; | ||
} | ||
|
||
return null; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.