mirror of
https://github.com/danog/psalm.git
synced 2024-12-15 02:47:02 +01:00
8d36bdc3ed
* Make array shapes strict by default * Fix PSL tests
287 lines
13 KiB
PHP
287 lines
13 KiB
PHP
<?php
|
|
|
|
namespace Psalm\Tests\TypeReconciliation;
|
|
|
|
use Psalm\Context;
|
|
use Psalm\Internal\Analyzer\FileAnalyzer;
|
|
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
|
use Psalm\Internal\Provider\NodeDataProvider;
|
|
use Psalm\Internal\Type\AssertionReconciler;
|
|
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
|
use Psalm\Storage\Assertion;
|
|
use Psalm\Storage\Assertion\Any;
|
|
use Psalm\Storage\Assertion\Falsy;
|
|
use Psalm\Storage\Assertion\IsIdentical;
|
|
use Psalm\Storage\Assertion\IsLooselyEqual;
|
|
use Psalm\Storage\Assertion\IsNotIdentical;
|
|
use Psalm\Storage\Assertion\IsNotType;
|
|
use Psalm\Storage\Assertion\IsType;
|
|
use Psalm\Storage\Assertion\NonEmpty;
|
|
use Psalm\Storage\Assertion\Truthy;
|
|
use Psalm\Tests\TestCase;
|
|
use Psalm\Type;
|
|
use Psalm\Type\Atomic\TArray;
|
|
use Psalm\Type\Atomic\TClassConstant;
|
|
use Psalm\Type\Atomic\TFalse;
|
|
use Psalm\Type\Atomic\TFloat;
|
|
use Psalm\Type\Atomic\TInt;
|
|
use Psalm\Type\Atomic\TIterable;
|
|
use Psalm\Type\Atomic\TKeyedArray;
|
|
use Psalm\Type\Atomic\TLiteralString;
|
|
use Psalm\Type\Atomic\TNamedObject;
|
|
use Psalm\Type\Atomic\TNull;
|
|
use Psalm\Type\Atomic\TNumeric;
|
|
use Psalm\Type\Atomic\TObject;
|
|
use Psalm\Type\Atomic\TTrue;
|
|
use Psalm\Type\Union;
|
|
|
|
class ReconcilerTest extends TestCase
|
|
{
|
|
/** @var FileAnalyzer */
|
|
protected $file_analyzer;
|
|
|
|
/** @var StatementsAnalyzer */
|
|
protected $statements_analyzer;
|
|
|
|
public function setUp(): void
|
|
{
|
|
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,
|
|
new NodeDataProvider()
|
|
);
|
|
|
|
$this->addFile('newfile.php', '
|
|
<?php
|
|
class SomeClass {}
|
|
class SomeChildClass extends SomeClass {}
|
|
class A {}
|
|
class B {}
|
|
interface SomeInterface {}
|
|
');
|
|
$this->project_analyzer->getCodebase()->scanFiles();
|
|
}
|
|
|
|
/**
|
|
* @dataProvider providerTestReconcilation
|
|
*/
|
|
public function testReconcilation(string $expected_type, Assertion $assertion, string $original_type): void
|
|
{
|
|
$reconciled = AssertionReconciler::reconcile(
|
|
$assertion,
|
|
Type::parseString($original_type),
|
|
null,
|
|
$this->statements_analyzer,
|
|
false,
|
|
[]
|
|
);
|
|
|
|
$this->assertSame(
|
|
$expected_type,
|
|
$reconciled->getId()
|
|
);
|
|
|
|
$this->assertContainsOnlyInstancesOf('Psalm\Type\Atomic', $reconciled->getAtomicTypes());
|
|
}
|
|
|
|
/**
|
|
* @dataProvider providerTestTypeIsContainedBy
|
|
*
|
|
* @param string $input
|
|
* @param string $container
|
|
*
|
|
*/
|
|
public function testTypeIsContainedBy($input, $container): void
|
|
{
|
|
$this->assertTrue(
|
|
UnionTypeComparator::isContainedBy(
|
|
$this->project_analyzer->getCodebase(),
|
|
Type::parseString($input),
|
|
Type::parseString($container)
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array<string,array{string,Assertion,string}>
|
|
*/
|
|
public function providerTestReconcilation(): array
|
|
{
|
|
return [
|
|
'notNullWithObject' => ['SomeClass', new IsNotType(new TNull()), 'SomeClass'],
|
|
'notNullWithObjectPipeNull' => ['SomeClass', new IsNotType(new TNull()), 'SomeClass|null'],
|
|
'notNullWithSomeClassPipeFalse' => ['SomeClass|false', new IsNotType(new TNull()), 'SomeClass|false'],
|
|
'notNullWithMixed' => ['mixed', new IsNotType(new TNull()), 'mixed'],
|
|
|
|
'notEmptyWithSomeClass' => ['SomeClass', new Truthy(), 'SomeClass'],
|
|
'notEmptyWithSomeClassPipeNull' => ['SomeClass', new Truthy(), 'SomeClass|null'],
|
|
'notEmptyWithSomeClassPipeFalse' => ['SomeClass', new Truthy(), 'SomeClass|false'],
|
|
'notEmptyWithMixed' => ['non-empty-mixed', new Truthy(), 'mixed'],
|
|
// @todo in the future this should also work
|
|
//'notEmptyWithSomeClassFalseTrue' => ['SomeClass|true', '!falsy', 'SomeClass|bool'],
|
|
|
|
'nullWithSomeClassPipeNull' => ['null', new IsType(new TNull()), 'SomeClass|null'],
|
|
'nullWithMixed' => ['null', new IsType(new TNull()), 'mixed'],
|
|
|
|
'falsyWithSomeClass' => ['never', new Falsy(), 'SomeClass'],
|
|
'falsyWithSomeClassPipeFalse' => ['false', new Falsy(), 'SomeClass|false'],
|
|
'falsyWithSomeClassPipeBool' => ['false', new Falsy(), 'SomeClass|bool'],
|
|
'falsyWithMixed' => ['empty-mixed', new Falsy(), 'mixed'],
|
|
'falsyWithBool' => ['false', new Falsy(), 'bool'],
|
|
'falsyWithStringOrNull' => ["''|'0'|null", new Falsy(), 'string|null'],
|
|
'falsyWithScalarOrNull' => ['empty-scalar', new Falsy(), 'scalar'],
|
|
'trueWithBool' => ['true', new IsType(new TTrue()), 'bool'],
|
|
'falseWithBool' => ['false', new IsType(new TFalse()), 'bool'],
|
|
'notTrueWithBool' => ['false', new IsNotIdentical(new TTrue()), 'bool'],
|
|
'notFalseWithBool' => ['true', new IsNotIdentical(new TFalse()), 'bool'],
|
|
|
|
'notSomeClassWithSomeClassPipeBool' => ['bool', new IsNotType(new TNamedObject('SomeClass')), 'SomeClass|bool'],
|
|
'notSomeClassWithSomeClassPipeNull' => ['null', new IsNotType(new TNamedObject('SomeClass')), 'SomeClass|null'],
|
|
'notSomeClassWithAPipeB' => ['B', new IsNotType(new TNamedObject('A')), 'A|B'],
|
|
'notDateTimeWithDateTimeInterface' => ['DateTimeImmutable', new IsNotType(new TNamedObject('DateTime')), 'DateTimeInterface'],
|
|
'notDateTimeImmutableWithDateTimeInterface' => ['DateTime', new IsNotType(new TNamedObject('DateTimeImmutable')), 'DateTimeInterface'],
|
|
|
|
'myObjectWithSomeClassPipeBool' => ['SomeClass', new IsType(new TNamedObject('SomeClass')), 'SomeClass|bool'],
|
|
'myObjectWithAPipeB' => ['A', new IsType(new TNamedObject('A')), 'A|B'],
|
|
|
|
'array' => ['array<array-key, mixed>', new IsType(new TArray([Type::getArrayKey(), Type::getMixed()])), 'array|null'],
|
|
|
|
'2dArray' => ['array<array-key, array<array-key, string>>', new IsType(new TArray([Type::getArrayKey(), Type::getMixed()])), 'array<array<string>>|null'],
|
|
|
|
'numeric' => ['numeric-string', new IsType(new TNumeric()), 'string'],
|
|
|
|
'nullableClassString' => ['null', new Falsy(), '?class-string'],
|
|
'mixedOrNullNotFalsy' => ['non-empty-mixed', new Truthy(), 'mixed|null'],
|
|
'mixedOrNullFalsy' => ['empty-mixed|null', new Falsy(), 'mixed|null'],
|
|
'nullableClassStringFalsy' => ['null', new Falsy(), 'class-string<SomeClass>|null'],
|
|
'nullableClassStringEqualsNull' => ['null', new IsIdentical(new TNull()), 'class-string<SomeClass>|null'],
|
|
'nullableClassStringTruthy' => ['class-string<SomeClass>', new Truthy(), 'class-string<SomeClass>|null'],
|
|
'iterableToArray' => ['array<int, int>', new IsType(new TArray([Type::getArrayKey(), Type::getMixed()])), 'iterable<int, int>'],
|
|
'iterableToTraversable' => ['Traversable<int, int>', new IsType(new TNamedObject('Traversable')), 'iterable<int, int>'],
|
|
'callableToCallableArray' => ['callable-array{0: class-string|object, 1: string}', new IsType(new TArray([Type::getArrayKey(), Type::getMixed()])), 'callable'],
|
|
'SmallKeyedArrayAndCallable' => ['array{test: string}', new IsType(new TKeyedArray(['test' => Type::getString()])), 'callable'],
|
|
'BigKeyedArrayAndCallable' => ['array{foo: string, test: string, thing: string}', new IsType(new TKeyedArray(['foo' => Type::getString(), 'test' => Type::getString(), 'thing' => Type::getString()])), 'callable'],
|
|
'callableOrArrayToCallableArray' => ['array<array-key, mixed>', new IsType(new TArray([Type::getArrayKey(), Type::getMixed()])), 'callable|array'],
|
|
'traversableToIntersection' => ['Countable&Traversable', new IsType(new TNamedObject('Traversable')), 'Countable'],
|
|
'iterableWithoutParamsToTraversableWithoutParams' => ['Traversable', new IsNotType(new TArray([Type::getArrayKey(), Type::getMixed()])), 'iterable'],
|
|
'iterableWithParamsToTraversableWithParams' => ['Traversable<int, string>', new IsNotType(new TArray([Type::getArrayKey(), Type::getMixed()])), 'iterable<int, string>'],
|
|
'iterableAndObject' => ['Traversable<int, string>', new IsType(new TObject()), 'iterable<int, string>'],
|
|
'iterableAndNotObject' => ['array<int, string>', new IsNotType(new TObject()), 'iterable<int, string>'],
|
|
'boolNotEmptyIsTrue' => ['true', new NonEmpty(), 'bool'],
|
|
'interfaceAssertionOnClassInterfaceUnion' => ['SomeInterface|SomeInterface&SomeClass', new IsType(new TNamedObject('SomeInterface')), 'SomeClass|SomeInterface'],
|
|
'classAssertionOnClassInterfaceUnion' => ['SomeClass|SomeClass&SomeInterface', new IsType(new TNamedObject('SomeClass')), 'SomeClass|SomeInterface'],
|
|
'stringToNumericStringWithInt' => ['numeric-string', new IsLooselyEqual(new TInt()), 'string'],
|
|
'stringToNumericStringWithFloat' => ['numeric-string', new IsLooselyEqual(new TFloat()), 'string'],
|
|
'filterKeyedArrayWithIterable' => ['array{some: string}',new IsType(new TIterable([Type::getMixed(), Type::getString()])), 'array{some: mixed}'],
|
|
'SimpleXMLElementNotAlwaysTruthy' => ['SimpleXMLElement', new Truthy(), 'SimpleXMLElement'],
|
|
'SimpleXMLElementNotAlwaysTruthy2' => ['SimpleXMLElement', new Falsy(), 'SimpleXMLElement'],
|
|
'SimpleXMLIteratorNotAlwaysTruthy' => ['SimpleXMLIterator', new Truthy(), 'SimpleXMLIterator'],
|
|
'SimpleXMLIteratorNotAlwaysTruthy2' => ['SimpleXMLIterator', new Falsy(), 'SimpleXMLIterator'],
|
|
'stringWithAny' => ['string', new Any(), 'string'],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string,array{string,string}>
|
|
*/
|
|
public function providerTestTypeIsContainedBy(): array
|
|
{
|
|
return [
|
|
'arrayContainsWithArrayOfStrings' => ['array<string>', 'array'],
|
|
'arrayContainsWithArrayOfExceptions' => ['array<Exception>', 'array'],
|
|
'arrayOfIterable' => ['array', 'iterable'],
|
|
'arrayOfIterableWithType' => ['array<SomeClass>', 'iterable<SomeClass>'],
|
|
'arrayOfIterableWithSubclass' => ['array<SomeChildClass>', 'iterable<SomeClass>'],
|
|
'arrayOfSubclassOfParent' => ['array<SomeChildClass>', 'array<SomeClass>'],
|
|
'subclassOfParent' => ['SomeChildClass', 'SomeClass'],
|
|
'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<never, never>',
|
|
'array{a?: string, b?: string}',
|
|
],
|
|
'literalNumericStringInt' => [
|
|
'"0"',
|
|
'numeric',
|
|
],
|
|
'literalNumericString' => [
|
|
'"10.03"',
|
|
'numeric',
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider constantAssertions
|
|
*/
|
|
public function testReconciliationOfClassConstantInAssertions(Assertion $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();
|
|
|
|
$reconciled = AssertionReconciler::reconcile(
|
|
$assertion,
|
|
new Union([
|
|
new TLiteralString(''),
|
|
]),
|
|
null,
|
|
$this->statements_analyzer,
|
|
false,
|
|
[]
|
|
);
|
|
|
|
$this->assertSame(
|
|
$expected_type,
|
|
$reconciled->getId()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array<non-empty-string,array{Assertion,string}>
|
|
*/
|
|
public function constantAssertions(): array
|
|
{
|
|
return [
|
|
'constant-with-prefix' => [
|
|
new IsType(new TClassConstant('ReconciliationTest\\Foo', 'PREFIX_*')),
|
|
"'bar'|'baz'",
|
|
],
|
|
'single-class-constant' => [
|
|
new IsType(new TClassConstant('ReconciliationTest\\Foo', 'PREFIX_BAR')),
|
|
"'bar'",
|
|
],
|
|
'referencing-another-class-constant' => [
|
|
new IsType(new TClassConstant('ReconciliationTest\\Foo', 'PREFIX_QOO')),
|
|
"'bar'",
|
|
],
|
|
'referencing-all-class-constants' => [
|
|
new IsType(new TClassConstant('ReconciliationTest\\Foo', '*')),
|
|
"'bar'|'baz'",
|
|
],
|
|
'referencing-some-class-constants-with-wildcard' => [
|
|
new IsType(new TClassConstant('ReconciliationTest\\Foo', 'PREFIX_B*')),
|
|
"'bar'|'baz'",
|
|
],
|
|
];
|
|
}
|
|
}
|