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
1 change: 1 addition & 0 deletions docs/ForbidPestPhpOnly.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,6 @@ uses()->group('integration'); // ✓ Valid
## Important Notes

- The rule considers a file to be a test when it resides inside a `tests/` directory, ends with `Test.php`, or is named `Pest.php`
- Only `->only()` calls chained from Pest's `test()` or `it()` helpers are reported; other methods named `only` are ignored
- `only()` is meant for temporary local debugging; remove the call before committing
- Pest also provides other granular filters (`--filter`, `--group`, datasets). Prefer those when you need persistent targeting without modifying source files
36 changes: 36 additions & 0 deletions src/Rules/ForbidPestPhpOnly/ForbidPestPhpOnlyRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
namespace Orrison\MeliorStan\Rules\ForbidPestPhpOnly;

use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
Expand All @@ -17,6 +20,11 @@ class ForbidPestPhpOnlyRule implements Rule
{
public const ERROR_MESSAGE = 'Pest\'s only() filter should not be used in committed tests.';

private const PEST_ENTRY_POINTS = [
'test',
'it',
];

/**
* @return class-string<Node>
*/
Expand All @@ -42,13 +50,41 @@ public function processNode(Node $node, Scope $scope): array
return [];
}

if (! $this->originatesFromPestEntry($node)) {
return [];
}

return [
RuleErrorBuilder::message(self::ERROR_MESSAGE)
->identifier('MeliorStan.forbidPestPhpOnly')
->build(),
];
}

protected function originatesFromPestEntry(MethodCall $node): bool
{
/** @var Expr $expression */
$expression = $node->var;

while ($expression instanceof MethodCall) {
/** @var MethodCall $innerCall */
$innerCall = $expression;
$expression = $innerCall->var;
}

if (! $expression instanceof FuncCall) {
return false;
}

$name = $expression->name;

if (! $name instanceof Name) {
return false;
}

return in_array(strtolower($name->getLast()), self::PEST_ENTRY_POINTS, true);
}

protected function isTestFile(string $filePath): bool
{
$normalized = str_replace('\\', '/', $filePath);
Expand Down
2 changes: 0 additions & 2 deletions tests/Rules/ForbidPestPhpOnly/Fixture/InvalidPestFixture.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,3 @@
it('can run another test', function () {
expect(true)->toBeTrue();
})->only();

uses()->group('integration')->only();
35 changes: 35 additions & 0 deletions tests/Rules/ForbidPestPhpOnly/Fixture/NonPestOnlyUsageFixture.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

class NonPestOnlyUsageFixture
{
public function demonstrate(): void
{
$collection = new class () {
public function only(): void {}

public function filter(): self
{
return $this;
}
};

$collection->only();
$collection->filter()->only();
}

public function useProperty(): void
{
$helper = new class () {
public function only(): void {}
};

$this->callOnly($helper);
}

protected function callOnly(object $object): void
{
if (method_exists($object, 'only')) {
$object->only();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ public function testOnlyUsageIsReported(): void
], [
[ForbidPestPhpOnlyRule::ERROR_MESSAGE, 3],
[ForbidPestPhpOnlyRule::ERROR_MESSAGE, 7],
[ForbidPestPhpOnlyRule::ERROR_MESSAGE, 11],
]);
}

public function testValidUsagePasses(): void
{
$this->analyse([
__DIR__ . '/Fixture/ValidPestFixture.php',
__DIR__ . '/Fixture/NonPestOnlyUsageFixture.php',
], []);
}

Expand Down