mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Add support for class constant visibility
This commit is contained in:
parent
48ba91a05b
commit
7cb6891a0b
@ -65,6 +65,8 @@ class ClassChecker extends ClassLikeChecker
|
||||
$this->namespace,
|
||||
$this->aliased_classes
|
||||
);
|
||||
|
||||
self::$class_extends[$this->fq_class_name][$this->parent_class] = true;
|
||||
}
|
||||
|
||||
foreach ($class->implements as $interface_name) {
|
||||
|
@ -146,6 +146,20 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
*/
|
||||
protected static $public_class_constants = [];
|
||||
|
||||
/**
|
||||
* A lookup table for protected class constants
|
||||
*
|
||||
* @var array<string,array<string,Type\Union>>
|
||||
*/
|
||||
protected static $protected_class_constants = [];
|
||||
|
||||
/**
|
||||
* A lookup table for private class constants
|
||||
*
|
||||
* @var array<string,array<string,Type\Union>>
|
||||
*/
|
||||
protected static $private_class_constants = [];
|
||||
|
||||
/**
|
||||
* A lookup table to record which classes have been scanned
|
||||
*
|
||||
@ -256,6 +270,8 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
self::$private_static_class_properties[$this->fq_class_name] = [];
|
||||
|
||||
self::$public_class_constants[$this->fq_class_name] = [];
|
||||
self::$protected_class_constants[$this->fq_class_name] = [];
|
||||
self::$private_class_constants[$this->fq_class_name] = [];
|
||||
|
||||
if (!$class_context) {
|
||||
$class_context = new Context($this->file_name, $this->fq_class_name);
|
||||
@ -267,7 +283,13 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
foreach ($this->class->stmts as $stmt) {
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\ClassConst) {
|
||||
foreach ($stmt->consts as $const) {
|
||||
self::$public_class_constants[$class_context->self][$const->name] = Type::getMixed();
|
||||
if ($stmt->isProtected()) {
|
||||
self::$protected_class_constants[$class_context->self][$const->name] = Type::getMixed();
|
||||
} elseif ($stmt->isPrivate()) {
|
||||
self::$private_class_constants[$class_context->self][$const->name] = Type::getMixed();
|
||||
} else {
|
||||
self::$public_class_constants[$class_context->self][$const->name] = Type::getMixed();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -469,10 +491,10 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
self::$protected_static_class_properties[$this->fq_class_name] =
|
||||
self::$protected_static_class_properties[$parent_class];
|
||||
|
||||
self::$public_class_constants[$this->fq_class_name] = array_merge(
|
||||
self::$public_class_constants[$parent_class],
|
||||
self::$public_class_constants[$this->fq_class_name]
|
||||
);
|
||||
self::$public_class_constants[$this->fq_class_name] = self::$public_class_constants[$parent_class];
|
||||
|
||||
self::$protected_class_constants[$this->fq_class_name] =
|
||||
self::$protected_class_constants[$parent_class];
|
||||
|
||||
return null;
|
||||
}
|
||||
@ -683,7 +705,13 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
$const_type = $type_in_comment ? $type_in_comment : Type::getMixed();
|
||||
|
||||
foreach ($stmt->consts as $const) {
|
||||
self::$public_class_constants[$class_context->self][$const->name] = $const_type;
|
||||
if ($stmt->isProtected()) {
|
||||
self::$protected_class_constants[$class_context->self][$const->name] = $const_type;
|
||||
} elseif ($stmt->isPrivate()) {
|
||||
self::$private_class_constants[$class_context->self][$const->name] = $const_type;
|
||||
} else {
|
||||
self::$public_class_constants[$class_context->self][$const->name] = $const_type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1063,6 +1091,8 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
}
|
||||
|
||||
self::$public_class_constants[$class_name] = [];
|
||||
self::$private_class_constants[$class_name] = [];
|
||||
self::$protected_class_constants[$class_name] = [];
|
||||
|
||||
foreach ($class_constants as $name => $value) {
|
||||
self::$public_class_constants[$class_name][$name] = self::getTypeFromValue($value);
|
||||
@ -1228,9 +1258,6 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
*/
|
||||
public static function getConstantsForClass($class_name, $visibility)
|
||||
{
|
||||
// remove for PHP 7.1 support
|
||||
$visibility = ReflectionProperty::IS_PUBLIC;
|
||||
|
||||
if (self::registerClass($class_name) === false) {
|
||||
return [];
|
||||
}
|
||||
@ -1239,18 +1266,40 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
return self::$public_class_constants[$class_name];
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException('Given $visibility not supported');
|
||||
if ($visibility === ReflectionProperty::IS_PROTECTED) {
|
||||
return array_merge(
|
||||
self::$public_class_constants[$class_name],
|
||||
self::$protected_class_constants[$class_name]
|
||||
);
|
||||
}
|
||||
|
||||
if ($visibility === ReflectionProperty::IS_PRIVATE) {
|
||||
return array_merge(
|
||||
self::$public_class_constants[$class_name],
|
||||
self::$protected_class_constants[$class_name],
|
||||
self::$private_class_constants[$class_name]
|
||||
);
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException('Must specify $visibility');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $class_name
|
||||
* @param string $const_name
|
||||
* @param Type\Union $type
|
||||
* @param int $visibility
|
||||
* @return void
|
||||
*/
|
||||
public static function setConstantType($class_name, $const_name, Type\Union $type)
|
||||
public static function setConstantType($class_name, $const_name, Type\Union $type, $visibility)
|
||||
{
|
||||
self::$public_class_constants[$class_name][$const_name] = $type;
|
||||
if ($visibility === ReflectionProperty::IS_PUBLIC) {
|
||||
self::$public_class_constants[$class_name][$const_name] = $type;
|
||||
} elseif ($visibility === ReflectionProperty::IS_PROTECTED) {
|
||||
self::$protected_class_constants[$class_name][$const_name] = $type;
|
||||
} elseif ($visibility === ReflectionProperty::IS_PRIVATE) {
|
||||
self::$private_class_constants[$class_name][$const_name] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1360,6 +1409,8 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
self::$private_static_class_properties = [];
|
||||
|
||||
self::$public_class_constants = [];
|
||||
self::$protected_class_constants = [];
|
||||
self::$private_class_constants = [];
|
||||
|
||||
self::$registered_classes = [];
|
||||
|
||||
|
@ -15,7 +15,7 @@ use Psalm\Issue\FailedTypeResolution;
|
||||
use Psalm\Issue\InvalidArrayAssignment;
|
||||
use Psalm\Issue\InvalidPropertyAssignment;
|
||||
use Psalm\Issue\InvalidScope;
|
||||
use Psalm\Issue\InvisibleProperty;
|
||||
use Psalm\Issue\InaccessibleProperty;
|
||||
use Psalm\Issue\MissingPropertyDeclaration;
|
||||
use Psalm\Issue\MissingPropertyType;
|
||||
use Psalm\Issue\MixedPropertyAssignment;
|
||||
@ -568,7 +568,7 @@ class AssignmentChecker
|
||||
|
||||
if (isset($all_class_properties[$prop_name])) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvisibleProperty(
|
||||
new InaccessibleProperty(
|
||||
'Static property ' . $var_id . ' is not visible in this context',
|
||||
new CodeLocation($statements_checker->getSource(), $stmt)
|
||||
),
|
||||
|
@ -14,7 +14,8 @@ use Psalm\Context;
|
||||
use Psalm\Issue\InvalidArrayAccess;
|
||||
use Psalm\Issue\InvalidArrayAssignment;
|
||||
use Psalm\Issue\InvalidPropertyFetch;
|
||||
use Psalm\Issue\InvisibleProperty;
|
||||
use Psalm\Issue\InaccessibleClassConstant;
|
||||
use Psalm\Issue\InaccessibleProperty;
|
||||
use Psalm\Issue\MissingPropertyType;
|
||||
use Psalm\Issue\MixedArrayAccess;
|
||||
use Psalm\Issue\MixedArrayOffset;
|
||||
@ -393,18 +394,48 @@ class FetchChecker
|
||||
return null;
|
||||
}
|
||||
|
||||
$class_constants = ClassLikeChecker::getConstantsForClass($fq_class_name, \ReflectionProperty::IS_PUBLIC);
|
||||
if ($fq_class_name === $context->self
|
||||
|| (
|
||||
$statements_checker->getSource()->getSource() instanceof TraitChecker &&
|
||||
$fq_class_name === $statements_checker->getSource()->getFQCLN()
|
||||
)
|
||||
) {
|
||||
$class_visibility = \ReflectionProperty::IS_PRIVATE;
|
||||
} elseif ($context->self && ClassChecker::classExtends($context->self, $fq_class_name)) {
|
||||
$class_visibility = \ReflectionProperty::IS_PROTECTED;
|
||||
} else {
|
||||
$class_visibility = \ReflectionProperty::IS_PUBLIC;
|
||||
}
|
||||
|
||||
$class_constants = ClassLikeChecker::getConstantsForClass($fq_class_name, $class_visibility);
|
||||
|
||||
if (!isset($class_constants[$stmt->name])) {
|
||||
if (IssueBuffer::accepts(
|
||||
new UndefinedConstant(
|
||||
'Const ' . $const_id . ' is not defined',
|
||||
new CodeLocation($statements_checker->getSource(), $stmt)
|
||||
),
|
||||
$statements_checker->getSuppressedIssues()
|
||||
)) {
|
||||
return false;
|
||||
$all_class_constants = [];
|
||||
|
||||
if ($fq_class_name !== $context->self) {
|
||||
$all_class_constants = ClassLikeChecker::getConstantsForClass(
|
||||
$fq_class_name,
|
||||
\ReflectionProperty::IS_PRIVATE
|
||||
);
|
||||
}
|
||||
|
||||
if ($all_class_constants && isset($all_class_constants[$stmt->name])) {
|
||||
IssueBuffer::add(
|
||||
new InaccessibleClassConstant(
|
||||
'Constant ' . $const_id . ' is not visible in this context',
|
||||
new CodeLocation($statements_checker->getSource(), $stmt)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
IssueBuffer::add(
|
||||
new UndefinedConstant(
|
||||
'Constant ' . $const_id . ' is not defined',
|
||||
new CodeLocation($statements_checker->getSource(), $stmt)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
} else {
|
||||
$stmt->inferredType = $class_constants[$stmt->name];
|
||||
}
|
||||
@ -540,7 +571,7 @@ class FetchChecker
|
||||
|
||||
if ($all_class_properties && isset($all_class_properties[$stmt->name])) {
|
||||
IssueBuffer::add(
|
||||
new InvisibleProperty(
|
||||
new InaccessibleProperty(
|
||||
'Static property ' . $var_id . ' is not visible in this context',
|
||||
new CodeLocation($statements_checker->getSource(), $stmt)
|
||||
)
|
||||
|
@ -359,6 +359,16 @@ class StatementsChecker
|
||||
}
|
||||
}
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\ClassConst) {
|
||||
$const_visibility = \ReflectionProperty::IS_PUBLIC;
|
||||
|
||||
if ($stmt->isProtected()) {
|
||||
$const_visibility = \ReflectionProperty::IS_PROTECTED;
|
||||
}
|
||||
|
||||
if ($stmt->isPrivate()) {
|
||||
$const_visibility = \ReflectionProperty::IS_PRIVATE;
|
||||
}
|
||||
|
||||
foreach ($stmt->consts as $const) {
|
||||
ExpressionChecker::check($this, $const->value, $context);
|
||||
|
||||
@ -366,7 +376,8 @@ class StatementsChecker
|
||||
ClassLikeChecker::setConstantType(
|
||||
$this->fq_class_name,
|
||||
$const->name,
|
||||
$const->value->inferredType
|
||||
$const->value->inferredType,
|
||||
$const_visibility
|
||||
);
|
||||
}
|
||||
}
|
||||
|
6
src/Psalm/Issue/InaccessibleClassConstant.php
Normal file
6
src/Psalm/Issue/InaccessibleClassConstant.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Psalm\Issue;
|
||||
|
||||
class InaccessibleClassConstant extends CodeError
|
||||
{
|
||||
}
|
6
src/Psalm/Issue/InaccessibleProperty.php
Normal file
6
src/Psalm/Issue/InaccessibleProperty.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Psalm\Issue;
|
||||
|
||||
class InaccessibleProperty extends CodeError
|
||||
{
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
<?php
|
||||
namespace Psalm\Issue;
|
||||
|
||||
class InvisibleProperty extends CodeError
|
||||
{
|
||||
}
|
@ -58,4 +58,134 @@ class Php71Test extends PHPUnit_Framework_TestCase
|
||||
$context = new Context('somefile.php');
|
||||
$file_checker->check(true, true, $context);
|
||||
}
|
||||
|
||||
public function testPrivateClassConst()
|
||||
{
|
||||
$stmts = self::$parser->parse('<?php
|
||||
class A
|
||||
{
|
||||
private const IS_PRIVATE = 1;
|
||||
|
||||
function foo() : int {
|
||||
return A::IS_PRIVATE;
|
||||
}
|
||||
}
|
||||
');
|
||||
|
||||
$file_checker = new FileChecker('somefile.php', $stmts);
|
||||
$context = new Context('somefile.php');
|
||||
$file_checker->check(true, true, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Psalm\Exception\CodeException
|
||||
* @expectedExceptionMessage InaccessibleClassConstant
|
||||
*/
|
||||
public function testInvalidPrivateClassConstFetch()
|
||||
{
|
||||
$stmts = self::$parser->parse('<?php
|
||||
class A
|
||||
{
|
||||
private const IS_PRIVATE = 1;
|
||||
}
|
||||
|
||||
echo A::IS_PRIVATE;
|
||||
');
|
||||
|
||||
$file_checker = new FileChecker('somefile.php', $stmts);
|
||||
$context = new Context('somefile.php');
|
||||
$file_checker->check(true, true, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Psalm\Exception\CodeException
|
||||
* @expectedExceptionMessage InaccessibleClassConstant
|
||||
*/
|
||||
public function testInvalidPrivateClassConstFetchFromSubclass()
|
||||
{
|
||||
$stmts = self::$parser->parse('<?php
|
||||
class A
|
||||
{
|
||||
private const IS_PRIVATE = 1;
|
||||
}
|
||||
|
||||
class B extends A
|
||||
{
|
||||
function foo() : int {
|
||||
return A::IS_PRIVATE;
|
||||
}
|
||||
}
|
||||
');
|
||||
|
||||
$file_checker = new FileChecker('somefile.php', $stmts);
|
||||
$context = new Context('somefile.php');
|
||||
$file_checker->check(true, true, $context);
|
||||
}
|
||||
|
||||
public function testProtectedClassConst()
|
||||
{
|
||||
$stmts = self::$parser->parse('<?php
|
||||
class A
|
||||
{
|
||||
protected const IS_PROTECTED = 1;
|
||||
}
|
||||
|
||||
class B extends A
|
||||
{
|
||||
function foo() : int {
|
||||
return A::IS_PROTECTED;
|
||||
}
|
||||
}
|
||||
');
|
||||
|
||||
$file_checker = new FileChecker('somefile.php', $stmts);
|
||||
$context = new Context('somefile.php');
|
||||
$file_checker->check(true, true, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Psalm\Exception\CodeException
|
||||
* @expectedExceptionMessage InaccessibleClassConstant
|
||||
*/
|
||||
public function testInvalidProtectedClassConstFetch()
|
||||
{
|
||||
$stmts = self::$parser->parse('<?php
|
||||
class A
|
||||
{
|
||||
protected const IS_PROTECTED = 1;
|
||||
}
|
||||
|
||||
echo A::IS_PROTECTED;
|
||||
');
|
||||
|
||||
$file_checker = new FileChecker('somefile.php', $stmts);
|
||||
$context = new Context('somefile.php');
|
||||
$file_checker->check(true, true, $context);
|
||||
}
|
||||
|
||||
public function testPublicClassConstFetch()
|
||||
{
|
||||
$stmts = self::$parser->parse('<?php
|
||||
class A
|
||||
{
|
||||
public const IS_PUBLIC = 1;
|
||||
const IS_ALSO_PUBLIC = 2;
|
||||
}
|
||||
|
||||
class B extends A
|
||||
{
|
||||
function foo() : int {
|
||||
echo A::IS_PUBLIC;
|
||||
return A::IS_ALSO_PUBLIC;
|
||||
}
|
||||
}
|
||||
|
||||
echo A::IS_PUBLIC;
|
||||
echo A::IS_ALSO_PUBLIC;
|
||||
');
|
||||
|
||||
$file_checker = new FileChecker('somefile.php', $stmts);
|
||||
$context = new Context('somefile.php');
|
||||
$file_checker->check(true, true, $context);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user