[PHPStan] Add rule to forbid test coverage attributes

This commit is contained in:
Oskar Stark
2025-09-24 07:09:19 +02:00
parent a086203aac
commit b8f4abe304
13 changed files with 95 additions and 38 deletions

View File

@@ -0,0 +1,94 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <oskarstark@googlemail.com>
*
* @implements Rule<Node>
*/
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;
}
}

View File

@@ -1,3 +1,4 @@
rules:
- Symfony\AI\PHPStan\ForbidDeclareStrictTypesRule
- Symfony\AI\PHPStan\ForbidNativeExceptionRule
- Symfony\AI\PHPStan\ForbidTestCoverageAttributesRule

View File

@@ -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()

View File

@@ -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;

View File

@@ -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()

View File

@@ -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\JsonRpcHandler;
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.')]

View File

@@ -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()

View File

@@ -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()

View File

@@ -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\Capability\Tool\ToolAnnotationsInterface;
use Symfony\AI\McpSdk\Message\Request;
use Symfony\AI\McpSdk\Server\RequestHandler\ToolListHandler;
#[Small]
#[CoversClass(ToolListHandler::class)]
class ToolListHandlerTest extends TestCase
{
public function testHandleEmpty()

View File

@@ -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;
use Symfony\AI\McpSdk\Server\JsonRpcHandler;
use Symfony\AI\McpSdk\Tests\Fixtures\InMemoryTransport;
#[Small]
#[CoversClass(Server::class)]
class ServerTest extends TestCase
{
public function testJsonExceptions()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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\Bridge\OpenAi\DallE\UrlImage;
use Symfony\AI\Platform\Result\RawHttpResult;
use Symfony\Contracts\HttpClient\ResponseInterface as HttpResponse;
#[UsesClass(Base64Image::class)]
final class ResultConverterTest extends TestCase
{
public function testItIsConvertingTheResponse()