mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Break out parent and implemented class checks
This commit is contained in:
parent
affaa5ea85
commit
8696873cb9
@ -2,6 +2,7 @@
|
|||||||
namespace Psalm\Internal\Analyzer;
|
namespace Psalm\Internal\Analyzer;
|
||||||
|
|
||||||
use PhpParser;
|
use PhpParser;
|
||||||
|
use PhpParser\Node\Stmt\Class_;
|
||||||
use Psalm\Aliases;
|
use Psalm\Aliases;
|
||||||
use Psalm\DocComment;
|
use Psalm\DocComment;
|
||||||
use Psalm\Exception\DocblockParseException;
|
use Psalm\Exception\DocblockParseException;
|
||||||
@ -217,244 +218,16 @@ class ClassAnalyzer extends ClassLikeAnalyzer
|
|||||||
|
|
||||||
$parent_fq_class_name = $this->parent_fq_class_name;
|
$parent_fq_class_name = $this->parent_fq_class_name;
|
||||||
|
|
||||||
if ($class->extends) {
|
if ($class->extends && $parent_fq_class_name) {
|
||||||
if (!$parent_fq_class_name) {
|
$this->checkParentClass(
|
||||||
throw new \UnexpectedValueException('Parent class should be filled in for ' . $fq_class_name);
|
$class,
|
||||||
}
|
$class->extends,
|
||||||
|
$fq_class_name,
|
||||||
$parent_reference_location = new CodeLocation($this, $class->extends);
|
|
||||||
|
|
||||||
if (self::checkFullyQualifiedClassLikeName(
|
|
||||||
$this->getSource(),
|
|
||||||
$parent_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,
|
$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(
|
|
||||||
$codebase,
|
$codebase,
|
||||||
$this,
|
$class_context
|
||||||
$interface_name,
|
|
||||||
$fq_interface_name,
|
|
||||||
null
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$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) {
|
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) {
|
if (!$class_context) {
|
||||||
$class_context = new Context($this->fq_class_name);
|
$class_context = new Context($this->fq_class_name);
|
||||||
$class_context->parent = $parent_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;
|
$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) {
|
if ($this->leftover_stmts) {
|
||||||
(new StatementsAnalyzer(
|
(new StatementsAnalyzer(
|
||||||
$this,
|
$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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -663,7 +663,7 @@ class UnusedCodeTest extends TestCase
|
|||||||
/** @psalm-suppress UnimplementedInterfaceMethod */
|
/** @psalm-suppress UnimplementedInterfaceMethod */
|
||||||
class IterableResult implements \Iterator {
|
class IterableResult implements \Iterator {
|
||||||
public function current() {
|
public function current() {
|
||||||
return $this->current;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function key() {
|
public function key() {
|
||||||
|
Loading…
Reference in New Issue
Block a user