1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00

Prohibit leaking of template params across class boundaries

This commit is contained in:
Brown 2019-01-10 12:13:49 -05:00
parent 1e20cbfa79
commit b8d822cd26
24 changed files with 162 additions and 90 deletions

View File

@ -405,7 +405,7 @@ abstract class ClassLikeAnalyzer extends SourceAnalyzer implements StatementsSou
}
/**
* @return array<string, Type\Union>|null
* @return array<string, array{Type\Union, ?string}>|null
*/
public function getTemplateTypeMap()
{

View File

@ -23,7 +23,7 @@ class CommentAnalyzer
/**
* @param string $comment
* @param Aliases $aliases
* @param array<string, Type\Union>|null $template_type_map
* @param array<string, array{Type\Union, ?string}>|null $template_type_map
* @param int|null $var_line_number
* @param int|null $came_from_line_number what line number in $source that $comment came from
* @param array<string, array<int, string>> $type_aliases

View File

@ -496,7 +496,7 @@ class FileAnalyzer extends SourceAnalyzer implements StatementsSource
}
/**
* @return null|array<string, Type\Union>
* @return null|array<string,array{Type\Union, ?string}>
*/
public function getTemplateTypeMap()
{

View File

@ -992,7 +992,7 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
}
/**
* @return array<string, Type\Union>|null
* @return array<string,array{Type\Union, ?string}>|null
*/
public function getTemplateTypeMap()
{

View File

@ -198,7 +198,7 @@ abstract class SourceAnalyzer implements StatementsSource
}
/**
* @return array<string, Type\Union>|null
* @return array<string,array{Type\Union, ?string}>|null
*/
public function getTemplateTypeMap()
{

View File

@ -336,7 +336,7 @@ class FunctionCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expressio
if ($function_storage && $function_storage->template_types) {
foreach ($function_storage->template_types as $template_name => $_) {
if (!isset($generic_params[$template_name])) {
$generic_params[$template_name] = Type::getMixed();
$generic_params[$template_name] = [Type::getMixed(), null];
}
}
}

View File

@ -589,10 +589,12 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
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] =
$lhs_type_part->type_params[$provided_type_param_count - 1 - $i];
$class_template_params[$type_name] = [
$lhs_type_part->type_params[$provided_type_param_count - 1 - $i],
$fq_class_name,
];
} else {
$class_template_params[$type_name] = Type::getMixed();
$class_template_params[$type_name] = [Type::getMixed(), null];
}
}
} else {
@ -600,7 +602,7 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
if (!$stmt->var instanceof PhpParser\Node\Expr\Variable
|| $stmt->var->name !== 'this'
) {
$class_template_params[$type_name] = Type::getMixed();
$class_template_params[$type_name] = [Type::getMixed(), null];
}
}
}

View File

@ -360,7 +360,7 @@ class NewAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\CallAna
if (isset($found_generic_params[$template_name])) {
$generic_params[] = $found_generic_params[$template_name];
} else {
$generic_params[] = Type::getMixed();
$generic_params[] = [Type::getMixed(), null];
}
}
}
@ -416,7 +416,15 @@ class NewAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\CallAna
$stmt->inferredType = new Type\Union([
new Type\Atomic\TGenericObject(
$fq_class_name,
$generic_params
array_map(
/**
* @param array{Type\Union, ?string} $i
*/
function (array $i) : Type\Union {
return $i[0];
},
$generic_params
)
),
]);
}

View File

