Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Psalm support #643

Open
patrickkusebauch opened this issue Jan 27, 2024 · 0 comments
Open

Psalm support #643

patrickkusebauch opened this issue Jan 27, 2024 · 0 comments

Comments

@patrickkusebauch
Copy link
Contributor

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()))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant