From b7f11029e2efbfab6853acef929c2c3ea8c28e9b Mon Sep 17 00:00:00 2001 From: Farhad Safarov Date: Fri, 21 Oct 2022 01:23:13 +0300 Subject: [PATCH 01/22] Support PHP 8.1 & drop PHP 7.3 (#279) --- .github/workflows/integrate.yaml | 32 +--- composer.json | 4 +- src/Test/CodeceptionModule.php | 32 ++-- .../acceptance/AuthenticatorInterface.feature | 2 +- .../acceptance/ServiceSubscriber.feature | 4 +- .../acceptance/TestContainerService.feature | 6 +- .../TwigTaintingWithCachedTemplates.feature | 175 ------------------ .../SerializerInterface.feature | 2 +- .../serializer/SerializerInterface6.feature | 65 +++++++ .../{ => validator}/Constraint.feature | 0 .../ConstraintValidator.feature | 2 +- .../validator/ConstraintValidator6.feature | 31 ++++ .../ConstraintViolationListInterface.feature | 0 13 files changed, 127 insertions(+), 228 deletions(-) delete mode 100644 tests/acceptance/acceptance/TwigTaintingWithCachedTemplates.feature rename tests/acceptance/acceptance/{ => serializer}/SerializerInterface.feature (98%) create mode 100644 tests/acceptance/acceptance/serializer/SerializerInterface6.feature rename tests/acceptance/acceptance/{ => validator}/Constraint.feature (100%) rename tests/acceptance/acceptance/{ => validator}/ConstraintValidator.feature (97%) create mode 100644 tests/acceptance/acceptance/validator/ConstraintValidator6.feature rename tests/acceptance/acceptance/{ => validator}/ConstraintViolationListInterface.feature (100%) diff --git a/.github/workflows/integrate.yaml b/.github/workflows/integrate.yaml index a3eb6a67..0868121e 100644 --- a/.github/workflows/integrate.yaml +++ b/.github/workflows/integrate.yaml @@ -18,20 +18,13 @@ jobs: fail-fast: false matrix: php-version: - - 7.3 - - 7.4 - 8.0 + - 8.1 dependencies: - highest - lowest - exclude: - - php-version: 7.3 - dependencies: lowest - - php-version: 7.4 - dependencies: lowest - steps: - name: "Checkout" uses: actions/checkout@v2 @@ -71,11 +64,8 @@ jobs: fail-fast: false matrix: php-version: - - 7.1 - - 7.2 - - 7.3 - - 7.4 - 8.0 + - 8.1 symfony-version: - 4 @@ -83,20 +73,6 @@ jobs: - 6 exclude: - - php-version: 7.1 - symfony-version: 5 - - php-version: 7.1 - symfony-version: 6 - - php-version: 7.2 - symfony-version: 6 - - php-version: 7.3 - symfony-version: 6 - - php-version: 7.4 - symfony-version: 6 - - php-version: 7.1 - symfony-version: 6 - - php-version: 8.0 - symfony-version: 3 - php-version: 8.0 symfony-version: 6 @@ -125,7 +101,3 @@ jobs: - name: "Run acceptance tests with codeception" run: vendor/bin/codecept run -v -g symfony-common -g symfony-${{ matrix.symfony-version }} - - - name: "Run acceptance tests with codeception PHP8 only tests" - run: vendor/bin/codecept run -v -g php-8 - if: matrix.php-version == '8.0' diff --git a/composer.json b/composer.json index 2f91b5ea..06c8a4b1 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ } ], "require": { - "php": "^7.1 || ^8.0", + "php": "^7.4 || ^8.0", "ext-simplexml": "*", "symfony/framework-bundle": "^4.0 || ^5.0 || ^6.0", "vimeo/psalm": "^4.12" @@ -27,7 +27,7 @@ "symfony/serializer": "^4.0 || ^5.0 || ^6.0", "symfony/validator": "*", "twig/twig": "^2.10 || ^3.0", - "weirdan/codeception-psalm-module": "^0.13.1" + "weirdan/codeception-psalm-module": "dev-master" }, "autoload": { "psr-4": { diff --git a/src/Test/CodeceptionModule.php b/src/Test/CodeceptionModule.php index 192dd82f..491e4b84 100644 --- a/src/Test/CodeceptionModule.php +++ b/src/Test/CodeceptionModule.php @@ -4,6 +4,7 @@ namespace Psalm\SymfonyPsalmPlugin\Test; +use Behat\Gherkin\Node\PyStringNode; use Codeception\Exception\ModuleRequireException; use Codeception\Module as BaseModule; use Codeception\TestInterface; @@ -21,15 +22,6 @@ */ class CodeceptionModule extends BaseModule { - /** - * @var mixed[] - * - * @psalm-suppress NonInvariantDocblockPropertyType - */ - protected $config = [ - 'default_dir' => 'tests/_run/', - ]; - private const DEFAULT_TWIG_TEMPLATES_DIR = 'templates'; /** @@ -52,6 +44,13 @@ class CodeceptionModule extends BaseModule */ private $suppressedIssueHandlers = []; + public function _initialize(): void + { + $this->_setConfig([ + 'default_dir' => 'tests/_run/', + ]); + } + public function _after(TestInterface $test): void { $this->twigCache = $this->lastCachePath = null; @@ -69,7 +68,7 @@ public function setTheTemplateRootDirectory(string $rootDir): void /** * @Given I have the following :templateName template :code */ - public function haveTheFollowingTemplate(string $templateName, string $code): void + public function haveTheFollowingTemplate(string $templateName, PyStringNode $code): void { $rootDirectory = rtrim($this->config['default_dir'], DIRECTORY_SEPARATOR); $templatePath = ( @@ -82,7 +81,7 @@ public function haveTheFollowingTemplate(string $templateName, string $code): vo mkdir($templateDirectory, 0755, true); } - file_put_contents($templatePath, $code); + file_put_contents($templatePath, $code->getRaw()); } /** @@ -137,9 +136,16 @@ public function configureIgnoredIssueHandlers(string $issueHandlers): void /** * @Given I have Symfony plugin enabled + */ + public function configureCommonPsalmconfigEmpty(): void + { + $this->configureCommonPsalmconfig(new PyStringNode([], 0)); + } + + /** * @Given I have Symfony plugin enabled with the following config :configuration */ - public function configureCommonPsalmconfig(string $configuration = ''): void + public function configureCommonPsalmconfig(PyStringNode $configuration): void { $suppressedIssueHandlers = implode("\n", array_map(function (string $issueHandler) { return "<$issueHandler errorLevel=\"info\"/>"; @@ -161,7 +167,7 @@ public function configureCommonPsalmconfig(string $configuration = ''): void - $configuration + {$configuration->getRaw()} diff --git a/tests/acceptance/acceptance/AuthenticatorInterface.feature b/tests/acceptance/acceptance/AuthenticatorInterface.feature index 765ce7c9..6e859410 100644 --- a/tests/acceptance/acceptance/AuthenticatorInterface.feature +++ b/tests/acceptance/acceptance/AuthenticatorInterface.feature @@ -1,4 +1,4 @@ -@symfony-common +@symfony-4 @symfony-5 Feature: AuthenticatorInterface Background: diff --git a/tests/acceptance/acceptance/ServiceSubscriber.feature b/tests/acceptance/acceptance/ServiceSubscriber.feature index e2c0a7b1..36b786d1 100644 --- a/tests/acceptance/acceptance/ServiceSubscriber.feature +++ b/tests/acceptance/acceptance/ServiceSubscriber.feature @@ -1,4 +1,4 @@ -@symfony-4 @symfony-5 @symfony-6 +@symfony-common Feature: Service Subscriber Background: @@ -26,7 +26,7 @@ Feature: Service Subscriber $this->container = $container; } - public static function getSubscribedServices() + public static function getSubscribedServices(): array { return [ // takes container.xml into account diff --git a/tests/acceptance/acceptance/TestContainerService.feature b/tests/acceptance/acceptance/TestContainerService.feature index 4ad63805..9b14370e 100644 --- a/tests/acceptance/acceptance/TestContainerService.feature +++ b/tests/acceptance/acceptance/TestContainerService.feature @@ -1,4 +1,4 @@ -@symfony-4 @symfony-5 @symfony-6 +@symfony-5 @symfony-6 Feature: Test Container service Background: @@ -21,7 +21,7 @@ Feature: Test Container service { public function testService(): void { - $service = static::$container->get('dummy_private_service'); + $service = static::getContainer()->get('dummy_private_service'); trim($service->foo()); } } @@ -42,7 +42,7 @@ Feature: Test Container service { public function testService(): void { - $service = static::$container->get('dummy_private_service'); + $service = static::getContainer()->get('dummy_private_service'); trim($service->foo()); } } diff --git a/tests/acceptance/acceptance/TwigTaintingWithCachedTemplates.feature b/tests/acceptance/acceptance/TwigTaintingWithCachedTemplates.feature deleted file mode 100644 index e1ed970b..00000000 --- a/tests/acceptance/acceptance/TwigTaintingWithCachedTemplates.feature +++ /dev/null @@ -1,175 +0,0 @@ -@symfony-common -Feature: Twig tainting with cached templates - - Background: - Given I have the following config - """ - - - - - - - - - - - - - - - /cache/twig - - - - - - - """ - And I have the following code preamble - """ - render('index.html.twig'); - """ - And I have the following "index.html.twig" template - """ -

- Nothing. -

- """ - And the "index.html.twig" template is compiled in the "cache/twig/" directory - When I run Psalm with taint analysis - And I see no errors - - Scenario: One parameter of the twig rendering is tainted but autoescaping is on - Given I have the following code - """ - $untrusted = $_GET['untrusted']; - twig()->render('index.html.twig', ['untrusted' => $untrusted]); - """ - And I have the following "index.html.twig" template - """ -

- {{ untrusted }} -

- """ - And the "index.html.twig" template is compiled in the "cache/twig/" directory - When I run Psalm with taint analysis - And I see no errors - - Scenario: One parameter of the twig rendering is tainted - Given I have the following code - """ - $untrusted = $_GET['untrusted']; - twig()->render('index.html.twig', ['untrusted' => $untrusted]); - """ - And I have the following "index.html.twig" template - """ -

- {{ untrusted|raw }} -

- """ - And the "index.html.twig" template is compiled in the "cache/twig/" directory - When I run Psalm with taint analysis - Then I see these errors - | Type | Message | - | TaintedHtml | Detected tainted HTML | - | TaintedTextWithQuotes | Detected tainted text with possible quotes | - And I see no other errors - -# Scenario: One tainted parameter (in a variable) of the twig template (named in a variable) is displayed with only the raw filter -# Given I have the following code -# """ -# $untrustedParameters = ['untrusted' => $_GET['untrusted']]; -# $template = 'index.html.twig'; -# -# twig()->render($template, $untrustedParameters); -# """ -# And I have the following "index.html.twig" template -# """ -#

-# {{ untrusted|raw }} -#

-# """ -# And the "index.html.twig" template is compiled in the "cache/twig/" directory -# 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: The template has a taint sink and is aliased - Given I have the following code - """ - $untrusted = $_GET['untrusted']; - twig()->render('@Acme/index.html.twig', ['untrusted' => $untrusted]); - """ - And I have the following "AcmeBundle/Resources/views/index.html.twig" template - """ -

- {{ untrusted|raw }} -

- """ - And the "AcmeBundle/Resources/views/index.html.twig" template is compiled in the "cache/twig/" directory - And the last compiled template got his alias changed to "@Acme/index.html.twig" - When I run Psalm with taint analysis - Then I see these errors - | Type | Message | - | TaintedHtml | Detected tainted HTML | - | TaintedTextWithQuotes | Detected tainted text with possible quotes | - And I see no other errors - - Scenario: The template has a taint sink and is aliased using the old notation - Given I have the following code - """ - $untrusted = $_GET['untrusted']; - twig()->render('@Acme/index.html.twig', ['untrusted' => $untrusted]); - """ - And I have the following "AcmeBundle/Resources/views/index.html.twig" template - """ -

- {{ untrusted|raw }} -

- """ - And the "AcmeBundle/Resources/views/index.html.twig" template is compiled in the "cache/twig/" directory - And the last compiled template got his alias changed to "AcmeBundle::index.html.twig" - When I run Psalm with taint analysis - Then I see these errors - | Type | Message | - | TaintedHtml | Detected tainted HTML | - | TaintedTextWithQuotes | Detected tainted text with possible quotes | - And I see no other errors - - Scenario: The template has a taint sink and is rendered using the old alias notation - Given I have the following code - """ - $untrusted = $_GET['untrusted']; - twig()->render('AcmeBundle::index.html.twig', ['untrusted' => $untrusted]); - """ - And I have the following "AcmeBundle/Resources/views/index.html.twig" template - """ -

- {{ untrusted|raw }} -

- """ - And the "AcmeBundle/Resources/views/index.html.twig" template is compiled in the "cache/twig/" directory - And the last compiled template got his alias changed to "@Acme/index.html.twig" - When I run Psalm with taint analysis - Then I see these errors - | Type | Message | - | TaintedHtml | Detected tainted HTML | - | TaintedTextWithQuotes | Detected tainted text with possible quotes | - And I see no other errors diff --git a/tests/acceptance/acceptance/SerializerInterface.feature b/tests/acceptance/acceptance/serializer/SerializerInterface.feature similarity index 98% rename from tests/acceptance/acceptance/SerializerInterface.feature rename to tests/acceptance/acceptance/serializer/SerializerInterface.feature index 25208465..5d2e7e69 100644 --- a/tests/acceptance/acceptance/SerializerInterface.feature +++ b/tests/acceptance/acceptance/serializer/SerializerInterface.feature @@ -1,4 +1,4 @@ -@symfony-common +@symfony-4 @symfony-5 Feature: Serializer interface Detect SerializerInterface::deserialize() result type diff --git a/tests/acceptance/acceptance/serializer/SerializerInterface6.feature b/tests/acceptance/acceptance/serializer/SerializerInterface6.feature new file mode 100644 index 00000000..3b0153bd --- /dev/null +++ b/tests/acceptance/acceptance/serializer/SerializerInterface6.feature @@ -0,0 +1,65 @@ +@symfony-6 +Feature: Serializer interface + Detect SerializerInterface::deserialize() result type + + Background: + Given 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 + """ + deserialize([], stdClass::class, 'json'); + /** @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 deserialization result type when a string is passed as a type + Given I have the following code + """ + deserialize([], 'stdClass[]', 'json'); + /** @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 + + Scenario: Psalm does not complain about the missing $data parameter type in the serializer implementation + Given I have the following code + """ + context + ->buildViolation('foo') + ->atPath('foo') + ->addViolation(); + } + } + } + """ + When I run Psalm + Then I see no errors diff --git a/tests/acceptance/acceptance/ConstraintViolationListInterface.feature b/tests/acceptance/acceptance/validator/ConstraintViolationListInterface.feature similarity index 100% rename from tests/acceptance/acceptance/ConstraintViolationListInterface.feature rename to tests/acceptance/acceptance/validator/ConstraintViolationListInterface.feature From 07b9e393571b215993f36d44e8c28c6f354e14fd Mon Sep 17 00:00:00 2001 From: Farhad Safarov Date: Sat, 29 Oct 2022 18:13:25 +0300 Subject: [PATCH 02/22] [serializer] fix serializer for Symfony 6 (#282) --- .../Normalizer/DenormalizerInterface.stubphp | 15 +++++++++++++++ .../Serializer/SerializerInterface.stubphp | 0 .../Normalizer/DenormalizerInterface.stubphp | 15 +++++++++++++++ .../Serializer/SerializerInterface.stubphp | 15 +++++++++++++++ .../serializer/SerializerInterface6.feature | 4 ++-- 5 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 src/Stubs/4/Component/Serializer/Normalizer/DenormalizerInterface.stubphp rename src/Stubs/{common => 4}/Component/Serializer/SerializerInterface.stubphp (100%) create mode 100644 src/Stubs/5/Component/Serializer/Normalizer/DenormalizerInterface.stubphp create mode 100644 src/Stubs/5/Component/Serializer/SerializerInterface.stubphp diff --git a/src/Stubs/4/Component/Serializer/Normalizer/DenormalizerInterface.stubphp b/src/Stubs/4/Component/Serializer/Normalizer/DenormalizerInterface.stubphp new file mode 100644 index 00000000..e559b751 --- /dev/null +++ b/src/Stubs/4/Component/Serializer/Normalizer/DenormalizerInterface.stubphp @@ -0,0 +1,15 @@ + + * @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/common/Component/Serializer/SerializerInterface.stubphp b/src/Stubs/4/Component/Serializer/SerializerInterface.stubphp similarity index 100% rename from src/Stubs/common/Component/Serializer/SerializerInterface.stubphp rename to src/Stubs/4/Component/Serializer/SerializerInterface.stubphp diff --git a/src/Stubs/5/Component/Serializer/Normalizer/DenormalizerInterface.stubphp b/src/Stubs/5/Component/Serializer/Normalizer/DenormalizerInterface.stubphp new file mode 100644 index 00000000..e559b751 --- /dev/null +++ b/src/Stubs/5/Component/Serializer/Normalizer/DenormalizerInterface.stubphp @@ -0,0 +1,15 @@ + + * @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/5/Component/Serializer/SerializerInterface.stubphp b/src/Stubs/5/Component/Serializer/SerializerInterface.stubphp new file mode 100644 index 00000000..a2f1aa1b --- /dev/null +++ b/src/Stubs/5/Component/Serializer/SerializerInterface.stubphp @@ -0,0 +1,15 @@ + + * @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/tests/acceptance/acceptance/serializer/SerializerInterface6.feature b/tests/acceptance/acceptance/serializer/SerializerInterface6.feature index 3b0153bd..cfa31793 100644 --- a/tests/acceptance/acceptance/serializer/SerializerInterface6.feature +++ b/tests/acceptance/acceptance/serializer/SerializerInterface6.feature @@ -50,12 +50,12 @@ Feature: Serializer interface final class Serializer implements SerializerInterface { - public function serialize($data, string $format, array $context = []): string + public function serialize(mixed $data, string $format, array $context = []): string { return ''; } - public function deserialize($data, string $type, string $format, array $context = []): mixed + public function deserialize(mixed $data, string $type, string $format, array $context = []): mixed { return []; } From 40589bcc366b46a80754f874e83d430089a8cbfe Mon Sep 17 00:00:00 2001 From: Farhad Safarov Date: Sat, 29 Oct 2022 18:24:33 +0300 Subject: [PATCH 03/22] [doctrine] fix repository crash when variable classname is used (#280) (#283) --- src/Handler/DoctrineRepositoryHandler.php | 14 +++-- .../DoctrineQueryBuilder.feature | 0 .../doctrine/RepositoryClass.feature | 53 +++++++++++++++++++ .../RepositoryStringShortcut.feature | 24 ++++----- tests/fixture/Doctrine/Foo.php | 14 +++++ tests/fixture/Doctrine/FooRepository.php | 11 ++++ 6 files changed, 97 insertions(+), 19 deletions(-) rename tests/acceptance/acceptance/{ => doctrine}/DoctrineQueryBuilder.feature (100%) create mode 100644 tests/acceptance/acceptance/doctrine/RepositoryClass.feature rename tests/acceptance/acceptance/{ => doctrine}/RepositoryStringShortcut.feature (75%) create mode 100644 tests/fixture/Doctrine/Foo.php create mode 100644 tests/fixture/Doctrine/FooRepository.php diff --git a/src/Handler/DoctrineRepositoryHandler.php b/src/Handler/DoctrineRepositoryHandler.php index f5967157..0569c4ed 100644 --- a/src/Handler/DoctrineRepositoryHandler.php +++ b/src/Handler/DoctrineRepositoryHandler.php @@ -17,6 +17,8 @@ use Psalm\SymfonyPsalmPlugin\Issue\RepositoryStringShortcut; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Union; +use ReflectionClass; +use ReflectionException; class DoctrineRepositoryHandler implements AfterMethodCallAnalysisInterface, AfterClassLikeVisitInterface { @@ -41,13 +43,17 @@ public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $eve $statements_source->getSuppressedIssues() ); } elseif ($entityName instanceof Expr\ClassConstFetch) { - /** @psalm-var class-string $className */ + /** @psalm-var class-string|null $className */ $className = $entityName->class->getAttribute('resolvedName'); + if (null === $className) { + return; + } + try { - $reflectionClass = new \ReflectionClass($className); + $reflectionClass = new ReflectionClass($className); - if (method_exists(\ReflectionClass::class, 'getAttributes')) { + if (\PHP_VERSION_ID >= 80000 && method_exists(ReflectionClass::class, 'getAttributes')) { $entityAttributes = $reflectionClass->getAttributes(EntityAnnotation::class); foreach ($entityAttributes as $entityAttribute) { @@ -70,7 +76,7 @@ public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $eve $event->setReturnTypeCandidate(new Union([new TNamedObject($entityAnnotation->repositoryClass)])); } } - } catch (\ReflectionException $e) { + } catch (ReflectionException $e) { } } } diff --git a/tests/acceptance/acceptance/DoctrineQueryBuilder.feature b/tests/acceptance/acceptance/doctrine/DoctrineQueryBuilder.feature similarity index 100% rename from tests/acceptance/acceptance/DoctrineQueryBuilder.feature rename to tests/acceptance/acceptance/doctrine/DoctrineQueryBuilder.feature diff --git a/tests/acceptance/acceptance/doctrine/RepositoryClass.feature b/tests/acceptance/acceptance/doctrine/RepositoryClass.feature new file mode 100644 index 00000000..0399d408 --- /dev/null +++ b/tests/acceptance/acceptance/doctrine/RepositoryClass.feature @@ -0,0 +1,53 @@ +@symfony-common +Feature: RepositoryClass + + Background: + Given I have issue handlers "UndefinedClass,UnusedVariable" suppressed + And I have Symfony plugin enabled + And I have the following code preamble + """ + getRepository(Foo::class); + } + } + """ + When I run Psalm + Then I see these errors + | Type | Message | + | Trace | $repository: Psalm\SymfonyPsalmPlugin\Tests\Fixture\Doctrine\FooRepository | + And I see no other errors + + Scenario: Passing variable class does not crash the plugin + Given I have the following code + """ + class SomeService + { + public function __construct(EntityManagerInterface $entityManager) + { + $entity = 'Psalm\SymfonyPsalmPlugin\Tests\Fixture\Doctrine\Foo'; + /** @psalm-trace $repository */ + $repository = $entityManager->getRepository($entity::class); + } + } + """ + When I run Psalm + Then I see these errors + | Type | Message | + | Trace | $repository: Doctrine\ORM\EntityRepository | + | MixedArgument | Argument 1 of Doctrine\ORM\EntityManagerInterface::getRepository cannot be mixed, expecting class-string | + And I see no other errors diff --git a/tests/acceptance/acceptance/RepositoryStringShortcut.feature b/tests/acceptance/acceptance/doctrine/RepositoryStringShortcut.feature similarity index 75% rename from tests/acceptance/acceptance/RepositoryStringShortcut.feature rename to tests/acceptance/acceptance/doctrine/RepositoryStringShortcut.feature index 79f58721..06403498 100644 --- a/tests/acceptance/acceptance/RepositoryStringShortcut.feature +++ b/tests/acceptance/acceptance/doctrine/RepositoryStringShortcut.feature @@ -5,27 +5,21 @@ Feature: RepositoryStringShortcut I need Psalm to check preferred repository syntax Background: - Given I have issue handlers "UndefinedClass,UnusedVariable" suppressed + Given I have issue handlers "ArgumentTypeCoercion,MixedArgument,UndefinedClass" suppressed And I have Symfony plugin enabled And I have the following code preamble """ getRepository(Entity::class); + $entityManager->getRepository(Foo::class); } } """ @@ -61,7 +55,7 @@ Feature: RepositoryStringShortcut { public function __construct(EntityManagerInterface $entityManager) { - $className = 'App\Entity\EntityA'; + $className = Foo::class; $entityManager->getRepository($className); } } diff --git a/tests/fixture/Doctrine/Foo.php b/tests/fixture/Doctrine/Foo.php new file mode 100644 index 00000000..3a6c8a1f --- /dev/null +++ b/tests/fixture/Doctrine/Foo.php @@ -0,0 +1,14 @@ + Date: Fri, 9 Dec 2022 06:54:05 +0100 Subject: [PATCH 04/22] Fixed tests to work with psalm5 (#289) --- .../acceptance/forms/AbstractTypeExtension.feature | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/acceptance/acceptance/forms/AbstractTypeExtension.feature b/tests/acceptance/acceptance/forms/AbstractTypeExtension.feature index baf5eee4..37294b0e 100644 --- a/tests/acceptance/acceptance/forms/AbstractTypeExtension.feature +++ b/tests/acceptance/acceptance/forms/AbstractTypeExtension.feature @@ -11,6 +11,9 @@ Feature: FormType templates use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\Extension\Core\Type\FormType; + /** + * @extends AbstractTypeExtension + */ class TestExtension extends AbstractTypeExtension { public static function getExtendedTypes(): iterable @@ -30,6 +33,9 @@ Feature: FormType templates use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\Extension\Core\Type\FormType; + /** + * @extends AbstractTypeExtension + */ class TestExtension extends AbstractTypeExtension { public static function getExtendedTypes(): iterable From e58db2b253026e00a0401d6c48b4138a251c9991 Mon Sep 17 00:00:00 2001 From: Farhad Safarov Date: Sat, 17 Dec 2022 20:19:16 +0300 Subject: [PATCH 05/22] 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 0868121e..d8e32ba3 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 06c8a4b1..ee91a304 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 e5eb7d95..8af87c65 100644 --- a/src/Handler/ConsoleHandler.php +++ b/src/Handler/ConsoleHandler.php @@ -21,6 +21,7 @@ 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 @@ private static function analyseArgument(array $args, StatementsSource $statement } 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 @@ private static function analyseArgument(array $args, StatementsSource $statement } } - self::$arguments[$identifier] = $returnTypes; + self::$arguments[$identifier] = $returnTypes->freeze(); } /** @@ -199,7 +200,7 @@ private static function analyseOption(array $args, StatementsSource $statements_ $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 @@ private static function analyseOption(array $args, StatementsSource $statements_ } 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 @@ private static function analyseOption(array $args, StatementsSource $statements_ } 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 d24df3a0..3356aa9f 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 @@ public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $eve $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 0569c4ed..899f50e1 100644 --- a/src/Handler/DoctrineRepositoryHandler.php +++ b/src/Handler/DoctrineRepositoryHandler.php @@ -17,8 +17,6 @@ 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 @@ public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $eve } 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 @@ public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $eve $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 b82be3a0..6bb80dcf 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -21,7 +21,6 @@ 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 9dd5b346..00000000 --- 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 0f7462bb..00000000 --- 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 a2f1aa1b..00000000 --- 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 e559b751..47df42bb 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 e559b751..47df42bb 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 4230fafb..89a3c4da 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 e0664ae8..497773c1 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 3d2a95c8..a494a141 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 491e4b84..0e58bc28 100644 --- a/src/Test/CodeceptionModule.php +++ b/src/Test/CodeceptionModule.php @@ -8,7 +8,6 @@ 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 @@ private function loadTemplate(string $templateName, string $rootDirectory, strin { 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 9e115c63..b29e7541 100644 --- a/src/Twig/AnalyzedTemplatesTainter.php +++ b/src/Twig/AnalyzedTemplatesTainter.php @@ -16,7 +16,6 @@ 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 @@ private static function generateTemplateParameters(Expr $templateParameters, Sta { $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 @@ private static function generateTemplateParameters(Expr $templateParameters, Sta 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 24106471..1ae983eb 100644 --- a/src/Twig/CachedTemplateNotFoundException.php +++ b/src/Twig/CachedTemplateNotFoundException.php @@ -4,9 +4,7 @@ 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 6ec2d448..c2c21b85 100644 --- a/src/Twig/CachedTemplatesMapping.php +++ b/src/Twig/CachedTemplatesMapping.php @@ -6,7 +6,6 @@ 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 @@ public static function setCachePath(string $cachePath): void 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 6c169152..787d6a17 100644 --- a/src/Twig/CachedTemplatesRegistry.php +++ b/src/Twig/CachedTemplatesRegistry.php @@ -4,8 +4,6 @@ namespace Psalm\SymfonyPsalmPlugin\Twig; -use Generator; - class CachedTemplatesRegistry { /** @@ -36,9 +34,9 @@ public function getCacheClassName(string $templateName): string } /** - * @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 180e9257..f8f73059 100644 --- a/src/Twig/CachedTemplatesTainter.php +++ b/src/Twig/CachedTemplatesTainter.php @@ -14,7 +14,6 @@ use Psalm\SymfonyPsalmPlugin\Exception\TemplateNameUnresolvedException; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Union; -use RuntimeException; use Twig\Environment; /** @@ -35,7 +34,7 @@ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event) $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 8a7ac12a..9bdc9ae6 100644 --- a/src/Twig/PrintNodeAnalyzer.php +++ b/src/Twig/PrintNodeAnalyzer.php @@ -5,7 +5,6 @@ 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 @@ public function analyzePrintNode(PrintNode $node): void { $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 4d38c30e..d1c72844 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 6e859410..b609785b 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 4d4bdc39..a89aa987 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 a8971c6b..7d53a32e 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 8e6f9716..c6deae7e 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 d99acacd..cbd6b04b 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 c58bbdde..5e48ea85 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 63d98d10..ade3ea1e 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 31bf04c3..35e759c3 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 fe5294d6..d32353d9 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 5d2e7e69..ed4c0f00 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 00000000..8a319698 --- /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 54b8b2ac..d860e208 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 8a89e3da..b6d82808 100644 --- a/tests/unit/Symfony/TwigUtilsTest.php +++ b/tests/unit/Symfony/TwigUtilsTest.php @@ -30,7 +30,7 @@ public function testItCanExtractTheTemplateNameFromAnExpression(string $expressi { $hasErrors = false; $code = ' Date: Fri, 23 Dec 2022 07:07:54 +0100 Subject: [PATCH 06/22] Compatibility with Doctrine Annotations 2 (#296) * Compatibility with Annotations 2 * Suppress UnusedPsalmSuppress Co-authored-by: Bruce Weirdan --- composer.json | 2 +- psalm-baseline.xml | 5 +++++ src/Plugin.php | 6 ++++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index ee91a304..cd879125 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ }, "require-dev": { "symfony/form": "^5.0 || ^6.0", - "doctrine/annotations": "^1.8", + "doctrine/annotations": "^1.8|^2", "doctrine/orm": "^2.9", "phpunit/phpunit": "~7.5 || ~9.5", "symfony/cache-contracts": "^1.0 || ^2.0", diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 30800ce4..b40e5133 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,10 @@ + + + DeprecatedMethod + + int diff --git a/src/Plugin.php b/src/Plugin.php index 6bb80dcf..1ed2810a 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -53,8 +53,10 @@ public function __invoke(RegistrationInterface $api, \SimpleXMLElement $config = if (class_exists(AnnotationRegistry::class)) { require_once __DIR__.'/Handler/DoctrineRepositoryHandler.php'; - /** @psalm-suppress DeprecatedMethod */ - AnnotationRegistry::registerLoader('class_exists'); + if (method_exists(AnnotationRegistry::class, 'registerLoader')) { + /** @psalm-suppress DeprecatedMethod */ + AnnotationRegistry::registerLoader('class_exists'); + } $api->registerHooksFromClass(DoctrineRepositoryHandler::class); require_once __DIR__.'/Handler/AnnotationHandler.php'; From 53ec8065fafadbae759a87d95ab3bb37b55b0d55 Mon Sep 17 00:00:00 2001 From: Thomas Landauer Date: Thu, 22 Dec 2022 07:29:06 +0100 Subject: [PATCH 07/22] Adding for env() and param() (#297) Info is taken from https://github.com/psalm/psalm-plugin-symfony/issues/292#issuecomment-1345512945 --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bac01948..945063e9 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ This file path may change based on your Symfony version, file structure and envi Default files are: - Symfony 3: `var/cache/dev/srcDevDebugProjectContainer.xml` - Symfony 4: `var/cache/dev/srcApp_KernelDevDebugContainer.xml` -- Symfony 5: `var/cache/dev/App_KernelDevDebugContainer.xml` +- Symfony 5+: `var/cache/dev/App_KernelDevDebugContainer.xml` Multiple container files can be configured. In this case, the first valid file is taken into account. If none of the given files is valid, a configuration exception is thrown. @@ -80,6 +80,14 @@ If you're using PHP config files for Symfony 5.3+, you also need this for auto-l ``` +If you're using Symfony's `env()` or `param()` functions in your PHP config files, you also need this for auto-loading them: + +```xml + + + +``` + If you're getting the following error > MissingFile - config/preload.php - Cannot find file ...var/cache/prod/App_KernelProdContainer.preload.php to include From 96d09e642135d432a4efb2e7003a4cdf78cc8779 Mon Sep 17 00:00:00 2001 From: Farhad Safarov Date: Mon, 26 Dec 2022 09:10:35 +0300 Subject: [PATCH 08/22] add versions & dependencies table --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 945063e9..753e59c2 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,16 @@ vendor/bin/psalm --init vendor/bin/psalm-plugin enable psalm/plugin-symfony ``` +### Versions & Dependencies + +| Symfony Psalm Plugin | PHP | Symfony | Psalm | +|----------------------|------------|---------|-------| +| 5.x | ^7.4, ^8.0 | 5, 6 | 5 | +| 4.x | ^7.4, ^8.0 | 4, 5, 6 | 4 | +| 3.x | ^7.1, ^8.0 | 4, 5, 6 | 4 | +| 2.x | ^7.1, ^8.0 | 4, 5 | 4 | +| 1.x | ^7.1 | 3, 4, 5 | 3 | + ### Features - Detects the `ContainerInterface::get()` result type. Works better if you [configure](#configuration) a compiled container XML file. From e9900d2f0186564a30934a2ebcf60481d916ebf0 Mon Sep 17 00:00:00 2001 From: Maarten de Boer Date: Sun, 8 Jan 2023 19:52:07 +0100 Subject: [PATCH 09/22] [console] Fix console handler with named arguments and add more tests (#300) --- src/Handler/ConsoleHandler.php | 2 +- .../console/ConsoleArgumentNamedArgs.feature | 26 ++++++++++++++++ .../console/ConsoleOptionNamedArgs.feature | 31 +++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/Handler/ConsoleHandler.php b/src/Handler/ConsoleHandler.php index 8af87c65..b6acee6c 100644 --- a/src/Handler/ConsoleHandler.php +++ b/src/Handler/ConsoleHandler.php @@ -265,7 +265,7 @@ private static function normalizeParams(array $params, array $args): array $key = array_search($name, $params); Assert::integer($key); - $params = array_slice($params, $key + 1); + unset($params[$key]); } else { $name = array_shift($params); } diff --git a/tests/acceptance/acceptance/console/ConsoleArgumentNamedArgs.feature b/tests/acceptance/acceptance/console/ConsoleArgumentNamedArgs.feature index 4f85d23d..f0754e3a 100644 --- a/tests/acceptance/acceptance/console/ConsoleArgumentNamedArgs.feature +++ b/tests/acceptance/acceptance/console/ConsoleArgumentNamedArgs.feature @@ -8,6 +8,7 @@ Feature: ConsoleArgument named arguments with PHP8 addArgument(name: 'test', description: 'foo', mode: InputArgument::OPTIONAL, default: 'test'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + /** @psalm-trace $argument */ + $argument = $input->getArgument('test'); + + return 0; + } + } + """ + When I run Psalm + Then I see these errors + | Type | Message | + | Trace | $argument: string | + And I see no other errors diff --git a/tests/acceptance/acceptance/console/ConsoleOptionNamedArgs.feature b/tests/acceptance/acceptance/console/ConsoleOptionNamedArgs.feature index 1f4d14c3..6d34fecf 100644 --- a/tests/acceptance/acceptance/console/ConsoleOptionNamedArgs.feature +++ b/tests/acceptance/acceptance/console/ConsoleOptionNamedArgs.feature @@ -37,3 +37,34 @@ Feature: ConsoleOption named arguments with PHP8 | Type | Message | | Trace | $string: string | And I see no other errors + + Scenario: Assert adding options with only named arguments works as expected + Given I have the following code + """ + class MyCommand extends Command + { + public function configure(): void + { + $this->addOption( + name: 'test', + mode: InputOption::VALUE_REQUIRED, + description: 'foo', + shortcut: 't', + default: 'test' + ); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + /** @psalm-trace $string */ + $string = $input->getOption('test'); + + return 0; + } + } + """ + When I run Psalm + Then I see these errors + | Type | Message | + | Trace | $string: string | + And I see no other errors From 0d1ff71e1081805de853ce0fc95b5746a1df8bdd Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sun, 5 Mar 2023 05:07:43 -0400 Subject: [PATCH 10/22] Dependabot updates for GH actions (#308) (cherry picked from commit 3eb53193b6ad1102dc2c51d12b1aca6965316b13) --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..3a626c3a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly From 063d3e92a3b0ef0ded259253c84930b52617be4b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 09:07:39 +0300 Subject: [PATCH 11/22] Bump actions/checkout from 2 to 3 (#310) Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit f11a54acfffa97b81df94ce81f8a885e37726b96) --- .github/workflows/integrate.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integrate.yaml b/.github/workflows/integrate.yaml index d8e32ba3..b64db716 100644 --- a/.github/workflows/integrate.yaml +++ b/.github/workflows/integrate.yaml @@ -27,7 +27,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: "Install PHP with extensions" uses: shivammathur/setup-php@v2 @@ -77,7 +77,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: "Install PHP with extensions" uses: shivammathur/setup-php@v2 From 3dc5194941106942a42b694bb1b151b776556c67 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 12:48:19 +0300 Subject: [PATCH 12/22] Bump actions/cache from 1 to 3 (#309) Bumps [actions/cache](https://github.com/actions/cache) from 1 to 3. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v1...v3) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit 64ef8b54a50b127cadf270469e2efdd43f9772d2) --- .github/workflows/integrate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integrate.yaml b/.github/workflows/integrate.yaml index b64db716..8543a23b 100644 --- a/.github/workflows/integrate.yaml +++ b/.github/workflows/integrate.yaml @@ -41,7 +41,7 @@ jobs: dependency-versions: ${{ matrix.dependencies }} - name: "Cache cache directory for vimeo/psalm" - uses: actions/cache@v1 + uses: actions/cache@v3 with: path: .build/psalm key: php-${{ matrix.php-version }}-psalm-${{ github.sha }} From af95597a2368def7e823c0ac713469f535517632 Mon Sep 17 00:00:00 2001 From: "Dr. Ulrich Thomas Gabor" Date: Wed, 22 Mar 2023 21:46:28 +0100 Subject: [PATCH 13/22] Extended RequiredSetterHandler to also check for attributes (#312) (cherry picked from commit 08b1cd732167f31e1b459c93efde603daa63ccd2) --- src/Handler/RequiredSetterHandler.php | 55 ++++++++++++++----- .../acceptance/RequiredSetter.feature | 41 ++++++++++++++ 2 files changed, 81 insertions(+), 15 deletions(-) diff --git a/src/Handler/RequiredSetterHandler.php b/src/Handler/RequiredSetterHandler.php index 1feb112a..5d21dce0 100644 --- a/src/Handler/RequiredSetterHandler.php +++ b/src/Handler/RequiredSetterHandler.php @@ -10,6 +10,7 @@ use Psalm\Internal\PhpVisitor\AssignmentMapVisitor; use Psalm\Plugin\EventHandler\AfterClassLikeVisitInterface; use Psalm\Plugin\EventHandler\Event\AfterClassLikeVisitEvent; +use Psalm\Storage\ClassLikeStorage; class RequiredSetterHandler implements AfterClassLikeVisitInterface { @@ -18,32 +19,56 @@ public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event) $stmt = $event->getStmt(); $storage = $event->getStorage(); + $stmt_source = $event->getStatementsSource(); + $aliases = $stmt_source->getAliases(); + if (!$stmt instanceof Class_) { return; } foreach ($stmt->getMethods() as $method) { + // Check for PhpDoc annotation $docComment = $method->getDocComment(); - if ($docComment instanceof Doc && false !== strpos($docComment->getText(), '@required')) { - $traverser = new NodeTraverser(); - $visitor = new AssignmentMapVisitor(null); - $traverser->addVisitor($visitor); - $traverser->traverse($method->getStmts() ?? []); - - foreach (array_keys($visitor->getAssignmentMap()) as $assignment) { - if (0 !== strpos($assignment, '$this->')) { - continue; - } + self::markAsInitializedProperties($storage, $method->getStmts() ?? []); + } - $property = substr($assignment, strlen('$this->')); - if (!array_key_exists($property, $storage->properties)) { - continue; + // Check for attribute annotation + foreach ($method->getAttrGroups() as $attrGroup) { + foreach ($attrGroup->attrs as $attribute) { + /** @var lowercase-string $lcName */ + $lcName = $attribute->name->toLowerString(); + if (array_key_exists($lcName, $aliases->uses)) { + $name = $aliases->uses[$lcName]; + } else { + $name = $attribute->name->toString(); + } + if ('Symfony\Contracts\Service\Attribute\Required' === $name) { + self::markAsInitializedProperties($storage, $method->getStmts() ?? []); } - - $storage->initialized_properties[$property] = true; } } } } + + private static function markAsInitializedProperties(ClassLikeStorage $storage, array $stmts): void + { + $traverser = new NodeTraverser(); + $visitor = new AssignmentMapVisitor(null); + $traverser->addVisitor($visitor); + $traverser->traverse($stmts); + + foreach (array_keys($visitor->getAssignmentMap()) as $assignment) { + if (0 !== strpos($assignment, '$this->')) { + continue; + } + + $property = substr($assignment, strlen('$this->')); + if (!array_key_exists($property, $storage->properties)) { + continue; + } + + $storage->initialized_properties[$property] = true; + } + } } diff --git a/tests/acceptance/acceptance/RequiredSetter.feature b/tests/acceptance/acceptance/RequiredSetter.feature index a16aea99..f8644b9a 100644 --- a/tests/acceptance/acceptance/RequiredSetter.feature +++ b/tests/acceptance/acceptance/RequiredSetter.feature @@ -44,3 +44,44 @@ Feature: Annotation class | Type | Message | | PropertyNotSetInConstructor | Property MyServiceB::$a is not defined in constructor of MyServiceB or in any private or final methods called in the constructor | And I see no other errors + + Scenario: PropertyNotSetInConstructor error is not raised when the required attribute is present (with use). + Given I have the following code + """ + a = $a; } + } + """ + When I run Psalm + Then I see no errors + + + Scenario: PropertyNotSetInConstructor error is not raised when the required attribute is present (without use). + Given I have the following code + """ + a = $a; } + } + """ + When I run Psalm + Then I see no errors From 91e18e199146ac9c3018cce34c17d82dda7bbcea Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 16 Jan 2023 06:37:24 +0100 Subject: [PATCH 14/22] [http foundation] revert hacky stub for getSession (#304) (cherry picked from commit 80670da23bfdd052f40d74bf3d13069fe96d96a0) --- .../Component/HttpFoundation/Request.stubphp | 5 ---- .../Component/HttpFoundation/Request.stubphp | 5 ---- .../acceptance/RequestSession.feature | 25 ------------------- 3 files changed, 35 deletions(-) delete mode 100644 tests/acceptance/acceptance/RequestSession.feature diff --git a/src/Stubs/5/Component/HttpFoundation/Request.stubphp b/src/Stubs/5/Component/HttpFoundation/Request.stubphp index 777850f4..edda81ca 100644 --- a/src/Stubs/5/Component/HttpFoundation/Request.stubphp +++ b/src/Stubs/5/Component/HttpFoundation/Request.stubphp @@ -18,9 +18,4 @@ class Request * @psalm-var InputBag */ public $cookies; - - /** - * @return \Symfony\Component\HttpFoundation\Session\Session - */ - public function getSession(): SessionInterface; } diff --git a/src/Stubs/6/Component/HttpFoundation/Request.stubphp b/src/Stubs/6/Component/HttpFoundation/Request.stubphp index 777850f4..edda81ca 100644 --- a/src/Stubs/6/Component/HttpFoundation/Request.stubphp +++ b/src/Stubs/6/Component/HttpFoundation/Request.stubphp @@ -18,9 +18,4 @@ class Request * @psalm-var InputBag */ public $cookies; - - /** - * @return \Symfony\Component\HttpFoundation\Session\Session - */ - public function getSession(): SessionInterface; } diff --git a/tests/acceptance/acceptance/RequestSession.feature b/tests/acceptance/acceptance/RequestSession.feature deleted file mode 100644 index bcf872d9..00000000 --- a/tests/acceptance/acceptance/RequestSession.feature +++ /dev/null @@ -1,25 +0,0 @@ -@symfony-5 @symfony-6 -Feature: Request getSessions - Symfony Request getSession method is returning a Session - - Background: - Given I have Symfony plugin enabled - And I have the following code preamble - """ - getSession()' has a Flashbag - Given I have the following code - """ - class App - { - public function index(Request $request): void - { - $request->getSession()->getFlashBag(); - } - } - """ - When I run Psalm - Then I see no errors From f017563fa40728a5a40628915774e7d3469a0cbd Mon Sep 17 00:00:00 2001 From: Farhad Safarov Date: Wed, 18 Jan 2023 22:42:22 +0300 Subject: [PATCH 15/22] [console] fix required argument value analysis (#305) (cherry picked from commit 9756ee3129b673d15278a6c79505097d85f9f403) --- src/Handler/ConsoleHandler.php | 5 +---- tests/acceptance/acceptance/console/ConsoleArgument.feature | 5 +++++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Handler/ConsoleHandler.php b/src/Handler/ConsoleHandler.php index b6acee6c..5303a9c8 100644 --- a/src/Handler/ConsoleHandler.php +++ b/src/Handler/ConsoleHandler.php @@ -158,11 +158,8 @@ private static function analyseArgument(array $args, StatementsSource $statement } $defaultParam = $normalizedParams['default']; - if ($defaultParam) { + if ($defaultParam && (!$defaultParam->value instanceof Expr\ConstFetch || 'null' !== $defaultParam->value->name->parts[0])) { $returnTypes->removeType('null'); - if ($defaultParam->value instanceof Expr\ConstFetch && 'null' === $defaultParam->value->name->parts[0]) { - $returnTypes->addType(new TNull()); - } } self::$arguments[$identifier] = $returnTypes->freeze(); diff --git a/tests/acceptance/acceptance/console/ConsoleArgument.feature b/tests/acceptance/acceptance/console/ConsoleArgument.feature index 010429e8..4b329c96 100644 --- a/tests/acceptance/acceptance/console/ConsoleArgument.feature +++ b/tests/acceptance/acceptance/console/ConsoleArgument.feature @@ -168,6 +168,7 @@ Feature: ConsoleArgument ->addArgument('arg5', InputArgument::OPTIONAL, '', 'default value') ->addArgument('arg6', InputArgument::OPTIONAL) ->addArgument('arg7', InputArgument::OPTIONAL, '', null) + ->addArgument('arg8', InputArgument::REQUIRED, '', null) ; } @@ -194,6 +195,9 @@ Feature: ConsoleArgument /** @psalm-trace $arg7 */ $arg7 = $input->getArgument('arg7'); + /** @psalm-trace $arg8 */ + $arg8 = $input->getArgument('arg8'); + return 0; } } @@ -208,6 +212,7 @@ Feature: ConsoleArgument | Trace | $arg5: string | | Trace | $arg6: null\|string | | Trace | $arg7: null\|string | + | Trace | $arg8: string | And I see no other errors Scenario Outline: Asserting array arguments return types have inferred From 623eeaa43a0c317dff5948947c0cc7912ca99a8f Mon Sep 17 00:00:00 2001 From: Zeljko Mitic Date: Fri, 14 Apr 2023 19:46:57 +0200 Subject: [PATCH 16/22] [framework bundle] abstract controller createForm visibility --- .../FrameworkBundle/Controller/AbstractController.stubphp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Stubs/6/Bundle/FrameworkBundle/Controller/AbstractController.stubphp b/src/Stubs/6/Bundle/FrameworkBundle/Controller/AbstractController.stubphp index 9dd5b346..bdf1c0bd 100644 --- a/src/Stubs/6/Bundle/FrameworkBundle/Controller/AbstractController.stubphp +++ b/src/Stubs/6/Bundle/FrameworkBundle/Controller/AbstractController.stubphp @@ -23,5 +23,5 @@ class AbstractController implements ServiceSubscriberInterface * * @psalm-return FormInterface */ - public function createForm(string $type, $data = null, array $options = []): FormInterface {} + protected function createForm(string $type, $data = null, array $options = []): FormInterface {} } From a6cef9c701686d17d4254b544d05345e9d3e0b88 Mon Sep 17 00:00:00 2001 From: Farhad Safarov Date: Fri, 14 Apr 2023 21:04:54 +0300 Subject: [PATCH 17/22] [framework] abstractcontroller - fix createForm method visibility (#315) --- .../FrameworkBundle/Controller/AbstractController.stubphp | 4 ++-- .../FrameworkBundle/Controller/AbstractController.stubphp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Stubs/5/Bundle/FrameworkBundle/Controller/AbstractController.stubphp b/src/Stubs/5/Bundle/FrameworkBundle/Controller/AbstractController.stubphp index 9dd5b346..8bf41530 100644 --- a/src/Stubs/5/Bundle/FrameworkBundle/Controller/AbstractController.stubphp +++ b/src/Stubs/5/Bundle/FrameworkBundle/Controller/AbstractController.stubphp @@ -7,7 +7,7 @@ use Psr\Container\ContainerInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormTypeInterface; -class AbstractController implements ServiceSubscriberInterface +abstract class AbstractController implements ServiceSubscriberInterface { /** * @var ContainerInterface @@ -23,5 +23,5 @@ class AbstractController implements ServiceSubscriberInterface * * @psalm-return FormInterface */ - public function createForm(string $type, $data = null, array $options = []): FormInterface {} + protected function createForm(string $type, $data = null, array $options = []): FormInterface {} } diff --git a/src/Stubs/6/Bundle/FrameworkBundle/Controller/AbstractController.stubphp b/src/Stubs/6/Bundle/FrameworkBundle/Controller/AbstractController.stubphp index bdf1c0bd..8bf41530 100644 --- a/src/Stubs/6/Bundle/FrameworkBundle/Controller/AbstractController.stubphp +++ b/src/Stubs/6/Bundle/FrameworkBundle/Controller/AbstractController.stubphp @@ -7,7 +7,7 @@ use Psr\Container\ContainerInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormTypeInterface; -class AbstractController implements ServiceSubscriberInterface +abstract class AbstractController implements ServiceSubscriberInterface { /** * @var ContainerInterface From cacf99f2658f1e13a74098a51d3a7be7758d6d1e Mon Sep 17 00:00:00 2001 From: Farhad Safarov Date: Mon, 6 Nov 2023 10:00:04 +0300 Subject: [PATCH 18/22] [DI] parameter type improvement (#319) --- psalm-baseline.xml | 14 +----- psalm.xml | 2 + src/Handler/AnnotationHandler.php | 3 -- src/Handler/ConsoleHandler.php | 12 ++--- src/Handler/ContainerDependencyHandler.php | 3 -- src/Handler/ContainerHandler.php | 10 +--- src/Handler/DoctrineQueryBuilderHandler.php | 3 -- src/Handler/DoctrineRepositoryHandler.php | 3 -- src/Handler/HeaderBagHandler.php | 2 +- src/Handler/ParameterBagHandler.php | 9 ++-- src/Plugin.php | 9 ++-- src/Symfony/ContainerMeta.php | 2 +- src/Twig/CachedTemplatesRegistry.php | 4 +- src/Twig/CachedTemplatesTainter.php | 4 ++ src/Twig/TemplateFileAnalyzer.php | 3 ++ tests/acceptance/acceptance/Envelope.feature | 8 ++-- .../acceptance/ParameterBag.feature | 46 ++++++++++++++++++- 17 files changed, 79 insertions(+), 58 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index b40e5133..6f8f4232 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,24 +1,14 @@ - + DeprecatedMethod - + int string - - - NonInvariantDocblockPropertyType - - - - - attributes - - diff --git a/psalm.xml b/psalm.xml index 29f47298..259e0c0d 100644 --- a/psalm.xml +++ b/psalm.xml @@ -5,6 +5,8 @@ xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" cacheDirectory=".build/psalm" errorBaseline="psalm-baseline.xml" + findUnusedCode="true" + findUnusedBaselineEntry="false" > diff --git a/src/Handler/AnnotationHandler.php b/src/Handler/AnnotationHandler.php index 7487834d..37a54e43 100644 --- a/src/Handler/AnnotationHandler.php +++ b/src/Handler/AnnotationHandler.php @@ -9,9 +9,6 @@ class AnnotationHandler implements AfterClassLikeVisitInterface { - /** - * {@inheritdoc} - */ public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event) { $stmt = $event->getStmt(); diff --git a/src/Handler/ConsoleHandler.php b/src/Handler/ConsoleHandler.php index 5303a9c8..c18f511f 100644 --- a/src/Handler/ConsoleHandler.php +++ b/src/Handler/ConsoleHandler.php @@ -38,9 +38,6 @@ class ConsoleHandler implements AfterMethodCallAnalysisInterface */ private static array $options = []; - /** - * {@inheritdoc} - */ public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $event): void { $statements_source = $event->getStatementsSource(); @@ -158,7 +155,7 @@ private static function analyseArgument(array $args, StatementsSource $statement } $defaultParam = $normalizedParams['default']; - if ($defaultParam && (!$defaultParam->value instanceof Expr\ConstFetch || 'null' !== $defaultParam->value->name->parts[0])) { + if ($defaultParam && (!$defaultParam->value instanceof Expr\ConstFetch || 'null' !== $defaultParam->value->name->getFirst())) { $returnTypes->removeType('null'); } @@ -206,7 +203,7 @@ private static function analyseOption(array $args, StatementsSource $statements_ } if ($defaultParam->value instanceof Expr\ConstFetch) { - switch ($defaultParam->value->name->parts[0]) { + switch ($defaultParam->value->name->getFirst()) { case 'null': $returnTypes->addType(new TNull()); break; @@ -273,10 +270,7 @@ private static function normalizeParams(array $params, array $args): array return $result; } - /** - * @param mixed $mode - */ - private static function getModeValue($mode): ?int + private static function getModeValue(Expr $mode): ?int { if ($mode instanceof Expr\BinaryOp\BitwiseOr) { return self::getModeValue($mode->left) | self::getModeValue($mode->right); diff --git a/src/Handler/ContainerDependencyHandler.php b/src/Handler/ContainerDependencyHandler.php index 52ac8a72..0988ba32 100644 --- a/src/Handler/ContainerDependencyHandler.php +++ b/src/Handler/ContainerDependencyHandler.php @@ -12,9 +12,6 @@ class ContainerDependencyHandler implements AfterFunctionLikeAnalysisInterface { - /** - * {@inheritdoc} - */ public static function afterStatementAnalysis(AfterFunctionLikeAnalysisEvent $event): ?bool { $stmt = $event->getStmt(); diff --git a/src/Handler/ContainerHandler.php b/src/Handler/ContainerHandler.php index 3356aa9f..117b9416 100644 --- a/src/Handler/ContainerHandler.php +++ b/src/Handler/ContainerHandler.php @@ -41,9 +41,6 @@ public static function init(ContainerMeta $containerMeta): void self::$containerMeta = $containerMeta; } - /** - * {@inheritdoc} - */ public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $event): void { $declaring_method_id = $event->getDeclaringMethodId(); @@ -131,8 +128,8 @@ public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $eve if (!$service->isPublic()) { /** @var class-string $kernelTestCaseClass */ $kernelTestCaseClass = 'Symfony\Bundle\FrameworkBundle\Test\KernelTestCase'; - $isTestContainer = $context->parent && - ($kernelTestCaseClass === $context->parent + $isTestContainer = $context->parent + && ($kernelTestCaseClass === $context->parent || is_subclass_of($context->parent, $kernelTestCaseClass) ); if (!$isTestContainer) { @@ -150,9 +147,6 @@ public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $eve } } - /** - * {@inheritdoc} - */ public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event) { $codebase = $event->getCodebase(); diff --git a/src/Handler/DoctrineQueryBuilderHandler.php b/src/Handler/DoctrineQueryBuilderHandler.php index 1104999b..a7d8dc9e 100644 --- a/src/Handler/DoctrineQueryBuilderHandler.php +++ b/src/Handler/DoctrineQueryBuilderHandler.php @@ -12,9 +12,6 @@ class DoctrineQueryBuilderHandler implements AfterMethodCallAnalysisInterface { - /** - * {@inheritdoc} - */ public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $event): void { $expr = $event->getExpr(); diff --git a/src/Handler/DoctrineRepositoryHandler.php b/src/Handler/DoctrineRepositoryHandler.php index 899f50e1..91d8c883 100644 --- a/src/Handler/DoctrineRepositoryHandler.php +++ b/src/Handler/DoctrineRepositoryHandler.php @@ -20,9 +20,6 @@ class DoctrineRepositoryHandler implements AfterMethodCallAnalysisInterface, AfterClassLikeVisitInterface { - /** - * {@inheritdoc} - */ public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $event): void { $expr = $event->getExpr(); diff --git a/src/Handler/HeaderBagHandler.php b/src/Handler/HeaderBagHandler.php index 8493e4ab..e5827089 100644 --- a/src/Handler/HeaderBagHandler.php +++ b/src/Handler/HeaderBagHandler.php @@ -60,7 +60,7 @@ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event) */ private static function makeReturnType(array $call_args): Union { - if (3 === count($call_args) && (($arg = $call_args[2]->value) instanceof ConstFetch) && 'false' === $arg->name->parts[0]) { + if (3 === count($call_args) && (($arg = $call_args[2]->value) instanceof ConstFetch) && 'false' === $arg->name->getFirst()) { return new Union([new TArray([new Union([new TInt()]), new Union([new TString()])])]); } diff --git a/src/Handler/ParameterBagHandler.php b/src/Handler/ParameterBagHandler.php index eec91c2b..9dd1318d 100644 --- a/src/Handler/ParameterBagHandler.php +++ b/src/Handler/ParameterBagHandler.php @@ -27,7 +27,11 @@ public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $eve $declaring_method_id = $event->getDeclaringMethodId(); $expr = $event->getExpr(); - if (!self::$containerMeta || 'Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface::get' !== $declaring_method_id) { + if (!self::$containerMeta || !in_array($declaring_method_id, [ + 'Symfony\Bundle\FrameworkBundle\Controller\AbstractController::getparameter', + 'Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface::get', + 'Symfony\Component\DependencyInjection\ContainerInterface::getparameter', + ], true)) { return; } @@ -36,7 +40,6 @@ public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $eve } $argument = $expr->args[0]->value->value; - try { $parameter = self::$containerMeta->getParameter($argument); } catch (ParameterNotFoundException $e) { @@ -53,7 +56,7 @@ public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $eve $event->setReturnTypeCandidate(new Union([Atomic::create('bool')])); break; case 'integer': - $event->setReturnTypeCandidate(new Union([Atomic::create('integer')])); + $event->setReturnTypeCandidate(new Union([Atomic::create('int')])); break; case 'double': $event->setReturnTypeCandidate(new Union([Atomic::create('float')])); diff --git a/src/Plugin.php b/src/Plugin.php index 1ed2810a..ab61d402 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -29,9 +29,6 @@ */ class Plugin implements PluginEntryPointInterface { - /** - * {@inheritdoc} - */ public function __invoke(RegistrationInterface $api, \SimpleXMLElement $config = null): void { require_once __DIR__.'/Handler/HeaderBagHandler.php'; @@ -68,7 +65,8 @@ public function __invoke(RegistrationInterface $api, \SimpleXMLElement $config = ContainerHandler::init($containerMeta); try { - TemplateFileAnalyzer::initExtensions(array_filter(array_map(function (array $m) use ($containerMeta) { + /** @psalm-var list $extensionClasses */ + $extensionClasses = array_filter(array_map(function (array $m) use ($containerMeta) { if ('addExtension' !== $m[0]) { return null; } @@ -78,7 +76,8 @@ public function __invoke(RegistrationInterface $api, \SimpleXMLElement $config = } catch (ServiceNotFoundException $e) { return null; } - }, $containerMeta->get('twig')->getMethodCalls()))); + }, $containerMeta->get('twig')->getMethodCalls())); + TemplateFileAnalyzer::initExtensions($extensionClasses); } catch (ServiceNotFoundException $e) { } diff --git a/src/Symfony/ContainerMeta.php b/src/Symfony/ContainerMeta.php index 30ba76a0..7dfee1d9 100644 --- a/src/Symfony/ContainerMeta.php +++ b/src/Symfony/ContainerMeta.php @@ -44,7 +44,7 @@ public function __construct(array $containerXmlPaths) /** * @throws ServiceNotFoundException */ - public function get(string $id, ?string $contextClass = null): Definition + public function get(string $id, string $contextClass = null): Definition { if ($contextClass && isset($this->classLocators[$contextClass]) && isset($this->serviceLocators[$this->classLocators[$contextClass]]) && isset($this->serviceLocators[$this->classLocators[$contextClass]][$id])) { $id = $this->serviceLocators[$this->classLocators[$contextClass]][$id]; diff --git a/src/Twig/CachedTemplatesRegistry.php b/src/Twig/CachedTemplatesRegistry.php index 787d6a17..14c479ab 100644 --- a/src/Twig/CachedTemplatesRegistry.php +++ b/src/Twig/CachedTemplatesRegistry.php @@ -55,8 +55,10 @@ private static function generateNames(string $baseName): \Generator $oldNotation = $baseName; } - if (null !== $oldNotation) { + if (null !== $oldNotation && false !== strpos($oldNotation, ':')) { + /** @psalm-suppress PossiblyUndefinedArrayOffset */ list($bundleName, $rest) = explode(':', $oldNotation, 2); + /** @psalm-suppress PossiblyUndefinedArrayOffset */ list($revTemplateName, $revRest) = explode(':', strrev($rest), 2); $pathParts = explode('/', strrev($revRest)); $pathParts = array_merge($pathParts, explode('/', strrev($revTemplateName))); diff --git a/src/Twig/CachedTemplatesTainter.php b/src/Twig/CachedTemplatesTainter.php index f8f73059..fe01ba15 100644 --- a/src/Twig/CachedTemplatesTainter.php +++ b/src/Twig/CachedTemplatesTainter.php @@ -51,6 +51,10 @@ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event) isset($call_args[1]) ? [$call_args[1]] : [] ); + if (!isset($call_args[0])) { + return null; + } + try { $templateName = TwigUtils::extractTemplateNameFromExpression($call_args[0]->value, $source); } catch (TemplateNameUnresolvedException $exception) { diff --git a/src/Twig/TemplateFileAnalyzer.php b/src/Twig/TemplateFileAnalyzer.php index 0ad37c27..cbb3251f 100644 --- a/src/Twig/TemplateFileAnalyzer.php +++ b/src/Twig/TemplateFileAnalyzer.php @@ -28,6 +28,9 @@ class TemplateFileAnalyzer extends FileAnalyzer */ private static $extensionClasses = []; + /** + * @param list $extensionClasses + */ public static function initExtensions(array $extensionClasses): void { self::$extensionClasses = $extensionClasses; diff --git a/tests/acceptance/acceptance/Envelope.feature b/tests/acceptance/acceptance/Envelope.feature index 7d53a32e..59b3ddd6 100644 --- a/tests/acceptance/acceptance/Envelope.feature +++ b/tests/acceptance/acceptance/Envelope.feature @@ -110,10 +110,10 @@ Feature: Messenger Envelope $stamp = $envelope->last(Symfony\Component\Messenger\Worker::class); """ When I run Psalm - Then I see these errors - | Type | Message | - | InvalidArgument | Argument 1 of Symfony\Component\Messenger\Envelope::last expects class-string, but Symfony\Component\Messenger\Worker::class provided | - And I see no other errors +# Then I see these errors +# | Type | Message | +# | InvalidArgument | Argument 1 of Symfony\Component\Messenger\Envelope::last expects class-string, but Symfony\Component\Messenger\Worker::class provided | + Then I see no other errors Scenario: Envelope::last() returns a nullable object of the provided class name Given I have the following code diff --git a/tests/acceptance/acceptance/ParameterBag.feature b/tests/acceptance/acceptance/ParameterBag.feature index cbd6b04b..0eb341d3 100644 --- a/tests/acceptance/acceptance/ParameterBag.feature +++ b/tests/acceptance/acceptance/ParameterBag.feature @@ -1,5 +1,5 @@ @symfony-5 @symfony-6 -Feature: ParameterBag +Feature: ParameterBag return type detection if container.xml is provided Background: Given I have Symfony plugin enabled with the following config @@ -13,7 +13,7 @@ Feature: ParameterBag use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; """ - Scenario: Asserting psalm recognizes return type of Symfony parameters if container.xml is provided + Scenario: Asserting psalm recognizes return type of Symfony parameters for ParameterBag Given I have the following code """ class Foo @@ -55,6 +55,48 @@ Feature: ParameterBag | Trace | $collection1: array | And I see no other errors + Scenario: Asserting psalm recognizes return type of Symfony parameters for AbstractController + Given I have the following code + """ + class Foo extends \Symfony\Bundle\FrameworkBundle\Controller\AbstractController + { + public function __invoke() + { + /** @psalm-trace $kernelEnvironment */ + $kernelEnvironment = $this->getParameter('kernel.environment'); + + /** @psalm-trace $debugEnabled */ + $debugEnabled = $this->getParameter('debug_enabled'); + + /** @psalm-trace $debugDisabled */ + $debugDisabled = $this->getParameter('debug_disabled'); + + /** @psalm-trace $version */ + $version = $this->getParameter('version'); + + /** @psalm-trace $integerOne */ + $integerOne = $this->getParameter('integer_one'); + + /** @psalm-trace $pi */ + $pi = $this->getParameter('pi'); + + /** @psalm-trace $collection1 */ + $collection1 = $this->getParameter('collection1'); + } + } + """ + When I run Psalm + Then I see these errors + | Type | Message | + | Trace | $kernelEnvironment: string | + | Trace | $debugEnabled: bool | + | Trace | $debugDisabled: bool | + | Trace | $version: string | + | Trace | $integerOne: int | + | Trace | $pi: float | + | Trace | $collection1: array | + And I see no other errors + Scenario: Get non-existent parameter Given I have the following code """ From 517205bd91669787c00b15ce0cdf1e408f11c257 Mon Sep 17 00:00:00 2001 From: Farhad Safarov Date: Fri, 10 Nov 2023 08:58:22 +0300 Subject: [PATCH 19/22] [container] parameterbag handler - fix container classes (#324) --- src/Handler/ContainerHandler.php | 2 +- src/Handler/ParameterBagHandler.php | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Handler/ContainerHandler.php b/src/Handler/ContainerHandler.php index 117b9416..a4ce5a83 100644 --- a/src/Handler/ContainerHandler.php +++ b/src/Handler/ContainerHandler.php @@ -165,7 +165,7 @@ public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event) } } - private static function isContainerMethod(string $declaringMethodId, string $methodName): bool + public static function isContainerMethod(string $declaringMethodId, string $methodName): bool { return in_array( $declaringMethodId, diff --git a/src/Handler/ParameterBagHandler.php b/src/Handler/ParameterBagHandler.php index 9dd1318d..f8a84ad1 100644 --- a/src/Handler/ParameterBagHandler.php +++ b/src/Handler/ParameterBagHandler.php @@ -24,14 +24,16 @@ public static function init(ContainerMeta $containerMeta): void public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $event): void { + if (!self::$containerMeta) { + return; + } + $declaring_method_id = $event->getDeclaringMethodId(); $expr = $event->getExpr(); - if (!self::$containerMeta || !in_array($declaring_method_id, [ - 'Symfony\Bundle\FrameworkBundle\Controller\AbstractController::getparameter', - 'Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface::get', - 'Symfony\Component\DependencyInjection\ContainerInterface::getparameter', - ], true)) { + if (!ContainerHandler::isContainerMethod($declaring_method_id, 'getparameter') + && 'Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface::get' !== $declaring_method_id + ) { return; } From 2a3f81e62278f488a21b70603df4cfb845af70ad Mon Sep 17 00:00:00 2001 From: Farhad Safarov Date: Fri, 10 Nov 2023 10:14:46 +0300 Subject: [PATCH 20/22] Handle UnusedClass false positive (#325) --- src/Handler/ContainerHandler.php | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Handler/ContainerHandler.php b/src/Handler/ContainerHandler.php index a4ce5a83..cdc19d0a 100644 --- a/src/Handler/ContainerHandler.php +++ b/src/Handler/ContainerHandler.php @@ -9,8 +9,10 @@ use Psalm\CodeLocation; use Psalm\IssueBuffer; use Psalm\Plugin\EventHandler\AfterClassLikeVisitInterface; +use Psalm\Plugin\EventHandler\AfterCodebasePopulatedInterface; use Psalm\Plugin\EventHandler\AfterMethodCallAnalysisInterface; use Psalm\Plugin\EventHandler\Event\AfterClassLikeVisitEvent; +use Psalm\Plugin\EventHandler\Event\AfterCodebasePopulatedEvent; use Psalm\Plugin\EventHandler\Event\AfterMethodCallAnalysisEvent; use Psalm\SymfonyPsalmPlugin\Issue\NamingConventionViolation; use Psalm\SymfonyPsalmPlugin\Issue\PrivateService; @@ -20,7 +22,7 @@ use Psalm\Type\Union; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; -class ContainerHandler implements AfterMethodCallAnalysisInterface, AfterClassLikeVisitInterface +class ContainerHandler implements AfterMethodCallAnalysisInterface, AfterClassLikeVisitInterface, AfterCodebasePopulatedInterface { private const GET_CLASSLIKES = [ 'Psr\Container\ContainerInterface', @@ -165,6 +167,23 @@ public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event) } } + public static function afterCodebasePopulated(AfterCodebasePopulatedEvent $event): void + { + if (null === self::$containerMeta) { + return; + } + + $containerClassNames = array_map(function (string $className): string { + return strtolower($className); + }, self::$containerMeta->getClassNames()); + + foreach ($event->getCodebase()->classlike_storage_provider->getAll() as $name => $storage) { + if (in_array($name, $containerClassNames, true)) { + $storage->suppressed_issues[] = 'UnusedClass'; + } + } + } + public static function isContainerMethod(string $declaringMethodId, string $methodName): bool { return in_array( From cfbc483f57f23b14dca49c4b833583df4a440b77 Mon Sep 17 00:00:00 2001 From: Farhad Safarov Date: Sun, 12 Nov 2023 12:44:21 +0300 Subject: [PATCH 21/22] [container] don't report service constructors as PossiblyUnusedMethod (#326) --- src/Handler/ContainerHandler.php | 33 ++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/Handler/ContainerHandler.php b/src/Handler/ContainerHandler.php index cdc19d0a..c22ae57f 100644 --- a/src/Handler/ContainerHandler.php +++ b/src/Handler/ContainerHandler.php @@ -11,9 +11,11 @@ use Psalm\Plugin\EventHandler\AfterClassLikeVisitInterface; use Psalm\Plugin\EventHandler\AfterCodebasePopulatedInterface; use Psalm\Plugin\EventHandler\AfterMethodCallAnalysisInterface; +use Psalm\Plugin\EventHandler\BeforeAddIssueInterface; use Psalm\Plugin\EventHandler\Event\AfterClassLikeVisitEvent; use Psalm\Plugin\EventHandler\Event\AfterCodebasePopulatedEvent; use Psalm\Plugin\EventHandler\Event\AfterMethodCallAnalysisEvent; +use Psalm\Plugin\EventHandler\Event\BeforeAddIssueEvent; use Psalm\SymfonyPsalmPlugin\Issue\NamingConventionViolation; use Psalm\SymfonyPsalmPlugin\Issue\PrivateService; use Psalm\SymfonyPsalmPlugin\Issue\ServiceNotFound; @@ -22,7 +24,7 @@ use Psalm\Type\Union; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; -class ContainerHandler implements AfterMethodCallAnalysisInterface, AfterClassLikeVisitInterface, AfterCodebasePopulatedInterface +class ContainerHandler implements AfterMethodCallAnalysisInterface, AfterClassLikeVisitInterface, AfterCodebasePopulatedInterface, BeforeAddIssueInterface { private const GET_CLASSLIKES = [ 'Psr\Container\ContainerInterface', @@ -38,9 +40,18 @@ class ContainerHandler implements AfterMethodCallAnalysisInterface, AfterClassLi */ private static $containerMeta; + /** + * @var array collection of cower-cased class names that are present in the container + */ + private static array $containerClassNames = []; + public static function init(ContainerMeta $containerMeta): void { self::$containerMeta = $containerMeta; + + self::$containerClassNames = array_map(function (string $className): string { + return strtolower($className); + }, self::$containerMeta->getClassNames()); } public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $event): void @@ -173,17 +184,27 @@ public static function afterCodebasePopulated(AfterCodebasePopulatedEvent $event return; } - $containerClassNames = array_map(function (string $className): string { - return strtolower($className); - }, self::$containerMeta->getClassNames()); - foreach ($event->getCodebase()->classlike_storage_provider->getAll() as $name => $storage) { - if (in_array($name, $containerClassNames, true)) { + if (in_array($name, self::$containerClassNames, true)) { $storage->suppressed_issues[] = 'UnusedClass'; } } } + public static function beforeAddIssue(BeforeAddIssueEvent $event): ?bool + { + $data = $event->getIssue()->toIssueData('error'); + if ('PossiblyUnusedMethod' === $data->type + && '__construct' === $data->selected_text + && null !== $data->dupe_key + && in_array(preg_replace('/::__construct$/', '', $data->dupe_key), self::$containerClassNames, true)) { + // Don't report service constructors as PossiblyUnusedMethod + return false; + } + + return null; + } + public static function isContainerMethod(string $declaringMethodId, string $methodName): bool { return in_array( From f23ec3439743fb24f5c1101e52d032f23d5befa6 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Sun, 12 Nov 2023 05:04:27 -0500 Subject: [PATCH 22/22] Allow/test with Symfony 7 (#323) --- .github/workflows/integrate.yaml | 6 ++++++ composer.json | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/integrate.yaml b/.github/workflows/integrate.yaml index 8543a23b..620a5f93 100644 --- a/.github/workflows/integrate.yaml +++ b/.github/workflows/integrate.yaml @@ -66,14 +66,20 @@ jobs: php-version: - 8.0 - 8.1 + - 8.2 symfony-version: - 5 - 6 + - 7 exclude: - php-version: 8.0 symfony-version: 6 + - php-version: 8.0 + symfony-version: 7 + - php-version: 8.1 + symfony-version: 7 steps: - name: "Checkout" diff --git a/composer.json b/composer.json index cd879125..6d3bc610 100644 --- a/composer.json +++ b/composer.json @@ -12,19 +12,19 @@ "require": { "php": "^7.4 || ^8.0", "ext-simplexml": "*", - "symfony/framework-bundle": "^5.0 || ^6.0", + "symfony/framework-bundle": "^5.0 || ^6.0 || ^7.0", "vimeo/psalm": "^5.1" }, "require-dev": { - "symfony/form": "^5.0 || ^6.0", + "symfony/form": "^5.0 || ^6.0 || ^7.0", "doctrine/annotations": "^1.8|^2", "doctrine/orm": "^2.9", "phpunit/phpunit": "~7.5 || ~9.5", "symfony/cache-contracts": "^1.0 || ^2.0", "symfony/console": "*", - "symfony/messenger": "^5.0 || ^6.0", + "symfony/messenger": "^5.0 || ^6.0 || ^7.0", "symfony/security-guard": "*", - "symfony/serializer": "^5.0 || ^6.0", + "symfony/serializer": "^5.0 || ^6.0 || ^7.0", "symfony/validator": "*", "twig/twig": "^2.10 || ^3.0", "weirdan/codeception-psalm-module": "dev-master"