mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Allow @psalm-type and @psalm-import-type to be used in extends/implements (#5205)
* Fix #4240 - allow type aliases to be used as type parameters * Fix issues that phpcs found * Fix #4240 - stop type aliases being everywhere in the same file * Fix #4240- re-add stuff that was deleted unnecessarily
This commit is contained in:
parent
8c0a5b7059
commit
e476625c1e
@ -2,6 +2,7 @@
|
||||
namespace Psalm\Internal\PhpVisitor\Reflector;
|
||||
|
||||
use Psalm\Internal\Analyzer\NamespaceAnalyzer;
|
||||
use Psalm\Internal\Scanner\ClassLikeDocblockComment;
|
||||
use function array_pop;
|
||||
use function count;
|
||||
use function explode;
|
||||
@ -273,381 +274,23 @@ class ClassLikeNodeScanner
|
||||
$this->codebase->classlikes->addFullyQualifiedTraitName($fq_classlike_name, $this->file_path);
|
||||
}
|
||||
|
||||
$docblock_info = null;
|
||||
$doc_comment = $node->getDocComment();
|
||||
if ($doc_comment) {
|
||||
$docblock_info = null;
|
||||
try {
|
||||
$docblock_info = ClassLikeDocblockParser::parse(
|
||||
$node,
|
||||
$doc_comment,
|
||||
$this->aliases
|
||||
);
|
||||
|
||||
$this->type_aliases += $this->getImportedTypeAliases($docblock_info, $fq_classlike_name);
|
||||
} catch (DocblockParseException $e) {
|
||||
$storage->docblock_issues[] = new InvalidDocblock(
|
||||
$e->getMessage() . ' in docblock for ' . $fq_classlike_name,
|
||||
$name_location ?: $class_location
|
||||
);
|
||||
}
|
||||
|
||||
if ($docblock_info) {
|
||||
if ($docblock_info->stub_override && !$is_classlike_overridden) {
|
||||
throw new InvalidClasslikeOverrideException(
|
||||
'Class/interface/trait ' . $fq_classlike_name . ' is marked as stub override,'
|
||||
. ' but no original counterpart found'
|
||||
);
|
||||
}
|
||||
|
||||
if ($docblock_info->templates) {
|
||||
$storage->template_types = [];
|
||||
|
||||
\usort(
|
||||
$docblock_info->templates,
|
||||
function (array $l, array $r) : int {
|
||||
return $l[4] > $r[4] ? 1 : -1;
|
||||
}
|
||||
);
|
||||
|
||||
foreach ($docblock_info->templates as $i => $template_map) {
|
||||
$template_name = $template_map[0];
|
||||
|
||||
if ($template_map[1] !== null && $template_map[2] !== null) {
|
||||
if (trim($template_map[2])) {
|
||||
try {
|
||||
$template_type = TypeParser::parseTokens(
|
||||
TypeTokenizer::getFullyQualifiedTokens(
|
||||
$template_map[2],
|
||||
$this->aliases,
|
||||
$storage->template_types,
|
||||
$this->type_aliases
|
||||
),
|
||||
null,
|
||||
$storage->template_types,
|
||||
$this->type_aliases
|
||||
);
|
||||
} catch (TypeParseTreeException $e) {
|
||||
$storage->docblock_issues[] = new InvalidDocblock(
|
||||
$e->getMessage() . ' in docblock for ' . $fq_classlike_name,
|
||||
$name_location ?: $class_location
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$storage->template_types[$template_name] = [
|
||||
$fq_classlike_name => $template_type,
|
||||
];
|
||||
} else {
|
||||
$storage->docblock_issues[] = new InvalidDocblock(
|
||||
'Template missing as type',
|
||||
$name_location ?: $class_location
|
||||
);
|
||||
}
|
||||
} else {
|
||||
/** @psalm-suppress PropertyTypeCoercion due to a Psalm bug */
|
||||
$storage->template_types[$template_name][$fq_classlike_name] = Type::getMixed();
|
||||
}
|
||||
|
||||
$storage->template_covariants[$i] = $template_map[3];
|
||||
}
|
||||
|
||||
$this->class_template_types = $storage->template_types;
|
||||
}
|
||||
|
||||
foreach ($docblock_info->template_extends as $extended_class_name) {
|
||||
$this->extendTemplatedType($storage, $node, $extended_class_name);
|
||||
}
|
||||
|
||||
foreach ($docblock_info->template_implements as $implemented_class_name) {
|
||||
$this->implementTemplatedType($storage, $node, $implemented_class_name);
|
||||
}
|
||||
|
||||
if ($docblock_info->yield) {
|
||||
try {
|
||||
$yield_type_tokens = TypeTokenizer::getFullyQualifiedTokens(
|
||||
$docblock_info->yield,
|
||||
$this->aliases,
|
||||
$storage->template_types,
|
||||
$this->type_aliases
|
||||
);
|
||||
|
||||
$yield_type = TypeParser::parseTokens(
|
||||
$yield_type_tokens,
|
||||
null,
|
||||
$storage->template_types ?: [],
|
||||
$this->type_aliases
|
||||
);
|
||||
$yield_type->setFromDocblock();
|
||||
$yield_type->queueClassLikesForScanning(
|
||||
$this->codebase,
|
||||
$this->file_storage,
|
||||
$storage->template_types ?: []
|
||||
);
|
||||
|
||||
$storage->yield = $yield_type;
|
||||
} catch (TypeParseTreeException $e) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
if ($docblock_info->extension_requirement !== null) {
|
||||
$storage->extension_requirement = (string) TypeParser::parseTokens(
|
||||
TypeTokenizer::getFullyQualifiedTokens(
|
||||
$docblock_info->extension_requirement,
|
||||
$this->aliases,
|
||||
$this->class_template_types,
|
||||
$this->type_aliases
|
||||
),
|
||||
null,
|
||||
$this->class_template_types,
|
||||
$this->type_aliases
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($docblock_info->implementation_requirements as $implementation_requirement) {
|
||||
$storage->implementation_requirements[] = (string) TypeParser::parseTokens(
|
||||
TypeTokenizer::getFullyQualifiedTokens(
|
||||
$implementation_requirement,
|
||||
$this->aliases,
|
||||
$this->class_template_types,
|
||||
$this->type_aliases
|
||||
),
|
||||
null,
|
||||
$this->class_template_types,
|
||||
$this->type_aliases
|
||||
);
|
||||
}
|
||||
|
||||
$storage->sealed_properties = $docblock_info->sealed_properties;
|
||||
$storage->sealed_methods = $docblock_info->sealed_methods;
|
||||
|
||||
if ($docblock_info->properties) {
|
||||
foreach ($docblock_info->properties as $property) {
|
||||
$pseudo_property_type_tokens = TypeTokenizer::getFullyQualifiedTokens(
|
||||
$property['type'],
|
||||
$this->aliases,
|
||||
$this->class_template_types,
|
||||
$this->type_aliases
|
||||
);
|
||||
|
||||
try {
|
||||
$pseudo_property_type = TypeParser::parseTokens(
|
||||
$pseudo_property_type_tokens,
|
||||
null,
|
||||
$this->class_template_types,
|
||||
$this->type_aliases
|
||||
);
|
||||
$pseudo_property_type->setFromDocblock();
|
||||
$pseudo_property_type->queueClassLikesForScanning(
|
||||
$this->codebase,
|
||||
$this->file_storage,
|
||||
$storage->template_types ?: []
|
||||
);
|
||||
|
||||
if ($property['tag'] !== 'property-read' && $property['tag'] !== 'psalm-property-read') {
|
||||
$storage->pseudo_property_set_types[$property['name']] = $pseudo_property_type;
|
||||
}
|
||||
|
||||
if ($property['tag'] !== 'property-write' && $property['tag'] !== 'psalm-property-write') {
|
||||
$storage->pseudo_property_get_types[$property['name']] = $pseudo_property_type;
|
||||
}
|
||||
} catch (TypeParseTreeException $e) {
|
||||
$storage->docblock_issues[] = new InvalidDocblock(
|
||||
$e->getMessage() . ' in docblock for ' . $fq_classlike_name,
|
||||
$name_location ?: $class_location
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$storage->sealed_properties = true;
|
||||
}
|
||||
|
||||
foreach ($docblock_info->methods as $method) {
|
||||
$functionlike_node_scanner = new FunctionLikeNodeScanner(
|
||||
$this->codebase,
|
||||
$this->file_scanner,
|
||||
$this->file_storage,
|
||||
$this->aliases,
|
||||
$this->type_aliases,
|
||||
$this->storage,
|
||||
[]
|
||||
);
|
||||
|
||||
/** @var MethodStorage */
|
||||
$pseudo_method_storage = $functionlike_node_scanner->start($method, true);
|
||||
|
||||
if ($pseudo_method_storage->is_static) {
|
||||
$storage->pseudo_static_methods[strtolower($method->name->name)] = $pseudo_method_storage;
|
||||
} else {
|
||||
$storage->pseudo_methods[strtolower($method->name->name)] = $pseudo_method_storage;
|
||||
}
|
||||
|
||||
$storage->sealed_methods = true;
|
||||
}
|
||||
|
||||
foreach ($docblock_info->imported_types as $import_type_entry) {
|
||||
$imported_type_data = $import_type_entry['parts'];
|
||||
$location = new DocblockTypeLocation(
|
||||
$this->file_scanner,
|
||||
$import_type_entry['start_offset'],
|
||||
$import_type_entry['end_offset'],
|
||||
$import_type_entry['line_number']
|
||||
);
|
||||
// There are two valid forms:
|
||||
// @psalm-import Thing from Something
|
||||
// @psalm-import Thing from Something as Alias
|
||||
// but there could be leftovers after that
|
||||
if (count($imported_type_data) < 3) {
|
||||
$storage->docblock_issues[] = new InvalidTypeImport(
|
||||
'Invalid import in docblock for ' . $fq_classlike_name
|
||||
. ', expecting "<TypeName> from <ClassName>",'
|
||||
. ' got "' . implode(' ', $imported_type_data) . '" instead.',
|
||||
$location
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($imported_type_data[1] === 'from'
|
||||
&& !empty($imported_type_data[0])
|
||||
&& !empty($imported_type_data[2])
|
||||
) {
|
||||
$type_alias_name = $as_alias_name = $imported_type_data[0];
|
||||
$declaring_classlike_name = $imported_type_data[2];
|
||||
} else {
|
||||
$storage->docblock_issues[] = new InvalidTypeImport(
|
||||
'Invalid import in docblock for ' . $fq_classlike_name
|
||||
. ', expecting "<TypeName> from <ClassName>", got "'
|
||||
. implode(
|
||||
' ',
|
||||
[$imported_type_data[0], $imported_type_data[1], $imported_type_data[2]]
|
||||
) . '" instead.',
|
||||
$location
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count($imported_type_data) >= 4 && $imported_type_data[3] === 'as') {
|
||||
// long form
|
||||
if (empty($imported_type_data[4])) {
|
||||
$storage->docblock_issues[] = new InvalidTypeImport(
|
||||
'Invalid import in docblock for ' . $fq_classlike_name
|
||||
. ', expecting "as <TypeName>", got "'
|
||||
. $imported_type_data[3] . ' ' . ($imported_type_data[4] ?? '') . '" instead.',
|
||||
$location
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$as_alias_name = $imported_type_data[4];
|
||||
}
|
||||
|
||||
$declaring_fq_classlike_name = Type::getFQCLNFromString(
|
||||
$declaring_classlike_name,
|
||||
$this->aliases
|
||||
);
|
||||
|
||||
$this->codebase->scanner->queueClassLikeForScanning($declaring_fq_classlike_name);
|
||||
$this->file_storage->referenced_classlikes[strtolower($declaring_fq_classlike_name)]
|
||||
= $declaring_fq_classlike_name;
|
||||
|
||||
$this->type_aliases[$as_alias_name] = new TypeAlias\LinkableTypeAlias(
|
||||
$declaring_fq_classlike_name,
|
||||
$type_alias_name,
|
||||
$import_type_entry['line_number'],
|
||||
$import_type_entry['start_offset'],
|
||||
$import_type_entry['end_offset']
|
||||
);
|
||||
}
|
||||
|
||||
$storage->deprecated = $docblock_info->deprecated;
|
||||
|
||||
if ($docblock_info->internal
|
||||
&& !$docblock_info->psalm_internal
|
||||
&& $this->aliases->namespace
|
||||
) {
|
||||
$storage->internal = explode('\\', $this->aliases->namespace)[0];
|
||||
} else {
|
||||
$storage->internal = $docblock_info->psalm_internal ?? '';
|
||||
}
|
||||
|
||||
if ($docblock_info->final && !$storage->final) {
|
||||
$storage->final = true;
|
||||
$storage->final_from_docblock = true;
|
||||
}
|
||||
|
||||
$storage->preserve_constructor_signature = $docblock_info->consistent_constructor;
|
||||
|
||||
if ($storage->preserve_constructor_signature) {
|
||||
$has_constructor = false;
|
||||
|
||||
foreach ($node->stmts as $stmt) {
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod
|
||||
&& $stmt->name->name === '__construct'
|
||||
) {
|
||||
$has_constructor = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$has_constructor) {
|
||||
self::registerEmptyConstructor($storage);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($docblock_info->mixins as $key => $mixin) {
|
||||
$mixin_type = TypeParser::parseTokens(
|
||||
TypeTokenizer::getFullyQualifiedTokens(
|
||||
$mixin,
|
||||
$this->aliases,
|
||||
$this->class_template_types,
|
||||
$this->type_aliases,
|
||||
$fq_classlike_name
|
||||
),
|
||||
null,
|
||||
$this->class_template_types,
|
||||
$this->type_aliases
|
||||
);
|
||||
|
||||
$mixin_type->queueClassLikesForScanning(
|
||||
$this->codebase,
|
||||
$this->file_storage,
|
||||
$storage->template_types ?: []
|
||||
);
|
||||
|
||||
$mixin_type->setFromDocblock();
|
||||
|
||||
if ($mixin_type->isSingle()) {
|
||||
$mixin_type = \array_values($mixin_type->getAtomicTypes())[0];
|
||||
|
||||
if ($mixin_type instanceof Type\Atomic\TNamedObject) {
|
||||
$storage->namedMixins[] = $mixin_type;
|
||||
}
|
||||
|
||||
if ($mixin_type instanceof Type\Atomic\TTemplateParam) {
|
||||
$storage->templatedMixins[] = $mixin_type;
|
||||
}
|
||||
}
|
||||
|
||||
if ($key === 0) {
|
||||
$storage->mixin_declaring_fqcln = $storage->name;
|
||||
|
||||
// backwards compatibility
|
||||
if ($mixin_type instanceof Type\Atomic\TNamedObject
|
||||
|| $mixin_type instanceof Type\Atomic\TTemplateParam) {
|
||||
/** @psalm-suppress DeprecatedProperty **/
|
||||
$storage->mixin = $mixin_type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$storage->mutation_free = $docblock_info->mutation_free;
|
||||
$storage->external_mutation_free = $docblock_info->external_mutation_free;
|
||||
$storage->specialize_instance = $docblock_info->taint_specialize;
|
||||
|
||||
$storage->override_property_visibility = $docblock_info->override_property_visibility;
|
||||
$storage->override_method_visibility = $docblock_info->override_method_visibility;
|
||||
|
||||
$storage->suppressed_issues = $docblock_info->suppressed_issues;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($node->getComments() as $comment) {
|
||||
@ -686,6 +329,293 @@ class ClassLikeNodeScanner
|
||||
}
|
||||
}
|
||||
|
||||
if ($docblock_info) {
|
||||
if ($docblock_info->stub_override && !$is_classlike_overridden) {
|
||||
throw new InvalidClasslikeOverrideException(
|
||||
'Class/interface/trait ' . $fq_classlike_name . ' is marked as stub override,'
|
||||
. ' but no original counterpart found'
|
||||
);
|
||||
}
|
||||
|
||||
if ($docblock_info->templates) {
|
||||
$storage->template_types = [];
|
||||
|
||||
\usort(
|
||||
$docblock_info->templates,
|
||||
function (array $l, array $r) : int {
|
||||
return $l[4] > $r[4] ? 1 : -1;
|
||||
}
|
||||
);
|
||||
|
||||
foreach ($docblock_info->templates as $i => $template_map) {
|
||||
$template_name = $template_map[0];
|
||||
|
||||
if ($template_map[1] !== null && $template_map[2] !== null) {
|
||||
if (trim($template_map[2])) {
|
||||
try {
|
||||
$template_type = TypeParser::parseTokens(
|
||||
TypeTokenizer::getFullyQualifiedTokens(
|
||||
$template_map[2],
|
||||
$this->aliases,
|
||||
$storage->template_types,
|
||||
$this->type_aliases
|
||||
),
|
||||
null,
|
||||
$storage->template_types,
|
||||
$this->type_aliases
|
||||
);
|
||||
} catch (TypeParseTreeException $e) {
|
||||
$storage->docblock_issues[] = new InvalidDocblock(
|
||||
$e->getMessage() . ' in docblock for ' . $fq_classlike_name,
|
||||
$name_location ?: $class_location
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$storage->template_types[$template_name] = [
|
||||
$fq_classlike_name => $template_type,
|
||||
];
|
||||
} else {
|
||||
$storage->docblock_issues[] = new InvalidDocblock(
|
||||
'Template missing as type',
|
||||
$name_location ?: $class_location
|
||||
);
|
||||
}
|
||||
} else {
|
||||
/** @psalm-suppress PropertyTypeCoercion due to a Psalm bug */
|
||||
$storage->template_types[$template_name][$fq_classlike_name] = Type::getMixed();
|
||||
}
|
||||
|
||||
$storage->template_covariants[$i] = $template_map[3];
|
||||
}
|
||||
|
||||
$this->class_template_types = $storage->template_types;
|
||||
}
|
||||
|
||||
foreach ($docblock_info->template_extends as $extended_class_name) {
|
||||
$this->extendTemplatedType($storage, $node, $extended_class_name);
|
||||
}
|
||||
|
||||
foreach ($docblock_info->template_implements as $implemented_class_name) {
|
||||
$this->implementTemplatedType($storage, $node, $implemented_class_name);
|
||||
}
|
||||
|
||||
if ($docblock_info->yield) {
|
||||
try {
|
||||
$yield_type_tokens = TypeTokenizer::getFullyQualifiedTokens(
|
||||
$docblock_info->yield,
|
||||
$this->aliases,
|
||||
$storage->template_types,
|
||||
$this->type_aliases
|
||||
);
|
||||
|
||||
$yield_type = TypeParser::parseTokens(
|
||||
$yield_type_tokens,
|
||||
null,
|
||||
$storage->template_types ?: [],
|
||||
$this->type_aliases
|
||||
);
|
||||
$yield_type->setFromDocblock();
|
||||
$yield_type->queueClassLikesForScanning(
|
||||
$this->codebase,
|
||||
$this->file_storage,
|
||||
$storage->template_types ?: []
|
||||
);
|
||||
|
||||
$storage->yield = $yield_type;
|
||||
} catch (TypeParseTreeException $e) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
if ($docblock_info->extension_requirement !== null) {
|
||||
$storage->extension_requirement = (string) TypeParser::parseTokens(
|
||||
TypeTokenizer::getFullyQualifiedTokens(
|
||||
$docblock_info->extension_requirement,
|
||||
$this->aliases,
|
||||
$this->class_template_types,
|
||||
$this->type_aliases
|
||||
),
|
||||
null,
|
||||
$this->class_template_types,
|
||||
$this->type_aliases
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($docblock_info->implementation_requirements as $implementation_requirement) {
|
||||
$storage->implementation_requirements[] = (string) TypeParser::parseTokens(
|
||||
TypeTokenizer::getFullyQualifiedTokens(
|
||||
$implementation_requirement,
|
||||
$this->aliases,
|
||||
$this->class_template_types,
|
||||
$this->type_aliases
|
||||
),
|
||||
null,
|
||||
$this->class_template_types,
|
||||
$this->type_aliases
|
||||
);
|
||||
}
|
||||
|
||||
$storage->sealed_properties = $docblock_info->sealed_properties;
|
||||
$storage->sealed_methods = $docblock_info->sealed_methods;
|
||||
|
||||
if ($docblock_info->properties) {
|
||||
foreach ($docblock_info->properties as $property) {
|
||||
$pseudo_property_type_tokens = TypeTokenizer::getFullyQualifiedTokens(
|
||||
$property['type'],
|
||||
$this->aliases,
|
||||
$this->class_template_types,
|
||||
$this->type_aliases
|
||||
);
|
||||
|
||||
try {
|
||||
$pseudo_property_type = TypeParser::parseTokens(
|
||||
$pseudo_property_type_tokens,
|
||||
null,
|
||||
$this->class_template_types,
|
||||
$this->type_aliases
|
||||
);
|
||||
$pseudo_property_type->setFromDocblock();
|
||||
$pseudo_property_type->queueClassLikesForScanning(
|
||||
$this->codebase,
|
||||
$this->file_storage,
|
||||
$storage->template_types ?: []
|
||||
);
|
||||
|
||||
if ($property['tag'] !== 'property-read' && $property['tag'] !== 'psalm-property-read') {
|
||||
$storage->pseudo_property_set_types[$property['name']] = $pseudo_property_type;
|
||||
}
|
||||
|
||||
if ($property['tag'] !== 'property-write' && $property['tag'] !== 'psalm-property-write') {
|
||||
$storage->pseudo_property_get_types[$property['name']] = $pseudo_property_type;
|
||||
}
|
||||
} catch (TypeParseTreeException $e) {
|
||||
$storage->docblock_issues[] = new InvalidDocblock(
|
||||
$e->getMessage() . ' in docblock for ' . $fq_classlike_name,
|
||||
$name_location ?: $class_location
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$storage->sealed_properties = true;
|
||||
}
|
||||
|
||||
foreach ($docblock_info->methods as $method) {
|
||||
$functionlike_node_scanner = new FunctionLikeNodeScanner(
|
||||
$this->codebase,
|
||||
$this->file_scanner,
|
||||
$this->file_storage,
|
||||
$this->aliases,
|
||||
$this->type_aliases,
|
||||
$this->storage,
|
||||
[]
|
||||
);
|
||||
|
||||
/** @var MethodStorage */
|
||||
$pseudo_method_storage = $functionlike_node_scanner->start($method, true);
|
||||
|
||||
if ($pseudo_method_storage->is_static) {
|
||||
$storage->pseudo_static_methods[strtolower($method->name->name)] = $pseudo_method_storage;
|
||||
} else {
|
||||
$storage->pseudo_methods[strtolower($method->name->name)] = $pseudo_method_storage;
|
||||
}
|
||||
|
||||
$storage->sealed_methods = true;
|
||||
}
|
||||
|
||||
|
||||
$storage->deprecated = $docblock_info->deprecated;
|
||||
|
||||
if ($docblock_info->internal
|
||||
&& !$docblock_info->psalm_internal
|
||||
&& $this->aliases->namespace
|
||||
) {
|
||||
$storage->internal = explode('\\', $this->aliases->namespace)[0];
|
||||
} else {
|
||||
$storage->internal = $docblock_info->psalm_internal ?? '';
|
||||
}
|
||||
|
||||
if ($docblock_info->final && !$storage->final) {
|
||||
$storage->final = true;
|
||||
$storage->final_from_docblock = true;
|
||||
}
|
||||
|
||||
$storage->preserve_constructor_signature = $docblock_info->consistent_constructor;
|
||||
|
||||
if ($storage->preserve_constructor_signature) {
|
||||
$has_constructor = false;
|
||||
|
||||
foreach ($node->stmts as $stmt) {
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod
|
||||
&& $stmt->name->name === '__construct'
|
||||
) {
|
||||
$has_constructor = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$has_constructor) {
|
||||
self::registerEmptyConstructor($storage);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($docblock_info->mixins as $key => $mixin) {
|
||||
$mixin_type = TypeParser::parseTokens(
|
||||
TypeTokenizer::getFullyQualifiedTokens(
|
||||
$mixin,
|
||||
$this->aliases,
|
||||
$this->class_template_types,
|
||||
$this->type_aliases,
|
||||
$fq_classlike_name
|
||||
),
|
||||
null,
|
||||
$this->class_template_types,
|
||||
$this->type_aliases
|
||||
);
|
||||
|
||||
$mixin_type->queueClassLikesForScanning(
|
||||
$this->codebase,
|
||||
$this->file_storage,
|
||||
$storage->template_types ?: []
|
||||
);
|
||||
|
||||
$mixin_type->setFromDocblock();
|
||||
|
||||
if ($mixin_type->isSingle()) {
|
||||
$mixin_type = \array_values($mixin_type->getAtomicTypes())[0];
|
||||
|
||||
if ($mixin_type instanceof Type\Atomic\TNamedObject) {
|
||||
$storage->namedMixins[] = $mixin_type;
|
||||
}
|
||||
|
||||
if ($mixin_type instanceof Type\Atomic\TTemplateParam) {
|
||||
$storage->templatedMixins[] = $mixin_type;
|
||||
}
|
||||
}
|
||||
|
||||
if ($key === 0) {
|
||||
$storage->mixin_declaring_fqcln = $storage->name;
|
||||
|
||||
// backwards compatibility
|
||||
if ($mixin_type instanceof Type\Atomic\TNamedObject
|
||||
|| $mixin_type instanceof Type\Atomic\TTemplateParam) {
|
||||
/** @psalm-suppress DeprecatedProperty **/
|
||||
$storage->mixin = $mixin_type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$storage->mutation_free = $docblock_info->mutation_free;
|
||||
$storage->external_mutation_free = $docblock_info->external_mutation_free;
|
||||
$storage->specialize_instance = $docblock_info->taint_specialize;
|
||||
|
||||
$storage->override_property_visibility = $docblock_info->override_property_visibility;
|
||||
$storage->override_method_visibility = $docblock_info->override_method_visibility;
|
||||
|
||||
$storage->suppressed_issues = $docblock_info->suppressed_issues;
|
||||
}
|
||||
|
||||
foreach ($node->stmts as $node_stmt) {
|
||||
if ($node_stmt instanceof PhpParser\Node\Stmt\ClassConst) {
|
||||
$this->visitClassConstDeclaration($node_stmt, $storage, $fq_classlike_name);
|
||||
@ -1477,6 +1407,94 @@ class ClassLikeNodeScanner
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ClassLikeDocblockComment $comment
|
||||
* @param string $fq_classlike_name
|
||||
*
|
||||
* @return array<string, TypeAlias\LinkableTypeAlias>
|
||||
*/
|
||||
private function getImportedTypeAliases(ClassLikeDocblockComment $comment, string $fq_classlike_name) : array
|
||||
{
|
||||
/** @var array<string, TypeAlias\LinkableTypeAlias> $results */
|
||||
$results = [];
|
||||
|
||||
foreach ($comment->imported_types as $import_type_entry) {
|
||||
$imported_type_data = $import_type_entry['parts'];
|
||||
$location = new DocblockTypeLocation(
|
||||
$this->file_scanner,
|
||||
$import_type_entry['start_offset'],
|
||||
$import_type_entry['end_offset'],
|
||||
$import_type_entry['line_number']
|
||||
);
|
||||
// There are two valid forms:
|
||||
// @psalm-import Thing from Something
|
||||
// @psalm-import Thing from Something as Alias
|
||||
// but there could be leftovers after that
|
||||
if (count($imported_type_data) < 3) {
|
||||
$this->file_storage->docblock_issues[] = new InvalidTypeImport(
|
||||
'Invalid import in docblock for ' . $fq_classlike_name
|
||||
. ', expecting "<TypeName> from <ClassName>",'
|
||||
. ' got "' . implode(' ', $imported_type_data) . '" instead.',
|
||||
$location
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($imported_type_data[1] === 'from'
|
||||
&& !empty($imported_type_data[0])
|
||||
&& !empty($imported_type_data[2])
|
||||
) {
|
||||
$type_alias_name = $as_alias_name = $imported_type_data[0];
|
||||
$declaring_classlike_name = $imported_type_data[2];
|
||||
} else {
|
||||
$this->file_storage->docblock_issues[] = new InvalidTypeImport(
|
||||
'Invalid import in docblock for ' . $fq_classlike_name
|
||||
. ', expecting "<TypeName> from <ClassName>", got "'
|
||||
. implode(
|
||||
' ',
|
||||
[$imported_type_data[0], $imported_type_data[1], $imported_type_data[2]]
|
||||
) . '" instead.',
|
||||
$location
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count($imported_type_data) >= 4 && $imported_type_data[3] === 'as') {
|
||||
// long form
|
||||
if (empty($imported_type_data[4])) {
|
||||
$this->file_storage->docblock_issues[] = new InvalidTypeImport(
|
||||
'Invalid import in docblock for ' . $fq_classlike_name
|
||||
. ', expecting "as <TypeName>", got "'
|
||||
. $imported_type_data[3] . ' ' . ($imported_type_data[4] ?? '') . '" instead.',
|
||||
$location
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$as_alias_name = $imported_type_data[4];
|
||||
}
|
||||
|
||||
$declaring_fq_classlike_name = Type::getFQCLNFromString(
|
||||
$declaring_classlike_name,
|
||||
$this->aliases
|
||||
);
|
||||
|
||||
$this->codebase->scanner->queueClassLikeForScanning($declaring_fq_classlike_name);
|
||||
$this->file_storage->referenced_classlikes[strtolower($declaring_fq_classlike_name)]
|
||||
= $declaring_fq_classlike_name;
|
||||
|
||||
$results[$as_alias_name] = new TypeAlias\LinkableTypeAlias(
|
||||
$declaring_fq_classlike_name,
|
||||
$type_alias_name,
|
||||
$import_type_entry['line_number'],
|
||||
$import_type_entry['start_offset'],
|
||||
$import_type_entry['end_offset']
|
||||
);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, TypeAlias> $type_aliases
|
||||
*
|
||||
|
@ -336,6 +336,208 @@ class TypeAnnotationTest extends TestCase
|
||||
}
|
||||
}'
|
||||
],
|
||||
'sameDocBlockTypeAliasAsTypeParameterForInterface' => [
|
||||
'<?php
|
||||
/** @template T */
|
||||
interface A {
|
||||
/** @return T */
|
||||
public function output();
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-type Foo=string
|
||||
* @implements A<Foo>
|
||||
*/
|
||||
class C implements A {
|
||||
public function output() {
|
||||
return "hello";
|
||||
}
|
||||
}
|
||||
|
||||
$instance = new C();
|
||||
$output = $instance->output();',
|
||||
[
|
||||
'$output' => 'string',
|
||||
],
|
||||
],
|
||||
'sameDocBlockTypeAliasAsTypeParameterForExtendedRegularClass' => [
|
||||
'<?php
|
||||
/** @template T */
|
||||
class A {
|
||||
/** @var T */
|
||||
public $value;
|
||||
|
||||
/** @param T $value */
|
||||
public function __construct($value) {
|
||||
$this->value = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-type Foo=string
|
||||
* @extends A<Foo>
|
||||
*/
|
||||
class C extends A {}
|
||||
|
||||
$instance = new C("hello");
|
||||
$output = $instance->value;',
|
||||
[
|
||||
'$output' => 'string',
|
||||
],
|
||||
],
|
||||
'sameDocBlockTypeAliasAsTypeParameterForExtendedAbstractClass' => [
|
||||
'<?php
|
||||
/** @template T */
|
||||
abstract class A {
|
||||
/** @var T */
|
||||
public $value;
|
||||
|
||||
/** @param T $value */
|
||||
public function __construct($value) {
|
||||
$this->value = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-type Foo=string
|
||||
* @extends A<Foo>
|
||||
*/
|
||||
class C extends A {}
|
||||
|
||||
$instance = new C("hello");
|
||||
$output = $instance->value;',
|
||||
[
|
||||
'$output' => 'string',
|
||||
],
|
||||
],
|
||||
'importedTypeAliasAsTypeParameterForImplementation' => [
|
||||
'<?php
|
||||
namespace Bar;
|
||||
|
||||
/** @template T */
|
||||
interface A {}
|
||||
|
||||
/** @psalm-type Foo=string */
|
||||
class B {}
|
||||
|
||||
/**
|
||||
* @psalm-import-type Foo from B
|
||||
* @implements A<Foo>
|
||||
*/
|
||||
class C implements A {}',
|
||||
],
|
||||
'importedTypeAliasAsTypeParameterForExtendedClass' => [
|
||||
'<?php
|
||||
namespace Bar;
|
||||
|
||||
/** @template T */
|
||||
class A {}
|
||||
|
||||
/** @psalm-type Foo=string */
|
||||
class B {}
|
||||
|
||||
/**
|
||||
* @psalm-import-type Foo from B
|
||||
* @extends A<Foo>
|
||||
*/
|
||||
class C extends A {}',
|
||||
],
|
||||
'importedTypeAliasAsTypeParameterForExtendedAbstractClass' => [
|
||||
'<?php
|
||||
namespace Bar;
|
||||
|
||||
/** @template T */
|
||||
abstract class A {}
|
||||
|
||||
/** @psalm-type Foo=string */
|
||||
class B {}
|
||||
|
||||
/**
|
||||
* @psalm-import-type Foo from B
|
||||
* @extends A<Foo>
|
||||
*/
|
||||
class C extends A {}',
|
||||
],
|
||||
'importedTypeAliasRenamedAsTypeParameterForImplementation' => [
|
||||
'<?php
|
||||
namespace Bar;
|
||||
|
||||
/** @template T */
|
||||
interface A {}
|
||||
|
||||
/** @psalm-type Foo=string */
|
||||
class B {}
|
||||
|
||||
/**
|
||||
* @psalm-import-type Foo from B as NewName
|
||||
* @implements A<NewName>
|
||||
*/
|
||||
class C implements A {}',
|
||||
],
|
||||
'importedTypeAliasRenamedAsTypeParameterForExtendedClass' => [
|
||||
'<?php
|
||||
namespace Bar;
|
||||
|
||||
/** @template T */
|
||||
class A {}
|
||||
|
||||
/** @psalm-type Foo=string */
|
||||
class B {}
|
||||
|
||||
/**
|
||||
* @psalm-import-type Foo from B as NewName
|
||||
* @extends A<NewName>
|
||||
*/
|
||||
class C extends A {}',
|
||||
],
|
||||
'importedTypeAliasRenamedAsTypeParameterForExtendedAbstractClass' => [
|
||||
'<?php
|
||||
namespace Bar;
|
||||
|
||||
/** @template T */
|
||||
abstract class A {}
|
||||
|
||||
/** @psalm-type Foo=string */
|
||||
class B {}
|
||||
|
||||
/**
|
||||
* @psalm-import-type Foo from B as NewName
|
||||
* @extends A<NewName>
|
||||
*/
|
||||
class C extends A {}',
|
||||
],
|
||||
'importedTypeInsideLocalTypeAliasUsedAsTypeParameter' => [
|
||||
'<?php
|
||||
/** @template T */
|
||||
abstract class A {
|
||||
/** @var T */
|
||||
public $value;
|
||||
|
||||
/** @param T $value */
|
||||
public function __construct($value) {
|
||||
$this->value = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-type Foo=string
|
||||
*/
|
||||
class B {}
|
||||
|
||||
/**
|
||||
* @psalm-import-type Foo from B
|
||||
* @psalm-type Baz=Foo
|
||||
*
|
||||
* @extends A<Baz>
|
||||
*/
|
||||
class C extends A {}
|
||||
|
||||
$instance = new C("hello");
|
||||
$output = $instance->value;',
|
||||
[
|
||||
'$output' => 'string',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@ -533,6 +735,35 @@ class TypeAnnotationTest extends TestCase
|
||||
function test(array $input):void {}',
|
||||
'error_message' => 'InvalidDocblock',
|
||||
],
|
||||
'invalidTypeWhenNotImported' => [
|
||||
'<?php
|
||||
|
||||
/** @psalm-type Foo = string */
|
||||
class A {}
|
||||
|
||||
/** @template T */
|
||||
interface B {}
|
||||
|
||||
/** @implements B<Foo> */
|
||||
class C implements B {}',
|
||||
'error_message' => 'UndefinedDocblockClass',
|
||||
],
|
||||
'invalidTypeWhenNotImportedInsideAnotherTypeAlias' => [
|
||||
'<?php
|
||||
|
||||
/** @psalm-type Foo = string */
|
||||
class A {}
|
||||
|
||||
/** @template T */
|
||||
interface B {}
|
||||
|
||||
/**
|
||||
* @psalm-type Baz=Foo
|
||||
* @implements B<Baz>
|
||||
*/
|
||||
class C implements B {}',
|
||||
'error_message' => 'UndefinedDocblockClass',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user