@ -190,7 +190,7 @@ class CallAnalyzer
/**
* @param string|null $method_id
* @param array<int, PhpParser\Node\Arg> $args
* @param array<string, Type\Union>|null &$generic_params
* @param array<string, array{Type\Union, ?string}>|null &$generic_params
* @param Context $context
* @param CodeLocation $code_location
* @param StatementsAnalyzer $statements_analyzer
@ -792,7 +792,7 @@ class CallAnalyzer
* @param array<int,FunctionLikeParameter> $function_params
* @param FunctionLikeStorage|null $function_storage
* @param ClassLikeStorage|null $class_storage
* @param array<string, Type\Union>|null $generic_params
* @param array<string, array{Type\Union, ?string}>|null $generic_params
* @param CodeLocation $code_location
* @param Context $context
*
@ -872,18 +872,20 @@ class CallAnalyzer
$template_types = $function_storage->template_types;
}
if ($class_storage && $class_storage->template_types) {
$template_types = array_merge($template_types, $class_storage->template_types);
foreach ($class_storage->template_types as $template_name => $type) {
$template_types[$template_name] = $type;
}
}
foreach ($template_types as $key => $type) {
$template_types[$key] = clone $type;
$template_types[$key][0] = clone $type[0];
}
}
$existing_generic_params = $generic_params ?: [];
foreach ($existing_generic_params as $key => $type) {
$existing_generic_params[$key] = clone $type;
$existing_generic_params[$key][0] = clone $type[0];
}
foreach ($args as $argument_offset => $arg) {
@ -2152,7 +2154,7 @@ class CallAnalyzer
* @param \Psalm\Storage\Assertion[] $assertions
* @param array<int, PhpParser\Node\Arg> $args
* @param Context $context
* @param array<string, Type\Union> $generic_params,
* @param array<string, array{Type\Union, ?string}> $generic_params,
* @param StatementsAnalyzer $statements_analyzer
*
* @return void
@ -2208,11 +2210,11 @@ class CallAnalyzer
}
if (isset($generic_params[$rule])) {
if ($generic_params[$rule]->hasMixed()) {
if ($generic_params[$rule][0]->hasMixed()) {
continue;
}
$replacement_atomic_types = $generic_params[$rule]->getTypes();
$replacement_atomic_types = $generic_params[$rule][0]->getTypes();
if (count($replacement_atomic_types) > 1) {
continue;

View File

@ -161,7 +161,10 @@ class Reflection
);
if ($class_name_lower === 'generator') {
$storage->template_types = ['TKey' => Type::getMixed(), 'TValue' => Type::getMixed()];
$storage->template_types = [
'TKey' => [Type::getMixed(), 'Generator'],
'TValue' => [Type::getMixed(), 'Generator']
];
}
$interfaces = $reflected_class->getInterfaces();

View File

@ -68,10 +68,10 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
/** @var Config */
private $config;
/** @var array<string, Type\Union> */
/** @var array<string, array{Type\Union, ?string}> */
private $class_template_types = [];
/** @var array<string, Type\Union> */
/** @var array<string, array{Type\Union, ?string}> */
private $function_template_types = [];
/** @var FunctionLikeStorage[] */
@ -727,16 +727,20 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
$storage->template_types = [];
foreach ($docblock_info->templates as $template_type) {
$template_name = $template_type[0];
if (count($template_type) === 3) {
if (trim($template_type[2])) {
$storage->template_types[$template_type[0]] = Type::parseTokens(
Type::fixUpLocalType(
$template_type[2],
$this->aliases,
null,
$this->type_aliases
)
);
$storage->template_types[$template_name] = [
Type::parseTokens(
Type::fixUpLocalType(
$template_type[2],
$this->aliases,
null,
$this->type_aliases
)
),
$fq_classlike_name
];
} else {
if (IssueBuffer::accepts(
new InvalidDocblock(
@ -747,7 +751,7 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
}
}
} else {
$storage->template_types[$template_type[0]] = Type::getMixed();
$storage->template_types[$template_name] = [Type::getMixed(), $fq_classlike_name];
}
}
@ -884,6 +888,7 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
private function registerFunctionLike(PhpParser\Node\FunctionLike $stmt, $fake_method = false)
{
$class_storage = null;
$fq_classlike_name = null;
if ($fake_method && $stmt instanceof PhpParser\Node\Stmt\ClassMethod) {
$cased_function_id = '@method ' . $stmt->name->name;
@ -1359,14 +1364,17 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
foreach ($docblock_info->templates as $template_type) {
if (count($template_type) === 3) {
if (trim($template_type[2])) {
$storage->template_types[$template_type[0]] = Type::parseTokens(
Type::fixUpLocalType(
$template_type[2],
$this->aliases,
null,
$this->type_aliases
)
);
$storage->template_types[$template_type[0]] = [
Type::parseTokens(
Type::fixUpLocalType(
$template_type[2],
$this->aliases,
null,
$this->type_aliases
)
),
$fq_classlike_name
];
} else {
if (IssueBuffer::accepts(
new InvalidDocblock(
@ -1377,7 +1385,7 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
}
}
} else {
$storage->template_types[$template_type[0]] = Type::getMixed();
$storage->template_types[$template_type[0]] = [Type::getMixed(), $fq_classlike_name];
}
}
@ -1586,7 +1594,9 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
if ($param->name === $template_typeof['param_name']) {
$param_type_nullable = $param->type && $param->type->isNullable();
$template_type = $template_types[$template_typeof['template_type']] ?? null;
$template_type = isset($template_types[$template_typeof['template_type']])
? $template_types[$template_typeof['template_type']][0]
: null;
$param->type = new Type\Union([
new Type\Atomic\TGenericParamClass(

View File

@ -29,7 +29,7 @@ interface StatementsSource extends FileSource
public function getParentFQCLN();
/**
* @return array<string,Type\Union>|null
* @return array<string,array{Type\Union, ?string}>|null
*/
public function getTemplateTypeMap();

View File

@ -261,7 +261,7 @@ class ClassLikeStorage
public $overridden_property_ids = [];
/**
* @var array<string, Type\Union>|null
* @var array<string, array{Type\Union, ?string}>|null
*/
public $template_types;

View File

@ -93,7 +93,7 @@ class FunctionLikeStorage
public $global_types = [];
/**
* @var array<string, Type\Union>|null
* @var array<string, array{Type\Union, ?string}>|null
*/
public $template_types;

View File

@ -85,7 +85,7 @@ abstract class Type
*
* @param string $type_string
* @param bool $php_compatible
* @param array<string, Union> $template_type_map
* @param array<string, array{Union, ?string}> $template_type_map
*
* @return Union
*/
@ -102,7 +102,7 @@ abstract class Type
*
* @param array<int, string> $type_tokens
* @param bool $php_compatible
* @param array<string, Union> $template_type_map
* @param array<string, array{Union, ?string}> $template_type_map
*
* @return Union
*/
@ -185,7 +185,7 @@ abstract class Type
/**
* @param ParseTree $parse_tree
* @param bool $php_compatible
* @param array<string, Union> $template_type_map
* @param array<string, array{Union, ?string}> $template_type_map
*
* @return Atomic|TArray|TGenericObject|ObjectLike|Union
*/
@ -235,7 +235,11 @@ abstract class Type
$class_name = (string) $generic_params[0];
if (isset($template_type_map[$class_name])) {
return self::getGenericParamClass($class_name, $template_type_map[$class_name]);
return self::getGenericParamClass(
$class_name,
$template_type_map[$class_name][0],
$template_type_map[$class_name][1]
);
}
return new TClassString($class_name);
@ -456,7 +460,11 @@ abstract class Type
list($fq_classlike_name, $const_name) = explode('::', $parse_tree->value);
if (isset($template_type_map[$fq_classlike_name]) && $const_name === 'class') {
return self::getGenericParamClass($fq_classlike_name, $template_type_map[$fq_classlike_name]);
return self::getGenericParamClass(
$fq_classlike_name,
$template_type_map[$fq_classlike_name][0],
$template_type_map[$fq_classlike_name][1]
);
}
if ($const_name === 'class') {
@ -479,12 +487,17 @@ abstract class Type
return Atomic::create($atomic_type, $php_compatible, $template_type_map);
}
private static function getGenericParamClass(string $param_name, Union $as) : Atomic\TGenericParamClass
{
private static function getGenericParamClass(
string $param_name,
Union $as,
?string $defining_class = null
) : Atomic\TGenericParamClass {
if ($as->hasMixed()) {
return new Atomic\TGenericParamClass(
$param_name,
'object'
'object',
null,
$defining_class
);
}
@ -497,7 +510,10 @@ abstract class Type
foreach ($as->getTypes() as $t) {
if ($t instanceof TObject) {
return new Atomic\TGenericParamClass(
$param_name
$param_name,
'object',
null,
$defining_class
);
}
@ -512,7 +528,8 @@ abstract class Type
return new Atomic\TGenericParamClass(
$param_name,
$traversable->value,
new Union([$traversable])
new Union([$traversable]),
$defining_class
);
}
@ -522,7 +539,8 @@ abstract class Type
return new Atomic\TGenericParamClass(
$param_name,
$traversable->value,
new Union([$traversable])
new Union([$traversable]),
$defining_class
);
}
@ -535,7 +553,8 @@ abstract class Type
return new Atomic\TGenericParamClass(
$param_name,
$t->value,
new Union([$t])
new Union([$t]),
$defining_class
);
}
@ -697,7 +716,7 @@ abstract class Type
/**
* @param string $string_type
* @param Aliases $aliases
* @param array<string, string>|null $template_type_map
* @param array<string, mixed>|null $template_type_map
* @param array<string, array<int, string>>|null $type_aliases
*
* @return array<int, string>

View File

@ -62,7 +62,7 @@ abstract class Atomic
/**
* @param string $value
* @param bool $php_compatible
* @param array<string, Union> $template_type_map
* @param array<string, array{Union, ?string}> $template_type_map
*
* @return Atomic
*/
@ -156,7 +156,7 @@ abstract class Atomic
}
if (isset($template_type_map[$value])) {
return new TGenericParam($value, $template_type_map[$value]);
return new TGenericParam($value, $template_type_map[$value][0], $template_type_map[$value][1]);
}
return new TNamedObject($value);
@ -510,8 +510,8 @@ abstract class Atomic
}
/**
* @param array<string, Type\Union> $template_types
* @param array<string, Type\Union> $generic_params
* @param array<string, array{Type\Union, ?string}> $template_types
* @param array<string, array{Type\Union, ?string}> $generic_params
* @param Type\Atomic|null $input_type
*
* @return void
@ -526,7 +526,7 @@ abstract class Atomic
}
/**
* @param array<string, Type\Union> $template_types
* @param array<string, array{Type\Union, ?string}> $template_types
*
* @return void
*/

View File

@ -159,8 +159,8 @@ trait CallableTrait
}
/**
* @param array<string, Union> $template_types
* @param array<string, Union> $generic_params
* @param array<string, array{Union, ?string}> $template_types
* @param array<string, array{Union, ?string}> $generic_params
* @param Atomic|null $input_type
*
* @return void
@ -209,7 +209,7 @@ trait CallableTrait
}
/**
* @param array<string, Union> $template_types
* @param array<string, array{Union, ?string}> $template_types
*
* @return void
*/

View File

@ -139,8 +139,8 @@ trait GenericTrait
}
/**
* @param array<string, Union> $template_types
* @param array<string, Union> $generic_params
* @param array<string, array{Union, ?string}> $template_types
* @param array<string, array{Union, ?string}> $generic_params
* @param Atomic|null $input_type
*
* @return void
@ -181,7 +181,7 @@ trait GenericTrait
}
/**
* @param array<string, Union> $template_types
* @param array<string, array{Union, ?string}> $template_types
*
* @return void
*/

View File

@ -17,6 +17,7 @@ class TGenericObject extends TNamedObject
if ($value[0] === '\\') {
$value = substr($value, 1);
}
$this->value = $value;
$this->type_params = $type_params;
}

View File

@ -17,13 +17,19 @@ class TGenericParam extends \Psalm\Type\Atomic
*/
public $as;
/**
* @var ?string
*/
public $defining_class;
/**
* @param string $param_name
*/
public function __construct($param_name, Union $extends)
public function __construct($param_name, Union $extends, string $defining_class = null)
{
$this->param_name = $param_name;
$this->as = $extends;
$this->defining_class = $defining_class;
}
public function __toString()

View File

@ -21,14 +21,24 @@ class TGenericParamClass extends TClassString
*/
public $as_type;
/**
* @var ?string
*/
public $defining_class;
/**
* @param string $param_name
*/
public function __construct(string $param_name, string $as = 'object', Union $as_type = null)
{
public function __construct(
string $param_name,
string $as = 'object',
Union $as_type = null,
string $defining_class = null
) {
$this->param_name = $param_name;
$this->as = $as;
$this->as_type = $as_type;
$this->defining_class = $defining_class;
}
/**

View File

@ -138,7 +138,7 @@ class TNamedObject extends Atomic
}
/**
* @param array<string, Union> $template_types
* @param array<string, array{Union, ?string}> $template_types
*
* @return void
*/
@ -152,7 +152,7 @@ class TNamedObject extends Atomic
foreach ($this->extra_types as $extra_type) {
if ($extra_type instanceof TGenericParam && isset($template_types[$extra_type->param_name])) {
$template_type = clone $template_types[$extra_type->param_name];
$template_type = clone $template_types[$extra_type->param_name][0];
foreach ($template_type->getTypes() as $template_type_part) {
if ($template_type_part instanceof TNamedObject) {

View File

@ -782,8 +782,8 @@ class Union
}
/**
* @param array<string, Union> $template_types
* @param array<string, Union> $generic_params
* @param array<string, array{Union, ?string}> $template_types
* @param array<string, array{Union, ?string}> $generic_params
* @param Type\Union|null $input_type
*
* @return void
@ -801,10 +801,10 @@ class Union
if ($atomic_type instanceof Type\Atomic\TGenericParam
&& isset($template_types[$key])
) {
if ($template_types[$key]->getId() !== $key) {
$first_atomic_type = array_values($template_types[$key]->getTypes())[0];
if ($template_types[$key][0]->getId() !== $key) {
$first_atomic_type = array_values($template_types[$key][0]->getTypes())[0];
if ($add_upper_bound && $input_type) {
$template_types[$key] = clone $input_type;
$template_types[$key][0] = clone $input_type;
}
$this->types[$first_atomic_type->getKey()] = clone $first_atomic_type;
if ($first_atomic_type->getKey() !== $key) {
@ -812,8 +812,13 @@ class Union
}
if ($input_type) {
$generic_params[$key] = clone $input_type;
$generic_params[$key]->setFromDocblock();
$generic_param = clone $input_type;
$generic_param->setFromDocblock();
$generic_params[$key] = [
$generic_param,
$atomic_type->defining_class
];
}
}
} elseif ($atomic_type instanceof Type\Atomic\TGenericParamClass
@ -835,11 +840,16 @@ class Union
}
if ($valid_input_atomic_types) {
$generic_params[$atomic_type->param_name] = new Union($valid_input_atomic_types);
$generic_params[$atomic_type->param_name]->setFromDocblock();
$generic_param = new Union($valid_input_atomic_types);
$generic_param->setFromDocblock();
} else {
$generic_params[$atomic_type->param_name] = Type::getMixed();
$generic_param = Type::getMixed();
}
$generic_params[$atomic_type->param_name] = [
$generic_param,
$atomic_type->defining_class
];
}
} else {
$matching_atomic_type = null;
@ -900,7 +910,7 @@ class Union
}
/**
* @param array<string, Type\Union> $template_types
* @param array<string, array{Union, ?string}> $template_types
*
* @return void
*/
@ -916,7 +926,8 @@ class Union
if ($atomic_type instanceof Type\Atomic\TGenericParam) {
$keys_to_unset[] = $key;
$template_type = isset($template_types[$key])
? clone $template_types[$key]
&& $atomic_type->defining_class === $template_types[$key][1]
? clone $template_types[$key][0]
: Type::getMixed();
foreach ($template_type->types as $template_type_part) {
@ -929,7 +940,7 @@ class Union
} elseif ($atomic_type instanceof Type\Atomic\TGenericParamClass) {
$keys_to_unset[] = $key;
$template_type = isset($template_types[$atomic_type->param_name])
? clone $template_types[$atomic_type->param_name]
? clone $template_types[$atomic_type->param_name][0]
: Type::getMixed();
foreach ($template_type->types as $template_type_part) {

View File

@ -1303,7 +1303,7 @@ class TemplateTest extends TestCase
/**
* @template TKey
* @template Tv
* @template TValue
*/
class KeyValueContainer extends ValueContainer
{
@ -1313,7 +1313,7 @@ class TemplateTest extends TestCase
private $k;
/**
* @param TKey $k
* @param Tv $v
* @param TValue $v
*/
public function __construct($k, $v)
{
@ -1332,7 +1332,7 @@ class TemplateTest extends TestCase
$b = $a->getValue();',
[
'$a' => 'KeyValueContainer<string, int>',
'$b' => 'mixed',
'$b' => 'mixed'
],
'error_levels' => ['MixedAssignment'],
],