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

Add support for $a::class

This commit is contained in:
Matthew Brown 2019-01-05 15:12:42 -05:00
parent f5378bdca8
commit 8024b4e275
2 changed files with 209 additions and 137 deletions

View File

@ -90,134 +90,158 @@ class ConstFetchAnalyzer
) {
$codebase = $statements_analyzer->getCodebase();
if (($context->check_consts
|| ($stmt->name instanceof PhpParser\Node\Identifier && $stmt->name->name === 'class'))
&& $stmt->class instanceof PhpParser\Node\Name
) {
$first_part_lc = strtolower($stmt->class->parts[0]);
if ($stmt->class instanceof PhpParser\Node\Name) {
if ($context->check_consts
|| ($stmt->name instanceof PhpParser\Node\Identifier && $stmt->name->name === 'class')
) {
$first_part_lc = strtolower($stmt->class->parts[0]);
if ($first_part_lc === 'self' || $first_part_lc === 'static') {
if (!$context->self) {
throw new \UnexpectedValueException('$context->self cannot be null');
}
$fq_class_name = (string)$context->self;
} elseif ($first_part_lc === 'parent') {
$fq_class_name = $statements_analyzer->getParentFQCLN();
if ($fq_class_name === null) {
if (IssueBuffer::accepts(
new ParentNotFound(
'Cannot check property fetch on parent as this class does not extend another',
new CodeLocation($statements_analyzer->getSource(), $stmt)
),
$statements_analyzer->getSuppressedIssues()
)) {
return false;
if ($first_part_lc === 'self' || $first_part_lc === 'static') {
if (!$context->self) {
throw new \UnexpectedValueException('$context->self cannot be null');
}
return;
}
} else {
$fq_class_name = ClassLikeAnalyzer::getFQCLNFromNameObject(
$stmt->class,
$statements_analyzer->getAliases()
);
$fq_class_name = (string)$context->self;
} elseif ($first_part_lc === 'parent') {
$fq_class_name = $statements_analyzer->getParentFQCLN();
if ($stmt->name instanceof PhpParser\Node\Identifier) {
if (!$context->inside_class_exists || $stmt->name->name !== 'class') {
if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
$statements_analyzer,
$fq_class_name,
new CodeLocation($statements_analyzer->getSource(), $stmt->class),
$statements_analyzer->getSuppressedIssues(),
false
) === false) {
if ($fq_class_name === null) {
if (IssueBuffer::accepts(
new ParentNotFound(
'Cannot check property fetch on parent as this class does not extend another',
new CodeLocation($statements_analyzer->getSource(), $stmt)
),
$statements_analyzer->getSuppressedIssues()
)) {
return false;
}
return;
}
} else {
$fq_class_name = ClassLikeAnalyzer::getFQCLNFromNameObject(
$stmt->class,
$statements_analyzer->getAliases()
);
if ($stmt->name instanceof PhpParser\Node\Identifier) {
if (!$context->inside_class_exists || $stmt->name->name !== 'class') {
if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
$statements_analyzer,
$fq_class_name,
new CodeLocation($statements_analyzer->getSource(), $stmt->class),
$statements_analyzer->getSuppressedIssues(),
false
) === false) {
return false;
}
}
}
}
}
if ($stmt->name instanceof PhpParser\Node\Identifier && $stmt->name->name === 'class') {
$stmt->inferredType = Type::getLiteralClassString($fq_class_name);
if ($stmt->name instanceof PhpParser\Node\Identifier && $stmt->name->name === 'class') {
$stmt->inferredType = Type::getLiteralClassString($fq_class_name);
return null;
}
return null;
}
// if we're ignoring that the class doesn't exist, exit anyway
if (!$codebase->classOrInterfaceExists($fq_class_name)) {
$stmt->inferredType = Type::getMixed();
// if we're ignoring that the class doesn't exist, exit anyway
if (!$codebase->classOrInterfaceExists($fq_class_name)) {
$stmt->inferredType = Type::getMixed();
return null;
}
return null;
}
if ($codebase->server_mode) {
$codebase->analyzer->addNodeReference(
$statements_analyzer->getFilePath(),
$stmt->class,
$fq_class_name
);
}
if (!$stmt->name instanceof PhpParser\Node\Identifier) {
return null;
}
$const_id = $fq_class_name . '::' . $stmt->name;
if ($codebase->server_mode) {
$codebase->analyzer->addNodeReference(
$statements_analyzer->getFilePath(),
$stmt->name,
$const_id
);
}
if ($fq_class_name === $context->self
|| (
$statements_analyzer->getSource()->getSource() instanceof TraitAnalyzer &&
$fq_class_name === $statements_analyzer->getSource()->getFQCLN()
)
) {
$class_visibility = \ReflectionProperty::IS_PRIVATE;
} elseif ($context->self &&
$codebase->classExtends($context->self, $fq_class_name)
) {
$class_visibility = \ReflectionProperty::IS_PROTECTED;
} else {
$class_visibility = \ReflectionProperty::IS_PUBLIC;
}
$class_constants = $codebase->classlikes->getConstantsForClass(
$fq_class_name,
$class_visibility
);
if (!isset($class_constants[$stmt->name->name]) && $first_part_lc !== 'static') {
$all_class_constants = [];
if ($fq_class_name !== $context->self) {
$all_class_constants = $codebase->classlikes->getConstantsForClass(
$fq_class_name,
\ReflectionProperty::IS_PRIVATE
if ($codebase->server_mode) {
$codebase->analyzer->addNodeReference(
$statements_analyzer->getFilePath(),
$stmt->class,
$fq_class_name
);
}
if ($all_class_constants && isset($all_class_constants[$stmt->name->name])) {
if (IssueBuffer::accepts(
new InaccessibleClassConstant(
'Constant ' . $const_id . ' is not visible in this context',
new CodeLocation($statements_analyzer->getSource(), $stmt)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
if (!$stmt->name instanceof PhpParser\Node\Identifier) {
return null;
}
$const_id = $fq_class_name . '::' . $stmt->name;
if ($codebase->server_mode) {
$codebase->analyzer->addNodeReference(
$statements_analyzer->getFilePath(),
$stmt->name,
$const_id
);
}
if ($fq_class_name === $context->self
|| (
$statements_analyzer->getSource()->getSource() instanceof TraitAnalyzer &&
$fq_class_name === $statements_analyzer->getSource()->getFQCLN()
)
) {
$class_visibility = \ReflectionProperty::IS_PRIVATE;
} elseif ($context->self &&
$codebase->classExtends($context->self, $fq_class_name)
) {
$class_visibility = \ReflectionProperty::IS_PROTECTED;
} else {
$class_visibility = \ReflectionProperty::IS_PUBLIC;
}
$class_constants = $codebase->classlikes->getConstantsForClass(
$fq_class_name,
$class_visibility
);
if (!isset($class_constants[$stmt->name->name]) && $first_part_lc !== 'static') {
$all_class_constants = [];
if ($fq_class_name !== $context->self) {
$all_class_constants = $codebase->classlikes->getConstantsForClass(
$fq_class_name,
\ReflectionProperty::IS_PRIVATE
);
}
if ($all_class_constants && isset($all_class_constants[$stmt->name->name])) {
if (IssueBuffer::accepts(
new InaccessibleClassConstant(
'Constant ' . $const_id . ' is not visible in this context',
new CodeLocation($statements_analyzer->getSource(), $stmt)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} else {
if (IssueBuffer::accepts(
new UndefinedConstant(
'Constant ' . $const_id . ' is not defined',
new CodeLocation($statements_analyzer->getSource(), $stmt)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
return false;
}
if ($context->calling_method_id) {
$codebase->file_reference_provider->addReferenceToClassMethod(
$context->calling_method_id,
strtolower($fq_class_name) . '::' . $stmt->name->name
);
}
$class_const_storage = $codebase->classlike_storage_provider->get($fq_class_name);
if (isset($class_const_storage->deprecated_constants[$stmt->name->name])) {
if (IssueBuffer::accepts(
new UndefinedConstant(
'Constant ' . $const_id . ' is not defined',
new DeprecatedConstant(
'Constant ' . $const_id . ' is deprecated',
new CodeLocation($statements_analyzer->getSource(), $stmt)
),
$statements_analyzer->getSuppressedIssues()
@ -226,37 +250,23 @@ class ConstFetchAnalyzer
}
}
return false;
}
if ($context->calling_method_id) {
$codebase->file_reference_provider->addReferenceToClassMethod(
$context->calling_method_id,
strtolower($fq_class_name) . '::' . $stmt->name->name
);
}
$class_const_storage = $codebase->classlike_storage_provider->get($fq_class_name);
if (isset($class_const_storage->deprecated_constants[$stmt->name->name])) {
if (IssueBuffer::accepts(
new DeprecatedConstant(
'Constant ' . $const_id . ' is deprecated',
new CodeLocation($statements_analyzer->getSource(), $stmt)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
if (isset($class_constants[$stmt->name->name]) && $first_part_lc !== 'static') {
$stmt->inferredType = clone $class_constants[$stmt->name->name];
} else {
$stmt->inferredType = Type::getMixed();
}
}
if (isset($class_constants[$stmt->name->name]) && $first_part_lc !== 'static') {
$stmt->inferredType = clone $class_constants[$stmt->name->name];
} else {
$stmt->inferredType = Type::getMixed();
return null;
}
} elseif ($stmt->name instanceof PhpParser\Node\Identifier && $stmt->name->name === 'class') {
ExpressionAnalyzer::analyze($statements_analyzer, $stmt->class, $context);
$lhs_type = $stmt->class->inferredType;
return null;
$stmt->inferredType = new Type\Union([
new Type\Atomic\TClassString('object', $lhs_type)
]);
return;
}
$stmt->inferredType = Type::getMixed();

View File

@ -311,6 +311,47 @@ class ClassStringTest extends TestCase
foo(AChild::class);',
],
'returnClassConstantClassStringParameterized' => [
'<?php
class A {}
/**
* @return class-string<A> $s
*/
function foo(A $a) : string {
return $a::class;
}',
],
'returnGetClassClassStringParameterized' => [
'<?php
class A {}
/**
* @return class-string<A> $s
*/
function foo(A $a) : string {
return get_class($a);
}',
],
'createClassOfTypeFromString' => [
'<?php
class A {}
/**
* @return class-string<A> $s
*/
function foo(string $s) : string {
if (!class_exists($s)) {
throw new \UnexpectedValueException("bad");
}
if (!is_a($s, A::class, true)) {
throw new \UnexpectedValueException("bad");
}
return $s;
}',
],
];
}
@ -428,6 +469,27 @@ class ClassStringTest extends TestCase
foo(AChild::class);',
'error_message' => 'InvalidArgument',
],
'createClassOfWrongTypeFromString' => [
'<?php
class A {}
class B {}
/**
* @return class-string<A> $s
*/
function foo(string $s) : string {
if (!class_exists($s)) {
throw new \UnexpectedValueException("bad");
}
if (!is_a($s, B::class, true)) {
throw new \UnexpectedValueException("bad");
}
return $s;
}',
'error_message' => 'InvalidReturnStatement',
],
];
}
}