1
0
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:
Matthew Brown 2019-02-02 11:28:48 -05:00
parent 36343b07ce
commit db89b3cc3f
12 changed files with 182 additions and 106 deletions

View File

@ -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
*/ */

View File

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

View File

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

View File

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

View File

@ -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) {

View File

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

View File

@ -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) {

View File

@ -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) {

View File

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

View File

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

View File

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

View File

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