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:
parent
1cc63fe718
commit
8aabcbce35
@ -23,5 +23,6 @@
|
||||
|
||||
<PropertyNotSetInConstructor errorLevel="info" />
|
||||
<MissingConstructor errorLevel="info" />
|
||||
<UntypedParam errorLevel="info" />
|
||||
</issueHandlers>
|
||||
</psalm>
|
||||
|
@ -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 -->
|
||||
|
||||
|
@ -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 -->
|
||||
|
||||
|
@ -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" />
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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];
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
6
src/Psalm/Issue/UntypedParam.php
Normal file
6
src/Psalm/Issue/UntypedParam.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Psalm\Issue;
|
||||
|
||||
class UntypedParam extends CodeError
|
||||
{
|
||||
}
|
@ -18,7 +18,7 @@ class FunctionLikeStorage
|
||||
public $params = [];
|
||||
|
||||
/**
|
||||
* @var array<string, Type\Union>
|
||||
* @var array<string, Type\Union|null>
|
||||
*/
|
||||
public $param_types = [];
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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' => [
|
||||
|
@ -338,8 +338,12 @@ class FunctionCallTest extends TestCase
|
||||
],
|
||||
'duplicateParam' => [
|
||||
'<?php
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
function f($p, $p) {}',
|
||||
'error_message' => 'DuplicateParam',
|
||||
'error_levels' => ['UntypedParam'],
|
||||
],
|
||||
'invalidParamDefault' => [
|
||||
'<?php
|
||||
|
@ -244,6 +244,7 @@ class TypeReconciliationTest extends TestCase
|
||||
return [
|
||||
'intIsMixed' => [
|
||||
'<?php
|
||||
/** @param mixed $a */
|
||||
function foo($a) : void {
|
||||
$b = 5;
|
||||
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user