1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-03 18:17:55 +01:00
psalm/tests/TypeReconciliation/ReconcilerTest.php
Daniil Gentili 1986c8b4a8
Add support for strict arrays, fix type alias intersection, fix array_is_list assertion on non-lists (#8395)
* Immutable CodeLocation

* Remove excess clones

* Remove external clones

* Remove leftover clones

* Fix final clone issue

* Immutable storages

* Refactoring

* Fixes

* Fixes

* Fix

* Fix

* Fixes

* Simplify

* Fixes

* Fix

* Fixes

* Update

* Fix

* Cache global types

* Fix

* Update

* Update

* Fixes

* Fixes

* Refactor

* Fixes

* Fix

* Fix

* More caching

* Fix

* Fix

* Update

* Update

* Fix

* Fixes

* Update

* Refactor

* Update

* Fixes

* Break one more test

* Fix

* FIx

* Fix

* Fix

* Fix

* Fix

* Improve performance and readability

* Equivalent logic

* Fixes

* Revert

* Revert "Revert"

This reverts commit f9175100c8452c80559234200663fd4c4f4dd889.

* Fix

* Fix reference bug

* Make default TypeVisitor immutable

* Bugfix

* Remove clones

* Partial refactoring

* Refactoring

* Fixes

* Fix

* Fixes

* Fixes

* cs-fix

* Fix final bugs

* Add test

* Misc fixes

* Update

* Fixes

* Experiment with removing different property

* revert "Experiment with removing different property"

This reverts commit ac1156e077fc4ea633530d51096d27b6e88bfdf9.

* Uniform naming

* Uniform naming

* Hack hotfix

* Clean up $_FILES ref #8621

* Undo hack, try fixing properly

* Helper method

* Remove redundant call

* Partially fix bugs

* Cleanup

* Change defaults

* Fix bug

* Fix (?, hope this doesn't break anything else)

* cs-fix

* Review fixes

* Bugfix

* Bugfix

* Improve logic

* Add support for list{} and callable-list{} types, properly implement array_is_list assertions (fixes #8389)

* Default to sealed arrays

* Fix array_merge bug

* Fixes

* Fix

* Sealed type checks

* Properly infer properties-of and get_object_vars on final classes

* Fix array_map zipping

* Fix tests

* Fixes

* Fixes

* Fix more stuff

* Recursively resolve type aliases

* Fix typo

* Fixes

* Fix array_is_list assertion on keyed array

* Add BC docs

* Fixes

* fix

* Update

* Update

* Update

* Update

* Seal arrays with count assertions

* Fix #8528

* Fix

* Update

* Improve sealed array foreach logic

* get_object_vars on template properties

* Fix sealed array assertion reconciler logic

* Improved reconciler

* Add tests

* Single source of truth for test types

* Fix tests

* Fixup tests

* Fixup tests

* Fixup tests

* Update

* Fix tests

* Fix tests

* Final fixes

* Fixes

* Use list syntax only when needed

* Fix tests

* Cs-fix

* Update docs

* Update docs

* Update docs

* Update docs

* Update docs

* Document missing types

* Update docs

* Improve class-string-map docs

* Update

* Update

* I love working on psalm :)

* Keep arrays unsealed by default

* Fixup tests

* Fix syntax mistake

* cs-fix

* Fix typo

* Re-import missing types

* Keep strict types only in return types

* argc/argv fixes

* argc/argv fixes

* Fix test

* Comment-out valinor code, pinging @romm pls merge https://github.com/CuyZ/Valinor/pull/246 so we can add valinor to the psalm docs :)
2022-11-05 22:34:42 +01:00

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,strict-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' => ['strict-array{some: string}',new IsType(new TIterable([Type::getMixed(), Type::getString()])), 'strict-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,strict-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' => [
'strict-array{0: strict-array{a: string}, 1: strict-array{c: string, e: string}}',
'array<int, array<string, string>>',
],
'objectLikeTypeWithPossiblyUndefinedToEmpty' => [
'array<never, never>',
'strict-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,strict-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'",
],
];
}
}