Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/test-phpstan-extension.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: PHPStan extension Test

on:
push:
paths: &paths
- .github/workflows/test-phpstan-extension.yml
- utils/phpstan/**
- phpunit-utils.xml.dist
pull_request:
paths: *paths
schedule:
- cron: '0 0 1,16 * *'

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
test-phpstan-rules:
name: Test PHPStan extension
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.4
coverage: none

- name: Install dependencies
uses: ramsey/composer-install@v2
with:
composer-options: --prefer-dist

- name: Install PHPStan
run: |
composer bin phpstan install

- name: Test
run: vendor/bin/phpunit -c phpunit-utils.xml.dist --bootstrap utils/rector/tests/bootstrap.php --testsuite Phpstan
shell: bash

- name: Static analysis
run: bin/tools/phpstan/vendor/phpstan/phpstan/phpstan analyse -c utils/phpstan/phpstan.neon
shell: bash
3 changes: 2 additions & 1 deletion .github/workflows/test-rector-rules.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
paths: &paths
- .github/workflows/test-rector-rules.yml
- utils/rector/**
- phpunit-utils.xml.dist
pull_request:
paths: *paths
schedule:
Expand Down Expand Up @@ -39,7 +40,7 @@ jobs:
composer bin rector install

- name: Test
run: vendor/bin/phpunit -c phpunit-rector.xml.dist
run: vendor/bin/phpunit -c phpunit-utils.xml.dist --bootstrap utils/rector/tests/bootstrap.php --testsuite Rector
shell: bash

- name: Static analysis
Expand Down
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@
"psr-4": {
"Zenstruck\\Foundry\\": "src/",
"Zenstruck\\Foundry\\Psalm\\": "utils/psalm",
"Zenstruck\\Foundry\\Utils\\Rector\\": "utils/rector/src/"
"Zenstruck\\Foundry\\Utils\\Rector\\": "utils/rector/src/",
"Zenstruck\\Foundry\\Utils\\PHPStan\\": "utils/phpstan/src/"
},
"files": [
"src/functions.php",
Expand All @@ -69,7 +70,8 @@
"Zenstruck\\Foundry\\Tests\\": ["tests/"],
"App\\": "tests/Fixture/Maker/tmp/src",
"App\\Tests\\": "tests/Fixture/Maker/tmp/tests",
"Zenstruck\\Foundry\\Utils\\Rector\\Tests\\": "utils/rector/tests/"
"Zenstruck\\Foundry\\Utils\\Rector\\Tests\\": "utils/rector/tests/",
"Zenstruck\\Foundry\\Utils\\PHPStan\\Tests\\": "utils/phpstan/tests/"
},
"exclude-from-classmap": ["tests/Fixture/Maker/expected", "utils/rector/tests/**/Fixtures/"]
},
Expand Down
7 changes: 5 additions & 2 deletions phpunit-rector.xml.dist → phpunit-utils.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
colors="true"
bootstrap="utils/rector/tests/bootstrap.php"
failOnRisky="true"
failOnWarning="true">

Expand All @@ -14,8 +13,12 @@
</php>

<testsuites>
<testsuite name="Rector">
<testsuite name="Rector" bootstrap="utils/rector/tests/bootstrap.php">
<directory>./utils/rector/tests/</directory>
</testsuite>
<testsuite name="Phpstan" bootstrap="utils/phpstan/tests/bootstrap.php">
<directory>./utils/phpstan/tests/</directory>
</testsuite>
</testsuites>
</phpunit>

16 changes: 16 additions & 0 deletions utils/phpstan/extension.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
parameters:
foundry:
cannotCallStaticCreateMethodOnFactoryInstance: true

parametersSchema:
foundry: structure([
cannotCallStaticCreateMethodOnFactoryInstance: anyOf(bool(), arrayOf(bool()))
])

conditionalTags:
Zenstruck\Foundry\Utils\PHPStan\CannotCallStaticCreateMethodOnFactoryInstanceRule:
phpstan.rules.rule: %foundry.cannotCallStaticCreateMethodOnFactoryInstance%

services:
-
class: Zenstruck\Foundry\Utils\PHPStan\CannotCallStaticCreateMethodOnFactoryInstanceRule
11 changes: 11 additions & 0 deletions utils/phpstan/phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
parameters:
inferPrivatePropertyTypeFromConstructor: true
checkUninitializedProperties: true
paths:
- ./src
- ./tests
level: 8
bootstrapFiles:
- ./tests/bootstrap.php
excludePaths:
- ./tests/Fixtures
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace Zenstruck\Foundry\Utils\PHPStan;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\ObjectType;
use Zenstruck\Foundry\Factory;

