2019-02-20 17:55:56 +01:00
|
|
|
<?php
|
2021-12-15 04:58:32 +01:00
|
|
|
|
2023-10-19 13:12:06 +02:00
|
|
|
declare(strict_types=1);
|
|
|
|
|
2019-02-20 17:55:56 +01:00
|
|
|
namespace Psalm\Tests;
|
|
|
|
|
2021-01-23 14:17:29 +01:00
|
|
|
use PhpParser\Node\Name;
|
|
|
|
use PhpParser\Node\Stmt\Class_;
|
2019-02-20 17:55:56 +01:00
|
|
|
use Psalm\Codebase;
|
|
|
|
use Psalm\Context;
|
2023-11-30 13:48:32 +01:00
|
|
|
use Psalm\Exception\CodeException;
|
2021-12-03 20:29:06 +01:00
|
|
|
use Psalm\Exception\UnpopulatedClasslikeException;
|
2021-02-01 17:00:07 +01:00
|
|
|
use Psalm\Issue\InvalidReturnStatement;
|
|
|
|
use Psalm\Issue\InvalidReturnType;
|
|
|
|
use Psalm\IssueBuffer;
|
2021-01-06 15:05:53 +01:00
|
|
|
use Psalm\Plugin\EventHandler\AfterClassLikeVisitInterface;
|
2021-02-01 17:00:07 +01:00
|
|
|
use Psalm\Plugin\EventHandler\BeforeAddIssueInterface;
|
2021-06-08 04:55:21 +02:00
|
|
|
use Psalm\Plugin\EventHandler\Event\AfterClassLikeVisitEvent;
|
2021-02-01 17:00:07 +01:00
|
|
|
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;
|
|
|
|
|
2021-01-23 14:17:29 +01:00
|
|
|
use function array_map;
|
2021-06-08 04:55:21 +02:00
|
|
|
use function array_values;
|
|
|
|
use function get_class;
|
2023-11-30 13:48:32 +01:00
|
|
|
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
|
|
|
|
2021-12-05 18:51:26 +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
|
|
|
|
*/
|
2020-09-12 17:24:05 +02:00
|
|
|
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
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-11-12 02:14:21 +01:00
|
|
|
/** @return iterable<int,array{string,string,bool}> */
|
2021-12-05 18:51:26 +01:00
|
|
|
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
|
|
|
|
*/
|
2020-09-12 17:24:05 +02:00
|
|
|
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
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-11-12 02:14:21 +01:00
|
|
|
/** @return iterable<int,array{string,string,bool}> */
|
2021-12-05 18:51:26 +01:00
|
|
|
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
|
2022-11-12 02:14:21 +01:00
|
|
|
* @param array{string,string} $expected
|
2019-02-20 17:55:56 +01:00
|
|
|
*/
|
2020-09-12 17:24:05 +02:00
|
|
|
public function getKeyValueParamsForTraversableObject(string $input, array $expected): void
|
2019-02-20 17:55:56 +01:00
|
|
|
{
|
2020-09-02 06:17:41 +02: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
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-11-12 02:14:21 +01:00
|
|
|
/** @return iterable<int,array{string,array{string,string}}> */
|
2021-12-05 18:51:26 +01:00
|
|
|
public function iterableParams(): iterable
|
2019-02-20 17:55:56 +01:00
|
|
|
{
|
|
|
|
yield ['iterable<int,string>', ['int', 'string']];
|
2019-06-15 23:57:40 +02:00
|
|
|
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
|
|
|
|
*/
|
2020-09-12 17:24:05 +02:00
|
|
|
public function customMetadataIsPersisted(): void
|
2019-02-24 14:12:00 +01:00
|
|
|
{
|
|
|
|
$this->addFile(
|
|
|
|
'somefile.php',
|
|
|
|
'<?php
|
2021-01-23 14:17:29 +01:00
|
|
|
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
|
2021-12-05 18:51:26 +01:00
|
|
|
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint
|
2019-02-24 14:12:00 +01:00
|
|
|
*/
|
2021-01-06 15:05:53 +01:00
|
|
|
public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event)
|
|
|
|
{
|
2021-01-23 14:17:29 +01:00
|
|
|
$stmt = $event->getStmt();
|
2021-01-06 15:05:53 +01:00
|
|
|
$storage = $event->getStorage();
|
|
|
|
$codebase = $event->getCodebase();
|
2021-01-23 14:17:29 +01:00
|
|
|
if ($storage->name === 'Psalm\\CurrentTest\\C' && $stmt instanceof Class_) {
|
2021-11-29 20:11:58 +01:00
|
|
|
$storage->custom_metadata['fqcn'] = (string)($stmt->getAttribute('namespacedName') ?? $stmt->name);
|
2021-01-23 14:17:29 +01:00
|
|
|
$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,
|
2021-01-23 14:17:29 +01:00
|
|
|
);
|
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';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2019-03-01 14:57:10 +01:00
|
|
|
(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);
|
|
|
|
|
2021-01-23 14:17:29 +01:00
|
|
|
$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
|
|
|
|
2021-01-23 14:17:29 +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');
|
|
|
|
|
2021-01-23 14:17:29 +01:00
|
|
|
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
|
|
|
}
|
2019-03-16 23:03:37 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @test
|
|
|
|
*/
|
2020-09-12 17:24:05 +02:00
|
|
|
public function classExtendsRejectsUnpopulatedClasslikes(): void
|
2019-03-16 23:03:37 +01:00
|
|
|
{
|
|
|
|
$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);
|
2019-03-16 23:03:37 +01:00
|
|
|
|
|
|
|
$this->codebase->classExtends('A', 'B');
|
|
|
|
}
|
2021-02-01 17:00:07 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @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
|
|
|
',
|
2021-02-01 17:00:07 +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());
|
|
|
|
}
|
2023-11-30 13:48:32 +01:00
|
|
|
/**
|
|
|
|
* @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
|
|
|
}
|