Skip to content

Commit 092ef3b

Browse files
committed
Optimize match/enum performance
1 parent 45404d9 commit 092ef3b

File tree

10 files changed

+1151
-41
lines changed

10 files changed

+1151
-41
lines changed

‎src/Rules/Comparison/MatchExpressionRule.php‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,19 @@
88
use PHPStan\Rules\Rule;
99
use PHPStan\Rules\RuleErrorBuilder;
1010
use PHPStan\Type\Constant\ConstantBooleanType;
11+
use PHPStan\Type\Enum\EnumCaseObjectType;
1112
use PHPStan\Type\NeverType;
1213
use PHPStan\Type\ObjectType;
14+
use PHPStan\Type\SubtractableType;
1315
use PHPStan\Type\TypeCombinator;
16+
use PHPStan\Type\TypeUtils;
17+
use PHPStan\Type\TypeWithClassName;
1418
use PHPStan\Type\UnionType;
1519
use PHPStan\Type\VerbosityLevel;
1620
use UnhandledMatchError;
21+
use function array_keys;
1722
use function array_map;
23+
use function array_values;
1824
use function count;
1925
use function sprintf;
2026

@@ -85,6 +91,45 @@ public function processNode(Node $node, Scope $scope): array
8591

8692
if (!$hasDefault && !$nextArmIsDead) {
8793
$remainingType = $node->getEndScope()->getType($matchCondition);
94+
if ($remainingType instanceof TypeWithClassName && $remainingType instanceof SubtractableType) {
95+
$subtractedType = $remainingType->getSubtractedType();
96+
if ($subtractedType !== null && $remainingType->getClassReflection() !== null) {
97+
$classReflection = $remainingType->getClassReflection();
98+
if ($classReflection->isEnum()) {
99+
$cases = [];
100+
foreach (array_keys($classReflection->getEnumCases()) as $name) {
101+
$cases[$name] = new EnumCaseObjectType($classReflection->getName(), $name);
102+
}
103+
104+
$subtractedTypes = TypeUtils::flattenTypes($subtractedType);
105+
$set = true;
106+
foreach ($subtractedTypes as $subType) {
107+
if (!$subType instanceof EnumCaseObjectType) {
108+
$set = false;
109+
break;
110+
}
111+
112+
if ($subType->getClassName() !== $classReflection->getName()) {
113+
$set = false;
114+
break;
115+
}
116+
117+
unset($cases[$subType->getEnumCaseName()]);
118+
}
119+
120+
$cases = array_values($cases);
121+
$casesCount = count($cases);
122+
if ($set) {
123+
if ($casesCount > 1) {
124+
$remainingType = new UnionType($cases);
125+
}
126+
if ($casesCount === 1) {
127+
$remainingType = $cases[0];
128+
}
129+
}
130+
}
131+
}
132+
}
88133
if (
89134
!$remainingType instanceof NeverType
90135
&& !$this->isUnhandledMatchErrorCaught($node)

‎src/TrinaryLogic.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ public static function maxMin(self ...$operands): self
108108
throw new ShouldNotHappenException();
109109
}
110110
$operandValues = array_column($operands, 'value');
111-
return self::create(max($operandValues) > 0 ? max($operandValues) : min($operandValues));
111+
return self::create(max($operandValues) > 0 ? 1 : min($operandValues));
112112
}
113113

114114
public function negate(): self

‎src/Type/IntersectionType.php‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,12 @@ public function isSuperTypeOf(Type $otherType): TrinaryLogic
146146

147147
$results = [];
148148
foreach ($this->getTypes() as $innerType) {
149-
$results[] = $innerType->isSuperTypeOf($otherType);
149+
$result = $innerType->isSuperTypeOf($otherType);
150+
if ($result->no()) {
151+
return $result;
152+
}
153+
154+
$results[] = $result;
150155
}
151156

152157
return TrinaryLogic::createYes()->and(...$results);
@@ -160,7 +165,11 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic
160165

