Skip to content

Commit c0a09f9

Browse files
committed
minor #662 [PHPStan] Add rule to forbid test coverage attributes (OskarStark)
This PR was squashed before being merged into the main branch. Discussion ---------- [PHPStan] Add rule to forbid test coverage attributes | Q | A | ------------- | --- | Bug fix? | no | New feature? | no | Docs? | no | Issues | Follows #647 | License | MIT This rule prevents the use of Large, Small, Medium, CoversClass and UsesClass attributes in test files, helping maintain consistency in the test codebase by avoiding these coverage-related attributes. The rule operates only on files ending with 'Test.php' and provides clear error messages with tips for resolving violations. ### Proof <img width="1602" height="1292" alt="CleanShot 2025-09-24 at 07 18 01@2x" src="https://github.com/user-attachments/assets/0f60461f-c429-4bf3-a364-c051e6f1b686" /> Commits ------- b8f4abe [PHPStan] Add rule to forbid test coverage attributes
2 parents 44f9b0f + b8f4abe commit c0a09f9

13 files changed

+95
-38
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\PHPStan;
13+
14+
use PhpParser\Node;
15+
use PhpParser\Node\AttributeGroup;
16+
use PhpParser\Node\Stmt\Class_;
17+
use PhpParser\Node\Stmt\ClassMethod;
18+
use PHPStan\Analyser\Scope;
19+
use PHPStan\Rules\Rule;
20+
use PHPStan\Rules\RuleErrorBuilder;
21+
22+
/**
23+
* PHPStan rule that forbids usage of test coverage attributes in tests.
24+
*
25+
* This rule enforces that Large, Small, Medium, CoversClass and UsesClass attributes
26+
* should not be used in test files.
27+
*
28+
* @author Oskar Stark <[email protected]>
29+
*
30+
* @implements Rule<Node>
31+
*/
32+
final class ForbidTestCoverageAttributesRule implements Rule
33+
{
34+
private const FORBIDDEN_ATTRIBUTES = [
35+
'Large',
36+
'Small',
37+
'Medium',
38+
'CoversClass',
39+
'UsesClass',
40+
];
41+
42+
public function getNodeType(): string
43+
{
44+
return Node::class;
45+
}
46+
47+
public function processNode(Node $node, Scope $scope): array
48+
{
49+
// Only check test files
50+
if (!str_ends_with($scope->getFile(), 'Test.php')) {
51+
return [];
52+
}
53+
54+
$errors = [];
55+
56+
if ($node instanceof Class_ || $node instanceof ClassMethod) {
57+
foreach ($node->attrGroups as $attrGroup) {
58+
$errors = array_merge($errors, $this->checkAttributeGroup($attrGroup));
59+
}
60+
}
61+
62+
return $errors;
63+
}
64+
65+
/**
66+
* @return array<\PHPStan\Rules\RuleError>
67+
*/
68+
private function checkAttributeGroup(AttributeGroup $attrGroup): array
69+
{
70+
$errors = [];
71+
72+
foreach ($attrGroup->attrs as $attr) {
73+
$attributeName = $attr->name->toString();
74+
75+
// Handle both fully qualified and short names
76+
$shortName = $attributeName;
77+
if (str_contains($attributeName, '\\')) {
78+
$shortName = substr($attributeName, strrpos($attributeName, '\\') + 1);
79+
}
80+
81+
if (\in_array($shortName, self::FORBIDDEN_ATTRIBUTES, true)) {
82+
$errors[] = RuleErrorBuilder::message(
83+
\sprintf('Usage of #[%s] attribute is forbidden in test files. Remove the attribute.', $shortName)
84+
)
85+
->line($attr->getLine())
86+
->identifier('symfonyAi.forbidTestCoverageAttributes')
87+
->tip(\sprintf('Remove the #[%s] attribute from the test.', $shortName))
88+
->build();
89+
}
90+
}
91+
92+
return $errors;
93+
}
94+
}

.phpstan/extension.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
rules:
22
- Symfony\AI\PHPStan\ForbidDeclareStrictTypesRule
33
- Symfony\AI\PHPStan\ForbidNativeExceptionRule
4+
- Symfony\AI\PHPStan\ForbidTestCoverageAttributesRule

src/mcp-sdk/tests/Message/ErrorTest.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,9 @@
1111

1212
namespace Symfony\AI\McpSdk\Tests\Message;
1313

14-
use PHPUnit\Framework\Attributes\CoversClass;
15-
use PHPUnit\Framework\Attributes\Small;
1614
use PHPUnit\Framework\TestCase;
1715
use Symfony\AI\McpSdk\Message\Error;
1816

