Giter Club home page Giter Club logo

psalm-plugin-symfony's People

Contributors

adrienlucas avatar amberovsky avatar andyexeter avatar aszenz avatar b-vadym avatar bendavies avatar chalasr avatar ddeboer avatar dependabot[bot] avatar enumag avatar fluffycondor avatar kbond avatar kevin-emo avatar mdeboer avatar micheh avatar mitelg avatar muglug avatar ostrolucky avatar patriziawacht avatar rgrassian avatar seferov avatar sidz avatar thomaslandauer avatar vincentlanglet avatar vudaltsov avatar weirdan avatar weph avatar wooky avatar wouterj avatar zmitic avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

psalm-plugin-symfony's Issues

Cannot suppress ContainerDependency

I have several services that use the container by their very nature, like lazy-load-proxy-services or generic classes that execute methods on configurable services. This plugin flags these services with the ContainerDependency issue, which i would like to suppress for these instances. Psalm itself allows suppressing any issues using the psalm-suppress annotation, but this does not seem to work for the ContainerDependency issue-type.

https://psalm.dev/docs/running_psalm/dealing_with_code_issues/
https://psalm.dev/docs/annotating_code/supported_annotations/#psalm-suppress-someissuename

$ bin/psalm src/Foo/Bar/SomeExampleLazyLoadProxy.php
Scanning files...
Analyzing files...

E

