1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-02 17:52:45 +01:00
psalm/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php
Ayesh Karunaratne 431977cb1d
[PHP 8.4] Fixes for implicit nullability deprecation
Fixes all issues that emits a deprecation notice on PHP 8.4.

See:
 - [RFC](https://wiki.php.net/rfc/deprecate-implicitly-nullable-types)
 - [PHP 8.4: Implicitly nullable parameter declarations deprecated](https://php.watch/versions/8.4/implicitly-marking-parameter-type-nullable-deprecated)
2024-03-16 02:19:34 +07:00

2245 lines
80 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Psalm\Internal\Analyzer;
use PhpParser;
use PhpParser\Node\Expr\ArrowFunction;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Function_;
use Psalm\CodeLocation;
use Psalm\Codebase;
use Psalm\Context;
use Psalm\Exception\UnresolvableConstantException;
use Psalm\FileManipulation;
use Psalm\Internal\Analyzer\FunctionLike\ReturnTypeAnalyzer;
use Psalm\Internal\Analyzer\FunctionLike\ReturnTypeCollector;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\Codebase\VariableUseGraph;
use Psalm\Internal\DataFlow\DataFlowNode;
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
use Psalm\Internal\FileManipulation\FunctionDocblockManipulator;
use Psalm\Internal\MethodIdentifier;
use Psalm\Internal\PhpVisitor\NodeCounterVisitor;
use Psalm\Internal\Provider\NodeDataProvider;
use Psalm\Internal\Type\Comparator\TypeComparisonResult;
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
use Psalm\Internal\Type\TemplateInferredTypeReplacer;
use Psalm\Internal\Type\TemplateResult;
use Psalm\Internal\Type\TemplateStandinTypeReplacer;
use Psalm\Internal\Type\TypeExpander;
use Psalm\Issue\InvalidDocblockParamName;
use Psalm\Issue\InvalidOverride;
use Psalm\Issue\InvalidParamDefault;
use Psalm\Issue\InvalidThrow;
use Psalm\Issue\MethodSignatureMismatch;
use Psalm\Issue\MismatchingDocblockParamType;
use Psalm\Issue\MissingClosureParamType;
use Psalm\Issue\MissingOverrideAttribute;
use Psalm\Issue\MissingParamType;
use Psalm\Issue\MissingThrowsDocblock;
use Psalm\Issue\ReferenceConstraintViolation;
use Psalm\Issue\ReservedWord;
use Psalm\Issue\UnresolvableConstant;
use Psalm\Issue\UnusedClosureParam;
use Psalm\Issue\UnusedDocblockParam;
use Psalm\Issue\UnusedParam;
use Psalm\IssueBuffer;
use Psalm\Node\Expr\VirtualVariable;
use Psalm\Node\Stmt\VirtualWhile;
use Psalm\Plugin\EventHandler\Event\AfterFunctionLikeAnalysisEvent;
use Psalm\Storage\AttributeStorage;
use Psalm\Storage\ClassLikeStorage;
use Psalm\Storage\FunctionLikeParameter;
use Psalm\Storage\FunctionLikeStorage;
use Psalm\Storage\FunctionStorage;
use Psalm\Storage\MethodStorage;
use Psalm\Type;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TClosure;
use Psalm\Type\Atomic\TGenericObject;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Union;
use UnexpectedValueException;
use function array_combine;
use function array_diff_key;
use function array_filter;
use function array_key_exists;
use function array_keys;
use function array_merge;
use function array_search;
use function array_values;
use function count;
use function end;
use function in_array;
use function is_string;
use function krsort;
use function mb_strpos;
use function md5;
use function microtime;
use function reset;
use function strpos;
use function strtolower;
use function substr;
use const SORT_NUMERIC;
/**
* @internal
* @template-covariant TFunction as Closure|Function_|ClassMethod|ArrowFunction
*/
abstract class FunctionLikeAnalyzer extends SourceAnalyzer
{
/**
* @var TFunction
*/
protected $function;
protected Codebase $codebase;
/**
* @var array<string>
*/
protected array $suppressed_issues;
protected bool $is_static = false;
/**
* @var ?array<string, Union>
*/
protected ?array $return_vars_in_scope = [];
/**
* @var ?array<string, bool>
*/
protected ?array $return_vars_possibly_in_scope = [];
private ?Union $local_return_type = null;
/**
* @var array<string, bool>
*/
protected static array $no_effects_hashes = [];
public bool $track_mutations = false;
public bool $inferred_impure = false;
public bool $inferred_has_mutation = false;
/**
* Holds param nodes for functions with func_get_args calls
*
* @var array<string, DataFlowNode>
*/
public array $param_nodes = [];
protected FunctionLikeStorage $storage;
/**
* @param TFunction $function
*/
public function __construct($function, SourceAnalyzer $source, FunctionLikeStorage $storage)
{
$this->function = $function;
$this->source = $source;
$this->suppressed_issues = $source->getSuppressedIssues();
$this->codebase = $source->getCodebase();
$this->storage = $storage;
}
/**
* @param bool $add_mutations whether or not to add mutations to this method
* @param array<string, Union> $byref_vars
* @param-out array<string, Union> $byref_vars
* @return false|null
* @psalm-suppress PossiblyUnusedReturnValue unused but seems important
* @psalm-suppress ComplexMethod Unavoidably complex
*/
public function analyze(
Context $context,
NodeDataProvider $type_provider,
?Context $global_context = null,
bool $add_mutations = false,
array &$byref_vars = []
): ?bool {
$storage = $this->storage;
$function_stmts = $this->function->getStmts() ?: [];
if ($this->function instanceof ArrowFunction
&& isset($function_stmts[0])
&& $function_stmts[0] instanceof PhpParser\Node\Stmt\Return_
&& $function_stmts[0]->expr
) {
$function_stmts[0]->setAttributes($function_stmts[0]->expr->getAttributes());
}
if ($global_context) {
foreach ($global_context->constants as $const_name => $var_type) {
if (!$context->hasVariable($const_name)) {
$context->vars_in_scope[$const_name] = $var_type;
}
}
}
$codebase = $this->codebase;
$project_analyzer = $this->getProjectAnalyzer();
if ($codebase->track_unused_suppressions && !isset($storage->suppressed_issues[0])) {
if (count($storage->suppressed_issues) === 1 // UnusedPsalmSuppress by itself should be marked as unused
|| !in_array("UnusedPsalmSuppress", $storage->suppressed_issues)
) {
foreach ($storage->suppressed_issues as $offset => $issue_name) {
IssueBuffer::addUnusedSuppression(
$storage->location !== null
? $storage->location->file_path
: $this->getFilePath(),
$offset,
$issue_name,
);
}
}
}
foreach ($storage->docblock_issues as $docblock_issue) {
IssueBuffer::maybeAdd($docblock_issue);
}
$function_information = $this->getFunctionInformation(
$context,
$codebase,
$type_provider,
$storage,
$add_mutations,
);
if ($function_information === null) {
return null;
}
[
$real_method_id,
$method_id,
$appearing_class_storage,
$hash,
$cased_method_id,
$overridden_method_ids,
] = $function_information;
$this->suppressed_issues = $this->getSource()->getSuppressedIssues() + $storage->suppressed_issues;
if ($appearing_class_storage) {
$this->suppressed_issues += $appearing_class_storage->suppressed_issues;
}
if (($storage instanceof MethodStorage || $storage instanceof FunctionStorage)
&& $storage->is_static
) {
$this->is_static = true;
}
$statements_analyzer = new StatementsAnalyzer($this, $type_provider);
$byref_uses = [];
if ($this instanceof ClosureAnalyzer && $this->function instanceof Closure) {
foreach ($this->function->uses as $use) {
if (!is_string($use->var->name)) {
continue;
}
$use_var_id = '$' . $use->var->name;
$use_location = new CodeLocation($this, $use);
$use_assignment = null;
if ($statements_analyzer->data_flow_graph) {
$use_assignment = DataFlowNode::getForAssignment(
$use_var_id,
$use_location,
);
$statements_analyzer->data_flow_graph->addNode($use_assignment);
$context->vars_in_scope[$use_var_id] =
$context->vars_in_scope[$use_var_id]->addParentNodes(
[$use_assignment->id => $use_assignment],
);
}
if ($use->byRef) {
$byref_uses[$use_var_id] = true;
if ($statements_analyzer->data_flow_graph && $use_assignment) {
$statements_analyzer->data_flow_graph->addPath(
$use_assignment,
new DataFlowNode('closure-use', 'closure use', null),
'closure-use',
);
}
} else {
$statements_analyzer->registerVariable($use_var_id, $use_location, null);
}
}
$statements_analyzer->setByRefUses($byref_uses);
}
if ($storage->template_types) {
foreach ($storage->template_types as $param_name => $_) {
$fq_classlike_name = Type::getFQCLNFromString(
$param_name,
$this->getAliases(),
);
if ($codebase->classOrInterfaceExists($fq_classlike_name)) {
IssueBuffer::maybeAdd(
new ReservedWord(
'Cannot use ' . $param_name . ' as template name since the class already exists',
new CodeLocation($this, $this->function),
'resource',
),
$this->getSuppressedIssues(),
);
}
}
}
$template_types = $storage->template_types;
if ($appearing_class_storage && $appearing_class_storage->template_types) {
$template_types = array_merge($template_types ?: [], $appearing_class_storage->template_types);
}
$params = $storage->params;
if ($codebase->alter_code) {
$this->alterParams($codebase, $storage, $params, $context);
}
foreach ($codebase->methods_to_rename as $original_method_id => $new_method_name) {
if ($this instanceof MethodAnalyzer
&& strtolower((string) $this->getMethodId()) === $original_method_id
) {
$file_manipulations = [
new FileManipulation(
(int) $this->function->name->getAttribute('startFilePos'),
(int) $this->function->name->getAttribute('endFilePos') + 1,
$new_method_name,
),
];
FileManipulationBuffer::add(
$this->getFilePath(),
$file_manipulations,
);
}
}
if ($storage instanceof MethodStorage
&& $method_id instanceof MethodIdentifier
&& $overridden_method_ids
) {
$params = $codebase->methods->getMethodParams(
$method_id,
$this,
);
}
$check_stmts = $this->processParams(
$statements_analyzer,
$storage,
$cased_method_id,
$params,
array_values($this->function->params),
$context,
(bool) $template_types,
);
if ($byref_uses) {
$ref_context = clone $context;
$var = '$__tmp_byref_closure_if__' . (int) $this->function->getAttribute('startFilePos');
$ref_context->vars_in_scope[$var] = Type::getBool();
$var = new VirtualVariable(
substr($var, 1),
);
$virtual_while = new VirtualWhile(
$var,
$function_stmts,
);
$statements_analyzer->analyze(
[$virtual_while],
$ref_context,
);
foreach ($byref_uses as $var_id => $_) {
$byref_vars[$var_id] = $ref_context->vars_in_scope[$var_id];
$context->vars_in_scope[$var_id] = $ref_context->vars_in_scope[$var_id];
}
}
if ($storage->pure) {
$context->pure = true;
}
if ($storage->mutation_free
&& $cased_method_id
&& !strpos($cased_method_id, '__construct')
&& !($storage instanceof MethodStorage && $storage->mutation_free_inferred)
) {
$context->mutation_free = true;
}
if ($storage instanceof MethodStorage
&& $storage->external_mutation_free
&& !$storage->mutation_free_inferred
) {
$context->external_mutation_free = true;
}
foreach ($storage->unused_docblock_parameters as $param_name => $param_location) {
if ($storage->has_undertyped_native_parameters) {
IssueBuffer::maybeAdd(
new InvalidDocblockParamName(
'Incorrect param name $' . $param_name . ' in docblock for ' . $cased_method_id,
$param_location,
),
);
} elseif ($codebase->find_unused_code) {
IssueBuffer::maybeAdd(
new UnusedDocblockParam(
'Docblock parameter $' . $param_name . ' in docblock for ' . $cased_method_id
. ' does not have a counterpart in signature parameter list',
$param_location,
),
);
}
}
if ($storage->signature_return_type && $storage->signature_return_type_location) {
[$start, $end] = $storage->signature_return_type_location->getSelectionBounds();
$codebase->analyzer->addOffsetReference(
$this->getFilePath(),
$start,
$end,
(string) $storage->signature_return_type,
);
}
if ($storage instanceof MethodStorage && $storage->location && !$storage->allow_named_arg_calls) {
foreach ($overridden_method_ids as $overridden_method_id) {
$overridden_storage = $codebase->methods->getStorage($overridden_method_id);
if ($overridden_storage->allow_named_arg_calls) {
IssueBuffer::maybeAdd(new MethodSignatureMismatch(
'Method ' . (string) $method_id . ' should accept named arguments '
. ' as ' . (string) $overridden_method_id . ' does',
$storage->location,
));
}
}
}
if (ReturnTypeAnalyzer::checkReturnType(
$this->function,
$project_analyzer,
$this,
$storage,
$context,
) === false) {
$check_stmts = false;
}
if (!$check_stmts) {
return false;
}
if ($context->collect_initializations || $context->collect_mutations) {
$statements_analyzer->addSuppressedIssues([
'DocblockTypeContradiction',
'InvalidReturnStatement',
'RedundantCondition',
'RedundantConditionGivenDocblockType',
'TypeDoesNotContainNull',
'TypeDoesNotContainType',
'LoopInvalidation',
]);
if ($context->collect_initializations) {
$statements_analyzer->addSuppressedIssues([
'UndefinedInterfaceMethod',
'UndefinedMethod',
'PossiblyUndefinedMethod',
]);
}
} elseif ($cased_method_id && strpos($cased_method_id, '__destruct')) {
$statements_analyzer->addSuppressedIssues([
'InvalidPropertyAssignmentValue',
'PossiblyNullPropertyAssignmentValue',
]);
}
$time = microtime(true);
$project_analyzer = $statements_analyzer->getProjectAnalyzer();
if ($codebase->alter_code
&& (isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
|| isset($project_analyzer->getIssuesToFix()['MissingImmutableAnnotation']))
) {
$this->track_mutations = true;
} elseif ($this->function instanceof Closure
|| $this->function instanceof ArrowFunction
) {
$this->track_mutations = true;
}
if ($this->function instanceof ArrowFunction && (!$storage->return_type || $storage->return_type->isNever())) {
// ArrowFunction perform a return implicitly so if the return type is never, we have to suppress the error
// note: the never can only come from phpdoc. PHP will refuse short closures with never in signature
$statements_analyzer->addSuppressedIssues(['NoValue']);
}
$statements_analyzer->analyze($function_stmts, $context, $global_context, true);
if ($codebase->alter_code
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
&& !$this->inferred_impure
&& ($this->function instanceof Function_
|| $this->function instanceof ClassMethod)
&& $storage->params
&& !$overridden_method_ids
) {
$manipulator = FunctionDocblockManipulator::getForFunction(
$project_analyzer,
$this->source->getFilePath(),
$this->function,
);
$yield_types = [];
$inferred_return_types = ReturnTypeCollector::getReturnTypes(
$codebase,
$type_provider,
$function_stmts,
$yield_types,
true,
);
$inferred_return_type = $inferred_return_types
? Type::combineUnionTypeArray(
$inferred_return_types,
$codebase,
)
: Type::getVoid();
if (!$inferred_return_type->isVoid()
&& !$inferred_return_type->isFalse()
&& !$inferred_return_type->isNull()
&& !$inferred_return_type->isSingleIntLiteral()
&& !$inferred_return_type->isSingleStringLiteral()
&& !$inferred_return_type->isTrue()
&& !$inferred_return_type->isEmptyArray()
) {
$manipulator->makePure();
}
}
if ($this->inferred_has_mutation && $context->self) {
$this->codebase->analyzer->addMutableClass($context->self);
}
if (!$context->collect_initializations
&& !$context->collect_mutations
&& $project_analyzer->debug_performance
&& $cased_method_id
) {
$traverser = new PhpParser\NodeTraverser;
$node_counter = new NodeCounterVisitor();
$traverser->addVisitor($node_counter);
$traverser->traverse($function_stmts);
if ($node_counter->count > 5) {
$time_taken = microtime(true) - $time;
$codebase->analyzer->addFunctionTiming($cased_method_id, $time_taken / $node_counter->count);
}
}
$final_actions = ScopeAnalyzer::getControlActions(
$this->function->getStmts() ?: [],
null,
[],
);
if ($final_actions !== [ScopeAnalyzer::ACTION_END]) {
$this->examineParamTypes($statements_analyzer, $context, $codebase);
}
foreach ($params as $function_param) {
// only complain if there's no type defined by a parent type
if (!$function_param->type
&& $function_param->location
) {
if ($this->function instanceof Closure
|| $this->function instanceof ArrowFunction
) {
IssueBuffer::maybeAdd(
new MissingClosureParamType(
'Parameter $' . $function_param->name . ' has no provided type',
$function_param->location,
),
$storage->suppressed_issues + $this->getSuppressedIssues(),
);
} else {
IssueBuffer::maybeAdd(
new MissingParamType(
'Parameter $' . $function_param->name . ' has no provided type',
$function_param->location,
),
$storage->suppressed_issues + $this->getSuppressedIssues(),
);
}
}
}
if ($this->function instanceof Closure
|| $this->function instanceof ArrowFunction
) {
$this->verifyReturnType(
$function_stmts,
$statements_analyzer,
$storage->return_type,
$this->source->getFQCLN(),
$storage->return_type_location,
$context->has_returned,
$global_context && ($global_context->inside_call || $global_context->inside_return),
);
$closure_yield_types = [];
$closure_return_types = ReturnTypeCollector::getReturnTypes(
$codebase,
$type_provider,
$function_stmts,
$closure_yield_types,
true,
);
$closure_return_type = $closure_return_types
? Type::combineUnionTypeArray(
$closure_return_types,
$codebase,
)
: Type::getVoid();
$closure_yield_type = $closure_yield_types
? Type::combineUnionTypeArray(
$closure_yield_types,
$codebase,
)
: null;
if ($closure_yield_type) {
$closure_return_type = $closure_yield_type;
}
if ($function_type = $statements_analyzer->node_data->getType($this->function)) {
/**
* @var TClosure
*/
$closure_atomic = $function_type->getSingleAtomic();
$new_closure_return_type = $closure_atomic->return_type;
if (($storage->return_type === $storage->signature_return_type)
&& (!$storage->return_type
|| $storage->return_type->hasMixed()
|| UnionTypeComparator::isContainedBy(
$codebase,
$closure_return_type,
$storage->return_type,
))
) {
$new_closure_return_type = $closure_return_type;
}
$new_closure_is_pure = !$this->inferred_impure;
$statements_analyzer->node_data->setType(
$this->function,
new Union([
new TClosure(
$closure_atomic->value,
$closure_atomic->params,
$new_closure_return_type,
$new_closure_is_pure,
$closure_atomic->byref_uses,
$closure_atomic->extra_types,
$closure_atomic->from_docblock,
),
]),
);
}
}
if ($codebase->collect_references
&& !$context->collect_initializations
&& !$context->collect_mutations
&& $codebase->find_unused_variables
&& $context->check_variables
) {
$this->checkParamReferences(
$statements_analyzer,
$storage,
$appearing_class_storage,
$context,
);
}
foreach ($storage->throws as $expected_exception => $_) {
if (($expected_exception === 'self'
|| $expected_exception === 'static')
&& $context->self
) {
$expected_exception = $context->self;
}
if (isset($storage->throw_locations[$expected_exception])) {
if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
$statements_analyzer,
$expected_exception,
$storage->throw_locations[$expected_exception],
$context->self,
$context->calling_method_id,
$statements_analyzer->getSuppressedIssues(),
new ClassLikeNameOptions(
false,
false,
true,
true,
true,
),
)) {
$input_type = new Union([new TNamedObject($expected_exception)]);
$container_type = new Union([new TNamedObject('Exception'), new TNamedObject('Throwable')]);
if (!UnionTypeComparator::isContainedBy($codebase, $input_type, $container_type)) {
IssueBuffer::maybeAdd(
new InvalidThrow(
'Class supplied for @throws ' . $expected_exception
. ' does not implement Throwable',
$storage->throw_locations[$expected_exception],
$expected_exception,
),
$statements_analyzer->getSuppressedIssues(),
);
}
if ($codebase->alter_code) {
$codebase->classlikes->handleDocblockTypeInMigration(
$codebase,
$this,
$input_type,
$storage->throw_locations[$expected_exception],
$context->calling_method_id,
);
}
}
}
}
$missingThrowsDocblockErrors = [];
foreach ($statements_analyzer->getUncaughtThrows($context) as $possibly_thrown_exception => $codelocations) {
$is_expected = false;
foreach ($storage->throws as $expected_exception => $_) {
if ($expected_exception === $possibly_thrown_exception
|| (
$codebase->classOrInterfaceExists($possibly_thrown_exception)
&& (
$codebase->interfaceExtends($possibly_thrown_exception, $expected_exception)
|| $codebase->classExtendsOrImplements($possibly_thrown_exception, $expected_exception)
)
)
) {
$is_expected = true;
break;
}
}
if (!$is_expected) {
$missing_docblock_exception = new TNamedObject($possibly_thrown_exception);
$missingThrowsDocblockErrors[] = $missing_docblock_exception->toNamespacedString(
$this->source->getNamespace(),
$this->source->getAliasedClassesFlipped(),
$this->source->getFQCLN(),
true,
);
foreach ($codelocations as $codelocation) {
// issues are suppressed in ThrowAnalyzer, CallAnalyzer, etc.
IssueBuffer::maybeAdd(
new MissingThrowsDocblock(
$possibly_thrown_exception . ' is thrown but not caught - please either catch'
. ' or add a @throws annotation',
$codelocation,
$possibly_thrown_exception,
),
);
}
}
}
if ($codebase->alter_code
&& isset($project_analyzer->getIssuesToFix()['MissingThrowsDocblock'])
) {
$manipulator = FunctionDocblockManipulator::getForFunction(
$project_analyzer,
$this->source->getFilePath(),
$this->function,
);
$manipulator->addThrowsDocblock($missingThrowsDocblockErrors);
}
if ($codebase->taint_flow_graph
&& $this->function instanceof ClassMethod
&& $cased_method_id
&& $storage->specialize_call
&& isset($context->vars_in_scope['$this'])
&& $context->vars_in_scope['$this']->parent_nodes
) {
$method_source = DataFlowNode::getForMethodReturn(
(string) $method_id,
$cased_method_id,
$storage->location,
);
$codebase->taint_flow_graph->addNode($method_source);
foreach ($context->vars_in_scope['$this']->parent_nodes as $parent_node) {
$codebase->taint_flow_graph->addPath(
$parent_node,
$method_source,
'$this',
);
}
}
if ($add_mutations) {
if ($this->return_vars_in_scope !== null) {
$context->vars_in_scope = TypeAnalyzer::combineKeyedTypes(
$context->vars_in_scope,
$this->return_vars_in_scope,
);
}
if ($this->return_vars_possibly_in_scope !== null) {
$context->vars_possibly_in_scope = array_merge(
$context->vars_possibly_in_scope,
$this->return_vars_possibly_in_scope,
);
}
foreach ($context->vars_in_scope as $var => $_) {
if (strpos($var, '$this->') !== 0 && $var !== '$this') {
$context->removePossibleReference($var);
}
}
foreach ($context->vars_possibly_in_scope as $var => $_) {
if (strpos($var, '$this->') !== 0 && $var !== '$this') {
unset($context->vars_possibly_in_scope[$var]);
}
}
if ($hash
&& $real_method_id
&& $this instanceof MethodAnalyzer
&& !$context->collect_initializations
) {
$new_hash = md5($real_method_id . '::' . $context->getScopeSummary());
if ($new_hash === $hash) {
self::$no_effects_hashes[$hash] = true;
}
}
}
$event = new AfterFunctionLikeAnalysisEvent(
$this->function,
$storage,
$this,
$codebase,
[],
$type_provider,
$context,
);
if ($codebase->config->eventDispatcher->dispatchAfterFunctionLikeAnalysis($event) === false) {
return false;
}
$file_manipulations = $event->getFileReplacements();
if ($file_manipulations) {
FileManipulationBuffer::add(
$this->getFilePath(),
$file_manipulations,
);
}
AttributesAnalyzer::analyze(
$this,
$context,
$storage,
$this->function->attrGroups,
$storage instanceof MethodStorage ? AttributesAnalyzer::TARGET_METHOD : AttributesAnalyzer::TARGET_FUNCTION,
$storage->suppressed_issues + $this->getSuppressedIssues(),
);
return null;
}
private function checkParamReferences(
StatementsAnalyzer $statements_analyzer,
FunctionLikeStorage $storage,
?ClassLikeStorage $class_storage,
Context $context
): void {
$codebase = $statements_analyzer->getCodebase();
$unused_params = $this->detectUnusedParameters($statements_analyzer, $storage, $context);
if (!$storage instanceof MethodStorage
|| !$storage->cased_name
|| $storage->visibility === ClassLikeAnalyzer::VISIBILITY_PRIVATE
) {
$last_unused_argument_position = $this->detectPreviousUnusedArgumentPosition(
$storage,
count($storage->params) - 1,
);
// Sort parameters in reverse order so that we can start from the end of parameters
krsort($unused_params, SORT_NUMERIC);
foreach ($unused_params as $unused_param_position => $unused_param_code_location) {
$unused_param_var_name = $storage->params[$unused_param_position]->name;
$unused_param_message = 'Param ' . $unused_param_var_name . ' is never referenced in this method';
// Remove the key as we already report the issue
unset($unused_params[$unused_param_position]);
// Do not report unused required parameters
if ($unused_param_position !== $last_unused_argument_position) {
break;
}
$last_unused_argument_position = $this->detectPreviousUnusedArgumentPosition(
$storage,
$unused_param_position - 1,
);
if ($this instanceof ClosureAnalyzer) {
IssueBuffer::maybeAdd(
new UnusedClosureParam(
$unused_param_message,
$unused_param_code_location,
),
$this->getSuppressedIssues(),
);
continue;
}
IssueBuffer::maybeAdd(
new UnusedParam(
$unused_param_message,
$unused_param_code_location,
),
$this->getSuppressedIssues(),
);
}
}
if ($storage instanceof MethodStorage
&& $this instanceof MethodAnalyzer
&& $class_storage
&& $storage->cased_name
&& $storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PRIVATE
) {
$method_id_lc = strtolower((string) $this->getMethodId());
foreach ($storage->params as $i => $_) {
if (!isset($unused_params[$i])) {
$codebase->file_reference_provider->addMethodParamUse(
$method_id_lc,
$i,
$method_id_lc,
);
$method_name_lc = strtolower($storage->cased_name);
if (!isset($class_storage->overridden_method_ids[$method_name_lc])) {
continue;
}
foreach ($class_storage->overridden_method_ids[$method_name_lc] as $parent_method_id) {
$codebase->file_reference_provider->addMethodParamUse(
strtolower((string) $parent_method_id),
$i,
$method_id_lc,
);
}
}
}
}
}
/**
* @param list<FunctionLikeParameter> $params
* @param list<Param> $param_stmts
*/
private function processParams(
StatementsAnalyzer $statements_analyzer,
FunctionLikeStorage $storage,
?string $cased_method_id,
array $params,
array $param_stmts,
Context $context,
bool $has_template_types
): bool {
$check_stmts = true;
$codebase = $statements_analyzer->getCodebase();
$project_analyzer = $statements_analyzer->getProjectAnalyzer();
foreach ($params as $offset => $function_param) {
$function_param_id = '$' . $function_param->name;
$signature_type = $function_param->signature_type;
$signature_type_location = $function_param->signature_type_location;
if ($signature_type && $signature_type_location && $signature_type->hasObjectType()) {
$referenced_type = $signature_type;
if ($referenced_type->isNullable()) {
$referenced_type = $referenced_type->getBuilder();
$referenced_type->removeType('null');
$referenced_type = $referenced_type->freeze();
}
[$start, $end] = $signature_type_location->getSelectionBounds();
$codebase->analyzer->addOffsetReference(
$this->getFilePath(),
$start,
$end,
(string) $referenced_type,
);
}
if ($signature_type) {
$signature_type = TypeExpander::expandUnion(
$codebase,
$signature_type,
$context->self,
$context->self,
$this->getParentFQCLN(),
);
}
$parent_nodes = [];
if ($statements_analyzer->data_flow_graph
&& $function_param->location
) {
//don't add to taint flow graph if the type can't transmit taints
if (!$statements_analyzer->data_flow_graph instanceof TaintFlowGraph
|| $function_param->type === null
|| !$function_param->type->isSingle()
|| (!$function_param->type->isInt()
&& !$function_param->type->isFloat()
&& !$function_param->type->isBool())
) {
$param_assignment = DataFlowNode::getForAssignment(
$function_param_id,
$function_param->location,
);
$statements_analyzer->data_flow_graph->addNode($param_assignment);
if ($cased_method_id) {
$type_source = DataFlowNode::getForMethodArgument(
$cased_method_id,
$cased_method_id,
$offset,
$function_param->location,
null,
);
$statements_analyzer->data_flow_graph->addPath($type_source, $param_assignment, 'param');
}
if ($storage->variadic) {
$this->param_nodes += [$param_assignment->id => $param_assignment];
}
$parent_nodes = [$param_assignment->id => $param_assignment];
}
}
if ($function_param->type) {
$param_type = $function_param->type;
try {
$param_type = TypeExpander::expandUnion(
$codebase,
$param_type,
$context->self,
$context->self,
$this->getParentFQCLN(),
true,
false,
false,
true,
false,
true,
);
} catch (UnresolvableConstantException $e) {
if ($function_param->type_location !== null) {
IssueBuffer::maybeAdd(
new UnresolvableConstant(
"Could not resolve constant {$e->class_name}::{$e->const_name}",
$function_param->type_location,
),
$storage->suppressed_issues,
true,
);
}
}
if ($function_param->type_location) {
if ($param_type->check(
$this,
$function_param->type_location,
$storage->suppressed_issues,
[],
false,
false,
$this->function instanceof ClassMethod
&& strtolower($this->function->name->name) !== '__construct',
$context->calling_method_id,
) === false) {
$check_stmts = false;
}
}
$param_type = $param_type->addParentNodes($parent_nodes);
} else {
$param_type = new Union([new TMixed()], [
'by_ref' => $function_param->by_ref,
'parent_nodes' => $parent_nodes,
]);
}
$var_type = $param_type;
if ($function_param->is_variadic) {
if ($storage->allow_named_arg_calls) {
$var_type = new Union([
new TArray([Type::getArrayKey(), $param_type]),
], [
'by_ref' => $function_param->by_ref,
'parent_nodes' => $parent_nodes,
]);
} else {
$var_type = new Union([
Type::getListAtomic($param_type),
], [
'by_ref' => $function_param->by_ref,
'parent_nodes' => $parent_nodes,
]);
}
}
$context->vars_in_scope[$function_param_id] = $var_type;
$context->vars_possibly_in_scope[$function_param_id] = true;
if ($function_param->by_ref) {
$context->vars_in_scope[$function_param_id] =
$context->vars_in_scope[$function_param_id]->setProperties(['by_ref' => true]);
$context->references_to_external_scope[$function_param_id] = true;
}
$parser_param = $this->function->getParams()[$offset] ?? null;
if ($function_param->location) {
$statements_analyzer->registerVariable(
$function_param_id,
$function_param->location,
null,
);
}
if (!$function_param->type_location || !$function_param->location) {
if ($parser_param && $parser_param->default) {
ExpressionAnalyzer::analyze($statements_analyzer, $parser_param->default, $context);
}
continue;
}
if ($signature_type) {
$union_comparison_result = new TypeComparisonResult();
if (!UnionTypeComparator::isContainedBy(
$codebase,
$param_type,
$signature_type,
false,
false,
$union_comparison_result,
) && !$union_comparison_result->type_coerced_from_mixed
) {
if ($codebase->alter_code
&& isset($project_analyzer->getIssuesToFix()['MismatchingDocblockParamType'])
) {
$this->addOrUpdateParamType($project_analyzer, $function_param->name, $signature_type, true);
continue;
}
IssueBuffer::maybeAdd(
new MismatchingDocblockParamType(
'Parameter ' . $function_param_id . ' has wrong type \'' . $param_type .
'\', should be \'' . $signature_type . '\'',
$function_param->type_location,
),
$storage->suppressed_issues,
true,
);
if ($signature_type->check(
$this,
$function_param->type_location,
$storage->suppressed_issues,
[],
false,
) === false) {
$check_stmts = false;
}
continue;
}
}
if ($parser_param && $parser_param->default) {
ExpressionAnalyzer::analyze($statements_analyzer, $parser_param->default, $context);
$default_type = $statements_analyzer->node_data->getType($parser_param->default);
if ($default_type
&& !$default_type->hasMixed()
&& !UnionTypeComparator::isContainedBy(
$codebase,
$default_type,
$param_type,
false,
false,
null,
true,
)
) {
IssueBuffer::maybeAdd(
new InvalidParamDefault(
'Default value type ' . $default_type->getId() . ' for argument ' . ($offset + 1)
. ' of method ' . $cased_method_id
. ' does not match the given type ' . $param_type->getId(),
$function_param->type_location,
),
);
}
if ($default_type
&& !$default_type->isNull()
&& $param_type->isSingleAndMaybeNullable()
&& $param_type->getCallableTypes()
) {
IssueBuffer::maybeAdd(
new InvalidParamDefault(
'Default value type for ' . $param_type->getId() . ' argument ' . ($offset + 1)
. ' of method ' . $cased_method_id
. ' can only be null, ' . $default_type->getId() . ' specified',
$function_param->type_location,
),
);
}
}
if ($has_template_types) {
if ($param_type->check(
$this->source,
$function_param->type_location,
$this->suppressed_issues,
[],
false,
) === false) {
$check_stmts = false;
}
} else {
if ($param_type->isVoid()) {
IssueBuffer::maybeAdd(
new ReservedWord(
'Parameter cannot be void',
$function_param->type_location,
'void',
),
$this->suppressed_issues,
);
}
if ($param_type->isNever()) {
IssueBuffer::maybeAdd(
new ReservedWord(
'Parameter cannot be never',
$function_param->type_location,
'never',
),
$this->suppressed_issues,
);
}
if ($param_type->check(
$this->source,
$function_param->type_location,
$this->suppressed_issues,
[],
false,
) === false) {
$check_stmts = false;
}
}
if ($codebase->collect_locations) {
if ($function_param->type_location !== $function_param->signature_type_location &&
$function_param->signature_type_location &&
$function_param->signature_type
) {
if ($function_param->signature_type->check(
$this->source,
$function_param->signature_type_location,
$this->suppressed_issues,
[],
false,
) === false) {
$check_stmts = false;
}
}
}
if ($function_param->by_ref) {
// register by ref params as having been used, to avoid false positives
// @todo change the assignment analysis *just* for byref params
// so that we don't have to do this
$context->hasVariable('$' . $function_param->name);
}
if (count($param_stmts) === count($params)) {
AttributesAnalyzer::analyze(
$this,
$context,
$function_param,
$param_stmts[$offset]->attrGroups,
AttributesAnalyzer::TARGET_PARAMETER
| ($function_param->promoted_property ? AttributesAnalyzer::TARGET_PROPERTY : 0),
$storage->suppressed_issues + $this->getSuppressedIssues(),
);
}
}
return $check_stmts;
}
/**
* @param FunctionLikeParameter[] $params
*/
private function alterParams(
Codebase $codebase,
FunctionLikeStorage $storage,
array $params,
Context $context
): void {
foreach ($this->function->params as $param) {
$param_name_node = null;
if ($param->type instanceof PhpParser\Node\Name) {
$param_name_node = $param->type;
} elseif ($param->type instanceof PhpParser\Node\NullableType
&& $param->type->type instanceof PhpParser\Node\Name
) {
$param_name_node = $param->type->type;
}
if ($param_name_node) {
$resolved_name = ClassLikeAnalyzer::getFQCLNFromNameObject($param_name_node, $this->getAliases());
$parent_fqcln = $this->getParentFQCLN();
if ($resolved_name === 'self' && $context->self) {
$resolved_name = $context->self;
} elseif ($resolved_name === 'parent' && $parent_fqcln) {
$resolved_name = $parent_fqcln;
}
$codebase->classlikes->handleClassLikeReferenceInMigration(
$codebase,
$this,
$param_name_node,
$resolved_name,
$context->calling_method_id,
false,
true,
);
}
}
if ($this->function->returnType) {
$return_name_node = null;
if ($this->function->returnType instanceof PhpParser\Node\Name) {
$return_name_node = $this->function->returnType;
} elseif ($this->function->returnType instanceof PhpParser\Node\NullableType
&& $this->function->returnType->type instanceof PhpParser\Node\Name
) {
$return_name_node = $this->function->returnType->type;
}
if ($return_name_node) {
$resolved_name = ClassLikeAnalyzer::getFQCLNFromNameObject($return_name_node, $this->getAliases());
$parent_fqcln = $this->getParentFQCLN();
if ($resolved_name === 'self' && $context->self) {
$resolved_name = $context->self;
} elseif ($resolved_name === 'parent' && $parent_fqcln) {
$resolved_name = $parent_fqcln;
}
$codebase->classlikes->handleClassLikeReferenceInMigration(
$codebase,
$this,
$return_name_node,
$resolved_name,
$context->calling_method_id,
false,
true,
);
}
}
if ($storage->return_type
&& $storage->return_type_location
&& $storage->return_type_location !== $storage->signature_return_type_location
) {
$replace_type = TypeExpander::expandUnion(
$codebase,
$storage->return_type,
$context->self,
'static',
$this->getParentFQCLN(),
false,
);
$codebase->classlikes->handleDocblockTypeInMigration(
$codebase,
$this,
$replace_type,
$storage->return_type_location,
$context->calling_method_id,
);
}
foreach ($params as $function_param) {
if ($function_param->type
&& $function_param->type_location
&& $function_param->type_location !== $function_param->signature_type_location
&& $function_param->type_location->file_path === $this->getFilePath()
) {
$replace_type = TypeExpander::expandUnion(
$codebase,
$function_param->type,
$context->self,
'static',
$this->getParentFQCLN(),
false,
);
$codebase->classlikes->handleDocblockTypeInMigration(
$codebase,
$this,
$replace_type,
$function_param->type_location,
$context->calling_method_id,
);
}
}
}
/**
* @param array<PhpParser\Node\Stmt> $function_stmts
*/
public function verifyReturnType(
array $function_stmts,
StatementsAnalyzer $statements_analyzer,
?Union $return_type = null,
?string $fq_class_name = null,
?CodeLocation $return_type_location = null,
bool $did_explicitly_return = false,
bool $closure_inside_call = false
): void {
ReturnTypeAnalyzer::verifyReturnType(
$this->function,
$function_stmts,
$statements_analyzer,
$statements_analyzer->node_data,
$this,
$return_type,
$fq_class_name,
$fq_class_name,
$return_type_location,
[],
$did_explicitly_return,
$closure_inside_call,
);
}
public function addOrUpdateParamType(
ProjectAnalyzer $project_analyzer,
string $param_name,
Union $inferred_return_type,
bool $docblock_only = false
): void {
$manipulator = FunctionDocblockManipulator::getForFunction(
$project_analyzer,
$this->source->getFilePath(),
$this->function,
);
$codebase = $project_analyzer->getCodebase();
$is_final = true;
$fqcln = $this->source->getFQCLN();
if ($fqcln !== null && $this instanceof MethodAnalyzer) {
$class_storage = $codebase->classlike_storage_provider->get($fqcln);
$is_final = $this->function->isFinal() || $class_storage->final;
}
$allow_native_type = !$docblock_only
&& $codebase->analysis_php_version_id >= 7_00_00
&& (
$codebase->allow_backwards_incompatible_changes
|| $is_final
|| !$this instanceof MethodAnalyzer
);
$manipulator->setParamType(
$param_name,
$allow_native_type
? $inferred_return_type->toPhpString(
$this->source->getNamespace(),
$this->source->getAliasedClassesFlipped(),
$this->source->getFQCLN(),
$project_analyzer->getCodebase()->analysis_php_version_id,
) : null,
$inferred_return_type->toNamespacedString(
$this->source->getNamespace(),
$this->source->getAliasedClassesFlipped(),
$this->source->getFQCLN(),
false,
),
$inferred_return_type->toNamespacedString(
$this->source->getNamespace(),
$this->source->getAliasedClassesFlipped(),
$this->source->getFQCLN(),
true,
),
);
}
/**
* Adds return types for the given function
*/
public function addReturnTypes(Context $context): void
{
if ($this->return_vars_in_scope !== null) {
$this->return_vars_in_scope = TypeAnalyzer::combineKeyedTypes(
$context->vars_in_scope,
$this->return_vars_in_scope,
);
} else {
$this->return_vars_in_scope = $context->vars_in_scope;
}
if ($this->return_vars_possibly_in_scope !== null) {
$this->return_vars_possibly_in_scope = array_merge(
$context->vars_possibly_in_scope,
$this->return_vars_possibly_in_scope,
);
} else {
$this->return_vars_possibly_in_scope = $context->vars_possibly_in_scope;
}
}
public function examineParamTypes(
StatementsAnalyzer $statements_analyzer,
Context $context,
Codebase $codebase,
?PhpParser\Node $stmt = null
): void {
$storage = $this->getFunctionLikeStorage($statements_analyzer);
foreach ($storage->params as $param) {
if ($param->by_ref && isset($context->vars_in_scope['$' . $param->name]) && !$param->is_variadic) {
$actual_type = $context->vars_in_scope['$' . $param->name];
$param_out_type = $param->out_type ?: $param->type;
if ($param_out_type && !$actual_type->hasMixed() && $param->location) {
if (!UnionTypeComparator::isContainedBy(
$codebase,
$actual_type,
$param_out_type,
$actual_type->ignore_nullable_issues,
$actual_type->ignore_falsable_issues,
)
) {
IssueBuffer::maybeAdd(
new ReferenceConstraintViolation(
'Variable ' . '$' . $param->name . ' is limited to values of type '
. $param_out_type->getId()
. ' because it is passed by reference, '
. $actual_type->getId() . ' type found. Use @param-out to specify '
. 'a different output type',
$stmt
? new CodeLocation($this, $stmt)
: $param->location,
),
$statements_analyzer->getSuppressedIssues(),
);
}
}
}
}
}
public function getMethodName(): ?string
{
if ($this->function instanceof ClassMethod) {
return (string)$this->function->name;
}
return null;
}
public function getCorrectlyCasedMethodId(?string $context_self = null): string
{
if ($this->function instanceof ClassMethod) {
$function_name = (string)$this->function->name;
return ($context_self ?: $this->source->getFQCLN()) . '::' . $function_name;
}
if ($this->function instanceof Function_) {
$namespace = $this->source->getNamespace();
return ($namespace ? $namespace . '\\' : '') . $this->function->name;
}
if (!$this instanceof ClosureAnalyzer) {
throw new UnexpectedValueException('This is weird');
}
return $this->getClosureId();
}
public function getFunctionLikeStorage(?StatementsAnalyzer $statements_analyzer = null): FunctionLikeStorage
{
$codebase = $this->codebase;
if ($this->function instanceof ClassMethod && $this instanceof MethodAnalyzer) {
$method_id = $this->getMethodId();
$codebase_methods = $codebase->methods;
try {
return $codebase_methods->getStorage($method_id);
} catch (UnexpectedValueException $e) {
$declaring_method_id = $codebase_methods->getDeclaringMethodId($method_id);
if ($declaring_method_id === null) {
throw new UnexpectedValueException('Cannot get storage for function that doesnt exist');
}
// happens for fake constructors
return $codebase_methods->getStorage($declaring_method_id);
}
}
if ($this instanceof FunctionAnalyzer) {
$function_id = $this->getFunctionId();
} elseif ($this instanceof ClosureAnalyzer) {
$function_id = $this->getClosureId();
} else {
throw new UnexpectedValueException('This is weird');
}
return $codebase->functions->getStorage($statements_analyzer, $function_id);
}
/** @return non-empty-string */
public function getId(): string
{
if ($this instanceof MethodAnalyzer) {
return (string) $this->getMethodId();
}
if ($this instanceof FunctionAnalyzer) {
return $this->getFunctionId();
}
if ($this instanceof ClosureAnalyzer) {
return $this->getClosureId();
}
throw new UnexpectedValueException('This is weird');
}
/**
* @psalm-mutation-free
* @return array<lowercase-string, string>
*/
public function getAliasedClassesFlipped(): array
{
if ($this->source instanceof NamespaceAnalyzer ||
$this->source instanceof FileAnalyzer ||
$this->source instanceof ClassLikeAnalyzer
) {
return $this->source->getAliasedClassesFlipped();
}
return [];
}
/**
* @psalm-mutation-free
* @return array<string, string>
*/
public function getAliasedClassesFlippedReplaceable(): array
{
if ($this->source instanceof NamespaceAnalyzer ||
$this->source instanceof FileAnalyzer ||
$this->source instanceof ClassLikeAnalyzer
) {
return $this->source->getAliasedClassesFlippedReplaceable();
}
return [];
}
/**
* @psalm-mutation-free
* @return array<string, array<string, Union>>|null
*/
public function getTemplateTypeMap(): ?array
{
if ($this->source instanceof ClassLikeAnalyzer) {
return ($this->source->getTemplateTypeMap() ?: [])
+ ($this->storage->template_types ?: []);
}
return $this->storage->template_types;
}
public function isStatic(): bool
{
return $this->is_static;
}
public function getCodebase(): Codebase
{
return $this->codebase;
}
/**
* Get a list of suppressed issues
*
* @return array<string>
*/
public function getSuppressedIssues(): array
{
return $this->suppressed_issues;
}
/**
* @param array<int, string> $new_issues
*/
public function addSuppressedIssues(array $new_issues): void
{
if (isset($new_issues[0])) {
$new_issues = array_combine($new_issues, $new_issues);
}
$this->suppressed_issues = $new_issues + $this->suppressed_issues;
}
/**
* @param array<int, string> $new_issues
*/
public function removeSuppressedIssues(array $new_issues): void
{
if (isset($new_issues[0])) {
$new_issues = array_combine($new_issues, $new_issues);
}
$this->suppressed_issues = array_diff_key($this->suppressed_issues, $new_issues);
}
/**
* Adds a suppressed issue, useful when creating a method checker from scratch
*/
public function addSuppressedIssue(string $issue_name): void
{
$this->suppressed_issues[] = $issue_name;
}
public static function clearCache(): void
{
self::$no_effects_hashes = [];
}
public function getLocalReturnType(Union $storage_return_type, bool $final = false): Union
{
if ($this->local_return_type) {
return $this->local_return_type;
}
$this->local_return_type = TypeExpander::expandUnion(
$this->codebase,
$storage_return_type,
$this->getFQCLN(),
$this->getFQCLN(),
$this->getParentFQCLN(),
true,
true,
$final,
);
return $this->local_return_type;
}
/**
* @return array{
* MethodIdentifier|null,
* MethodIdentifier|null,
* ClassLikeStorage|null,
* ?string,
* ?string,
* array<string, MethodIdentifier>
* }|null
*/
private function getFunctionInformation(
Context $context,
Codebase $codebase,
NodeDataProvider $type_provider,
FunctionLikeStorage $storage,
bool $add_mutations
): ?array {
$classlike_storage_provider = $codebase->classlike_storage_provider;
$real_method_id = null;
$method_id = null;
$cased_method_id = null;
$hash = null;
$appearing_class_storage = null;
$overridden_method_ids = [];
if ($this instanceof MethodAnalyzer) {
if (!$storage instanceof MethodStorage) {
throw new UnexpectedValueException('$storage must be MethodStorage');
}
$real_method_id = $this->getMethodId();
$method_id = $this->getMethodId($context->self);
$fq_class_name = (string)$context->self;
$appearing_class_storage = $classlike_storage_provider->get($fq_class_name);
if ($add_mutations) {
if (!$context->collect_initializations) {
$hash = md5($real_method_id . '::' . $context->getScopeSummary());
// if we know that the function has no effects on vars, we don't bother rechecking
if (isset(self::$no_effects_hashes[$hash])) {
return null;
}
}
} elseif ($context->self) {
if ($appearing_class_storage->template_types) {
$template_params = [];
foreach ($appearing_class_storage->template_types as $param_name => $template_map) {
$key = array_keys($template_map)[0];
$template_params[] = new Union([
new TTemplateParam(
$param_name,
reset($template_map),
$key,
),
]);
}
$this_object_type = new TGenericObject(
$context->self,
$template_params,
false,
!$storage->final,
);
} else {
$this_object_type = new TNamedObject(
$context->self,
!$storage->final,
);
}
$props = [];
if ($storage->external_mutation_free
&& !$storage->mutation_free_inferred
) {
$props = ['reference_free' => true];
if ($this->function->name->name !== '__construct') {
$props['allow_mutations'] = false;
}
}
if ($codebase->taint_flow_graph
&& $storage->specialize_call
&& $storage->location
) {
$new_parent_node = DataFlowNode::getForAssignment('$this in ' . $method_id, $storage->location);
$codebase->taint_flow_graph->addNode($new_parent_node);
$props['parent_nodes'] = [$new_parent_node->id => $new_parent_node];
}
if ($this->storage instanceof MethodStorage && $this->storage->if_this_is_type) {
$template_result = new TemplateResult($this->getTemplateTypeMap() ?? [], []);
TemplateStandinTypeReplacer::fillTemplateResult(
new Union([$this_object_type]),
$template_result,
$codebase,
null,
$this->storage->if_this_is_type,
);
foreach ($context->vars_in_scope as $var_name => &$var_type) {
if (0 === mb_strpos($var_name, '$this->')) {
$var_type = TemplateInferredTypeReplacer::replace($var_type, $template_result, $codebase);
}
}
$context->vars_in_scope['$this'] = $this->storage->if_this_is_type
->setProperties($props);
} else {
$context->vars_in_scope['$this'] = new Union([$this_object_type], $props);
}
$context->vars_possibly_in_scope['$this'] = true;
}
if ($appearing_class_storage->has_visitor_issues) {
return null;
}
$cased_method_id = $fq_class_name . '::' . $storage->cased_name;
$overridden_method_ids = $codebase->methods->getOverriddenMethodIds($method_id);
$codeLocation = new CodeLocation(
$this,
$this->function,
null,
true,
);
if ($codebase->analysis_php_version_id >= 8_03_00) {
$has_override_attribute = array_filter(
$storage->attributes,
static fn(AttributeStorage $s): bool => $s->fq_class_name === 'Override',
);
if ($has_override_attribute
&& (!$overridden_method_ids || $storage->cased_name === '__construct')
) {
IssueBuffer::maybeAdd(
new InvalidOverride(
'Method ' . $storage->cased_name . ' does not match any parent method',
$codeLocation,
),
$this->getSuppressedIssues(),
);
}
if (!$has_override_attribute
&& $codebase->config->ensure_override_attribute
&& $overridden_method_ids
&& $storage->cased_name !== '__construct'
) {
IssueBuffer::maybeAdd(
new MissingOverrideAttribute(
'Method ' . $storage->cased_name . ' should have the "Override" attribute',
$codeLocation,
),
$this->getSuppressedIssues(),
);
}
}
if ($overridden_method_ids
&& !$context->collect_initializations
&& !$context->collect_mutations
) {
foreach ($overridden_method_ids as $overridden_method_id) {
$parent_method_storage = $codebase->methods->getStorage($overridden_method_id);
$overridden_fq_class_name = $overridden_method_id->fq_class_name;
$parent_storage = $classlike_storage_provider->get($overridden_fq_class_name);
if ($this->function->name->name === '__construct'
&& !$parent_storage->preserve_constructor_signature
) {
continue;
}
$implementer_visibility = $storage->visibility;
$implementer_appearing_method_id = $codebase->methods->getAppearingMethodId($method_id);
$implementer_declaring_method_id = $real_method_id;
$declaring_class_storage = $appearing_class_storage;
if ($implementer_appearing_method_id
&& $implementer_appearing_method_id !== $implementer_declaring_method_id
) {
$appearing_fq_class_name = $implementer_appearing_method_id->fq_class_name;
$appearing_method_name = $implementer_appearing_method_id->method_name;
$declaring_fq_class_name = $implementer_declaring_method_id->fq_class_name;
$appearing_class_storage = $classlike_storage_provider->get(
$appearing_fq_class_name,
);
$declaring_class_storage = $classlike_storage_provider->get(
$declaring_fq_class_name,
);
if (isset($appearing_class_storage->trait_visibility_map[$appearing_method_name])) {
$implementer_visibility
= $appearing_class_storage->trait_visibility_map[$appearing_method_name];
}
}
// we've already checked this in the class checker
if (!isset($appearing_class_storage->class_implements[strtolower($overridden_fq_class_name)])) {
MethodComparator::compare(
$codebase,
count($overridden_method_ids) === 1 ? $this->function : null,
$declaring_class_storage,
$parent_storage,
$storage,
$parent_method_storage,
$fq_class_name,
$implementer_visibility,
$codeLocation,
$storage->suppressed_issues,
);
}
}
}
MethodAnalyzer::checkMethodSignatureMustOmitReturnType($storage, $codeLocation);
if ($appearing_class_storage->is_enum) {
MethodAnalyzer::checkForbiddenEnumMethod($storage, $appearing_class_storage);
}
if (!$context->calling_method_id || !$context->collect_initializations) {
$context->calling_method_id = strtolower((string)$method_id);
}
} elseif ($this instanceof FunctionAnalyzer) {
$function_name = $this->function->name->name;
$namespace_prefix = $this->getNamespace();
$cased_method_id = ($namespace_prefix !== null ? $namespace_prefix . '\\' : '') . $function_name;
$context->calling_function_id = strtolower($cased_method_id);
} elseif ($this instanceof ClosureAnalyzer) {
if ($storage->return_type) {
$closure_return_type = TypeExpander::expandUnion(
$codebase,
$storage->return_type,
$context->self,
$context->self,
$this->getParentFQCLN(),
);
} else {
$closure_return_type = Type::getMixed();
}
$closure_type = new TClosure(
'Closure',
$storage->params,
$closure_return_type,
$storage instanceof FunctionStorage ? $storage->pure : null,
$storage instanceof FunctionStorage ? $storage->byref_uses : [],
);
$type_provider->setType(
$this->function,
new Union([
$closure_type,
]),
);
} else {
throw new UnexpectedValueException('Impossible');
}
return [
$real_method_id,
$method_id,
$appearing_class_storage,
$hash,
$cased_method_id,
$overridden_method_ids,
];
}
/**
* @return array<int,CodeLocation>
*/
private function detectUnusedParameters(
StatementsAnalyzer $statements_analyzer,
FunctionLikeStorage $storage,
Context $context
): array {
$codebase = $statements_analyzer->getCodebase();
$unused_params = [];
foreach ($statements_analyzer->getUnusedVarLocations() as [$var_name, $original_location]) {
if (!array_key_exists(substr($var_name, 1), $storage->param_lookup)) {
continue;
}
if ($this->isIgnoredForUnusedParam($var_name)) {
continue;
}
$position = array_search(substr($var_name, 1), array_keys($storage->param_lookup), true);
if ($position === false) {
throw new UnexpectedValueException('$position should not be false here');
}
if ($storage->params[$position]->promoted_property) {
continue;
}
$did_match_param = false;
foreach ($this->function->params as $param) {
if ($param->var->getAttribute('endFilePos') === $original_location->raw_file_end) {
$did_match_param = true;
break;
}
}
if (!$did_match_param) {
continue;
}
$assignment_node = DataFlowNode::getForAssignment($var_name, $original_location);
if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph
&& $statements_analyzer->data_flow_graph->isVariableUsed($assignment_node)
) {
continue;
}
if (!$storage instanceof MethodStorage
|| !$storage->cased_name
|| $storage->visibility === ClassLikeAnalyzer::VISIBILITY_PRIVATE
) {
$unused_params[$position] = $original_location;
continue;
}
$fq_class_name = (string)$context->self;
$class_storage = $codebase->classlike_storage_provider->get($fq_class_name);
$method_name_lc = strtolower($storage->cased_name);
if ($storage->abstract) {
continue;
}
if (isset($class_storage->overridden_method_ids[$method_name_lc])) {
$parent_method_id = end($class_storage->overridden_method_ids[$method_name_lc]);
if ($parent_method_id) {
$parent_method_storage = $codebase->methods->getStorage($parent_method_id);
// if the parent method has a param at that position and isn't abstract
if (!$parent_method_storage->abstract
&& isset($parent_method_storage->params[$position])
) {
continue;
}
}
}
$unused_params[$position] = $original_location;
}
return $unused_params;
}
private function detectPreviousUnusedArgumentPosition(FunctionLikeStorage $function, int $position): int
{
$params = $function->params;
krsort($params, SORT_NUMERIC);
foreach ($params as $index => $param) {
if ($index > $position) {
continue;
}
if ($this->isIgnoredForUnusedParam($param->name)) {
continue;
}
return $index;
}
return 0;
}
private function isIgnoredForUnusedParam(string $var_name): bool
{
return strpos($var_name, '$_') === 0 || (strpos($var_name, '$unused') === 0 && $var_name !== '$unused');
}
}