file_checker = new FileChecker('somefile.php', $this->project_checker); $this->file_checker->context = new Context(); } /** * @dataProvider providerTestReconcilation * * @param string $expected * @param string $type * @param string $string * * @return void */ public function testReconcilation($expected, $type, $string) { $reconciled = TypeChecker::reconcileTypes( $type, Type::parseString($string), null, $this->file_checker ); $this->assertSame( $expected, (string) $reconciled ); if ($reconciled && is_array($reconciled->types)) { foreach ($reconciled->types as $type) { $this->assertInstanceOf('Psalm\Type\Atomic', $type); } } } /** * @dataProvider providerTestTypeIsContainedBy * * @param string $input * @param string $container * * @return void */ public function testTypeIsContainedBy($input, $container) { $this->assertTrue( TypeChecker::isContainedBy( Type::parseString($input), Type::parseString($container), $this->file_checker ) ); } /** * @return void */ public function testNegateFormula() { $formula = [ new Clause(['$a' => ['!empty']]), ]; $negated_formula = AlgebraChecker::negateFormula($formula); $this->assertSame(1, count($negated_formula)); $this->assertSame(['$a' => ['empty']], $negated_formula[0]->possibilities); $formula = [ new Clause(['$a' => ['!empty'], '$b' => ['!empty']]), ]; $negated_formula = AlgebraChecker::negateFormula($formula); $this->assertSame(2, count($negated_formula)); $this->assertSame(['$a' => ['empty']], $negated_formula[0]->possibilities); $this->assertSame(['$b' => ['empty']], $negated_formula[1]->possibilities); $formula = [ new Clause(['$a' => ['!empty']]), new Clause(['$b' => ['!empty']]), ]; $negated_formula = AlgebraChecker::negateFormula($formula); $this->assertSame(1, count($negated_formula)); $this->assertSame(['$a' => ['empty'], '$b' => ['empty']], $negated_formula[0]->possibilities); $formula = [ new Clause(['$a' => ['int', 'string'], '$b' => ['!empty']]), ]; $negated_formula = AlgebraChecker::negateFormula($formula); $this->assertSame(3, count($negated_formula)); $this->assertSame(['$a' => ['!int']], $negated_formula[0]->possibilities); $this->assertSame(['$a' => ['!string']], $negated_formula[1]->possibilities); $this->assertSame(['$b' => ['empty']], $negated_formula[2]->possibilities); } /** * @return void */ public function testContainsClause() { $this->assertTrue( (new Clause( [ '$a' => ['!empty'], '$b' => ['!empty'], ] ))->contains( new Clause( [ '$a' => ['!empty'], ] ) ) ); $this->assertFalse( (new Clause( [ '$a' => ['!empty'], ] ))->contains( new Clause( [ '$a' => ['!empty'], '$b' => ['!empty'], ] ) ) ); } /** * @return void */ public function testSimplifyCNF() { $formula = [ new Clause(['$a' => ['!empty']]), new Clause(['$a' => ['empty'], '$b' => ['empty']]), ]; $simplified_formula = AlgebraChecker::simplifyCNF($formula); $this->assertSame(2, count($simplified_formula)); $this->assertSame(['$a' => ['!empty']], $simplified_formula[0]->possibilities); $this->assertSame(['$b' => ['empty']], $simplified_formula[1]->possibilities); } /** * @return array */ public function providerTestReconcilation() { return [ 'notNullWithObject' => ['MyObject', '!null', 'MyObject'], 'notNullWithObjectPipeNull' => ['MyObject', '!null', 'MyObject|null'], 'notNullWithMyObjectPipeFalse' => ['MyObject|false', '!null', 'MyObject|false'], 'notNullWithMixed' => ['mixed', '!null', 'mixed'], 'notEmptyWithMyObject' => ['MyObject', '!empty', 'MyObject'], 'notEmptyWithMyObjectPipeNull' => ['MyObject', '!empty', 'MyObject|null'], 'notEmptyWithMyObjectPipeFalse' => ['MyObject', '!empty', 'MyObject|false'], 'notEmptyWithMixed' => ['mixed', '!empty', 'mixed'], // @todo in the future this should also work //'notEmptyWithMyObjectFalseTrue' => ['MyObject|true', '!empty', 'MyObject|bool'], 'notEmptyWithMyObjectPipeNull' => ['null', 'null', 'MyObject|null'], 'notEmptyWithMixed' => ['null', 'null', 'mixed'], 'emptyWithMyObject' => ['null', 'empty', 'MyObject'], 'emptyWithMyObjectPipeFalse' => ['false', 'empty', 'MyObject|false'], 'emptyWithMyObjectPipeBool' => ['false', 'empty', 'MyObject|bool'], 'emptyWithMixed' => ['mixed', 'empty', 'mixed'], 'emptyWithBool' => ['false', 'empty', 'bool'], 'notMyObjectWithMyObjectPipeBool' => ['bool', '!MyObject', 'MyObject|bool'], 'notMyObjectWithMyObjectPipeNull' => ['null', '!MyObject', 'MyObject|null'], 'notMyObjectWithMyObjectAPipeMyObjectB' => ['MyObjectB', '!MyObjectA', 'MyObjectA|MyObjectB'], 'myObjectWithMyObjectPipeBool' => ['MyObject', 'MyObject', 'MyObject|bool'], 'myObjectWithMyObjectAPipeMyObjectB' => ['MyObjectA', 'MyObjectA', 'MyObjectA|MyObjectB'], 'array' => ['array', 'array', 'array|null'], '2dArray' => ['array>', 'array', 'array>|null'], 'numeric' => ['string', 'numeric', 'string'], ]; } /** * @return array */ public function providerTestTypeIsContainedBy() { return [ 'arrayContainsWithArrayOfStrings' => ['array', 'array'], 'arrayContainsWithArrayOfExceptions' => ['array', 'array'], 'unionContainsWithstring' => ['string', 'string|false'], 'unionContainsWithFalse' => ['false', 'string|false'], ]; } /** * @return array */ public function providerFileCheckerValidCodeParse() { return [ 'intIsMixed' => [ ' [ ' [ ' [ ' [ ' [ ['null|A' => '$out'], ], 'error_levels' => [], 'scope_vars' => [ '$a' => Type::parseString('A'), ], ], 'notInstanceOfProperty' => [ 'foo = new B(); } } $a = new A(); $out = null; if ($a->foo instanceof C) { // do something } else { $out = $a->foo; }', 'assertions' => [ ['null|B' => '$out'], ], 'error_levels' => [], 'scope_vars' => [ '$a' => Type::parseString('A'), ], ], 'notInstanceOfPropertyElseif' => [ 'foo)) { } elseif ($a->foo instanceof C) { // do something } else { $out = $a->foo; }', 'assertions' => [ ['null|B' => '$out'], ], 'error_levels' => [], 'scope_vars' => [ '$a' => Type::parseString('A'), ], ], 'typeArguments' => [ ' [ ['int' => '$a'], ['int' => '$b'], ['string' => '$c'], ['string|int|float' => '$hours'], ['string|int|float' => '$minutes'], ['string|int|float' => '$seconds'], ], ], 'typeRefinementWithIsNumeric' => [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ 'getMessage() === "hello") { return "hello"; } elseif (empty($a)) { return "goodbye"; } return $a->getMessage(); }', ], 'typeReconciliationAfterIfAndReturn' => [ ' [ ' [ ['null' => '$b'], ], 'error_levels' => ['FailedTypeResolution'], ], 'ignoreNullCheckAndMaintainNullableValue' => [ ' [ ['int|null' => '$b'], ], ], 'ternaryByRefVar' => [ ' [ ' [ ' [ 'bar(); $this->bat(); takesA($this); takesI($this); } } protected function bar() : void {} } class B extends A implements I { public function bat() : void {} }', ], ]; } /** * @return array */ public function providerFileCheckerInvalidCodeParse() { return [ 'makeNonNullableNull' => [ ' 'TypeDoesNotContainNull', ], 'makeInstanceOfThingInElseif' => [ ' 5 ? new A() : new B(); if ($a instanceof A) { } elseif ($a instanceof C) { }', 'error_message' => 'TypeDoesNotContainType', ], 'functionValueIsNotType' => [ ' 'TypeDoesNotContainType', ], 'stringIsNotTnt' => [ ' 'TypeDoesNotContainType', ], 'stringIsNotNull' => [ ' 'TypeDoesNotContainNull', ], 'stringIsNotFalse' => [ ' 'TypeDoesNotContainType', ], 'failedTypeResolution' => [ ' 'FailedTypeResolution', ], 'failedTypeResolutionWithDocblock' => [ ' 'FailedTypeResolution', ], 'typeResolutionFromDocblockAndInstanceof' => [ ' 'FailedTypeResolution', ], 'typeTransformation' => [ ' 'TypeDoesNotContainType', ], ]; } }