From e58db2b253026e00a0401d6c48b4138a251c9991 Mon Sep 17 00:00:00 2001 From: Farhad Safarov Date: Sat, 17 Dec 2022 20:19:16 +0300 Subject: [PATCH] Psalm 5 (#293) --- .github/workflows/integrate.yaml | 1 - composer.json | 12 +++--- src/Handler/ConsoleHandler.php | 21 ++++----- src/Handler/ContainerHandler.php | 4 +- src/Handler/DoctrineRepositoryHandler.php | 8 ++-- src/Plugin.php | 3 +- .../Controller/AbstractController.stubphp | 27 ------------ .../HttpFoundation/ParameterBag.stubphp | 28 ------------ .../Normalizer/DenormalizerInterface.stubphp | 15 ------- .../Serializer/SerializerInterface.stubphp | 15 ------- .../Normalizer/DenormalizerInterface.stubphp | 4 +- .../Normalizer/DenormalizerInterface.stubphp | 4 +- .../HttpFoundation/HeaderBag.stubphp | 6 +++ .../Component/HttpFoundation/Request.stubphp | 8 ++-- .../Component/HttpFoundation/Response.stubphp | 2 +- src/Test/CodeceptionModule.php | 3 +- src/Twig/AnalyzedTemplatesTainter.php | 5 +-- src/Twig/CachedTemplateNotFoundException.php | 4 +- src/Twig/CachedTemplatesMapping.php | 3 +- src/Twig/CachedTemplatesRegistry.php | 6 +-- src/Twig/CachedTemplatesTainter.php | 3 +- src/Twig/PrintNodeAnalyzer.php | 3 +- .../acceptance/AbstractController.feature | 2 +- .../acceptance/AuthenticatorInterface.feature | 10 ++--- .../acceptance/DenormalizerInterface.feature | 12 ++++-- tests/acceptance/acceptance/Envelope.feature | 4 +- tests/acceptance/acceptance/InputBag.feature | 27 ++++++------ .../acceptance/ParameterBag.feature | 2 +- .../acceptance/RequestContent.feature | 25 +++++++---- tests/acceptance/acceptance/Tainting.feature | 33 +++++++------- .../acceptance/console/ConsoleOption.feature | 2 +- .../acceptance/acceptance/forms/Form.feature | 2 +- .../serializer/SerializerInterface.feature | 5 ++- .../serializer/DenormalizerInterface.feature | 43 +++++++++++++++++++ .../validator/ConstraintValidator.feature | 2 +- tests/unit/Symfony/TwigUtilsTest.php | 4 +- 36 files changed, 162 insertions(+), 196 deletions(-) delete mode 100644 src/Stubs/4/Bundle/FrameworkBundle/Controller/AbstractController.stubphp delete mode 100644 src/Stubs/4/Component/HttpFoundation/ParameterBag.stubphp delete mode 100644 src/Stubs/4/Component/Serializer/Normalizer/DenormalizerInterface.stubphp delete mode 100644 src/Stubs/4/Component/Serializer/SerializerInterface.stubphp rename src/Stubs/{common => 6}/Component/Serializer/Normalizer/DenormalizerInterface.stubphp (70%) create mode 100644 tests/acceptance/acceptance/symfony6/serializer/DenormalizerInterface.feature diff --git a/.github/workflows/integrate.yaml b/.github/workflows/integrate.yaml index 0868121..d8e32ba 100644 --- a/.github/workflows/integrate.yaml +++ b/.github/workflows/integrate.yaml @@ -68,7 +68,6 @@ jobs: - 8.1 symfony-version: - - 4 - 5 - 6 diff --git a/composer.json b/composer.json index 06c8a4b..ee91a30 100644 --- a/composer.json +++ b/composer.json @@ -12,19 +12,19 @@ "require": { "php": "^7.4 || ^8.0", "ext-simplexml": "*", - "symfony/framework-bundle": "^4.0 || ^5.0 || ^6.0", - "vimeo/psalm": "^4.12" + "symfony/framework-bundle": "^5.0 || ^6.0", + "vimeo/psalm": "^5.1" }, "require-dev": { - "symfony/form": "^4.0 || ^5.0 || ^6.0", + "symfony/form": "^5.0 || ^6.0", "doctrine/annotations": "^1.8", - "doctrine/orm": "^2.7", + "doctrine/orm": "^2.9", "phpunit/phpunit": "~7.5 || ~9.5", "symfony/cache-contracts": "^1.0 || ^2.0", "symfony/console": "*", - "symfony/messenger": "^4.2 || ^5.0 || ^6.0", + "symfony/messenger": "^5.0 || ^6.0", "symfony/security-guard": "*", - "symfony/serializer": "^4.0 || ^5.0 || ^6.0", + "symfony/serializer": "^5.0 || ^6.0", "symfony/validator": "*", "twig/twig": "^2.10 || ^3.0", "weirdan/codeception-psalm-module": "dev-master" diff --git a/src/Handler/ConsoleHandler.php b/src/Handler/ConsoleHandler.php index e5eb7d9..8af87c6 100644 --- a/src/Handler/ConsoleHandler.php +++ b/src/Handler/ConsoleHandler.php @@ -21,6 +21,7 @@ use Psalm\Type\Atomic\TBool; use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TString; +use Psalm\Type\MutableUnion; use Psalm\Type\Union; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; @@ -31,11 +32,11 @@ class ConsoleHandler implements AfterMethodCallAnalysisInterface /** * @var Union[] */ - private static $arguments = []; + private static array $arguments = []; /** * @var Union[] */ - private static $options = []; + private static array $options = []; /** * {@inheritdoc} @@ -149,11 +150,11 @@ class ConsoleHandler implements AfterMethodCallAnalysisInterface } if ($mode & InputArgument::IS_ARRAY) { - $returnTypes = new Union([new TArray([new Union([new TInt()]), new Union([new TString()])])]); + $returnTypes = new MutableUnion([new TArray([new Union([new TInt()]), new Union([new TString()])])]); } elseif ($mode & InputArgument::REQUIRED) { - $returnTypes = new Union([new TString()]); + $returnTypes = new MutableUnion([new TString()]); } else { - $returnTypes = new Union([new TString(), new TNull()]); + $returnTypes = new MutableUnion([new TString(), new TNull()]); } $defaultParam = $normalizedParams['default']; @@ -164,7 +165,7 @@ class ConsoleHandler implements AfterMethodCallAnalysisInterface } } - self::$arguments[$identifier] = $returnTypes; + self::$arguments[$identifier] = $returnTypes->freeze(); } /** @@ -199,7 +200,7 @@ class ConsoleHandler implements AfterMethodCallAnalysisInterface $mode = InputOption::VALUE_OPTIONAL; } - $returnTypes = new Union([new TString(), new TNull()]); + $returnTypes = new MutableUnion([new TString(), new TNull()]); $defaultParam = $normalizedParams['default']; if ($defaultParam) { @@ -221,7 +222,7 @@ class ConsoleHandler implements AfterMethodCallAnalysisInterface } if ($mode & InputOption::VALUE_NONE) { - $returnTypes = new Union([new TBool()]); + $returnTypes = new MutableUnion([new TBool()]); } if ($mode & InputOption::VALUE_REQUIRED && $mode & InputOption::VALUE_IS_ARRAY) { @@ -229,10 +230,10 @@ class ConsoleHandler implements AfterMethodCallAnalysisInterface } if ($mode & InputOption::VALUE_IS_ARRAY) { - $returnTypes = new Union([new TArray([new Union([new TInt()]), $returnTypes])]); + $returnTypes = new MutableUnion([new TArray([new Union([new TInt()]), $returnTypes->freeze()])]); } - self::$options[$identifier] = $returnTypes; + self::$options[$identifier] = $returnTypes->freeze(); } /** diff --git a/src/Handler/ContainerHandler.php b/src/Handler/ContainerHandler.php index d24df3a..3356aa9 100644 --- a/src/Handler/ContainerHandler.php +++ b/src/Handler/ContainerHandler.php @@ -2,8 +2,6 @@ namespace Psalm\SymfonyPsalmPlugin\Handler; -use function constant; - use PhpParser\Node\Arg; use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Identifier; @@ -105,7 +103,7 @@ class ContainerHandler implements AfterMethodCallAnalysisInterface, AfterClassLi $serviceId = $className; } else { try { - $serviceId = constant($className.'::'.$idArgument->name->name); + $serviceId = \constant($className.'::'.$idArgument->name->name); } catch (\Exception $e) { return; } diff --git a/src/Handler/DoctrineRepositoryHandler.php b/src/Handler/DoctrineRepositoryHandler.php index 0569c4e..899f50e 100644 --- a/src/Handler/DoctrineRepositoryHandler.php +++ b/src/Handler/DoctrineRepositoryHandler.php @@ -17,8 +17,6 @@ use Psalm\Plugin\EventHandler\Event\AfterMethodCallAnalysisEvent; use Psalm\SymfonyPsalmPlugin\Issue\RepositoryStringShortcut; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Union; -use ReflectionClass; -use ReflectionException; class DoctrineRepositoryHandler implements AfterMethodCallAnalysisInterface, AfterClassLikeVisitInterface { @@ -51,9 +49,9 @@ class DoctrineRepositoryHandler implements AfterMethodCallAnalysisInterface, Aft } try { - $reflectionClass = new ReflectionClass($className); + $reflectionClass = new \ReflectionClass($className); - if (\PHP_VERSION_ID >= 80000 && method_exists(ReflectionClass::class, 'getAttributes')) { + if (\PHP_VERSION_ID >= 80000 && method_exists(\ReflectionClass::class, 'getAttributes')) { $entityAttributes = $reflectionClass->getAttributes(EntityAnnotation::class); foreach ($entityAttributes as $entityAttribute) { @@ -76,7 +74,7 @@ class DoctrineRepositoryHandler implements AfterMethodCallAnalysisInterface, Aft $event->setReturnTypeCandidate(new Union([new TNamedObject($entityAnnotation->repositoryClass)])); } } - } catch (ReflectionException $e) { + } catch (\ReflectionException $e) { } } } diff --git a/src/Plugin.php b/src/Plugin.php index b82be3a..6bb80dc 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -21,7 +21,6 @@ use Psalm\SymfonyPsalmPlugin\Twig\AnalyzedTemplatesTainter; use Psalm\SymfonyPsalmPlugin\Twig\CachedTemplatesMapping; use Psalm\SymfonyPsalmPlugin\Twig\CachedTemplatesTainter; use Psalm\SymfonyPsalmPlugin\Twig\TemplateFileAnalyzer; -use SimpleXMLElement; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\HttpKernel\Kernel; @@ -33,7 +32,7 @@ class Plugin implements PluginEntryPointInterface /** * {@inheritdoc} */ - public function __invoke(RegistrationInterface $api, SimpleXMLElement $config = null): void + public function __invoke(RegistrationInterface $api, \SimpleXMLElement $config = null): void { require_once __DIR__.'/Handler/HeaderBagHandler.php'; require_once __DIR__.'/Handler/ContainerHandler.php'; diff --git a/src/Stubs/4/Bundle/FrameworkBundle/Controller/AbstractController.stubphp b/src/Stubs/4/Bundle/FrameworkBundle/Controller/AbstractController.stubphp deleted file mode 100644 index 9dd5b34..0000000 --- a/src/Stubs/4/Bundle/FrameworkBundle/Controller/AbstractController.stubphp +++ /dev/null @@ -1,27 +0,0 @@ - - * - * @psalm-param class-string $type - * - * @psalm-return FormInterface - */ - public function createForm(string $type, $data = null, array $options = []): FormInterface {} -} diff --git a/src/Stubs/4/Component/HttpFoundation/ParameterBag.stubphp b/src/Stubs/4/Component/HttpFoundation/ParameterBag.stubphp deleted file mode 100644 index 0f7462b..0000000 --- a/src/Stubs/4/Component/HttpFoundation/ParameterBag.stubphp +++ /dev/null @@ -1,28 +0,0 @@ - - * @psalm-param mixed $data - * @psalm-param TType $type - * @psalm-return (TType is class-string ? TObject : mixed) - */ - public function denormalize($data, string $type, string $format = null, array $context = []); -} diff --git a/src/Stubs/4/Component/Serializer/SerializerInterface.stubphp b/src/Stubs/4/Component/Serializer/SerializerInterface.stubphp deleted file mode 100644 index a2f1aa1..0000000 --- a/src/Stubs/4/Component/Serializer/SerializerInterface.stubphp +++ /dev/null @@ -1,15 +0,0 @@ - - * @psalm-param mixed $data - * @psalm-param TType $type - * @psalm-return (TType is class-string ? TObject : mixed) - */ - public function deserialize($data, string $type, string $format, array $context = []); -} diff --git a/src/Stubs/5/Component/Serializer/Normalizer/DenormalizerInterface.stubphp b/src/Stubs/5/Component/Serializer/Normalizer/DenormalizerInterface.stubphp index e559b75..47df42b 100644 --- a/src/Stubs/5/Component/Serializer/Normalizer/DenormalizerInterface.stubphp +++ b/src/Stubs/5/Component/Serializer/Normalizer/DenormalizerInterface.stubphp @@ -7,9 +7,9 @@ interface DenormalizerInterface /** * @template TObject of object * @template TType of string|class-string - * @psalm-param mixed $data + * * @psalm-param TType $type * @psalm-return (TType is class-string ? TObject : mixed) */ - public function denormalize($data, string $type, string $format = null, array $context = []); + public function denormalize(mixed $data, string $type, string $format = null, array $context = []); } diff --git a/src/Stubs/common/Component/Serializer/Normalizer/DenormalizerInterface.stubphp b/src/Stubs/6/Component/Serializer/Normalizer/DenormalizerInterface.stubphp similarity index 70% rename from src/Stubs/common/Component/Serializer/Normalizer/DenormalizerInterface.stubphp rename to src/Stubs/6/Component/Serializer/Normalizer/DenormalizerInterface.stubphp index e559b75..47df42b 100644 --- a/src/Stubs/common/Component/Serializer/Normalizer/DenormalizerInterface.stubphp +++ b/src/Stubs/6/Component/Serializer/Normalizer/DenormalizerInterface.stubphp @@ -7,9 +7,9 @@ interface DenormalizerInterface /** * @template TObject of object * @template TType of string|class-string - * @psalm-param mixed $data + * * @psalm-param TType $type * @psalm-return (TType is class-string ? TObject : mixed) */ - public function denormalize($data, string $type, string $format = null, array $context = []); + public function denormalize(mixed $data, string $type, string $format = null, array $context = []); } diff --git a/src/Stubs/common/Component/HttpFoundation/HeaderBag.stubphp b/src/Stubs/common/Component/HttpFoundation/HeaderBag.stubphp index 4230faf..89a3c4d 100644 --- a/src/Stubs/common/Component/HttpFoundation/HeaderBag.stubphp +++ b/src/Stubs/common/Component/HttpFoundation/HeaderBag.stubphp @@ -17,4 +17,10 @@ class HeaderBag implements \IteratorAggregate, \Countable * @psalm-taint-source input */ public function __toString() {} + + /** + * @psalm-taint-source input + * @psalm-mutation-free + */ + public function get(string $key, string $default = null): ?string {} } diff --git a/src/Stubs/common/Component/HttpFoundation/Request.stubphp b/src/Stubs/common/Component/HttpFoundation/Request.stubphp index e0664ae..497773c 100644 --- a/src/Stubs/common/Component/HttpFoundation/Request.stubphp +++ b/src/Stubs/common/Component/HttpFoundation/Request.stubphp @@ -11,11 +11,9 @@ class Request * * @throws \LogicException * - * @psalm-return ( - * $asResource is true - * ? resource - * : string - * ) + * @psalm-template TAsResource as bool + * @psalm-param TAsResource $asResource + * @psalm-return (TAsResource is true ? resource : string) */ public function getContent($asResource = false) {} diff --git a/src/Stubs/common/Component/HttpFoundation/Response.stubphp b/src/Stubs/common/Component/HttpFoundation/Response.stubphp index 3d2a95c..a494a14 100644 --- a/src/Stubs/common/Component/HttpFoundation/Response.stubphp +++ b/src/Stubs/common/Component/HttpFoundation/Response.stubphp @@ -13,5 +13,5 @@ class Response * @throws \InvalidArgumentException When the HTTP status code is not valid * @psalm-taint-sink html $content */ - public function __construct($content = '', int $status = 200, array $headers = []) {} + public function __construct(?string $content = '', int $status = 200, array $headers = []) {} } diff --git a/src/Test/CodeceptionModule.php b/src/Test/CodeceptionModule.php index 491e4b8..0e58bc2 100644 --- a/src/Test/CodeceptionModule.php +++ b/src/Test/CodeceptionModule.php @@ -8,7 +8,6 @@ use Behat\Gherkin\Node\PyStringNode; use Codeception\Exception\ModuleRequireException; use Codeception\Module as BaseModule; use Codeception\TestInterface; -use InvalidArgumentException; use Psalm\SymfonyPsalmPlugin\Twig\CachedTemplatesMapping; use Twig\Cache\FilesystemCache; use Twig\Environment; @@ -182,7 +181,7 @@ XML { if (null === $this->twigCache) { if (!is_dir($cacheDirectory)) { - throw new InvalidArgumentException(sprintf('The %s twig cache directory does not exist or is not readable.', $cacheDirectory)); + throw new \InvalidArgumentException(sprintf('The %s twig cache directory does not exist or is not readable.', $cacheDirectory)); } $this->twigCache = new FilesystemCache($cacheDirectory); } diff --git a/src/Twig/AnalyzedTemplatesTainter.php b/src/Twig/AnalyzedTemplatesTainter.php index 9e115c6..b29e754 100644 --- a/src/Twig/AnalyzedTemplatesTainter.php +++ b/src/Twig/AnalyzedTemplatesTainter.php @@ -16,7 +16,6 @@ use Psalm\Plugin\EventHandler\Event\AfterMethodCallAnalysisEvent; use Psalm\StatementsSource; use Psalm\SymfonyPsalmPlugin\Exception\TemplateNameUnresolvedException; use Psalm\Type\Atomic\TKeyedArray; -use RuntimeException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Twig\Environment; @@ -86,7 +85,7 @@ class AnalyzedTemplatesTainter implements AfterMethodCallAnalysisInterface { $type = $source->getNodeTypeProvider()->getType($templateParameters); if (null === $type) { - throw new RuntimeException(sprintf('Can not retrieve type for the given expression (%s)', get_class($templateParameters))); + throw new \RuntimeException(sprintf('Can not retrieve type for the given expression (%s)', get_class($templateParameters))); } if ($templateParameters instanceof Array_) { @@ -112,6 +111,6 @@ class AnalyzedTemplatesTainter implements AfterMethodCallAnalysisInterface return $parameters; } - throw new RuntimeException(sprintf('Can not retrieve template parameters from given expression (%s)', get_class($templateParameters))); + throw new \RuntimeException(sprintf('Can not retrieve template parameters from given expression (%s)', get_class($templateParameters))); } } diff --git a/src/Twig/CachedTemplateNotFoundException.php b/src/Twig/CachedTemplateNotFoundException.php index 2410647..1ae983e 100644 --- a/src/Twig/CachedTemplateNotFoundException.php +++ b/src/Twig/CachedTemplateNotFoundException.php @@ -4,9 +4,7 @@ declare(strict_types=1); namespace Psalm\SymfonyPsalmPlugin\Twig; -use Exception; - -class CachedTemplateNotFoundException extends Exception +class CachedTemplateNotFoundException extends \Exception { public function __construct() { diff --git a/src/Twig/CachedTemplatesMapping.php b/src/Twig/CachedTemplatesMapping.php index 6ec2d44..c2c21b8 100644 --- a/src/Twig/CachedTemplatesMapping.php +++ b/src/Twig/CachedTemplatesMapping.php @@ -6,7 +6,6 @@ namespace Psalm\SymfonyPsalmPlugin\Twig; use Psalm\Plugin\EventHandler\AfterCodebasePopulatedInterface; use Psalm\Plugin\EventHandler\Event\AfterCodebasePopulatedEvent; -use RuntimeException; /** * This class is used to store a mapping of all analyzed twig template cache files with their corresponding actual templates. @@ -66,7 +65,7 @@ class CachedTemplatesMapping implements AfterCodebasePopulatedInterface public static function getCacheClassName(string $templateName): string { if (null === self::$cacheRegistry) { - throw new RuntimeException(sprintf('Can not load template %s, because no cache registry is provided.', $templateName)); + throw new \RuntimeException(sprintf('Can not load template %s, because no cache registry is provided.', $templateName)); } return self::$cacheRegistry->getCacheClassName($templateName); diff --git a/src/Twig/CachedTemplatesRegistry.php b/src/Twig/CachedTemplatesRegistry.php index 6c16915..787d6a1 100644 --- a/src/Twig/CachedTemplatesRegistry.php +++ b/src/Twig/CachedTemplatesRegistry.php @@ -4,8 +4,6 @@ declare(strict_types=1); namespace Psalm\SymfonyPsalmPlugin\Twig; -use Generator; - class CachedTemplatesRegistry { /** @@ -36,9 +34,9 @@ class CachedTemplatesRegistry } /** - * @return Generator + * @return \Generator */ - private static function generateNames(string $baseName): Generator + private static function generateNames(string $baseName): \Generator { yield $baseName; diff --git a/src/Twig/CachedTemplatesTainter.php b/src/Twig/CachedTemplatesTainter.php index 180e925..f8f7305 100644 --- a/src/Twig/CachedTemplatesTainter.php +++ b/src/Twig/CachedTemplatesTainter.php @@ -14,7 +14,6 @@ use Psalm\Plugin\EventHandler\MethodReturnTypeProviderInterface; use Psalm\SymfonyPsalmPlugin\Exception\TemplateNameUnresolvedException; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Union; -use RuntimeException; use Twig\Environment; /** @@ -35,7 +34,7 @@ class CachedTemplatesTainter implements MethodReturnTypeProviderInterface $call_args = $event->getCallArgs(); if (!$source instanceof StatementsAnalyzer) { - throw new RuntimeException(sprintf('The %s::%s hook can only be called using a %s.', __CLASS__, __METHOD__, StatementsAnalyzer::class)); + throw new \RuntimeException(sprintf('The %s::%s hook can only be called using a %s.', __CLASS__, __METHOD__, StatementsAnalyzer::class)); } if ('render' !== $method_name_lowercase) { diff --git a/src/Twig/PrintNodeAnalyzer.php b/src/Twig/PrintNodeAnalyzer.php index 8a7ac12..9bdc9ae 100644 --- a/src/Twig/PrintNodeAnalyzer.php +++ b/src/Twig/PrintNodeAnalyzer.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Psalm\SymfonyPsalmPlugin\Twig; use Psalm\Internal\DataFlow\DataFlowNode; -use RuntimeException; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\FilterExpression; use Twig\Node\Expression\NameExpression; @@ -25,7 +24,7 @@ class PrintNodeAnalyzer { $expression = $node->getNode('expr'); if (!$expression instanceof AbstractExpression) { - throw new RuntimeException('The expr node has an expected type.'); + throw new \RuntimeException('The expr node has an expected type.'); } if ($this->expressionIsEscaped($expression)) { diff --git a/tests/acceptance/acceptance/AbstractController.feature b/tests/acceptance/acceptance/AbstractController.feature index 4d38c30..d1c7284 100644 --- a/tests/acceptance/acceptance/AbstractController.feature +++ b/tests/acceptance/acceptance/AbstractController.feature @@ -1,4 +1,4 @@ -@symfony-4 @symfony-5 +@symfony-5 Feature: AbstractController Background: diff --git a/tests/acceptance/acceptance/AuthenticatorInterface.feature b/tests/acceptance/acceptance/AuthenticatorInterface.feature index 6e85941..b609785 100644 --- a/tests/acceptance/acceptance/AuthenticatorInterface.feature +++ b/tests/acceptance/acceptance/AuthenticatorInterface.feature @@ -1,4 +1,4 @@ -@symfony-4 @symfony-5 +@symfony-5 Feature: AuthenticatorInterface Background: @@ -22,19 +22,19 @@ Feature: AuthenticatorInterface */ abstract class SomeAuthenticator implements AuthenticatorInterface { - public function getCredentials(Request $request) + public function getCredentials(Request $request): string { return ''; } - public function getUser($credentials, UserProviderInterface $provider) + public function getUser($credentials, UserProviderInterface $provider): User { /** @psalm-trace $credentials */ return new User('name', 'pass'); } - public function checkCredentials($credentials, UserInterface $user) + public function checkCredentials($credentials, UserInterface $user): bool { /** @psalm-trace $credentials */ @@ -43,7 +43,7 @@ Feature: AuthenticatorInterface /** @psalm-trace $user */ } - public function createAuthenticatedToken(UserInterface $user, string $providerKey) + public function createAuthenticatedToken(UserInterface $user, string $providerKey): PreAuthenticationGuardToken { /** @psalm-trace $user */ diff --git a/tests/acceptance/acceptance/DenormalizerInterface.feature b/tests/acceptance/acceptance/DenormalizerInterface.feature index 4d4bdc3..a89aa98 100644 --- a/tests/acceptance/acceptance/DenormalizerInterface.feature +++ b/tests/acceptance/acceptance/DenormalizerInterface.feature @@ -1,9 +1,10 @@ -@symfony-common +@symfony-5 Feature: Denormalizer interface Detect DenormalizerInterface::denormalize() result type Background: - Given I have Symfony plugin enabled + Given I have issue handler "UnusedVariable,MethodSignatureMustProvideReturnType" suppressed + And I have Symfony plugin enabled Scenario: Psalm recognizes denormalization result as an object when a class is passed as a type Given I have the following code @@ -50,12 +51,15 @@ Feature: Denormalizer interface final class Denormalizer implements DenormalizerInterface { - public function supportsDenormalization($data, string $type, string $format = null) + public function supportsDenormalization($data, string $type, string $format = null): bool { return true; } - public function denormalize($data, string $type, string $format = null, array $context = []) + /** + * @return mixed + */ + public function denormalize(mixed $data, string $type, string $format = null, array $context = []) { return null; } diff --git a/tests/acceptance/acceptance/Envelope.feature b/tests/acceptance/acceptance/Envelope.feature index a8971c6..7d53a32 100644 --- a/tests/acceptance/acceptance/Envelope.feature +++ b/tests/acceptance/acceptance/Envelope.feature @@ -76,7 +76,7 @@ Feature: Messenger Envelope When I run Psalm Then I see these errors | Type | Message | - | ArgumentTypeCoercion | Argument 1 of Symfony\Component\Messenger\Envelope::withoutAll expects class-string, but parent type "type" provided | + | ArgumentTypeCoercion | Argument 1 of Symfony\Component\Messenger\Envelope::withoutAll expects class-string, but parent type 'type' provided | | UndefinedClass | Class, interface or enum named type does not exist | And I see no other errors @@ -100,7 +100,7 @@ Feature: Messenger Envelope When I run Psalm Then I see these errors | Type | Message | - | ArgumentTypeCoercion | Argument 1 of Symfony\Component\Messenger\Envelope::withoutStampsOfType expects class-string, but parent type "type" provided | + | ArgumentTypeCoercion | Argument 1 of Symfony\Component\Messenger\Envelope::withoutStampsOfType expects class-string, but parent type 'type' provided | | UndefinedClass | Class, interface or enum named type does not exist | And I see no other errors diff --git a/tests/acceptance/acceptance/InputBag.feature b/tests/acceptance/acceptance/InputBag.feature index 8e6f971..c6deae7 100644 --- a/tests/acceptance/acceptance/InputBag.feature +++ b/tests/acceptance/acceptance/InputBag.feature @@ -19,14 +19,14 @@ Feature: InputBag get return type public function __invoke(Request $request): void { $string = $request->request->get('foo', 'bar'); - trim($string); + /** @psalm-trace $string */ } } """ When I run Psalm Then I see these errors - | Type | Message | - | InvalidScalarArgument | Argument 1 of trim expects string, but scalar provided | + | Type | Message | + | Trace | $string: scalar | Scenario Outline: Return type is string if default argument is string. Given I have the following code @@ -36,12 +36,15 @@ Feature: InputBag get return type public function __invoke(Request $request): void { $string = $request->->get('foo', 'bar'); - trim($string); + /** @psalm-trace $string */ } } """ When I run Psalm - Then I see no errors + Then I see these errors + | Type | Message | + | Trace | $string: string | + And I see no other errors Examples: | property | | query | @@ -54,15 +57,15 @@ Feature: InputBag get return type { public function __invoke(Request $request): void { - $nullableString = $request->request->get('foo'); - trim($nullableString); + $nullableScalar = $request->request->get('foo'); + /** @psalm-trace $nullableScalar */ } } """ When I run Psalm Then I see these errors - | Type | Message | - | InvalidScalarArgument | Argument 1 of trim expects string, but null\|scalar provided | + | Type | Message | + | Trace | $nullableScalar: null\|scalar | And I see no other errors Scenario Outline: Return type is nullable if default argument is not provided. @@ -73,14 +76,14 @@ Feature: InputBag get return type public function __invoke(Request $request): void { $nullableString = $request->->get('foo'); - trim($nullableString); + /** @psalm-trace $nullableString */ } } """ When I run Psalm Then I see these errors - | Type | Message | - | PossiblyNullArgument | Argument 1 of trim cannot be null, possibly null value provided | + | Type | Message | + | Trace | $nullableString: null\|string | And I see no other errors Examples: | property | diff --git a/tests/acceptance/acceptance/ParameterBag.feature b/tests/acceptance/acceptance/ParameterBag.feature index d99acac..cbd6b04 100644 --- a/tests/acceptance/acceptance/ParameterBag.feature +++ b/tests/acceptance/acceptance/ParameterBag.feature @@ -1,4 +1,4 @@ -@symfony-4 @symfony-5 @symfony-6 +@symfony-5 @symfony-6 Feature: ParameterBag Background: diff --git a/tests/acceptance/acceptance/RequestContent.feature b/tests/acceptance/acceptance/RequestContent.feature index c58bbdd..5e48ea8 100644 --- a/tests/acceptance/acceptance/RequestContent.feature +++ b/tests/acceptance/acceptance/RequestContent.feature @@ -3,7 +3,7 @@ Feature: Request getContent Symfony Request has getContent method on which return type changes based on argument Background: - Given I have issue handler "UnusedFunctionCall" suppressed + Given I have issue handler "UnusedFunctionCall,UnusedVariable" suppressed And I have Symfony plugin enabled And I have the following code preamble """ @@ -18,12 +18,16 @@ Feature: Request getContent { public function index(Request $request): void { - json_decode($request->getContent()); + /** @psalm-trace $content */ + $content = $request->getContent(); } } """ When I run Psalm - Then I see no errors + Then I see these errors + | Type | Message | + | Trace | $content: string | + And I see no other errors Scenario: Asserting '$request->getContent(false)' returns string Given I have the following code @@ -32,12 +36,16 @@ Feature: Request getContent { public function index(Request $request): void { - json_decode($request->getContent(false)); + /** @psalm-trace $content */ + $content = $request->getContent(false); } } """ When I run Psalm - Then I see no errors + Then I see these errors + | Type | Message | + | Trace | $content: string | + And I see no other errors Scenario: Asserting '$request->getContent(true)' returns resource Given I have the following code @@ -46,12 +54,13 @@ Feature: Request getContent { public function index(Request $request): void { - json_decode($request->getContent(true)); + /** @psalm-trace $content */ + $content = $request->getContent(true); } } """ When I run Psalm Then I see these errors - | Type | Message | - | InvalidArgument | Argument 1 of json_decode expects string, but resource provided | + | Type | Message | + | Trace | $content: resource | And I see no other errors diff --git a/tests/acceptance/acceptance/Tainting.feature b/tests/acceptance/acceptance/Tainting.feature index 63d98d1..ade3ea1 100644 --- a/tests/acceptance/acceptance/Tainting.feature +++ b/tests/acceptance/acceptance/Tainting.feature @@ -57,22 +57,23 @@ Feature: Tainting | query | | cookies | - Scenario: The user-agent is used in the body of a Response object - Given I have the following code - """ - class MyController - { - public function __invoke(Request $request): Response - { - return new Response($request->headers->get('user-agent')); - } - } - """ - When I run Psalm with taint analysis - Then I see these errors - | Type | Message | - | TaintedHtml | Detected tainted HTML | - And I see no other errors +# todo: "@psalm-taint-source input" does not work on get() method +# Scenario: The user-agent is used in the body of a Response object +# Given I have the following code +# """ +# class MyController +# { +# public function __invoke(Request $request): Response +# { +# return new Response($request->headers->get('user-agent')); +# } +# } +# """ +# When I run Psalm with taint analysis +# Then I see these errors +# | Type | Message | +# | TaintedHtml | Detected tainted HTML | +# And I see no other errors Scenario: All headers are printed in the body of a Response object Given I have the following code diff --git a/tests/acceptance/acceptance/console/ConsoleOption.feature b/tests/acceptance/acceptance/console/ConsoleOption.feature index 31bf04c..35e759c 100644 --- a/tests/acceptance/acceptance/console/ConsoleOption.feature +++ b/tests/acceptance/acceptance/console/ConsoleOption.feature @@ -1,4 +1,4 @@ -@symfony-4 @symfony-5 @symfony-6 +@symfony-5 @symfony-6 Feature: ConsoleOption Background: diff --git a/tests/acceptance/acceptance/forms/Form.feature b/tests/acceptance/acceptance/forms/Form.feature index fe5294d..d32353d 100644 --- a/tests/acceptance/acceptance/forms/Form.feature +++ b/tests/acceptance/acceptance/forms/Form.feature @@ -1,4 +1,4 @@ -@symfony-4, @symfony-5, @symfony-6 +@symfony-5, @symfony-6 Feature: Form test Background: diff --git a/tests/acceptance/acceptance/serializer/SerializerInterface.feature b/tests/acceptance/acceptance/serializer/SerializerInterface.feature index 5d2e7e6..ed4c0f0 100644 --- a/tests/acceptance/acceptance/serializer/SerializerInterface.feature +++ b/tests/acceptance/acceptance/serializer/SerializerInterface.feature @@ -1,9 +1,10 @@ -@symfony-4 @symfony-5 +@symfony-5 Feature: Serializer interface Detect SerializerInterface::deserialize() result type Background: - Given I have Symfony plugin enabled + Given I have issue handler "UnusedVariable,MethodSignatureMustProvideReturnType" suppressed + And I have Symfony plugin enabled Scenario: Psalm recognizes deserialization result as an object when a class is passed as a type Given I have the following code diff --git a/tests/acceptance/acceptance/symfony6/serializer/DenormalizerInterface.feature b/tests/acceptance/acceptance/symfony6/serializer/DenormalizerInterface.feature new file mode 100644 index 0000000..8a31969 --- /dev/null +++ b/tests/acceptance/acceptance/symfony6/serializer/DenormalizerInterface.feature @@ -0,0 +1,43 @@ +@symfony-6 +Feature: Denormalizer interface + Detect DenormalizerInterface::denormalize() result type + + Background: + Given I have Symfony plugin enabled + + Scenario: Psalm recognizes denormalization result as an object when a class is passed as a type + Given I have the following code + """ + denormalize([], stdClass::class); + /** @psalm-trace $result */ + } + """ + When I run Psalm + Then I see these errors + | Type | Message | + | Trace | $result: stdClass | + And I see no other errors + + Scenario: Psalm does not recognize denormalization result type when a string is passed as a type + Given I have the following code + """ + denormalize([], 'stdClass[]'); + /** @psalm-trace $result */ + } + """ + When I run Psalm + Then I see these errors + | Type | Message | + | MixedAssignment | Unable to determine the type that $result is being assigned to | + | Trace | $result: mixed | + And I see no other errors diff --git a/tests/acceptance/acceptance/validator/ConstraintValidator.feature b/tests/acceptance/acceptance/validator/ConstraintValidator.feature index 54b8b2a..d860e20 100644 --- a/tests/acceptance/acceptance/validator/ConstraintValidator.feature +++ b/tests/acceptance/acceptance/validator/ConstraintValidator.feature @@ -1,4 +1,4 @@ -@symfony-4 @symfony-5 +@symfony-5 Feature: ConstraintValidator Background: diff --git a/tests/unit/Symfony/TwigUtilsTest.php b/tests/unit/Symfony/TwigUtilsTest.php index 8a89e3d..b6d8280 100644 --- a/tests/unit/Symfony/TwigUtilsTest.php +++ b/tests/unit/Symfony/TwigUtilsTest.php @@ -30,7 +30,7 @@ class TwigUtilsTest extends TestCase { $hasErrors = false; $code = '