addFile( 'somefile.php', ' 'GMP', '$b' => 'GMP', '$c' => 'GMP', '$d' => 'GMP', '$f' => 'GMP', '$g' => 'GMP', '$h' => 'GMP', '$i' => 'GMP', '$j' => 'GMP', '$k' => 'GMP', '$l' => 'GMP', '$m' => 'GMP', '$n' => 'GMP', '$o' => 'GMP', '$p' => 'GMP', '$q' => 'GMP', '$r' => 'GMP', '$s' => 'GMP', '$t' => 'GMP', ]; $context = new Context(); $this->analyzeFile('somefile.php', $context); $actual_vars = []; foreach ($assertions as $var => $_) { if (isset($context->vars_in_scope[$var])) { $actual_vars[$var] = (string)$context->vars_in_scope[$var]; } } $this->assertSame($assertions, $actual_vars); } public function testDecimalOperations(): void { $this->addFile( 'somefile.php', ' 'Decimal\\Decimal', '$b' => 'Decimal\\Decimal', '$c' => 'Decimal\\Decimal', '$d' => 'Decimal\\Decimal', '$f' => 'Decimal\\Decimal', '$g' => 'Decimal\\Decimal', '$h' => 'Decimal\\Decimal', '$i' => 'Decimal\\Decimal', '$j' => 'Decimal\\Decimal', '$k' => 'Decimal\\Decimal', '$l' => 'Decimal\\Decimal', '$m' => 'Decimal\\Decimal', '$n' => 'Decimal\\Decimal', '$o' => 'Decimal\\Decimal', '$p' => 'Decimal\\Decimal', '$q' => 'Decimal\\Decimal', '$r' => 'Decimal\\Decimal', '$s' => 'Decimal\\Decimal', '$t' => 'Decimal\\Decimal', ]; $context = new Context(); $this->analyzeFile('somefile.php', $context); $actual_vars = []; foreach ($assertions as $var => $_) { if (isset($context->vars_in_scope[$var])) { $actual_vars[$var] = (string)$context->vars_in_scope[$var]; } } $this->assertSame($assertions, $actual_vars); } public function testMatchOnBoolean(): void { $config = Config::getInstance(); $config->strict_binary_operands = true; $this->addFile( 'somefile.php', ' 123, $obj instanceof b => 321, }; $result2 = match (false) { $obj instanceof a => 123, $obj instanceof b => 321, }; ', ); $assertions = [ '$obj' => 'a|b', '$result1' => '123|321', '$result2' => '123|321', ]; $context = new Context(); $this->project_analyzer->setPhpVersion('8.0', 'tests'); $this->analyzeFile('somefile.php', $context); $actual_vars = []; foreach ($assertions as $var => $_) { if (isset($context->vars_in_scope[$var])) { $actual_vars[$var] = $context->vars_in_scope[$var]->getId(true); } } $this->assertSame($assertions, $actual_vars); } public function testStrictTrueEquivalence(): void { $config = Config::getInstance(); $config->strict_binary_operands = true; $this->addFile( 'somefile.php', 'expectException(CodeException::class); $this->expectExceptionMessage('RedundantIdentityWithTrue'); $this->analyzeFile('somefile.php', new Context()); } public function testStringFalseInequivalence(): void { $config = Config::getInstance(); $config->strict_binary_operands = true; $this->addFile( 'somefile.php', 'expectException(CodeException::class); $this->expectExceptionMessage('RedundantIdentityWithTrue'); $this->analyzeFile('somefile.php', new Context()); } public function testDifferingNumericLiteralTypesAdditionInStrictMode(): void { $config = Config::getInstance(); $config->strict_binary_operands = true; $this->addFile( 'somefile.php', 'expectException(CodeException::class); $this->expectExceptionMessage('InvalidOperand'); $this->analyzeFile('somefile.php', new Context()); } public function testDifferingNumericTypesAdditionInStrictMode(): void { $config = Config::getInstance(); $config->strict_binary_operands = true; $this->addFile( 'somefile.php', 'expectException(CodeException::class); $this->expectExceptionMessage('InvalidOperand'); $this->analyzeFile('somefile.php', new Context()); } public function testConcatenationWithNumberInStrictMode(): void { $config = Config::getInstance(); $config->strict_binary_operands = true; $this->addFile( 'somefile.php', 'expectException(CodeException::class); $this->expectExceptionMessage('InvalidOperand'); $this->analyzeFile('somefile.php', new Context()); } public function testImplicitStringConcatenation(): void { $config = Config::getInstance(); $config->strict_binary_operands = true; $this->addFile( 'somefile.php', 'expectException(CodeException::class); $this->expectExceptionMessage('ImplicitToStringCast'); $this->analyzeFile('somefile.php', new Context()); } public function providerValidCodeParse(): iterable { return [ 'regularAddition' => [ 'code' => ' [ '$a' => 'int', ], ], 'differingNumericTypesAdditionInWeakMode' => [ 'code' => ' [ '$a' => 'float', ], ], 'modulo' => [ 'code' => ' [ '$a' => 'int', '$b' => 'int', '$c' => 'int', '$d' => 'int', '$e' => 'int', ], ], 'numericAddition' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ '$a' => 'string',//will contain "75" ], ], 'concatenationWithTwoInt' => [ 'code' => ' [ 'code' => ' false, "foobaz" => true, "barbaz" => true]; $foo = random_int(0, 1) ? "foo" : "bar"; $foo .= "baz"; $val = $arr[$foo]; ', 'assertions' => ['$val' => 'true'], ], 'concatenateLiteralIntAndString' => [ 'code' => ' false, "foo123" => true]; $foo = "foo"; $foo .= 123; $val = $arr[$foo]; ', 'assertions' => ['$val' => 'true'], ], 'concatenateNonEmptyResultsInNonEmpty' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [], 'ignored_issues' => ['PossiblyFalseOperand'], ], 'bitwiseoperations' => [ 'code' => '> 2; $f = "a" & "b";', 'assertions' => [ '$a' => 'int', '$b' => 'int', '$c' => 'int', '$d' => 'int', '$e' => 'int', '$f' => 'string', ], ], 'ComplexLiteralBitwise' => [ 'code' => ' [ 'code' => ' [ '$a' => 'int', '$b' => 'int', '$c' => 'bool', '$d' => 'bool', ], ], 'ternaryAssignment' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ '$a' => 'float', '$b' => 'float', ], ], 'exponent' => [ 'code' => ' [ '$b' => 'int', ], ], 'bitwiseNot' => [ 'code' => ' [ '$a' => 'int', '$b' => 'int', '$c' => 'int', '$d' => 'string', ], ], 'stringIncrementSuppressed' => [ 'code' => ' [ '$a' => 'string', ], ], 'stringIncrementWithCheck' => [ 'code' => ' [ '$a===' => 'non-empty-string', ], ], 'nullCoalescingAssignment' => [ 'code' => ' [], 'ignored_issues' => [], 'php_version' => '7.4', ], 'nullCoalescingArrayAssignment' => [ 'code' => ' $arr */ function foo(array $arr) : void { $b = []; foreach ($arr as $a) { $b[0] ??= $a; } }', 'assertions' => [], 'ignored_issues' => [], 'php_version' => '7.4', ], 'addArrays' => [ 'code' => ' 5]; }', ], 'addIntToZero' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' $b); }', ], 'notAlwaysPositiveBitOperations' => [ 'code' => '> $b)) { echo "Actually, zero\n"; } if (8 === PHP_INT_SIZE) { if (0 === ($a << $d)) { echo "Actually, zero\n"; } }', ], 'IntOverflowMul' => [ 'code' => ' [ '$a' => 'float', ], ], 'IntOverflowPow' => [ 'code' => ' [ '$a' => 'float', ], ], 'IntOverflowPlus' => [ 'code' => ' [ '$a' => 'int', '$b' => 'float', ], ], 'IntOverflowPowSub' => [ 'code' => ' [ '$a' => 'float', ], ], 'IntOverflowSub' => [ 'code' => ' [ '$a' => 'float', ], ], 'literalConcatCreatesLiteral' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' ['$interpolated===' => "'12.3foobar'"], ], 'concatenatedStringIsInferredAsLiteral' => [ 'code' => ' ['$concatenated===' => "'12.3foobar'"], ], 'encapsedNonEmptyNonSpecificLiteralString' => [ 'code' => ' ['$interpolated===' => 'non-empty-literal-string'], ], 'concatenatedNonEmptyNonSpecificLiteralString' => [ 'code' => ' ['$concatenated===' => 'non-empty-literal-string'], ], 'encapsedPossiblyEmptyLiteralString' => [ 'code' => ' ['$interpolated===' => 'literal-string'], ], 'literalIntConcatCreatesLiteral' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ '$a' => 'float|int', '$b' => 'float|int', '$c' => 'float|int', '$d' => 'float|int', ], ], 'encapsedStringWithIntIncludingLiterals' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [], 'ignored_issues' => [], 'php_version' => '8.0', ], 'NumericStringIncrementLiteral' => [ 'code' => ' [ '$a' => 'float|int', '$b' => 'float|int', ], ], 'coalesceFilterOutNullEvenWithTernary' => [ 'code' => 'toString() : null) ?? "Not a stringable foo"; }', ], 'handleLiteralInequalityWithInts' => [ 'code' => ' $i * @return int<1, max> */ function toPositiveInt(int $i): int { if ($i !== 0) { return $i; } return 1; }', ], 'calculateLiteralResultForFloats' => [ 'code' => ' ['$foo===' => 'float(3)'], ], 'concatNonEmptyReturnNonFalsyString' => [ 'code' => ' [ '$a===' => 'non-falsy-string', ], ], 'concatNumericWithNonEmptyReturnNonFalsyString' => [ 'code' => ' [ '$a===' => 'non-falsy-string', '$b===' => 'non-falsy-string', ], ], ]; } public function providerInvalidCodeParse(): iterable { return [ 'badAddition' => [ 'code' => ' 'InvalidOperand', ], 'addArrayToNumber' => [ 'code' => ' 'InvalidOperand', ], 'concatenateNegativeIntRightSideIsNotNumeric' => [ 'code' => ' 'ArgumentTypeCoercion', ], 'additionWithClassInWeakMode' => [ 'code' => ' 'InvalidOperand', ], 'possiblyInvalidOperand' => [ 'code' => ' 'PossiblyInvalidOperand', ], 'possiblyInvalidConcat' => [ 'code' => ' 'PossiblyInvalidOperand', ], 'invalidGMPOperation' => [ 'code' => ' 'InvalidOperand - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:26 - Cannot add GMP to non-numeric type', ], 'stringIncrement' => [ 'code' => ' 'StringIncrement', ], 'falseIncrement' => [ 'code' => ' 'FalseOperand', ], 'trueIncrement' => [ 'code' => ' 'InvalidOperand', ], 'possiblyDivByZero' => [ 'code' => ' 'PossiblyNullOperand', ], 'invalidExponent' => [ 'code' => ' 'InvalidOperand', ], 'invalidBitwiseOr' => [ 'code' => ' 'InvalidOperand', ], 'invalidBitwiseNot' => [ 'code' => ' 'InvalidOperand', ], 'possiblyInvalidBitwiseNot' => [ 'code' => ' 'PossiblyInvalidOperand', ], 'invalidBooleanBitwiseNot' => [ 'code' => ' 'InvalidOperand', ], 'substrImpossible' => [ 'code' => ' 'TypeDoesNotContainType', ], 'literalConcatWithStringCreatesString' => [ 'code' => ' 'LessSpecificReturnStatement', ], 'encapsedConcatWithStringCreatesString' => [ 'code' => ' 'LessSpecificReturnStatement', ], ]; } }