file_provider = new FakeFileProvider(); $this->project_analyzer = new ProjectAnalyzer( new TestConfig(), new Providers( $this->file_provider, new FakeParserCacheProvider() ) ); $this->project_analyzer->setPhpVersion('7.4', 'tests'); $this->project_analyzer->getCodebase()->reportUnusedVariables(); } /** * @dataProvider providerValidCodeParse * * @param string $code * @param array $error_levels * */ public function testValidCode($code, array $error_levels = []): void { $test_name = $this->getTestName(); if (strpos($test_name, 'SKIPPED-') !== false) { $this->markTestSkipped('Skipped due to a bug.'); } $file_path = self::$src_dir_path . 'somefile.php'; $this->addFile( $file_path, $code ); foreach ($error_levels as $error_level) { $this->project_analyzer->getCodebase()->config->setCustomErrorLevel($error_level, Config::REPORT_SUPPRESS); } $this->analyzeFile($file_path, new Context()); } /** * @dataProvider providerInvalidCodeParse * * @param string $code * @param string $error_message * @param array $error_levels * */ public function testInvalidCode($code, $error_message, $error_levels = []): void { if (strpos($this->getTestName(), 'SKIPPED-') !== false) { $this->markTestSkipped(); } $this->expectException(CodeException::class); $this->expectExceptionMessageMatches('/\b' . preg_quote($error_message, '/') . '\b/'); $file_path = self::$src_dir_path . 'somefile.php'; foreach ($error_levels as $error_level) { $this->project_analyzer->getCodebase()->config->setCustomErrorLevel($error_level, Config::REPORT_SUPPRESS); } $this->addFile( $file_path, $code ); $this->analyzeFile($file_path, new Context()); } /** * @return array */ public function providerValidCodeParse(): array { return [ 'arrayOffset' => [ ' [ ' [ ' [ 'PossiblyUndefinedVariable', 'MixedArrayAccess', 'MixedOperand', 'MixedAssignment', 'InvalidStringClass', ], ], 'varDefinedInIfWithReference' => [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' $arr */ function foo(array $arr) : void { $a = false; foreach ($arr as $b) { $a = true; echo $b; } echo $a; }', ], 'doWhileReassigned' => [ ' 3);', ], 'loopTypeChangedInIfAndContinueWithReference' => [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ 'getMessage(); } }', ], 'throwWithMessageCallAndAssignmentAndReference' => [ 'getMessage(); } if ($s) {} }', ], 'throwWithMessageCallAndAssignmentInCatchAndReference' => [ 'getMessage(); $s = "hello"; } if ($s) {} }', ], 'throwWithMessageCallAndAssignmentInTryAndCatchAndReference' => [ 'getMessage(); $s = "hello"; } if ($s) {} }', ], 'throwWithMessageCallAndNestedAssignmentInTryAndCatchAndReference' => [ 'getMessage(); $t = "hello"; } if ($t) { $s = $t; } } if ($s) {} }', ], 'throwWithReturnInOneCatch' => [ 'getMessage(); $s = false; } catch (Exception $e) { return; } if ($s) {} }', ], 'loopWithIfRedefinition' => [ ' [ 'passedByRef($b); }', ], 'usedMethodCallVariable' => [ '$methodName()] = true; } return $ret; }', 'error_levels' => [ 'MixedAssignment', 'MixedMethodCall', 'MixedArrayOffset', ], ], 'globalVariableUsage' => [ ' [ ' [ ' [ ' 0) { return "value"; } throw new Exception("fail"); } function main() : void { try { $s = example_string(); if (!$s) { echo "Failed to get string\n"; } } catch (Exception $e) { $s = "fallback"; } printf("s is %s\n", $s); }', ], 'loopTypeChangedInIfAndBreakWithReference' => [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' $arr */ function far(array $arr): void { foreach ($arr as [$a, $b]) { echo $a; echo $b; } }', ], 'arrayAssignmentInFunctionCoerced' => [ 'a = (int) $a; $this->b = (int) $b; } } ' ], 'varCheckAfterNestedAssignmentAndBreak' => [ ' [ ' [ ' [ ' [ ' [ ' [ ' $options */ public function __construct(array $options) { $this->setOptions($options); } /** * @param array $options */ protected function setOptions(array $options): void { foreach ($options as $key => $value) { $normalized = ucfirst($key); $method = "set" . $normalized; if (method_exists($this, $method)) { $this->$method($value); } } } } new A(["bar" => "bat"]);', ], 'instanceofVarUse' => [ ' [ ' 0); echo $i;', ], 'callableReferencesItself' => [ ' [ ' $type */ function bar(string $type) : ArrayObject { $data = [["foo"], ["bar"]]; /** @psalm-suppress UnsafeInstantiation */ return new $type($data[0]); }', ], 'byRefVariableUsedInAddition' => [ ' [ ' [ ' [ 'bar([$b]); }', ], 'paramUsedInsideLoop' => [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' $a */ function getLastNum(array $a): int { foreach ($a as $num) { $last = $num; } return $last; }' ], 'usedStrtolowerInArray' => [ ' $row */ function foo(array $row, string $s) : array { $row["a" . strtolower($s)] += 1; return $row; }', ], 'pureWithReflectionMethodSetValue' => [ 'setValue([get_class($mock) => "hello"]); }' ], 'defineBeforeAssignmentInConditional' => [ ' [ ' [ 'bar($i); }', ], 'noUnusedVariableAfterRedeclaredInCatch' => [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' $test */ function foo(array $test) : void { foreach($test as $key => $_testValue) { echo $key; } }' ], 'usedAfterMixedVariableAssignment' => [ ' [ ' [ ' [ ' [ 'foo(new Exception($mixed_or_null)); }', ], 'validMixedAnnotation' => [ ' [ ' [ 'value; $update = $value; } }' ], 'createdAndUsedInCondition' => [ 'foo()) {} return; } if (!($a = getA()) || $a->foo()) {}' ], 'usedInUndefinedFunction' => [ ' [ ' "b", "c" => "d"]; foreach ($variables as $name => $value) { ${$name} = $value; }' ], 'usedLoopVariable' => [ ' [ ' [ ' 0; $i--) { echo $i . "\n"; } }' ], 'usedForVariablePlusString' => [ ' [ ' [ ' [ ' [ ' [ ' [1], "b" => [2] ]; foreach (["a"] as $e){ takes_ref($a[$e]); } /** @param array $p */ function takes_ref(array &$p): void { echo implode(",", $p); }' ], 'doWhileWithBreak' => [ ' [ ' [ '= $index = nextNumber($index)) { // ... } } function nextNumber(int $eee): int { return $eee + 1; }' ], 'usedParamInWhileIndirectly' => [ '= $index = nextNumber($index)) { // ... } } function nextNumber(int $i): int { return $i + 1; }' ], 'doArrayIncrement' => [ ' $keys * @param int $key */ function error2(array $keys, int $key): int { if ($key === 1) {} do { $nextKey = $keys[++$key] ?? null; } while ($nextKey === null); return $nextKey; }' ], 'variableUsedIndirectly' => [ ' [ ' [ ' [ ' [ ' [ ' */ public $b = []; } function foo(Clause $c, int $var): void { $new_b = $c->b; if (isset($c->b[0])) { $new_b[$var] = 0; } if ($new_b) {} }', ], 'arrayAssignOpAdditionInsideLoop' => [ ' $arr0 * @param array $arr1 * @param array $arr2 * @return void */ function parp(array $arr0, array $arr1, array $arr2) { $arr3 = $arr0; foreach ($arr1 as $a) { echo $a; $arr3 += $arr2; } if ($arr3) {} }', ], 'arrayAdditionInsideLoop' => [ ' $arr0 * @param array $arr1 * @param array $arr2 * @return void */ function parp(array $arr0, array $arr1, array $arr2) { $arr3 = $arr0; foreach ($arr1 as $a) { echo $a; $arr3 = $arr3 + $arr2; } if ($arr3) {} }', ], 'checkValueBeforeAdding' => [ 'b) {} }' ], 'loopOverUnknown' => [ ' [ ' */ $diff_call_map = require($delta_file); foreach ($diff_call_map as $key => $_) { $cased_key = strtolower($key); echo $cased_key; } } }', ], 'loopAgain' => [ ' $lines */ function parse(array $lines) : array { $last = 0; foreach ($lines as $k => $line) { if (rand(0, 1)) { $last = $k; } elseif (rand(0, 1)) { $last = 0; } elseif ($last !== 0) { $lines[$last] .= $line; } } return $lines; }' ], 'necessaryVarAnnotation' => [ ' $_) { echo $key; } }' ], 'continuingEducation' => [ ' [ ' [ ' [ ' [ ' [ ' [ 'foo($hue); }' ], 'usedAsArrayKey' => [ ' $lightness]; return $arr; }' ], 'assignToGlobalVar' => [ ' $value) { $_GET[$key] = $value; } }' ], 'assignToArrayTwice' => [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' $keys */ function foo(iterable $keys, int $colno) : void { $i = 0; $key = 0; $index = 0; foreach ($keys as $index => $key) { if ($key === $colno) { $i = $index; break; } elseif ($key > $colno) { $i = $index; break; } } echo $i; echo $index; echo $key; }' ], 'whileLoopVarUpdatedInWhileLoop' => [ ' $arr */ function foo(array $arr) : void { while ($a = array_pop($arr)) { if ($a === 4) { $arr = array_merge($arr, ["a", "b", "c"]); continue; } echo "here"; } }' ], 'usedThroughParamByRef' => [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ ' $e->getMessage(); }' ], 'useImmutableGetIteratorInForeach' => [ ' */ public function getIterator() { yield from [1, 2, 3]; } } $a = new A(); foreach ($a as $v) { echo $v; }' ], 'castToBoolAndDouble' => [ ' [ ' [ ' [ ' [ ' [ ' [ ' [ '= 5 ? true : false; $b = (int) $a; return $b; } ' ], 'promotedPropertiesAreNeverMarkedAsUnusedParams' => [ ' [ 'validate()) && ($result = $this->save())) { return 0; } elseif (is_string($result)) { return 1; } else { return 2; } } }' ], 'concatWithUnknownProperty' => [ ' $key */ function foo(object $a, string $k) : string { $sortA = ""; /** @psalm-suppress MixedOperand */ $sortA .= $a->$k; return $sortA; }' ], 'varDocblockVariableIsUsedByRef' => [ ' $arr */ function foo(array $arr) : string { /** @var string $val */ foreach ($arr as &$val) { $val = urlencode($val); } return implode("/", $arr); }' ], 'initVariableInOffset' => [ ' $b, ]; foreach ($a as $key => $value) { echo $key . " " . $value; }', ], 'intAndBitwiseNotOperator' => [ ' [ '> 1); $randomBytes = random_bytes(1); $randomBytes[0] = $randomBytes[0] & $bitmask; return $randomBytes; }' ], 'globalChangeValue' => [ ' [ ' [ ' [ ' */ public function providerInvalidCodeParse(): array { return [ 'simpleUnusedVariable' => [ ' 'UnusedVariable', ], 'unusedVarWithAdditionOp' => [ ' 'UnusedVariable', ], 'unusedVarWithConditionalAdditionOp' => [ ' 'UnusedVariable', ], 'unusedVarWithConditionalAddition' => [ ' 'UnusedVariable', ], 'unusedVarWithIncrement' => [ ' 'UnusedVariable', ], 'unusedVarWithConditionalIncrement' => [ ' 'UnusedVariable', ], 'ifInBothBranchesWithoutReference' => [ ' 'UnusedVariable', ], 'varInNestedAssignmentWithoutReference' => [ ' 'UnusedVariable', ], 'varInSecondNestedAssignmentWithoutReference' => [ ' 'UnusedVariable', ], 'varReassignedInBothBranchesOfIf' => [ ' 'UnusedVariable', ], 'varReassignedInNestedBranchesOfIf' => [ ' 'UnusedVariable', ], 'ifVarReassignedInBranchWithNoUse' => [ ' 'UnusedVariable', ], 'elseVarReassignedInBranchAndNoReference' => [ ' 'UnusedVariable', ], 'switchVarReassignedInBranch' => [ ' 'UnusedVariable', ], 'switchVarReassignedInBranchWithDefault' => [ ' 'UnusedVariable', ], 'switchVarReassignedInAllBranches' => [ ' 'UnusedVariable', ], 'unusedListVar' => [ ' 'UnusedVariable', ], 'unusedPreForVar' => [ ' 'UnusedVariable', ], 'unusedIfInReturnBlock' => [ ' 'UnusedVariable', ], 'unusedIfVarInBranch' => [ ' 'UnusedVariable', ], 'throwWithMessageCallAndAssignmentAndNoReference' => [ 'getMessage(); } }', 'error_message' => 'UnusedVariable', ], 'throwWithMessageCallAndAssignmentInCatchAndNoReference' => [ 'getMessage(); $s = "hello"; } }', 'error_message' => 'UnusedVariable', ], 'throwWithMessageCallAndNestedAssignmentInTryAndCatchAndNoReference' => [ 'getMessage(); $t = "hello"; } if ($t) { $s = $t; } } }', 'error_message' => 'UnusedVariable', ], 'throwWithReturnInOneCatchAndNoReference' => [ 'getMessage(); $s = false; } catch (Exception $e) { return; } }', 'error_message' => 'UnusedVariable', ], 'loopTypeChangedInIfWithoutReference' => [ ' 'UnusedVariable', ], 'loopTypeChangedInIfAndContinueWithoutReference' => [ ' 'UnusedVariable', ], 'loopReassignedInIfAndContinueWithoutReferenceAfter' => [ ' 'UnusedVariable', ], 'loopReassignedInIfAndContinueWithoutReference' => [ ' 'UnusedVariable', ], 'unusedConditionalCode' => [ ' 'UnusedVariable', ], 'varDefinedInIfWithoutReference' => [ ' 'UnusedVariable', ], 'SKIPPED-byrefInForeachLoopWithoutReference' => [ ' 'UnusedVariable', ], 'loopSetIfNullWithBreakWithoutReference' => [ ' 'UnusedVariable', ], 'loopSetIfNullWithBreakWithoutReference2' => [ ' 'UnusedVariable', ], 'loopSetIfNullWithContinueWithoutReference' => [ ' 'UnusedVariable', ], 'loopAssignmentAfterReferenceWithBreak' => [ ' 'UnusedVariable', ], 'loopAssignmentAfterReferenceWithBreakInIf' => [ ' 'UnusedVariable', ], 'switchVarConditionalAssignmentWithoutReference' => [ ' 'UnusedVariable', ], 'switchInIf' => [ ' 'UnusedVariable', ], 'reusedKeyVar' => [ ' "foo.foo"]; foreach ($arr as $key => $v) { list($key) = explode(".", $v); echo $key; }', 'error_message' => 'UnusedVariable', ], 'detectUnusedVarBeforeTryInsideForeach' => [ ' 'UnusedVariable', ], 'detectUnusedVariableInsideIfLoop' => [ ' 'UnusedVariable', ], 'detectUnusedVariableInsideIfElseLoop' => [ ' 'UnusedVariable', ], 'detectUnusedVariableInsideIfElseifLoop' => [ ' 'UnusedVariable', ], 'detectUnusedVariableInsideIfLoopWithEchoInside' => [ ' 'UnusedVariable', ], 'detectUnusedVariableInsideLoopAfterAssignment' => [ ' 'UnusedForeachValue', ], 'detectUnusedVariableInsideLoopAfterAssignmentWithAddition' => [ ' 'UnusedForeachValue', ], 'detectUnusedVariableInsideLoopCalledInFunction' => [ ' 'UnusedVariable', ], 'detectUnusedVariableReassignedInIfFollowedByTryInsideForLoop' => [ ' 'UnusedVariable', ], 'detectUnusedVariableReassignedInIfFollowedByTryInsideForeachLoop' => [ ' 'UnusedVariable', ], 'detectUselessArrayAssignment' => [ ' 'UnusedVariable', ], 'detectUnusedSecondAssignmentBeforeTry' => [ ' 'UnusedVariable', ], 'detectRedundancyAfterLoopWithContinue' => [ ' 'UnusedVariable', ], 'setInLoopThatsAlwaysEnteredButNotReferenced' => [ ' $a */ function getLastNum(array $a): int { foreach ($a as $num) { $last = $num; } return 4; }', 'error_message' => 'UnusedForeachValue', ], 'conditionalForeachWithUnusedValue' => [ ' 0) { foreach ([1, 2, 3] as $val) {} } ', 'error_message' => 'UnusedForeachValue', ], 'doubleForeachWithInnerUnusedValue' => [ '> $arr * @return list */ function f(array $arr): array { foreach ($arr as $elt) { foreach ($elt as $subelt) {} } return $elt; } ', 'error_message' => 'UnusedForeachValue' ], 'defineInBothBranchesOfConditional' => [ ' 'UnusedVariable', ], 'knownVarType' => [ ' 'UnnecessaryVarAnnotation', ], 'knownVarTypeWithName' => [ ' 'UnnecessaryVarAnnotation', ], 'knownForeachVarType' => [ ' 'UnnecessaryVarAnnotation', ], 'arrowFunctionUnusedVariable' => [ ' ++$p );', 'error_message' => 'UnusedVariable', ], 'arrowFunctionUnusedParam' => [ ' 0 );', 'error_message' => 'UnusedClosureParam', ], 'unusedFunctionParamWithDefault' => [ ' 'UnusedParam', ], 'arrayMapClosureWithParamTypeNoUse' => [ ' 'UnusedClosureParam', ], 'noUseOfInstantArrayAssignment' => [ ' 'UnusedVariable', ], 'expectsNonNullAndPassedPossiblyNull' => [ ' 'PossiblyNullArgument' ], 'useArrayAssignmentNeverUsed' => [ ' 'UnusedVariable', ], 'warnAboutOriginalBadArray' => [ ' 'MixedAssignment - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:42 - Unable to determine the type that $a is being assigned to. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:2:47' ], 'warnAboutOriginalBadFunctionCall' => [ ' 'MixedAssignment - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:38 - Unable to determine the type that $a is being assigned to. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:2:44' ], 'warnAboutOriginalBadStaticCall' => [ ' 'MixedAssignment - src' . DIRECTORY_SEPARATOR . 'somefile.php:10:38 - Unable to determine the type that $a is being assigned to. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:3:62' ], 'warnAboutOriginalBadInstanceCall' => [ 'makeArray(); foreach ($arr as $a) { echo $a; }', 'error_message' => 'MixedAssignment - src' . DIRECTORY_SEPARATOR . 'somefile.php:10:38 - Unable to determine the type that $a is being assigned to. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:3:55' ], 'warnAboutDocblockReturnType' => [ ' 'MixedAssignment - src' . DIRECTORY_SEPARATOR . 'somefile.php:10:47 - Unable to determine the type that $a is being assigned to. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:2:33' ], 'warnAboutMixedArgument' => [ ' 'MixedArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:10:30 - Argument 1 of echo cannot be mixed, expecting string. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:2:44' ], 'warnAboutMixedMethodCall' => [ 'foo(); }', 'error_message' => 'MixedMethodCall - src' . DIRECTORY_SEPARATOR . 'somefile.php:10:29 - Cannot determine the type of $a when calling method foo. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:2:44' ], 'warnAboutMixedReturnStatement' => [ ' 'MixedReturnStatement - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:36 - Could not infer a return type. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:2:44' ], 'warnAboutIterableKeySource' => [ ' $_) {} }', 'error_message' => 'MixedAssignment - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:42 - Unable to determine the type that $key is being assigned to. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:2:43' ], 'warnAboutMixedKeySource' => [ ' $_) {} }', 'error_message' => 'MixedAssignment - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:42 - Unable to determine the type that $key is being assigned to. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:3:34' ], 'warnAboutMixedArgumentTypeCoercionSource' => [ ' $arr */ function takesArrayOfString(array $arr) : void { foreach ($arr as $a) { echo $a; } } /** @param mixed $a */ function takesArray($a) : void { $arr = [$a]; takesArrayOfString($arr); }', 'error_message' => 'MixedArgumentTypeCoercion - src' . DIRECTORY_SEPARATOR . 'somefile.php:12:44 - Argument 1 of takesArrayOfString expects array, parent type array{mixed} provided. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:10:41' ], 'warnAboutUnusedVariableInTryReassignedInCatch' => [ ' 'UnusedVariable', ], 'warnAboutUnusedVariableInTryReassignedInFinally' => [ ' 'UnusedVariable', ], 'SKIPPED-warnAboutVariableUsedInNestedTryNotUsedInOuterTry' => [ ' 'UnusedVariable', ], ]; } }