diff --git a/src/Psalm/Checker/Statements/Expression/Call/FunctionCallChecker.php b/src/Psalm/Checker/Statements/Expression/Call/FunctionCallChecker.php index 50d67da8c..7b34c4f90 100644 --- a/src/Psalm/Checker/Statements/Expression/Call/FunctionCallChecker.php +++ b/src/Psalm/Checker/Statements/Expression/Call/FunctionCallChecker.php @@ -40,41 +40,41 @@ class FunctionCallChecker extends \Psalm\Checker\Statements\Expression\CallCheck PhpParser\Node\Expr\FuncCall $stmt, Context $context ) { - $method = $stmt->name; + $function = $stmt->name; - if ($method instanceof PhpParser\Node\Name) { + if ($function instanceof PhpParser\Node\Name) { $first_arg = isset($stmt->args[0]) ? $stmt->args[0] : null; - if ($method->parts === ['method_exists']) { + if ($function->parts === ['method_exists']) { $context->check_methods = false; - } elseif ($method->parts === ['class_exists']) { + } elseif ($function->parts === ['class_exists']) { if ($first_arg && $first_arg->value instanceof PhpParser\Node\Scalar\String_) { $context->addPhantomClass($first_arg->value->value); } else { $context->check_classes = false; } - } elseif ($method->parts === ['extension_loaded']) { + } elseif ($function->parts === ['extension_loaded']) { $context->check_classes = false; - } elseif ($method->parts === ['function_exists']) { + } elseif ($function->parts === ['function_exists']) { $context->check_functions = false; - } elseif ($method->parts === ['is_callable']) { + } elseif ($function->parts === ['is_callable']) { $context->check_methods = false; $context->check_functions = false; - } elseif ($method->parts === ['defined']) { + } elseif ($function->parts === ['defined']) { $context->check_consts = false; - } elseif ($method->parts === ['extract']) { + } elseif ($function->parts === ['extract']) { $context->check_variables = false; - } elseif ($method->parts === ['var_dump'] || $method->parts === ['shell_exec']) { + } elseif ($function->parts === ['var_dump'] || $function->parts === ['shell_exec']) { if (IssueBuffer::accepts( new ForbiddenCode( - 'Unsafe ' . implode('', $method->parts), + 'Unsafe ' . implode('', $function->parts), new CodeLocation($statements_checker->getSource(), $stmt) ), $statements_checker->getSuppressedIssues() )) { return false; } - } elseif ($method->parts === ['define']) { + } elseif ($function->parts === ['define']) { if ($first_arg && $first_arg->value instanceof PhpParser\Node\Scalar\String_) { $second_arg = $stmt->args[1]; ExpressionChecker::analyze($statements_checker, $second_arg->value, $context); @@ -91,7 +91,7 @@ class FunctionCallChecker extends \Psalm\Checker\Statements\Expression\CallCheck } } - $method_id = null; + $function_id = null; $function_params = null; $in_call_map = false; @@ -209,21 +209,21 @@ class FunctionCallChecker extends \Psalm\Checker\Statements\Expression\CallCheck $stmt->inferredType = Type::getMixed(); } } else { - $method_id = implode('\\', $stmt->name->parts); + $function_id = implode('\\', $stmt->name->parts); - $in_call_map = CallMap::inCallMap($method_id); - $is_stubbed = $codebase_functions->hasStubbedFunction($method_id); + $in_call_map = CallMap::inCallMap($function_id); + $is_stubbed = $codebase_functions->hasStubbedFunction($function_id); $is_predefined = true; if (!$in_call_map) { $predefined_functions = $config->getPredefinedFunctions(); - $is_predefined = isset($predefined_functions[$method_id]); + $is_predefined = isset($predefined_functions[$function_id]); } if (!$in_call_map && !$stmt->name instanceof PhpParser\Node\Name\FullyQualified) { - $method_id = $codebase_functions->getFullyQualifiedFunctionNameFromString( - $method_id, + $function_id = $codebase_functions->getFullyQualifiedFunctionNameFromString( + $function_id, $statements_checker ); } @@ -232,17 +232,19 @@ class FunctionCallChecker extends \Psalm\Checker\Statements\Expression\CallCheck if ($context->check_functions) { if (self::checkFunctionExists( $statements_checker, - $method_id, + $function_id, $code_location ) === false ) { return false; } + } else { + $function_id = self::getExistingFunctionId($statements_checker, $function_id); } $function_exists = $is_stubbed || $codebase_functions->functionExists( $statements_checker, - strtolower($method_id) + strtolower($function_id) ); } else { $function_exists = true; @@ -252,7 +254,7 @@ class FunctionCallChecker extends \Psalm\Checker\Statements\Expression\CallCheck if (!$in_call_map || $is_stubbed) { $function_storage = $codebase_functions->getStorage( $statements_checker, - strtolower($method_id) + strtolower($function_id) ); $function_params = $function_storage->params; @@ -265,7 +267,7 @@ class FunctionCallChecker extends \Psalm\Checker\Statements\Expression\CallCheck if ($in_call_map && !$is_stubbed) { $function_params = FunctionLikeChecker::getFunctionParamsFromCallMapById( $statements_checker->getFileChecker()->project_checker, - $method_id, + $function_id, $stmt->args ); } @@ -276,7 +278,7 @@ class FunctionCallChecker extends \Psalm\Checker\Statements\Expression\CallCheck $statements_checker, $stmt->args, $function_params, - $method_id, + $function_id, $context ) === false) { // fall through @@ -285,11 +287,11 @@ class FunctionCallChecker extends \Psalm\Checker\Statements\Expression\CallCheck if ($function_exists) { $generic_params = null; - if ($stmt->name instanceof PhpParser\Node\Name && $method_id) { + if ($stmt->name instanceof PhpParser\Node\Name && $function_id) { if (!$is_stubbed && $in_call_map) { $function_params = FunctionLikeChecker::getFunctionParamsFromCallMapById( $statements_checker->getFileChecker()->project_checker, - $method_id, + $function_id, $stmt->args ); } @@ -299,7 +301,7 @@ class FunctionCallChecker extends \Psalm\Checker\Statements\Expression\CallCheck if (self::checkFunctionLikeArgumentsMatch( $statements_checker, $stmt->args, - $method_id, + $function_id, $function_params ?: [], $function_storage, null, @@ -310,7 +312,7 @@ class FunctionCallChecker extends \Psalm\Checker\Statements\Expression\CallCheck // fall through } - if ($stmt->name instanceof PhpParser\Node\Name && $method_id) { + if ($stmt->name instanceof PhpParser\Node\Name && $function_id) { if (!$in_call_map || $is_stubbed) { if ($function_storage && $function_storage->template_types) { foreach ($function_storage->template_types as $template_name => $_) { @@ -355,7 +357,7 @@ class FunctionCallChecker extends \Psalm\Checker\Statements\Expression\CallCheck } else { $stmt->inferredType = FunctionChecker::getReturnTypeFromCallMapWithArgs( $statements_checker, - $method_id, + $function_id, $stmt->args, $code_location, $statements_checker->getSuppressedIssues() @@ -369,8 +371,8 @@ class FunctionCallChecker extends \Psalm\Checker\Statements\Expression\CallCheck } if ($config->use_assert_for_type && - $method instanceof PhpParser\Node\Name && - $method->parts === ['assert'] && + $function instanceof PhpParser\Node\Name && + $function->parts === ['assert'] && isset($stmt->args[0]) ) { $assert_clauses = AlgebraChecker::getFormula( diff --git a/src/Psalm/Checker/Statements/Expression/CallChecker.php b/src/Psalm/Checker/Statements/Expression/CallChecker.php index 08a71a60b..67a422a77 100644 --- a/src/Psalm/Checker/Statements/Expression/CallChecker.php +++ b/src/Psalm/Checker/Statements/Expression/CallChecker.php @@ -1414,4 +1414,31 @@ class CallChecker return true; } + + /** + * @param StatementsChecker $statements_checker + * @param string $function_id + * + * @return string + */ + protected static function getExistingFunctionId(StatementsChecker $statements_checker, $function_id) + { + $function_id = strtolower($function_id); + + $codebase = $statements_checker->getFileChecker()->project_checker->codebase; + + if ($codebase->functions->functionExists($statements_checker, $function_id)) { + return $function_id; + } + + $root_function_id = preg_replace('/.*\\\/', '', $function_id); + + if ($function_id !== $root_function_id + && $codebase->functions->functionExists($statements_checker, $root_function_id) + ) { + return $root_function_id; + } + + return $function_id; + } } diff --git a/src/Psalm/Codebase/Functions.php b/src/Psalm/Codebase/Functions.php index 7b83d3c46..cccecdd77 100644 --- a/src/Psalm/Codebase/Functions.php +++ b/src/Psalm/Codebase/Functions.php @@ -39,8 +39,8 @@ class Functions */ public function getStorage(StatementsChecker $statements_checker, $function_id) { - if (isset(self::$stubbed_functions[$function_id])) { - return self::$stubbed_functions[$function_id]; + if (isset(self::$stubbed_functions[strtolower($function_id)])) { + return self::$stubbed_functions[strtolower($function_id)]; } if ($this->reflection->hasFunction($function_id)) { @@ -96,7 +96,7 @@ class Functions */ public function addStubbedFunction($function_id, FunctionLikeStorage $storage) { - self::$stubbed_functions[$function_id] = $storage; + self::$stubbed_functions[strtolower($function_id)] = $storage; } /** @@ -106,7 +106,7 @@ class Functions */ public function hasStubbedFunction($function_id) { - return isset(self::$stubbed_functions[$function_id]); + return isset(self::$stubbed_functions[strtolower($function_id)]); } /** @@ -126,7 +126,7 @@ class Functions return true; } - if (isset(self::$stubbed_functions[$function_id])) { + if (isset(self::$stubbed_functions[strtolower($function_id)])) { return true; } diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php index 0d56fe90c..734976cfe 100644 --- a/tests/ConfigTest.php +++ b/tests/ConfigTest.php @@ -395,6 +395,73 @@ class ConfigTest extends TestCase $this->analyzeFile($file_path, new Context()); } + /** + * @return void + */ + public function testStubFunctionWithFunctionExists() + { + $this->project_checker = $this->getProjectCheckerWithConfig( + TestConfig::loadFromXML( + dirname(__DIR__), + ' + + + + + + + + + ' + ) + ); + + $file_path = getcwd() . '/src/somefile.php'; + + $this->addFile( + $file_path, + 'analyzeFile($file_path, new Context()); + } + + /** + * @return void + */ + public function testNamespacedStubFunctionWithFunctionExists() + { + $this->project_checker = $this->getProjectCheckerWithConfig( + TestConfig::loadFromXML( + dirname(__DIR__), + ' + + + + + + + + + ' + ) + ); + + $file_path = getcwd() . '/src/somefile.php'; + + $this->addFile( + $file_path, + 'analyzeFile($file_path, new Context()); + } + /** * @expectedException \Psalm\Exception\CodeException * @expectedExceptionMessage UndefinedFunction - /src/somefile.php:2 - Function barBar does not exist