mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Do more to separate out processing steps
This commit is contained in:
parent
df941bd575
commit
6ee69e547f
@ -431,24 +431,42 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->method_checkers as $method_id => $method_checker) {
|
||||
$method_checker->check(clone $class_context, null);
|
||||
foreach ($this->class->stmts as $stmt) {
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod) {
|
||||
$method_checker = new MethodChecker($stmt, $this);
|
||||
|
||||
if (!$config->excludeIssueInFile('InvalidReturnType', $this->file_name)) {
|
||||
$secondary_return_type_location = null;
|
||||
$method_checker->check(clone $class_context, null);
|
||||
|
||||
$return_type_location = MethodChecker::getMethodReturnTypeLocation(
|
||||
$method_id,
|
||||
$secondary_return_type_location
|
||||
);
|
||||
$method_id = (string)$method_checker->getMethodId();
|
||||
|
||||
$method_checker->checkReturnTypes(
|
||||
$update_docblocks,
|
||||
MethodChecker::getMethodReturnType($method_id),
|
||||
$this->fq_class_name,
|
||||
$return_type_location,
|
||||
$secondary_return_type_location
|
||||
);
|
||||
if (!$config->excludeIssueInFile('InvalidReturnType', $this->file_name)) {
|
||||
$secondary_return_type_location = null;
|
||||
|
||||
$return_type_location = MethodChecker::getMethodReturnTypeLocation(
|
||||
$method_id,
|
||||
$secondary_return_type_location
|
||||
);
|
||||
|
||||
$method_checker->checkReturnTypes(
|
||||
$update_docblocks,
|
||||
MethodChecker::getMethodReturnType($method_id),
|
||||
$class_context->self,
|
||||
$return_type_location,
|
||||
$secondary_return_type_location
|
||||
);
|
||||
}
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\TraitUse) {
|
||||
foreach ($stmt->traits as $trait) {
|
||||
$trait_name = self::getFQCLNFromNameObject(
|
||||
$trait,
|
||||
$this->namespace,
|
||||
$this->aliased_classes
|
||||
);
|
||||
|
||||
$trait_checker = self::$trait_checkers[$trait_name];
|
||||
|
||||
$trait_checker->checkMethods($class_context, $global_context, $update_docblocks);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -510,10 +528,13 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
PhpParser\Node\Stmt\ClassMethod $stmt,
|
||||
Context $class_context
|
||||
) {
|
||||
$method_id = $this->fq_class_name . '::' . strtolower($stmt->name);
|
||||
$storage = self::$storage[$class_context->self];
|
||||
$method_name = strtolower($stmt->name);
|
||||
$method_id = $this->fq_class_name . '::' . $method_name;
|
||||
$storage = self::$storage[$this->fq_class_name];
|
||||
|
||||
$this->method_checkers[$method_id] = new MethodChecker($stmt, $this);
|
||||
if (!isset($storage->methods[$method_name])) {
|
||||
FunctionLikeChecker::register($stmt, $this);
|
||||
}
|
||||
|
||||
if (!$stmt->isAbstract() && $class_context->self) {
|
||||
$implemented_method_id =
|
||||
@ -589,8 +610,6 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
|
||||
$trait_checker->check($class_context);
|
||||
|
||||
$this->method_checkers = array_merge($trait_checker->method_checkers, $this->method_checkers);
|
||||
|
||||
ClassLikeChecker::registerTraitUse($this->fq_class_name, $trait_name);
|
||||
|
||||
FileChecker::addFileInheritanceToClass(
|
||||
@ -858,11 +877,19 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
$namespace,
|
||||
array $aliased_classes
|
||||
) {
|
||||
if ($class_name->parts == ['self']) {
|
||||
return 'self';
|
||||
}
|
||||
|
||||
if ($class_name instanceof PhpParser\Node\Name\FullyQualified) {
|
||||
return implode('\\', $class_name->parts);
|
||||
}
|
||||
|
||||
return self::getFQCLNFromString(implode('\\', $class_name->parts), $namespace, $aliased_classes);
|
||||
return self::getFQCLNFromString(
|
||||
implode('\\', $class_name->parts),
|
||||
$namespace,
|
||||
$aliased_classes
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -236,7 +236,7 @@ class FileChecker extends SourceChecker implements StatementsSource
|
||||
|
||||
// hoist functions to the top
|
||||
foreach ($function_stmts as $stmt) {
|
||||
$function_checkers[$stmt->name] = new FunctionChecker($stmt, $this, $this->file_path);
|
||||
$function_checkers[$stmt->name] = new FunctionChecker($stmt, $this);
|
||||
$function_id = $function_checkers[$stmt->name]->getMethodId();
|
||||
$this->function_checkers[$function_id] = $function_checkers[$stmt->name];
|
||||
}
|
||||
|
@ -35,9 +35,8 @@ class FunctionChecker extends FunctionLikeChecker
|
||||
/**
|
||||
* @param mixed $function
|
||||
* @param StatementsSource $source
|
||||
* @param string $base_file_path
|
||||
*/
|
||||
public function __construct($function, StatementsSource $source, $base_file_path)
|
||||
public function __construct($function, StatementsSource $source)
|
||||
{
|
||||
if (!$function instanceof PhpParser\Node\Stmt\Function_) {
|
||||
throw new \InvalidArgumentException('Bad');
|
||||
@ -45,7 +44,7 @@ class FunctionChecker extends FunctionLikeChecker
|
||||
|
||||
parent::__construct($function, $source);
|
||||
|
||||
$this->registerFunction($function, $base_file_path);
|
||||
self::register($function, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -117,7 +116,8 @@ class FunctionChecker extends FunctionLikeChecker
|
||||
|
||||
/** @var \ReflectionParameter $param */
|
||||
foreach ($reflection_params as $param) {
|
||||
$storage->params[] = self::getReflectionParamArray($param);
|
||||
$param_obj = self::getReflectionParamData($param);
|
||||
$storage->params[] = $param_obj;
|
||||
}
|
||||
|
||||
$storage->cased_name = $reflection_function->getName();
|
||||
@ -180,186 +180,6 @@ class FunctionChecker extends FunctionLikeChecker
|
||||
return $file_storage->functions[$function_id]->cased_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PhpParser\Node\Stmt\Function_ $function
|
||||
* @param string $file_path
|
||||
* @return null|false
|
||||
*/
|
||||
protected function registerFunction(PhpParser\Node\Stmt\Function_ $function, $file_path)
|
||||
{
|
||||
$cased_function_id = ($this->namespace ? $this->namespace . '\\' : '') . $function->name;
|
||||
$function_id = strtolower($cased_function_id);
|
||||
|
||||
$file_storage = FileChecker::$storage[$file_path];
|
||||
|
||||
if (isset($file_storage->functions[$function_id])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$storage = $file_storage->functions[$function_id] = new FunctionLikeStorage();
|
||||
|
||||
$storage->namespace = $this->namespace;
|
||||
$storage->cased_name = $cased_function_id;
|
||||
|
||||
$function_param_names = [];
|
||||
|
||||
/** @var PhpParser\Node\Param $param */
|
||||
foreach ($function->getParams() as $param) {
|
||||
$param_array = self::getTranslatedParam(
|
||||
$param,
|
||||
$this
|
||||
);
|
||||
|
||||
$storage->params[] = $param_array;
|
||||
|
||||
if (isset($function_param_names[$param->name])) {
|
||||
if (IssueBuffer::accepts(
|
||||
new DuplicateParam(
|
||||
'Duplicate param $' . $param->name . ' in docblock for ' . $this->function->name,
|
||||
new CodeLocation($this, $param, true)
|
||||
),
|
||||
$this->suppressed_issues
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$function_param_names[$param->name] = $param_array->type;
|
||||
}
|
||||
|
||||
$config = Config::getInstance();
|
||||
$return_type = null;
|
||||
|
||||
$return_type_location = null;
|
||||
|
||||
$docblock_info = null;
|
||||
|
||||
$this->suppressed_issues = [];
|
||||
|
||||
$doc_comment = $function->getDocComment();
|
||||
|
||||
if ($function->returnType) {
|
||||
$parser_return_type = $function->returnType;
|
||||
|
||||
$suffix = '';
|
||||
|
||||
if ($parser_return_type instanceof PhpParser\Node\NullableType) {
|
||||
$suffix = '|null';
|
||||
$parser_return_type = $parser_return_type->type;
|
||||
}
|
||||
|
||||
if (is_string($parser_return_type)) {
|
||||
$return_type_string = $parser_return_type . $suffix;
|
||||
} else {
|
||||
$fq_class_name = ClassLikeChecker::getFQCLNFromNameObject(
|
||||
$parser_return_type,
|
||||
$this->namespace,
|
||||
$this->getAliasedClasses()
|
||||
);
|
||||
|
||||
ClassLikeChecker::checkFullyQualifiedClassLikeName(
|
||||
$fq_class_name,
|
||||
$this->getFileChecker(),
|
||||
new CodeLocation($this, $parser_return_type),
|
||||
$this->suppressed_issues
|
||||
);
|
||||
|
||||
$return_type_string = $fq_class_name . $suffix;
|
||||
}
|
||||
|
||||
$return_type = Type::parseString($return_type_string);
|
||||
|
||||
$return_type_location = new CodeLocation($this->getSource(), $function, false, self::RETURN_TYPE_REGEX);
|
||||
}
|
||||
|
||||
$storage->return_type = $return_type;
|
||||
$storage->return_type_location = $return_type_location;
|
||||
|
||||
if ($doc_comment) {
|
||||
try {
|
||||
$docblock_info = CommentChecker::extractDocblockInfo(
|
||||
(string)$doc_comment,
|
||||
$doc_comment->getLine()
|
||||
);
|
||||
} catch (DocblockParseException $e) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidDocblock(
|
||||
$e->getMessage() . ' in docblock for ' . $this->function->name,
|
||||
new CodeLocation($this, $function, true)
|
||||
)
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
|
||||
if ($docblock_info) {
|
||||
if ($docblock_info->deprecated) {
|
||||
$storage->deprecated = true;
|
||||
}
|
||||
|
||||
if ($docblock_info->variadic) {
|
||||
$storage->variadic = true;
|
||||
}
|
||||
|
||||
$this->suppressed_issues = $docblock_info->suppress;
|
||||
|
||||
if ($config->use_docblock_types) {
|
||||
if ($docblock_info->return_type) {
|
||||
$docblock_return_type =
|
||||
Type::parseString(
|
||||
self::fixUpLocalType(
|
||||
(string)$docblock_info->return_type,
|
||||
null,
|
||||
$this->namespace,
|
||||
$this->getAliasedClasses()
|
||||
)
|
||||
);
|
||||
|
||||
if (!$return_type_location) {
|
||||
$return_type_location = new CodeLocation($this->getSource(), $function, true);
|
||||
}
|
||||
|
||||
if ($return_type &&
|
||||
!TypeChecker::isContainedBy(
|
||||
$docblock_return_type,
|
||||
$return_type,
|
||||
$this->getFileChecker()
|
||||
)
|
||||
) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidDocblock(
|
||||
'Docblock return type does not match function return type for ' .
|
||||
$this->getMethodId(),
|
||||
new CodeLocation($this, $function, true)
|
||||
)
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$return_type = $docblock_return_type;
|
||||
}
|
||||
|
||||
$return_type_location->setCommentLine($docblock_info->return_type_line_number);
|
||||
|
||||
$storage->return_type = $return_type;
|
||||
$storage->return_type_location = $return_type_location;
|
||||
}
|
||||
|
||||
if ($docblock_info->params) {
|
||||
$this->improveParamsFromDocblock(
|
||||
$docblock_info->params,
|
||||
$function_param_names,
|
||||
$storage->params,
|
||||
new CodeLocation($this, $function, false)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $function_id
|
||||
* @return array|null
|
||||
|
@ -8,6 +8,7 @@ use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Checker\Statements\ExpressionChecker;
|
||||
use Psalm\Checker\TypeChecker;
|
||||
use Psalm\Config;
|
||||
use Psalm\Context;
|
||||
use Psalm\EffectsAnalyser;
|
||||
use Psalm\Exception\DocblockParseException;
|
||||
@ -22,6 +23,8 @@ use Psalm\Issue\MissingReturnType;
|
||||
use Psalm\Issue\MixedInferredReturnType;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\StatementsSource;
|
||||
use Psalm\Storage\FunctionLikeStorage;
|
||||
use Psalm\Storage\MethodStorage;
|
||||
use Psalm\Type;
|
||||
|
||||
abstract class FunctionLikeChecker extends SourceChecker implements StatementsSource
|
||||
@ -113,16 +116,14 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
*/
|
||||
public function check(Context $context, Context $global_context = null)
|
||||
{
|
||||
/** @var array<PhpParser\Node> */
|
||||
$function_stmts = $this->function->getStmts() ?: [];
|
||||
|
||||
$statements_checker = new StatementsChecker($this);
|
||||
|
||||
$hash = null;
|
||||
|
||||
$closure_return_type = null;
|
||||
$closure_return_type_location = null;
|
||||
$cased_method_id = null;
|
||||
|
||||
if ($this instanceof MethodChecker) {
|
||||
if ($this->function instanceof ClassMethod) {
|
||||
if (ClassLikeChecker::getThisClass()) {
|
||||
$hash = $this->getMethodId() . json_encode([
|
||||
$context->vars_in_scope,
|
||||
@ -142,14 +143,14 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
$context->vars_in_scope['$this'] = new Type\Union([new Type\Atomic($context->self)]);
|
||||
}
|
||||
|
||||
$function_params = MethodChecker::getMethodParams((string)$this->getMethodId());
|
||||
|
||||
if ($function_params === null) {
|
||||
throw new \InvalidArgumentException('Cannot get params for own method');
|
||||
}
|
||||
|
||||
$method_id = (string)$this->getMethodId($context->self);
|
||||
|
||||
$fq_class_name = (string)$context->self;
|
||||
|
||||
$storage = MethodChecker::getStorage(MethodChecker::getDeclaringMethodId($method_id));
|
||||
|
||||
$cased_method_id = $fq_class_name . '::' . $storage->cased_name;
|
||||
|
||||
$implemented_method_ids = MethodChecker::getOverriddenMethodIds($method_id);
|
||||
|
||||
if ($implemented_method_ids) {
|
||||
@ -171,8 +172,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
}
|
||||
|
||||
foreach ($implemented_params as $i => $implemented_param) {
|
||||
if (!isset($function_params[$i])) {
|
||||
$cased_method_id = MethodChecker::getCasedMethodId((string)$this->getMethodId());
|
||||
if (!isset($storage->params[$i])) {
|
||||
$parent_method_id = MethodChecker::getCasedMethodId($implemented_method_id);
|
||||
|
||||
if (IssueBuffer::accepts(
|
||||
@ -189,7 +189,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
break;
|
||||
}
|
||||
|
||||
if ((string)$function_params[$i]->signature_type !==
|
||||
if ((string)$storage->params[$i]->signature_type !==
|
||||
(string)$implemented_param->signature_type
|
||||
) {
|
||||
$cased_method_id = MethodChecker::getCasedMethodId((string)$this->getMethodId());
|
||||
@ -198,7 +198,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
if (IssueBuffer::accepts(
|
||||
new MethodSignatureMismatch(
|
||||
'Argument ' . ($i + 1) . ' of ' . $cased_method_id .' has wrong type \'' .
|
||||
$function_params[$i]->signature_type . '\', expecting \'' .
|
||||
$storage->params[$i]->signature_type . '\', expecting \'' .
|
||||
$implemented_param->signature_type . '\' as defined by ' .
|
||||
$parent_method_id,
|
||||
new CodeLocation($this, $this->function)
|
||||
@ -213,163 +213,32 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($this instanceof FunctionChecker) {
|
||||
$function_params = FunctionChecker::getParams(strtolower((string)$this->getMethodId()), $this->file_path);
|
||||
} elseif ($this->function instanceof Function_) {
|
||||
$storage = FileChecker::$storage[$this->file_path]->functions[(string)$this->getMethodId()];
|
||||
|
||||
$cased_method_id = $this->function->name;
|
||||
} else { // Closure
|
||||
$function_params = [];
|
||||
$function_param_names = [];
|
||||
|
||||
foreach ($this->function->getParams() as $param) {
|
||||
$param_array = self::getTranslatedParam(
|
||||
$param,
|
||||
$this
|
||||
);
|
||||
|
||||
$function_params[] = $param_array;
|
||||
|
||||
if (isset($function_param_names[$param->name])) {
|
||||
if (IssueBuffer::accepts(
|
||||
new DuplicateParam(
|
||||
'Duplicate param $' . $param->name . ' in closure docblock',
|
||||
new CodeLocation($this, $param, true)
|
||||
),
|
||||
$this->suppressed_issues
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$function_param_names[$param->name] = $param_array->type;
|
||||
}
|
||||
|
||||
$doc_comment = $this->function->getDocComment();
|
||||
|
||||
if ($this->function->returnType) {
|
||||
$parser_return_type = $this->function->returnType;
|
||||
|
||||
$suffix = '';
|
||||
|
||||
if ($parser_return_type instanceof PhpParser\Node\NullableType) {
|
||||
$suffix = '|null';
|
||||
$parser_return_type = $parser_return_type->type;
|
||||
}
|
||||
|
||||
if (is_string($parser_return_type)) {
|
||||
$return_type_string = $parser_return_type . $suffix;
|
||||
} else {
|
||||
$fq_class_name = ClassLikeChecker::getFQCLNFromNameObject(
|
||||
$parser_return_type,
|
||||
$this->namespace,
|
||||
$this->getAliasedClasses()
|
||||
);
|
||||
|
||||
ClassLikeChecker::checkFullyQualifiedClassLikeName(
|
||||
$fq_class_name,
|
||||
$this->getFileChecker(),
|
||||
new CodeLocation($this, $parser_return_type),
|
||||
$this->suppressed_issues
|
||||
);
|
||||
|
||||
$return_type_string = $fq_class_name . $suffix;
|
||||
}
|
||||
|
||||
$closure_return_type = Type::parseString($return_type_string);
|
||||
|
||||
$closure_return_type_location = new CodeLocation(
|
||||
$this->getSource(),
|
||||
$this->function,
|
||||
false,
|
||||
self::RETURN_TYPE_REGEX
|
||||
);
|
||||
}
|
||||
|
||||
if ($doc_comment) {
|
||||
try {
|
||||
$docblock_info = CommentChecker::extractDocblockInfo(
|
||||
(string)$doc_comment,
|
||||
$doc_comment->getLine()
|
||||
);
|
||||
} catch (DocblockParseException $e) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidDocblock(
|
||||
'Invalid type passed in docblock for ' . $this->getMethodId(),
|
||||
new CodeLocation($this, $this->function, true)
|
||||
)
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($docblock_info) {
|
||||
$this->suppressed_issues = $docblock_info->suppress;
|
||||
|
||||
$config = \Psalm\Config::getInstance();
|
||||
|
||||
if ($config->use_docblock_types) {
|
||||
if ($docblock_info->return_type) {
|
||||
$closure_docblock_return_type =
|
||||
Type::parseString(
|
||||
self::fixUpLocalType(
|
||||
(string)$docblock_info->return_type,
|
||||
null,
|
||||
$this->namespace,
|
||||
$this->getAliasedClasses()
|
||||
)
|
||||
);
|
||||
|
||||
if (!$closure_return_type_location) {
|
||||
$closure_return_type_location = new CodeLocation(
|
||||
$this->getSource(),
|
||||
$this->function,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
if ($closure_return_type &&
|
||||
!TypeChecker::isContainedBy(
|
||||
$closure_return_type,
|
||||
$closure_docblock_return_type,
|
||||
$statements_checker->getFileChecker()
|
||||
)
|
||||
) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidDocblock(
|
||||
'Docblock return type does not match closure return type ' . $this->getMethodId(),
|
||||
new CodeLocation($this, $this->function, true)
|
||||
)
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$closure_return_type = $closure_docblock_return_type;
|
||||
}
|
||||
|
||||
$closure_return_type_location->setCommentLine($docblock_info->return_type_line_number);
|
||||
}
|
||||
|
||||
if ($docblock_info->params) {
|
||||
$this->improveParamsFromDocblock(
|
||||
$docblock_info->params,
|
||||
$function_param_names,
|
||||
$function_params,
|
||||
new CodeLocation($this, $this->function, false)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$storage = self::register($this->function, $this->getSource());
|
||||
|
||||
/** @var PhpParser\Node\Expr\Closure $this->function */
|
||||
$this->function->inferredType = new Type\Union([
|
||||
new Type\Fn(
|
||||
'Closure',
|
||||
$function_params,
|
||||
$closure_return_type ?: Type::getMixed()
|
||||
$storage->params,
|
||||
$storage->return_type ?: Type::getMixed()
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($function_params as $offset => $function_param) {
|
||||
$this->suppressed_issues = $storage->suppressed_issues;
|
||||
|
||||
if ($storage instanceof MethodStorage && $storage->is_static) {
|
||||
$this->is_static = true;
|
||||
}
|
||||
|
||||
$statements_checker = new StatementsChecker($this);
|
||||
|
||||
foreach ($storage->params as $offset => $function_param) {
|
||||
$param_type = ExpressionChecker::fleshOutTypes(
|
||||
clone $function_param->type,
|
||||
[],
|
||||
@ -394,15 +263,6 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
)
|
||||
) {
|
||||
$method_id = $this->getMethodId();
|
||||
$cased_method_id = $method_id;
|
||||
|
||||
if ($cased_method_id) {
|
||||
if ($this instanceof MethodChecker) {
|
||||
$cased_method_id = MethodChecker::getCasedMethodId($cased_method_id);
|
||||
} elseif ($this->function instanceof PhpParser\Node\Stmt\Function_) {
|
||||
$cased_method_id = $this->function->name;
|
||||
}
|
||||
}
|
||||
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidParamDefault(
|
||||
@ -446,12 +306,12 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
|
||||
$this->checkReturnTypes(
|
||||
false,
|
||||
$closure_return_type,
|
||||
$storage->return_type,
|
||||
$this->fq_class_name,
|
||||
$closure_return_type_location
|
||||
$storage->return_type_location
|
||||
);
|
||||
|
||||
if (!$closure_return_type || $closure_return_type->isMixed()) {
|
||||
if (!$storage->return_type || $storage->return_type->isMixed()) {
|
||||
$closure_yield_types = [];
|
||||
$closure_return_types = EffectsAnalyser::getReturnTypes(
|
||||
$this->function->stmts,
|
||||
@ -504,6 +364,225 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Closure|Function_|ClassMethod $function
|
||||
* @param StatementsSource $source
|
||||
* @return FunctionLikeStorage
|
||||
*/
|
||||
public static function register(
|
||||
$function,
|
||||
StatementsSource $source
|
||||
) {
|
||||
$namespace = $source->getNamespace();
|
||||
|
||||
if ($function instanceof PhpParser\Node\Stmt\Function_) {
|
||||
$cased_function_id = ($namespace ? $namespace . '\\' : '') . $function->name;
|
||||
$function_id = strtolower($cased_function_id);
|
||||
|
||||
$file_storage = FileChecker::$storage[$source->getFilePath()];
|
||||
|
||||
if (isset($file_storage->functions[$function_id])) {
|
||||
throw new \InvalidArgumentException('Cannot re-register ' . $function_id);
|
||||
}
|
||||
|
||||
$storage = $file_storage->functions[$function_id] = new FunctionLikeStorage();
|
||||
} elseif ($function instanceof PhpParser\Node\Stmt\ClassMethod) {
|
||||
$fq_class_name = $source->getFQCLN();
|
||||
|
||||
$function_id = $fq_class_name . '::' . strtolower($function->name);
|
||||
$cased_function_id = $fq_class_name . '::' . $function->name;
|
||||
|
||||
if (!isset(ClassLikeChecker::$storage[$fq_class_name])) {
|
||||
throw new \UnexpectedValueException('$class_storage cannot be empty for ' . $function_id);
|
||||
}
|
||||
|
||||
$class_storage = ClassLikeChecker::$storage[$fq_class_name];
|
||||
|
||||
if (isset($class_storage->methods[strtolower($function->name)])) {
|
||||
throw new \InvalidArgumentException('Cannot re-register ' . $function_id);
|
||||
}
|
||||
|
||||
$storage = $class_storage->methods[strtolower($function->name)] = new MethodStorage();
|
||||
|
||||
$class_name_parts = explode('\\', $fq_class_name);
|
||||
$class_name = array_pop($class_name_parts);
|
||||
|
||||
if (strtolower((string)$function->name) === strtolower($class_name)) {
|
||||
MethodChecker::setDeclaringMethodId($fq_class_name . '::__construct', $function_id);
|
||||
MethodChecker::setAppearingMethodId($fq_class_name . '::__construct', $function_id);
|
||||
}
|
||||
|
||||
$class_storage->declaring_method_ids[strtolower($function->name)] = $function_id;
|
||||
$class_storage->appearing_method_ids[strtolower($function->name)] = $function_id;
|
||||
|
||||
if (!isset($class_storage->overridden_method_ids[strtolower($function->name)])) {
|
||||
$class_storage->overridden_method_ids[strtolower($function->name)] = [];
|
||||
}
|
||||
|
||||
/** @var bool */
|
||||
$storage->is_static = $function->isStatic();
|
||||
|
||||
if ($function->isPrivate()) {
|
||||
$storage->visibility = ClassLikeChecker::VISIBILITY_PRIVATE;
|
||||
} elseif ($function->isProtected()) {
|
||||
$storage->visibility = ClassLikeChecker::VISIBILITY_PROTECTED;
|
||||
} else {
|
||||
$storage->visibility = ClassLikeChecker::VISIBILITY_PUBLIC;
|
||||
}
|
||||
} else {
|
||||
$function_id = $cased_function_id = 'closure';
|
||||
|
||||
$storage = new FunctionLikeStorage();
|
||||
}
|
||||
|
||||
if ($function instanceof ClassMethod || $function instanceof Function_) {
|
||||
$storage->cased_name = $function->name;
|
||||
}
|
||||
|
||||
$storage->file_name = $source->getFileName();
|
||||
$storage->namespace = $source->getNamespace();
|
||||
|
||||
/** @var PhpParser\Node\Param $param */
|
||||
foreach ($function->getParams() as $param) {
|
||||
$param_array = self::getTranslatedParam($param, $source);
|
||||
|
||||
if (isset($storage->param_types[$param_array->name])) {
|
||||
if (IssueBuffer::accepts(
|
||||
new DuplicateParam(
|
||||
'Duplicate param $' . $param->name . ' in docblock for ' . $cased_function_id,
|
||||
new CodeLocation($source, $param, true)
|
||||
),
|
||||
$source->getSuppressedIssues()
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
|
||||
$storage->param_types[$param_array->name] = $param_array->type;
|
||||
$storage->params[] = $param_array;
|
||||
}
|
||||
|
||||
$config = Config::getInstance();
|
||||
|
||||
$docblock_info = null;
|
||||
$doc_comment = $function->getDocComment();
|
||||
|
||||
if ($parser_return_type = $function->getReturnType()) {
|
||||
$suffix = '';
|
||||
|
||||
if ($parser_return_type instanceof PhpParser\Node\NullableType) {
|
||||
$suffix = '|null';
|
||||
$parser_return_type = $parser_return_type->type;
|
||||
}
|
||||
|
||||
if (is_string($parser_return_type)) {
|
||||
$return_type_string = $parser_return_type . $suffix;
|
||||
} else {
|
||||
$return_type_fq_class_name = ClassLikeChecker::getFQCLNFromNameObject(
|
||||
$parser_return_type,
|
||||
$namespace,
|
||||
$source->getAliasedClasses()
|
||||
);
|
||||
|
||||
$return_type_string = $return_type_fq_class_name . $suffix;
|
||||
}
|
||||
|
||||
$storage->return_type = Type::parseString($return_type_string);
|
||||
$storage->return_type_location = new CodeLocation(
|
||||
$source,
|
||||
$function,
|
||||
false,
|
||||
self::RETURN_TYPE_REGEX
|
||||
);
|
||||
}
|
||||
|
||||
if (!$doc_comment) {
|
||||
return $storage;
|
||||
}
|
||||
|
||||
|
||||
$docblock_info = null;
|
||||
|
||||
try {
|
||||
$docblock_info = CommentChecker::extractDocblockInfo((string)$doc_comment, $doc_comment->getLine());
|
||||
} catch (DocblockParseException $e) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidDocblock(
|
||||
$e->getMessage() . ' in docblock for ' . $cased_function_id,
|
||||
new CodeLocation($source, $function, true)
|
||||
)
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
|
||||
if (!$docblock_info) {
|
||||
return $storage;
|
||||
}
|
||||
|
||||
if ($docblock_info->deprecated) {
|
||||
$storage->deprecated = true;
|
||||
}
|
||||
|
||||
if ($docblock_info->variadic) {
|
||||
$storage->variadic = true;
|
||||
}
|
||||
|
||||
$storage->suppressed_issues = $docblock_info->suppress;
|
||||
|
||||
if (!$config->use_docblock_types) {
|
||||
return $storage;
|
||||
}
|
||||
|
||||
if ($docblock_info->return_type) {
|
||||
$docblock_return_type = Type::parseString(
|
||||
self::fixUpLocalType(
|
||||
(string)$docblock_info->return_type,
|
||||
$source->getFQCLN(),
|
||||
$source->getNamespace(),
|
||||
$source->getAliasedClasses()
|
||||
)
|
||||
);
|
||||
|
||||
if (!$storage->return_type_location) {
|
||||
$storage->return_type_location = new CodeLocation($source, $function, true);
|
||||
}
|
||||
|
||||
if ($storage->return_type &&
|
||||
!TypeChecker::hasIdenticalTypes(
|
||||
$storage->return_type,
|
||||
$docblock_return_type,
|
||||
$source->getFileChecker()
|
||||
)
|
||||
) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidDocblock(
|
||||
'Docblock return type does not match method return type for ' . $cased_function_id,
|
||||
new CodeLocation($source, $function, true)
|
||||
)
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
} else {
|
||||
$storage->return_type = $docblock_return_type;
|
||||
}
|
||||
|
||||
$storage->return_type_location->setCommentLine($docblock_info->return_type_line_number);
|
||||
}
|
||||
|
||||
if ($docblock_info->params) {
|
||||
self::improveParamsFromDocblock(
|
||||
$storage,
|
||||
$docblock_info->params,
|
||||
$source,
|
||||
$source->getFQCLN(),
|
||||
new CodeLocation($source, $function, true)
|
||||
);
|
||||
}
|
||||
|
||||
return $storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds return types for the given function
|
||||
*
|
||||
@ -541,10 +620,6 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
if ($this->function instanceof ClassMethod) {
|
||||
$function_name = (string)$this->function->name;
|
||||
|
||||
if (strtolower($function_name) === strtolower((string)$this->class_name)) {
|
||||
$function_name = '__construct';
|
||||
}
|
||||
|
||||
return ($context_self ?: $this->fq_class_name) . '::' . strtolower($function_name);
|
||||
}
|
||||
|
||||
@ -840,32 +915,31 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
|
||||
/**
|
||||
* @param array<array{type:string,name:string,line_number:int}> $docblock_params
|
||||
* @param array<string,Type\Union> $function_param_names
|
||||
* @param array<\Psalm\FunctionLikeParameter> &$function_signature
|
||||
* @param CodeLocation $code_location
|
||||
* @param FunctionLikeStorage $storage
|
||||
* @param StatementsSource $source
|
||||
* @param string $fq_class_name
|
||||
* @param CodeLocation $code_location
|
||||
* @return false|null
|
||||
*/
|
||||
protected function improveParamsFromDocblock(
|
||||
protected static function improveParamsFromDocblock(
|
||||
FunctionLikeStorage $storage,
|
||||
array $docblock_params,
|
||||
array $function_param_names,
|
||||
array &$function_signature,
|
||||
StatementsSource $source,
|
||||
$fq_class_name,
|
||||
CodeLocation $code_location
|
||||
) {
|
||||
$docblock_param_vars = [];
|
||||
|
||||
$method_id = $cased_method_id = $this->getMethodId();
|
||||
$base = $fq_class_name ? $fq_class_name . '::' : '';
|
||||
|
||||
if ($method_id) {
|
||||
$cased_method_id = $this instanceof MethodChecker
|
||||
? MethodChecker::getCasedMethodId($method_id)
|
||||
: FunctionChecker::getCasedFunctionId($method_id, $this->file_path);
|
||||
}
|
||||
$method_id = $base . strtolower($storage->cased_name);
|
||||
$cased_method_id = $base . $storage->cased_name;
|
||||
|
||||
foreach ($docblock_params as $docblock_param) {
|
||||
$param_name = $docblock_param['name'];
|
||||
$line_number = $docblock_param['line_number'];
|
||||
|
||||
if (!array_key_exists($param_name, $function_param_names)) {
|
||||
if (!isset($storage->param_types[$param_name])) {
|
||||
$code_location->setCommentLine($line_number);
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidDocblock(
|
||||
@ -886,23 +960,23 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
self::fixUpLocalType(
|
||||
$docblock_param['type'],
|
||||
null,
|
||||
$this->namespace,
|
||||
$this->getAliasedClasses()
|
||||
$source->getNamespace(),
|
||||
$source->getAliasedClasses()
|
||||
)
|
||||
);
|
||||
|
||||
if ($function_param_names[$param_name] && !$function_param_names[$param_name]->isMixed()) {
|
||||
if (!$storage->param_types[$param_name]->isMixed()) {
|
||||
if (!TypeChecker::isContainedBy(
|
||||
$new_param_type,
|
||||
$function_param_names[$param_name],
|
||||
$this->getFileChecker()
|
||||
$storage->param_types[$param_name],
|
||||
$source->getFileChecker()
|
||||
)
|
||||
) {
|
||||
$code_location->setCommentLine($line_number);
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidDocblock(
|
||||
'Parameter $' . $param_name .' has wrong type \'' . $new_param_type . '\', should be \'' .
|
||||
$function_param_names[$param_name] . '\'',
|
||||
$storage->param_types[$param_name] . '\'',
|
||||
$code_location
|
||||
)
|
||||
)) {
|
||||
@ -913,7 +987,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($function_signature as &$function_signature_param) {
|
||||
foreach ($storage->params as $function_signature_param) {
|
||||
if ($function_signature_param->name === $param_name) {
|
||||
$existing_param_type_nullable = $function_signature_param->is_nullable;
|
||||
|
||||
@ -928,8 +1002,10 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($function_signature as &$function_signature_param) {
|
||||
if (!isset($docblock_param_vars[$function_signature_param->name]) && $function_signature_param->code_location) {
|
||||
foreach ($storage->params as $function_signature_param) {
|
||||
if (!isset($docblock_param_vars[$function_signature_param->name]) &&
|
||||
$function_signature_param->code_location
|
||||
) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidDocblock(
|
||||
'Parameter $' . $function_signature_param->name .' does not appear in the docbock for ' .
|
||||
@ -1032,7 +1108,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
* @param \ReflectionParameter $param
|
||||
* @return FunctionLikeParameter
|
||||
*/
|
||||
protected static function getReflectionParamArray(\ReflectionParameter $param)
|
||||
protected static function getReflectionParamData(\ReflectionParameter $param)
|
||||
{
|
||||
$param_type_string = null;
|
||||
|
||||
@ -1204,8 +1280,6 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
array $args,
|
||||
FileChecker $file_checker
|
||||
) {
|
||||
$function_params = null;
|
||||
|
||||
if (count($function_param_options) === 1) {
|
||||
return $function_param_options[0];
|
||||
}
|
||||
|
@ -32,9 +32,6 @@ class MethodChecker extends FunctionLikeChecker
|
||||
}
|
||||
|
||||
parent::__construct($function, $source);
|
||||
|
||||
$this->registerMethod($function);
|
||||
$this->is_static = $function->isStatic();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -148,6 +145,13 @@ class MethodChecker extends FunctionLikeChecker
|
||||
$method_name = strtolower($method->getName());
|
||||
|
||||
$class_storage = ClassLikeChecker::$storage[$method->class];
|
||||
|
||||
if (isset($class_storage->methods[strtolower($method_name)])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$method_id = $method->class . '::' . $method_name;
|
||||
|
||||
$storage = $class_storage->methods[strtolower($method_name)] = new MethodStorage();
|
||||
|
||||
$storage->cased_name = $method->name;
|
||||
@ -157,15 +161,9 @@ class MethodChecker extends FunctionLikeChecker
|
||||
self::setAppearingMethodId($method->class . '::__construct', $method->class . '::' . $method_name);
|
||||
}
|
||||
|
||||
if ($storage->reflected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var \ReflectionClass */
|
||||
$declaring_class = $method->getDeclaringClass();
|
||||
|
||||
$storage->reflected = true;
|
||||
|
||||
$storage->is_static = $method->isStatic();
|
||||
$storage->file_name = $method->getFileName();
|
||||
$storage->namespace = $declaring_class->getNamespaceName();
|
||||
@ -188,19 +186,10 @@ class MethodChecker extends FunctionLikeChecker
|
||||
|
||||
/** @var \ReflectionParameter $param */
|
||||
foreach ($params as $param) {
|
||||
$param_array = self::getReflectionParamArray($param);
|
||||
$param_array = self::getReflectionParamData($param);
|
||||
$storage->params[] = $param_array;
|
||||
$method_param_names[$param->name] = true;
|
||||
$method_param_types[$param->name] = $param_array->type;
|
||||
$storage->param_types[$param->name] = $param_array->type;
|
||||
}
|
||||
|
||||
$return_types = null;
|
||||
|
||||
$config = Config::getInstance();
|
||||
|
||||
$return_type = null;
|
||||
|
||||
$storage->return_type = $return_type;
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -255,206 +244,6 @@ class MethodChecker extends FunctionLikeChecker
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PhpParser\Node\Stmt\ClassMethod $method
|
||||
* @return null|false
|
||||
* @psalm-suppress MixedAssignment
|
||||
*/
|
||||
protected function registerMethod(PhpParser\Node\Stmt\ClassMethod $method)
|
||||
{
|
||||
$method_id = $this->fq_class_name . '::' . strtolower($method->name);
|
||||
|
||||
$class_storage = ClassLikeChecker::$storage[$this->fq_class_name];
|
||||
$storage = $class_storage->methods[strtolower($method->name)] = new MethodStorage();
|
||||
|
||||
$cased_method_id = $storage->cased_name = $method->name;
|
||||
|
||||
if (strtolower((string)$method->name) === strtolower((string)$this->class_name)) {
|
||||
self::setDeclaringMethodId($this->fq_class_name . '::__construct', $method_id);
|
||||
self::setAppearingMethodId($this->fq_class_name . '::__construct', $method_id);
|
||||
}
|
||||
|
||||
if ($storage->reflected || $storage->registered) {
|
||||
$this->suppressed_issues = $storage->suppressed_issues;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$this->source instanceof TraitChecker) {
|
||||
$storage->registered = true;
|
||||
}
|
||||
|
||||
$class_storage->declaring_method_ids[strtolower($method->name)] = $method_id;
|
||||
$class_storage->appearing_method_ids[strtolower($method->name)] = $method_id;
|
||||
|
||||
if (!isset($class_storage->overridden_method_ids[strtolower($method->name)])) {
|
||||
$class_storage->overridden_method_ids[strtolower($method->name)] = [];
|
||||
}
|
||||
|
||||
$storage->is_static = $method->isStatic();
|
||||
|
||||
$storage->namespace = $this->namespace;
|
||||
$storage->file_name = $this->file_name;
|
||||
|
||||
if ($method->isPrivate()) {
|
||||
$storage->visibility = ClassLikeChecker::VISIBILITY_PRIVATE;
|
||||
} elseif ($method->isProtected()) {
|
||||
$storage->visibility = ClassLikeChecker::VISIBILITY_PROTECTED;
|
||||
} else {
|
||||
$storage->visibility = ClassLikeChecker::VISIBILITY_PUBLIC;
|
||||
}
|
||||
|
||||
$method_param_names = [];
|
||||
|
||||
foreach ($method->getParams() as $param) {
|
||||
$param_array = $this->getTranslatedParam(
|
||||
$param,
|
||||
$this
|
||||
);
|
||||
|
||||
$storage->params[] = $param_array;
|
||||
|
||||
if (isset($function_param_names[$param->name])) {
|
||||
if (IssueBuffer::accepts(
|
||||
new DuplicateParam(
|
||||
'Duplicate param $' . $param->name . ' in docblock for ' . $cased_method_id,
|
||||
new CodeLocation($this, $param, true)
|
||||
),
|
||||
$this->suppressed_issues
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$method_param_names[$param->name] = $param_array->type;
|
||||
}
|
||||
|
||||
$config = Config::getInstance();
|
||||
$return_type = null;
|
||||
$return_type_location = null;
|
||||
|
||||
$doc_comment = $method->getDocComment();
|
||||
|
||||
$storage->suppressed_issues = [];
|
||||
|
||||
if (isset($method->returnType)) {
|
||||
$parser_return_type = $method->returnType;
|
||||
|
||||
$suffix = '';
|
||||
|
||||
if ($parser_return_type instanceof PhpParser\Node\NullableType) {
|
||||
$suffix = '|null';
|
||||
$parser_return_type = $parser_return_type->type;
|
||||
}
|
||||
|
||||
if (is_string($parser_return_type)) {
|
||||
$return_type_string = $parser_return_type . $suffix;
|
||||
} else {
|
||||
$fq_class_name = ClassLikeChecker::getFQCLNFromNameObject(
|
||||
$parser_return_type,
|
||||
$this->namespace,
|
||||
$this->getAliasedClasses()
|
||||
);
|
||||
|
||||
if ($fq_class_name !== 'self') {
|
||||
ClassLikeChecker::checkFullyQualifiedClassLikeName(
|
||||
$fq_class_name,
|
||||
$this->getFileChecker(),
|
||||
new CodeLocation($this, $parser_return_type),
|
||||
$this->suppressed_issues
|
||||
);
|
||||
}
|
||||
|
||||
$return_type_string = $fq_class_name . $suffix;
|
||||
}
|
||||
|
||||
$return_type = Type::parseString($return_type_string);
|
||||
|
||||
$return_type_location = new CodeLocation($this->getSource(), $method, false, self::RETURN_TYPE_REGEX);
|
||||
}
|
||||
|
||||
if ($doc_comment) {
|
||||
$docblock_info = null;
|
||||
|
||||
try {
|
||||
$docblock_info = CommentChecker::extractDocblockInfo((string)$doc_comment, $doc_comment->getLine());
|
||||
} catch (DocblockParseException $e) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidDocblock(
|
||||
'Invalid type passed in docblock for ' . $cased_method_id,
|
||||
new CodeLocation($this, $method)
|
||||
)
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($docblock_info) {
|
||||
if ($docblock_info->deprecated) {
|
||||
$storage->deprecated = true;
|
||||
}
|
||||
|
||||
if ($docblock_info->variadic) {
|
||||
$storage->variadic = true;
|
||||
}
|
||||
|
||||
$this->suppressed_issues = $docblock_info->suppress;
|
||||
$storage->suppressed_issues = $this->suppressed_issues;
|
||||
|
||||
if ($config->use_docblock_types) {
|
||||
if ($docblock_info->return_type) {
|
||||
$docblock_return_type = Type::parseString(
|
||||
$this->fixUpLocalType(
|
||||
(string)$docblock_info->return_type,
|
||||
$this->fq_class_name,
|
||||
$this->namespace,
|
||||
$this->getAliasedClasses()
|
||||
)
|
||||
);
|
||||
|
||||
if (!$return_type_location) {
|
||||
$return_type_location = new CodeLocation($this->getSource(), $method, true);
|
||||
}
|
||||
|
||||
if ($return_type &&
|
||||
!TypeChecker::hasIdenticalTypes(
|
||||
$return_type,
|
||||
$docblock_return_type,
|
||||
$this->getFileChecker()
|
||||
)
|
||||
) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidDocblock(
|
||||
'Docblock return type does not match method return type for ' . $this->getMethodId(),
|
||||
new CodeLocation($this, $method, true)
|
||||
)
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$return_type = $docblock_return_type;
|
||||
}
|
||||
|
||||
$return_type_location->setCommentLine($docblock_info->return_type_line_number);
|
||||
}
|
||||
|
||||
if ($docblock_info->params) {
|
||||
$this->improveParamsFromDocblock(
|
||||
$docblock_info->params,
|
||||
$method_param_names,
|
||||
$storage->params,
|
||||
new CodeLocation($this, $method, true)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$storage->return_type_location = $return_type_location;
|
||||
$storage->return_type = $return_type;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $return_type
|
||||
* @param string $method_id
|
||||
@ -575,7 +364,7 @@ class MethodChecker extends FunctionLikeChecker
|
||||
|
||||
/**
|
||||
* @param string $method_id
|
||||
* @return MethodStorage|null
|
||||
* @return MethodStorage
|
||||
*/
|
||||
public static function getStorage($method_id)
|
||||
{
|
||||
@ -583,11 +372,11 @@ class MethodChecker extends FunctionLikeChecker
|
||||
|
||||
$class_storage = ClassLikeChecker::$storage[$fq_class_name];
|
||||
|
||||
if (isset($class_storage->methods[strtolower($method_name)])) {
|
||||
return $class_storage->methods[strtolower($method_name)];
|
||||
if (!isset($class_storage->methods[strtolower($method_name)])) {
|
||||
throw new \UnexpectedValueException('$storage should not be null for ' . $method_id);
|
||||
}
|
||||
|
||||
return null;
|
||||
return $class_storage->methods[strtolower($method_name)];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -601,10 +390,6 @@ class MethodChecker extends FunctionLikeChecker
|
||||
$method_id = (string) self::getDeclaringMethodId($method_id);
|
||||
$storage = self::getStorage($method_id);
|
||||
|
||||
if (!$storage) {
|
||||
throw new \UnexpectedValueException('$storage should not be null');
|
||||
}
|
||||
|
||||
if ($storage->deprecated) {
|
||||
if (IssueBuffer::accepts(
|
||||
new DeprecatedMethod(
|
||||
@ -751,6 +536,10 @@ class MethodChecker extends FunctionLikeChecker
|
||||
{
|
||||
list($fq_class_name, $method_name) = explode('::', $method_id);
|
||||
|
||||
if (!isset(ClassLikeChecker::$storage[$fq_class_name])) {
|
||||
throw new \UnexpectedValueException('$storage should not be null for ' . $fq_class_name);
|
||||
}
|
||||
|
||||
if (isset(ClassLikeChecker::$storage[$fq_class_name]->declaring_method_ids[$method_name])) {
|
||||
return ClassLikeChecker::$storage[$fq_class_name]->declaring_method_ids[$method_name];
|
||||
}
|
||||
@ -766,6 +555,10 @@ class MethodChecker extends FunctionLikeChecker
|
||||
{
|
||||
list($fq_class_name, $method_name) = explode('::', $method_id);
|
||||
|
||||
if (!isset(ClassLikeChecker::$storage[$fq_class_name])) {
|
||||
throw new \UnexpectedValueException('$storage should not be null for ' . $fq_class_name);
|
||||
}
|
||||
|
||||
if (isset(ClassLikeChecker::$storage[$fq_class_name]->appearing_method_ids[$method_name])) {
|
||||
return ClassLikeChecker::$storage[$fq_class_name]->appearing_method_ids[$method_name];
|
||||
}
|
||||
|
@ -287,6 +287,9 @@ abstract class SourceChecker implements StatementsSource
|
||||
return $this->suppressed_issues;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getNamespace()
|
||||
{
|
||||
return '';
|
||||
|
@ -111,7 +111,7 @@ class CallChecker
|
||||
if (isset($stmt->name->inferredType)) {
|
||||
foreach ($stmt->name->inferredType->types as $var_type_part) {
|
||||
if ($var_type_part instanceof Type\Fn) {
|
||||
$function_params = $var_type_part->parameters;
|
||||
$function_params = $var_type_part->params;
|
||||
|
||||
if ($var_type_part->return_type) {
|
||||
if (isset($stmt->inferredType)) {
|
||||
@ -1147,7 +1147,7 @@ class CallChecker
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count($closure_type->parameters) > $expected_closure_param_count) {
|
||||
if (count($closure_type->params) > $expected_closure_param_count) {
|
||||
if (IssueBuffer::accepts(
|
||||
new TooManyArguments(
|
||||
'Too many arguments in closure for ' . $method_id,
|
||||
@ -1157,7 +1157,7 @@ class CallChecker
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
} elseif (count($closure_type->parameters) < $expected_closure_param_count) {
|
||||
} elseif (count($closure_type->params) < $expected_closure_param_count) {
|
||||
if (IssueBuffer::accepts(
|
||||
new TooFewArguments(
|
||||
'You must supply a param in the closure for ' . $method_id,
|
||||
@ -1170,11 +1170,14 @@ class CallChecker
|
||||
}
|
||||
|
||||
|
||||
$closure_params = $closure_type->parameters;
|
||||
$closure_params = $closure_type->params;
|
||||
$closure_return_type = $closure_type->return_type;
|
||||
|
||||
foreach ($closure_params as $i => $closure_param) {
|
||||
$i = 0;
|
||||
|
||||
foreach ($closure_params as $param_name => $closure_param) {
|
||||
if (!$array_arg_types[$i]) {
|
||||
$i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -1184,6 +1187,7 @@ class CallChecker
|
||||
$input_type = $array_arg_type->type_params[1];
|
||||
|
||||
if ($input_type->isMixed()) {
|
||||
$i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -1234,6 +1238,8 @@ class CallChecker
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -171,7 +171,7 @@ class StatementsChecker
|
||||
// hoist functions to the top
|
||||
foreach ($stmts as $stmt) {
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\Function_) {
|
||||
$function_checker = new FunctionChecker($stmt, $this->source, $this->file_path);
|
||||
$function_checker = new FunctionChecker($stmt, $this->source);
|
||||
$function_checkers[$stmt->name] = $function_checker;
|
||||
}
|
||||
}
|
||||
|
@ -179,7 +179,7 @@ class IssueBuffer
|
||||
|
||||
if ($start_time) {
|
||||
echo('Checks took ' . ((float)microtime(true) - self::$start_time));
|
||||
echo(' and used ' . memory_get_peak_usage() . PHP_EOL);
|
||||
echo(' and used ' . number_format(memory_get_peak_usage() / (1024 * 1024), 3) . 'MB' . PHP_EOL);
|
||||
}
|
||||
|
||||
if (count(self::$emitted)) {
|
||||
|
@ -17,6 +17,11 @@ class FunctionLikeStorage
|
||||
*/
|
||||
public $params = [];
|
||||
|
||||
/**
|
||||
* @var array<string, Type\Union>
|
||||
*/
|
||||
public $param_types = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
|
@ -5,6 +5,11 @@ use Psalm\Type;
|
||||
|
||||
class MethodStorage extends FunctionLikeStorage
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $fq_class_name;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
|
@ -13,7 +13,7 @@ class Fn extends Atomic
|
||||
/**
|
||||
* @var array<int, FunctionLikeParameter>
|
||||
*/
|
||||
public $parameters = [];
|
||||
public $params = [];
|
||||
|
||||
/**
|
||||
* @var Union
|
||||
@ -24,12 +24,12 @@ class Fn extends Atomic
|
||||
* Constructs a new instance of a generic type
|
||||
*
|
||||
* @param string $value
|
||||
* @param array<int, FunctionLikeParameter> $parameters
|
||||
* @param array<int, FunctionLikeParameter> $params
|
||||
* @param Union $return_type
|
||||
*/
|
||||
public function __construct($value, array $parameters, Union $return_type)
|
||||
public function __construct($value, array $params, Union $return_type)
|
||||
{
|
||||
$this->parameters = $parameters;
|
||||
$this->params = $params;
|
||||
$this->return_type = $return_type;
|
||||
}
|
||||
}
|
||||
|
@ -181,9 +181,9 @@ class AnnotationTest extends PHPUnit_Framework_TestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<int, string>>
|
||||
* @return array<int, string>
|
||||
*/
|
||||
function foo2() : array {
|
||||
function foo3() : array {
|
||||
return ["hello"];
|
||||
}
|
||||
');
|
||||
|
@ -38,7 +38,7 @@ class IssueSuppressionTest extends PHPUnit_Framework_TestCase
|
||||
* @psalm-suppress MixedMethodCall
|
||||
* @psalm-suppress MissingReturnType
|
||||
*/
|
||||
public function a() {
|
||||
public function b() {
|
||||
B::fooFoo()->barBar()->bat()->baz()->bam()->bas()->bee()->bet()->bes()->bis();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user