Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce value-of with backed enum cases in assertions #9586

Merged
merged 3 commits into from
Apr 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 17 additions & 16 deletions src/Psalm/Internal/Codebase/ClassConstantByWildcardResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,50 +9,51 @@
use Psalm\Type\Atomic\TMixed;

use function array_merge;
use function array_values;
use function preg_match;
use function sprintf;
use function str_replace;

/**
* @internal
*/
final class ClassConstantByWildcardResolver
{
private StorageByPatternResolver $resolver;
private Codebase $codebase;

public function __construct(Codebase $codebase)
{
$this->resolver = new StorageByPatternResolver();
$this->codebase = $codebase;
}

/**
* @return list<Atomic>|null
* @return non-empty-array<array-key,Atomic>|null
*/
public function resolve(string $class_name, string $constant_pattern): ?array
{
if (!$this->codebase->classlike_storage_provider->has($class_name)) {
return null;
}

$constant_regex_pattern = sprintf('#^%s$#', str_replace('*', '.*', $constant_pattern));
$classlike_storage = $this->codebase->classlike_storage_provider->get($class_name);

$class_like_storage = $this->codebase->classlike_storage_provider->get($class_name);
$matched_class_constant_types = [];

foreach ($class_like_storage->constants as $constant => $class_constant_storage) {
if (preg_match($constant_regex_pattern, $constant) === 0) {
continue;
}
$constants = $this->resolver->resolveConstants(
$classlike_storage,
$constant_pattern,
);

$types = [];
foreach ($constants as $class_constant_storage) {
if (! $class_constant_storage->type) {
$matched_class_constant_types[] = [new TMixed()];
$types[] = [new TMixed()];
continue;
}

$matched_class_constant_types[] = $class_constant_storage->type->getAtomicTypes();
$types[] = $class_constant_storage->type->getAtomicTypes();
}

if ($types === []) {
return null;
}

return array_values(array_merge([], ...$matched_class_constant_types));
return array_merge([], ...$types);
}
}
175 changes: 143 additions & 32 deletions src/Psalm/Internal/Codebase/ClassLikes.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
use UnexpectedValueException;

use function array_filter;
use function array_keys;
use function array_merge;
use function array_pop;
use function count;
Expand Down Expand Up @@ -1603,16 +1604,16 @@ public function getConstantsForClass(string $class_name, int $visibility): array
}

/**
* @param ReflectionProperty::IS_PUBLIC|ReflectionProperty::IS_PROTECTED|ReflectionProperty::IS_PRIVATE
* $visibility
* @param ReflectionProperty::IS_PUBLIC|ReflectionProperty::IS_PROTECTED|ReflectionProperty::IS_PRIVATE $visibility
*/
public function getClassConstantType(
string $class_name,
string $constant_name,
int $visibility,
?StatementsAnalyzer $statements_analyzer = null,
array $visited_constant_ids = [],
bool $late_static_binding = false
bool $late_static_binding = false,
bool $in_value_of_context = false
): ?Union {
$class_name = strtolower($class_name);

Expand All @@ -1622,41 +1623,42 @@ public function getClassConstantType(

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

if (isset($storage->constants[$constant_name])) {
$constant_storage = $storage->constants[$constant_name];
$enum_types = null;

if ($visibility === ReflectionProperty::IS_PUBLIC
&& $constant_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PUBLIC
) {
return null;
}
if ($storage->is_enum) {
$enum_types = $this->getEnumType(
$storage,
$constant_name,
);

if ($visibility === ReflectionProperty::IS_PROTECTED
&& $constant_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PUBLIC
&& $constant_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PROTECTED
) {
return null;
if ($in_value_of_context) {
return $enum_types;
}
}

if ($constant_storage->unresolved_node) {
/** @psalm-suppress InaccessibleProperty Lazy resolution */
$constant_storage->inferred_type = new Union([ConstantTypeResolver::resolve(
$this,
$constant_storage->unresolved_node,
$statements_analyzer,
$visited_constant_ids,
)]);
if ($constant_storage->type === null || !$constant_storage->type->from_docblock) {
/** @psalm-suppress InaccessibleProperty Lazy resolution */
$constant_storage->type = $constant_storage->inferred_type;
}
}
$constant_types = $this->getConstantType(
$storage,
$constant_name,
$visibility,
$statements_analyzer,
$visited_constant_ids,
$late_static_binding,
);

$types = [];
if ($enum_types !== null) {
$types = array_merge($types, $enum_types->getAtomicTypes());
}

return $late_static_binding ? $constant_storage->type : ($constant_storage->inferred_type ?? null);
} elseif (isset($storage->enum_cases[$constant_name])) {
return new Union([new TEnumCase($storage->name, $constant_name)]);
if ($constant_types !== null) {
$types = array_merge($types, $constant_types->getAtomicTypes());
}
return null;

if ($types === []) {
return null;
}

return new Union($types);
}

private function checkMethodReferences(ClassLikeStorage $classlike_storage, Methods $methods): void
Expand Down Expand Up @@ -2366,4 +2368,113 @@ public function getStorageFor(string $fq_class_name): ?ClassLikeStorage
return null;
}
}

private function getConstantType(
ClassLikeStorage $class_like_storage,
string $constant_name,
int $visibility,
?StatementsAnalyzer $statements_analyzer,
array $visited_constant_ids,
bool $late_static_binding
): ?Union {
$constant_resolver = new StorageByPatternResolver();
$resolved_constants = $constant_resolver->resolveConstants(
$class_like_storage,
$constant_name,
);

$filtered_constants_by_visibility = array_filter(
$resolved_constants,
fn(ClassConstantStorage $resolved_constant) => $this->filterConstantNameByVisibility(
$resolved_constant,
$visibility,
)
);

if ($filtered_constants_by_visibility === []) {
return null;
}

$new_atomic_types = [];

foreach ($filtered_constants_by_visibility as $filtered_constant_name => $constant_storage) {
if (!isset($class_like_storage->constants[$filtered_constant_name])) {
continue;
}

if ($constant_storage->unresolved_node) {
/** @psalm-suppress InaccessibleProperty Lazy resolution */
$constant_storage->inferred_type = new Union([ConstantTypeResolver::resolve(
$this,
$constant_storage->unresolved_node,
$statements_analyzer,
$visited_constant_ids,
)]);

if ($constant_storage->type === null || !$constant_storage->type->from_docblock) {
/** @psalm-suppress InaccessibleProperty Lazy resolution */
$constant_storage->type = $constant_storage->inferred_type;
}
}

$constant_type = $late_static_binding
? $constant_storage->type
: ($constant_storage->inferred_type ?? null);

if ($constant_type === null) {
continue;
}

$new_atomic_types[] = $constant_type->getAtomicTypes();
}

if ($new_atomic_types === []) {
return null;
}

return new Union(array_merge([], ...$new_atomic_types));
}

private function getEnumType(
ClassLikeStorage $class_like_storage,
string $constant_name
): ?Union {
$constant_resolver = new StorageByPatternResolver();
$resolved_enums = $constant_resolver->resolveEnums(
$class_like_storage,
$constant_name,
);

if ($resolved_enums === []) {
return null;
}

$types = [];
foreach (array_keys($resolved_enums) as $enum_case_name) {
$types[$enum_case_name] = new TEnumCase($class_like_storage->name, $enum_case_name);
}

return new Union($types);
}

private function filterConstantNameByVisibility(
ClassConstantStorage $constant_storage,
int $visibility
): bool {

if ($visibility === ReflectionProperty::IS_PUBLIC
&& $constant_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PUBLIC
) {
return false;
}

if ($visibility === ReflectionProperty::IS_PROTECTED
&& $constant_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PUBLIC
&& $constant_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PROTECTED
) {
return false;
}

return true;
}
}
87 changes: 87 additions & 0 deletions src/Psalm/Internal/Codebase/StorageByPatternResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