161166
$results = [];
162167
foreach ($this->getTypes() as $innerType) {
163-
$results[] = $otherType->isSuperTypeOf($innerType);
168+
$result = $otherType->isSuperTypeOf($innerType);
169+
if ($result->yes()) {
170+
return $result;
171+
}
172+
$results[] = $result;
164173
}
165174

166175
return TrinaryLogic::maxMin(...$results);
@@ -170,7 +179,12 @@ public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLog
170179
{
171180
$results = [];
172181
foreach ($this->getTypes() as $innerType) {
173-
$results[] = $acceptingType->accepts($innerType, $strictTypes);
182+
$result = $acceptingType->accepts($innerType, $strictTypes);
183+
if ($result->yes()) {
184+
return $result;
185+
}
186+
187+
$results[] = $result;
174188
}
175189

176190
return TrinaryLogic::maxMin(...$results);

‎src/Type/ObjectType.php‎

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
use function array_key_exists;
4141
use function array_keys;
4242
use function array_map;
43+
use function array_merge;
4344
use function array_values;
4445
use function count;
4546
use function implode;
@@ -1052,7 +1053,14 @@ public function changeSubtractedType(?Type $subtractedType): Type
10521053
$cases[$name] = new EnumCaseObjectType($classReflection->getName(), $name);
10531054
}
10541055

1055-
foreach (TypeUtils::flattenTypes($subtractedType) as $subType) {
1056+
$originalCases = $cases;
1057+
1058+
$subtractedTypes = TypeUtils::flattenTypes($subtractedType);
1059+
if ($this->subtractedType !== null) {
1060+
$subtractedTypes = array_merge($subtractedTypes, TypeUtils::flattenTypes($this->subtractedType));
1061+
}
1062+
$subtractedCases = [];
1063+
foreach ($subtractedTypes as $subType) {
10561064
if (!$subType instanceof EnumCaseObjectType) {
10571065
return new self($this->className, $subtractedType);
10581066
}
@@ -1061,19 +1069,33 @@ public function changeSubtractedType(?Type $subtractedType): Type
10611069
return new self($this->className, $subtractedType);
10621070
}
10631071

1064-
unset($cases[$subType->getEnumCaseName()]);
1072+
if (!array_key_exists($subType->getEnumCaseName(), $cases)) {
1073+
return new self($this->className, $subtractedType);
1074+
}
1075+
1076+
$subtractedCases[$subType->getEnumCaseName()] = $subType;
1077+
unset($originalCases[$subType->getEnumCaseName()]);
10651078
}
10661079

1067-
$cases = array_values($cases);
1068-
if (count($cases) === 0) {
1080+
if (count($originalCases) === 1) {
1081+
return array_values($originalCases)[0];
1082+
}
1083+
1084+
$subtractedCases = array_values($subtractedCases);
1085+
$subtractedCasesCount = count($subtractedCases);
1086+
if ($subtractedCasesCount === count($cases)) {
10691087
return new NeverType();
10701088
}
10711089

1072-
if (count($cases) === 1) {
1073-
return $cases[0];
1090+
if ($subtractedCasesCount === 0) {
1091+
return new self($this->className);
1092+
}
1093+
1094+
if (count($subtractedCases) === 1) {
1095+
return new self($this->className, $subtractedCases[0]);
10741096
}
10751097

1076-
return new UnionType(array_values($cases));
1098+
return new self($this->className, new UnionType($subtractedCases));
10771099
}
10781100
}
10791101

‎src/Type/StaticType.php‎

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@
2020
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
2121
use PHPStan\Type\Traits\NonGenericTypeTrait;
2222
use PHPStan\Type\Traits\UndecidedComparisonTypeTrait;
23-
use function array_keys;
24-
use function array_values;
25-
use function count;
2623
use function get_class;
2724
use function sprintf;
2825

@@ -432,35 +429,24 @@ public function getTypeWithoutSubtractedType(): Type
432429

