1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00
psalm/tests/CodebaseTest.php

303 lines
11 KiB
PHP
Raw Permalink Normal View History

2019-02-20 17:55:56 +01:00
<?php
2023-10-19 13:12:06 +02:00
declare(strict_types=1);
2019-02-20 17:55:56 +01:00
namespace Psalm\Tests;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Class_;
2019-02-20 17:55:56 +01:00
use Psalm\Codebase;
use Psalm\Context;
use Psalm\Exception\CodeException;
2021-12-03 20:29:06 +01:00
use Psalm\Exception\UnpopulatedClasslikeException;
use Psalm\Issue\InvalidReturnStatement;
use Psalm\Issue\InvalidReturnType;
use Psalm\IssueBuffer;
use Psalm\Plugin\EventHandler\AfterClassLikeVisitInterface;
use Psalm\Plugin\EventHandler\BeforeAddIssueInterface;
2021-06-08 04:55:21 +02:00
use Psalm\Plugin\EventHandler\Event\AfterClassLikeVisitEvent;
use Psalm\Plugin\EventHandler\Event\BeforeAddIssueEvent;
2019-03-23 19:27:54 +01:00
use Psalm\PluginRegistrationSocket;
2019-02-24 14:12:00 +01:00
use Psalm\Tests\Internal\Provider\ClassLikeStorageInstanceCacheProvider;
2019-02-20 17:55:56 +01:00
use Psalm\Type;
use function array_map;
2021-06-08 04:55:21 +02:00
use function array_values;
use function get_class;
use function getcwd;
use const DIRECTORY_SEPARATOR;
2021-06-08 04:55:21 +02:00
2019-02-20 17:55:56 +01:00
class CodebaseTest extends TestCase
{
2022-12-16 19:58:47 +01:00
private Codebase $codebase;
2019-02-20 17:55:56 +01:00
public function setUp(): void
2019-02-20 17:55:56 +01:00
{
parent::setUp();
$this->codebase = $this->project_analyzer->getCodebase();
}
/**
* @test
* @dataProvider typeContainments
*/
public function isTypeContainedByType(string $input, string $container, bool $expected): void
2019-02-20 17:55:56 +01:00
{
$input = Type::parseString($input);
$container = Type::parseString($container);
2019-03-23 19:27:54 +01:00
$this->assertSame(
2019-02-20 17:55:56 +01:00
$expected,
$this->codebase->isTypeContainedByType($input, $container),
'Expected ' . $input->getId() . ($expected ? ' ' : ' not ')
2022-12-18 17:15:15 +01:00
. 'to be contained in ' . $container->getId(),
2019-02-20 17:55:56 +01:00
);
}
/** @return iterable<int,array{string,string,bool}> */
public function typeContainments(): iterable
2019-02-20 17:55:56 +01:00
{
yield ['int', 'int|string', true];
yield ['int|string', 'int', false];
// This fails with 'could not get class storage' :(
// yield ['RuntimeException', 'Exception', true];
// yield ['Exception', 'RuntimeException', false];
}
/**
* @test
* @dataProvider typeIntersections
*/
public function canTypeBeContainedByType(string $input, string $container, bool $expected): void
2019-02-20 17:55:56 +01:00
{
$input = Type::parseString($input);
$container = Type::parseString($container);
2019-03-23 19:27:54 +01:00
$this->assertSame(
2019-02-20 17:55:56 +01:00
$expected,
$this->codebase->canTypeBeContainedByType($input, $container),
'Expected ' . $input->getId() . ($expected ? ' ' : ' not ')
2022-12-18 17:15:15 +01:00
. 'to be contained in ' . $container->getId(),
2019-02-20 17:55:56 +01:00
);
}
/** @return iterable<int,array{string,string,bool}> */
public function typeIntersections(): iterable
2019-02-20 17:55:56 +01:00
{
yield ['int', 'int|string', true];
yield ['int|string', 'int', true];
yield ['int|string', 'string|float', true];
yield ['int', 'string', false];
yield ['int|string', 'array|float', false];
}
/**
* @test
* @dataProvider iterableParams
* @param array{string,string} $expected
2019-02-20 17:55:56 +01:00
*/
public function getKeyValueParamsForTraversableObject(string $input, array $expected): void
2019-02-20 17:55:56 +01:00
{
[$input] = array_values(Type::parseString($input)->getAtomicTypes());
2019-02-20 17:55:56 +01:00
$expected_key_type = Type::parseString($expected[0]);
$expected_value_type = Type::parseString($expected[1]);
$actual = $this->codebase->getKeyValueParamsForTraversableObject($input);
$this->assertTrue(
$expected_key_type->equals($actual[0]),
'Expected ' . $input->getId() . ' to have ' . $expected_key_type
2022-12-18 17:15:15 +01:00
. ' but got ' . $actual[0]->getId(),
2019-02-20 17:55:56 +01:00
);
$this->assertTrue(
$expected_value_type->equals($actual[1]),
'Expected ' . $input->getId() . ' to have ' . $expected_value_type
2022-12-18 17:15:15 +01:00
. ' but got ' . $actual[1]->getId(),
2019-02-20 17:55:56 +01:00
);
}
/** @return iterable<int,array{string,array{string,string}}> */
public function iterableParams(): iterable
2019-02-20 17:55:56 +01:00
{
yield ['iterable<int,string>', ['int', 'string']];
yield ['iterable<int|string,bool|float>', ['int|string', 'bool|float']];
2019-02-20 17:55:56 +01:00
}
2019-02-24 14:12:00 +01:00
/**
* @test
*/
public function customMetadataIsPersisted(): void
2019-02-24 14:12:00 +01:00
{
$this->addFile(
'somefile.php',
'<?php
namespace Psalm\CurrentTest;
abstract class A {}
interface I {}
class C extends A implements I
{
2019-02-24 14:12:00 +01:00
/** @var string */
private $prop = "";
/** @return void */
public function m(int $_i = 1) {}
}
2022-12-18 17:15:15 +01:00
',
2019-02-24 14:12:00 +01:00
);
2019-03-23 19:27:54 +01:00
$hook = new class implements AfterClassLikeVisitInterface {
2019-02-24 14:12:00 +01:00
/**
* @return void
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint
2019-02-24 14:12:00 +01:00
*/
public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event)
{
$stmt = $event->getStmt();
$storage = $event->getStorage();
$codebase = $event->getCodebase();
if ($storage->name === 'Psalm\\CurrentTest\\C' && $stmt instanceof Class_) {
$storage->custom_metadata['fqcn'] = (string)($stmt->getAttribute('namespacedName') ?? $stmt->name);
$storage->custom_metadata['extends'] = $stmt->extends instanceof Name
? (string)$stmt->extends->getAttribute('resolvedName')
: '';
$storage->custom_metadata['implements'] = array_map(
2023-10-21 20:45:09 +02:00
static fn(Name $aspect): string => (string)$aspect->getAttribute('resolvedName'),
2022-12-18 17:15:15 +01:00
$stmt->implements,
);
2019-02-24 14:12:00 +01:00
$storage->custom_metadata['a'] = 'b';
$storage->methods['m']->custom_metadata['c'] = 'd';
$storage->properties['prop']->custom_metadata['e'] = 'f';
$storage->methods['m']->params[0]->custom_metadata['g'] = 'h';
$codebase->file_storage_provider->get('somefile.php')->custom_metadata['i'] = 'j';
}
}
};
(new PluginRegistrationSocket($this->codebase->config, $this->codebase))
2019-02-24 14:12:00 +01:00
->registerHooksFromClass(get_class($hook));
$this->codebase->classlike_storage_provider->cache = new ClassLikeStorageInstanceCacheProvider;
$this->analyzeFile('somefile.php', new Context);
$fixtureNamespace = 'Psalm\\CurrentTest\\';
$this->codebase->classlike_storage_provider->remove($fixtureNamespace . 'C');
$this->codebase->exhumeClassLikeStorage($fixtureNamespace . 'C', 'somefile.php');
2019-02-24 14:12:00 +01:00
$class_storage = $this->codebase->classlike_storage_provider->get($fixtureNamespace . 'C');
2019-02-24 14:12:00 +01:00
$file_storage = $this->codebase->file_storage_provider->get('somefile.php');
self::assertSame($fixtureNamespace . 'C', $class_storage->custom_metadata['fqcn']);
self::assertSame($fixtureNamespace . 'A', $class_storage->custom_metadata['extends']);
self::assertSame([$fixtureNamespace . 'I'], $class_storage->custom_metadata['implements']);
self::assertSame('b', $class_storage->custom_metadata['a']);
self::assertSame('d', $class_storage->methods['m']->custom_metadata['c']);
self::assertSame('f', $class_storage->properties['prop']->custom_metadata['e']);
self::assertSame('h', $class_storage->methods['m']->params[0]->custom_metadata['g']);
self::assertSame('j', $file_storage->custom_metadata['i']);
2019-02-24 14:12:00 +01:00
}
/**
* @test
*/
public function classExtendsRejectsUnpopulatedClasslikes(): void
{
$this->codebase->classlike_storage_provider->create('A');
$this->codebase->classlike_storage_provider->create('B');
2021-12-03 20:29:06 +01:00
$this->expectException(UnpopulatedClasslikeException::class);
$this->codebase->classExtends('A', 'B');
}
/**
* @test
*/
public function addingCodeIssueIsIntercepted(): void
{
$this->addFile(
'somefile.php',
'<?php
namespace Psalm\CurrentTest;
function invalidReturnType(int $value): string
{
return $value;
}
echo invalidReturnType(123);
2022-12-18 17:15:15 +01:00
',
);
$eventHandler = new class implements BeforeAddIssueInterface
{
public static function beforeAddIssue(BeforeAddIssueEvent $event): ?bool
{
$issue = $event->getIssue();
if ($issue->code_location->file_path !== 'somefile.php') {
return null;
}
if ($issue instanceof InvalidReturnStatement && $event->isFixable() === false) {
return false;
} elseif ($issue instanceof InvalidReturnType && $event->isFixable() === true) {
return false;
}
return null;
}
};
(new PluginRegistrationSocket($this->codebase->config, $this->codebase))
->registerHooksFromClass(get_class($eventHandler));
$this->analyzeFile('somefile.php', new Context);
self::assertSame(0, IssueBuffer::getErrorCount());
}
/**
* @test
*/
public function addingCodeIssueIsMarkedAsRedundant(): void
{
$this->expectException(CodeException::class);
$this->expectExceptionMessage('UnusedPsalmSuppress');
$this->addFile(
(string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php',
'<?php
namespace Psalm\CurrentTest;
/** @psalm-suppress InvalidReturnType */
function invalidReturnType(int $value): string
{
/** @psalm-suppress InvalidReturnStatement */
return $value;
}
echo invalidReturnType(123);
',
);
$eventHandler = new class implements BeforeAddIssueInterface
{
public static function beforeAddIssue(BeforeAddIssueEvent $event): ?bool
{
$issue = $event->getIssue();
if ($issue->code_location->file_path !== (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php') {
return null;
}
if ($issue instanceof InvalidReturnStatement && $event->isFixable() === false) {
return false;
} elseif ($issue instanceof InvalidReturnType && $event->isFixable() === true) {
return false;
}
return null;
}
};
(new PluginRegistrationSocket($this->codebase->config, $this->codebase))
->registerHooksFromClass(get_class($eventHandler));
$this->analyzeFile(
(string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php',
new Context,
);
}
2019-02-20 17:55:56 +01:00
}