staabm / phpstan-dba

PHPStan based SQL static analysis and type inference for the database access layer
https://staabm.github.io/archive.html#phpstan-dba
MIT License
249 stars 17 forks source link

Psalm support #643

Open patrickkusebauch opened 6 months ago

patrickkusebauch commented 6 months ago

I know this is a PHPStan extension. However nowadays it is not uncommon for people to run both PHPStan and Psalm on a project simultaneously since the feature set is not completely equivalent. And Psalm currently does not have anything similar to the functionality of this extension.

In my project, I did this to provide support for Dibi. It may be a good starting point if someone decides to give it a shot:

use PhpParser\Node\Scalar\String_;
use PHPStan\Reflection\ReflectionProvider\DummyReflectionProvider;
use PHPStan\Reflection\ReflectionProviderStaticAccessor;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\VerbosityLevel;
use Psalm\Plugin\EventHandler\Event\MethodReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\MethodReturnTypeProviderInterface;
use Psalm\Type;
use Psalm\Type\Union;
use staabm\PHPStanDba\DibiReflection\DibiReflection;
use DibiConnection;

final class DibiMethodReturnTypeProvider implements MethodReturnTypeProviderInterface
{

    public static function getClassLikeNames(): array
    {
        return [
            DibiConnection::class,
        ];
    }

    public static function getMethodReturnType(MethodReturnTypeProviderEvent $event): ?Union
    {
        if (!in_array($event->getMethodNameLowercase(), ['fetchall', 'fetch', 'fetchpairs'], true)) {
            return null;
        }

        ReflectionProviderStaticAccessor::registerInstance(new DummyReflectionProvider());
        $cache   = require(__DIR__.'/../.dba.cache.php');
        $records = $cache['records'];

        $query = $event->getCallArgs()[0]->value;
        assert($query instanceof String_);
        $queryString = self::replacePlaceholders($query->value);

        if (array_key_exists($queryString, $records) === false) {
            return null;
        }

        $result = $records[$queryString]['result'][3];
        assert($result instanceof ConstantArrayType);

        $stringToParse = match ($event->getMethodNameLowercase()) {
            'fetchall' => 'list<'.$result->describe(VerbosityLevel::precise()).'>',
            'fetch' => '?' . $result->describe(VerbosityLevel::precise()),
            'fetchpairs' => 'list<'.$result->getFirstIterableValueType()->describe(VerbosityLevel::precise()).'>',
        };
        return Type::parseString($stringToParse);
    }

    private static function replacePlaceholders(string $queryString): string
    {
        $rewriteQuery = (new DibiReflection())->rewriteQuery($queryString);
        return trim($rewriteQuery ?? '');
    }
}

final readonly class DibiConnection
{
    public function __construct(
        private Connection $connection
    ) {
    }

    final public function query(mixed ...$args): Result
    {
        /** @throws void  */
        return $this->connection->query($args);
    }

    public function fetch(mixed ...$args): ?array
    {
        /** @throws void  */
        return $this->connection->query($args)
            ->setRowClass(null)
            ->fetch();
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function fetchAll(mixed ...$args): array
    {
        /** @throws void  */
        return $this->connection->query($args)
            ->setRowClass(null)
            ->fetchAll();
    }

    public function fetchSingle(mixed ...$args): mixed
    {
        /** @throws void  */
        return $this->connection->query($args)
            ->setRowClass(null)
            ->fetchSingle();
    }

    /**
     * @return list<mixed>
     */
    public function fetchPairs(mixed ...$args): array
    {
        /** @throws void */
        return $this->connection->query($args)
            ->setRowClass(null)
            ->fetchPairs();
    }
}

It turns out that it is fairly easy to convert PHPStan types to Psalm types by calling Type::parseString($phpstanType->describe(VerbosityLevel::precise()))