mirror of
https://github.com/danog/psalm.git
synced 2025-01-21 21:31:13 +01:00
Add support for namespaced templates
This commit is contained in:
parent
4a82c0a09a
commit
8ffd45407c
@ -942,7 +942,13 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
|
||||
if ($comment && $config->use_docblock_types) {
|
||||
try {
|
||||
$type_in_comment = CommentChecker::getTypeFromComment((string) $comment, null, $this);
|
||||
$type_in_comment = CommentChecker::getTypeFromComment(
|
||||
(string)$comment,
|
||||
null,
|
||||
$this,
|
||||
null,
|
||||
$storage->template_types
|
||||
);
|
||||
} catch (DocblockParseException $e) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidDocblock(
|
||||
|
@ -19,6 +19,7 @@ class CommentChecker
|
||||
* @param Context|null $context
|
||||
* @param StatementsSource $source
|
||||
* @param string $var_id
|
||||
* @param array<string, string>|null $template_types
|
||||
* @return Type\Union|null
|
||||
* @throws DocblockParseException If there was a problem parsing the docblock.
|
||||
* @psalm-suppress MixedArrayAccess
|
||||
@ -27,7 +28,8 @@ class CommentChecker
|
||||
$comment,
|
||||
Context $context = null,
|
||||
StatementsSource $source,
|
||||
$var_id = null
|
||||
$var_id = null,
|
||||
array $template_types = null
|
||||
) {
|
||||
$type_in_comments_var_id = null;
|
||||
|
||||
@ -47,7 +49,7 @@ class CommentChecker
|
||||
$line_parts[0],
|
||||
$source->getFQCLN(),
|
||||
$source,
|
||||
[]
|
||||
$template_types
|
||||
);
|
||||
|
||||
// support PHPStorm-style docblocks like
|
||||
|
@ -309,8 +309,10 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
}
|
||||
|
||||
foreach ($storage->params as $offset => $function_param) {
|
||||
$param_type = clone $function_param->type;
|
||||
|
||||
$param_type = ExpressionChecker::fleshOutTypes(
|
||||
clone $function_param->type,
|
||||
$param_type,
|
||||
[],
|
||||
$context->self,
|
||||
$this->getMethodId()
|
||||
@ -347,10 +349,10 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
}
|
||||
|
||||
if ($template_types) {
|
||||
$substituded_type = clone $param_type;
|
||||
$substituted_type = clone $param_type;
|
||||
$generic_types = [];
|
||||
$substituded_type->replaceTemplateTypes($template_types, $generic_types, null);
|
||||
$substituded_type->check($this->source, $function_param->code_location, $this->suppressed_issues);
|
||||
$substituted_type->replaceTemplateTypes($template_types, $generic_types, null);
|
||||
$substituted_type->check($this->source, $function_param->code_location, $this->suppressed_issues);
|
||||
} else {
|
||||
$param_type->check($this->source, $function_param->code_location, $this->suppressed_issues);
|
||||
}
|
||||
@ -615,9 +617,6 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
|
||||
$config = Config::getInstance();
|
||||
|
||||
$docblock_info = null;
|
||||
$doc_comment = $function->getDocComment();
|
||||
|
||||
if ($parser_return_type = $function->getReturnType()) {
|
||||
$suffix = '';
|
||||
|
||||
@ -646,12 +645,13 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
);
|
||||
}
|
||||
|
||||
$docblock_info = null;
|
||||
$doc_comment = $function->getDocComment();
|
||||
|
||||
if (!$doc_comment) {
|
||||
return $storage;
|
||||
}
|
||||
|
||||
$docblock_info = null;
|
||||
|
||||
try {
|
||||
$docblock_info = CommentChecker::extractFunctionDocblockInfo(
|
||||
(string)$doc_comment,
|
||||
@ -680,49 +680,45 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
$storage->variadic = true;
|
||||
}
|
||||
|
||||
if ($docblock_info->template_types) {
|
||||
$storage->template_types = [];
|
||||
|
||||
foreach ($docblock_info->template_types as $template_type) {
|
||||
if (count($template_type) === 3) {
|
||||
$as_type_string = ClassLikeChecker::getFQCLNFromString($template_type[2], $source);
|
||||
$storage->template_types[$template_type[0]] = $as_type_string;
|
||||
} else {
|
||||
$storage->template_types[$template_type[0]] = 'mixed';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($docblock_info->template_typeofs) {
|
||||
$storage->template_typeof_params = [];
|
||||
|
||||
foreach ($docblock_info->template_typeofs as $template_typeof) {
|
||||
foreach ($storage->params as $i => $param) {
|
||||
if ($param->name === $template_typeof['param_name']) {
|
||||
$storage->template_typeof_params[$i] = $template_typeof['template_type'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$storage->suppressed_issues = $docblock_info->suppress;
|
||||
|
||||
if (!$config->use_docblock_types) {
|
||||
return $storage;
|
||||
}
|
||||
|
||||
$template_types = $class_storage && $class_storage->template_types ? $class_storage->template_types : null;
|
||||
|
||||
if ($docblock_info) {
|
||||
if ($docblock_info->template_types) {
|
||||
$storage->template_types = [];
|
||||
|
||||
foreach ($docblock_info->template_types as $template_type) {
|
||||
if (count($template_type) === 3) {
|
||||
$as_type_string = ClassLikeChecker::getFQCLNFromString($template_type[2], $source);
|
||||
$storage->template_types[$template_type[0]] = $as_type_string;
|
||||
} else {
|
||||
$storage->template_types[$template_type[0]] = 'mixed';
|
||||
}
|
||||
}
|
||||
|
||||
$template_types = array_merge($template_types ?: [], $storage->template_types);
|
||||
}
|
||||
|
||||
if ($docblock_info->template_typeofs) {
|
||||
$storage->template_typeof_params = [];
|
||||
|
||||
foreach ($docblock_info->template_typeofs as $template_typeof) {
|
||||
foreach ($storage->params as $i => $param) {
|
||||
if ($param->name === $template_typeof['param_name']) {
|
||||
$storage->template_typeof_params[$i] = $template_typeof['template_type'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($docblock_info->return_type) {
|
||||
$template_types = null;
|
||||
|
||||
if ($storage->template_types) {
|
||||
$template_types = $storage->template_types;
|
||||
}
|
||||
|
||||
if ($class_storage && $class_storage->template_types) {
|
||||
$template_types = array_merge($class_storage->template_types, $template_types ?: []);
|
||||
}
|
||||
|
||||
$storage->has_template_return_type =
|
||||
$template_types !== null &&
|
||||
count(array_intersect(Type::tokenize($docblock_info->return_type), array_keys($template_types))) > 0;
|
||||
@ -768,6 +764,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
self::improveParamsFromDocblock(
|
||||
$storage,
|
||||
$docblock_info->params,
|
||||
$template_types,
|
||||
$source,
|
||||
$source->getFQCLN(),
|
||||
new CodeLocation($source, $function, true)
|
||||
@ -1135,6 +1132,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
/**
|
||||
* @param array<int, array{type:string,name:string,line_number:int}> $docblock_params
|
||||
* @param FunctionLikeStorage $storage
|
||||
* @param array<string, string>|null $template_types
|
||||
* @param StatementsSource $source
|
||||
* @param string|null $fq_class_name
|
||||
* @param CodeLocation $code_location
|
||||
@ -1143,6 +1141,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
protected static function improveParamsFromDocblock(
|
||||
FunctionLikeStorage $storage,
|
||||
array $docblock_params,
|
||||
$template_types,
|
||||
StatementsSource $source,
|
||||
$fq_class_name,
|
||||
CodeLocation $code_location
|
||||
@ -1190,7 +1189,8 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
self::fixUpLocalType(
|
||||
$docblock_param['type'],
|
||||
null,
|
||||
$source
|
||||
$source,
|
||||
$template_types
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -283,8 +283,9 @@ class FetchChecker
|
||||
|
||||
$declaring_property_class = ClassLikeChecker::getDeclaringClassForProperty($property_id);
|
||||
|
||||
$property_storage =
|
||||
ClassLikeChecker::$storage[strtolower((string)$declaring_property_class)]->properties[$stmt->name];
|
||||
$declaring_class_storage = ClassLikeChecker::$storage[strtolower((string)$declaring_property_class)];
|
||||
|
||||
$property_storage = $declaring_class_storage->properties[$stmt->name];
|
||||
|
||||
$class_property_type = $property_storage->type;
|
||||
|
||||
@ -302,6 +303,38 @@ class FetchChecker
|
||||
$class_property_type = Type::getMixed();
|
||||
} else {
|
||||
$class_property_type = clone $class_property_type;
|
||||
|
||||
if ($lhs_type_part instanceof TGenericObject) {
|
||||
$class_storage = ClassLikeChecker::$storage[strtolower($lhs_type_part->value)];
|
||||
|
||||
if ($class_storage->template_types) {
|
||||
$class_template_params = [];
|
||||
|
||||
/** @var array<int, string> */
|
||||
$reversed_class_template_types = array_reverse(array_keys($class_storage->template_types));
|
||||
|
||||
$provided_type_param_count = count($lhs_type_part->type_params);
|
||||
|
||||
foreach ($reversed_class_template_types as $i => $type_name) {
|
||||
if (isset($lhs_type_part->type_params[$provided_type_param_count - 1 - $i])) {
|
||||
$class_template_params[$type_name] =
|
||||
(string)$lhs_type_part->type_params[$provided_type_param_count - 1 - $i];
|
||||
} else {
|
||||
$class_template_params[$type_name] = 'mixed';
|
||||
}
|
||||
}
|
||||
|
||||
$type_tokens = Type::tokenize((string)$class_property_type);
|
||||
|
||||
foreach ($type_tokens as &$type_token) {
|
||||
if (isset($class_template_params[$type_token])) {
|
||||
$type_token = $class_template_params[$type_token];
|
||||
}
|
||||
}
|
||||
|
||||
$class_property_type = Type::parseString(implode('', $type_tokens));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($stmt->inferredType)) {
|
||||
|
@ -144,6 +144,141 @@ class TemplateTest extends PHPUnit_Framework_TestCase
|
||||
$this->assertEquals('A', (string) $context->vars_in_scope['$afoo_bar']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testPhanTuple()
|
||||
{
|
||||
$stmts = self::$parser->parse('<?php
|
||||
namespace Phan\Library;
|
||||
|
||||
/**
|
||||
* An abstract tuple.
|
||||
*/
|
||||
abstract class Tuple
|
||||
{
|
||||
const ARITY = 0;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
* The arity of this tuple
|
||||
*/
|
||||
public function arity() : int
|
||||
{
|
||||
return (int)static::ARITY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* An array of all elements in this tuple.
|
||||
*/
|
||||
abstract public function toArray() : array;
|
||||
}
|
||||
|
||||
/**
|
||||
* A tuple of 1 element.
|
||||
*
|
||||
* @template T0
|
||||
* The type of element zero
|
||||
*/
|
||||
class Tuple1 extends Tuple
|
||||
{
|
||||
/** @var int */
|
||||
const ARITY = 1;
|
||||
|
||||
/** @var T0 */
|
||||
public $_0;
|
||||
|
||||
/**
|
||||
* @param T0 $_0
|
||||
* The 0th element
|
||||
*/
|
||||
public function __construct($_0) {
|
||||
$this->_0 = $_0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
* The arity of this tuple
|
||||
*/
|
||||
public function arity() : int
|
||||
{
|
||||
return (int)static::ARITY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* An array of all elements in this tuple.
|
||||
*/
|
||||
public function toArray() : array
|
||||
{
|
||||
return [
|
||||
$this->_0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A tuple of 2 elements.
|
||||
*
|
||||
* @template T0
|
||||
* The type of element zero
|
||||
*
|
||||
* @template T1
|
||||
* The type of element one
|
||||
*
|
||||
* @inherits Tuple1<T0>
|
||||
*/
|
||||
class Tuple2 extends Tuple1
|
||||
{
|
||||
/** @var int */
|
||||
const ARITY = 2;
|
||||
|
||||
/** @var T1 */
|
||||
public $_1;
|
||||
|
||||
/**
|
||||
* @param T0 $_0
|
||||
* The 0th element
|
||||
*
|
||||
* @param T1 $_1
|
||||
* The 1st element
|
||||
*/
|
||||
public function __construct($_0, $_1) {
|
||||
parent::__construct($_0);
|
||||
$this->_1 = $_1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* An array of all elements in this tuple.
|
||||
*/
|
||||
public function toArray() : array
|
||||
{
|
||||
return [
|
||||
$this->_0,
|
||||
$this->_1,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$a = new Tuple2("cool", 5);
|
||||
|
||||
/** @return void */
|
||||
function takes_int(int $i) {}
|
||||
|
||||
/** @return void */
|
||||
function takes_string(string $s) {}
|
||||
|
||||
takes_string($a->_0);
|
||||
takes_int($a->_1);
|
||||
');
|
||||
|
||||
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
|
||||
$context = new Context();
|
||||
$file_checker->visitAndAnalyzeMethods($context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
|
Loading…
x
Reference in New Issue
Block a user