mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
parent
8e372d30ac
commit
ce38868871
@ -2,23 +2,10 @@
|
||||
namespace Psalm\Internal\Analyzer\Statements\Expression\BinaryOp;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\Internal\Algebra\FormulaGenerator;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Type;
|
||||
use Psalm\Internal\Algebra;
|
||||
use Psalm\Type\Reconciler;
|
||||
use Psalm\Internal\Type\AssertionReconciler;
|
||||
use function array_merge;
|
||||
use function array_values;
|
||||
use function array_map;
|
||||
use function array_keys;
|
||||
use function preg_match;
|
||||
use function preg_quote;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
|
@ -12,7 +12,6 @@ use Psalm\Internal\Analyzer\Statements\Expression\CastAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Type\Comparator\CallableTypeComparator;
|
||||
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
||||
use Psalm\Internal\DataFlow\TaintSink;
|
||||
use Psalm\Internal\DataFlow\DataFlowNode;
|
||||
use Psalm\Internal\Codebase\TaintFlowGraph;
|
||||
use Psalm\Internal\MethodIdentifier;
|
||||
|
@ -12,7 +12,6 @@ use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Codebase\InternalCallMapHandler;
|
||||
use Psalm\Internal\Codebase\TaintFlowGraph;
|
||||
use Psalm\Internal\DataFlow\TaintSink;
|
||||
use Psalm\Internal\DataFlow\DataFlowNode;
|
||||
use Psalm\Internal\Stubs\Generator\StubsGenerator;
|
||||
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
||||
use Psalm\Internal\MethodIdentifier;
|
||||
|
@ -5,12 +5,8 @@ use PhpParser;
|
||||
use PhpParser\BuilderFactory;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\AssertionFinder;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ConstFetchAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Type\Comparator\CallableTypeComparator;
|
||||
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
||||
use Psalm\Internal\Codebase\InternalCallMapHandler;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
@ -21,7 +17,6 @@ use Psalm\Internal\DataFlow\DataFlowNode;
|
||||
use Psalm\Internal\Codebase\TaintFlowGraph;
|
||||
use Psalm\Internal\Type\TypeExpander;
|
||||
use Psalm\Issue\DeprecatedFunction;
|
||||
use Psalm\Issue\ForbiddenCode;
|
||||
use Psalm\Issue\MixedFunctionCall;
|
||||
use Psalm\Issue\InvalidFunctionCall;
|
||||
use Psalm\Issue\ImpureFunctionCall;
|
||||
@ -49,9 +44,7 @@ use function reset;
|
||||
use function implode;
|
||||
use function strtolower;
|
||||
use function array_merge;
|
||||
use function is_string;
|
||||
use function array_map;
|
||||
use function extension_loaded;
|
||||
use function strpos;
|
||||
use Psalm\Internal\Type\TemplateBound;
|
||||
use Psalm\Internal\Type\TemplateResult;
|
||||
@ -499,14 +492,14 @@ class FunctionCallAnalyzer extends CallAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
if ($function_name instanceof PhpParser\Node\Name) {
|
||||
self::handleNamedFunction(
|
||||
if ($function_name instanceof PhpParser\Node\Name && $function_id) {
|
||||
NamedFunctionCallHandler::handle(
|
||||
$statements_analyzer,
|
||||
$codebase,
|
||||
$stmt,
|
||||
$real_stmt,
|
||||
$function_name,
|
||||
$function_id,
|
||||
strtolower($function_id),
|
||||
$context
|
||||
);
|
||||
}
|
||||
@ -1698,459 +1691,4 @@ class FunctionCallAnalyzer extends CallAnalyzer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function handleNamedFunction(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
\Psalm\Codebase $codebase,
|
||||
PhpParser\Node\Expr\FuncCall $stmt,
|
||||
PhpParser\Node\Expr\FuncCall $real_stmt,
|
||||
PhpParser\Node\Name $function_name,
|
||||
?string $function_id,
|
||||
Context $context
|
||||
) : void {
|
||||
$first_arg = isset($stmt->args[0]) ? $stmt->args[0] : null;
|
||||
|
||||
if ($function_name->parts === ['get_class']
|
||||
|| $function_name->parts === ['gettype']
|
||||
|| $function_name->parts === ['get_debug_type']
|
||||
) {
|
||||
if ($first_arg) {
|
||||
$var = $first_arg->value;
|
||||
|
||||
if ($var instanceof PhpParser\Node\Expr\Variable
|
||||
&& is_string($var->name)
|
||||
) {
|
||||
$var_id = '$' . $var->name;
|
||||
|
||||
if (isset($context->vars_in_scope[$var_id])) {
|
||||
if ($function_name->parts === ['get_class']) {
|
||||
$atomic_type = new Type\Atomic\TDependentGetClass(
|
||||
$var_id,
|
||||
$context->vars_in_scope[$var_id]->hasMixed()
|
||||
? Type::getObject()
|
||||
: $context->vars_in_scope[$var_id]
|
||||
);
|
||||
} elseif ($function_name->parts === ['gettype']) {
|
||||
$atomic_type = new Type\Atomic\TDependentGetType($var_id);
|
||||
} else {
|
||||
$atomic_type = new Type\Atomic\TDependentGetDebugType($var_id);
|
||||
}
|
||||
|
||||
$statements_analyzer->node_data->setType($real_stmt, new Type\Union([$atomic_type]));
|
||||
}
|
||||
} elseif (($var_type = $statements_analyzer->node_data->getType($var))
|
||||
&& ($function_name->parts === ['get_class']
|
||||
|| $function_name->parts === ['get_debug_type']
|
||||
)
|
||||
) {
|
||||
$class_string_types = [];
|
||||
|
||||
foreach ($var_type->getAtomicTypes() as $class_type) {
|
||||
if ($class_type instanceof Type\Atomic\TNamedObject) {
|
||||
$class_string_types[] = new Type\Atomic\TClassString($class_type->value, clone $class_type);
|
||||
} elseif ($class_type instanceof Type\Atomic\TTemplateParam
|
||||
&& $class_type->as->isSingle()
|
||||
) {
|
||||
$as_atomic_type = \array_values($class_type->as->getAtomicTypes())[0];
|
||||
|
||||
if ($as_atomic_type instanceof Type\Atomic\TObject) {
|
||||
$class_string_types[] = new Type\Atomic\TTemplateParamClass(
|
||||
$class_type->param_name,
|
||||
'object',
|
||||
null,
|
||||
$class_type->defining_class
|
||||
);
|
||||
} elseif ($as_atomic_type instanceof TNamedObject) {
|
||||
$class_string_types[] = new Type\Atomic\TTemplateParamClass(
|
||||
$class_type->param_name,
|
||||
$as_atomic_type->value,
|
||||
$as_atomic_type,
|
||||
$class_type->defining_class
|
||||
);
|
||||
}
|
||||
} elseif ($function_name->parts === ['get_class']) {
|
||||
$class_string_types[] = new Type\Atomic\TClassString();
|
||||
} elseif ($function_name->parts === ['get_debug_type']) {
|
||||
if ($class_type instanceof Type\Atomic\TInt) {
|
||||
$class_string_types[] = new Type\Atomic\TLiteralString('int');
|
||||
} elseif ($class_type instanceof Type\Atomic\TString) {
|
||||
$class_string_types[] = new Type\Atomic\TLiteralString('string');
|
||||
} elseif ($class_type instanceof Type\Atomic\TFloat) {
|
||||
$class_string_types[] = new Type\Atomic\TLiteralString('float');
|
||||
} elseif ($class_type instanceof Type\Atomic\TBool) {
|
||||
$class_string_types[] = new Type\Atomic\TLiteralString('bool');
|
||||
} elseif ($class_type instanceof Type\Atomic\TClosedResource) {
|
||||
$class_string_types[] = new Type\Atomic\TLiteralString('resource (closed)');
|
||||
} elseif ($class_type instanceof Type\Atomic\TNull) {
|
||||
$class_string_types[] = new Type\Atomic\TLiteralString('null');
|
||||
} else {
|
||||
$class_string_types[] = new Type\Atomic\TString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($class_string_types) {
|
||||
$statements_analyzer->node_data->setType($real_stmt, new Type\Union($class_string_types));
|
||||
}
|
||||
}
|
||||
} elseif ($function_name->parts === ['get_class']
|
||||
&& ($get_class_name = $statements_analyzer->getFQCLN())
|
||||
) {
|
||||
$statements_analyzer->node_data->setType(
|
||||
$real_stmt,
|
||||
new Type\Union([
|
||||
new Type\Atomic\TClassString(
|
||||
$get_class_name,
|
||||
new Type\Atomic\TNamedObject($get_class_name)
|
||||
)
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_name->parts === ['method_exists']) {
|
||||
$second_arg = isset($stmt->args[1]) ? $stmt->args[1] : null;
|
||||
|
||||
if ($first_arg
|
||||
&& $first_arg->value instanceof PhpParser\Node\Expr\Variable
|
||||
&& $second_arg
|
||||
&& $second_arg->value instanceof PhpParser\Node\Scalar\String_
|
||||
) {
|
||||
// do nothing
|
||||
} else {
|
||||
$context->check_methods = false;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_name->parts === ['class_exists']) {
|
||||
if ($first_arg) {
|
||||
if ($first_arg->value instanceof PhpParser\Node\Scalar\String_) {
|
||||
if (!$codebase->classlikes->classExists($first_arg->value->value)) {
|
||||
$context->phantom_classes[strtolower($first_arg->value->value)] = true;
|
||||
}
|
||||
} elseif ($first_arg->value instanceof PhpParser\Node\Expr\ClassConstFetch
|
||||
&& $first_arg->value->class instanceof PhpParser\Node\Name
|
||||
&& $first_arg->value->name instanceof PhpParser\Node\Identifier
|
||||
&& $first_arg->value->name->name === 'class'
|
||||
) {
|
||||
$resolved_name = (string) $first_arg->value->class->getAttribute('resolvedName');
|
||||
|
||||
if (!$codebase->classlikes->classExists($resolved_name)) {
|
||||
$context->phantom_classes[strtolower($resolved_name)] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_name->parts === ['interface_exists']) {
|
||||
if ($first_arg) {
|
||||
if ($first_arg->value instanceof PhpParser\Node\Scalar\String_) {
|
||||
$context->phantom_classes[strtolower($first_arg->value->value)] = true;
|
||||
} elseif ($first_arg->value instanceof PhpParser\Node\Expr\ClassConstFetch
|
||||
&& $first_arg->value->class instanceof PhpParser\Node\Name
|
||||
&& $first_arg->value->name instanceof PhpParser\Node\Identifier
|
||||
&& $first_arg->value->name->name === 'class'
|
||||
) {
|
||||
$resolved_name = (string) $first_arg->value->class->getAttribute('resolvedName');
|
||||
|
||||
if (!$codebase->classlikes->interfaceExists($resolved_name)) {
|
||||
$context->phantom_classes[strtolower($resolved_name)] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_name->parts === ['file_exists'] && $first_arg) {
|
||||
$var_id = ExpressionIdentifier::getArrayVarId($first_arg->value, null);
|
||||
|
||||
if ($var_id) {
|
||||
$context->phantom_files[$var_id] = true;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_name->parts === ['extension_loaded']) {
|
||||
if ($first_arg
|
||||
&& $first_arg->value instanceof PhpParser\Node\Scalar\String_
|
||||
) {
|
||||
if (@extension_loaded($first_arg->value->value)) {
|
||||
// do nothing
|
||||
} else {
|
||||
$context->check_classes = false;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_name->parts === ['function_exists']) {
|
||||
$context->check_functions = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_name->parts === ['is_callable']) {
|
||||
$context->check_methods = false;
|
||||
$context->check_functions = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_name->parts === ['defined']) {
|
||||
$context->check_consts = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_name->parts === ['extract']) {
|
||||
$context->check_variables = false;
|
||||
|
||||
foreach ($context->vars_in_scope as $var_id => $_) {
|
||||
if ($var_id === '$this' || strpos($var_id, '[') || strpos($var_id, '>')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mixed_type = Type::getMixed();
|
||||
$mixed_type->parent_nodes = $context->vars_in_scope[$var_id]->parent_nodes;
|
||||
|
||||
$context->vars_in_scope[$var_id] = $mixed_type;
|
||||
$context->assigned_var_ids[$var_id] = (int) $stmt->getAttribute('startFilePos');
|
||||
$context->possibly_assigned_var_ids[$var_id] = true;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_name->parts === ['compact']) {
|
||||
$all_args_string_literals = true;
|
||||
$new_items = [];
|
||||
|
||||
foreach ($stmt->args as $arg) {
|
||||
$arg_type = $statements_analyzer->node_data->getType($arg->value);
|
||||
|
||||
if (!$arg_type || !$arg_type->isSingleStringLiteral()) {
|
||||
$all_args_string_literals = false;
|
||||
break;
|
||||
}
|
||||
|
||||
$var_name = $arg_type->getSingleStringLiteral()->value;
|
||||
|
||||
$new_items[] = new PhpParser\Node\Expr\ArrayItem(
|
||||
new PhpParser\Node\Expr\Variable($var_name, $arg->value->getAttributes()),
|
||||
new PhpParser\Node\Scalar\String_($var_name, $arg->value->getAttributes()),
|
||||
false,
|
||||
$arg->getAttributes()
|
||||
);
|
||||
}
|
||||
|
||||
if ($all_args_string_literals) {
|
||||
$arr = new PhpParser\Node\Expr\Array_($new_items, $stmt->getAttributes());
|
||||
$old_node_data = $statements_analyzer->node_data;
|
||||
$statements_analyzer->node_data = clone $statements_analyzer->node_data;
|
||||
|
||||
ExpressionAnalyzer::analyze($statements_analyzer, $arr, $context);
|
||||
|
||||
$arr_type = $statements_analyzer->node_data->getType($arr);
|
||||
|
||||
$statements_analyzer->node_data = $old_node_data;
|
||||
|
||||
if ($arr_type) {
|
||||
$statements_analyzer->node_data->setType($stmt, $arr_type);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_name->parts === ['func_get_args']) {
|
||||
$source = $statements_analyzer->getSource();
|
||||
|
||||
if ($statements_analyzer->data_flow_graph
|
||||
&& $source instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
|
||||
) {
|
||||
if ($statements_analyzer->data_flow_graph instanceof \Psalm\Internal\Codebase\VariableUseGraph) {
|
||||
foreach ($source->param_nodes as $param_node) {
|
||||
$statements_analyzer->data_flow_graph->addPath(
|
||||
$param_node,
|
||||
new DataFlowNode('variable-use', 'variable use', null),
|
||||
'variable-use'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (strtolower($function_name->parts[0]) === 'var_dump'
|
||||
|| strtolower($function_name->parts[0]) === 'shell_exec') {
|
||||
if (IssueBuffer::accepts(
|
||||
new ForbiddenCode(
|
||||
'Unsafe ' . implode('', $function_name->parts),
|
||||
new CodeLocation($statements_analyzer->getSource(), $stmt)
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues()
|
||||
)) {
|
||||
// continue
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($codebase->config->forbidden_functions[strtolower((string) $function_name)])) {
|
||||
if (IssueBuffer::accepts(
|
||||
new ForbiddenCode(
|
||||
'You have forbidden the use of ' . $function_name,
|
||||
new CodeLocation($statements_analyzer->getSource(), $stmt)
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues()
|
||||
)) {
|
||||
// continue
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_name->parts === ['define']) {
|
||||
if ($first_arg) {
|
||||
$fq_const_name = ConstFetchAnalyzer::getConstName(
|
||||
$first_arg->value,
|
||||
$statements_analyzer->node_data,
|
||||
$codebase,
|
||||
$statements_analyzer->getAliases()
|
||||
);
|
||||
|
||||
if ($fq_const_name !== null && isset($stmt->args[1])) {
|
||||
$second_arg = $stmt->args[1];
|
||||
$was_in_call = $context->inside_call;
|
||||
$context->inside_call = true;
|
||||
ExpressionAnalyzer::analyze($statements_analyzer, $second_arg->value, $context);
|
||||
$context->inside_call = $was_in_call;
|
||||
|
||||
ConstFetchAnalyzer::setConstType(
|
||||
$statements_analyzer,
|
||||
$fq_const_name,
|
||||
$statements_analyzer->node_data->getType($second_arg->value) ?: Type::getMixed(),
|
||||
$context
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$context->check_consts = false;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_name->parts === ['constant']) {
|
||||
if ($first_arg) {
|
||||
$fq_const_name = ConstFetchAnalyzer::getConstName(
|
||||
$first_arg->value,
|
||||
$statements_analyzer->node_data,
|
||||
$codebase,
|
||||
$statements_analyzer->getAliases()
|
||||
);
|
||||
|
||||
if ($fq_const_name !== null) {
|
||||
$const_type = ConstFetchAnalyzer::getConstType(
|
||||
$statements_analyzer,
|
||||
$fq_const_name,
|
||||
true,
|
||||
$context
|
||||
);
|
||||
|
||||
if ($const_type) {
|
||||
$statements_analyzer->node_data->setType($real_stmt, $const_type);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$context->check_consts = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($first_arg
|
||||
&& $function_id
|
||||
&& strpos($function_id, 'is_') === 0
|
||||
&& $function_id !== 'is_a'
|
||||
&& !$context->inside_negation
|
||||
) {
|
||||
$stmt_assertions = $statements_analyzer->node_data->getAssertions($stmt);
|
||||
|
||||
if ($stmt_assertions !== null) {
|
||||
$anded_assertions = $stmt_assertions;
|
||||
} else {
|
||||
$anded_assertions = AssertionFinder::processFunctionCall(
|
||||
$stmt,
|
||||
$context->self,
|
||||
$statements_analyzer,
|
||||
$codebase,
|
||||
$context->inside_negation
|
||||
);
|
||||
}
|
||||
|
||||
$changed_vars = [];
|
||||
|
||||
foreach ($anded_assertions as $assertions) {
|
||||
$referenced_var_ids = array_map(
|
||||
function (array $_) : bool {
|
||||
return true;
|
||||
},
|
||||
$assertions
|
||||
);
|
||||
|
||||
Reconciler::reconcileKeyedTypes(
|
||||
$assertions,
|
||||
$assertions,
|
||||
$context->vars_in_scope,
|
||||
$changed_vars,
|
||||
$referenced_var_ids,
|
||||
$statements_analyzer,
|
||||
[],
|
||||
$context->inside_loop,
|
||||
new CodeLocation($statements_analyzer->getSource(), $stmt)
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($first_arg && $function_id === 'strtolower') {
|
||||
$first_arg_type = $statements_analyzer->node_data->getType($first_arg->value);
|
||||
|
||||
if ($first_arg_type
|
||||
&& UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$first_arg_type,
|
||||
new Type\Union([new Type\Atomic\TLowercaseString()])
|
||||
)
|
||||
) {
|
||||
if ($first_arg_type->from_docblock) {
|
||||
if (IssueBuffer::accepts(
|
||||
new \Psalm\Issue\RedundantConditionGivenDocblockType(
|
||||
'The call to strtolower is unnecessary given the docblock type',
|
||||
new CodeLocation($statements_analyzer, $function_name),
|
||||
null
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues()
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
} else {
|
||||
if (IssueBuffer::accepts(
|
||||
new \Psalm\Issue\RedundantCondition(
|
||||
'The call to strtolower is unnecessary',
|
||||
new CodeLocation($statements_analyzer, $function_name),
|
||||
null
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues()
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ use Psalm\Internal\Analyzer\FunctionLikeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\MethodAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentMapPopulator;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\Call\ClassTemplateParamCollector;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
@ -25,7 +24,6 @@ use function array_shift;
|
||||
use function get_class;
|
||||
use function strtolower;
|
||||
use function array_merge;
|
||||
use function explode;
|
||||
|
||||
/**
|
||||
* This is a bunch of complex logic to handle the potential for missing methods,
|
||||
@ -50,8 +48,6 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
?string $lhs_var_id,
|
||||
AtomicMethodCallAnalysisResult $result
|
||||
) : void {
|
||||
$config = $codebase->config;
|
||||
|
||||
if ($lhs_type_part instanceof Type\Atomic\TTemplateParam
|
||||
&& !$lhs_type_part->as->isMixed()
|
||||
) {
|
||||
@ -158,55 +154,6 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
|
||||
$intersection_types = $lhs_type_part->getIntersectionTypes();
|
||||
|
||||
$all_intersection_return_type = null;
|
||||
$all_intersection_existent_method_ids = [];
|
||||
|
||||
if ($intersection_types) {
|
||||
foreach ($intersection_types as $intersection_type) {
|
||||
$intersection_result = clone $result;
|
||||
|
||||
/** @var ?Type\Union */
|
||||
$intersection_result->return_type = null;
|
||||
|
||||
self::analyze(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
$codebase,
|
||||
$context,
|
||||
$intersection_type,
|
||||
$lhs_type_part,
|
||||
true,
|
||||
$lhs_var_id,
|
||||
$intersection_result
|
||||
);
|
||||
|
||||
$result->returns_by_ref = $intersection_result->returns_by_ref;
|
||||
$result->has_mock = $intersection_result->has_mock;
|
||||
$result->has_valid_method_call_type = $intersection_result->has_valid_method_call_type;
|
||||
$result->has_mixed_method_call = $intersection_result->has_mixed_method_call;
|
||||
$result->invalid_method_call_types = $intersection_result->invalid_method_call_types;
|
||||
$result->check_visibility = $intersection_result->check_visibility;
|
||||
$result->too_many_arguments = $intersection_result->too_many_arguments;
|
||||
|
||||
$all_intersection_existent_method_ids = array_merge(
|
||||
$all_intersection_existent_method_ids,
|
||||
$intersection_result->existent_method_ids
|
||||
);
|
||||
|
||||
if ($intersection_result->return_type) {
|
||||
if (!$all_intersection_return_type || $all_intersection_return_type->isMixed()) {
|
||||
$all_intersection_return_type = $intersection_result->return_type;
|
||||
} else {
|
||||
$all_intersection_return_type = Type::intersectUnionTypes(
|
||||
$all_intersection_return_type,
|
||||
$intersection_result->return_type,
|
||||
$codebase
|
||||
) ?: Type::getMixed();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$stmt->name instanceof PhpParser\Node\Identifier) {
|
||||
if (!$context->ignore_variable_method) {
|
||||
$codebase->analyzer->addMixedMemberName(
|
||||
@ -231,11 +178,6 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
$method_name_lc = strtolower($stmt->name->name);
|
||||
|
||||
$method_id = new MethodIdentifier($fq_class_name, $method_name_lc);
|
||||
$cased_method_id = $fq_class_name . '::' . $stmt->name->name;
|
||||
|
||||
$intersection_method_id = $intersection_types
|
||||
? '(' . $lhs_type_part . ')' . '::' . $stmt->name->name
|
||||
: null;
|
||||
|
||||
$args = $stmt->args;
|
||||
|
||||
@ -289,6 +231,23 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
);
|
||||
}
|
||||
|
||||
$all_intersection_return_type = null;
|
||||
$all_intersection_existent_method_ids = [];
|
||||
|
||||
if ($intersection_types) {
|
||||
[$all_intersection_return_type, $all_intersection_existent_method_ids]
|
||||
= self::getIntersectionReturnType(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
$codebase,
|
||||
$context,
|
||||
$lhs_type_part,
|
||||
$lhs_var_id,
|
||||
$result,
|
||||
$intersection_types
|
||||
);
|
||||
}
|
||||
|
||||
if (($fake_method_exists
|
||||
&& $codebase->methods->methodExists(new MethodIdentifier($fq_class_name, '__call')))
|
||||
|| !$naive_method_exists
|
||||
@ -337,7 +296,7 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
$method_id,
|
||||
$class_storage,
|
||||
$context,
|
||||
$config,
|
||||
$codebase->config,
|
||||
$all_intersection_return_type,
|
||||
$result
|
||||
);
|
||||
@ -363,6 +322,11 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
$classlike_source = $source_source->getSource();
|
||||
$classlike_source_fqcln = $classlike_source ? $classlike_source->getFQCLN() : null;
|
||||
|
||||
$intersection_method_id = $intersection_types
|
||||
? '(' . $lhs_type_part . ')' . '::' . $stmt->name->name
|
||||
: null;
|
||||
$cased_method_id = $fq_class_name . '::' . $stmt->name->name;
|
||||
|
||||
if ($lhs_var_id === '$this'
|
||||
&& $context->self
|
||||
&& $classlike_source_fqcln
|
||||
@ -397,7 +361,7 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
));
|
||||
|
||||
if (!$corrected_method_exists
|
||||
|| ($config->use_phpdoc_method_without_magic_or_parent
|
||||
|| ($codebase->config->use_phpdoc_method_without_magic_or_parent
|
||||
&& isset($class_storage->pseudo_methods[$method_name_lc]))
|
||||
) {
|
||||
MissingMethodCallHandler::handleMissingOrMagicMethod(
|
||||
@ -407,69 +371,17 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
$method_id,
|
||||
$is_interface,
|
||||
$context,
|
||||
$config,
|
||||
$codebase->config,
|
||||
$all_intersection_return_type,
|
||||
$all_intersection_existent_method_ids,
|
||||
$intersection_method_id,
|
||||
$cased_method_id,
|
||||
$result
|
||||
);
|
||||
|
||||
if ($all_intersection_return_type && $all_intersection_existent_method_ids) {
|
||||
$result->existent_method_ids = array_merge(
|
||||
$result->existent_method_ids,
|
||||
$all_intersection_existent_method_ids
|
||||
);
|
||||
|
||||
if (!$result->return_type) {
|
||||
$result->return_type = $all_intersection_return_type;
|
||||
} else {
|
||||
$result->return_type = Type::combineUnionTypes($all_intersection_return_type, $result->return_type);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ((!$is_interface && !$config->use_phpdoc_method_without_magic_or_parent)
|
||||
|| !isset($class_storage->pseudo_methods[$method_name_lc])
|
||||
) {
|
||||
if ($is_interface) {
|
||||
$result->non_existent_interface_method_ids[] = $intersection_method_id ?: $cased_method_id;
|
||||
} else {
|
||||
$result->non_existent_class_method_ids[] = $intersection_method_id ?: $cased_method_id;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($context->collect_initializations && $context->calling_method_id) {
|
||||
[$calling_method_class] = explode('::', $context->calling_method_id);
|
||||
$codebase->file_reference_provider->addMethodReferenceToClassMember(
|
||||
$calling_method_class . '::__construct',
|
||||
strtolower((string) $method_id)
|
||||
);
|
||||
}
|
||||
|
||||
if ($codebase->store_node_types
|
||||
&& !$context->collect_initializations
|
||||
&& !$context->collect_mutations
|
||||
) {
|
||||
ArgumentMapPopulator::recordArgumentPositions(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
$codebase,
|
||||
(string) $method_id
|
||||
);
|
||||
}
|
||||
|
||||
$result->existent_method_ids[] = $method_id;
|
||||
|
||||
$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
|
||||
|
||||
$in_call_map = InternalCallMapHandler::inCallMap((string) ($declaring_method_id ?: $method_id));
|
||||
|
||||
if (!$lhs_type_part instanceof Type\Atomic\TNamedObject) {
|
||||
throw new \UnexpectedValueException('should be a named object here');
|
||||
}
|
||||
|
||||
$old_node_data = $statements_analyzer->node_data;
|
||||
|
||||
$return_type_candidate = ExistingAtomicMethodCallAnalyzer::analyze(
|
||||
@ -488,6 +400,10 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
|
||||
$statements_analyzer->node_data = $old_node_data;
|
||||
|
||||
$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
|
||||
|
||||
$in_call_map = InternalCallMapHandler::inCallMap((string) ($declaring_method_id ?: $method_id));
|
||||
|
||||
if (!$in_call_map) {
|
||||
if ($result->check_visibility) {
|
||||
$name_code_location = new CodeLocation($statements_analyzer, $stmt->name);
|
||||
@ -511,6 +427,72 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Type\Atomic\TNamedObject|Type\Atomic\TTemplateParam $lhs_type_part
|
||||
* @param array<string, Type\Atomic> $intersection_types
|
||||
*
|
||||
* @return array{?Type\Union, array<string>}
|
||||
*/
|
||||
private static function getIntersectionReturnType(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr\MethodCall $stmt,
|
||||
Codebase $codebase,
|
||||
Context $context,
|
||||
Type\Atomic $lhs_type_part,
|
||||
?string $lhs_var_id,
|
||||
AtomicMethodCallAnalysisResult $result,
|
||||
array $intersection_types
|
||||
) : array {
|
||||
$all_intersection_return_type = null;
|
||||
$all_intersection_existent_method_ids = [];
|
||||
|
||||
foreach ($intersection_types as $intersection_type) {
|
||||
$intersection_result = clone $result;
|
||||
|
||||
/** @var ?Type\Union */
|
||||
$intersection_result->return_type = null;
|
||||
|
||||
self::analyze(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
$codebase,
|
||||
$context,
|
||||
$intersection_type,
|
||||
$lhs_type_part,
|
||||
true,
|
||||
$lhs_var_id,
|
||||
$intersection_result
|
||||
);
|
||||
|
||||
$result->returns_by_ref = $intersection_result->returns_by_ref;
|
||||
$result->has_mock = $intersection_result->has_mock;
|
||||
$result->has_valid_method_call_type = $intersection_result->has_valid_method_call_type;
|
||||
$result->has_mixed_method_call = $intersection_result->has_mixed_method_call;
|
||||
$result->invalid_method_call_types = $intersection_result->invalid_method_call_types;
|
||||
$result->check_visibility = $intersection_result->check_visibility;
|
||||
$result->too_many_arguments = $intersection_result->too_many_arguments;
|
||||
|
||||
$all_intersection_existent_method_ids = array_merge(
|
||||
$all_intersection_existent_method_ids,
|
||||
$intersection_result->existent_method_ids
|
||||
);
|
||||
|
||||
if ($intersection_result->return_type) {
|
||||
if (!$all_intersection_return_type || $all_intersection_return_type->isMixed()) {
|
||||
$all_intersection_return_type = $intersection_result->return_type;
|
||||
} else {
|
||||
$all_intersection_return_type = Type::intersectUnionTypes(
|
||||
$all_intersection_return_type,
|
||||
$intersection_result->return_type,
|
||||
$codebase
|
||||
) ?: Type::getMixed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [$all_intersection_return_type, $all_intersection_existent_method_ids];
|
||||
}
|
||||
|
||||
private static function updateResultReturnType(
|
||||
AtomicMethodCallAnalysisResult $result,
|
||||
?Type\Union $return_type_candidate,
|
||||
@ -639,11 +621,11 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
|
||||
/**
|
||||
* @param lowercase-string $method_name_lc
|
||||
* @return array{Type\Atomic, \Psalm\Storage\ClassLikeStorage, bool, MethodIdentifier, string}
|
||||
* @return array{Type\Atomic\TNamedObject, \Psalm\Storage\ClassLikeStorage, bool, MethodIdentifier, string}
|
||||
*/
|
||||
private static function handleMixins(
|
||||
\Psalm\Storage\ClassLikeStorage $class_storage,
|
||||
Type\Atomic $lhs_type_part,
|
||||
Type\Atomic\TNamedObject $lhs_type_part,
|
||||
string $method_name_lc,
|
||||
Codebase $codebase,
|
||||
Context $context,
|
||||
|
@ -62,6 +62,28 @@ class ExistingAtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
|
||||
$cased_method_id = $fq_class_name . '::' . $stmt_name->name;
|
||||
|
||||
$result->existent_method_ids[] = $method_id;
|
||||
|
||||
if ($context->collect_initializations && $context->calling_method_id) {
|
||||
[$calling_method_class] = explode('::', $context->calling_method_id);
|
||||
$codebase->file_reference_provider->addMethodReferenceToClassMember(
|
||||
$calling_method_class . '::__construct',
|
||||
strtolower((string) $method_id)
|
||||
);
|
||||
}
|
||||
|
||||
if ($codebase->store_node_types
|
||||
&& !$context->collect_initializations
|
||||
&& !$context->collect_mutations
|
||||
) {
|
||||
ArgumentMapPopulator::recordArgumentPositions(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
$codebase,
|
||||
(string) $method_id
|
||||
);
|
||||
}
|
||||
|
||||
if ($fq_class_name === 'Closure' && $method_name_lc === '__invoke') {
|
||||
$statements_analyzer->node_data = clone $statements_analyzer->node_data;
|
||||
|
||||
|
@ -11,6 +11,7 @@ use Psalm\Context;
|
||||
use Psalm\Internal\MethodIdentifier;
|
||||
use Psalm\Type;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
|
||||
class MissingMethodCallHandler
|
||||
{
|
||||
@ -189,6 +190,9 @@ class MissingMethodCallHandler
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $all_intersection_existent_method_ids
|
||||
*/
|
||||
public static function handleMissingOrMagicMethod(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
Codebase $codebase,
|
||||
@ -198,6 +202,9 @@ class MissingMethodCallHandler
|
||||
Context $context,
|
||||
\Psalm\Config $config,
|
||||
?Type\Union $all_intersection_return_type,
|
||||
array $all_intersection_existent_method_ids,
|
||||
?string $intersection_method_id,
|
||||
string $cased_method_id,
|
||||
AtomicMethodCallAnalysisResult $result
|
||||
) : void {
|
||||
$fq_class_name = $method_id->fq_class_name;
|
||||
@ -284,5 +291,30 @@ class MissingMethodCallHandler
|
||||
) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($all_intersection_return_type && $all_intersection_existent_method_ids) {
|
||||
$result->existent_method_ids = array_merge(
|
||||
$result->existent_method_ids,
|
||||
$all_intersection_existent_method_ids
|
||||
);
|
||||
|
||||
if (!$result->return_type) {
|
||||
$result->return_type = $all_intersection_return_type;
|
||||
} else {
|
||||
$result->return_type = Type::combineUnionTypes($all_intersection_return_type, $result->return_type);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ((!$is_interface && !$config->use_phpdoc_method_without_magic_or_parent)
|
||||
|| !isset($class_storage->pseudo_methods[$method_name_lc])
|
||||
) {
|
||||
if ($is_interface) {
|
||||
$result->non_existent_interface_method_ids[] = $intersection_method_id ?: $cased_method_id;
|
||||
} else {
|
||||
$result->non_existent_class_method_ids[] = $intersection_method_id ?: $cased_method_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,596 @@
|
||||
<?php
|
||||
namespace Psalm\Internal\Analyzer\Statements\Expression\Call;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\AssertionFinder;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ConstFetchAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\DataFlow\DataFlowNode;
|
||||
use Psalm\Issue\ForbiddenCode;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
use Psalm\Type\Reconciler;
|
||||
use function implode;
|
||||
use function strtolower;
|
||||
use function is_string;
|
||||
use function array_map;
|
||||
use function extension_loaded;
|
||||
use function strpos;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class NamedFunctionCallHandler
|
||||
{
|
||||
/**
|
||||
* @param lowercase-string $function_id
|
||||
*/
|
||||
public static function handle(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
\Psalm\Codebase $codebase,
|
||||
PhpParser\Node\Expr\FuncCall $stmt,
|
||||
PhpParser\Node\Expr\FuncCall $real_stmt,
|
||||
PhpParser\Node\Name $function_name,
|
||||
?string $function_id,
|
||||
Context $context
|
||||
) : void {
|
||||
if ($function_id === 'get_class'
|
||||
|| $function_id === 'gettype'
|
||||
|| $function_id === 'get_debug_type'
|
||||
) {
|
||||
self::handleDependentTypeFunction(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
$real_stmt,
|
||||
$function_id,
|
||||
$context
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$first_arg = isset($stmt->args[0]) ? $stmt->args[0] : null;
|
||||
|
||||
if ($function_id === 'method_exists') {
|
||||
$second_arg = isset($stmt->args[1]) ? $stmt->args[1] : null;
|
||||
|
||||
if ($first_arg
|
||||
&& $first_arg->value instanceof PhpParser\Node\Expr\Variable
|
||||
&& $second_arg
|
||||
&& $second_arg->value instanceof PhpParser\Node\Scalar\String_
|
||||
) {
|
||||
// do nothing
|
||||
} else {
|
||||
$context->check_methods = false;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_id === 'class_exists') {
|
||||
if ($first_arg) {
|
||||
if ($first_arg->value instanceof PhpParser\Node\Scalar\String_) {
|
||||
if (!$codebase->classlikes->classExists($first_arg->value->value)) {
|
||||
$context->phantom_classes[strtolower($first_arg->value->value)] = true;
|
||||
}
|
||||
} elseif ($first_arg->value instanceof PhpParser\Node\Expr\ClassConstFetch
|
||||
&& $first_arg->value->class instanceof PhpParser\Node\Name
|
||||
&& $first_arg->value->name instanceof PhpParser\Node\Identifier
|
||||
&& $first_arg->value->name->name === 'class'
|
||||
) {
|
||||
$resolved_name = (string) $first_arg->value->class->getAttribute('resolvedName');
|
||||
|
||||
if (!$codebase->classlikes->classExists($resolved_name)) {
|
||||
$context->phantom_classes[strtolower($resolved_name)] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_id === 'interface_exists') {
|
||||
if ($first_arg) {
|
||||
if ($first_arg->value instanceof PhpParser\Node\Scalar\String_) {
|
||||
$context->phantom_classes[strtolower($first_arg->value->value)] = true;
|
||||
} elseif ($first_arg->value instanceof PhpParser\Node\Expr\ClassConstFetch
|
||||
&& $first_arg->value->class instanceof PhpParser\Node\Name
|
||||
&& $first_arg->value->name instanceof PhpParser\Node\Identifier
|
||||
&& $first_arg->value->name->name === 'class'
|
||||
) {
|
||||
$resolved_name = (string) $first_arg->value->class->getAttribute('resolvedName');
|
||||
|
||||
if (!$codebase->classlikes->interfaceExists($resolved_name)) {
|
||||
$context->phantom_classes[strtolower($resolved_name)] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_id === 'file_exists' && $first_arg) {
|
||||
$var_id = ExpressionIdentifier::getArrayVarId($first_arg->value, null);
|
||||
|
||||
if ($var_id) {
|
||||
$context->phantom_files[$var_id] = true;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_id === 'extension_loaded') {
|
||||
if ($first_arg
|
||||
&& $first_arg->value instanceof PhpParser\Node\Scalar\String_
|
||||
) {
|
||||
if (@extension_loaded($first_arg->value->value)) {
|
||||
// do nothing
|
||||
} else {
|
||||
$context->check_classes = false;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_id === 'function_exists') {
|
||||
$context->check_functions = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_id === 'is_callable') {
|
||||
$context->check_methods = false;
|
||||
$context->check_functions = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_id === 'defined') {
|
||||
$context->check_consts = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_id === 'extract') {
|
||||
$context->check_variables = false;
|
||||
|
||||
foreach ($context->vars_in_scope as $var_id => $_) {
|
||||
if ($var_id === '$this' || strpos($var_id, '[') || strpos($var_id, '>')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mixed_type = Type::getMixed();
|
||||
$mixed_type->parent_nodes = $context->vars_in_scope[$var_id]->parent_nodes;
|
||||
|
||||
$context->vars_in_scope[$var_id] = $mixed_type;
|
||||
$context->assigned_var_ids[$var_id] = (int) $stmt->getAttribute('startFilePos');
|
||||
$context->possibly_assigned_var_ids[$var_id] = true;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_id === 'compact') {
|
||||
$all_args_string_literals = true;
|
||||
$new_items = [];
|
||||
|
||||
foreach ($stmt->args as $arg) {
|
||||
$arg_type = $statements_analyzer->node_data->getType($arg->value);
|
||||
|
||||
if (!$arg_type || !$arg_type->isSingleStringLiteral()) {
|
||||
$all_args_string_literals = false;
|
||||
break;
|
||||
}
|
||||
|
||||
$var_name = $arg_type->getSingleStringLiteral()->value;
|
||||
|
||||
$new_items[] = new PhpParser\Node\Expr\ArrayItem(
|
||||
new PhpParser\Node\Expr\Variable($var_name, $arg->value->getAttributes()),
|
||||
new PhpParser\Node\Scalar\String_($var_name, $arg->value->getAttributes()),
|
||||
false,
|
||||
$arg->getAttributes()
|
||||
);
|
||||
}
|
||||
|
||||
if ($all_args_string_literals) {
|
||||
$arr = new PhpParser\Node\Expr\Array_($new_items, $stmt->getAttributes());
|
||||
$old_node_data = $statements_analyzer->node_data;
|
||||
$statements_analyzer->node_data = clone $statements_analyzer->node_data;
|
||||
|
||||
ExpressionAnalyzer::analyze($statements_analyzer, $arr, $context);
|
||||
|
||||
$arr_type = $statements_analyzer->node_data->getType($arr);
|
||||
|
||||
$statements_analyzer->node_data = $old_node_data;
|
||||
|
||||
if ($arr_type) {
|
||||
$statements_analyzer->node_data->setType($stmt, $arr_type);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_id === 'func_get_args') {
|
||||
$source = $statements_analyzer->getSource();
|
||||
|
||||
if ($statements_analyzer->data_flow_graph
|
||||
&& $source instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
|
||||
) {
|
||||
if ($statements_analyzer->data_flow_graph instanceof \Psalm\Internal\Codebase\VariableUseGraph) {
|
||||
foreach ($source->param_nodes as $param_node) {
|
||||
$statements_analyzer->data_flow_graph->addPath(
|
||||
$param_node,
|
||||
new DataFlowNode('variable-use', 'variable use', null),
|
||||
'variable-use'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_id === 'var_dump'
|
||||
|| $function_id === 'shell_exec'
|
||||
) {
|
||||
if (IssueBuffer::accepts(
|
||||
new ForbiddenCode(
|
||||
'Unsafe ' . implode('', $function_name->parts),
|
||||
new CodeLocation($statements_analyzer->getSource(), $stmt)
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues()
|
||||
)) {
|
||||
// continue
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($codebase->config->forbidden_functions[strtolower((string) $function_name)])) {
|
||||
if (IssueBuffer::accepts(
|
||||
new ForbiddenCode(
|
||||
'You have forbidden the use of ' . $function_name,
|
||||
new CodeLocation($statements_analyzer->getSource(), $stmt)
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues()
|
||||
)) {
|
||||
// continue
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_id === 'define') {
|
||||
if ($first_arg) {
|
||||
$fq_const_name = ConstFetchAnalyzer::getConstName(
|
||||
$first_arg->value,
|
||||
$statements_analyzer->node_data,
|
||||
$codebase,
|
||||
$statements_analyzer->getAliases()
|
||||
);
|
||||
|
||||
if ($fq_const_name !== null && isset($stmt->args[1])) {
|
||||
$second_arg = $stmt->args[1];
|
||||
$was_in_call = $context->inside_call;
|
||||
$context->inside_call = true;
|
||||
ExpressionAnalyzer::analyze($statements_analyzer, $second_arg->value, $context);
|
||||
$context->inside_call = $was_in_call;
|
||||
|
||||
ConstFetchAnalyzer::setConstType(
|
||||
$statements_analyzer,
|
||||
$fq_const_name,
|
||||
$statements_analyzer->node_data->getType($second_arg->value) ?: Type::getMixed(),
|
||||
$context
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$context->check_consts = false;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($function_id === 'constant') {
|
||||
if ($first_arg) {
|
||||
$fq_const_name = ConstFetchAnalyzer::getConstName(
|
||||
$first_arg->value,
|
||||
$statements_analyzer->node_data,
|
||||
$codebase,
|
||||
$statements_analyzer->getAliases()
|
||||
);
|
||||
|
||||
if ($fq_const_name !== null) {
|
||||
$const_type = ConstFetchAnalyzer::getConstType(
|
||||
$statements_analyzer,
|
||||
$fq_const_name,
|
||||
true,
|
||||
$context
|
||||
);
|
||||
|
||||
if ($const_type) {
|
||||
$statements_analyzer->node_data->setType($real_stmt, $const_type);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$context->check_consts = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($first_arg
|
||||
&& $function_id
|
||||
&& strpos($function_id, 'is_') === 0
|
||||
&& $function_id !== 'is_a'
|
||||
&& !$context->inside_negation
|
||||
) {
|
||||
$stmt_assertions = $statements_analyzer->node_data->getAssertions($stmt);
|
||||
|
||||
if ($stmt_assertions !== null) {
|
||||
$anded_assertions = $stmt_assertions;
|
||||
} else {
|
||||
$anded_assertions = AssertionFinder::processFunctionCall(
|
||||
$stmt,
|
||||
$context->self,
|
||||
$statements_analyzer,
|
||||
$codebase,
|
||||
$context->inside_negation
|
||||
);
|
||||
}
|
||||
|
||||
$changed_vars = [];
|
||||
|
||||
foreach ($anded_assertions as $assertions) {
|
||||
$referenced_var_ids = array_map(
|
||||
function (array $_) : bool {
|
||||
return true;
|
||||
},
|
||||
$assertions
|
||||
);
|
||||
|
||||
Reconciler::reconcileKeyedTypes(
|
||||
$assertions,
|
||||
$assertions,
|
||||
$context->vars_in_scope,
|
||||
$changed_vars,
|
||||
$referenced_var_ids,
|
||||
$statements_analyzer,
|
||||
[],
|
||||
$context->inside_loop,
|
||||
new CodeLocation($statements_analyzer->getSource(), $stmt)
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($first_arg && $function_id === 'strtolower') {
|
||||
$first_arg_type = $statements_analyzer->node_data->getType($first_arg->value);
|
||||
|
||||
if ($first_arg_type
|
||||
&& UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$first_arg_type,
|
||||
new Type\Union([new Type\Atomic\TLowercaseString()])
|
||||
)
|
||||
) {
|
||||
if ($first_arg_type->from_docblock) {
|
||||
if (IssueBuffer::accepts(
|
||||
new \Psalm\Issue\RedundantConditionGivenDocblockType(
|
||||
'The call to strtolower is unnecessary given the docblock type',
|
||||
new CodeLocation($statements_analyzer, $function_name),
|
||||
null
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues()
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
} else {
|
||||
if (IssueBuffer::accepts(
|
||||
new \Psalm\Issue\RedundantCondition(
|
||||
'The call to strtolower is unnecessary',
|
||||
new CodeLocation($statements_analyzer, $function_name),
|
||||
null
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues()
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function handleDependentTypeFunction(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr\FuncCall $stmt,
|
||||
PhpParser\Node\Expr\FuncCall $real_stmt,
|
||||
?string $function_id,
|
||||
Context $context
|
||||
) : void {
|
||||
$first_arg = isset($stmt->args[0]) ? $stmt->args[0] : null;
|
||||
|
||||
if ($first_arg) {
|
||||
$var = $first_arg->value;
|
||||
|
||||
if ($var instanceof PhpParser\Node\Expr\Variable
|
||||
&& is_string($var->name)
|
||||
) {
|
||||
$var_id = '$' . $var->name;
|
||||
|
||||
if (isset($context->vars_in_scope[$var_id])) {
|
||||
if ($function_id === 'get_class') {
|
||||
$atomic_type = new Type\Atomic\TDependentGetClass(
|
||||
$var_id,
|
||||
$context->vars_in_scope[$var_id]->hasMixed()
|
||||
? Type::getObject()
|
||||
: $context->vars_in_scope[$var_id]
|
||||
);
|
||||
} elseif ($function_id === 'gettype') {
|
||||
$atomic_type = new Type\Atomic\TDependentGetType($var_id);
|
||||
} else {
|
||||
$atomic_type = new Type\Atomic\TDependentGetDebugType($var_id);
|
||||
}
|
||||
|
||||
$statements_analyzer->node_data->setType($real_stmt, new Type\Union([$atomic_type]));
|
||||
}
|
||||
} elseif (($var_type = $statements_analyzer->node_data->getType($var))
|
||||
&& ($function_id === 'get_class'
|
||||
|| $function_id === 'get_debug_type'
|
||||
)
|
||||
) {
|
||||
$class_string_types = [];
|
||||
|
||||
foreach ($var_type->getAtomicTypes() as $class_type) {
|
||||
if ($class_type instanceof Type\Atomic\TNamedObject) {
|
||||
$class_string_types[] = new Type\Atomic\TClassString($class_type->value, clone $class_type);
|
||||
} elseif ($class_type instanceof Type\Atomic\TTemplateParam
|
||||
&& $class_type->as->isSingle()
|
||||
) {
|
||||
$as_atomic_type = \array_values($class_type->as->getAtomicTypes())[0];
|
||||
|
||||
if ($as_atomic_type instanceof Type\Atomic\TObject) {
|
||||
$class_string_types[] = new Type\Atomic\TTemplateParamClass(
|
||||
$class_type->param_name,
|
||||
'object',
|
||||
null,
|
||||
$class_type->defining_class
|
||||
);
|
||||
} elseif ($as_atomic_type instanceof TNamedObject) {
|
||||
$class_string_types[] = new Type\Atomic\TTemplateParamClass(
|
||||
$class_type->param_name,
|
||||
$as_atomic_type->value,
|
||||
$as_atomic_type,
|
||||
$class_type->defining_class
|
||||
);
|
||||
}
|
||||
} elseif ($function_id === 'get_class') {
|
||||
$class_string_types[] = new Type\Atomic\TClassString();
|
||||
} {
|
||||
if ($class_type instanceof Type\Atomic\TInt) {
|
||||
$class_string_types[] = new Type\Atomic\TLiteralString('int');
|
||||
} elseif ($class_type instanceof Type\Atomic\TString) {
|
||||
$class_string_types[] = new Type\Atomic\TLiteralString('string');
|
||||
} elseif ($class_type instanceof Type\Atomic\TFloat) {
|
||||
$class_string_types[] = new Type\Atomic\TLiteralString('float');
|
||||
} elseif ($class_type instanceof Type\Atomic\TBool) {
|
||||
$class_string_types[] = new Type\Atomic\TLiteralString('bool');
|
||||
} elseif ($class_type instanceof Type\Atomic\TClosedResource) {
|
||||
$class_string_types[] = new Type\Atomic\TLiteralString('resource (closed)');
|
||||
} elseif ($class_type instanceof Type\Atomic\TNull) {
|
||||
$class_string_types[] = new Type\Atomic\TLiteralString('null');
|
||||
} else {
|
||||
$class_string_types[] = new Type\Atomic\TString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$statements_analyzer->node_data->setType($real_stmt, new Type\Union($class_string_types));
|
||||
}
|
||||
} elseif ($function_id === 'get_class'
|
||||
&& ($get_class_name = $statements_analyzer->getFQCLN())
|
||||
) {
|
||||
$statements_analyzer->node_data->setType(
|
||||
$real_stmt,
|
||||
new Type\Union([
|
||||
new Type\Atomic\TClassString(
|
||||
$get_class_name,
|
||||
new Type\Atomic\TNamedObject($get_class_name)
|
||||
)
|
||||
])
|
||||
);
|
||||
}if ($first_arg) {
|
||||
$var = $first_arg->value;
|
||||
|
||||
if ($var instanceof PhpParser\Node\Expr\Variable
|
||||
&& is_string($var->name)
|
||||
) {
|
||||
$var_id = '$' . $var->name;
|
||||
|
||||
if (isset($context->vars_in_scope[$var_id])) {
|
||||
if ($function_id === 'get_class') {
|
||||
$atomic_type = new Type\Atomic\TDependentGetClass(
|
||||
$var_id,
|
||||
$context->vars_in_scope[$var_id]->hasMixed()
|
||||
? Type::getObject()
|
||||
: $context->vars_in_scope[$var_id]
|
||||
);
|
||||
} elseif ($function_id === 'gettype') {
|
||||
$atomic_type = new Type\Atomic\TDependentGetType($var_id);
|
||||
} else {
|
||||
$atomic_type = new Type\Atomic\TDependentGetDebugType($var_id);
|
||||
}
|
||||
|
||||
$statements_analyzer->node_data->setType($real_stmt, new Type\Union([$atomic_type]));
|
||||
}
|
||||
} elseif (($var_type = $statements_analyzer->node_data->getType($var))
|
||||
&& ($function_id === 'get_class'
|
||||
|| $function_id === 'get_debug_type'
|
||||
)
|
||||
) {
|
||||
$class_string_types = [];
|
||||
|
||||
foreach ($var_type->getAtomicTypes() as $class_type) {
|
||||
if ($class_type instanceof Type\Atomic\TNamedObject) {
|
||||
$class_string_types[] = new Type\Atomic\TClassString($class_type->value, clone $class_type);
|
||||
} elseif ($class_type instanceof Type\Atomic\TTemplateParam
|
||||
&& $class_type->as->isSingle()
|
||||
) {
|
||||
$as_atomic_type = \array_values($class_type->as->getAtomicTypes())[0];
|
||||
|
||||
if ($as_atomic_type instanceof Type\Atomic\TObject) {
|
||||
$class_string_types[] = new Type\Atomic\TTemplateParamClass(
|
||||
$class_type->param_name,
|
||||
'object',
|
||||
null,
|
||||
$class_type->defining_class
|
||||
);
|
||||
} elseif ($as_atomic_type instanceof TNamedObject) {
|
||||
$class_string_types[] = new Type\Atomic\TTemplateParamClass(
|
||||
$class_type->param_name,
|
||||
$as_atomic_type->value,
|
||||
$as_atomic_type,
|
||||
$class_type->defining_class
|
||||
);
|
||||
}
|
||||
} elseif ($function_id === 'get_class') {
|
||||
$class_string_types[] = new Type\Atomic\TClassString();
|
||||
} else {
|
||||
if ($class_type instanceof Type\Atomic\TInt) {
|
||||
$class_string_types[] = new Type\Atomic\TLiteralString('int');
|
||||
} elseif ($class_type instanceof Type\Atomic\TString) {
|
||||
$class_string_types[] = new Type\Atomic\TLiteralString('string');
|
||||
} elseif ($class_type instanceof Type\Atomic\TFloat) {
|
||||
$class_string_types[] = new Type\Atomic\TLiteralString('float');
|
||||
} elseif ($class_type instanceof Type\Atomic\TBool) {
|
||||
$class_string_types[] = new Type\Atomic\TLiteralString('bool');
|
||||
} elseif ($class_type instanceof Type\Atomic\TClosedResource) {
|
||||
$class_string_types[] = new Type\Atomic\TLiteralString('resource (closed)');
|
||||
} elseif ($class_type instanceof Type\Atomic\TNull) {
|
||||
$class_string_types[] = new Type\Atomic\TLiteralString('null');
|
||||
} else {
|
||||
$class_string_types[] = new Type\Atomic\TString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($class_string_types) {
|
||||
$statements_analyzer->node_data->setType($real_stmt, new Type\Union($class_string_types));
|
||||
}
|
||||
}
|
||||
} elseif ($function_id === 'get_class'
|
||||
&& ($get_class_name = $statements_analyzer->getFQCLN())
|
||||
) {
|
||||
$statements_analyzer->node_data->setType(
|
||||
$real_stmt,
|
||||
new Type\Union([
|
||||
new Type\Atomic\TClassString(
|
||||
$get_class_name,
|
||||
new Type\Atomic\TNamedObject($get_class_name)
|
||||
)
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -28,7 +28,6 @@ use function in_array;
|
||||
use function strtolower;
|
||||
use function implode;
|
||||
use function array_values;
|
||||
use function is_string;
|
||||
use function array_map;
|
||||
|
||||
/**
|
||||
|
@ -20,7 +20,6 @@ use Psalm\Issue\InvalidStringClass;
|
||||
use Psalm\Issue\InternalClass;
|
||||
use Psalm\Issue\MixedMethodCall;
|
||||
use Psalm\Issue\UndefinedClass;
|
||||
use Psalm\Issue\UndefinedMethod;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
@ -28,7 +27,6 @@ use function count;
|
||||
use function in_array;
|
||||
use function strtolower;
|
||||
use function array_map;
|
||||
use function strpos;
|
||||
use function array_filter;
|
||||
|
||||
class AtomicStaticCallAnalyzer
|
||||
|
@ -54,6 +54,7 @@ use function trim;
|
||||
use function array_column;
|
||||
use function array_combine;
|
||||
use function array_keys;
|
||||
use function round;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -705,7 +706,7 @@ class StatementsAnalyzer extends SourceAnalyzer
|
||||
&& $mean > $codebase->config->max_avg_path_length
|
||||
&& $branching > 1.1
|
||||
) {
|
||||
echo $source->getId() . "\n";
|
||||
echo $source->getId() . ' ' . $count . ' ' . round($mean) . ' ' . $branching . "\n";
|
||||
// do something else
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ use function count;
|
||||
use function is_int;
|
||||
use Psalm\Config;
|
||||
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
|
||||
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
||||
use Psalm\Internal\Provider\ClassLikeStorageProvider;
|
||||
use Psalm\Internal\Provider\FileReferenceProvider;
|
||||
use Psalm\Internal\Provider\FileStorageProvider;
|
||||
|
@ -8,7 +8,6 @@ use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\AssertionFinder;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
|
||||
use Psalm\Issue\InvalidReturnType;
|
||||
use Psalm\IssueBuffer;
|
||||
|
@ -8,10 +8,8 @@ use Psalm\Type\Atomic\TArrayKey;
|
||||
use Psalm\Type\Atomic\TFalse;
|
||||
use Psalm\Type\Atomic\TTemplateParam;
|
||||
use Psalm\Type\Atomic\TMixed;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
use Psalm\Type\Atomic\TNull;
|
||||
use Psalm\Type\Atomic\TNumeric;
|
||||
use function get_class;
|
||||
use function array_merge;
|
||||
|
||||
/**
|
||||
|
@ -2,9 +2,7 @@
|
||||
namespace Psalm\Report;
|
||||
|
||||
use Psalm\Internal\Json\Json;
|
||||
use function max;
|
||||
use Psalm\Config;
|
||||
use Psalm\Issue;
|
||||
use Psalm\Report;
|
||||
use function file_exists;
|
||||
use function file_get_contents;
|
||||
|
@ -53,7 +53,7 @@ class ClassLikeStorage
|
||||
public $templatedMixins = [];
|
||||
|
||||
/**
|
||||
* @var Type\Atomic\TNamedObject[]
|
||||
* @var list<Type\Atomic\TNamedObject>
|
||||
*/
|
||||
public $namedMixins = [];
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user