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); } /** * @dataProvider providerTestReconcilation * * @param string $expected * @param string $type * @param string $string * * @return void */ public function testReconcilation($expected, $type, $string) { $reconciled = Reconciler::reconcileTypes( $type, Type::parseString($string), null, $this->statements_analyzer, false, [] ); $this->assertSame( $expected, $reconciled->getId() ); if (is_array($reconciled->getTypes())) { $this->assertContainsOnlyInstancesOf('Psalm\Type\Atomic', $reconciled->getTypes()); } } /** * @dataProvider providerTestTypeIsContainedBy * * @param string $input * @param string $container * * @return void */ public function testTypeIsContainedBy($input, $container) { $this->assertTrue( TypeAnalyzer::isContainedBy( $this->project_analyzer->getCodebase(), Type::parseString($input), Type::parseString($container) ) ); } /** * @return void */ public function testNegateFormula() { $formula = [ new Clause(['$a' => ['!falsy']]), ]; $negated_formula = Algebra::negateFormula($formula); $this->assertSame(1, count($negated_formula)); $this->assertSame(['$a' => ['falsy']], $negated_formula[0]->possibilities); $formula = [ new Clause(['$a' => ['!falsy'], '$b' => ['!falsy']]), ]; $negated_formula = Algebra::negateFormula($formula); $this->assertSame(2, count($negated_formula)); $this->assertSame(['$a' => ['falsy']], $negated_formula[0]->possibilities); $this->assertSame(['$b' => ['falsy']], $negated_formula[1]->possibilities); $formula = [ new Clause(['$a' => ['!falsy']]), new Clause(['$b' => ['!falsy']]), ]; $negated_formula = Algebra::negateFormula($formula); $this->assertSame(1, count($negated_formula)); $this->assertSame(['$a' => ['falsy'], '$b' => ['falsy']], $negated_formula[0]->possibilities); $formula = [ new Clause(['$a' => ['int', 'string'], '$b' => ['!falsy']]), ]; $negated_formula = Algebra::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' => ['falsy']], $negated_formula[2]->possibilities); } /** * @return void */ public function testContainsClause() { $this->assertTrue( (new Clause( [ '$a' => ['!falsy'], '$b' => ['!falsy'], ] ))->contains( new Clause( [ '$a' => ['!falsy'], ] ) ) ); $this->assertFalse( (new Clause( [ '$a' => ['!falsy'], ] ))->contains( new Clause( [ '$a' => ['!falsy'], '$b' => ['!falsy'], ] ) ) ); } /** * @return void */ public function testSimplifyCNF() { $formula = [ new Clause(['$a' => ['!falsy']]), new Clause(['$a' => ['falsy'], '$b' => ['falsy']]), ]; $simplified_formula = Algebra::simplifyCNF($formula); $this->assertSame(2, count($simplified_formula)); $this->assertSame(['$a' => ['!falsy']], $simplified_formula[0]->possibilities); $this->assertSame(['$b' => ['falsy']], $simplified_formula[1]->possibilities); } /** * @return array */ public function providerTestReconcilation() { return [ 'notNullWithObject' => ['MyObject', '!null', 'MyObject'], 'notNullWithObjectPipeNull' => ['MyObject', '!null', 'MyObject|null'], 'notNullWithMyObjectPipeFalse' => ['false|MyObject', '!null', 'MyObject|false'], 'notNullWithMixed' => ['mixed', '!null', 'mixed'], 'notEmptyWithMyObject' => ['MyObject', '!falsy', 'MyObject'], 'notEmptyWithMyObjectPipeNull' => ['MyObject', '!falsy', 'MyObject|null'], 'notEmptyWithMyObjectPipeFalse' => ['MyObject', '!falsy', 'MyObject|false'], 'notEmptyWithMixed' => ['non-empty-mixed', '!falsy', 'mixed'], // @todo in the future this should also work //'notEmptyWithMyObjectFalseTrue' => ['MyObject|true', '!falsy', 'MyObject|bool'], 'nullWithMyObjectPipeNull' => ['null', 'null', 'MyObject|null'], 'nullWithMixed' => ['null', 'null', 'mixed'], 'falsyWithMyObject' => ['mixed', 'falsy', 'MyObject'], 'falsyWithMyObjectPipeFalse' => ['false', 'falsy', 'MyObject|false'], 'falsyWithMyObjectPipeBool' => ['false', 'falsy', 'MyObject|bool'], 'falsyWithMixed' => ['empty-mixed', 'falsy', 'mixed'], 'falsyWithBool' => ['false', 'falsy', 'bool'], 'falsyWithStringOrNull' => ['null|string()|string(0)', 'falsy', 'string|null'], 'falsyWithScalarOrNull' => ['empty-scalar', 'falsy', 'scalar'], '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' => ['numeric-string', 'numeric', 'string'], 'nullableClassString' => ['null', 'falsy', '?class-string'], 'mixedOrNullNotFalsy' => ['non-empty-mixed', '!falsy', 'mixed|null'], 'mixedOrNullFalsy' => ['null|empty-mixed', 'falsy', 'mixed|null'], 'nullableClassStringFalsy' => ['null', 'falsy', 'class-string|null'], 'nullableClassStringEqualsNull' => ['null', '=null', 'class-string|null'], 'nullableClassStringTruthy' => ['class-string', '!falsy', 'class-string|null'], ]; } /** * @return array */ public function providerTestTypeIsContainedBy() { return [ 'arrayContainsWithArrayOfStrings' => ['array', 'array'], 'arrayContainsWithArrayOfExceptions' => ['array', 'array'], '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}', ], ]; } /** * @return iterable,error_levels?:string[]}> */ public function providerValidCodeParse() { return [ 'intIsMixed' => [ ' [ ' [], 'error_levels' => ['RedundantConditionGivenDocblockType'], ], 'arrayTypeResolutionFromDocblock' => [ ' [], 'error_levels' => ['RedundantConditionGivenDocblockType'], ], 'typeResolutionFromDocblockInside' => [ ' [], 'error_levels' => ['DocblockTypeContradiction'], ], 'notInstanceof' => [ ' [ '$out' => 'null|A', ], ], 'notInstanceOfProperty' => [ 'foo = new B(); } } $a = new A(); $out = null; if ($a->foo instanceof C) { // do something } else { $out = $a->foo; }', 'assertions' => [ '$out' => 'null|B', ], 'error_levels' => [], ], 'notInstanceOfPropertyElseif' => [ 'foo)) { } elseif ($a->foo instanceof C) { // do something } else { $out = $a->foo; }', 'assertions' => [ '$out' => 'null|B', ], 'error_levels' => [], ], 'typeArguments' => [ ' [ '$a' => 'int', '$b' => 'int', '$c' => 'string', '$hours' => 'string|int|float|null', '$minutes' => 'string|int|float|null', '$seconds' => 'string|int|float|null', ], ], 'typeRefinementWithIsNumericOnIntOrFalse' => [ ' [ ' [ ' 4 ? "hello" : 5; if (is_numeric($a)) { exit; }', 'assertions' => [ '$a' => 'string', ], ], 'typeRefinementWithStringOrTrue' => [ ' 4 ? "hello" : true; if (is_bool($a)) { exit; }', 'assertions' => [ '$a' => 'string', ], ], 'updateMultipleIssetVars' => [ ' [ ' [ ' [ ' [], 'error_levels' => ['RedundantConditionGivenDocblockType'], ], 'ignoreNullCheckAndMaintainNullValue' => [ ' [ '$b' => 'null', ], 'error_levels' => ['TypeDoesNotContainType', 'RedundantCondition'], ], 'ignoreNullCheckAndMaintainNullableValue' => [ ' [ '$b' => 'int|null', ], ], 'ternaryByRefVar' => [ ' [ ' [ ' [ 'bar(); $this->bat(); takesA($this); takesI($this); takesAandI($this); takesIandA($this); } } protected function bar(): void {} } class B extends A implements I { public function bat(): void {} }', ], 'createIntersectionOfInterfaceAndClass' => [ 'bat(); $i->baz(); } } function bar(A $a) : void { if ($a instanceof I) { $a->bat(); $a->baz(); } } class B extends A implements I { public function baz() : void {} } foo(new B); bar(new B);', ], 'unionOfArrayOrTraversable' => [ ' [ ' [ ' [ ' [ ' [ ' 2) { $a = "hello"; } else { $a = false; } } return $a; }', ], 'nullableIntReplacement' => [ ' [ '$a' => 'int|null', ], ], 'eraseNullAfterInequalityCheck' => [ ' 0) { echo $a + 3; } if (0 < $a) { echo $a + 3; }', ], 'twoWrongsDontMakeARight' => [ ' [ '$a' => 'false', ], ], 'instanceofStatic' => [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ 'foo(); }', ], 'SKIPPED-isArrayOnArrayKeyOffset' => [ '|string>} */ $doc = []; if (!is_array($doc["s"]["t"])) { $doc["s"]["t"] = [$doc["s"]["t"]]; }', 'assertions' => [ '$doc[\'s\'][\'t\']' => 'array', ], ], 'removeTrue' => [ ' [ ' [ ' [ '$a' => 'null', ], ], 'removeNullWithIsScalar' => [ ' [ '$a' => 'string', ], ], 'scalarToNumeric' => [ ' [ ' [ ' [ ' [ '|null $foo */ function d(?iterable $foo): void { if (is_iterable($foo)) { foreach ($foo as $f) {} } if (!is_iterable($foo)) { } else { foreach ($foo as $f) {} } }', ], 'isStringServerVar' => [ ' [ ' [ 'b())) {} }', ], 'reconcileFloatToEmpty' => [ ' [ ' [ ' $v */ function foo(array $v) : void { if (!isset($v[0])) { return; } if ($v[0] === " ") { array_shift($v); } if (!isset($v[0])) {} }' ], 'arrayEquality' => [ '> $haystack * @param array $needle */ function foo(array $haystack, array $needle) : void { foreach ($haystack as $arr) { if ($arr === $needle) {} } }', ], 'classResolvesBackToSelfAfterComparison' => [ ' [ '$a' => 'A', ] ], 'isNumericCanBeScalar' => [ ' [ '|null $val */ function foo(?string $val) : void { if (!$val) {} if ($val) {} }', ], 'allowStringToObjectReconciliation' => [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' $a * @return array{from:bool, to: bool} */ function foo(array $a) : array { return $a; }', ], 'dontChangeScalar' => [ ' [ ' [ ' */ public function providerInvalidCodeParse() { 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', ], 'typeTransformation' => [ ' 'TypeDoesNotContainType', ], 'dontEraseNullAfterLessThanCheck' => [ ' 'PossiblyNullOperand', ], 'dontEraseNullAfterGreaterThanCheck' => [ ' $a) { echo $a + 3; }', 'error_message' => 'PossiblyNullOperand', ], 'nonRedundantConditionGivenDocblockType' => [ ' 'TypeDoesNotContainType', ], 'lessSpecificArrayFields' => [ ' "name"]);', 'error_message' => 'InvalidArgument', ], 'intersectionIncorrect' => [ ' 'InvalidArgument', ], 'catchTypeMismatchInBinaryOp' => [ ' */ function getStrings(): array { return ["hello", "world", 50]; } $a = getStrings(); if (is_bool($a[0]) && $a[0]) {}', 'error_message' => 'DocblockTypeContradiction', ], 'preventWeakEqualityToObject' => [ ' 'TypeDoesNotContainType', ], 'properReconciliationInElseIf' => [ ' 'RedundantCondition', ], 'allRemovalOfStringWithIsScalar' => [ ' 'RedundantCondition', ], 'noRemovalOfStringWithIsScalar' => [ ' 'TypeDoesNotContainType', ], 'impossibleNullEquality' => [ ' 'TypeDoesNotContainNull', ], 'impossibleTrueEquality' => [ ' 'TypeDoesNotContainType', ], 'impossibleFalseEquality' => [ ' 'TypeDoesNotContainType', ], 'impossibleNumberEquality' => [ ' 'TypeDoesNotContainType', ], 'SKIPPED-noIntersectionOfArrayOrTraversable' => [ ' 'TypeDoesNotContainType', ], 'scalarToBoolContradiction' => [ ' 'ParadoxicalCondition', ], 'noCrashWhenCastingArray' => [ ' 1, "b" => 2]; }', 'error_message' => 'InvalidReturnStatement', ], 'preventStrongEqualityScalarType' => [ ' 'TypeDoesNotContainType', ], 'preventYodaStrongEqualityScalarType' => [ ' 'TypeDoesNotContainType', ], 'classCannotNotBeSelf' => [ ' 'RedundantCondition' ], ]; } }