1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 12:24:49 +01:00

Break out parent and implemented class checks

This commit is contained in:
Matthew Brown 2021-01-05 17:49:55 -05:00 committed by Daniil Gentili
parent affaa5ea85
commit 8696873cb9
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
2 changed files with 435 additions and 396 deletions

View File

@ -2,6 +2,7 @@
namespace Psalm\Internal\Analyzer;
use PhpParser;
use PhpParser\Node\Stmt\Class_;
use Psalm\Aliases;
use Psalm\DocComment;
use Psalm\Exception\DocblockParseException;
@ -217,244 +218,16 @@ class ClassAnalyzer extends ClassLikeAnalyzer
$parent_fq_class_name = $this->parent_fq_class_name;
if ($class->extends) {
if (!$parent_fq_class_name) {
throw new \UnexpectedValueException('Parent class should be filled in for ' . $fq_class_name);
}
$parent_reference_location = new CodeLocation($this, $class->extends);
if (self::checkFullyQualifiedClassLikeName(
$this->getSource(),
if ($class->extends && $parent_fq_class_name) {
$this->checkParentClass(
$class,
$class->extends,
$fq_class_name,
$parent_fq_class_name,
$parent_reference_location,
null,
null,
$storage->suppressed_issues + $this->getSuppressedIssues(),
false
) === false) {
return false;
}
if ($codebase->alter_code && $codebase->classes_to_move) {
$codebase->classlikes->handleClassLikeReferenceInMigration(
$codebase,
$this,
$class->extends,
$parent_fq_class_name,
null
);
}
try {
$parent_class_storage = $classlike_storage_provider->get($parent_fq_class_name);
$code_location = new CodeLocation(
$this,
$class->extends,
$class_context ? $class_context->include_location : null,
true
);
if ($parent_class_storage->is_trait || $parent_class_storage->is_interface) {
if (IssueBuffer::accepts(
new UndefinedClass(
$parent_fq_class_name . ' is not a class',
$code_location,
$parent_fq_class_name . ' as class'
),
$storage->suppressed_issues + $this->getSuppressedIssues()
)) {
// fall through
}
}
if ($parent_class_storage->final) {
if (IssueBuffer::accepts(
new InvalidExtendClass(
'Class ' . $fq_class_name . ' may not inherit from final class ' . $parent_fq_class_name,
$code_location,
$fq_class_name
),
$storage->suppressed_issues + $this->getSuppressedIssues()
)) {
// fall through
}
}
if ($parent_class_storage->deprecated) {
if (IssueBuffer::accepts(
new DeprecatedClass(
$parent_fq_class_name . ' is marked deprecated',
$code_location,
$parent_fq_class_name
),
$storage->suppressed_issues + $this->getSuppressedIssues()
)) {
// fall through
}
}
if (! NamespaceAnalyzer::isWithin($fq_class_name, $parent_class_storage->internal)) {
if (IssueBuffer::accepts(
new InternalClass(
$parent_fq_class_name . ' is internal to ' . $parent_class_storage->internal
. ' but called from ' . $fq_class_name,
$code_location,
$parent_fq_class_name
),
$storage->suppressed_issues + $this->getSuppressedIssues()
)) {
// fall through
}
}
if ($parent_class_storage->external_mutation_free
&& !$storage->external_mutation_free
) {
if (IssueBuffer::accepts(
new MissingImmutableAnnotation(
$parent_fq_class_name . ' is marked @psalm-immutable, but '
. $fq_class_name . ' is not marked @psalm-immutable',
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
)) {
// fall through
}
}
if ($storage->mutation_free
&& !$parent_class_storage->mutation_free
) {
if (IssueBuffer::accepts(
new MutableDependency(
$fq_class_name . ' is marked @psalm-immutable but ' . $parent_fq_class_name . ' is not',
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
)) {
// fall through
}
}
if ($codebase->store_node_types) {
$codebase->analyzer->addNodeReference(
$this->getFilePath(),
$class->extends,
$codebase->classlikes->classExists($parent_fq_class_name)
? $parent_fq_class_name
: '*' . implode('\\', $class->extends->parts)
);
}
$code_location = new CodeLocation(
$this,
$class->name ?: $class,
$class_context ? $class_context->include_location : null,
true
);
if ($storage->template_extended_count !== null) {
$this->checkTemplateParams(
$codebase,
$storage,
$parent_class_storage,
$code_location,
$storage->template_extended_count
);
}
} catch (\InvalidArgumentException $e) {
// do nothing
}
}
foreach ($class->implements as $interface_name) {
$fq_interface_name = self::getFQCLNFromNameObject(
$interface_name,
$this->source->getAliases()
);
$codebase->analyzer->addNodeReference(
$this->getFilePath(),
$interface_name,
$codebase->classlikes->interfaceExists($fq_interface_name)
? $fq_interface_name
: '*' . implode('\\', $interface_name->parts)
);
$interface_location = new CodeLocation($this, $interface_name);
if (self::checkFullyQualifiedClassLikeName(
$this,
$fq_interface_name,
$interface_location,
null,
null,
$this->getSuppressedIssues(),
false
) === false) {
continue;
}
if ($codebase->store_node_types && $fq_class_name) {
$bounds = $interface_location->getSelectionBounds();
$codebase->analyzer->addOffsetReference(
$this->getFilePath(),
$bounds[0],
$bounds[1],
$fq_interface_name
);
}
$codebase->classlikes->handleClassLikeReferenceInMigration(
$storage,
$codebase,
$this,
$interface_name,
$fq_interface_name,
null
$class_context
);
$fq_interface_name_lc = strtolower($fq_interface_name);
try {
$interface_storage = $classlike_storage_provider->get($fq_interface_name_lc);
} catch (\InvalidArgumentException $e) {
continue;
}
$code_location = new CodeLocation(
$this,
$interface_name,
$class_context ? $class_context->include_location : null,
true
);
if (!$interface_storage->is_interface) {
if (IssueBuffer::accepts(
new UndefinedInterface(
$fq_interface_name . ' is not an interface',
$code_location,
$fq_interface_name
),
$storage->suppressed_issues + $this->getSuppressedIssues()
)) {
// fall through
}
}
if (isset($storage->template_type_implements_count[$fq_interface_name_lc])) {
$expected_param_count = $storage->template_type_implements_count[$fq_interface_name_lc];
$this->checkTemplateParams(
$codebase,
$storage,
$interface_storage,
$code_location,
$expected_param_count
);
}
}
if ($storage->template_types) {
@ -525,166 +298,6 @@ class ClassAnalyzer extends ClassLikeAnalyzer
}
}
if ($storage->invalid_dependencies) {
return null;
}
$class_interfaces = $storage->class_implements;
foreach ($class_interfaces as $interface_name) {
try {
$interface_storage = $classlike_storage_provider->get($interface_name);
} catch (\InvalidArgumentException $e) {
continue;
}
$code_location = new CodeLocation(
$this,
$class->name ? $class->name : $class,
$class_context ? $class_context->include_location : null,
true
);
if ($interface_storage->deprecated) {
if (IssueBuffer::accepts(
new DeprecatedInterface(
$interface_name . ' is marked deprecated',
$code_location,
$interface_name
),
$storage->suppressed_issues + $this->getSuppressedIssues()
)) {
// fall through
}
}
if ($interface_storage->external_mutation_free
&& !$storage->external_mutation_free
) {
if (IssueBuffer::accepts(
new MissingImmutableAnnotation(
$interface_name . ' is marked @psalm-immutable, but '
. $fq_class_name . ' is not marked @psalm-immutable',
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
)) {
// fall through
}
}
foreach ($interface_storage->methods as $interface_method_name_lc => $interface_method_storage) {
if ($interface_method_storage->visibility === self::VISIBILITY_PUBLIC) {
$implementer_declaring_method_id = $codebase->methods->getDeclaringMethodId(
new \Psalm\Internal\MethodIdentifier(
$this->fq_class_name,
$interface_method_name_lc
)
);
$implementer_method_storage = null;
$implementer_classlike_storage = null;
if ($implementer_declaring_method_id) {
$implementer_fq_class_name = $implementer_declaring_method_id->fq_class_name;
$implementer_method_storage = $codebase->methods->getStorage(
$implementer_declaring_method_id
);
$implementer_classlike_storage = $classlike_storage_provider->get(
$implementer_fq_class_name
);
}
if (!$implementer_method_storage) {
if (IssueBuffer::accepts(
new UnimplementedInterfaceMethod(
'Method ' . $interface_method_name_lc . ' is not defined on class ' .
$storage->name,
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
)) {
return false;
}
return null;
}
$implementer_appearing_method_id = $codebase->methods->getAppearingMethodId(
new \Psalm\Internal\MethodIdentifier(
$this->fq_class_name,
$interface_method_name_lc
)
);
$implementer_visibility = $implementer_method_storage->visibility;
if ($implementer_appearing_method_id
&& $implementer_appearing_method_id !== $implementer_declaring_method_id
) {
$appearing_fq_class_name = $implementer_appearing_method_id->fq_class_name;
$appearing_method_name = $implementer_appearing_method_id->method_name;
$appearing_class_storage = $classlike_storage_provider->get(
$appearing_fq_class_name
);
if (isset($appearing_class_storage->trait_visibility_map[$appearing_method_name])) {
$implementer_visibility
= $appearing_class_storage->trait_visibility_map[$appearing_method_name];
}
}
if ($implementer_visibility !== self::VISIBILITY_PUBLIC) {
if (IssueBuffer::accepts(
new InaccessibleMethod(
'Interface-defined method ' . $implementer_method_storage->cased_name
. ' must be public in ' . $storage->name,
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
)) {
return false;
}
return null;
}
if ($interface_method_storage->is_static && !$implementer_method_storage->is_static) {
if (IssueBuffer::accepts(
new MethodSignatureMismatch(
'Method ' . $implementer_method_storage->cased_name
. ' should be static like '
. $storage->name . '::' . $interface_method_storage->cased_name,
$code_location
),
$implementer_method_storage->suppressed_issues
)) {
return false;
}
}
if ($storage->abstract && $implementer_method_storage === $interface_method_storage) {
continue;
}
MethodComparator::compare(
$codebase,
null,
$implementer_classlike_storage ?: $storage,
$interface_storage,
$implementer_method_storage,
$interface_method_storage,
$this->fq_class_name,
$implementer_visibility,
$code_location,
$implementer_method_storage->suppressed_issues,
false
);
}
}
}
if (!$class_context) {
$class_context = new Context($this->fq_class_name);
$class_context->parent = $parent_fq_class_name;
@ -694,6 +307,20 @@ class ClassAnalyzer extends ClassLikeAnalyzer
$class_context->strict_types = $global_context->strict_types;
}
if ($this->checkImplementedInterfaces(
$class_context,
$class,
$codebase,
$fq_class_name,
$storage
) === false) {
return false;
}
if ($storage->invalid_dependencies) {
return null;
}
if ($this->leftover_stmts) {
(new StatementsAnalyzer(
$this,
@ -2308,4 +1935,416 @@ class ClassAnalyzer extends ClassLikeAnalyzer
}
}
}
private function checkImplementedInterfaces(
Context $class_context,
Class_ $class,
Codebase $codebase,
string $fq_class_name,
ClassLikeStorage $storage
) : bool {
$classlike_storage_provider = $codebase->classlike_storage_provider;
foreach ($class->implements as $interface_name) {
$fq_interface_name = self::getFQCLNFromNameObject(
$interface_name,
$this->source->getAliases()
);
$fq_interface_name_lc = strtolower($fq_interface_name);
$codebase->analyzer->addNodeReference(
$this->getFilePath(),
$interface_name,
$codebase->classlikes->interfaceExists($fq_interface_name)
? $fq_interface_name
: '*' . implode('\\', $interface_name->parts)
);
$interface_location = new CodeLocation($this, $interface_name);
if (self::checkFullyQualifiedClassLikeName(
$this,
$fq_interface_name,
$interface_location,
null,
null,
$this->getSuppressedIssues(),
false
) === false) {
return false;
}
if ($codebase->store_node_types && $fq_class_name) {
$bounds = $interface_location->getSelectionBounds();
$codebase->analyzer->addOffsetReference(
$this->getFilePath(),
$bounds[0],
$bounds[1],
$fq_interface_name
);
}
$codebase->classlikes->handleClassLikeReferenceInMigration(
$codebase,
$this,
$interface_name,
$fq_interface_name,
null
);
try {
$interface_storage = $classlike_storage_provider->get($fq_interface_name);
} catch (\InvalidArgumentException $e) {
return false;
}
$code_location = new CodeLocation(
$this,
$interface_name,
$class_context->include_location,
true
);
if (!$interface_storage->is_interface) {
if (IssueBuffer::accepts(
new UndefinedInterface(
$fq_interface_name . ' is not an interface',
$code_location,
$fq_interface_name
),
$storage->suppressed_issues + $this->getSuppressedIssues()
)) {
// fall through
}
}
if (isset($storage->template_type_implements_count[$fq_interface_name_lc])) {
$expected_param_count = $storage->template_type_implements_count[$fq_interface_name_lc];
$this->checkTemplateParams(
$codebase,
$storage,
$interface_storage,
$code_location,
$expected_param_count
);
}
}
foreach ($storage->class_implements as $fq_interface_name_lc => $fq_interface_name) {
try {
$interface_storage = $classlike_storage_provider->get($fq_interface_name_lc);
} catch (\InvalidArgumentException $e) {
return false;
}
$code_location = new CodeLocation(
$this,
$class->name ? $class->name : $class,
$class_context->include_location,
true
);
if ($interface_storage->deprecated) {
if (IssueBuffer::accepts(
new DeprecatedInterface(
$fq_interface_name . ' is marked deprecated',
$code_location,
$fq_interface_name
),
$storage->suppressed_issues + $this->getSuppressedIssues()
)) {
// fall through
}
}
if ($interface_storage->external_mutation_free
&& !$storage->external_mutation_free
) {
if (IssueBuffer::accepts(
new MissingImmutableAnnotation(
$fq_interface_name . ' is marked @psalm-immutable, but '
. $fq_class_name . ' is not marked @psalm-immutable',
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
)) {
// fall through
}
}
foreach ($interface_storage->methods as $interface_method_name_lc => $interface_method_storage) {
if ($interface_method_storage->visibility === self::VISIBILITY_PUBLIC) {
$implementer_declaring_method_id = $codebase->methods->getDeclaringMethodId(
new \Psalm\Internal\MethodIdentifier(
$this->fq_class_name,
$interface_method_name_lc
)
);
$implementer_method_storage = null;
$implementer_classlike_storage = null;
if ($implementer_declaring_method_id) {
$implementer_fq_class_name = $implementer_declaring_method_id->fq_class_name;
$implementer_method_storage = $codebase->methods->getStorage(
$implementer_declaring_method_id
);
$implementer_classlike_storage = $classlike_storage_provider->get(
$implementer_fq_class_name
);
}
if (!$implementer_method_storage) {
IssueBuffer::accepts(
new UnimplementedInterfaceMethod(
'Method ' . $interface_method_name_lc . ' is not defined on class ' .
$storage->name,
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
);
return true;
}
$implementer_appearing_method_id = $codebase->methods->getAppearingMethodId(
new \Psalm\Internal\MethodIdentifier(
$this->fq_class_name,
$interface_method_name_lc
)
);
$implementer_visibility = $implementer_method_storage->visibility;
if ($implementer_appearing_method_id
&& $implementer_appearing_method_id !== $implementer_declaring_method_id
) {
$appearing_fq_class_name = $implementer_appearing_method_id->fq_class_name;
$appearing_method_name = $implementer_appearing_method_id->method_name;
$appearing_class_storage = $classlike_storage_provider->get(
$appearing_fq_class_name
);
if (isset($appearing_class_storage->trait_visibility_map[$appearing_method_name])) {
$implementer_visibility
= $appearing_class_storage->trait_visibility_map[$appearing_method_name];
}
}
if ($implementer_visibility !== self::VISIBILITY_PUBLIC) {
IssueBuffer::accepts(
new InaccessibleMethod(
'Interface-defined method ' . $implementer_method_storage->cased_name
. ' must be public in ' . $storage->name,
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
);
return true;
}
if ($interface_method_storage->is_static && !$implementer_method_storage->is_static) {
IssueBuffer::accepts(
new MethodSignatureMismatch(
'Method ' . $implementer_method_storage->cased_name
. ' should be static like '
. $storage->name . '::' . $interface_method_storage->cased_name,
$code_location
),
$implementer_method_storage->suppressed_issues
);
return true;
}
if ($storage->abstract && $implementer_method_storage === $interface_method_storage) {
continue;
}
MethodComparator::compare(
$codebase,
null,
$implementer_classlike_storage ?: $storage,
$interface_storage,
$implementer_method_storage,
$interface_method_storage,
$this->fq_class_name,
$implementer_visibility,
$code_location,
$implementer_method_storage->suppressed_issues,
false
);
}
}
}
return true;
}
private function checkParentClass(
Class_ $class,
PhpParser\Node\Name $extended_class,
string $fq_class_name,
string $parent_fq_class_name,
ClassLikeStorage $storage,
Codebase $codebase,
?Context $class_context
) : void {
$classlike_storage_provider = $codebase->classlike_storage_provider;
if (!$parent_fq_class_name) {
throw new \UnexpectedValueException('Parent class should be filled in for ' . $fq_class_name);
}
$parent_reference_location = new CodeLocation($this, $extended_class);
if (self::checkFullyQualifiedClassLikeName(
$this->getSource(),
$parent_fq_class_name,
$parent_reference_location,
null,
null,
$storage->suppressed_issues + $this->getSuppressedIssues(),
false
) === false) {
return;
}
if ($codebase->alter_code && $codebase->classes_to_move) {
$codebase->classlikes->handleClassLikeReferenceInMigration(
$codebase,
$this,
$extended_class,
$parent_fq_class_name,
null
);
}
try {
$parent_class_storage = $classlike_storage_provider->get($parent_fq_class_name);
$code_location = new CodeLocation(
$this,
$extended_class,
$class_context ? $class_context->include_location : null,
true
);
if ($parent_class_storage->is_trait || $parent_class_storage->is_interface) {
if (IssueBuffer::accepts(
new UndefinedClass(
$parent_fq_class_name . ' is not a class',
$code_location,
$parent_fq_class_name . ' as class'
),
$storage->suppressed_issues + $this->getSuppressedIssues()
)) {
// fall through
}
}
if ($parent_class_storage->final) {
if (IssueBuffer::accepts(
new InvalidExtendClass(
'Class ' . $fq_class_name . ' may not inherit from final class ' . $parent_fq_class_name,
$code_location,
$fq_class_name
),
$storage->suppressed_issues + $this->getSuppressedIssues()
)) {
// fall through
}
}
if ($parent_class_storage->deprecated) {
if (IssueBuffer::accepts(
new DeprecatedClass(
$parent_fq_class_name . ' is marked deprecated',
$code_location,
$parent_fq_class_name
),
$storage->suppressed_issues + $this->getSuppressedIssues()
)) {
// fall through
}
}
if (!NamespaceAnalyzer::isWithin($fq_class_name, $parent_class_storage->internal)) {
if (IssueBuffer::accepts(
new InternalClass(
$parent_fq_class_name . ' is internal to ' . $parent_class_storage->internal
. ' but called from ' . $fq_class_name,
$code_location,
$parent_fq_class_name
),
$storage->suppressed_issues + $this->getSuppressedIssues()
)) {
// fall through
}
}
if ($parent_class_storage->external_mutation_free
&& !$storage->external_mutation_free
) {
if (IssueBuffer::accepts(
new MissingImmutableAnnotation(
$parent_fq_class_name . ' is marked @psalm-immutable, but '
. $fq_class_name . ' is not marked @psalm-immutable',
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
)) {
// fall through
}
}
if ($storage->mutation_free
&& !$parent_class_storage->mutation_free
) {
if (IssueBuffer::accepts(
new MutableDependency(
$fq_class_name . ' is marked @psalm-immutable but ' . $parent_fq_class_name . ' is not',
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
)) {
// fall through
}
}
if ($codebase->store_node_types) {
$codebase->analyzer->addNodeReference(
$this->getFilePath(),
$extended_class,
$codebase->classlikes->classExists($parent_fq_class_name)
? $parent_fq_class_name
: '*' . implode('\\', $extended_class->parts)
);
}
$code_location = new CodeLocation(
$this,
$class->name ?: $class,
$class_context ? $class_context->include_location : null,
true
);
if ($storage->template_extended_count !== null) {
$this->checkTemplateParams(
$codebase,
$storage,
$parent_class_storage,
$code_location,
$storage->template_extended_count
);
}
} catch (\InvalidArgumentException $e) {
// do nothing
}
}
}

View File

@ -663,7 +663,7 @@ class UnusedCodeTest extends TestCase
/** @psalm-suppress UnimplementedInterfaceMethod */
class IterableResult implements \Iterator {
public function current() {
return $this->current;
return null;
}
public function key() {