mirror of
https://github.com/danog/psalm.git
synced 2024-11-27 04:45:20 +01:00
Add initial support for fixing MissingParamType
This commit is contained in:
parent
36343b07ce
commit
db89b3cc3f
@ -265,6 +265,11 @@ class Context
|
|||||||
*/
|
*/
|
||||||
public $calling_method_id;
|
public $calling_method_id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $infer_types = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string|null $self
|
* @param string|null $self
|
||||||
*/
|
*/
|
||||||
|
@ -1258,6 +1258,7 @@ class ClassAnalyzer extends ClassLikeAnalyzer
|
|||||||
|
|
||||||
$method_context = clone $class_context;
|
$method_context = clone $class_context;
|
||||||
$method_context->collect_exceptions = $config->check_for_throws_docblock;
|
$method_context->collect_exceptions = $config->check_for_throws_docblock;
|
||||||
|
$method_context->infer_types = $codebase->infer_types_from_usage;
|
||||||
|
|
||||||
$method_analyzer->analyze(
|
$method_analyzer->analyze(
|
||||||
$method_context,
|
$method_context,
|
||||||
|
@ -553,20 +553,28 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
|
|||||||
&& $function_param->location
|
&& $function_param->location
|
||||||
&& !isset($implemented_docblock_param_types[$offset])
|
&& !isset($implemented_docblock_param_types[$offset])
|
||||||
) {
|
) {
|
||||||
$possible_type = null;
|
if ($codebase->alter_code
|
||||||
|
&& isset($project_analyzer->getIssuesToFix()['MissingParamType'])
|
||||||
|
) {
|
||||||
|
$possible_type = $context->possible_param_types[$function_param->name] ?? null;
|
||||||
|
|
||||||
if (isset($context->possible_param_types[$function_param->name])) {
|
if (!$possible_type || $possible_type->hasMixed() || $possible_type->isNull()) {
|
||||||
$possible_type = $context->possible_param_types[$function_param->name];
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::addOrUpdateParamType(
|
||||||
|
$project_analyzer,
|
||||||
|
$function_param->name,
|
||||||
|
$possible_type
|
||||||
|
);
|
||||||
|
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$infer_text = $codebase->infer_types_from_usage
|
|
||||||
? ', ' . ($possible_type ? 'should be ' . $possible_type : 'could not infer type')
|
|
||||||
: '';
|
|
||||||
|
|
||||||
if ($this->function instanceof Closure) {
|
if ($this->function instanceof Closure) {
|
||||||
IssueBuffer::accepts(
|
IssueBuffer::accepts(
|
||||||
new MissingClosureParamType(
|
new MissingClosureParamType(
|
||||||
'Parameter $' . $function_param->name . ' has no provided type' . $infer_text,
|
'Parameter $' . $function_param->name . ' has no provided type',
|
||||||
$function_param->location
|
$function_param->location
|
||||||
),
|
),
|
||||||
array_merge($this->suppressed_issues, $storage->suppressed_issues)
|
array_merge($this->suppressed_issues, $storage->suppressed_issues)
|
||||||
@ -574,7 +582,7 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
|
|||||||
} else {
|
} else {
|
||||||
IssueBuffer::accepts(
|
IssueBuffer::accepts(
|
||||||
new MissingParamType(
|
new MissingParamType(
|
||||||
'Parameter $' . $function_param->name . ' has no provided type' . $infer_text,
|
'Parameter $' . $function_param->name . ' has no provided type',
|
||||||
$function_param->location
|
$function_param->location
|
||||||
),
|
),
|
||||||
array_merge($this->suppressed_issues, $storage->suppressed_issues)
|
array_merge($this->suppressed_issues, $storage->suppressed_issues)
|
||||||
|
@ -750,6 +750,7 @@ class ProjectAnalyzer
|
|||||||
$safe_types = false
|
$safe_types = false
|
||||||
) {
|
) {
|
||||||
$this->codebase->alter_code = true;
|
$this->codebase->alter_code = true;
|
||||||
|
$this->codebase->infer_types_from_usage = true;
|
||||||
$this->show_issues = false;
|
$this->show_issues = false;
|
||||||
$this->php_major_version = $php_major_version;
|
$this->php_major_version = $php_major_version;
|
||||||
$this->php_minor_version = $php_minor_version;
|
$this->php_minor_version = $php_minor_version;
|
||||||
|
@ -655,7 +655,7 @@ class IfAnalyzer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($codebase->infer_types_from_usage) {
|
if ($if_context->infer_types) {
|
||||||
$if_scope->possible_param_types = $if_context->possible_param_types;
|
$if_scope->possible_param_types = $if_context->possible_param_types;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -1167,7 +1167,7 @@ class IfAnalyzer
|
|||||||
$if_scope->reasonable_clauses = [];
|
$if_scope->reasonable_clauses = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($codebase->infer_types_from_usage) {
|
if ($elseif_context->infer_types) {
|
||||||
$elseif_possible_param_types = $elseif_context->possible_param_types;
|
$elseif_possible_param_types = $elseif_context->possible_param_types;
|
||||||
|
|
||||||
if ($if_scope->possible_param_types) {
|
if ($if_scope->possible_param_types) {
|
||||||
@ -1587,7 +1587,7 @@ class IfAnalyzer
|
|||||||
$outer_context->possibly_thrown_exceptions += $else_context->possibly_thrown_exceptions;
|
$outer_context->possibly_thrown_exceptions += $else_context->possibly_thrown_exceptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($codebase->infer_types_from_usage) {
|
if ($else_context->infer_types) {
|
||||||
$else_possible_param_types = $else_context->possible_param_types;
|
$else_possible_param_types = $else_context->possible_param_types;
|
||||||
|
|
||||||
if ($if_scope->possible_param_types) {
|
if ($if_scope->possible_param_types) {
|
||||||
|
@ -541,7 +541,7 @@ class BinaryOpAnalyzer
|
|||||||
) {
|
) {
|
||||||
$stmt->inferredType = Type::getBool();
|
$stmt->inferredType = Type::getBool();
|
||||||
} elseif ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Div) {
|
} elseif ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Div) {
|
||||||
if ($codebase->infer_types_from_usage
|
if ($context->infer_types
|
||||||
&& isset($stmt->left->inferredType)
|
&& isset($stmt->left->inferredType)
|
||||||
&& isset($stmt->right->inferredType)
|
&& isset($stmt->right->inferredType)
|
||||||
&& ($stmt->left->inferredType->isMixed() || $stmt->right->inferredType->isMixed())
|
&& ($stmt->left->inferredType->isMixed() || $stmt->right->inferredType->isMixed())
|
||||||
@ -643,9 +643,9 @@ class BinaryOpAnalyzer
|
|||||||
$config = Config::getInstance();
|
$config = Config::getInstance();
|
||||||
|
|
||||||
if ($codebase
|
if ($codebase
|
||||||
&& $codebase->infer_types_from_usage
|
|
||||||
&& $statements_source
|
&& $statements_source
|
||||||
&& $context
|
&& $context
|
||||||
|
&& $context->infer_types
|
||||||
&& $left_type
|
&& $left_type
|
||||||
&& $right_type
|
&& $right_type
|
||||||
&& ($left_type->isVanillaMixed() || $right_type->isVanillaMixed())
|
&& ($left_type->isVanillaMixed() || $right_type->isVanillaMixed())
|
||||||
@ -1188,7 +1188,7 @@ class BinaryOpAnalyzer
|
|||||||
$right_type = isset($right->inferredType) ? $right->inferredType : null;
|
$right_type = isset($right->inferredType) ? $right->inferredType : null;
|
||||||
$config = Config::getInstance();
|
$config = Config::getInstance();
|
||||||
|
|
||||||
if ($codebase->infer_types_from_usage
|
if ($context->infer_types
|
||||||
&& $left_type
|
&& $left_type
|
||||||
&& $right_type
|
&& $right_type
|
||||||
&& ($left_type->isMixed() || $right_type->isMixed())
|
&& ($left_type->isMixed() || $right_type->isMixed())
|
||||||
|
@ -1761,7 +1761,7 @@ class CallAnalyzer
|
|||||||
|
|
||||||
$method_identifier = $cased_method_id ? ' of ' . $cased_method_id : '';
|
$method_identifier = $cased_method_id ? ' of ' . $cased_method_id : '';
|
||||||
|
|
||||||
if ($codebase->infer_types_from_usage && $input_expr->inferredType) {
|
if ($context->infer_types && isset($input_expr->inferredType)) {
|
||||||
$source_analyzer = $statements_analyzer->getSource();
|
$source_analyzer = $statements_analyzer->getSource();
|
||||||
|
|
||||||
if ($source_analyzer instanceof FunctionLikeAnalyzer) {
|
if ($source_analyzer instanceof FunctionLikeAnalyzer) {
|
||||||
|
@ -1308,11 +1308,9 @@ class ExpressionAnalyzer
|
|||||||
PhpParser\Node\Scalar\Encapsed $stmt,
|
PhpParser\Node\Scalar\Encapsed $stmt,
|
||||||
Context $context
|
Context $context
|
||||||
) {
|
) {
|
||||||
$codebase = $statements_analyzer->getCodebase();
|
|
||||||
|
|
||||||
$function_storage = null;
|
$function_storage = null;
|
||||||
|
|
||||||
if ($codebase->infer_types_from_usage) {
|
if ($context->infer_types) {
|
||||||
$source_analyzer = $statements_analyzer->getSource();
|
$source_analyzer = $statements_analyzer->getSource();
|
||||||
|
|
||||||
if ($source_analyzer instanceof FunctionLikeAnalyzer) {
|
if ($source_analyzer instanceof FunctionLikeAnalyzer) {
|
||||||
|
@ -496,6 +496,7 @@ class StatementsAnalyzer extends SourceAnalyzer implements StatementsSource
|
|||||||
$function_id = strtolower($stmt->name->name);
|
$function_id = strtolower($stmt->name->name);
|
||||||
$function_context = new Context($context->self);
|
$function_context = new Context($context->self);
|
||||||
$config = Config::getInstance();
|
$config = Config::getInstance();
|
||||||
|
$function_context->infer_types = $codebase->infer_types_from_usage;
|
||||||
$function_context->collect_references = $codebase->collect_references;
|
$function_context->collect_references = $codebase->collect_references;
|
||||||
$function_context->collect_exceptions = $config->check_for_throws_docblock;
|
$function_context->collect_exceptions = $config->check_for_throws_docblock;
|
||||||
$this->function_analyzers[$function_id]->analyze($function_context, $context);
|
$this->function_analyzers[$function_id]->analyze($function_context, $context);
|
||||||
|
@ -252,7 +252,7 @@ class FunctionDocblockManipulator
|
|||||||
*/
|
*/
|
||||||
public function setReturnType($php_type, $new_type, $phpdoc_type, $is_php_compatible, $description)
|
public function setReturnType($php_type, $new_type, $phpdoc_type, $is_php_compatible, $description)
|
||||||
{
|
{
|
||||||
$new_type = str_replace(['<mixed, mixed>', '<empty, empty>'], '', $new_type);
|
$new_type = str_replace(['<mixed, mixed>', '<array-key, mixed>', '<empty, empty>'], '', $new_type);
|
||||||
|
|
||||||
$this->new_php_return_type = $php_type;
|
$this->new_php_return_type = $php_type;
|
||||||
$this->new_phpdoc_return_type = $phpdoc_type;
|
$this->new_phpdoc_return_type = $phpdoc_type;
|
||||||
@ -274,7 +274,7 @@ class FunctionDocblockManipulator
|
|||||||
*/
|
*/
|
||||||
public function setParamType($param_name, $php_type, $new_type, $phpdoc_type, $is_php_compatible)
|
public function setParamType($param_name, $php_type, $new_type, $phpdoc_type, $is_php_compatible)
|
||||||
{
|
{
|
||||||
$new_type = str_replace(['<mixed, mixed>', '<empty, empty>'], '', $new_type);
|
$new_type = str_replace(['<mixed, mixed>', '<array-key, mixed>', '<empty, empty>'], '', $new_type);
|
||||||
|
|
||||||
if ($php_type) {
|
if ($php_type) {
|
||||||
$this->new_php_param_types[$param_name] = $php_type;
|
$this->new_php_param_types[$param_name] = $php_type;
|
||||||
@ -317,7 +317,7 @@ class FunctionDocblockManipulator
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!$found_in_params) {
|
if (!$found_in_params) {
|
||||||
$parsed_docblock['specials']['params'][] = $new_param_block;
|
$parsed_docblock['specials']['param'][] = $new_param_block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -961,64 +961,6 @@ class AnnotationTest extends TestCase
|
|||||||
}',
|
}',
|
||||||
'error_message' => 'MismatchingDocblockReturnType',
|
'error_message' => 'MismatchingDocblockReturnType',
|
||||||
],
|
],
|
||||||
'noStringParamType' => [
|
|
||||||
'<?php
|
|
||||||
function fooFoo($a): void {
|
|
||||||
echo substr($a, 4, 2);
|
|
||||||
}',
|
|
||||||
'error_message' => 'MissingParamType - src' . DIRECTORY_SEPARATOR . 'somefile.php:2 - Parameter $a has no provided type,'
|
|
||||||
. ' should be string',
|
|
||||||
'error_levels' => ['MixedArgument'],
|
|
||||||
],
|
|
||||||
'noParamTypeButConcat' => [
|
|
||||||
'<?php
|
|
||||||
function fooFoo($a): void {
|
|
||||||
echo $a . "foo";
|
|
||||||
}',
|
|
||||||
'error_message' => 'MissingParamType - src' . DIRECTORY_SEPARATOR . 'somefile.php:2 - Parameter $a has no provided type,'
|
|
||||||
. ' should be string',
|
|
||||||
'error_levels' => ['MixedOperand'],
|
|
||||||
],
|
|
||||||
'noParamTypeButAddition' => [
|
|
||||||
'<?php
|
|
||||||
function fooFoo($a): void {
|
|
||||||
echo $a + 5;
|
|
||||||
}',
|
|
||||||
'error_message' => 'MissingParamType - src' . DIRECTORY_SEPARATOR . 'somefile.php:2 - Parameter $a has no provided type,'
|
|
||||||
. ' should be int|float',
|
|
||||||
'error_levels' => ['MixedOperand', 'MixedArgument'],
|
|
||||||
],
|
|
||||||
'noParamTypeButDivision' => [
|
|
||||||
'<?php
|
|
||||||
function fooFoo($a): void {
|
|
||||||
echo $a / 5;
|
|
||||||
}',
|
|
||||||
'error_message' => 'MissingParamType - src' . DIRECTORY_SEPARATOR . 'somefile.php:2 - Parameter $a has no provided type,'
|
|
||||||
. ' should be int|float',
|
|
||||||
'error_levels' => ['MixedOperand', 'MixedArgument'],
|
|
||||||
],
|
|
||||||
'noParamTypeButTemplatedString' => [
|
|
||||||
'<?php
|
|
||||||
function fooFoo($a): void {
|
|
||||||
echo "$a";
|
|
||||||
}',
|
|
||||||
'error_message' => 'MissingParamType - src' . DIRECTORY_SEPARATOR . 'somefile.php:2 - Parameter $a has no provided type,'
|
|
||||||
. ' should be string',
|
|
||||||
'error_levels' => ['MixedOperand'],
|
|
||||||
],
|
|
||||||
'noStringIntParamType' => [
|
|
||||||
'<?php
|
|
||||||
function fooFoo($a): void {
|
|
||||||
if (is_string($a)) {
|
|
||||||
echo substr($a, 4, 2);
|
|
||||||
} else {
|
|
||||||
echo substr("hello", $a, 2);
|
|
||||||
}
|
|
||||||
}',
|
|
||||||
'error_message' => 'MissingParamType - src' . DIRECTORY_SEPARATOR . 'somefile.php:2 - Parameter $a has no provided type,'
|
|
||||||
. ' should be int|string',
|
|
||||||
'error_levels' => ['MixedArgument'],
|
|
||||||
],
|
|
||||||
'intParamTypeDefinedInParent' => [
|
'intParamTypeDefinedInParent' => [
|
||||||
'<?php
|
'<?php
|
||||||
class A {
|
class A {
|
||||||
@ -1031,32 +973,6 @@ class AnnotationTest extends TestCase
|
|||||||
'error_message' => 'MissingParamType',
|
'error_message' => 'MissingParamType',
|
||||||
'error_levels' => ['MethodSignatureMismatch'],
|
'error_levels' => ['MethodSignatureMismatch'],
|
||||||
],
|
],
|
||||||
'alreadyHasCheck' => [
|
|
||||||
'<?php
|
|
||||||
function takesString(string $s): void {}
|
|
||||||
|
|
||||||
function shouldTakeString($s): void {
|
|
||||||
if (is_string($s)) takesString($s);
|
|
||||||
}',
|
|
||||||
'error_message' => 'MissingParamType - src' . DIRECTORY_SEPARATOR . 'somefile.php:4 - Parameter $s has no provided type,'
|
|
||||||
. ' could not infer',
|
|
||||||
'error_levels' => ['MixedArgument'],
|
|
||||||
],
|
|
||||||
'isSetBeforeInferrence' => [
|
|
||||||
'input' => '<?php
|
|
||||||
function takesString(string $s): void {}
|
|
||||||
|
|
||||||
/** @return mixed */
|
|
||||||
function returnsMixed() {}
|
|
||||||
|
|
||||||
function shouldTakeString($s): void {
|
|
||||||
$s = returnsMixed();
|
|
||||||
takesString($s);
|
|
||||||
}',
|
|
||||||
'error_message' => 'MissingParamType - src' . DIRECTORY_SEPARATOR . 'somefile.php:7 - Parameter $s has no provided type,'
|
|
||||||
. ' could not infer',
|
|
||||||
'error_levels' => ['MixedArgument', 'InvalidReturnType', 'MixedAssignment'],
|
|
||||||
],
|
|
||||||
'psalmInvalidVar' => [
|
'psalmInvalidVar' => [
|
||||||
'<?php
|
'<?php
|
||||||
class A
|
class A
|
||||||
|
@ -1302,6 +1302,152 @@ class FileManipulationTest extends TestCase
|
|||||||
[],
|
[],
|
||||||
true,
|
true,
|
||||||
],
|
],
|
||||||
|
'noStringParamType' => [
|
||||||
|
'<?php
|
||||||
|
function fooFoo($a): void {
|
||||||
|
echo substr($a, 4, 2);
|
||||||
|
}',
|
||||||
|
'<?php
|
||||||
|
/**
|
||||||
|
* @param string $a
|
||||||
|
*/
|
||||||
|
function fooFoo($a): void {
|
||||||
|
echo substr($a, 4, 2);
|
||||||
|
}',
|
||||||
|
'7.1',
|
||||||
|
['MissingParamType'],
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
'noParamTypeButConcat' => [
|
||||||
|
'<?php
|
||||||
|
function fooFoo($a): void {
|
||||||
|
echo $a . "foo";
|
||||||
|
}',
|
||||||
|
'<?php
|
||||||
|
/**
|
||||||
|
* @param string $a
|
||||||
|
*/
|
||||||
|
function fooFoo($a): void {
|
||||||
|
echo $a . "foo";
|
||||||
|
}',
|
||||||
|
'7.1',
|
||||||
|
['MissingParamType'],
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
'noParamTypeButAddition' => [
|
||||||
|
'<?php
|
||||||
|
function fooFoo($a): void {
|
||||||
|
echo $a + 5;
|
||||||
|
}',
|
||||||
|
'<?php
|
||||||
|
/**
|
||||||
|
* @param int|float $a
|
||||||
|
*/
|
||||||
|
function fooFoo($a): void {
|
||||||
|
echo $a + 5;
|
||||||
|
}',
|
||||||
|
'7.1',
|
||||||
|
['MissingParamType'],
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
'noParamTypeButDivision' => [
|
||||||
|
'<?php
|
||||||
|
function fooFoo($a): void {
|
||||||
|
echo $a / 5;
|
||||||
|
}',
|
||||||
|
'<?php
|
||||||
|
/**
|
||||||
|
* @param int|float $a
|
||||||
|
*/
|
||||||
|
function fooFoo($a): void {
|
||||||
|
echo $a / 5;
|
||||||
|
}',
|
||||||
|
'7.1',
|
||||||
|
['MissingParamType'],
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
'noParamTypeButTemplatedString' => [
|
||||||
|
'<?php
|
||||||
|
function fooFoo($a): void {
|
||||||
|
echo "$a";
|
||||||
|
}',
|
||||||
|
'<?php
|
||||||
|
/**
|
||||||
|
* @param string $a
|
||||||
|
*/
|
||||||
|
function fooFoo($a): void {
|
||||||
|
echo "$a";
|
||||||
|
}',
|
||||||
|
'7.1',
|
||||||
|
['MissingParamType'],
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
'noStringIntParamType' => [
|
||||||
|
'<?php
|
||||||
|
function fooFoo($a): void {
|
||||||
|
if (is_string($a)) {
|
||||||
|
echo substr($a, 4, 2);
|
||||||
|
} else {
|
||||||
|
echo substr("hello", $a, 2);
|
||||||
|
}
|
||||||
|
}',
|
||||||
|
'<?php
|
||||||
|
/**
|
||||||
|
* @param int|string $a
|
||||||
|
*/
|
||||||
|
function fooFoo($a): void {
|
||||||
|
if (is_string($a)) {
|
||||||
|
echo substr($a, 4, 2);
|
||||||
|
} else {
|
||||||
|
echo substr("hello", $a, 2);
|
||||||
|
}
|
||||||
|
}',
|
||||||
|
'7.1',
|
||||||
|
['MissingParamType'],
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
'alreadyHasCheck' => [
|
||||||
|
'<?php
|
||||||
|
function takesString(string $s): void {}
|
||||||
|
|
||||||
|
function shouldTakeString($s): void {
|
||||||
|
if (is_string($s)) takesString($s);
|
||||||
|
}',
|
||||||
|
'<?php
|
||||||
|
function takesString(string $s): void {}
|
||||||
|
|
||||||
|
function shouldTakeString($s): void {
|
||||||
|
if (is_string($s)) takesString($s);
|
||||||
|
}',
|
||||||
|
'7.1',
|
||||||
|
['MissingParamType'],
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
'isSetBeforeInferrence' => [
|
||||||
|
'<?php
|
||||||
|
function takesString(string $s): void {}
|
||||||
|
|
||||||
|
/** @return mixed */
|
||||||
|
function returnsMixed() {}
|
||||||
|
|
||||||
|
function shouldTakeString($s): void {
|
||||||
|
$s = returnsMixed();
|
||||||
|
takesString($s);
|
||||||
|
}',
|
||||||
|
'<?php
|
||||||
|
function takesString(string $s): void {}
|
||||||
|
|
||||||
|
/** @return mixed */
|
||||||
|
function returnsMixed() {}
|
||||||
|
|
||||||
|
function shouldTakeString($s): void {
|
||||||
|
$s = returnsMixed();
|
||||||
|
takesString($s);
|
||||||
|
}',
|
||||||
|
'7.1',
|
||||||
|
['MissingParamType'],
|
||||||
|
true,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user