ERROR: ContainerDependency - src/Foo/Bar/SomeExampleLazyLoadProxy.php:26:5 - Container must not inject into services as dependency! Use dependency-injection. (see https://psalm.dev/000)
    /**
     * @psalm-suppress ContainerDependency
     */
    public function __construct(
        ContainerInterface $container,
        string $serviceId
    ) {
        $this->container = $container;
        $this->serviceId = $serviceId;
    }


------------------------------
1 errors found
------------------------------

Checks took 11.64 seconds and used 1,694.197MB of memory
Psalm was able to infer types for 93.8762% of the codebase

It even shows the suppress-annotation in the error-output itself.

I am not sure if this is an issue with psalm itself or with this plugin, but when looking at the issues in psalm itself that have to do with suppression, it seems to me that suppression works differently depending on the issue-type and the ContainerDependency issue is colored differently then the other issues in the console, so I think that this is probably a problem with the issue-type ContainerDependency itself.

Have I maybe applied the suppression in a wrong way? I tried different ways of specifying it (one-liner, on the class, on the assignment-line, supressing "all"), but suppressing this issue seems impossible at the moment.

Versions:
psalm/plugin-symfony: v1.2.3
vimeo/psalm: 3.11.6
symfony 3.4.41
PHP 7.2.31.

Access to undefined property ClassConstFetch::$value in ConsoleHandler

Running Psalm on lowest level on PHP 7.4.6 I am getting many of the following errors:

Notice: Undefined property: PhpParser\Node\Expr\ClassConstFetch::$value in /app/vendor/psalm/plugin-symfony/src/Handler/ConsoleHandler.php on line 113

This seems to originate from Symfony commands with custom arguments or options in such form:

$this->addArgument(self::FOO_ARGUMENT_NAME, InputArgument::REQUIRED);
$this->addOption(self::FOO_OPTION_NAME, null, InputOption::VALUE_NONE);

More specifically it's caused by the argument or option name being referenced from a constant.

nikic/php-parser: v4.4.0
psalm/plugin-symfony: v1.2.2

RouteCollection: Unable to determine the type that $route is being assigned to

class Test {
    private RouterInterface $router;
    
    public function __construct(RouterInterface $router) {
        $routeCollection = $this->router->getRouteCollection();
        
        // MixedAssignment: Unable to determine the type that $route is being assigned to
        foreach ($routeCollection as $routeName => $route) {
            // MixedArgumentTypeCoercion: Argument 1 of str_starts_with expects string, parent type array-key provided
            if (str_starts_with($routeName, 'whatever')) {
                echo 'hello!';
            }
        }
    }
}

It looks for me that $route always will be an instance of \Symfony\Component\Routing\Route in this case, and $routeName always will be a string, not an array-key.

Document the two twig tainting approaches

#61 added support for Twig taint analysis with two different approaches (file analyzer and cached template files).

According to the pull request, "The two have their own advantages.". Unfortunately, it is not documented what the differences and advantages are of each approach.

Optionally disable MissingReturnType on actions

Its a bit annoying to get around the MissingReturnType errors on controller action methods, since they can return many different things (Response, array, any objects depending on view listeners).

Would be nice to have a config to disable MissingReturnType if the method is a *Controller::*Action.

Plugin should reduce type for CLI arguments that don't specify InputArgument::IS_ARRAY

class CreateUserCommand extends ContainerAwareCommand
{
    protected function configure(): void
    {
        $this
            ->setDescription('Create a user')
            ->setDefinition(
                [
                    new InputArgument('email', InputArgument::REQUIRED, 'Email'),
                ],
            )
    }

    /**
     * @see Command
     */
    protected function execute(InputInterface $input, OutputInterface $output): void
    {
        $userManager = $this->getContainer()->get(UserManager::class);

        $user = new User();
        $user->setEmail($input->getArgument('email'));
    }
}

These kind of commands currently result in

ERROR: PossiblyInvalidArgument - src/AppBundle/Command/CreateUserCommand.php:63:25 - Argument 1 of AppBundle\Entity\User::setEmail expects null|string, possibly different type array<array-key, string>|bool|null|string provided (see https://psalm.dev/092)
        $user->setEmail($input->getOption('email'));

`NamingConventionViolation` does not correspond to Symfony standards

As Symfony documentation states, services' names can be FQCNs. But plugin raises NamingConventionViolation when service is accessed this way and I get:

ERROR: NamingConventionViolation - tests/... - Use snake_case for configuration parameter and service names (see https://psalm.dev/000)
        $this->hub = self::$container->get(HubInterface::class);

Works OK when using like self::$container->get('event_dispatcher'). But Sentry\State\HubInterface does not have snake_case alias and I don't want to create it just for Psalm ๐Ÿ˜‰ Would be great if naming convention was determined differently for parameters and services.

Twig taint analysis does not support @App/ syntax

So our project combines AppBundle:DataProvider/GraduateJobs:job.xml.twig and @App/DataProvider/GraduateJobs/job.xml.twig syntaxes. In a project, both works, however, looks like plugin supports former one only. Can we support both? Otherwise we get

Scanning files...
Analyzing files...

Uncaught RuntimeException: The template @App/DataProvider/GraduateJobs/job.xml.twig was not found. in /var/www/vendor/psalm/plugin-symfony/src/Twig/CachedTemplatesMapping.php:69
Stack trace:
#0 /var/www/vendor/psalm/plugin-symfony/src/Twig/CachedTemplatesTainter.php(58): Psalm\SymfonyPsalmPlugin\Twig\CachedTemplatesMapping::getCacheClassName('@App/DataProvid...')
#1 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php(105): Psalm\SymfonyPsalmPlugin\Twig\CachedTemplatesTainter::getMethodReturnType(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), 'Twig\\Environmen...', 'render', Array, Object(Psalm\Context), Object(Psalm\CodeLocation), NULL, NULL, NULL)
#2 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php(53): Psalm\Internal\Provider\MethodReturnTypeProvider->getReturnType(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), 'Twig\\Environmen...', 'render', Array, Object(Psalm\Context), Object(Psalm\CodeLocation), NULL)
#3 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php(680): Psalm\Internal\Analyzer\Statements\Expression\Call\Method\MethodCallReturnTypeFetcher::fetch(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(Psalm\Codebase), Object(PhpParser\Node\Expr\MethodCall), Object(Psalm\Context), Object(Psalm\Internal\MethodIdentifier), Object(Psalm\Internal\MethodIdentifier), Object(Psalm\Internal\MethodIdentifier), 'Twig\\Environmen...', Object(Psalm\Type\Atomic\TNamedObject), Object(Psalm\Type\Atomic\TNamedObject), Array, Object(Psalm\Internal\Analyzer\Statements\Expression\Call\Method\AtomicMethodCallAnalysisResult), Object(Psalm\Internal\Type\TemplateResult))
#4 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php(168): Psalm\Internal\Analyzer\Statements\Expression\Call\Method\AtomicMethodCallAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\MethodCall), Object(Psalm\Codebase), Object(Psalm\Context), Object(Psalm\Type\Atomic\TNamedObject), Object(Psalm\Type\Atomic\TNamedObject), false, NULL, Object(Psalm\Internal\Analyzer\Statements\Expression\Call\Method\AtomicMethodCallAnalysisResult))
#5 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php(144): Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\MethodCall), Object(Psalm\Context))
#6 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php(39): Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::handleExpression(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\MethodCall), Object(Psalm\Context), false, NULL, false)
#7 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php(135): Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\MethodCall), Object(Psalm\Context))
#8 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php(457): Psalm\Internal\Analyzer\Statements\ReturnAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Stmt\Return_), Object(Psalm\Context))
#9 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php(164): Psalm\Internal\Analyzer\StatementsAnalyzer::analyzeStatement(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Stmt\Return_), Object(Psalm\Context), Object(Psalm\Context))
#10 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php(591): Psalm\Internal\Analyzer\StatementsAnalyzer->analyze(Array, Object(Psalm\Context), Object(Psalm\Context), true)
#11 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ClassAnalyzer.php(1897): Psalm\Internal\Analyzer\FunctionLikeAnalyzer->analyze(Object(Psalm\Context), Object(Psalm\Internal\Provider\NodeDataProvider), Object(Psalm\Context))
#12 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ClassAnalyzer.php(734): Psalm\Internal\Analyzer\ClassAnalyzer->analyzeClassMethod(Object(PhpParser\Node\Stmt\ClassMethod), Object(Psalm\Storage\ClassLikeStorage), Object(Psalm\Internal\Analyzer\ClassAnalyzer), Object(Psalm\Context), Object(Psalm\Context))
#13 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FileAnalyzer.php(215): Psalm\Internal\Analyzer\ClassAnalyzer->analyze(Object(Psalm\Context), Object(Psalm\Context))
#14 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(336): Psalm\Internal\Analyzer\FileAnalyzer->analyze(NULL)
#15 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(576): Psalm\Internal\Codebase\Analyzer->Psalm\Internal\Codebase\{closure}(223, '/var/www/src/Ap...')
#16 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(268): Psalm\Internal\Codebase\Analyzer->doAnalysis(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 1)
#17 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php(639): Psalm\Internal\Codebase\Analyzer->analyzeFiles(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 1, false, true)
#18 /var/www/vendor/vimeo/psalm/src/psalm.php(677): Psalm\Internal\Analyzer\ProjectAnalyzer->check('/var/www/', false)
#19 /var/www/vendor/vimeo/psalm/psalm(2): require_once('/var/www/vendor...')
#20 {main}
(Psalm 3.16@d03e5ef057d6adc656c0ff7e166c50b73b4f48f3 crashed due to an uncaught Throwable)

cc @adrienlucas

Invalid RepositoryStringShortcut error

ERROR: RepositoryStringShortcut - src/AppBundle/Validator/Constraints/UniqueDataValidator.php:34:62 - Use Entity::class syntax instead (see https://psalm.dev/000)
        $exists = (bool) $this->entityManager->getRepository($constraint->entityClass)->findOneBy(

Here, code is retrieving class-string value from property of constraint instance, so I don't see how would I change that with ::class notation.

Performance problem - Rescanning all services during stub registration.

Hi there,

This plugin is causing a large performance penalty which is most obvious on symfony projects with a large amount of services.

This line:

$codebase->queueClassLikeForScanning($className);

is causing all services to be re-scanned, and is being triggered here: https://github.com/vimeo/psalm/blob/a80d5b736bddfee3e1d261ad3460d9fa8e7879f0/src/Psalm/Config.php#L1811

In my particular use case that's waiting 1 minute plus for thousands of services to be re-scanned - for a simple change to one file.

I have discussed this with @muglug on slack briefly, and the suggestion is that Codebase::queueClassLikeForScanning needs a way to specify that the file to be scanned is not a stub.

Raising here for before that conversation disappears into the ether.

Thanks

Support reduced type inferring from Collection::filter

Plugin should add capability to infer reduced type from Collection::filter based on assertions in a callback, similarly as Psalm currently does for array_filter. Here is what should be supported: https://psalm.dev/r/1b5cbdc9c0

Backstory: From conversation with @muglug:

Add that to the doctrine plugin

This is the current annotation: https://github.com/doctrine/collections/blob/a4504c79efd8847cc77d10b70209ef838b10338f/lib/Doctrine/Common/Collections/Collection.php#L214-L225

thereโ€™s basically no way to annotate that in a docblock

PropertyNotSetInConstructor and @required setter

See original issue at vimeo/psalm#4241

Hello,

In my code base, there are many services, who handled by symfony container (dependencies injection).
Sometimes I inject in my service an another service via a setter method.

Example:

<?php

final class MyServiceA {
}

final class MyServiceB {
    private MyServiceA $a;
    public function __construct(){}

    /** @required */
    private function setMyServiceA(MyServiceA $a): void { $this->a = $a; }
}

In symfony, if I annotate my setter with @required annotation, symfony will inject the service in setter just after the constructor. The property $a will therefore always initialized.

In my exemple, Psalm raise an PropertyNotSetInConstructor error.
It would be nice if Psalm would handle this annotation.

Thanks you for your jobs.

https://psalm.dev/r/df711a1f40

False positive "UnusedClass" if classes used via their interface

Hello.

Given:

Psalm 4.6.2
psalm/symfony-plugin v.2.1.2
Symfony v4.4.*

PHP code:

namespace App;

interface SomeImportantInterface {}

class SomeLogic implements SomeImportantInterface {}

class Handler
{
    public function __invoke(SomeImportantInterface $logic)
    {
    }
}

symfony DI config:

App\SomeImportantInterface:
    class: App\SomeLogic

App\Handler:
    autowire: true

psalm.xml:

    <plugins>
        <pluginClass class="Psalm\SymfonyPsalmPlugin\Plugin">
            <containerXml>var/path/to/xml_dump.xml</containerXml>
        </pluginClass>
    </plugins>

psalm output:

ERROR: UnusedClass - src/App/SomeLogic.php:3:7 - Class App\SomeLogic is never used (see https://psalm.dev/075)
class SomeLogic implements SomeImportantInterface

It would be very helpful to detect this kind of cases and not to mark this classes as Unused.

Incompatible [Declaration of Psalm\SymfonyPsalmPlugin\Twig\TemplateFileAnalyzer::analyze] with Psalm 4

Declaration of Psalm\SymfonyPsalmPlugin\Twig\TemplateFileAnalyzer::analyze has invalid signature override from FileAnalyzer in vimeo/psalm 4.x

PHP Warning:  Declaration of Psalm\SymfonyPsalmPlugin\Twig\TemplateFileAnalyzer::analyze(?Psalm\Context $file_context = NULL, bool $preserve_analyzers = false, ?Psalm\Context $global_context = NULL): void should be compatible with Psalm\Internal\Analyzer\FileAnalyzer::analyze(?Psalm\Context $file_context = NULL, ?Psalm\Context $global_context = NULL): void in /var/www/vendor/psalm/plugin-symfony/src/Twig/TemplateFileAnalyzer.php on line 21
Warning: Declaration of Psalm\SymfonyPsalmPlugin\Twig\TemplateFileAnalyzer::analyze(?Psalm\Context $file_context = NULL, bool $preserve_analyzers = false, ?Psalm\Context $global_context = NULL): void should be compatible with Psalm\Internal\Analyzer\FileAnalyzer::analyze(?Psalm\Context $file_context = NULL, ?Psalm\Context $global_context = NULL): void in /var/www/vendor/psalm/plugin-symfony/src/Twig/TemplateFileAnalyzer.php on line 21

Uncaught InvalidArgumentException: Could not get class storage for self in /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Provider/ClassLikeStorageProvider.php

So this happens when doing $container->get(self::class) I know it's weird code, but it shouldn't crash Psalm regardless. And happens only when this plugin is enabled. Following reproduces error for me, but in your case it would be of course incomplete.

class SettingManager extends CommonManager
{
    public function isUpdatedLarsDatabase(): string
    {
        $larsStatus = $this->container->get(self::class)->get(
            ImportLarsCsvCommand::SETTINGS_LARS_UPDATING_STATUS_KEY,
        );

        return $larsStatus->getValue();
    }
}

Result it

Analyzing /var/www/src/AppBundle/Doctrine/Manager/SettingManager.php
Parsing /var/www/src/AppBundle/Doctrine/Manager/SettingManager.php
Uncaught InvalidArgumentException: Could not get class storage for self in /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Provider/ClassLikeStorageProvider.php:45
Stack trace:
#0 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/AtomicMethodCallAnalyzer.php(161): Psalm\Internal\Provider\ClassLikeStorageProvider->get('self')
#1 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php(173): Psalm\Internal\Analyzer\Statements\Expression\Call\AtomicMethodCallAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\MethodCall), Object(Psalm\Codebase), Object(Psalm\Context), Object(Psalm\Type\Atomic\TNamedObject), Object(Psalm\Type\Atomic\TNamedObject), false, NULL, Object(Psalm\Internal\Analyzer\Statements\Expression\Call\AtomicMethodCallAnalysisResult))
#2 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php(135): Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\MethodCall), Object(Psalm\Context))
#3 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php(245): Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\MethodCall), Object(Psalm\Context))
#4 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php(120): Psalm\Internal\Analyzer\Statements\Expression\AssignmentAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\Variable), Object(PhpParser\Node\Expr\MethodCall), NULL, Object(Psalm\Context), NULL)
#5 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php(751): Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\Assign), Object(Psalm\Context), false, Object(Psalm\Context), true)
#6 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php(541): Psalm\Internal\Analyzer\StatementsAnalyzer->analyze(Array, Object(Psalm\Context), Object(Psalm\Context), true)
#7 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ClassAnalyzer.php(1767): Psalm\Internal\Analyzer\FunctionLikeAnalyzer->analyze(Object(Psalm\Context), Object(Psalm\Internal\Provider\NodeDataProvider), Object(Psalm\Context))
#8 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ClassAnalyzer.php(727): Psalm\Internal\Analyzer\ClassAnalyzer->analyzeClassMethod(Object(PhpParser\Node\Stmt\ClassMethod), Object(Psalm\Storage\ClassLikeStorage), Object(Psalm\Internal\Analyzer\ClassAnalyzer), Object(Psalm\Context), Object(Psalm\Context))
#9 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FileAnalyzer.php(201): Psalm\Internal\Analyzer\ClassAnalyzer->analyze(Object(Psalm\Context), Object(Psalm\Context))
#10 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(357): Psalm\Internal\Analyzer\FileAnalyzer->analyze(NULL)
#11 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(589): Psalm\Internal\Codebase\Analyzer->Psalm\Internal\Codebase\{closure}(0, '/var/www/src/Ap...')
#12 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(267): Psalm\Internal\Codebase\Analyzer->doAnalysis(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 1)
#13 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php(1160): Psalm\Internal\Codebase\Analyzer->analyzeFiles(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 1, false, false)
#14 /var/www/vendor/vimeo/psalm/src/psalm.php(590): Psalm\Internal\Analyzer\ProjectAnalyzer->checkPaths(Array)
#15 /var/www/vendor/vimeo/psalm/psalm(2): require_once('/var/www/vendor...')
#16 {main}
(Psalm 3.11.4@58e1d8e68e5098bf4fbfdfb420c38d563f882549 crashed due to an uncaught Throwable)

False positive RedundantCondition for console option with default value

    protected function configure()
    {
        $this
            ->addOption('foo', null, InputOption::VALUE_OPTIONAL, 'desc', false)
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $foo = $input->getOption('foo');

        if (false !== $foo) {
            // option is set (with or without value)
        }

        // ...
    }

This results in a RedundantCondition "string can never contain false".
But actually the value can be false, it is the default value.
I'm using this to differentiate between "option is not used" (false) and "option is used without value" (null).

Need opinion about stubs for symfony/forms

I made (some) stubs for symfony/forms and need opinion if my approach is correct. Simple use-case (shortened, Route is an entity):

/**
 * @extends AbstractType<Route>
 */
class RouteType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $route = $builder->getData(); // <- psalm knows it is Route|null
        $nrOfAssignedRoutes = $this->getNrOfAssignedAddresses($route);
    }

    private function getNrOfAssignedAddresses(?Route $route): int // no issues
    {
        return 42;  // real code removed for simplicity
    }
}

Controller usage:

    /** 
     * Injected using ArgumentResolver, removed for simplicity. See old https://github.com/strictify/coding-challenge/blob/symfony-4/src/Controller/Product/CreateAction.php#L38
     *
     * @param FormInterface<RouteEntity> $form
     */
    public function create(FormInterface $form): Response
    {
        if ($form->isSubmitted() && $form->isValid() && $data = $form->getData()) { 
            // psalm knows $data is instance of Route (not null)
            
            // persist, flush, redirect
        }

       // render template
    }

Future PHP could be:

    public function create(SubmittedForm<RouteType> $submittedForm): Response
    {
        $data = $submittedForm->getDataOrThrowException(); // returns T
    }

Assigning data type on form level also fixes buildView and finishView methods.


This approach could also detect errors when using this bundle: https://github.com/hitechcoding/strict-form-mapper-bundle/blob/master/docs/accessors.md

$builder->add('type', ChoiceType::class, [
    'get_value' => fn(Route $route) => $route->getType(),  // this can detect error is wrong param is typed
    'update_value' => fn(string $type, Route $route) => $route->setType($type),
]);

or with same bundle for factories:

public function configureOptions(OptionsResolver $resolver): void
{
    $resolver->setDefaults([
        'factory' => fn(string $name, string $type) => new Route($this->security->getAuthCompany(), $name, $type),
    ]);
}

or default empty_data:

public function configureOptions(OptionsResolver $resolver): void
{
    $resolver->setDefaults([
        'empty_data' => fn(FormInterface $form) => new Route($this->security->getAuthCompany(), $form->get('name')->getData(), $form->get('type')->getData()),
    ]);
}

Because of complexity and power of forms, I am asking if you think this is valid approach or if it can be improved somehow. I was thinking about adding $options as second generic parameter so

/**
 * @extends AbstractType<Route, array{some_external_parameter: bool}>
 */
class RouteType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $externalParameter = $options['some_external_parameter']; // <-- psalm would know it is bool value
    }
} 

Crash when analysing classes produced by container call

Copied from vimeo/psalm#3196 (opened by @tjveldhuizen)

I've made a reproducer which makes Psalm crash while analyzing the code.

$ git clone [email protected]:tjveldhuizen/psalm-crash-reproducer.git
$ cd psalm-crash-reproducer/
$ composer install
$ bin/phpunit
$ vendor/bin/psalm

Output with --debug: psalm-crash-reproducer.txt

tjv@PC:~/php-projects/temp/psalm-crash-reproducer$ php -v
PHP 7.2.29-1+ubuntu18.04.1+deb.sury.org+1 (cli) (built: Mar 20 2020 13:54:39) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.2.29-1+ubuntu18.04.1+deb.sury.org+1, Copyright (c) 1999-2018, by Zend Technologies
    with Xdebug v2.9.3, Copyright (c) 2002-2020, by Derick Rethans
tjv@PC:~/php-projects/temp/psalm-crash-reproducer$
tjv@PC:~/php-projects/temp/psalm-crash-reproducer$ vendor/bin/psalm
Scanning files...
Analyzing files...

Uncaught InvalidArgumentException: Could not get class storage for symfony\component\validator\validator\traceablevalidator in /home/tjv/php-projects/temp/psalm-crash-reproducer/vendor/vimeo/psalm/src/Psalm/Internal/Provider/ClassLikeStorageProvider.php:45
Stack trace:
#0 /home/tjv/php-projects/temp/psalm-crash-reproducer/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/ClassLikes.php(548): Psalm\Internal\Provider\ClassLikeStorageProvider->get('symfony\\compone...')
#1 /home/tjv/php-projects/temp/psalm-crash-reproducer/vendor/vimeo/psalm/src/Psalm/Codebase.php(723): Psalm\Internal\Codebase\ClassLikes->classExtends('symfony\\compone...', 'symfony\\compone...')
#2 /home/tjv/php-projects/temp/psalm-crash-reproducer/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/TypeAnalyzer.php(704): Psalm\Codebase->classExtendsOrImplements('symfony\\compone...', 'symfony\\compone...')
#3 /home/tjv/php-projects/temp/psalm-crash-reproducer/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/TypeAnalyzer.php(903): Psalm\Internal\Analyzer\TypeAnalyzer::isObjectContainedByObject(Object(Psalm\Codebase), Object(Psalm\Type\Atomic\TNamedObject), Object(Psalm\Type\Atomic\TNamedObject), false, Object(Psalm\Internal\Analyzer\TypeComparisonResult))
#4 /home/tjv/php-projects/temp/psalm-crash-reproducer/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/TypeAnalyzer.php(147): Psalm\Internal\Analyzer\TypeAnalyzer::isAtomicContainedBy(Object(Psalm\Codebase), Object(Psalm\Type\Atomic\TNamedObject), Object(Psalm\Type\Atomic\TNamedObject), false, true, Object(Psalm\Internal\Analyzer\TypeComparisonResult))
#5 /home/tjv/php-projects/temp/psalm-crash-reproducer/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/PropertyAssignmentAnalyzer.php(855): Psalm\Internal\Analyzer\TypeAnalyzer::isContainedBy(Object(Psalm\Codebase), Object(Psalm\Type\Union), Object(Psalm\Type\Union), true, true, Object(Psalm\Internal\Analyzer\TypeComparisonResult))
#6 /home/tjv/php-projects/temp/psalm-crash-reproducer/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php(730): Psalm\Internal\Analyzer\Statements\Expression\Assignment\PropertyAssignmentAnalyzer::analyzeInstance(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\PropertyFetch), 'validator', Object(PhpParser\Node\Expr\MethodCall), Object(Psalm\Type\Union), Object(Psalm\Context))
#7 /home/tjv/php-projects/temp/psalm-crash-reproducer/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php(120): Psalm\Internal\Analyzer\Statements\Expression\AssignmentAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\PropertyFetch), Object(PhpParser\Node\Expr\MethodCall), Object(Psalm\Type\Union), Object(Psalm\Context), NULL)
#8 /home/tjv/php-projects/temp/psalm-crash-reproducer/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php(736): Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\Assign), Object(Psalm\Context), false, Object(Psalm\Context), true)
#9 /home/tjv/php-projects/temp/psalm-crash-reproducer/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php(541): Psalm\Internal\Analyzer\StatementsAnalyzer->analyze(Array, Object(Psalm\Context), Object(Psalm\Context), true)
#10 /home/tjv/php-projects/temp/psalm-crash-reproducer/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ClassAnalyzer.php(1750): Psalm\Internal\Analyzer\FunctionLikeAnalyzer->analyze(Object(Psalm\Context), Object(Psalm\Internal\Provider\NodeDataProvider), Object(Psalm\Context))
#11 /home/tjv/php-projects/temp/psalm-crash-reproducer/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ClassAnalyzer.php(730): Psalm\Internal\Analyzer\ClassAnalyzer->analyzeClassMethod(Object(PhpParser\Node\Stmt\ClassMethod), Object(Psalm\Storage\ClassLikeStorage), Object(Psalm\Internal\Analyzer\ClassAnalyzer), Object(Psalm\Context), Object(Psalm\Context))
#12 /home/tjv/php-projects/temp/psalm-crash-reproducer/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FileAnalyzer.php(201): Psalm\Internal\Analyzer\ClassAnalyzer->analyze(Object(Psalm\Context), Object(Psalm\Context))
#13 /home/tjv/php-projects/temp/psalm-crash-reproducer/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(357): Psalm\Internal\Analyzer\FileAnalyzer->analyze(NULL)
#14 /home/tjv/php-projects/temp/psalm-crash-reproducer/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(589): Psalm\Internal\Codebase\Analyzer->Psalm\Internal\Codebase\{closure}(0, '/home/tjv/php-p...')
#15 /home/tjv/php-projects/temp/psalm-crash-reproducer/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(267): Psalm\Internal\Codebase\Analyzer->doAnalysis(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 3)
#16 /home/tjv/php-projects/temp/psalm-crash-reproducer/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php(568): Psalm\Internal\Codebase\Analyzer->analyzeFiles(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 3, false, true)
#17 /home/tjv/php-projects/temp/psalm-crash-reproducer/vendor/vimeo/psalm/src/psalm.php(588): Psalm\Internal\Analyzer\ProjectAnalyzer->check('/home/tjv/php-p...', false)
#18 /home/tjv/php-projects/temp/psalm-crash-reproducer/vendor/vimeo/psalm/psalm(2): require_once('/home/tjv/php-p...')
#19 {main}
(Psalm 3.11.2@d470903722cfcbc1cd04744c5491d3e6d13ec3d9 crashed due to an uncaught Throwable)

Notice after update psalm to 3.17.1

PHP Notice: Undefined property: Psalm\Codebase::$taint in /home/vadim/projects/the-real-estate-tours/vendor/psalm/plugin-symfony/src/Twig/AnalyzedTemplatesTainter.php on line 27

Update phpunit version to allow PHP 8

Currently when pulling the repo and trying to install its dependencies it fails because the phpunit version that is used only supports php ^7.1, the earliest phpunit version to support php 8 is phpunit 9.3.0 so I propose to update to at least this version if not updating to the latest 9.5.0 entirely.

#[Required] must supress PropertyNotSetInConstructor

Hello

we need something like this

<?php

declare(strict_types=1);

namespace App\Psalm;

use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassLike;
use Psalm\Codebase;
use Psalm\FileSource;
use Psalm\Plugin\Hook\AfterClassLikeVisitInterface;
use Psalm\Storage\ClassLikeStorage;
use Symfony\Contracts\Service\Attribute\Required;

class RequiredPropertyHandler implements AfterClassLikeVisitInterface
{
    public static function afterClassLikeVisit(
        ClassLike $stmt,
        ClassLikeStorage $storage,
        FileSource $statements_source,
        Codebase $codebase,
        array &$file_replacements = []
    ) {
        if (!$stmt instanceof Class_) {
            return;
        }
        foreach ($storage->properties as $name => $property) {
            foreach ($property->attributes as $attribute) {
                if (Required::class === $attribute->fq_class_name) {
                    $storage->initialized_properties[$name] = true;
                    break;
                }
            }
        }
    }
}

InvalidArgument when use object annotation

Using the code

use Symfony\Component\Messenger\Envelope;

$envelope = new Envelope(new stdClass());

I got the following error:

Argument 1 of Symfony\Component\Messenger\Exception\HandlerFailedException::__construct expects Symfony\Component\Messenger\Envelope<object>, Symfony\Component\Messenger\Envelope<stdClass> provided (see https://psalm.dev/004)

Even change the type of envelope param, doents solve the problem

Reference

InvalidConsoleOptionValue does not allow combining multiple constants

When specifying the mode argument for console-input's, the constants can be combined using the + operator to have multiple different modes active at the same time. This works because the constants are bit-wise integers and the mode is checked for specific constants by checking if their bit is set in the mode integer:

class InputOption
{
    const VALUE_NONE = 1;
    const VALUE_REQUIRED = 2;
    const VALUE_OPTIONAL = 4;
    const VALUE_IS_ARRAY = 8;
   ...
    public function __construct(string $name, $shortcut = null, int $mode = null, string $description = '', $default = null)
    {
   ...
        if (null === $mode) {
            $mode = self::VALUE_NONE;
        } elseif ($mode > 15 || $mode < 1) {
            throw new InvalidArgumentException(sprintf('Option mode "%s" is not valid.', $mode));
        }
   ...
    public function isValueRequired()
    {
        return self::VALUE_REQUIRED === (self::VALUE_REQUIRED & $this->mode);
    }
   ...
    public function isArray()
    {
        return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode);
    }

Yet this plugin does not recognize these combined constants and only allows one constant to be present at a time:

ERROR: InvalidConsoleOptionValue - src/Brille24/ProductionBundle/Command/CompareManufacturerCatalogsCommand.php:63:13 - Use Symfony\Component\Console\Input\InputOption constants (see https://psalm.dev/000)
            InputOption::VALUE_REQUIRED + InputOption::VALUE_IS_ARRAY,

At the moment the only way to deal with these issues is to suppress them using psalm-suppress.

Taint analysis fails with Psalm\Type\Atomic\TString

The following class throws the error: Can not retrieve template name from given expression (Psalm\Type\Atomic\TString)

<?php

namespace App\Templating;

use Twig\Environment;

final class TwigTemplating implements Templating
{
    private Environment $twig;

    public function __construct(Environment $twig)
    {
        $this->twig = $twig;
    }

    /** @param array<array-key, mixed> $data */
    public function render(string $template, array $data): string
    {
        return $this->twig->render($template, $data);
    }
}

Stacktrace:

ncaught Exception: Can not retrieve template name from given expression (Psalm\Type\Atomic\TString)
Stack trace in the forked worker:
#0 /Users/myuser/projects/myapp/vendor/psalm/plugin-symfony/src/Twig/TwigUtils.php(20): Psalm\SymfonyPsalmPlugin\Twig\TwigUtils::resolveStringFromExpression(Object(Psalm\Type\Atomic\TString), Object(Psalm\Internal\Analyzer\StatementsAnalyzer))
#1 /Users/myuser/projects/myapp/vendor/psalm/plugin-symfony/src/Twig/AnalyzedTemplatesTainter.php(39): Psalm\SymfonyPsalmPlugin\Twig\TwigUtils::extractTemplateNameFromExpression(Object(PhpParser\Node\Expr\Variable), Object(Psalm\Internal\Analyzer\StatementsAnalyzer))
#2 /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/Psalm/Internal/EventDispatcher.php(254): Psalm\SymfonyPsalmPlugin\Twig\AnalyzedTemplatesTainter::afterMethodCallAnalysis(Object(PhpParser\Node\Expr\MethodCall), 'Twig\\Environmen...', 'Twig\\Environmen...', 'Twig\\Environmen...', Object(Psalm\Context), Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(Psalm\Codebase), Array, Object(Psalm\Type\Union))
#3 /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php(392): Psalm\Internal\EventDispatcher->dispatchAfterMethodCallAnalysis(Object(Psalm\Plugin\EventHandler\Event\AfterMethodCallAnalysisEvent))
#4 /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php(401): Psalm\Internal\Analyzer\Statements\Expression\Call\Method\ExistingAtomicMethodCallAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\MethodCall), Object(PhpParser\Node\Identifier), Array, Object(Psalm\Codebase), Object(Psalm\Context), Object(Psalm\Type\Atomic\TNamedObject), Object(Psalm\Type\Atomic\TNamedObject), '$this->twig', Object(Psalm\Internal\MethodIdentifier), Object(Psalm\Internal\Analyzer\Statements\Expression\Call\Method\AtomicMethodCallAnalysisResult))
#5 /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php(184): Psalm\Internal\Analyzer\Statements\Expression\Call\Method\AtomicMethodCallAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\MethodCall), Object(Psalm\Codebase), Object(Psalm\Context), Object(Psalm\Type\Atomic\TNamedObject), Object(Psalm\Type\Atomic\TNamedObject), false, '$this->twig', Object(Psalm\Internal\Analyzer\Statements\Expression\Call\Method\AtomicMethodCallAnalysisResult))
#6 /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php(148): Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\MethodCall), Object(Psalm\Context))
#7 /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php(40): Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::handleExpression(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\MethodCall), Object(Psalm\Context), false, NULL, false)
#8 /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php(141): Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\MethodCall), Object(Psalm\Context))
#9 /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php(504): Psalm\Internal\Analyzer\Statements\ReturnAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Stmt\Return_), Object(Psalm\Context))
#10 /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php(172): Psalm\Internal\Analyzer\StatementsAnalyzer::analyzeStatement(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Stmt\Return_), Object(Psalm\Context), Object(Psalm\Context))
#11 /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php(421): Psalm\Internal\Analyzer\StatementsAnalyzer->analyze(Array, Object(Psalm\Context), Object(Psalm\Context), true)
#12 /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ClassAnalyzer.php(1639): Psalm\Internal\Analyzer\FunctionLikeAnalyzer->analyze(Object(Psalm\Context), Object(Psalm\Internal\Provider\NodeDataProvider), Object(Psalm\Context))
#13 /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ClassAnalyzer.php(387): Psalm\Internal\Analyzer\ClassAnalyzer->analyzeClassMethod(Object(PhpParser\Node\Stmt\ClassMethod), Object(Psalm\Storage\ClassLikeStorage), Object(Psalm\Internal\Analyzer\ClassAnalyzer), Object(Psalm\Context), Object(Psalm\Context))
#14 /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FileAnalyzer.php(213): Psalm\Internal\Analyzer\ClassAnalyzer->analyze(Object(Psalm\Context), Object(Psalm\Context))
#15 /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(340): Psalm\Internal\Analyzer\FileAnalyzer->analyze(NULL)
#16 /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/Psalm/Internal/Fork/Pool.php(193): Psalm\Internal\Codebase\Analyzer->Psalm\Internal\Codebase\{closure}(73, '/Users/myuser/p...')
#17 /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(406): Psalm\Internal\Fork\Pool->__construct(Array, Object(Closure), Object(Closure), Object(Closure), Object(Closure))
#18 /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(269): Psalm\Internal\Codebase\Analyzer->doAnalysis(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 3)
#19 /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php(635): Psalm\Internal\Codebase\Analyzer->analyzeFiles(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 3, false, true)
#20 /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/psalm.php(676): Psalm\Internal\Analyzer\ProjectAnalyzer->check('/Users/myuser/p...', false)
#21 /Users/myuser/projects/myapp/vendor/vimeo/psalm/psalm(2): require_once('/Users/myuser/p...')
#22 {main} in /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/Psalm/Internal/Fork/Pool.php:357
Stack trace:
#0 /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/Psalm/Internal/Fork/Pool.php(389): Psalm\Internal\Fork\Pool->readResultsFromChildren()
#1 /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(473): Psalm\Internal\Fork\Pool->wait()
#2 /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(269): Psalm\Internal\Codebase\Analyzer->doAnalysis(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 3)
#3 /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php(635): Psalm\Internal\Codebase\Analyzer->analyzeFiles(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 3, false, true)
#4 /Users/myuser/projects/myapp/vendor/vimeo/psalm/src/psalm.php(676): Psalm\Internal\Analyzer\ProjectAnalyzer->check('/Users/myuser/p...', false)
#5 /Users/myuser/projects/myapp/vendor/vimeo/psalm/psalm(2): require_once('/Users/myuser/p...')
#6 {main}
(Psalm 4.4.1@9fd7a7d885b3a216cff8dec9d8c21a132f275224 crashed due to an uncaught Throwable)

Add support for default command argument values

This plugin doesn't currently seem to handle default argument values in commands. For example, the following will cause Psalm to throw ERROR: PossiblyNullArgument - โ€ฆ - Argument 1 of trim cannot be null, possibly null value provided (see https://psalm.dev/078), even though that argument can never be null, as far as I'm aware.

class FooCommand extends Command
{
    protected function configure(): void
    {
        $this->addArgument('bar', InputArgument::OPTIONAL, 'Example optional argument.', 'defaultvalue');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        trim($input->getArgument('bar'));

        return 0;
    }
}

Reintroduce PHP 7.1 support

Psalm and most other relevant plugins allowed PHP 7.1 and PHP 7.2 again for Psalm V4.
Could you please activate them for the Symfony plugin as well? At least in the changes for 2.0 I see no new language features.

Twig extensions don't work in TemplateFileAnalyzer

It seems like Twig extensions don't work with the basic TemplateFileAnalyzer.

This code uses Symfony extensions and produces this error:

{{ render_inline(controller('my_controller')) }}
Uncaught Twig\Error\SyntaxError: Unknown "controller" function. in /var/www/html/templates/test.html.twig:1
Stack trace:
#0 /var/www/html/vendor/twig/twig/src/ExpressionParser.php(470): Twig\ExpressionParser->getFunctionNodeClass('controller', 8)
#1 /var/www/html/vendor/twig/twig/src/ExpressionParser.php(243): Twig\ExpressionParser->getFunctionNode('controller', 8)
#2 /var/www/html/vendor/twig/twig/src/ExpressionParser.php(183): Twig\ExpressionParser->parsePrimaryExpression()
#3 /var/www/html/vendor/twig/twig/src/ExpressionParser.php(78): Twig\ExpressionParser->getPrimary()
#4 /var/www/html/vendor/twig/twig/src/ExpressionParser.php(621): Twig\ExpressionParser->parseExpression(0, false)
#5 /var/www/html/vendor/twig/twig/src/ExpressionParser.php(469): Twig\ExpressionParser->parseArguments(true)
#6 /var/www/html/vendor/twig/twig/src/ExpressionParser.php(243): Twig\ExpressionParser->getFunctionNode('render_inline', 8)
#7 /var/www/html/vendor/twig/twig/src/ExpressionParser.php(183): Twig\ExpressionParser->parsePrimaryExpression()
#8 /var/www/html/vendor/twig/twig/src/ExpressionParser.php(78): Twig\ExpressionParser->getPrimary()
#9 /var/www/html/vendor/twig/twig/src/Parser.php(166): Twig\ExpressionParser->parseExpression()
#10 /var/www/html/vendor/twig/twig/src/TokenParser/BlockTokenParser.php(47): Twig\Parser->subparse(Array, true)
#11 /var/www/html/vendor/twig/twig/src/Parser.php(209): Twig\TokenParser\BlockTokenParser->parse(Object(Twig\Token))
#12 /var/www/html/vendor/twig/twig/src/Parser.php(122): Twig\Parser->subparse(NULL, false)
#13 /var/www/html/vendor/twig/twig/src/Environment.php(735): Twig\Parser->parse(Object(Twig\TokenStream))
#14 /var/www/html/tests/psalm/TemplateFileAnalyzer.php(80): Twig\Environment->parse(Object(Twig\TokenStream))
#15 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(340): TemplateFileAnalyzer->analyze(NULL)
#16 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(579): Psalm\Internal\Codebase\Analyzer->Psalm\Internal\Codebase\{closure}(318, '/var/www/html/j...')
#17 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(269): Psalm\Internal\Codebase\Analyzer->doAnalysis(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 1)
#18 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php(639): Psalm\Internal\Codebase\Analyzer->analyzeFiles(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 1, false, true)
#19 /var/www/html/vendor/vimeo/psalm/src/psalm.php(676): Psalm\Internal\Analyzer\ProjectAnalyzer->check('/var/www/html/', false)
#20 /var/www/html/vendor/vimeo/psalm/psalm(2): require_once('/var/www/html/v...')
#21 {main}
(Psalm 4.3.1@2feba22a005a18bf31d4c7b9bdb9252c73897476 crashed due to an uncaught Throwable)
  1. Can there be a way to specify extensions or write stubs for them?
  2. Would it be possible to add/stub the Symfony extensions by default?

Using service locators with private services results in false-positives

We have a couple of classes implementing Symfony\Contracts\Service\ServiceSubscriberInterface to allow autowiring of service locators in problems areas where there are issues with circular references. This plugin currently reports ERROR: PrivateService - โ€ฆ - Private service "Foo\Bar" used in container::get() in these scenarios.

Is there a way to annotate our services accordingly, as I understand parsing the return value of ::getSubscribedServices() would be impossible in many scenarios.

Allow add more then one containerXml-File

Currently it is possible add only one containerXml-File, that exists in the cache-path from environment for example "dev", but on build-server with environment "test" is an exception thrown , because the file doesn't exists . So my suggestion to allow more then one containerXml-File and only thrown the exception, if no containerXml-File from all listet files exists.

TemplateFileAnalyzer paths are not configurable

I get the following error when trying to run a taint analysis for the first time. Looks like a path is hard coded to baseDir/templates/ which isn't necessarily true for all projects.

Uncaught Twig\Error\LoaderError: The "templates" directory does not exist ("/var/www/html/templates"). in /var/www/html/vendor/twig/twig/src/Loader/FilesystemLoader.php:106
Stack trace:
#0 /var/www/html/vendor/twig/twig/src/Loader/FilesystemLoader.php(87): Twig\Loader\FilesystemLoader->addPath('templates', '__main__')
#1 /var/www/html/vendor/twig/twig/src/Loader/FilesystemLoader.php(45): Twig\Loader\FilesystemLoader->setPaths(Array)
#2 /var/www/html/vendor/psalm/plugin-symfony/src/Twig/TemplateFileAnalyzer.php(33): Twig\Loader\FilesystemLoader->__construct('templates', '/var/www/html/')
#3 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(340): Psalm\SymfonyPsalmPlugin\Twig\TemplateFileAnalyzer->analyze(NULL)
#4 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(579): Psalm\Internal\Codebase\Analyzer->Psalm\Internal\Codebase\{closure}(318, '/var/www/html/j...')
#5 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(269): Psalm\Internal\Codebase\Analyzer->doAnalysis(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 1)
#6 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php(639): Psalm\Internal\Codebase\Analyzer->analyzeFiles(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 1, false, true)
#7 /var/www/html/vendor/vimeo/psalm/src/psalm.php(676): Psalm\Internal\Analyzer\ProjectAnalyzer->check('/var/www/html/', false)
#8 /var/www/html/vendor/vimeo/psalm/psalm(2): require_once('/var/www/html/v...')
#9 {main}
(Psalm 4.3.1@2feba22a005a18bf31d4c7b9bdb9252c73897476 crashed due to an uncaught Throwable)

Psalm strategy with Voter::voteOnAttribute, NormalizerInterface::normalize, and similar methods.

From the Voter class

    /**
     * Perform a single access check operation on a given attribute, subject and token.
     * It is safe to assume that $attribute and $subject already passed the "supports()" method check.
     *
     * @param string $attribute
     * @param mixed  $subject
     *
     * @return bool
     */
    abstract protected function voteOnAttribute($attribute, $subject, TokenInterface $token);

Subject has a mixed type, but since there is a supports method, it's safe to restrict the $subject type.

With the following method supports

    public function supports($attribute, $subject)
    {
        if (self::CONTRACT_SHOW !== $attribute) {
            return false;
        }

        if (!$subject instanceof Contract) {
            return false;
        }

        return true;
    }

It should be ok to write

    /**
     * @param string         $attribute
     * @param Contract       $subject
     * @param TokenInterface $token
     *
     * @return bool
     */
    public function voteOnAttribute($attribute, $subject, TokenInterface $token)

Without getting a MoreSpecificImplementedParamType error.

Same for others classes with a supports method.

Is there a way to avoid the error ? Does a stub or something like this could be added to this plugin ? Or the only solution is to use psalm-suppress ?

Add support for test container

It would be nice to support the test container (Symfony\Bundle\FrameworkBundle\Test\TestContainer), which allows to fetch private services for testing.

A use case would be the Symfony\Bundle\FrameworkBundle\Test\KernelTestCase::$container. If you currently use this container in the tests, Psalm will report a PrivateService issue and will not know the correct type (only object|null).

I'm not sure how to do this properly. Maybe the ContainerHandler could always set the type, even if the service is not public and then suppress the PrivateService manualy for the tests directory?

False positive "UninitializedProperty" with Constraint

The following code

use App\Entity\Foo;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\InvalidArgumentException;

class MyConstraint extends Constraint
{
   /**
     * @var Foo
     */
    public $foo;

    /**
     * @var string
     */
    public $message = 'message';

    /**
     * @param mixed $options
     *
     * @return void
     */
    public function __construct($options = null)
    {
        parent::__construct($options);

        if (!$this->foo instanceof Foo) {
            throw new InvalidArgumentException('The "foo" parameter value is not valid.');
        }
    }

    /**
     * @return string
     */
    public function getDefaultOption()
    {
        return 'foo';
    }

    /**
     * @return array
     */
    public function getRequiredOptions()
    {
        return ['foo'];
    }
}

Is returning an error:

UninitializedProperty - Cannot use uninitialized property $this->foo

But the property is initialized in the parent::construct() since it does

$this->$option = $value

Is it something that can be improved in the symfony plugin (and how ?) or should psalm be more careful with the parent constructor ? @seferov @muglug

Fatal Error in my CI

Fatal error: Declaration of Psalm\SymfonyPsalmPlugin\Handler\HeaderBagHandler::getMethodReturnType(Psalm\StatementsSource $source, string $fq_classlike_name, string $method_name_lowercase, array $call_args, Psalm\Context $context, Psalm\CodeLocation $code_location, ?array $template_type_parameters = NULL, ?string $called_fq_classlike_name = NULL, ?string $called_method_name_lowercase = NULL) must be compatible with Psalm\Plugin\Hook\MethodReturnTypeProviderInterface::getMethodReturnType(Psalm\StatementsSource $source, string $fq_classlike_name, string $method_name_lowercase, array $call_args, Psalm\Context $context, Psalm\CodeLocation $code_location, ?array $template_type_parameters = NULL, ?string $called_fq_classlike_name = NULL, ?string $called_method_name_lowercase = NULL): ?Psalm\Type\Union in /github/workspace/vendor/psalm/plugin-symfony/src/Handler/HeaderBagHandler.php on line 27

The difference seems to be the return type being stricter in the interface.

psalm/psalm-plugin-symfony v1.4.3
CI via psalm/[email protected]

Crash v2.2 in Alpine Linux

Hi. I have a problem when upgrading to version 2.2 on Alpine Linux in Docker images
php-cli-8.0-alpine, problem next :

Uncaught Error: Undefined constant "GLOB_BRACE" in /app/vendor/psalm/plugin-symfony/src/Plugin.php:36
Stack trace:
#0 /app/vendor/psalm/plugin-symfony/src/Plugin.php(89): Psalm\SymfonyPsalmPlugin\Plugin->getCommonStubs()
#1 /app/vendor/vimeo/psalm/src/Psalm/Config.php(1146): Psalm\SymfonyPsalmPlugin\Plugin->__invoke(Object(Psalm\PluginRegistrationSocket), Object(SimpleXMLElement))
#2 /app/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php(585): Psalm\Config->initializePlugins(Object(Psalm\Internal\Analyzer\ProjectAnalyzer))
#3 /app/vendor/vimeo/psalm/src/psalm.php(683): Psalm\Internal\Analyzer\ProjectAnalyzer->check('/app/', true)
#4 /app/vendor/vimeo/psalm/psalm(2): require_once('/app/vendor/vim...')
#5 {main}

Next Psalm\Exception\ConfigException: Failed to load plugin Psalm\SymfonyPsalmPlugin\Plugin in /app/vendor/vimeo/psalm/src/Psalm/Config.php:1148
Stack trace:
#0 /app/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php(585): Psalm\Config->initializePlugins(Object(Psalm\Internal\Analyzer\ProjectAnalyzer))
#1 /app/vendor/vimeo/psalm/src/psalm.php(683): Psalm\Internal\Analyzer\ProjectAnalyzer->check('/app/', true)
#2 /app/vendor/vimeo/psalm/psalm(2): require_once('/app/vendor/vim...')
#3 {main}
(Psalm 4.6.2@bca09d74adc704c4eaee36a3c3e9d379e290fc3b crashed due to an uncaught Throwable)

I think, this happens because this const doesn't isset in this linux dist. I find similar problem in Symfony 4 symfony/symfony@d7a0679 .

Help me, please.

Console options declared with `--` prefix

Usually options are declared without the -- prefix:

$this->addOption('boolean', null, InputOption::VALUE_NONE);

But symfony allows to add the prefix:

$this->addOption('--boolean', null, InputOption::VALUE_NONE);

But this plugin does not resolve the type if the prefix is added.

Services are wrongly handled as public:true by default

The Debug Container reading classes are treating the public attribute as true if not available. In the latest Symfony 5.X releases it should never be false anymore (can't test it right now) after the private attribute had been completely removed.
https://github.com/symfony/dependency-injection/blob/master/Dumper/XmlDumper.php#L111

Here is the concerning code of this plugin
Symfony 3.3 was the last version producing a XML where missing public attributes had a value of true.

If a detection of the correct value is complicated could we configure it?

Template variables with an underscore lead to errors in taint analysis

The check in AnalyzedTemplatesTainter for Twig variables just checks for ([a-zA-Z]+) while Twig variables could contain other characters (like _). Having a variable named my_result leads to:

Uncaught Exception: Argument 2 passed to Psalm\SymfonyPsalmPlugin\Twig\TemplateFileAnalyzer::getTaintNodeForTwigNamedVariable() must be of the type string, null given, called in /Users/myuser/projects/myapp/vendor/psalm/plugin-symfony/src/Twig/AnalyzedTemplatesTainter.php on line 45

More stubs?

Hey team, what's the aim for this project? I see at the moment it's one stub there.

I've built a small collection (just 4, but still) of stubs here https://github.com/zerkms/symfony-psalm-plugin/tree/master/stubs

I'd rather contribute to other project than maintain a half-dead mine.

Hence a question: would my stubs be welcome if I PR them here?

And if "yes" - have you already thought how to version stubs from different symfony versions?

Twig taint analysis fatal errors on concatenation

Given this code:

return $this->twig->render('FooBundle:Bar:' . $var->getType() . '.html.twig', $params);

This fatal error occurs:

Uncaught RuntimeException: Can not retrieve template name from given expression (PhpParser\Node\Expr\BinaryOp\Concat) in /var/www/html/vendor/psalm/plugin-symfony/src/Twig/TwigUtils.php:27
Stack trace:
#0 /var/www/html/vendor/psalm/plugin-symfony/src/Twig/CachedTemplatesTainter.php(61): Psalm\SymfonyPsalmPlugin\Twig\TwigUtils::extractTemplateNameFromExpression(Object(PhpParser\Node\Expr\BinaryOp\Concat), Object(Psalm\Internal\Analyzer\StatementsAnalyzer))
#1 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php(105): Psalm\SymfonyPsalmPlugin\Twig\CachedTemplatesTainter::getMethodReturnType(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), 'Twig\\Environmen...', 'render', Array, Object(Psalm\Context), Object(Psalm\CodeLocation), NULL, 'Twig_Environmen...', 'render')
#2 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php(70): Psalm\Internal\Provider\MethodReturnTypeProvider->getReturnType(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), 'Twig\\Environmen...', 'render', Array, Object(Psalm\Context), Object(Psalm\CodeLocation), NULL, 'Twig_Environmen...', 'render')
#3 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php(206): Psalm\Internal\Analyzer\Statements\Expression\Call\Method\MethodCallReturnTypeFetcher::fetch(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(Psalm\Codebase), Object(PhpParser\Node\Expr\MethodCall), Object(Psalm\Context), Object(Psalm\Internal\MethodIdentifier), Object(Psalm\Internal\MethodIdentifier), Object(Psalm\Internal\MethodIdentifier), 'Twig_Environmen...', Object(Psalm\Type\Atomic\TNamedObject), Object(Psalm\Type\Atomic\TNamedObject), Array, Object(Psalm\Internal\Analyzer\Statements\Expression\Call\Method\AtomicMethodCallAnalysisResult), Object(Psalm\Internal\Type\TemplateResult))
#4 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php(388): Psalm\Internal\Analyzer\Statements\Expression\Call\Method\ExistingAtomicMethodCallAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\MethodCall), Object(PhpParser\Node\Identifier), Array, Object(Psalm\Codebase), Object(Psalm\Context), Object(Psalm\Type\Atomic\TNamedObject), Object(Psalm\Type\Atomic\TNamedObject), '$this->twig', Object(Psalm\Internal\MethodIdentifier), Object(Psalm\Internal\Analyzer\Statements\Expression\Call\Method\AtomicMethodCallAnalysisResult))
#5 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php(183): Psalm\Internal\Analyzer\Statements\Expression\Call\Method\AtomicMethodCallAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\MethodCall), Object(Psalm\Codebase), Object(Psalm\Context), Object(Psalm\Type\Atomic\TNamedObject), Object(Psalm\Type\Atomic\TNamedObject), false, '$this->twig', Object(Psalm\Internal\Analyzer\Statements\Expression\Call\Method\AtomicMethodCallAnalysisResult))
#6 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php(151): Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\MethodCall), Object(Psalm\Context))
#7 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php(39): Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::handleExpression(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\MethodCall), Object(Psalm\Context), false, NULL, false)
#8 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php(141): Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\MethodCall), Object(Psalm\Context))
#9 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php(496): Psalm\Internal\Analyzer\Statements\ReturnAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Stmt\Return_), Object(Psalm\Context))
#10 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php(171): Psalm\Internal\Analyzer\StatementsAnalyzer::analyzeStatement(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Stmt\Return_), Object(Psalm\Context), Object(Psalm\Context))
#11 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php(654): Psalm\Internal\Analyzer\StatementsAnalyzer->analyze(Array, Object(Psalm\Context), Object(Psalm\Context), true)
#12 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ClassAnalyzer.php(1978): Psalm\Internal\Analyzer\FunctionLikeAnalyzer->analyze(Object(Psalm\Context), Object(Psalm\Internal\Provider\NodeDataProvider), Object(Psalm\Context))
#13 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ClassAnalyzer.php(746): Psalm\Internal\Analyzer\ClassAnalyzer->analyzeClassMethod(Object(PhpParser\Node\Stmt\ClassMethod), Object(Psalm\Storage\ClassLikeStorage), Object(Psalm\Internal\Analyzer\ClassAnalyzer), Object(Psalm\Context), Object(Psalm\Context))
#14 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FileAnalyzer.php(211): Psalm\Internal\Analyzer\ClassAnalyzer->analyze(Object(Psalm\Context), Object(Psalm\Context))
#15 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(340): Psalm\Internal\Analyzer\FileAnalyzer->analyze(NULL)
#16 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(579): Psalm\Internal\Codebase\Analyzer->Psalm\Internal\Codebase\{closure}(825, '/var/www/html/j...')
#17 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(269): Psalm\Internal\Codebase\Analyzer->doAnalysis(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 1)
#18 /var/www/html/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php(639): Psalm\Internal\Codebase\Analyzer->analyzeFiles(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 1, false, true)
#19 /var/www/html/vendor/vimeo/psalm/src/psalm.php(676): Psalm\Internal\Analyzer\ProjectAnalyzer->check('/var/www/html/', false)
#20 /var/www/html/vendor/vimeo/psalm/psalm(2): require_once('/var/www/html/v...')
#21 {main}
(Psalm 4.3.1@2feba22a005a18bf31d4c7b9bdb9252c73897476 crashed due to an uncaught Throwable)

use ContainerAwareTrait must supress PropertyNotSetInConstructor

hello
we need something like this

<?php

declare(strict_types=1);

namespace App\Psalm;

use PhpParser\Node\Stmt\ClassLike;
use Psalm\Codebase;
use Psalm\FileSource;
use Psalm\Plugin\Hook\AfterClassLikeVisitInterface;
use Psalm\Storage\ClassLikeStorage;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;

class ContainerAwareTraitHandler implements AfterClassLikeVisitInterface
{
    public static function afterClassLikeVisit(
        ClassLike $stmt,
        ClassLikeStorage $storage,
        FileSource $statements_source,
        Codebase $codebase,
        array &$file_replacements = []
    ) {
        if (ContainerAwareTrait::class === $storage->name) {
            $storage->initialized_properties['container'] = true;
        }
    }
}

Twig taint analysis fails when PHP file referencing twig file is analyzed first

Plugin keeps crashing on

return $this->container->get('twig')->render('AppBundle:DataProvider/GraduateJobs:job.xml.twig', [...]);

with error

Uncaught RuntimeException: The template AppBundle:DataProvider/GraduateJobs:job.xml.twig was not found. in /var/www/vendor/psalm/plugin-symfony/src/Twig/CachedTemplatesMapping.php:67
Stack trace:
#0 /var/www/vendor/psalm/plugin-symfony/src/Twig/CachedTemplatesTainter.php(58): Psalm\SymfonyPsalmPlugin\Twig\CachedTemplatesMapping::getCacheClassName('AppBundle:DataP...')
#1 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php(105): Psalm\SymfonyPsalmPlugin\Twig\CachedTemplatesTainter::getMethodReturnType(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), 'Twig\\Environmen...', 'render', Array, Object(Psalm\Context), Object(Psalm\CodeLocation), NULL, NULL, NULL)
#2 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php(53): Psalm\Internal\Provider\MethodReturnTypeProvider->getReturnType(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), 'Twig\\Environmen...', 'render', Array, Object(Psalm\Context), Object(Psalm\CodeLocation), NULL)
#3 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php(680): Psalm\Internal\Analyzer\Statements\Expression\Call\Method\MethodCallReturnTypeFetcher::fetch(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(Psalm\Codebase), Object(PhpParser\Node\Expr\MethodCall), Object(Psalm\Context), Object(Psalm\Internal\MethodIdentifier), Object(Psalm\Internal\MethodIdentifier), Object(Psalm\Internal\MethodIdentifier), 'Twig\\Environmen...', Object(Psalm\Type\Atomic\TNamedObject), Object(Psalm\Type\Atomic\TNamedObject), Array, Object(Psalm\Internal\Analyzer\Statements\Expression\Call\Method\AtomicMethodCallAnalysisResult), Object(Psalm\Internal\Type\TemplateResult))
#4 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php(168): Psalm\Internal\Analyzer\Statements\Expression\Call\Method\AtomicMethodCallAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\MethodCall), Object(Psalm\Codebase), Object(Psalm\Context), Object(Psalm\Type\Atomic\TNamedObject), Object(Psalm\Type\Atomic\TNamedObject), false, NULL, Object(Psalm\Internal\Analyzer\Statements\Expression\Call\Method\AtomicMethodCallAnalysisResult))
#5 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php(144): Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\MethodCall), Object(Psalm\Context))
#6 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php(39): Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::handleExpression(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\MethodCall), Object(Psalm\Context), false, NULL, false)
#7 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php(135): Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\MethodCall), Object(Psalm\Context))
#8 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php(457): Psalm\Internal\Analyzer\Statements\ReturnAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Stmt\Return_), Object(Psalm\Context))
#9 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php(164): Psalm\Internal\Analyzer\StatementsAnalyzer::analyzeStatement(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Stmt\Return_), Object(Psalm\Context), Object(Psalm\Context))
#10 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php(591): Psalm\Internal\Analyzer\StatementsAnalyzer->analyze(Array, Object(Psalm\Context), Object(Psalm\Context), true)
#11 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ClassAnalyzer.php(1897): Psalm\Internal\Analyzer\FunctionLikeAnalyzer->analyze(Object(Psalm\Context), Object(Psalm\Internal\Provider\NodeDataProvider), Object(Psalm\Context))
#12 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ClassAnalyzer.php(734): Psalm\Internal\Analyzer\ClassAnalyzer->analyzeClassMethod(Object(PhpParser\Node\Stmt\ClassMethod), Object(Psalm\Storage\ClassLikeStorage), Object(Psalm\Internal\Analyzer\ClassAnalyzer), Object(Psalm\Context), Object(Psalm\Context))
#13 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FileAnalyzer.php(215): Psalm\Internal\Analyzer\ClassAnalyzer->analyze(Object(Psalm\Context), Object(Psalm\Context))
#14 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(336): Psalm\Internal\Analyzer\FileAnalyzer->analyze(NULL)
#15 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(576): Psalm\Internal\Codebase\Analyzer->Psalm\Internal\Codebase\{closure}(227, '/var/www/src/Ap...')
#16 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(268): Psalm\Internal\Codebase\Analyzer->doAnalysis(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 1)
#17 /var/www/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php(639): Psalm\Internal\Codebase\Analyzer->analyzeFiles(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 1, false, true)
#18 /var/www/vendor/vimeo/psalm/src/psalm.php(677): Psalm\Internal\Analyzer\ProjectAnalyzer->check('/var/www/', false)
#19 /var/www/vendor/vimeo/psalm/psalm(2): require_once('/var/www/vendor...')
#20 {main}
(Psalm 3.16@d03e5ef057d6adc656c0ff7e166c50b73b4f48f3 crashed due to an uncaught Throwable)

even though there is such cache file present in var/cache/dev/twig/6c/6c3e07fca810aa921fb8bc653998ccb1b777f9ed15f04263962af77578011498.php:

<?php

use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Extension\SandboxExtension;
use Twig\Markup;
use Twig\Sandbox\SecurityError;
use Twig\Sandbox\SecurityNotAllowedTagError;
use Twig\Sandbox\SecurityNotAllowedFilterError;
use Twig\Sandbox\SecurityNotAllowedFunctionError;
use Twig\Source;
use Twig\Template;

/* AppBundle:DataProvider/GraduateJobs:job.xml.twig */
class __TwigTemplate_943d73236d880c64fb3bff57201b2a5745c3c2ea7949e6cdfc27cd8cd2a37f48 extends \Twig\Template
{
    private $source;
    private $macros = [];

    public function __construct(Environment $env)
    {
        parent::__construct($env);

        $this->source = $this->getSourceContext();

        $this->parent = false;

        $this->blocks = [
        ];
    }

    protected function doDisplay(array $context, array $blocks = [])
    {
        $macros = $this->macros;
        $__internal_085b0142806202599c7fe3b329164a92397d8978207a37e79d70b8c52599e33e = $this->extensions["Symfony\\Bundle\\WebProfilerBundle\\Twig\\WebProfilerExtension"];
        $__internal_085b0142806202599c7fe3b329164a92397d8978207a37e79d70b8c52599e33e->enter($__internal_085b0142806202599c7fe3b329164a92397d8978207a37e79d70b8c52599e33e_prof = new \Twig\Profiler\Profile($this->getTemplateName(), "template", "AppBundle:DataProvider/GraduateJobs:job.xml.twig"));

        $__internal_319393461309892924ff6e74d6d6e64287df64b63545b994e100d4ab223aed02 = $this->extensions["Symfony\\Bridge\\Twig\\Extension\\ProfilerExtension"];
        $__internal_319393461309892924ff6e74d6d6e64287df64b63545b994e100d4ab223aed02->enter($__internal_319393461309892924ff6e74d6d6e64287df64b63545b994e100d4ab223aed02_prof = new \Twig\Profiler\Profile($this->getTemplateName(), "template", "AppBundle:DataProvider/GraduateJobs:job.xml.twig"));

        // line 1
        echo "<?xml version = '1.0' encoding = 'utf-8' ?>
<!DOCTYPE job PUBLIC 'gj-job-dtd' 'http://www.graduate-jobs.com/xml/job.dtd'>
<job>
    <status>";
        // line 4
        echo twig_escape_filter($this->env, (isset($context["workMode"]) || array_key_exists("workMode", $context) ? $context["workMode"] : (function () { throw new RuntimeError('Variable "workMode" does not exist.', 4, $this->source); })()), "html", null, true);
        echo "</status>
    <accountName>";
        // line 5
        echo twig_escape_filter($this->env, (isset($context["accountName"]) || array_key_exists("accountName", $context) ? $context["accountName"] : (function () { throw new RuntimeError('Variable "accountName" does not exist.', 5, $this->source); })()), "html", null, true);
        echo "</accountName>
    <password>";
        // line 6
        echo twig_escape_filter($this->env, (isset($context["password"]) || array_key_exists("password", $context) ? $context["password"] : (function () { throw new RuntimeError('Variable "password" does not exist.', 6, $this->source); })()), "html", null, true);
        echo "</password>
    <title>";
        // line 7
        echo twig_escape_filter($this->env, (isset($context["title"]) || array_key_exists("title", $context) ? $context["title"] : (function () { throw new RuntimeError('Variable "title" does not exist.', 7, $this->source); })()), "html", null, true);
        echo "</title>
    <yourRef>";
        // line 8
        echo twig_escape_filter($this->env, (isset($context["yourRef"]) || array_key_exists("yourRef", $context) ? $context["yourRef"] : (function () { throw new RuntimeError('Variable "yourRef" does not exist.', 8, $this->source); })()), "html", null, true);
        echo "</yourRef>
    <keyword>";
        // line 9
        echo twig_escape_filter($this->env, (isset($context["keyword"]) || array_key_exists("keyword", $context) ? $context["keyword"] : (function () { throw new RuntimeError('Variable "keyword" does not exist.', 9, $this->source); })()), "html", null, true);
        echo "</keyword>
    <detail>";
        // line 10
        echo twig_escape_filter($this->env, (isset($context["detail"]) || array_key_exists("detail", $context) ? $context["detail"] : (function () { throw new RuntimeError('Variable "detail" does not exist.', 10, $this->source); })()), "html", null, true);
        echo "</detail>
    ";
        // line 11
        $context['_parent'] = $context;
        $context['_seq'] = twig_ensure_traversable((isset($context["locations"]) || array_key_exists("locations", $context) ? $context["locations"] : (function () { throw new RuntimeError('Variable "locations" does not exist.', 11, $this->source); })()));
        foreach ($context['_seq'] as $context["_key"] => $context["location"]) {
            // line 12
            echo "        <location>";
            echo twig_escape_filter($this->env, $context["location"], "html", null, true);
            echo "</location>
    ";
        }
        $_parent = $context['_parent'];
        unset($context['_seq'], $context['_iterated'], $context['_key'], $context['location'], $context['_parent'], $context['loop']);
        $context = array_intersect_key($context, $_parent) + $_parent;
        // line 14
        echo "    ";
        if (((isset($context["postcodes"]) || array_key_exists("postcodes", $context) ? $context["postcodes"] : (function () { throw new RuntimeError('Variable "postcodes" does not exist.', 14, $this->source); })()) != "")) {
            // line 15
            echo "        <postcodes>";
            echo twig_escape_filter($this->env, (isset($context["postcodes"]) || array_key_exists("postcodes", $context) ? $context["postcodes"] : (function () { throw new RuntimeError('Variable "postcodes" does not exist.', 15, $this->source); })()), "html", null, true);
            echo "</postcodes>
    ";
        }
        // line 17
        echo "    <salary>";
        echo twig_escape_filter($this->env, (isset($context["salary"]) || array_key_exists("salary", $context) ? $context["salary"] : (function () { throw new RuntimeError('Variable "salary" does not exist.', 17, $this->source); })()), "html", null, true);
        echo "</salary>
    <displayEndDate>";
        // line 18
        echo twig_escape_filter($this->env, (isset($context["displayEndDate"]) || array_key_exists("displayEndDate", $context) ? $context["displayEndDate"] : (function () { throw new RuntimeError('Variable "displayEndDate" does not exist.', 18, $this->source); })()), "html", null, true);
        echo "</displayEndDate>
    ";
        // line 19
        $context['_parent'] = $context;
        $context['_seq'] = twig_ensure_traversable((isset($context["sectors"]) || array_key_exists("sectors", $context) ? $context["sectors"] : (function () { throw new RuntimeError('Variable "sectors" does not exist.', 19, $this->source); })()));
        foreach ($context['_seq'] as $context["_key"] => $context["sector"]) {
            // line 20
            echo "        <industrySector>";
            echo twig_escape_filter($this->env, $context["sector"], "html", null, true);
            echo "</industrySector>
    ";
        }
        $_parent = $context['_parent'];
        unset($context['_seq'], $context['_iterated'], $context['_key'], $context['sector'], $context['_parent'], $context['loop']);
        $context = array_intersect_key($context, $_parent) + $_parent;
        // line 22
        echo "    <applyTo>";
        echo twig_escape_filter($this->env, (isset($context["applyTo"]) || array_key_exists("applyTo", $context) ? $context["applyTo"] : (function () { throw new RuntimeError('Variable "applyTo" does not exist.', 22, $this->source); })()), "html", null, true);
        echo "</applyTo>
    ";
        // line 23
        $context['_parent'] = $context;
        $context['_seq'] = twig_ensure_traversable((isset($context["sites"]) || array_key_exists("sites", $context) ? $context["sites"] : (function () { throw new RuntimeError('Variable "sites" does not exist.', 23, $this->source); })()));
        foreach ($context['_seq'] as $context["_key"] => $context["site"]) {
            // line 24
            echo "        <site>";
            echo twig_escape_filter($this->env, $context["site"], "html", null, true);
            echo "</site>
    ";
        }
        $_parent = $context['_parent'];
        unset($context['_seq'], $context['_iterated'], $context['_key'], $context['site'], $context['_parent'], $context['loop']);
        $context = array_intersect_key($context, $_parent) + $_parent;
        // line 26
        echo "    <gradeRequired>";
        echo twig_escape_filter($this->env, (isset($context["gradeRequired"]) || array_key_exists("gradeRequired", $context) ? $context["gradeRequired"] : (function () { throw new RuntimeError('Variable "gradeRequired" does not exist.', 26, $this->source); })()), "html", null, true);
        echo "</gradeRequired>
</job>
";
        
        $__internal_085b0142806202599c7fe3b329164a92397d8978207a37e79d70b8c52599e33e->leave($__internal_085b0142806202599c7fe3b329164a92397d8978207a37e79d70b8c52599e33e_prof);

        
        $__internal_319393461309892924ff6e74d6d6e64287df64b63545b994e100d4ab223aed02->leave($__internal_319393461309892924ff6e74d6d6e64287df64b63545b994e100d4ab223aed02_prof);

    }

    public function getTemplateName()
    {
        return "AppBundle:DataProvider/GraduateJobs:job.xml.twig";
    }

    public function isTraitable()
    {
        return false;
    }

    public function getDebugInfo()
    {
        return array (  138 => 26,  129 => 24,  125 => 23,  120 => 22,  111 => 20,  107 => 19,  103 => 18,  98 => 17,  92 => 15,  89 => 14,  80 => 12,  76 => 11,  72 => 10,  68 => 9,  64 => 8,  60 => 7,  56 => 6,  52 => 5,  48 => 4,  43 => 1,);
    }

    public function getSourceContext()
    {
        return new Source("<?xml version = '1.0' encoding = 'utf-8' ?>
<!DOCTYPE job PUBLIC 'gj-job-dtd' 'http://www.graduate-jobs.com/xml/job.dtd'>
<job>
    <status>{{ workMode }}</status>
    <accountName>{{ accountName }}</accountName>
    <password>{{ password }}</password>
    <title>{{ title }}</title>
    <yourRef>{{ yourRef }}</yourRef>
    <keyword>{{ keyword }}</keyword>
    <detail>{{ detail }}</detail>
    {% for location in locations %}
        <location>{{ location }}</location>
    {% endfor %}
    {%  if postcodes != '' %}
        <postcodes>{{ postcodes }}</postcodes>
    {% endif %}
    <salary>{{ salary }}</salary>
    <displayEndDate>{{ displayEndDate }}</displayEndDate>
    {% for sector in sectors %}
        <industrySector>{{ sector }}</industrySector>
    {% endfor %}
    <applyTo>{{ applyTo }}</applyTo>
    {% for site in sites %}
        <site>{{ site }}</site>
    {% endfor %}
    <gradeRequired>{{ gradeRequired }}</gradeRequired>
</job>
", "AppBundle:DataProvider/GraduateJobs:job.xml.twig", "/var/www/src/AppBundle/Resources/views/DataProvider/GraduateJobs/job.xml.twig");
    }
}

Happy to assist with debugging the issue, but right now I am out of ideas, as I don't know how is this part designed to work.

Here's relevant part of config:

     <plugins>
        <pluginClass class="Psalm\SymfonyPsalmPlugin\Plugin">
            <containerXml>var/cache/dev/srcKernelDevDebugContainer.xml</containerXml>
            <twigCachePath>var/cache/dev/twig</twigCachePath>
        </pluginClass>
        <pluginClass class="Weirdan\DoctrinePsalmPlugin\Plugin"/>
    </plugins>

    <fileExtensions>
        <extension name=".php" />
        <extension name=".twig" checker="./vendor/psalm/plugin-symfony/src/Twig/TemplateFileAnalyzer.php"/>
    </fileExtensions>

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.