Skip to content

Commit 4ebdee6

Browse files
authored
Add Behat support (#267)
1 parent 05b569e commit 4ebdee6

File tree

5 files changed

+409
-0
lines changed

5 files changed

+409
-0
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ $ vendor/bin/phpstan
7676
- `#[BeforeMethods]`, `#[AfterMethods]` attributes
7777
- `#[ParamProviders]` attribute for param provider methods
7878

79+
#### Behat:
80+
- context class constructors
81+
- step definitions via annotations (`@Given`, `@When`, `@Then`) or attributes (`#[Given]`, `#[When]`, `#[Then]`)
82+
- hooks via annotations (`@BeforeScenario`, `@AfterScenario`, etc.) or attributes (`#[BeforeScenario]`, `#[AfterScenario]`, etc.)
83+
- transformations via `@Transform` or `#[Transform]`
84+
7985
#### PHPStan:
8086
- constructor calls for DIC services (rules, extensions, ...)
8187

rules.neon

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ services:
6363
arguments:
6464
enabled: %shipmonkDeadCode.usageProviders.phpbench.enabled%
6565

66+
-
67+
class: ShipMonk\PHPStan\DeadCode\Provider\BehatUsageProvider
68+
tags:
69+
- shipmonk.deadCode.memberUsageProvider
70+
arguments:
71+
enabled: %shipmonkDeadCode.usageProviders.behat.enabled%
72+
6673
-
6774
class: ShipMonk\PHPStan\DeadCode\Provider\SymfonyUsageProvider
6875
tags:
@@ -197,6 +204,8 @@ parameters:
197204
enabled: null
198205
phpbench:
199206
enabled: null
207+
behat:
208+
enabled: null
200209
symfony:
201210
enabled: null
202211
configDir: null
@@ -255,6 +264,9 @@ parametersSchema:
255264
phpbench: structure([
256265
enabled: schema(bool(), nullable())
257266
])
267+
behat: structure([
268+
enabled: schema(bool(), nullable())
269+
])
258270
symfony: structure([
259271
enabled: schema(bool(), nullable())
260272
configDir: schema(string(), nullable())
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ShipMonk\PHPStan\DeadCode\Provider;
4+
5+
use Composer\InstalledVersions;
6+
use PhpParser\Node;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod;
9+
use PHPStan\Node\InClassNode;
10+
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodRef;
11+
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodUsage;
12+
use ShipMonk\PHPStan\DeadCode\Graph\UsageOrigin;
13+
use function strpos;
14+
15+
class BehatUsageProvider implements MemberUsageProvider
16+
{
17+
18+
private bool $enabled;
19+
20+
public function __construct(?bool $enabled)
21+
{
22+
$this->enabled = $enabled ?? InstalledVersions::isInstalled('behat/behat');
23+
}
24+
25+
public function getUsages(
26+
Node $node,
27+
Scope $scope
28+
): array
29+
{
30+
if (!$this->enabled || !$node instanceof InClassNode) { // @phpstan-ignore phpstanApi.instanceofAssumption
31+
return [];
32+
}
33+
34+
$classReflection = $node->getClassReflection();
35+
36+
if (!$classReflection->implementsInterface('Behat\Behat\Context\Context')) {
37+
return [];
38+
}
39+
40+
$usages = [];
41+
$className = $classReflection->getName();
42+
43+
foreach ($classReflection->getNativeReflection()->getMethods() as $method) {
44+
$methodName = $method->getName();
45+
46+
if ($method->isConstructor()) {
47+
$usages[] = $this->createUsage($className, $methodName, 'Behat context constructor');
48+
} elseif ($this->isBehatContextMethod($method)) {
49+
$usages[] = $this->createUsage($className, $methodName, 'Behat step definition or hook');
50+
}
51+
}
52+
53+
return $usages;
54+
}
55+
56+
private function isBehatContextMethod(ReflectionMethod $method): bool
57+
{
58+
return $this->hasAnnotation($method, '@Given')
59+
|| $this->hasAnnotation($method, '@When')
60+
|| $this->hasAnnotation($method, '@Then')
61+
|| $this->hasAnnotation($method, '@BeforeScenario')
62+
|| $this->hasAnnotation($method, '@AfterScenario')
63+
|| $this->hasAnnotation($method, '@BeforeStep')
64+
|| $this->hasAnnotation($method, '@AfterStep')
65+
|| $this->hasAnnotation($method, '@BeforeSuite')
66+
|| $this->hasAnnotation($method, '@AfterSuite')
67+
|| $this->hasAnnotation($method, '@BeforeFeature')
68+
|| $this->hasAnnotation($method, '@AfterFeature')
69+
|| $this->hasAnnotation($method, '@Transform')
70+
|| $this->hasAttribute($method, 'Behat\Step\Given')
71+
|| $this->hasAttribute($method, 'Behat\Step\When')
72+
|| $this->hasAttribute($method, 'Behat\Step\Then')
73+
|| $this->hasAttribute($method, 'Behat\Hook\BeforeScenario')
74+
|| $this->hasAttribute($method, 'Behat\Hook\AfterScenario')
75+
|| $this->hasAttribute($method, 'Behat\Hook\BeforeStep')
76+
|| $this->hasAttribute($method, 'Behat\Hook\AfterStep')
77+
|| $this->hasAttribute($method, 'Behat\Hook\BeforeSuite')
78+
|| $this->hasAttribute($method, 'Behat\Hook\AfterSuite')
79+
|| $this->hasAttribute($method, 'Behat\Hook\BeforeFeature')
80+
|| $this->hasAttribute($method, 'Behat\Hook\AfterFeature')
81+
|| $this->hasAttribute($method, 'Behat\Transformation\Transform');
82+
}
83+
84+
private function hasAnnotation(
85+
ReflectionMethod $method,
86+
string $string
87+
): bool
88+
{
89+
if ($method->getDocComment() === false) {
90+
return false;
91+
}
92+
93+
return strpos($method->getDocComment(), $string) !== false;
94+
}
95+
96+
private function hasAttribute(
97+
ReflectionMethod $method,
98+
string $attributeClass
99+
): bool
100+
{
101+
return $method->getAttributes($attributeClass) !== [];
102+
}
103+
104+
private function createUsage(
105+
string $className,
106+
string $methodName,
107+
string $reason
108+
): ClassMethodUsage
109+
{
110+
return new ClassMethodUsage(
111+
UsageOrigin::createVirtual($this, VirtualUsageData::withNote($reason)),
112+
new ClassMethodRef(
113+
$className,
114+
$methodName,
115+
false,
116+
),
117+
);
118+
}
119+
120+
}

tests/Rule/DeadCodeRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
use ShipMonk\PHPStan\DeadCode\Hierarchy\ClassHierarchy;
3838
use ShipMonk\PHPStan\DeadCode\Output\OutputEnhancer;
3939
use ShipMonk\PHPStan\DeadCode\Provider\ApiPhpDocUsageProvider;
40+
use ShipMonk\PHPStan\DeadCode\Provider\BehatUsageProvider;
4041
use ShipMonk\PHPStan\DeadCode\Provider\BuiltinUsageProvider;
4142
use ShipMonk\PHPStan\DeadCode\Provider\DoctrineUsageProvider;
4243
use ShipMonk\PHPStan\DeadCode\Provider\EnumUsageProvider;
@@ -859,6 +860,7 @@ public static function provideFiles(): Traversable
859860
yield 'provider-twig' => [__DIR__ . '/data/providers/twig.php', self::requiresPhp(8_00_00)];
860861
yield 'provider-phpunit' => [__DIR__ . '/data/providers/phpunit.php', self::requiresPhp(8_00_00)];
861862
yield 'provider-phpbench' => [__DIR__ . '/data/providers/phpbench.php', self::requiresPhp(8_00_00)];
863+
yield 'provider-behat' => [__DIR__ . '/data/providers/behat.php', self::requiresPhp(8_00_00)];
862864
yield 'provider-doctrine' => [__DIR__ . '/data/providers/doctrine.php', self::requiresPhp(8_01_00)];
863865
yield 'provider-phpstan' => [__DIR__ . '/data/providers/phpstan.php'];
864866
yield 'provider-nette' => [__DIR__ . '/data/providers/nette.php'];
@@ -1000,6 +1002,9 @@ private function getMemberUsageProviders(): array
10001002
self::getContainer()->getByType(PhpDocParser::class),
10011003
self::getContainer()->getByType(Lexer::class),
10021004
),
1005+
new BehatUsageProvider(
1006+
$this->providersEnabled,
1007+
),
10031008
new DoctrineUsageProvider(
10041009
$this->providersEnabled,
10051010
),

0 commit comments

Comments
 (0)