1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-11 16:59:45 +01:00
psalm/tests/TypeReconciliation/ReconcilerTest.php

256 lines
11 KiB
PHP
Raw Normal View History

2019-12-06 20:58:18 +01:00
<?php
namespace Psalm\Tests\TypeReconciliation;
use Psalm\Context;
use Psalm\Internal\Analyzer\FileAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
2021-12-03 20:11:20 +01:00
use Psalm\Internal\Provider\NodeDataProvider;
use Psalm\Internal\Type\AssertionReconciler;
2020-07-22 01:40:35 +02:00
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
2021-12-03 20:11:20 +01:00
use Psalm\Tests\TestCase;
2019-12-06 20:58:18 +01:00
use Psalm\Type;
2021-12-03 20:11:20 +01:00
class ReconcilerTest extends TestCase
2019-12-06 20:58:18 +01:00
{
/** @var FileAnalyzer */
protected $file_analyzer;
/** @var StatementsAnalyzer */
protected $statements_analyzer;
public function setUp(): void
2019-12-06 20:58:18 +01:00
{
parent::setUp();
$this->file_analyzer = new FileAnalyzer($this->project_analyzer, 'somefile.php', 'somefile.php');
$this->file_analyzer->context = new Context();
$this->statements_analyzer = new StatementsAnalyzer(
$this->file_analyzer,
2021-12-03 20:11:20 +01:00
new NodeDataProvider()
2019-12-06 20:58:18 +01:00
);
$this->addFile('newfile.php', '
<?php
2021-02-24 04:37:26 +01:00
class SomeClass {}
class SomeChildClass extends SomeClass {}
class A {}
2021-02-24 04:37:26 +01:00
class B {}
interface SomeInterface {}
');
$this->project_analyzer->getCodebase()->scanFiles();
2019-12-06 20:58:18 +01:00
}
/**
* @dataProvider providerTestReconcilation
*/
public function testReconcilation(string $expected_type, string $assertion, string $original_type): void
2019-12-06 20:58:18 +01:00
{
2021-12-03 20:11:20 +01:00
$reconciled = AssertionReconciler::reconcile(
$assertion,
Type::parseString($original_type),
2019-12-06 20:58:18 +01:00
null,
$this->statements_analyzer,
false,
[]
);
$this->assertSame(
$expected_type,
2019-12-06 20:58:18 +01:00
$reconciled->getId()
);
2020-02-22 17:28:24 +01:00
$this->assertContainsOnlyInstancesOf('Psalm\Type\Atomic', $reconciled->getAtomicTypes());
2019-12-06 20:58:18 +01:00
}
/**
* @dataProvider providerTestTypeIsContainedBy
*
* @param string $input
* @param string $container
*
*/
public function testTypeIsContainedBy($input, $container): void
2019-12-06 20:58:18 +01:00
{
$this->assertTrue(
2020-07-22 01:40:35 +02:00
UnionTypeComparator::isContainedBy(
2019-12-06 20:58:18 +01:00
$this->project_analyzer->getCodebase(),
Type::parseString($input),
Type::parseString($container)
)
);
}
/**
* @return array<string,array{string,string,string}>
*/
public function providerTestReconcilation(): array
2019-12-06 20:58:18 +01:00
{
return [
2021-02-24 04:37:26 +01:00
'notNullWithObject' => ['SomeClass', '!null', 'SomeClass'],
'notNullWithObjectPipeNull' => ['SomeClass', '!null', 'SomeClass|null'],
'notNullWithSomeClassPipeFalse' => ['SomeClass|false', '!null', 'SomeClass|false'],
2019-12-06 20:58:18 +01:00
'notNullWithMixed' => ['mixed', '!null', 'mixed'],
2021-02-24 04:37:26 +01:00
'notEmptyWithSomeClass' => ['SomeClass', '!falsy', 'SomeClass'],
'notEmptyWithSomeClassPipeNull' => ['SomeClass', '!falsy', 'SomeClass|null'],
'notEmptyWithSomeClassPipeFalse' => ['SomeClass', '!falsy', 'SomeClass|false'],
2019-12-06 20:58:18 +01:00
'notEmptyWithMixed' => ['non-empty-mixed', '!falsy', 'mixed'],
// @todo in the future this should also work
2021-02-24 04:37:26 +01:00
//'notEmptyWithSomeClassFalseTrue' => ['SomeClass|true', '!falsy', 'SomeClass|bool'],
2019-12-06 20:58:18 +01:00
2021-02-24 04:37:26 +01:00
'nullWithSomeClassPipeNull' => ['null', 'null', 'SomeClass|null'],
2019-12-06 20:58:18 +01:00
'nullWithMixed' => ['null', 'null', 'mixed'],
2021-10-19 20:58:19 +02:00
'falsyWithSomeClass' => ['empty', 'falsy', 'SomeClass'],
2021-02-24 04:37:26 +01:00
'falsyWithSomeClassPipeFalse' => ['false', 'falsy', 'SomeClass|false'],
'falsyWithSomeClassPipeBool' => ['false', 'falsy', 'SomeClass|bool'],
2019-12-06 20:58:18 +01:00
'falsyWithMixed' => ['empty-mixed', 'falsy', 'mixed'],
'falsyWithBool' => ['false', 'falsy', 'bool'],
'falsyWithStringOrNull' => ['""|"0"|null', 'falsy', 'string|null'],
2019-12-06 20:58:18 +01:00
'falsyWithScalarOrNull' => ['empty-scalar', 'falsy', 'scalar'],
2021-02-24 04:37:26 +01:00
'notSomeClassWithSomeClassPipeBool' => ['bool', '!SomeClass', 'SomeClass|bool'],
'notSomeClassWithSomeClassPipeNull' => ['null', '!SomeClass', 'SomeClass|null'],
'notSomeClassWithAPipeB' => ['B', '!A', 'A|B'],
2021-07-26 09:50:12 +02:00
'notDateTimeWithDateTimeInterface' => ['DateTimeImmutable', '!DateTime', 'DateTimeInterface'],
'notDateTimeImmutableWithDateTimeInterface' => ['DateTime', '!DateTimeImmutable', 'DateTimeInterface'],
2019-12-06 20:58:18 +01:00
2021-02-24 04:37:26 +01:00
'myObjectWithSomeClassPipeBool' => ['SomeClass', 'SomeClass', 'SomeClass|bool'],
'myObjectWithAPipeB' => ['A', 'A', 'A|B'],
2019-12-06 20:58:18 +01:00
'array' => ['array<array-key, mixed>', 'array', 'array|null'],
'2dArray' => ['array<array-key, array<array-key, string>>', 'array', 'array<array<string>>|null'],
'numeric' => ['numeric-string', 'numeric', 'string'],
'nullableClassString' => ['null', 'falsy', '?class-string'],
'mixedOrNullNotFalsy' => ['non-empty-mixed', '!falsy', 'mixed|null'],
'mixedOrNullFalsy' => ['empty-mixed|null', 'falsy', 'mixed|null'],
2021-02-24 04:37:26 +01:00
'nullableClassStringFalsy' => ['null', 'falsy', 'class-string<SomeClass>|null'],
'nullableClassStringEqualsNull' => ['null', '=null', 'class-string<SomeClass>|null'],
'nullableClassStringTruthy' => ['class-string<SomeClass>', '!falsy', 'class-string<SomeClass>|null'],
2019-12-06 20:58:18 +01:00
'iterableToArray' => ['array<int, int>', 'array', 'iterable<int, int>'],
'iterableToTraversable' => ['Traversable<int, int>', 'Traversable', 'iterable<int, int>'],
2020-01-02 14:37:54 +01:00
'callableToCallableArray' => ['callable-array{0: class-string|object, 1: string}', 'array', 'callable'],
'SmallKeyedArrayAndCallable' => ['array{test: string}', 'array{test: string}', 'callable'],
'BigKeyedArrayAndCallable' => ['array{foo: string, test: string, thing: string}', 'array{foo: string, test: string, thing: string}', 'callable'],
2020-04-11 23:50:24 +02:00
'callableOrArrayToCallableArray' => ['array<array-key, mixed>', 'array', 'callable|array'],
2019-12-06 20:58:18 +01:00
'traversableToIntersection' => ['Countable&Traversable', 'Traversable', 'Countable'],
'iterableWithoutParamsToTraversableWithoutParams' => ['Traversable', '!array', 'iterable'],
'iterableWithParamsToTraversableWithParams' => ['Traversable<int, string>', '!array', 'iterable<int, string>'],
'iterableAndObject' => ['Traversable<int, string>', 'object', 'iterable<int, string>'],
'iterableAndNotObject' => ['array<int, string>', '!object', 'iterable<int, string>'],
'boolNotEmptyIsTrue' => ['true', '!empty', 'bool'],
'interfaceAssertionOnClassInterfaceUnion' => ['SomeInterface|SomeInterface&SomeClass', 'SomeInterface', 'SomeClass|SomeInterface'],
2021-10-11 14:33:21 +02:00
'stringToNumericStringWithInt' => ['numeric-string', '~int', 'string'],
'stringToNumericStringWithFloat' => ['numeric-string', '~float', 'string'],
2021-10-11 13:36:09 +02:00
'filterKeyedArrayWithIterable' => ['array{some: string}', 'iterable<string>', 'array{some: mixed}'],
2021-10-12 15:46:02 +02:00
'SimpleXMLElementNotAlwaysTruthy' => ['SimpleXMLElement', '!falsy', 'SimpleXMLElement'],
'SimpleXMLElementNotAlwaysTruthy2' => ['SimpleXMLElement', 'falsy', 'SimpleXMLElement'],
'SimpleXMLIteratorNotAlwaysTruthy' => ['SimpleXMLIterator', '!falsy', 'SimpleXMLIterator'],
'SimpleXMLIteratorNotAlwaysTruthy2' => ['SimpleXMLIterator', 'falsy', 'SimpleXMLIterator'],
2019-12-06 20:58:18 +01:00
];
}
/**
* @return array<string,array{string,string}>
*/
public function providerTestTypeIsContainedBy(): array
2019-12-06 20:58:18 +01:00
{
return [
'arrayContainsWithArrayOfStrings' => ['array<string>', 'array'],
'arrayContainsWithArrayOfExceptions' => ['array<Exception>', 'array'],
'arrayOfIterable' => ['array', 'iterable'],
2021-02-24 04:37:26 +01:00
'arrayOfIterableWithType' => ['array<SomeClass>', 'iterable<SomeClass>'],
'arrayOfIterableWithSubclass' => ['array<SomeChildClass>', 'iterable<SomeClass>'],
'arrayOfSubclassOfParent' => ['array<SomeChildClass>', 'array<SomeClass>'],
'subclassOfParent' => ['SomeChildClass', 'SomeClass'],
2019-12-06 20:58:18 +01:00
'unionContainsWithstring' => ['string', 'string|false'],
'unionContainsWithFalse' => ['false', 'string|false'],
'objectLikeTypeWithPossiblyUndefinedToGeneric' => [
'array{0: array{a: string}, 1: array{c: string, e: string}}',
'array<int, array<string, string>>',
],
'objectLikeTypeWithPossiblyUndefinedToEmpty' => [
'array<empty, empty>',
'array{a?: string, b?: string}',
],
'literalNumericStringInt' => [
'"0"',
'numeric',
],
'literalNumericString' => [
'"10.03"',
'numeric',
],
2019-12-06 20:58:18 +01:00
];
}
/**
* @dataProvider constantAssertions
*/
public function testReconciliationOfClassConstantInAssertions(string $assertion, string $expected_type): void
{
$this->addFile(
'psalm-assert.php',
'
<?php
namespace ReconciliationTest;
class Foo
{
const PREFIX_BAR = \'bar\';
const PREFIX_BAZ = \'baz\';
const PREFIX_QOO = Foo::PREFIX_BAR;
}
'
);
$this->project_analyzer->getCodebase()->scanFiles();
2021-12-03 20:11:20 +01:00
$reconciled = AssertionReconciler::reconcile(
$assertion,
new Type\Union([
new Type\Atomic\TLiteralString(''),
]),
null,
$this->statements_analyzer,
false,
[]
);
$this->assertSame(
$expected_type,
$reconciled->getId()
);
}
/**
* @return array<non-empty-string,array{non-empty-string,string}>
*/
public function constantAssertions(): array
{
return [
'constant-with-prefix' => [
'class-constant(ReconciliationTest\\Foo::PREFIX_*)',
'"bar"|"baz"',
],
'single-class-constant' => [
'class-constant(ReconciliationTest\\Foo::PREFIX_BAR)',
'"bar"',
],
'referencing-another-class-constant' => [
'class-constant(ReconciliationTest\\Foo::PREFIX_QOO)',
'"bar"',
],
'referencing-all-class-constants' => [
'class-constant(ReconciliationTest\\Foo::*)',
'"bar"|"baz"',
],
'referencing-some-class-constants-with-wildcard' => [
'class-constant(ReconciliationTest\\Foo::PREFIX_B*)',
'"bar"|"baz"',
],
];
}
2019-12-06 20:58:18 +01:00
}