From b8f4abe30477fc5eda3120cbe754d9cfdc53c672 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Wed, 24 Sep 2025 07:09:19 +0200 Subject: [PATCH] [PHPStan] Add rule to forbid test coverage attributes --- .phpstan/ForbidTestCoverageAttributesRule.php | 94 +++++++++++++++++++ .phpstan/extension.neon | 1 + src/mcp-sdk/tests/Message/ErrorTest.php | 4 - src/mcp-sdk/tests/Message/FactoryTest.php | 4 - src/mcp-sdk/tests/Message/ResponseTest.php | 4 - .../tests/Server/JsonRpcHandlerTest.php | 4 - .../RequestHandler/PromptListHandlerTest.php | 4 - .../ResourceListHandlerTest.php | 4 - .../RequestHandler/ToolListHandlerTest.php | 4 - src/mcp-sdk/tests/ServerTest.php | 4 - .../Bridge/OpenAi/DallE/Base64ImageTest.php | 2 - .../Bridge/OpenAi/DallE/ImageResultTest.php | 2 - .../OpenAi/DallE/ResultConverterTest.php | 2 - 13 files changed, 95 insertions(+), 38 deletions(-) create mode 100644 .phpstan/ForbidTestCoverageAttributesRule.php diff --git a/.phpstan/ForbidTestCoverageAttributesRule.php b/.phpstan/ForbidTestCoverageAttributesRule.php new file mode 100644 index 000000000..87ccceccd --- /dev/null +++ b/.phpstan/ForbidTestCoverageAttributesRule.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\PHPStan; + +use PhpParser\Node; +use PhpParser\Node\AttributeGroup; +use PhpParser\Node\Stmt\Class_; +use PhpParser\Node\Stmt\ClassMethod; +use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; + +/** + * PHPStan rule that forbids usage of test coverage attributes in tests. + * + * This rule enforces that Large, Small, Medium, CoversClass and UsesClass attributes + * should not be used in test files. + * + * @author Oskar Stark + * + * @implements Rule + */ +final class ForbidTestCoverageAttributesRule implements Rule +{ + private const FORBIDDEN_ATTRIBUTES = [ + 'Large', + 'Small', + 'Medium', + 'CoversClass', + 'UsesClass', + ]; + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + // Only check test files + if (!str_ends_with($scope->getFile(), 'Test.php')) { + return []; + } + + $errors = []; + + if ($node instanceof Class_ || $node instanceof ClassMethod) { + foreach ($node->attrGroups as $attrGroup) { + $errors = array_merge($errors, $this->checkAttributeGroup($attrGroup)); + } + } + + return $errors; + } + + /** + * @return array<\PHPStan\Rules\RuleError> + */ + private function checkAttributeGroup(AttributeGroup $attrGroup): array + { + $errors = []; + + foreach ($attrGroup->attrs as $attr) { + $attributeName = $attr->name->toString(); + + // Handle both fully qualified and short names + $shortName = $attributeName; + if (str_contains($attributeName, '\\')) { + $shortName = substr($attributeName, strrpos($attributeName, '\\') + 1); + } + + if (\in_array($shortName, self::FORBIDDEN_ATTRIBUTES, true)) { + $errors[] = RuleErrorBuilder::message( + \sprintf('Usage of #[%s] attribute is forbidden in test files. Remove the attribute.', $shortName) + ) + ->line($attr->getLine()) + ->identifier('symfonyAi.forbidTestCoverageAttributes') + ->tip(\sprintf('Remove the #[%s] attribute from the test.', $shortName)) + ->build(); + } + } + + return $errors; + } +} diff --git a/.phpstan/extension.neon b/.phpstan/extension.neon index ee6993669..4cfca1765 100644 --- a/.phpstan/extension.neon +++ b/.phpstan/extension.neon @@ -1,3 +1,4 @@ rules: - Symfony\AI\PHPStan\ForbidDeclareStrictTypesRule - Symfony\AI\PHPStan\ForbidNativeExceptionRule + - Symfony\AI\PHPStan\ForbidTestCoverageAttributesRule diff --git a/src/mcp-sdk/tests/Message/ErrorTest.php b/src/mcp-sdk/tests/Message/ErrorTest.php index 8ade8cf02..1fed107b7 100644 --- a/src/mcp-sdk/tests/Message/ErrorTest.php +++ b/src/mcp-sdk/tests/Message/ErrorTest.php @@ -11,13 +11,9 @@ namespace Symfony\AI\McpSdk\Tests\Message; -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\TestCase; use Symfony\AI\McpSdk\Message\Error; -#[Small] -#[CoversClass(Error::class)] final class ErrorTest extends TestCase { public function testWithIntegerId() diff --git a/src/mcp-sdk/tests/Message/FactoryTest.php b/src/mcp-sdk/tests/Message/FactoryTest.php index 69b146a13..6d0cdf10d 100644 --- a/src/mcp-sdk/tests/Message/FactoryTest.php +++ b/src/mcp-sdk/tests/Message/FactoryTest.php @@ -11,16 +11,12 @@ namespace Symfony\AI\McpSdk\Tests\Message; -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\TestCase; use Symfony\AI\McpSdk\Exception\InvalidInputMessageException; use Symfony\AI\McpSdk\Message\Factory; use Symfony\AI\McpSdk\Message\Notification; use Symfony\AI\McpSdk\Message\Request; -#[Small] -#[CoversClass(Factory::class)] final class FactoryTest extends TestCase { private Factory $factory; diff --git a/src/mcp-sdk/tests/Message/ResponseTest.php b/src/mcp-sdk/tests/Message/ResponseTest.php index 81e9c31cc..a8dd98e29 100644 --- a/src/mcp-sdk/tests/Message/ResponseTest.php +++ b/src/mcp-sdk/tests/Message/ResponseTest.php @@ -11,13 +11,9 @@ namespace Symfony\AI\McpSdk\Tests\Message; -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\TestCase; use Symfony\AI\McpSdk\Message\Response; -#[Small] -#[CoversClass(Response::class)] final class ResponseTest extends TestCase { public function testWithIntegerId() diff --git a/src/mcp-sdk/tests/Server/JsonRpcHandlerTest.php b/src/mcp-sdk/tests/Server/JsonRpcHandlerTest.php index 506610967..e3aad8fd2 100644 --- a/src/mcp-sdk/tests/Server/JsonRpcHandlerTest.php +++ b/src/mcp-sdk/tests/Server/JsonRpcHandlerTest.php @@ -11,8 +11,6 @@ namespace Symfony\AI\McpSdk\Tests\Server; -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; use Psr\Log\NullLogger; @@ -22,8 +20,6 @@ use Symfony\AI\McpSdk\Server\NotificationHandlerInterface; use Symfony\AI\McpSdk\Server\RequestHandlerInterface; -#[Small] -#[CoversClass(JsonRpcHandler::class)] class JsonRpcHandlerTest extends TestCase { #[TestDox('Make sure a single notification can be handled by multiple handlers.')] diff --git a/src/mcp-sdk/tests/Server/RequestHandler/PromptListHandlerTest.php b/src/mcp-sdk/tests/Server/RequestHandler/PromptListHandlerTest.php index 60ac5f34a..050bc05ba 100644 --- a/src/mcp-sdk/tests/Server/RequestHandler/PromptListHandlerTest.php +++ b/src/mcp-sdk/tests/Server/RequestHandler/PromptListHandlerTest.php @@ -11,16 +11,12 @@ namespace Symfony\AI\McpSdk\Tests\Server\RequestHandler; -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\TestCase; use Symfony\AI\McpSdk\Capability\Prompt\MetadataInterface; use Symfony\AI\McpSdk\Capability\PromptChain; use Symfony\AI\McpSdk\Message\Request; use Symfony\AI\McpSdk\Server\RequestHandler\PromptListHandler; -#[Small] -#[CoversClass(PromptListHandler::class)] class PromptListHandlerTest extends TestCase { public function testHandleEmpty() diff --git a/src/mcp-sdk/tests/Server/RequestHandler/ResourceListHandlerTest.php b/src/mcp-sdk/tests/Server/RequestHandler/ResourceListHandlerTest.php index cae7b0494..fcbe385b0 100644 --- a/src/mcp-sdk/tests/Server/RequestHandler/ResourceListHandlerTest.php +++ b/src/mcp-sdk/tests/Server/RequestHandler/ResourceListHandlerTest.php @@ -11,17 +11,13 @@ namespace Symfony\AI\McpSdk\Tests\Server\RequestHandler; -use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\TestCase; use Symfony\AI\McpSdk\Capability\Resource\CollectionInterface; use Symfony\AI\McpSdk\Capability\Resource\MetadataInterface; use Symfony\AI\McpSdk\Message\Request; use Symfony\AI\McpSdk\Server\RequestHandler\ResourceListHandler; -#[Small] -#[CoversClass(ResourceListHandler::class)] class ResourceListHandlerTest extends TestCase { public function testHandleEmpty() diff --git a/src/mcp-sdk/tests/Server/RequestHandler/ToolListHandlerTest.php b/src/mcp-sdk/tests/Server/RequestHandler/ToolListHandlerTest.php index f44649953..2d291b4ff 100644 --- a/src/mcp-sdk/tests/Server/RequestHandler/ToolListHandlerTest.php +++ b/src/mcp-sdk/tests/Server/RequestHandler/ToolListHandlerTest.php @@ -11,9 +11,7 @@ namespace Symfony\AI\McpSdk\Tests\Server\RequestHandler; -use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\TestCase; use Symfony\AI\McpSdk\Capability\Tool\CollectionInterface; use Symfony\AI\McpSdk\Capability\Tool\MetadataInterface; @@ -21,8 +19,6 @@ use Symfony\AI\McpSdk\Message\Request; use Symfony\AI\McpSdk\Server\RequestHandler\ToolListHandler; -#[Small] -#[CoversClass(ToolListHandler::class)] class ToolListHandlerTest extends TestCase { public function testHandleEmpty() diff --git a/src/mcp-sdk/tests/ServerTest.php b/src/mcp-sdk/tests/ServerTest.php index 975399e4a..acfc4b3cb 100644 --- a/src/mcp-sdk/tests/ServerTest.php +++ b/src/mcp-sdk/tests/ServerTest.php @@ -11,8 +11,6 @@ namespace Symfony\AI\McpSdk\Tests; -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\MockObject\Stub\Exception; use PHPUnit\Framework\TestCase; use Psr\Log\NullLogger; @@ -20,8 +18,6 @@ use Symfony\AI\McpSdk\Server\JsonRpcHandler; use Symfony\AI\McpSdk\Tests\Fixtures\InMemoryTransport; -#[Small] -#[CoversClass(Server::class)] class ServerTest extends TestCase { public function testJsonExceptions() diff --git a/src/platform/tests/Bridge/OpenAi/DallE/Base64ImageTest.php b/src/platform/tests/Bridge/OpenAi/DallE/Base64ImageTest.php index 93dde4a19..79eddb197 100644 --- a/src/platform/tests/Bridge/OpenAi/DallE/Base64ImageTest.php +++ b/src/platform/tests/Bridge/OpenAi/DallE/Base64ImageTest.php @@ -11,11 +11,9 @@ namespace Symfony\AI\Platform\Tests\Bridge\OpenAi\DallE; -use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Symfony\AI\Platform\Bridge\OpenAi\DallE\Base64Image; -#[CoversClass(Base64Image::class)] final class Base64ImageTest extends TestCase { public function testItCreatesBase64Image() diff --git a/src/platform/tests/Bridge/OpenAi/DallE/ImageResultTest.php b/src/platform/tests/Bridge/OpenAi/DallE/ImageResultTest.php index 1c0e26feb..cae6017ee 100644 --- a/src/platform/tests/Bridge/OpenAi/DallE/ImageResultTest.php +++ b/src/platform/tests/Bridge/OpenAi/DallE/ImageResultTest.php @@ -11,13 +11,11 @@ namespace Symfony\AI\Platform\Tests\Bridge\OpenAi\DallE; -use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; use Symfony\AI\Platform\Bridge\OpenAi\DallE\Base64Image; use Symfony\AI\Platform\Bridge\OpenAi\DallE\ImageResult; use Symfony\AI\Platform\Bridge\OpenAi\DallE\UrlImage; -#[UsesClass(Base64Image::class)] final class ImageResultTest extends TestCase { public function testItCreatesImagesResult() diff --git a/src/platform/tests/Bridge/OpenAi/DallE/ResultConverterTest.php b/src/platform/tests/Bridge/OpenAi/DallE/ResultConverterTest.php index 88f4a57ab..d96030c46 100644 --- a/src/platform/tests/Bridge/OpenAi/DallE/ResultConverterTest.php +++ b/src/platform/tests/Bridge/OpenAi/DallE/ResultConverterTest.php @@ -11,7 +11,6 @@ namespace Symfony\AI\Platform\Tests\Bridge\OpenAi\DallE; -use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; use Symfony\AI\Platform\Bridge\OpenAi\DallE\Base64Image; use Symfony\AI\Platform\Bridge\OpenAi\DallE\ImageResult; @@ -20,7 +19,6 @@ use Symfony\AI\Platform\Result\RawHttpResult; use Symfony\Contracts\HttpClient\ResponseInterface as HttpResponse; -#[UsesClass(Base64Image::class)] final class ResultConverterTest extends TestCase { public function testItIsConvertingTheResponse()