Skip to content
Merged
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ parameters:
#### PHPStan:
- constructor calls for DIC services (rules, extensions, ...)

#### PHPat:
- architecture test methods (`test*` or `#[TestRule]`) of classes registered via the `phpat.test` service tag

#### Nette:
- `handleXxx`, `renderXxx`, `actionXxx`, `injectXxx`, `createComponentXxx`
- `SmartObject` magic calls for `@property` annotations
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"nette/tester": "^2.4",
"nette/utils": "^3.0 || ^4.0",
"nikic/php-parser": "^5.4.0",
"phpat/phpat": "^0.12",
"phpbench/phpbench": "^1.2",
"phpstan/phpstan-phpunit": "^2.0.16",
"phpstan/phpstan-strict-rules": "^2.0.10",
Expand Down
59 changes: 58 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ services:
arguments:
enabled: %shipmonkDeadCode.usageProviders.phpstan.enabled%

-
class: ShipMonk\PHPStan\DeadCode\Provider\PhpatUsageProvider
tags:
- shipmonk.deadCode.memberUsageProvider
arguments:
enabled: %shipmonkDeadCode.usageProviders.phpat.enabled%

-
class: ShipMonk\PHPStan\DeadCode\Provider\EloquentUsageProvider
tags:
Expand Down Expand Up @@ -268,6 +275,8 @@ parameters:
enabled: true
phpstan:
enabled: true
phpat:
enabled: null
phpunit:
enabled: null
phpbench:
Expand Down Expand Up @@ -354,6 +363,9 @@ parametersSchema:
phpstan: structure([
enabled: bool()
])
phpat: structure([
enabled: schema(bool(), nullable())
])
phpunit: structure([
enabled: schema(bool(), nullable())
])
Expand Down
93 changes: 93 additions & 0 deletions src/Provider/PhpatUsageProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php declare(strict_types = 1);

namespace ShipMonk\PHPStan\DeadCode\Provider;

use Composer\InstalledVersions;
use PHPStan\DependencyInjection\Container;
use ReflectionMethod;
use function is_object;
use function str_starts_with;

/**
* phpat registers architecture tests as services tagged "phpat.test" in the PHPStan DIC.
* At runtime, phpat iterates those services and invokes their public methods that either
* carry the #[TestRule] attribute or whose name starts with "test" (see PHPat\Test\TestParser).
*
* Their constructors are already covered by PhpStanUsageProvider (registered DIC services),
* so this provider only marks the invoked test methods as used.
*/
final class PhpatUsageProvider extends ReflectionBasedMemberUsageProvider
{

private const TEST_TAG = 'phpat.test';

private const TEST_RULE_ATTRIBUTE = 'PHPat\Test\Attributes\TestRule';

private readonly bool $enabled;

private readonly Container $container;

/**
* @var array<string, true>|null Set of class names tagged as phpat tests, lazily resolved.
*/
private ?array $testClasses = null;

public function __construct(
?bool $enabled,
Container $container,
)
{
$this->enabled = $enabled ?? InstalledVersions::isInstalled('phpat/phpat');
$this->container = $container;
}

public function shouldMarkMethodAsUsed(ReflectionMethod $method): ?VirtualUsageData
{
if (!$this->enabled) {
return null;
}

if (!$method->isPublic()) {
return null;
}

if (!isset($this->getTestClasses()[$method->getDeclaringClass()->getName()])) {
return null;
}

if (!$this->isTestMethod($method)) {
return null;
}

return VirtualUsageData::withNote('Architecture test method invoked by phpat');
}

private function isTestMethod(ReflectionMethod $method): bool
{
if ($method->getAttributes(self::TEST_RULE_ATTRIBUTE) !== []) {
return true;
}

// phpat invokes every public method whose name starts with "test"
return str_starts_with($method->getName(), 'test');
}

/**
* @return array<string, true>
*/
private function getTestClasses(): array
{
if ($this->testClasses === null) {
$this->testClasses = [];

foreach ($this->container->getServicesByTag(self::TEST_TAG) as $service) {
if (is_object($service)) {
$this->testClasses[$service::class] = true;
}
}
}

return $this->testClasses;
}

}
15 changes: 14 additions & 1 deletion tests/Rule/DeadCodeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Composer\Semver\VersionParser;
use Generator;
use LogicException;
use PhpatProvider\RegisteredArchitectureTest;
use PhpParser\Node;
use PHPStan\Analyser\Error;
use PHPStan\Analyser\Scope;
Expand Down Expand Up @@ -56,6 +57,7 @@
use ShipMonk\PHPStan\DeadCode\Provider\MemberUsageProvider;
use ShipMonk\PHPStan\DeadCode\Provider\NetteTesterUsageProvider;
use ShipMonk\PHPStan\DeadCode\Provider\NetteUsageProvider;
use ShipMonk\PHPStan\DeadCode\Provider\PhpatUsageProvider;
use ShipMonk\PHPStan\DeadCode\Provider\PhpBenchUsageProvider;
use ShipMonk\PHPStan\DeadCode\Provider\PhpStanUsageProvider;
use ShipMonk\PHPStan\DeadCode\Provider\PhpUnitUsageProvider;
Expand Down Expand Up @@ -234,7 +236,7 @@ public function testNoFatalError(): void

