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()->queueClassLikeForScanning(Countable::class); $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 */ public function testTypeIsContainedBy(string $input, string $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', 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', new IsType(new TArray([Type::getArrayKey(), Type::getMixed()])), 'array|null'], '2dArray' => ['array>', new IsType(new TArray([Type::getArrayKey(), Type::getMixed()])), 'array>|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|null'], 'nullableClassStringEqualsNull' => ['null', new IsIdentical(new TNull()), 'class-string|null'], 'nullableClassStringTruthy' => ['class-string', new Truthy(), 'class-string|null'], 'iterableToArray' => ['array', new IsType(new TArray([Type::getArrayKey(), Type::getMixed()])), 'iterable'], 'iterableToTraversable' => ['Traversable', new IsType(new TNamedObject('Traversable')), 'iterable'], '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', 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', new IsNotType(new TArray([Type::getArrayKey(), Type::getMixed()])), 'iterable'], 'iterableAndObject' => ['Traversable', new IsType(new TObject()), 'iterable'], 'iterableAndNotObject' => ['array', new IsNotType(new TObject()), 'iterable'], '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'], 'IsNotAClassReconciliation' => ['int', new Assertion\IsNotAClass(new TNamedObject('IDObject'), true), 'int|IDObject'], 'nonEmptyArray' => ['non-empty-array', new IsType(Atomic::create('non-empty-array')), 'array'], 'nonEmptyList' => ['non-empty-list', new IsType(Atomic::create('non-empty-list')), 'array'], 'ListOfInts' => ['list', new IsType(new TIterable([Type::getMixed(), Type::getInt()])), 'list'], ]; } /** * @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(Assertion $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' => [ 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'", ], ]; } }