Skip to content

Instantly share code, notes, and snippets.

@matthiasnoback
Created March 8, 2022 12:40
Show Gist options
  • Save matthiasnoback/2ae71a53cce0cf18878687df65e518e6 to your computer and use it in GitHub Desktop.
Save matthiasnoback/2ae71a53cce0cf18878687df65e518e6 to your computer and use it in GitHub Desktop.
PHPStan rule and test combined
<?php
declare(strict_types=1);
namespace Utils\PHPStan;
use Generator;
use Illuminate\Database\Eloquent\Model;
use PhpParser\Node;
use PhpParser\Node\Expr\PropertyFetch;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Testing\RuleTestCase;
use PHPStan\Type\ObjectType;
final class EloquentModelAttributesRule extends RuleTestCase implements Rule
{
public function getNodeType(): string
{
return PropertyFetch::class;
}
/**
* @param PropertyFetch $node
*/
public function processNode(Node $node, Scope $scope): array
{
$isMethodCallOnModel = (new ObjectType(Model::class))
->isSuperTypeOf($scope->getType($node->var));
if ($isMethodCallOnModel->no()) {
return [];
}
// The object on which the property is fetched is a Model
if ($scope->isInClass()
&& $scope->getClassReflection() !== null
&& $scope->getClassReflection()->isSubclassOf(
Model::class
)) {
// The property fetch takes place inside a Model class
return [];
}
return [
RuleErrorBuilder::message(
'Accessing attributes is not allowed outside a Model'
)->build(),
];
}
/**
* @dataProvider provideSnippetsAndExpectedErrors
*/
public function testRule(string $code, array $expectedErrors): void
{
$file = sys_get_temp_dir() . '/' . md5($code) . '.php';
file_put_contents($file, $code);
try {
$this->analyse([$file], $expectedErrors);
} finally {
unlink($file);
}
}
/**
* @return Generator<string, {string, array<{string, int}>}>
*/
public function provideSnippetsAndExpectedErrors(): Generator
{
yield 'ERROR: property assignment is done on a Model from the outside' => [
<<<'PHP'
<?php
$model = new class extends \Illuminate\Database\Eloquent\Model {};
$model->foo = 'bar';
PHP
,
[
[
'Accessing attributes is not allowed outside a Model',
3,
],
],
];
yield 'ERROR: property fetch is done on a Model from the outside' => [
<<<'PHP'
<?php
$model = new class extends \Illuminate\Database\Eloquent\Model {};
$foo = $model->foo;
PHP
,
[
[
'Accessing attributes is not allowed outside a Model',
3,
],
],
];
yield 'OK: The object is not a Model' => [
<<<'PHP'
<?php
$notAModel = new class() {};
$foo = $notAModel->foo;
PHP
,
[],
];
yield 'OK: The object is a Model' => [
<<<'PHP'
<?php
$model = new class {};
$model->foo = 'foo';
PHP
,
[],
];
yield 'OK: attribute is fetched inside a Model' => [
<<<'PHP'
<?php
class Foo extends \Illuminate\Database\Eloquent\Model {
public function foo(): void {
$foo = $this->foo;
}
};
PHP
,
[],
];
yield 'OK: attribute is assigned inside a Model' => [
<<<'PHP'
<?php
class Foo extends \Illuminate\Database\Eloquent\Model {
public function foo(): void {
$this->foo = 'foo';
}
};
PHP
,
[],
];
}
protected function getRule(): Rule
{
return new self();
}
}
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php">
<testsuites>
<testsuite name="Tests">
<directory>test</directory>
</testsuite>
<testsuite name="PHPStan rules">
<directory suffix="Rule.php">utils/PHPStan</directory>
</testsuite>
</testsuites>
</phpunit>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment