mirror of
https://github.com/danog/psalm.git
synced 2024-12-03 18:17:55 +01:00
Daniil Gentili
1986c8b4a8
* 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 :)
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,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'",
|
|
],
|
|
];
|
|
}
|
|
}
|