/**
* @implements Rule<Node\Expr\StaticCall>
*/
final class CannotCallStaticCreateMethodOnFactoryInstanceRule implements Rule
{
private const STATIC_METHODS = [
'createOne' => 'create()',
'createMany' => 'many()->create()',
'createRange' => 'range()->create()',
'createSequence' => 'sequence()->create()',
];

public function getNodeType(): string
{
return Node\Expr\StaticCall::class;
}

public function processNode(Node $node, Scope $scope): array
{
if (!$node->name instanceof Node\Identifier
|| !$node->class instanceof Node\Expr) {
return [];
}

$type = $scope->getType($node->class);

$methodName = $node->name->toString();

if (
!\in_array($methodName, array_keys(self::STATIC_METHODS), true)
|| !$type->isObject()->yes()
|| !(new ObjectType(Factory::class))->accepts($type, true)->yes()
) {
return [];
}

return [
RuleErrorBuilder::message(
sprintf(
'Method "%s()" should not be called on an instance.',
$methodName,
)
)
->tip(
sprintf(
'Call the method statically instead: "SomeFactory::%s()", or use "$someFactory->%s" if you want to call the method on the instance.',
$methodName,
self::STATIC_METHODS[$methodName]
)
)
->identifier('foundry.staticMethodCalledOnInstance')
->build(),
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace Zenstruck\Foundry\Utils\PHPStan\Tests;

use PHPStan\Testing\RuleTestCase;
use Zenstruck\Foundry\Utils\PHPStan\CannotCallStaticCreateMethodOnFactoryInstanceRule;
use PHPStan\Rules\Rule;

/**
* @extends RuleTestCase<CannotCallStaticCreateMethodOnFactoryInstanceRule>
*/
final class CannotCallStaticCreateMethodOnFactoryInstanceRuleTest extends RuleTestCase
{
protected function getRule(): Rule
{
return new CannotCallStaticCreateMethodOnFactoryInstanceRule();
}

public function testInvalidCalls(): void
{
$this->analyse([
__DIR__ . '/Fixtures/invalid.php.inc',
], [
[
'Method "createOne()" should not be called on an instance.
💡 Call the method statically instead: "SomeFactory::createOne()", or use "$someFactory->create()" if you want to call the method on the instance.',
6,
],
[
'Method "createMany()" should not be called on an instance.
💡 Call the method statically instead: "SomeFactory::createMany()", or use "$someFactory->many()->create()" if you want to call the method on the instance.',
7,
],
[
'Method "createRange()" should not be called on an instance.
💡 Call the method statically instead: "SomeFactory::createRange()", or use "$someFactory->range()->create()" if you want to call the method on the instance.',
8,
],
[
'Method "createSequence()" should not be called on an instance.
💡 Call the method statically instead: "SomeFactory::createSequence()", or use "$someFactory->sequence()->create()" if you want to call the method on the instance.',
9,
],
[
'Method "createOne()" should not be called on an instance.
💡 Call the method statically instead: "SomeFactory::createOne()", or use "$someFactory->create()" if you want to call the method on the instance.',
10,
],
]);
}

public function testValidCalls(): void
{
$this->analyse([
__DIR__ . '/Fixtures/valid.php.inc',
], []);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

use Zenstruck\Foundry\Utils\PHPStan\Tests\Fixtures\DummyObjectFactory;

$f = DummyObjectFactory::new();
$f::createOne();
$f::createMany(2);
$f::createRange();
$f::createSequence([]);
DummyObjectFactory::new()::createOne();
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

// static call on class name - OK
use Zenstruck\Foundry\Utils\PHPStan\Tests\Fixtures\DummyObjectFactory;

DummyObjectFactory::createOne();
DummyObjectFactory::createMany(2);
DummyObjectFactory::createSequence([]);

$class = DummyObjectFactory::class;
$class::createOne();

$inst = DummyObjectFactory::new();
$inst->create();

'test'::createOne();
$foo::createOne();

(new \stdClass())::createOne();
28 changes: 28 additions & 0 deletions utils/phpstan/tests/Fixtures/DummyObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

/*
* This file is part of the zenstruck/foundry package.
*
* (c) Kevin Bond <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Zenstruck\Foundry\Utils\PHPStan\Tests\Fixtures;

class DummyObject
{
public ?int $id = null;

private function __construct()
{
}

public static function new(): static
{
return new self();
}
}
29 changes: 29 additions & 0 deletions utils/phpstan/tests/Fixtures/DummyObjectFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

/*
* This file is part of the zenstruck/foundry package.
*
* (c) Kevin Bond <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Zenstruck\Foundry\Utils\PHPStan\Tests\Fixtures;

use Zenstruck\Foundry\ObjectFactory;

final class DummyObjectFactory extends ObjectFactory
{
public static function class(): string
{
return DummyObject::class;
}

protected function defaults(): array
{
return [];
}
}
13 changes: 13 additions & 0 deletions utils/phpstan/tests/bootstrap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

/*
* This file is part of the zenstruck/foundry package.
*
* (c) Kevin Bond <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

require \dirname(__DIR__).'/../../vendor/autoload.php';
require \dirname(__DIR__).'/../../bin/tools/phpstan/vendor/autoload.php';
Loading