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', ' project_analyzer->getCodebase()->scanFiles(); } /** * @dataProvider providerTestReconcilation */ public function testReconcilation(string $expected_type, string $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 */ public function providerTestReconcilation(): array { return [ 'notNullWithObject' => ['SomeClass', '!null', 'SomeClass'], 'notNullWithObjectPipeNull' => ['SomeClass', '!null', 'SomeClass|null'], 'notNullWithSomeClassPipeFalse' => ['SomeClass|false', '!null', 'SomeClass|false'], 'notNullWithMixed' => ['mixed', '!null', 'mixed'], 'notEmptyWithSomeClass' => ['SomeClass', '!falsy', 'SomeClass'], 'notEmptyWithSomeClassPipeNull' => ['SomeClass', '!falsy', 'SomeClass|null'], 'notEmptyWithSomeClassPipeFalse' => ['SomeClass', '!falsy', 'SomeClass|false'], 'notEmptyWithMixed' => ['non-empty-mixed', '!falsy', 'mixed'], // @todo in the future this should also work //'notEmptyWithSomeClassFalseTrue' => ['SomeClass|true', '!falsy', 'SomeClass|bool'], 'nullWithSomeClassPipeNull' => ['null', 'null', 'SomeClass|null'], 'nullWithMixed' => ['null', 'null', 'mixed'], 'falsyWithSomeClass' => ['never', 'falsy', 'SomeClass'], 'falsyWithSomeClassPipeFalse' => ['false', 'falsy', 'SomeClass|false'], 'falsyWithSomeClassPipeBool' => ['false', 'falsy', 'SomeClass|bool'], 'falsyWithMixed' => ['empty-mixed', 'falsy', 'mixed'], 'falsyWithBool' => ['false', 'falsy', 'bool'], 'falsyWithStringOrNull' => ['""|"0"|null', 'falsy', 'string|null'], 'falsyWithScalarOrNull' => ['empty-scalar', 'falsy', 'scalar'], 'notSomeClassWithSomeClassPipeBool' => ['bool', '!SomeClass', 'SomeClass|bool'], 'notSomeClassWithSomeClassPipeNull' => ['null', '!SomeClass', 'SomeClass|null'], 'notSomeClassWithAPipeB' => ['B', '!A', 'A|B'], 'notDateTimeWithDateTimeInterface' => ['DateTimeImmutable', '!DateTime', 'DateTimeInterface'], 'notDateTimeImmutableWithDateTimeInterface' => ['DateTime', '!DateTimeImmutable', 'DateTimeInterface'], 'myObjectWithSomeClassPipeBool' => ['SomeClass', 'SomeClass', 'SomeClass|bool'], 'myObjectWithAPipeB' => ['A', 'A', 'A|B'], 'array' => ['array', 'array', 'array|null'], '2dArray' => ['array>', 'array', 'array>|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'], 'nullableClassStringFalsy' => ['null', 'falsy', 'class-string|null'], 'nullableClassStringEqualsNull' => ['null', '=null', 'class-string|null'], 'nullableClassStringTruthy' => ['class-string', '!falsy', 'class-string|null'], 'iterableToArray' => ['array', 'array', 'iterable'], 'iterableToTraversable' => ['Traversable', 'Traversable', 'iterable'], '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'], 'callableOrArrayToCallableArray' => ['array', 'array', 'callable|array'], 'traversableToIntersection' => ['Countable&Traversable', 'Traversable', 'Countable'], 'iterableWithoutParamsToTraversableWithoutParams' => ['Traversable', '!array', 'iterable'], 'iterableWithParamsToTraversableWithParams' => ['Traversable', '!array', 'iterable'], 'iterableAndObject' => ['Traversable', 'object', 'iterable'], 'iterableAndNotObject' => ['array', '!object', 'iterable'], 'boolNotEmptyIsTrue' => ['true', '!empty', 'bool'], 'interfaceAssertionOnClassInterfaceUnion' => ['SomeInterface|SomeInterface&SomeClass', 'SomeInterface', 'SomeClass|SomeInterface'], 'classAssertionOnClassInterfaceUnion' => ['SomeClass|SomeClass&SomeInterface', 'SomeClass', 'SomeClass|SomeInterface'], 'stringToNumericStringWithInt' => ['numeric-string', '~int', 'string'], 'stringToNumericStringWithFloat' => ['numeric-string', '~float', 'string'], 'filterKeyedArrayWithIterable' => ['array{some: string}', 'iterable', 'array{some: mixed}'], 'SimpleXMLElementNotAlwaysTruthy' => ['SimpleXMLElement', '!falsy', 'SimpleXMLElement'], 'SimpleXMLElementNotAlwaysTruthy2' => ['SimpleXMLElement', 'falsy', 'SimpleXMLElement'], 'SimpleXMLIteratorNotAlwaysTruthy' => ['SimpleXMLIterator', '!falsy', 'SimpleXMLIterator'], 'SimpleXMLIteratorNotAlwaysTruthy2' => ['SimpleXMLIterator', 'falsy', 'SimpleXMLIterator'], ]; } /** * @return array */ public function providerTestTypeIsContainedBy(): array { return [ 'arrayContainsWithArrayOfStrings' => ['array', 'array'], 'arrayContainsWithArrayOfExceptions' => ['array', 'array'], 'arrayOfIterable' => ['array', 'iterable'], 'arrayOfIterableWithType' => ['array', 'iterable'], 'arrayOfIterableWithSubclass' => ['array', 'iterable'], 'arrayOfSubclassOfParent' => ['array', 'array'], 'subclassOfParent' => ['SomeChildClass', 'SomeClass'], 'unionContainsWithstring' => ['string', 'string|false'], 'unionContainsWithFalse' => ['false', 'string|false'], 'objectLikeTypeWithPossiblyUndefinedToGeneric' => [ 'array{0: array{a: string}, 1: array{c: string, e: string}}', 'array>', ], 'objectLikeTypeWithPossiblyUndefinedToEmpty' => [ 'array', 'array{a?: string, b?: string}', ], 'literalNumericStringInt' => [ '"0"', 'numeric', ], 'literalNumericString' => [ '"10.03"', 'numeric', ], ]; } /** * @dataProvider constantAssertions */ public function testReconciliationOfClassConstantInAssertions(string $assertion, string $expected_type): void { $this->addFile( 'psalm-assert.php', ' 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 */ 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"', ], ]; } }