19-
#[Small]
20-
#[CoversClass(Error::class)]
2117
final class ErrorTest extends TestCase
2218
{
2319
public function testWithIntegerId()

src/mcp-sdk/tests/Message/FactoryTest.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,12 @@
1111

1212
namespace Symfony\AI\McpSdk\Tests\Message;
1313

14-
use PHPUnit\Framework\Attributes\CoversClass;
15-
use PHPUnit\Framework\Attributes\Small;
1614
use PHPUnit\Framework\TestCase;
1715
use Symfony\AI\McpSdk\Exception\InvalidInputMessageException;
1816
use Symfony\AI\McpSdk\Message\Factory;
1917
use Symfony\AI\McpSdk\Message\Notification;
2018
use Symfony\AI\McpSdk\Message\Request;
2119

22-
#[Small]
23-
#[CoversClass(Factory::class)]
2420
final class FactoryTest extends TestCase
2521
{
2622
private Factory $factory;

src/mcp-sdk/tests/Message/ResponseTest.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,9 @@
1111

1212
namespace Symfony\AI\McpSdk\Tests\Message;
1313

14-
use PHPUnit\Framework\Attributes\CoversClass;
15-
use PHPUnit\Framework\Attributes\Small;
1614
use PHPUnit\Framework\TestCase;
1715
use Symfony\AI\McpSdk\Message\Response;
1816

19-
#[Small]
20-
#[CoversClass(Response::class)]
2117
final class ResponseTest extends TestCase
2218
{
2319
public function testWithIntegerId()

src/mcp-sdk/tests/Server/JsonRpcHandlerTest.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111

1212
namespace Symfony\AI\McpSdk\Tests\Server;
1313

14-
use PHPUnit\Framework\Attributes\CoversClass;
15-
use PHPUnit\Framework\Attributes\Small;
1614
use PHPUnit\Framework\Attributes\TestDox;
1715
use PHPUnit\Framework\TestCase;
1816
use Psr\Log\NullLogger;
@@ -22,8 +20,6 @@
2220
use Symfony\AI\McpSdk\Server\NotificationHandlerInterface;
2321
use Symfony\AI\McpSdk\Server\RequestHandlerInterface;
2422

25-
#[Small]
26-
#[CoversClass(JsonRpcHandler::class)]
2723
class JsonRpcHandlerTest extends TestCase
2824
{
2925
#[TestDox('Make sure a single notification can be handled by multiple handlers.')]

src/mcp-sdk/tests/Server/RequestHandler/PromptListHandlerTest.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,12 @@
1111

1212
namespace Symfony\AI\McpSdk\Tests\Server\RequestHandler;
1313

14-
use PHPUnit\Framework\Attributes\CoversClass;
15-
use PHPUnit\Framework\Attributes\Small;
1614
use PHPUnit\Framework\TestCase;
1715
use Symfony\AI\McpSdk\Capability\Prompt\MetadataInterface;
1816
use Symfony\AI\McpSdk\Capability\PromptChain;
1917
use Symfony\AI\McpSdk\Message\Request;
2018
use Symfony\AI\McpSdk\Server\RequestHandler\PromptListHandler;
2119

22-
#[Small]
23-
#[CoversClass(PromptListHandler::class)]
2420
class PromptListHandlerTest extends TestCase
2521
{
2622
public function testHandleEmpty()

src/mcp-sdk/tests/Server/RequestHandler/ResourceListHandlerTest.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,13 @@
1111

1212
namespace Symfony\AI\McpSdk\Tests\Server\RequestHandler;
1313

14-
use PHPUnit\Framework\Attributes\CoversClass;
1514
use PHPUnit\Framework\Attributes\DataProvider;
16-
use PHPUnit\Framework\Attributes\Small;
1715
use PHPUnit\Framework\TestCase;
1816
use Symfony\AI\McpSdk\Capability\Resource\CollectionInterface;
1917
use Symfony\AI\McpSdk\Capability\Resource\MetadataInterface;
2018
use Symfony\AI\McpSdk\Message\Request;
2119
use Symfony\AI\McpSdk\Server\RequestHandler\ResourceListHandler;
2220

23-
#[Small]
24-
#[CoversClass(ResourceListHandler::class)]
2521
class ResourceListHandlerTest extends TestCase
2622
{
2723
public function testHandleEmpty()

src/mcp-sdk/tests/Server/RequestHandler/ToolListHandlerTest.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,14 @@
1111

1212
namespace Symfony\AI\McpSdk\Tests\Server\RequestHandler;
1313

14-
use PHPUnit\Framework\Attributes\CoversClass;
1514
use PHPUnit\Framework\Attributes\DataProvider;
16-
use PHPUnit\Framework\Attributes\Small;
1715
use PHPUnit\Framework\TestCase;
1816
use Symfony\AI\McpSdk\Capability\Tool\CollectionInterface;
1917
use Symfony\AI\McpSdk\Capability\Tool\MetadataInterface;
2018
use Symfony\AI\McpSdk\Capability\Tool\ToolAnnotationsInterface;
2119
use Symfony\AI\McpSdk\Message\Request;
2220
use Symfony\AI\McpSdk\Server\RequestHandler\ToolListHandler;
2321

24-
#[Small]
25-
#[CoversClass(ToolListHandler::class)]
2622
class ToolListHandlerTest extends TestCase
2723
{
2824
public function testHandleEmpty()

src/mcp-sdk/tests/ServerTest.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,13 @@
1111

1212
namespace Symfony\AI\McpSdk\Tests;
1313

14-
use PHPUnit\Framework\Attributes\CoversClass;
15-
use PHPUnit\Framework\Attributes\Small;
1614
use PHPUnit\Framework\MockObject\Stub\Exception;
1715
use PHPUnit\Framework\TestCase;
1816
use Psr\Log\NullLogger;
1917
use Symfony\AI\McpSdk\Server;
2018
use Symfony\AI\McpSdk\Server\JsonRpcHandler;
2119
use Symfony\AI\McpSdk\Tests\Fixtures\InMemoryTransport;
2220

23-
#[Small]
24-
#[CoversClass(Server::class)]
2521
class ServerTest extends TestCase
2622
{
2723
public function testJsonExceptions()

0 commit comments

Comments
 (0)