1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Fix #524 - prevent fatal error when checking stubbed function from namespace

This commit is contained in:
Matt Brown 2018-02-21 11:32:52 -05:00
parent 9dce508689
commit e04c4f866c
4 changed files with 132 additions and 36 deletions

View File

@ -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(

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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__),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/stubs/custom_functions.php" />
</stubs>
</psalm>'
)
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
function_exists("fooBar");
echo barBar("hello");'
);
$this->analyzeFile($file_path, new Context());
}
/**
* @return void
*/
public function testNamespacedStubFunctionWithFunctionExists()
{
$this->project_checker = $this->getProjectCheckerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<stubs>
<file name="tests/stubs/custom_functions.php" />
</stubs>
</psalm>'
)
);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
namespace A;
function_exists("fooBar");
echo barBar("hello");'
);
$this->analyzeFile($file_path, new Context());
}
/**
* @expectedException \Psalm\Exception\CodeException
* @expectedExceptionMessage UndefinedFunction - /src/somefile.php:2 - Function barBar does not exist