433430
public function changeSubtractedType(?Type $subtractedType): Type
434431
{
435-
$classReflection = $this->getClassReflection();
436-
if ($classReflection->isEnum() && $subtractedType !== null) {
437-
$cases = [];
438-
foreach (array_keys($classReflection->getEnumCases()) as $constantName) {
439-
$cases[$constantName] = new EnumCaseObjectType($classReflection->getName(), $constantName);
440-
}
441-
442-
foreach (TypeUtils::flattenTypes($subtractedType) as $subType) {
443-
if (!$subType instanceof EnumCaseObjectType) {
444-
return new self($this->classReflection, $subtractedType);
432+
if ($subtractedType !== null) {
433+
$classReflection = $this->getClassReflection();
434+
if ($classReflection->isEnum()) {
435+
$objectType = $this->getStaticObjectType()->changeSubtractedType($subtractedType);
436+
if ($objectType instanceof NeverType) {
437+
return $objectType;
445438
}
446439

447-
if ($subType->getClassName() !== $this->getClassName()) {
448-
return new self($this->classReflection, $subtractedType);
440+
if ($objectType instanceof EnumCaseObjectType) {
441+
return TypeCombinator::intersect($this, $objectType);
449442
}
450443

451-
unset($cases[$subType->getEnumCaseName()]);
452-
}
453-
454-
$cases = array_values($cases);
455-
if (count($cases) === 0) {
456-
return new NeverType();
457-
}
444+
if ($objectType instanceof ObjectType) {
445+
return new self($classReflection, $objectType->getSubtractedType());
446+
}
458447

459-
if (count($cases) === 1) {
460-
return TypeCombinator::intersect($this, $cases[0]);
448+
return $this;
461449
}
462-
463-
return TypeCombinator::intersect($this, new UnionType(array_values($cases)));
464450
}
465451

466452
return new self($this->classReflection, $subtractedType);

‎src/Type/UnionType.php‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,11 @@ public function isSuperTypeOf(Type $otherType): TrinaryLogic
139139

140140
$results = [];
141141
foreach ($this->getTypes() as $innerType) {
142-
$results[] = $innerType->isSuperTypeOf($otherType);
142+
$result = $innerType->isSuperTypeOf($otherType);
143+
if ($result->yes()) {
144+
return $result;
145+
}
146+
$results[] = $result;
143147
}
144148

145149
if ($otherType instanceof TemplateUnionType) {

‎tests/PHPStan/Analyser/AnalyserIntegrationTest.php‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,6 +922,16 @@ public function testBug7320(): void
922922
$this->assertSame(13, $errors[0]->getLine());
923923
}
924924

925+
public function testMatchPerformanceIssue(): void
926+
{
927+
if (PHP_VERSION_ID < 80100) {
928+
$this->markTestSkipped('Test requires PHP 8.1.');
929+
}
930+
931+
$errors = $this->runAnalyse(__DIR__ . '/data/match-performance-issue.php');
932+
$this->assertNoErrors($errors);
933+
}
934+
925935
/**
926936
* @param string[]|null $allAnalysedFiles
927937
* @return Error[]

‎tests/PHPStan/Analyser/data/bug-7176.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ function test(Suit $x): string {
1717
assertType('Bug7176\Suit::Clubs', $x);
1818
return 'WORKS';
1919
}
20-
assertType('Bug7176\Suit::Diamonds|Bug7176\Suit::Hearts|Bug7176\Suit::Spades', $x);
20+
assertType('Bug7176\Suit~Bug7176\Suit::Clubs', $x);
2121

2222
if (in_array($x, [Suit::Spades], true)) {
2323
assertType('Bug7176\Suit::Spades', $x);
2424
return 'DOES NOT WORK';
2525
}
26-
assertType('Bug7176\Suit::Diamonds|Bug7176\Suit::Hearts', $x);
26+
assertType('Bug7176\Suit~Bug7176\Suit::Clubs|Bug7176\Suit::Spades', $x);
2727

2828
return match ($x) {
2929
Suit::Hearts => 'a',

0 commit comments

Comments
 (0)