declare(strict_types=1);

namespace Psalm\Internal\Codebase;

use Psalm\Storage\ClassConstantStorage;
use Psalm\Storage\ClassLikeStorage;
use Psalm\Storage\EnumCaseStorage;

use function preg_match;
use function sprintf;
use function str_replace;
use function strpos;

/**
* @internal
*/
final class StorageByPatternResolver
{
public const RESOLVE_CONSTANTS = 1;
public const RESOLVE_ENUMS = 2;

/**
* @return array<string,ClassConstantStorage>
*/
public function resolveConstants(
ClassLikeStorage $class_like_storage,
string $pattern
): array {
$constants = $class_like_storage->constants;

if (strpos($pattern, '*') === false) {
if (isset($constants[$pattern])) {
return [$pattern => $constants[$pattern]];
}

return [];
} elseif ($pattern === '*') {
return $constants;
}

$regex_pattern = sprintf('#^%s$#', str_replace('*', '.*?', $pattern));
$matched_constants = [];

foreach ($constants as $constant => $class_constant_storage) {
if (preg_match($regex_pattern, $constant) === 0) {
continue;
}

$matched_constants[$constant] = $class_constant_storage;
}

return $matched_constants;
}

/**
* @return array<string,EnumCaseStorage>
*/
public function resolveEnums(
ClassLikeStorage $class_like_storage,
string $pattern
): array {
$enum_cases = $class_like_storage->enum_cases;
if (strpos($pattern, '*') === false) {
if (isset($enum_cases[$pattern])) {
return [$pattern => $enum_cases[$pattern]];
}

return [];
} elseif ($pattern === '*') {
return $enum_cases;
}

$regex_pattern = sprintf('#^%s$#', str_replace('*', '.*?', $pattern));
$matched_enums = [];
foreach ($enum_cases as $enum_case_name => $enum_case_storage) {
if (preg_match($regex_pattern, $enum_case_name) === 0) {
continue;
}

$matched_enums[$enum_case_name] = $enum_case_storage;
}

return $matched_enums;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,9 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool
try {
$type_string = CommentAnalyzer::splitDocLine($type_string)[0];
} catch (DocblockParseException $e) {
throw new DocblockParseException($type_string . ' is not a valid type: '.$e->getMessage());
throw new DocblockParseException(
$type_string . ' is not a valid type: ' . $e->getMessage(),
);
}
$type_string = CommentAnalyzer::sanitizeDocblockType($type_string);
try {
Expand Down
Loading