This commit is contained in:
Farhad Safarov 2022-12-17 20:19:16 +03:00 committed by GitHub
parent 7cbbf62ad9
commit e58db2b253
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 162 additions and 196 deletions

View File

@ -68,7 +68,6 @@ jobs:
- 8.1
symfony-version:
- 4
- 5
- 6

View File

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

View File

@ -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();
}
/**

View File

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

View File

@ -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) {
}
}
}

View File

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

View File

@ -1,27 +0,0 @@
<?php
namespace Symfony\Bundle\FrameworkBundle\Controller;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Psr\Container\ContainerInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormTypeInterface;
class AbstractController implements ServiceSubscriberInterface
{
/**
* @var ContainerInterface
* @psalm-suppress PropertyNotSetInConstructor
*/
protected $container;
/**
* @template TData
* @template TFormType of FormTypeInterface<TData>
*
* @psalm-param class-string<TFormType> $type
*
* @psalm-return FormInterface<TData>
*/
public function createForm(string $type, $data = null, array $options = []): FormInterface {}
}

View File

@ -1,28 +0,0 @@
<?php
namespace Symfony\Component\HttpFoundation;
class ParameterBag implements \IteratorAggregate, \Countable
{
/**
* Returns a parameter by name.
*
* @param string $key The key
* @param mixed $default The default value if the parameter key does not exist
*
* @return mixed
* @psalm-taint-source input
* @psalm-mutation-free
*/
public function get(string $key, $default = null) {}
/**
* Returns the parameters.
*
* @return array An array of parameters
* @psalm-taint-source input
* @psalm-mutation-free
*/
public function all() {}
}

View File

@ -1,15 +0,0 @@
<?php
namespace Symfony\Component\Serializer\Normalizer;
interface DenormalizerInterface
{
/**
* @template TObject of object
* @template TType of string|class-string<TObject>
* @psalm-param mixed $data
* @psalm-param TType $type
* @psalm-return (TType is class-string<TObject> ? TObject : mixed)
*/
public function denormalize($data, string $type, string $format = null, array $context = []);
}

View File

@ -1,15 +0,0 @@
<?php
namespace Symfony\Component\Serializer;
interface SerializerInterface
{
/**
* @template TObject of object
* @template TType of string|class-string<TObject>
* @psalm-param mixed $data
* @psalm-param TType $type
* @psalm-return (TType is class-string<TObject> ? TObject : mixed)
*/
public function deserialize($data, string $type, string $format, array $context = []);
}

View File

@ -7,9 +7,9 @@ interface DenormalizerInterface
/**
* @template TObject of object
* @template TType of string|class-string<TObject>
* @psalm-param mixed $data
*
* @psalm-param TType $type
* @psalm-return (TType is class-string<TObject> ? 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 = []);
}

View File

@ -7,9 +7,9 @@ interface DenormalizerInterface
/**
* @template TObject of object
* @template TType of string|class-string<TObject>
* @psalm-param mixed $data
*
* @psalm-param TType $type
* @psalm-return (TType is class-string<TObject> ? 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 = []);
}

View File

@ -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 {}
}

View File

@ -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) {}

View File

@ -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 = []) {}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -4,8 +4,6 @@ declare(strict_types=1);
namespace Psalm\SymfonyPsalmPlugin\Twig;
use Generator;
class CachedTemplatesRegistry
{
/**
@ -36,9 +34,9 @@ class CachedTemplatesRegistry
}
/**
* @return Generator<string>
* @return \Generator<string>
*/
private static function generateNames(string $baseName): Generator
private static function generateNames(string $baseName): \Generator
{
yield $baseName;

View File

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

View File

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

View File

@ -1,4 +1,4 @@
@symfony-4 @symfony-5
@symfony-5
Feature: AbstractController
Background:

View File

@ -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 */

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
@symfony-4 @symfony-5 @symfony-6
@symfony-5 @symfony-6
Feature: ParameterBag
Background:

View File

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

View File

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

View File

@ -1,4 +1,4 @@
@symfony-4 @symfony-5 @symfony-6
@symfony-5 @symfony-6
Feature: ConsoleOption
Background:

View File

@ -1,4 +1,4 @@
@symfony-4, @symfony-5, @symfony-6
@symfony-5, @symfony-6
Feature: Form test
Background:

View File

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

View File

@ -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
"""
<?php
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
function test(DenormalizerInterface $denormalizer): void
{
$result = $denormalizer->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
"""
<?php
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
function test(DenormalizerInterface $denormalizer): void
{
$result = $denormalizer->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

View File

@ -1,4 +1,4 @@
@symfony-4 @symfony-5
@symfony-5
Feature: ConstraintValidator
Background:

View File

@ -30,7 +30,7 @@ class TwigUtilsTest extends TestCase
{
$hasErrors = false;
$code = '<?php'."\n".$expression;
$statements = StatementsProvider::parseStatements($code, '7.1', $hasErrors);
$statements = StatementsProvider::parseStatements($code, PHP_VERSION_ID, $hasErrors);
$assertionHook = new class() implements AfterEveryFunctionCallAnalysisInterface {
public static function afterEveryFunctionCallAnalysis(AfterEveryFunctionCallAnalysisEvent $event): void
@ -61,7 +61,7 @@ class TwigUtilsTest extends TestCase
{
$hasErrors = false;
$code = '<?php'."\n".'dummy(123);';
$statements = StatementsProvider::parseStatements($code, '7.1', $hasErrors);
$statements = StatementsProvider::parseStatements($code, PHP_VERSION_ID, $hasErrors);
$assertionHook = new class() implements AfterEveryFunctionCallAnalysisInterface {
public static function afterEveryFunctionCallAnalysis(AfterEveryFunctionCallAnalysisEvent $event): void