psalm icon indicating copy to clipboard operation
psalm copied to clipboard

Different Code parts depending on other packages show different errors

Open Hanmac opened this issue 3 months ago • 3 comments

Using Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface

before, getTypes was used to return a List of in this file imported LegacyType with Symfony 7.1, a new method getType was added to return new Component https://symfony.com/doc/current/components/type_info.html


if (method_exists($this->extractor, 'getType') && class_exists(Type::class)) {
            $type = $this->extractor->getType($class, $property);
            if (null === $type) {
                return null;
            }
            if ($type->isIdentifiedBy(CronExpression::class)) {
                return new TypeGuess(CronExpressionType::class, [], Guess::VERY_HIGH_CONFIDENCE);
            }
        } else {
            $types = $this->extractor->getTypes($class, $property);
            if (null === $types) {
                return null;
            }
            foreach ($types as $lType) {
                if (LegacyType::BUILTIN_TYPE_OBJECT === $lType->getBuiltinType() &&
                    CronExpression::class === $lType->getClassName()) {
                    return new TypeGuess(CronExpressionType::class, [], Guess::VERY_HIGH_CONFIDENCE);
                }
            }
        }

right now, the psalm tests for Symfony 5.4 and 6.4 complain about the first part:

Error: src/Form/TypeGuesser/CronExpressionTypeGuesser.php:41:13: MixedAssignment: Unable to determine the type that $type is being assigned to (see https://psalm.dev/032)
Error: src/Form/TypeGuesser/CronExpressionTypeGuesser.php:45:24: MixedMethodCall: Cannot determine the type of $type when calling method isIdentifiedBy (see https://psalm.dev/015)

while the newer for Symfony 7.3 complain about the second part:

Error: src/Form/TypeGuesser/CronExpressionTypeGuesser.php:45:22: DeprecatedClass: Class Symfony\Component\PropertyInfo\Type is marked as deprecated (see https://psalm.dev/098)
Error: src/Form/TypeGuesser/CronExpressionTypeGuesser.php:45:40: DeprecatedMethod: The method Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface::getTypes has been marked as deprecated (see https://psalm.dev/001)
Error: src/Form/TypeGuesser/CronExpressionTypeGuesser.php:50:21: DeprecatedClass: Class Symfony\Component\PropertyInfo\Type is deprecated (see https://psalm.dev/098)

How can i tell psalm which part it should use? (i tried adding class_exists(Type::class) but it seems to be ignored)

Hanmac avatar Sep 16 '25 12:09 Hanmac

Hey @Hanmac, can you reproduce the issue on https://psalm.dev? These will be used as phpunit tests when implementing the feature or fixing this bug.

psalm-github-bot[bot] avatar Sep 16 '25 12:09 psalm-github-bot[bot]

Smyfony below 7.3 (mixed asignment/undefined docblock class) https://psalm.dev/r/dbe7e02a86

Symfony more than 7.3 (DeprecatedClass/DeprecatedMethod) https://psalm.dev/r/3915bdd186

Hanmac avatar Sep 16 '25 13:09 Hanmac

I found these snippets:

https://psalm.dev/r/dbe7e02a86
<?php

class CronExpression
{
};

class LegacyType
{
   const string BUILTIN_TYPE_OBJECT = "object";
   
    public function getBuiltinType(): string
    {
        return self::BUILTIN_TYPE_OBJECT;
    }
    
    public function getClassName(): string
    {
        return CronExpression::class;
    }
}
/*
class Type
{
    public function isIdentifiedBy(string $class): bool
    {
        return true;
    }
}
//*/

/**
 * Type Extractor Interface.
 *
 * @author Kévin Dunglas <[email protected]>
 *
 * @method Type|null getType(string $class, string $property, array $context = [])
 */
interface PropertyTypeExtractorInterface
{
    #public function getType(string $class, string $property, array $context = []): Type|null;
    /**
     * Gets types of a property.
     *
     * @return LegacyType[]|null
     */
    public function getTypes(string $class, string $property, array $context = []): ?array;
}
class PropertyInfoExtractor implements PropertyTypeExtractorInterface
{
    #public function getType(string $class, string $property, array $context = []): Type|null { return null; }
    public function getTypes(string $class, string $property, array $context = []): ?array
    {
        return[];
    }
}

final class CronExpressionTypeGuesser
{
    private PropertyTypeExtractorInterface $extractor;

    public function __construct(?PropertyTypeExtractorInterface $extractor = null)
    {
        $this->extractor = $extractor ?? $this->createExtractor();
    }

    public function guessType(string $class, string $property): ?bool
    {
        if (!class_exists($class)) {
            return null;
        }

        if (class_exists(Type::class) && method_exists($this->extractor, 'getType')) {
            $type = $this->extractor->getType($class, $property);
            if (null === $type) {
                return null;
            }
            if ($type->isIdentifiedBy(CronExpression::class)) {
                return true;
            }
        } else {
            $types = $this->extractor->getTypes($class, $property);
            if (null === $types) {
                return null;
            }
            foreach ($types as $lType) {
                if (LegacyType::BUILTIN_TYPE_OBJECT === $lType->getBuiltinType() &&
                    CronExpression::class === $lType->getClassName()) {
                    return true;
                }
            }
        }

        return null;
    }
    
    private function createExtractor(): PropertyTypeExtractorInterface
    {
        return new PropertyInfoExtractor();
    }
}
Psalm output (using commit cdceda0):

ERROR: UndefinedDocblockClass - 77:17 - Docblock-defined class, interface or enum named Type does not exist
https://psalm.dev/r/3915bdd186
<?php

class CronExpression
{
};

/**
 * @deprecated
*/
class LegacyType
{
   const string BUILTIN_TYPE_OBJECT = "object";
   
    public function getBuiltinType(): string
    {
        return self::BUILTIN_TYPE_OBJECT;
    }
    
    public function getClassName(): string
    {
        return CronExpression::class;
    }
}
//*
class Type
{
    public function isIdentifiedBy(string $class): bool
    {
        return true;
    }
}
//*/

/**
 * Type Extractor Interface.
 *
 * @author Kévin Dunglas <[email protected]>
 *
 * @method Type|null getType(string $class, string $property, array $context = [])
 */
interface PropertyTypeExtractorInterface
{
    #public function getType(string $class, string $property, array $context = []): Type|null;
    /**
     * Gets types of a property.
     *
     * @deprecated since Symfony 7.3, use "getType" instead
     *
     * @return LegacyType[]|null
     */
    public function getTypes(string $class, string $property, array $context = []): ?array;
}
class PropertyInfoExtractor implements PropertyTypeExtractorInterface
{
    public function getType(string $class, string $property, array $context = []): Type|null { return null; }
    public function getTypes(string $class, string $property, array $context = []): ?array
    {
        return[];
    }
}

final class CronExpressionTypeGuesser
{
    private PropertyTypeExtractorInterface $extractor;

    public function __construct(?PropertyTypeExtractorInterface $extractor = null)
    {
        $this->extractor = $extractor ?? $this->createExtractor();
    }

    public function guessType(string $class, string $property): ?bool
    {
        if (!class_exists($class)) {
            return null;
        }

        if (class_exists(Type::class) && method_exists($this->extractor, 'getType')) {
            $type = $this->extractor->getType($class, $property);
            if (null === $type) {
                return null;
            }
            if ($type->isIdentifiedBy(CronExpression::class)) {
                return true;
            }
        } else {
            $types = $this->extractor->getTypes($class, $property);
            if (null === $types) {
                return null;
            }
            foreach ($types as $lType) {
                if (LegacyType::BUILTIN_TYPE_OBJECT === $lType->getBuiltinType() &&
                    CronExpression::class === $lType->getClassName()) {
                    return true;
                }
            }
        }

        return null;
    }
    
    private function createExtractor(): PropertyTypeExtractorInterface
    {
        return new PropertyInfoExtractor();
    }
}
Psalm output (using commit cdceda0):

ERROR: DeprecatedClass - 49:16 - Class LegacyType is marked as deprecated

INFO: DeprecatedMethod - 86:40 - The method PropertyTypeExtractorInterface::getTypes has been marked as deprecated

ERROR: DeprecatedClass - 91:21 - Class LegacyType is deprecated

psalm-github-bot[bot] avatar Sep 16 '25 13:09 psalm-github-bot[bot]