Skip to content

checkUninitializedProperties does not consider @required #346

Open
@raalderink

Description

@raalderink
Contributor

When enabling PHPStan's checkUninitializedProperties, using @required to autowire a public property, or through a public method will still mark the property as uninitialized, although Symfony's autowiring will make sure it isn't.

This is on;
phpstan/phpstan 1.10.13
phpstan/phpstan-symfony 1.3.1

Activity

ondrejmirtes

ondrejmirtes commented on Apr 18, 2023

@ondrejmirtes
Member
raalderink

raalderink commented on Apr 18, 2023

@raalderink
ContributorAuthor

Okay, cool. This helps implement the case for public properties with @required or #[Required]. There is another strategy to autowire: through public methods with this annotation or attribute.

I've found \PHPStan\Node\ClassPropertiesNode, which calls \PHPStan\Rules\Properties\ReadWritePropertiesExtension::isInitialized. ClassPropertiesNode has all information needed to implement this, in getPropertyUsages. The PropertyWrite information would allow access to all setting methods, which could be checked for the annotation/attribute. This information, nor the Node/Scope, are made available to the extension, meaning that there is no way to access the setting methods.

How should I approach this?

ondrejmirtes

ondrejmirtes commented on Apr 18, 2023

@ondrejmirtes
Member

Please show a piece of code you’re talking about - both analysed and the extension you’re writing.

raalderink

raalderink commented on Apr 18, 2023

@raalderink
ContributorAuthor

This is an example piece of code to analyse;

<?php

class Test
{
    /** @required */
    public string $one;

    protected int $two;

    /**
     * @required
     */
    public function setTwo(int $value): void
    {
        $this->two = $value * 2;
    }
}

This is the extension thus far;

<?php

declare(strict_types=1);

namespace Infrastructure\PhpStan;

use PHPStan\Node\ClassStatementsGatherer;
use PHPStan\Reflection\Php\PhpPropertyReflection;
use PHPStan\Reflection\PropertyReflection;
use PHPStan\Rules\Properties\ReadWritePropertiesExtension;
use PHPStan\Type\FileTypeMapper;
use Symfony\Contracts\Service\Attribute\Required;

class SymfonyRequiredInitialization implements ReadWritePropertiesExtension
{
    private FileTypeMapper $fileTypeMapper;

    public function __construct(FileTypeMapper $fileTypeMapper)
    {
        $this->fileTypeMapper = $fileTypeMapper;
    }

    public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool
    {
        return false;
    }

    public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool
    {
        return false;
    }

    public function isInitialized(PropertyReflection $property, string $propertyName): bool
    {
        // If the property is public, check for @required on the property itself
        if ($property->isPublic()) {
            if ($property->getDocComment() !== null) {
                $phpDoc = $this->fileTypeMapper->getResolvedPhpDoc(null, null, null, null, $property->getDocComment());

                foreach ($phpDoc->getPhpDocNodes() as $node) {
                    // @required annotation is available, meaning this property is always initialized
                    if (count($node->getTagsByName('@required')) > 0) {
                        return true;
                    }
                }
            }

            // Check for the attribute version
            if ($property instanceof PhpPropertyReflection && count($property->getNativeReflection()->getAttributes(Required::class)) > 0) {
                return true;
            }
        } else {
            // 1. Get property writes of $property
            // 2. Collect annotations or attributes of the function that writes to $property
            // 3. Check if any annotation or attribute sets the @required or #[Required]
            
            // This will not work because we do not have a node or scope
            // $classStatementsGatherer = new ClassStatementsGatherer($property->getDeclaringClass(), fn () => null);
        }

        return false;
    }
}

The top part, if ($property->isPublic()) {, is functional (although I'm sure there's ways to improve it). The else part is where the issue lies. The $one in the analysed code will pass correctly, whereas $two will not, but should be as setTwo will make sure it is initialized.

ondrejmirtes

ondrejmirtes commented on Apr 18, 2023

@ondrejmirtes
Member

Yeah, this isn't great. I worry there would be performance implications by repeating the work of ClassStatementsGatherer there.

But there's something more promising to do - there's a setting additionalConstructors that 's used for example for PHPUnit's TestCase::setUp().

This setting is currently statically defined in the config, but we could create a new extension that would decide about "additional constructors" programatically.

Here's the relevant code: https://github.com/phpstan/phpstan-src/blob/1.11.x/src/Reflection/ConstructorsHelper.php

This is very similar thing we previously did for %stubFiles%: phpstan/phpstan-src@2ba9332

ondrejmirtes

ondrejmirtes commented on Apr 19, 2023

@ondrejmirtes
Member
added a commit that references this issue on Apr 20, 2023
added a commit that references this issue on Apr 21, 2023
added a commit that references this issue on Apr 21, 2023
raalderink

raalderink commented on Apr 21, 2023

@raalderink
ContributorAuthor

I've created a new PR for this package now, which should cover this. However, I'm running into a couple things I'm not quite understanding how to solve at this moment. As you can see in the builds, I'm getting these errors;

Error: Although PHPStan\Reflection\Php\PhpPropertyReflection is covered by backward compatibility promise, this instanceof assumption might break because it's not guaranteed to always stay the same.
Error: Creating new PHPStan\Rules\Properties\UninitializedPropertyRule is not covered by backward compatibility promise. The class might change in a minor PHPStan version.
Error: Creating new PHPStan\Reflection\ConstructorsHelper is not covered by backward compatibility promise. The class might change in a minor PHPStan version.

I'm not sure how to fix these. I'm also seeing a couple of similar ones in the baseline, so would baselining these be OK?

Then there's this error;

Error: Class Symfony\Contracts\Service\Attribute\Required not found.

Seems to me like this one should be available, but I must be missing something here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      Participants

      @ondrejmirtes@raalderink

      Issue actions

        checkUninitializedProperties does not consider @required · Issue #346 · phpstan/phpstan-symfony