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

Introduce UntypedParam warnings when functions are missing param types

This commit is contained in:
Matthew Brown 2017-09-02 11:18:56 -04:00
parent 1cc63fe718
commit 8aabcbce35
17 changed files with 172 additions and 53 deletions

View File

@ -23,5 +23,6 @@
<PropertyNotSetInConstructor errorLevel="info" />
<MissingConstructor errorLevel="info" />
<UntypedParam errorLevel="info" />
</issueHandlers>
</psalm>

View File

@ -24,6 +24,7 @@
<PropertyNotSetInConstructor errorLevel="info" />
<MissingConstructor errorLevel="info" />
<UntypedParam errorLevel="info" />
<!-- level 4 issues - points to possible deficiencies in logic, higher false-positives -->

View File

@ -24,6 +24,7 @@
<PropertyNotSetInConstructor errorLevel="info" />
<MissingConstructor errorLevel="info" />
<UntypedParam errorLevel="info" />
<!-- level 4 issues - points to possible deficiencies in logic, higher false-positives -->

View File

@ -180,6 +180,7 @@
<xs:element name="UndefinedThisPropertyFetch" type="IssueHandlerType" minOccurs="0" />
<xs:element name="UndefinedTrait" type="IssueHandlerType" minOccurs="0" />
<xs:element name="UndefinedVariable" type="IssueHandlerType" minOccurs="0" />
<xs:element name="UntypedParam" type="IssueHandlerType" minOccurs="0" />
<xs:element name="UnimplementedAbstractMethod" type="IssueHandlerType" minOccurs="0" />
<xs:element name="UnimplementedInterfaceMethod" type="IssueHandlerType" minOccurs="0" />
<xs:element name="UnrecognizedExpression" type="IssueHandlerType" minOccurs="0" />

View File

@ -534,9 +534,12 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
$fake_constructor_params = array_map(
/** @return PhpParser\Node\Param */
function (\Psalm\FunctionLikeParameter $param) {
return (new PhpParser\Builder\Param($param->name))
->setTypehint((string)$param->signature_type)
->getNode();
$fake_param = (new PhpParser\Builder\Param($param->name));
if ($param->signature_type) {
$fake_param->setTypehint((string)$param->signature_type);
}
return $fake_param->getNode();
},
$constructor_storage->params
);

View File