try {
ob_start();
require $file;
require_once $file;
ob_end_clean();
} catch (Throwable $e) {
self::fail("Fatal error in {$e->getFile()}:{$e->getLine()}:\n {$e->getMessage()}");
Expand Down Expand Up @@ -1014,6 +1016,7 @@ public static function provideFiles(): Traversable
yield 'provider-behat' => [__DIR__ . '/data/providers/behat.php'];
yield 'provider-doctrine' => [__DIR__ . '/data/providers/doctrine.php'];
yield 'provider-phpstan' => [__DIR__ . '/data/providers/phpstan.php'];
yield 'provider-phpat' => [__DIR__ . '/data/providers/phpat.php'];
yield 'provider-eloquent' => [__DIR__ . '/data/providers/eloquent.php'];
yield 'provider-laravel' => [__DIR__ . '/data/providers/laravel.php'];
yield 'provider-blade' => [__DIR__ . '/data/providers/blade.php'];
Expand Down Expand Up @@ -1224,6 +1227,10 @@ private function getMemberUsageProviders(): array
$this->providersEnabled,
$this->createPhpStanContainerMock(),
),
new PhpatUsageProvider(
$this->providersEnabled,
$this->createPhpStanContainerMock(),
),
new EloquentUsageProvider(
$this->providersEnabled,
),
Expand Down Expand Up @@ -1368,6 +1375,12 @@ static function (string $type): array {
return [];
},
);
$mock->method('getServicesByTag')
->willReturnCallback(
static fn (string $tag): array => $tag === 'phpat.test'
? [new RegisteredArchitectureTest()]
: [],
);
return $mock;
}

Expand Down
50 changes: 50 additions & 0 deletions tests/Rule/data/providers/phpat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php declare(strict_types = 1);

namespace PhpatProvider;

use PHPat\Selector\Selector;
use PHPat\Test\Attributes\TestRule;
use PHPat\Test\Builder\Rule;
use PHPat\Test\PHPat;

// Registered via the "phpat.test" service tag (see DeadCodeRuleTest container mock).
// phpat invokes its public test* / #[TestRule] methods, so they are not dead.
class RegisteredArchitectureTest
{

public function testSrcShouldNotDependOnTests(): Rule
{
return PHPat::rule()
->classes(Selector::AllOf())
->shouldNotDependOn()
->classes(Selector::AllOf());
}

#[TestRule]
public function customNamedRule(): Rule
{
return PHPat::rule()
->classes(Selector::AllOf())
->shouldNotDependOn()
->classes(Selector::AllOf());
}

public function unusedHelper(): void // error: Unused PhpatProvider\RegisteredArchitectureTest::unusedHelper
{
}

}

// Not registered as a phpat test → its test method really is dead.
class UnregisteredArchitectureTest
{

public function testOrphanRule(): Rule // error: Unused PhpatProvider\UnregisteredArchitectureTest::testOrphanRule
{
return PHPat::rule()
->classes(Selector::AllOf())
->shouldNotDependOn()
->classes(Selector::AllOf());
}

}
Loading