1
0
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:
Matthew Brown 2016-12-04 01:44:33 -05:00
parent 48ba91a05b
commit 7cb6891a0b
9 changed files with 263 additions and 32 deletions

View File

@ -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) {

View File

@ -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 = [];

View File

@ -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)
),

View File

@ -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)
)

View File

@ -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
);
}
}

View File

@ -0,0 +1,6 @@
<?php
namespace Psalm\Issue;
class InaccessibleClassConstant extends CodeError
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Psalm\Issue;
class InaccessibleProperty extends CodeError
{
}

View File

@ -1,6 +0,0 @@
<?php
namespace Psalm\Issue;
class InvisibleProperty extends CodeError
{
}

View File

@ -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);
}
}