Skip to content

Commit

Permalink
Fix #1551 - do better at inferring class constant types
Browse files Browse the repository at this point in the history
  • Loading branch information
muglug committed Sep 14, 2019
1 parent d941294 commit 0b4981f
Show file tree
Hide file tree
Showing 25 changed files with 595 additions and 92 deletions.
4 changes: 2 additions & 2 deletions src/Psalm/Codebase.php
Expand Up @@ -415,7 +415,7 @@ public function reloadFiles(ProjectAnalyzer $project_analyzer, array $candidate_

$this->file_reference_provider->updateReferenceCache($this, $referenced_files);

$this->populator->populateCodebase($this);
$this->populator->populateCodebase();
}

/** @return void */
Expand Down Expand Up @@ -479,7 +479,7 @@ public function scanFiles(int $threads = 1)
$has_changes = $this->scanner->scanFiles($this->classlikes, $threads);

if ($has_changes) {
$this->populator->populateCodebase($this);
$this->populator->populateCodebase();
}
}

Expand Down
Expand Up @@ -189,22 +189,24 @@ public static function analyze(
$class_visibility = \ReflectionProperty::IS_PUBLIC;
}

$class_constants = $codebase->classlikes->getConstantsForClass(
$class_constant_type = $codebase->classlikes->getConstantForClass(
$fq_class_name,
$class_visibility
$stmt->name->name,
$class_visibility,
$statements_analyzer
);

if (!isset($class_constants[$stmt->name->name])) {
$all_class_constants = [];

if (!$class_constant_type) {
if ($fq_class_name !== $context->self) {
$all_class_constants = $codebase->classlikes->getConstantsForClass(
$class_constant_type = $codebase->classlikes->getConstantForClass(
$fq_class_name,
\ReflectionProperty::IS_PRIVATE
$stmt->name->name,
\ReflectionProperty::IS_PRIVATE,
$statements_analyzer
);
}

if ($all_class_constants && isset($all_class_constants[$stmt->name->name])) {
if ($class_constant_type) {
if (IssueBuffer::accepts(
new InaccessibleClassConstant(
'Constant ' . $const_id . ' is not visible in this context',
Expand Down Expand Up @@ -294,10 +296,8 @@ public static function analyze(
}
}

if (isset($class_constants[$stmt->name->name])
&& ($first_part_lc !== 'static' || $class_const_storage->final)
) {
$stmt->inferredType = clone $class_constants[$stmt->name->name];
if ($first_part_lc !== 'static' || $class_const_storage->final) {
$stmt->inferredType = clone $class_constant_type;
$context->vars_in_scope[$const_id] = $stmt->inferredType;
} else {
$stmt->inferredType = Type::getMixed();
Expand Down
22 changes: 10 additions & 12 deletions src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php
Expand Up @@ -1182,18 +1182,17 @@ private static function fleshOutAtomicType(
return new Type\Atomic\TLiteralClassString($return_type->fq_classlike_name);
}

$class_constants = $codebase->classlikes->getConstantsForClass(
$class_constant = $codebase->classlikes->getConstantForClass(
$return_type->fq_classlike_name,
$return_type->const_name,
\ReflectionProperty::IS_PRIVATE
);

if (isset($class_constants[$return_type->const_name])) {
$const_type = $class_constants[$return_type->const_name];
if ($class_constant) {
if ($class_constant->isSingle()) {
$class_constant = clone $class_constant;

if ($const_type->isSingle()) {
$const_type = clone $const_type;

return array_values($const_type->getTypes())[0];
return array_values($class_constant->getTypes())[0];
}
}
}
Expand All @@ -1209,15 +1208,14 @@ private static function fleshOutAtomicType(
}

if ($evaluate && $codebase->classOrInterfaceExists($return_type->fq_classlike_name)) {
$class_constants = $codebase->classlikes->getConstantsForClass(
$class_constant_type = $codebase->classlikes->getConstantForClass(
$return_type->fq_classlike_name,
$return_type->const_name,
\ReflectionProperty::IS_PRIVATE
);

if (isset($class_constants[$return_type->const_name])) {
$const_type = $class_constants[$return_type->const_name];

foreach ($const_type->getTypes() as $const_type_atomic) {
if ($class_constant_type) {
foreach ($class_constant_type->getTypes() as $const_type_atomic) {
if ($const_type_atomic instanceof Type\Atomic\ObjectLike
|| $const_type_atomic instanceof Type\Atomic\TArray
) {
Expand Down
22 changes: 13 additions & 9 deletions src/Psalm/Internal/Analyzer/StatementsAnalyzer.php
Expand Up @@ -1639,15 +1639,19 @@ public static function getSimpleType(
return Type::getLiteralClassString($const_fq_class_name);
}

if ($existing_class_constants === null) {
if ($existing_class_constants === null
&& $file_source instanceof StatementsAnalyzer
) {
try {
$foreign_class_constants = $codebase->classlikes->getConstantsForClass(
$foreign_class_constant = $codebase->classlikes->getConstantForClass(
$const_fq_class_name,
\ReflectionProperty::IS_PRIVATE
$stmt->name->name,
\ReflectionProperty::IS_PRIVATE,
$file_source
);

if (isset($foreign_class_constants[$stmt->name->name])) {
return clone $foreign_class_constants[$stmt->name->name];
if ($foreign_class_constant) {
return clone $foreign_class_constant;
}

return null;
Expand Down Expand Up @@ -1898,9 +1902,9 @@ private function analyzeConstAssignment(PhpParser\Node\Stmt\Const_ $stmt, Contex
* @return Type\Union|null
*/
public function getConstType(
$const_name,
$is_fully_qualified,
Context $context
string $const_name,
bool $is_fully_qualified,
?Context $context
) {
$aliased_constants = $this->getAliases()->constants;

Expand All @@ -1926,7 +1930,7 @@ public function getConstType(
}
}

if ($context->hasVariable($fq_const_name, $this)) {
if ($context && $context->hasVariable($fq_const_name, $this)) {
return $context->vars_in_scope[$fq_const_name];
}

Expand Down
186 changes: 186 additions & 0 deletions src/Psalm/Internal/Codebase/ClassLikes.php
Expand Up @@ -21,6 +21,7 @@
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
use Psalm\Internal\Provider\ClassLikeStorageProvider;
use Psalm\Internal\Provider\FileReferenceProvider;
use Psalm\Internal\Scanner\UnresolvedConstant;
use Psalm\Issue\PossiblyUnusedMethod;
use Psalm\Issue\PossiblyUnusedParam;
use Psalm\Issue\PossiblyUnusedProperty;
Expand Down Expand Up @@ -1427,6 +1428,191 @@ public function getConstantsForClass($class_name, $visibility)
throw new \InvalidArgumentException('Must specify $visibility');
}

public function getConstantForClass(
string $class_name,
string $constant_name,
int $visibility,
?\Psalm\Internal\Analyzer\StatementsAnalyzer $statements_analyzer = null
) : ?Type\Union {
$class_name = strtolower($class_name);

$storage = $this->classlike_storage_provider->get($class_name);

if ($visibility === ReflectionProperty::IS_PUBLIC) {
$type_candidates = $storage->public_class_constants;

$fallbacks = $storage->public_class_constant_nodes;
} elseif ($visibility === ReflectionProperty::IS_PROTECTED) {
$type_candidates = array_merge(
$storage->public_class_constants,
$storage->protected_class_constants
);

$fallbacks = array_merge(
$storage->public_class_constant_nodes,
$storage->protected_class_constant_nodes
);
} elseif ($visibility === ReflectionProperty::IS_PRIVATE) {
$type_candidates = array_merge(
$storage->public_class_constants,
$storage->protected_class_constants,
$storage->private_class_constants
);

$fallbacks = array_merge(
$storage->public_class_constant_nodes,
$storage->protected_class_constant_nodes,
$storage->private_class_constant_nodes
);
} else {
throw new \InvalidArgumentException('Must specify $visibility');
}

if (isset($type_candidates[$constant_name])) {
return $type_candidates[$constant_name];
}

if (isset($fallbacks[$constant_name])) {
return new Type\Union([$this->resolveConstantType($fallbacks[$constant_name], $statements_analyzer)]);
}

return null;
}

/**
* @param string|int|float|bool|null $value
*/
private static function getLiteralTypeFromScalarValue($value) : Type\Atomic
{
if (\is_string($value)) {
return new Type\Atomic\TLiteralString($value);
} elseif (\is_int($value)) {
return new Type\Atomic\TLiteralInt($value);
} elseif (\is_float($value)) {
return new Type\Atomic\TLiteralFloat($value);
} elseif ($value === false) {
return new Type\Atomic\TFalse;
} elseif ($value === true) {
return new Type\Atomic\TTrue;
} else {
return new Type\Atomic\TNull;
}
}

private function resolveConstantType(
\Psalm\Internal\Scanner\UnresolvedConstantComponent $c,
\Psalm\Internal\Analyzer\StatementsAnalyzer $statements_analyzer = null
) : Type\Atomic {
if ($c instanceof UnresolvedConstant\ScalarValue) {
return self::getLiteralTypeFromScalarValue($c->value);
}

if ($c instanceof UnresolvedConstant\UnresolvedBinaryOp) {
$left = $this->resolveConstantType($c->left, $statements_analyzer);
$right = $this->resolveConstantType($c->right, $statements_analyzer);

if ($left instanceof Type\Atomic\TMixed || $right instanceof Type\Atomic\TMixed) {
return new Type\Atomic\TMixed;
}

if ($c instanceof UnresolvedConstant\UnresolvedConcatOp) {
if ($left instanceof Type\Atomic\TLiteralString && $right instanceof Type\Atomic\TLiteralString) {
return new Type\Atomic\TLiteralString($left->value . $right->value);
}

return new Type\Atomic\TMixed;
}

if ($c instanceof UnresolvedConstant\UnresolvedAdditionOp
|| $c instanceof UnresolvedConstant\UnresolvedSubtractionOp
|| $c instanceof UnresolvedConstant\UnresolvedDivisionOp
|| $c instanceof UnresolvedConstant\UnresolvedMultiplicationOp
|| $c instanceof UnresolvedConstant\UnresolvedBinaryOr
) {
if (($left instanceof Type\Atomic\TLiteralFloat || $left instanceof Type\Atomic\TLiteralInt)
&& ($right instanceof Type\Atomic\TLiteralFloat || $right instanceof Type\Atomic\TLiteralInt)
) {
if ($c instanceof UnresolvedConstant\UnresolvedAdditionOp) {
return self::getLiteralTypeFromScalarValue($left->value + $right->value);
}

if ($c instanceof UnresolvedConstant\UnresolvedSubtractionOp) {
return self::getLiteralTypeFromScalarValue($left->value - $right->value);
}

if ($c instanceof UnresolvedConstant\UnresolvedDivisionOp) {
return self::getLiteralTypeFromScalarValue($left->value / $right->value);
}

if ($c instanceof UnresolvedConstant\UnresolvedBinaryOr) {
return self::getLiteralTypeFromScalarValue($left->value | $right->value);
}

return self::getLiteralTypeFromScalarValue($left->value * $right->value);
}

return new Type\Atomic\TMixed;
}

return new Type\Atomic\TMixed;
}

if ($c instanceof UnresolvedConstant\ArrayValue) {
$properties = [];

foreach ($c->entries as $i => $entry) {
if ($entry->key) {
$key_type = $this->resolveConstantType($entry->key, $statements_analyzer);
} else {
$key_type = new Type\Atomic\TLiteralInt($i);
}

if ($key_type instanceof Type\Atomic\TLiteralInt
|| $key_type instanceof Type\Atomic\TLiteralString
) {
$key_value = $key_type->value;
} else {
return new Type\Atomic\TArray([Type::getArrayKey(), Type::getMixed()]);
}

$value_type = new Type\Union([$this->resolveConstantType($entry->value, $statements_analyzer)]);

$properties[$key_value] = $value_type;
}

return new Type\Atomic\ObjectLike($properties);
}

if ($c instanceof UnresolvedConstant\ClassConstant) {
$found_type = $this->getConstantForClass(
$c->fqcln,
$c->name,
ReflectionProperty::IS_PRIVATE,
$statements_analyzer
);

if ($found_type) {
return \array_values($found_type->getTypes())[0];
}
}

if ($c instanceof UnresolvedConstant\Constant) {
if ($statements_analyzer) {
$found_type = $statements_analyzer->getConstType(
$c->name,
$c->is_fully_qualified,
null
);

if ($found_type) {
return \array_values($found_type->getTypes())[0];
}
}
}

return new Type\Atomic\TMixed;
}

/**
* @param string $class_name
* @param string $const_name
Expand Down

0 comments on commit 0b4981f

Please sign in to comment.