Skip to content

Commit

Permalink
Merge pull request #7719 from whatUwant/4.x
Browse files Browse the repository at this point in the history
  • Loading branch information
weirdan committed Feb 22, 2022
2 parents d2493e2 + 40cc346 commit 919775c
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 27 deletions.
Expand Up @@ -49,13 +49,15 @@
use Psalm\Type\Atomic\TEnumCase;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TGenericObject;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Atomic\TObject;
use Psalm\Type\Atomic\TObjectWithProperties;
use Psalm\Type\Atomic\TString;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Union;

Expand Down Expand Up @@ -184,35 +186,19 @@ public static function analyze(

$property_id = $fq_class_name . '::$' . $prop_name;

if ($class_storage->is_enum) {
if ($prop_name === 'value' && $class_storage->enum_type !== null && $class_storage->enum_cases) {
$case_values = [];

foreach ($class_storage->enum_cases as $enum_case) {
if (is_string($enum_case->value)) {
$case_values[] = new TLiteralString($enum_case->value);
} elseif (is_int($enum_case->value)) {
$case_values[] = new TLiteralInt($enum_case->value);
} else {
// this should never happen
$case_values[] = new TMixed();
}
}

// todo: this is suboptimal when we reference enum directly, e.g. Status::Open->value
if ($class_storage->is_enum || in_array('UnitEnum', $codebase->getParentInterfaces($fq_class_name))) {
if ($prop_name === 'value' && !$class_storage->is_enum) {
$statements_analyzer->node_data->setType(
$stmt,
new Union($case_values)
new Union([
new TString(),
new TInt()
])
);
} elseif ($prop_name === 'value' && $class_storage->enum_type !== null && $class_storage->enum_cases) {
self::handleEnumValue($statements_analyzer, $stmt, $class_storage);
} elseif ($prop_name === 'name') {
if ($lhs_type_part instanceof TEnumCase) {
$statements_analyzer->node_data->setType(
$stmt,
new Union([new TLiteralString($lhs_type_part->case_name)])
);
} else {
$statements_analyzer->node_data->setType($stmt, Type::getNonEmptyString());
}
self::handleEnumName($statements_analyzer, $stmt, $lhs_type_part);
} else {
self::handleNonExistentProperty(
$statements_analyzer,
Expand Down Expand Up @@ -908,6 +894,47 @@ public static function processTaints(
}
}

private static function handleEnumName(
StatementsAnalyzer $statements_analyzer,
PropertyFetch $stmt,
Atomic $lhs_type_part
): void {
if ($lhs_type_part instanceof TEnumCase) {
$statements_analyzer->node_data->setType(
$stmt,
new Union([new TLiteralString($lhs_type_part->case_name)])
);
} else {
$statements_analyzer->node_data->setType($stmt, Type::getNonEmptyString());
}
}

private static function handleEnumValue(
StatementsAnalyzer $statements_analyzer,
PropertyFetch $stmt,
ClassLikeStorage $class_storage
): void {
$case_values = [];

foreach ($class_storage->enum_cases as $enum_case) {
if (is_string($enum_case->value)) {
$case_values[] = new TLiteralString($enum_case->value);
} elseif (is_int($enum_case->value)) {
$case_values[] = new TLiteralInt($enum_case->value);
} else {
// this should never happen
$case_values[] = new TMixed();
}
}

// todo: this is suboptimal when we reference enum directly, e.g. Status::Open->value
/** @psalm-suppress ArgumentTypeCoercion */
$statements_analyzer->node_data->setType(
$stmt,
new Union($case_values)
);
}

private static function handleUndefinedProperty(
Context $context,
StatementsAnalyzer $statements_analyzer,
Expand Down Expand Up @@ -1009,7 +1036,8 @@ private static function handleNonExistentClass(

if (!$class_exists &&
//interfaces can't have properties. Except when they do... In PHP Core, they can
!in_array($fq_class_name, ['UnitEnum', 'BackedEnum'], true)
!in_array($fq_class_name, ['UnitEnum', 'BackedEnum'], true) &&
!in_array('UnitEnum', $codebase->getParentInterfaces($fq_class_name))
) {
if (IssueBuffer::accepts(
new NoInterfaceProperties(
Expand Down
2 changes: 1 addition & 1 deletion stubs/Php81.phpstub
Expand Up @@ -11,7 +11,7 @@ namespace {
public static function cases(): array;
}

interface BackedEnum
interface BackedEnum extends UnitEnum
{
public readonly int|string $value;

Expand Down
6 changes: 6 additions & 0 deletions tests/EnumTest.php
Expand Up @@ -377,6 +377,12 @@ enum Status: int {
static fn (\UnitEnum $tag): string => $tag->name;
static fn (\BackedEnum $tag): string|int => $tag->value;
interface ExtendedUnitEnum extends \UnitEnum {}
static fn (ExtendedUnitEnum $tag): string => $tag->name;
interface ExtendedBackedEnum extends \BackedEnum {}
static fn (ExtendedBackedEnum $tag): string|int => $tag->value;
',
'assertions' => [],
[],
Expand Down
4 changes: 4 additions & 0 deletions tests/Traits/ValidCodeAnalysisTestTrait.php
Expand Up @@ -55,6 +55,10 @@ public function testValidCode(
if (version_compare(PHP_VERSION, '8.0.0', '<')) {
$this->markTestSkipped('Test case requires PHP 8.0.');
}
} elseif (strpos($test_name, 'PHP81-') !== false) {
if (version_compare(PHP_VERSION, '8.1.0', '<')) {
$this->markTestSkipped('Test case requires PHP 8.1.');
}
} elseif (strpos($test_name, 'SKIPPED-') !== false) {
$this->markTestSkipped('Skipped due to a bug.');
}
Expand Down

0 comments on commit 919775c

Please sign in to comment.