@ -24,6 +24,7 @@ use Psalm\Issue\MixedInferredReturnType;
use Psalm\Issue\MoreSpecificReturnType;
use Psalm\Issue\OverriddenMethodAccess;
use Psalm\Issue\PossiblyUnusedVariable;
use Psalm\Issue\UntypedParam;
use Psalm\Issue\UnusedVariable;
use Psalm\IssueBuffer;
use Psalm\Mutator\FileMutator;
@ -171,6 +172,8 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
$context->inside_constructor = true;
}
$implemented_docblock_param_types = [];
if ($implemented_method_ids) {
$have_emitted = false;
@ -260,7 +263,14 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
break 2;
}
if ($implemented_param->type
&& (!$implemented_param->signature_type || !$class_storage->user_defined)
) {
$implemented_docblock_param_types[$i] = true;
}
if (!$class_storage->user_defined &&
$implemented_param->type &&
!$implemented_param->type->isMixed() &&
(string)$storage->params[$i]->type !== (string)$implemented_param->type
) {
@ -362,14 +372,30 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
foreach ($storage->params as $offset => $function_param) {
$signature_type = $function_param->signature_type;
$param_type = clone $function_param->type;
$param_type = ExpressionChecker::fleshOutType(
$project_checker,
$param_type,
$context->self,
$this->getMethodId()
);
if ($function_param->type) {
$param_type = clone $function_param->type;
$param_type = ExpressionChecker::fleshOutType(
$project_checker,
$param_type,
$context->self,
$this->getMethodId()
);
} else {
// only complain if there's no type defined by a parent type
if ($function_param->location && !isset($implemented_docblock_param_types[$offset])) {
IssueBuffer::accepts(
new UntypedParam(
'Parameter $' . $function_param->name . ' has no provided type',
$function_param->location
),
$storage->suppressed_issues
);
}
$param_type = Type::getMixed();
}
$context->vars_in_scope['$' . $function_param->name] = $param_type;
$context->vars_possibly_in_scope['$' . $function_param->name] = true;
@ -1202,6 +1228,10 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
$param_type = $possible_function_params[$argument_offset]->type;
if (!$param_type) {
continue;
}
if (!isset($arg->value->inferredType)) {
continue;
}

View File

@ -220,7 +220,9 @@ class MethodChecker extends FunctionLikeChecker
}
} else {
foreach ($possible_params[0] as $param) {
$param->type->queueClassLikesForScanning($project_checker);
if ($param->type) {
$param->type->queueClassLikesForScanning($project_checker);
}
}
$storage->params = $possible_params[0];

View File

@ -1506,9 +1506,13 @@ class CallChecker
$by_ref_type = null;
if ($by_ref && $last_param) {
$by_ref_type = $argument_offset < count($function_params)
? clone $function_params[$argument_offset]->type
: clone $last_param->type;
if ($argument_offset < count($function_params)) {
$by_ref_type = $function_params[$argument_offset]->type;
} else {
$by_ref_type = $last_param->type;
}
$by_ref_type = $by_ref_type ? clone $by_ref_type : Type::getMixed();
}
if ($by_ref && $by_ref_type) {
@ -1548,9 +1552,13 @@ class CallChecker
$by_ref_type = null;
if ($by_ref && $last_param) {
$by_ref_type = $argument_offset < count($function_params)
? clone $function_params[$argument_offset]->type
: clone $last_param->type;
if ($argument_offset < count($function_params)) {
$by_ref_type = $function_params[$argument_offset]->type;
} else {
$by_ref_type = $last_param->type;
}
$by_ref_type = $by_ref_type ? clone $by_ref_type : Type::getMixed();
}
if (ExpressionChecker::analyzeVariable(
@ -1675,7 +1683,7 @@ class CallChecker
? $function_params[$argument_offset]
: ($last_param && $last_param->is_variadic ? $last_param : null);
if ($function_param) {
if ($function_param && $function_param->type) {
$param_type = clone $function_param->type;
if ($function_param->is_variadic) {
@ -1931,6 +1939,11 @@ class CallChecker
$closure_param_type = $closure_param->type;
if (!$closure_param_type) {
++$i;
continue;
}
$type_match_found = TypeChecker::isContainedBy(
$project_checker,
$input_type,

View File

@ -14,12 +14,12 @@ class FunctionLikeParameter
public $by_ref;
/**
* @var Type\Union
* @var Type\Union|null
*/
public $type;
/**
* @var Type\Union
* @var Type\Union|null
*/
public $signature_type;
@ -51,8 +51,8 @@ class FunctionLikeParameter
/**
* @param string $name
* @param bool $by_ref
* @param Type\Union $type
* @param CodeLocation $location
* @param Type\Union|null $type
* @param CodeLocation|null $location
* @param bool $is_optional
* @param bool $is_nullable
* @param bool $is_variadic
@ -60,7 +60,7 @@ class FunctionLikeParameter
public function __construct(
$name,
$by_ref,
Type\Union $type,
Type\Union $type = null,
CodeLocation $location = null,
$is_optional = true,
$is_nullable = false,

View File

@ -0,0 +1,6 @@
<?php
namespace Psalm\Issue;
class UntypedParam extends CodeError
{
}

View File

@ -18,7 +18,7 @@ class FunctionLikeStorage
public $params = [];
/**
* @var array<string, Type\Union>
* @var array<string, Type\Union|null>
*/
public $param_types = [];

View File

@ -293,10 +293,12 @@ class DependencyFinderVisitor extends PhpParser\NodeVisitorAbstract implements P
if ($function_params) {
foreach ($function_params as $function_param_group) {
foreach ($function_param_group as $function_param) {
$function_param->type->queueClassLikesForScanning(
$this->project_checker,
$this->file_path
);
if ($function_param->type) {
$function_param->type->queueClassLikesForScanning(
$this->project_checker,
$this->file_path
);
}
}
}
}
@ -554,21 +556,24 @@ class DependencyFinderVisitor extends PhpParser\NodeVisitorAbstract implements P
$i = 0;
$has_optional_param = false;
$existing_params = [];
/** @var PhpParser\Node\Param $param */
foreach ($stmt->getParams() as $param) {
$param_array = $this->getTranslatedFunctionParam($param);
if (isset($storage->param_types[$param_array->name])) {
if (isset($existing_params[$param_array->name])) {
if (IssueBuffer::accepts(
new DuplicateParam(
'Duplicate param $' . $param->name . ' in docblock for ' . $cased_function_id,
new CodeLocation($this->file_checker, $param, null, true)
)
)) {
// fall through
continue;
}
}
$existing_params[$param_array->name] = true;
$storage->param_types[$param_array->name] = $param_array->type;
$storage->params[] = $param_array;
@ -840,7 +845,7 @@ class DependencyFinderVisitor extends PhpParser\NodeVisitorAbstract implements P
return new FunctionLikeParameter(
$param->name,
$param->byRef,
$param_type ?: Type::getMixed(),
$param_type,
new CodeLocation($this->file_checker, $param, null, false, FunctionLikeChecker::PARAM_TYPE_REGEX),
$is_optional,
$is_nullable,
@ -887,10 +892,6 @@ class DependencyFinderVisitor extends PhpParser\NodeVisitorAbstract implements P
$param_name = substr($param_name, 1);
if (!isset($storage->param_types[$param_name])) {
continue;
}
$storage_param = null;
foreach ($storage->params as $function_signature_param) {
@ -901,11 +902,9 @@ class DependencyFinderVisitor extends PhpParser\NodeVisitorAbstract implements P
}
if ($storage_param === null) {
throw new \UnexpectedValueException('This should not be possible');
continue;
}
$storage_param_type = $storage->param_types[$param_name];
$docblock_param_vars[$param_name] = true;
$new_param_type = Type::parseString(
@ -935,7 +934,7 @@ class DependencyFinderVisitor extends PhpParser\NodeVisitorAbstract implements P
$new_param_type->setFromDocblock();
if ($storage_param->type->isMixed() || $storage->template_types) {
if (!$storage_param->type || $storage_param->type->isMixed() || $storage->template_types) {
if ($existing_param_type_nullable && !$new_param_type->isNullable()) {
$new_param_type->types['null'] = new Type\Atomic\TNull();
}

View File

@ -131,14 +131,19 @@ class AnnotationTest extends TestCase
* @property string $foo
*/
class A {
public function __get($name) : ?string {
if ($name === "foo") {
return "hello";
}
}
/** @param string $name */
public function __get($name) : ?string {
if ($name === "foo") {
return "hello";
}
}
public function __set($name, $value) : void {
}
/**
* @param string $name
* @param mixed $value
*/
public function __set($name, $value) : void {
}
}
$a = new A();
@ -194,6 +199,28 @@ class AnnotationTest extends TestCase
$a[0]->getMessage();',
],
'mixedDocblockParamTypeDefinedInParent' => [
'<?php
class A {
/** @param mixed $a */
public function foo($a) : void {}
}
class B extends A {
public function foo($a) : void {}
}',
],
'intDocblockParamTypeDefinedInParent' => [
'<?php
class A {
/** @param int $a */
public function foo($a) : void {}
}
class B extends A {
public function foo($a) : void {}
}',
],
];
}
@ -354,6 +381,25 @@ class AnnotationTest extends TestCase
$a->foo = 5;',
'error_message' => 'InvalidPropertyAssignment',
],
'noParamType' => [
'<?php
function fooFoo($a) : void {
if ($a) {}
}',
'error_message' => 'UntypedParam',
],
'intParamTypeDefinedInParent' => [
'<?php
class A {
public function foo(int $a) : void {}
}
class B extends A {
public function foo($a) : void {}
}',
'error_message' => 'UntypedParam',
'error_levels' => ['MethodSignatureMismatch'],
],
];
}
}

View File

@ -219,6 +219,11 @@ class ClosureTest extends TestCase
return new Foo([]);
}
/**
* @param mixed $argOne
* @param mixed $argTwo
* @return void
*/
public function bar($argOne, $argTwo)
{
$this->getFoo()($argOne, $argTwo);
@ -267,14 +272,17 @@ class ClosureTest extends TestCase
*/
$foo = null;
$foo = function ($bar) use (&$foo) : string
{
if (is_array($bar)) {
return $foo($bar);
}
return $bar;
};',
$foo =
/** @param mixed $bar */
function ($bar) use (&$foo) : string
{
if (is_array($bar)) {
return $foo($bar);
}
return $bar;
};',
'error_message' => 'PossiblyNullFunctionCall',
],
'stringFunctionCall' => [

View File

@ -338,8 +338,12 @@ class FunctionCallTest extends TestCase
],
'duplicateParam' => [
'<?php
/**
* @return void
*/
function f($p, $p) {}',
'error_message' => 'DuplicateParam',
'error_levels' => ['UntypedParam'],
],
'invalidParamDefault' => [
'<?php

View File

@ -244,6 +244,7 @@ class TypeReconciliationTest extends TestCase
return [
'intIsMixed' => [
'<?php
/** @param mixed $a */
function foo($a) : void {
$b = 5;

View File

@ -60,6 +60,9 @@ class VariadicTest extends TestCase
'variadic' => [
'<?php
/**
* @param mixed $req
* @param mixed $opt
* @param array<int, mixed> $params
* @return array<mixed>
*/
function f($req, $opt = null, ...$params) {