Skip to content

Commit 22df4ba

Browse files
committed
TASK: Introduce TypeInferrer inspired by phpstan to support future advanced type inference
1 parent 0c7061f commit 22df4ba

File tree

6 files changed

+261
-118
lines changed

6 files changed

+261
-118
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
/**
4+
* PackageFactory.ComponentEngine - Universal View Components for PHP
5+
* Copyright (C) 2022 Contributors of PackageFactory.ComponentEngine
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
namespace PackageFactory\ComponentEngine\TypeSystem\Inferrer;
24+
25+
use PackageFactory\ComponentEngine\TypeSystem\TypeInterface;
26+
27+
class InferredTypes
28+
{
29+
/**
30+
* @var TypeInterface[]
31+
*/
32+
public readonly array $types;
33+
34+
public function __construct(
35+
TypeInterface ...$types
36+
) {
37+
$this->types = $types;
38+
}
39+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
/**
4+
* PackageFactory.ComponentEngine - Universal View Components for PHP
5+
* Copyright (C) 2022 Contributors of PackageFactory.ComponentEngine
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
namespace PackageFactory\ComponentEngine\TypeSystem\Inferrer;
24+
25+
use PackageFactory\ComponentEngine\Definition\BinaryOperator;
26+
use PackageFactory\ComponentEngine\Parser\Ast\BinaryOperationNode;
27+
use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode;
28+
use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode;
29+
use PackageFactory\ComponentEngine\Parser\Ast\NullLiteralNode;
30+
use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface;
31+
use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType;
32+
use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType;
33+
34+
/**
35+
* This class handles the analysis of identifier types that are used in a condition
36+
* and based on the requested branch: truthy or falsy, will predict the types a variable will have in the respective branch
37+
* so it matches the expected runtime behaviour
38+
*
39+
* For example given this expression: `nullableString ? "nullableString is not null" : "nullableString is null"` based on the condition `nullableString`
40+
* It will infer that in the truthy context nullableString is a string while in the falsy context it will infer that it is a null
41+
*
42+
* The structure is partially inspired by phpstan
43+
* https://github.com/phpstan/phpstan-src/blob/07bb4aa2d5e39dafa78f56c5df132c763c2d1b67/src/Analyser/TypeSpecifier.php#L111
44+
*/
45+
class TypeInferrer
46+
{
47+
public function __construct(
48+
private readonly ScopeInterface $scope
49+
) {
50+
}
51+
52+
public function inferTypesInCondition(ExpressionNode $conditionNode, TypeInferrerContext $context): InferredTypes
53+
{
54+
if ($conditionNode->root instanceof IdentifierNode) {
55+
$type = $this->scope->lookupTypeFor($conditionNode->root->value);
56+
// case `nullableString ? "nullableString is not null" : "nullableString is null"`
57+
if (!$type instanceof UnionType || !$type->containsNull()) {
58+
return new InferredTypes();
59+
}
60+
61+
return new InferredTypes(
62+
...[$conditionNode->root->value => $context->isTrue() ? $type->withoutNull() : NullType::get()]
63+
);
64+
}
65+
66+
if (($binaryOperationNode = $conditionNode->root) instanceof BinaryOperationNode) {
67+
// cases
68+
// `nullableString === null ? "nullableString is null" : "nullableString is not null"`
69+
// `nullableString !== null ? "nullableString is not null" : "nullableString is null"`
70+
if (count($binaryOperationNode->operands->rest) !== 1) {
71+
return new InferredTypes();
72+
}
73+
$first = $binaryOperationNode->operands->first;
74+
$second = $binaryOperationNode->operands->rest[0];
75+
76+
$comparedIdentifierValueToNull = match (true) {
77+
// case `nullableString === null`
78+
$first->root instanceof IdentifierNode && $second->root instanceof NullLiteralNode => $first->root->value,
79+
// yodas case `null === nullableString`
80+
$first->root instanceof NullLiteralNode && $second->root instanceof IdentifierNode => $second->root->value,
81+
default => null
82+
};
83+
84+
if ($comparedIdentifierValueToNull === null) {
85+
return new InferredTypes();
86+
}
87+
88+
$type = $this->scope->lookupTypeFor($comparedIdentifierValueToNull);
89+
if (!$type instanceof UnionType || !$type->containsNull()) {
90+
return new InferredTypes();
91+
}
92+
93+
if ($binaryOperationNode->operator === BinaryOperator::EQUAL) {
94+
return new InferredTypes(
95+
...[$comparedIdentifierValueToNull => $context->isTrue() ? NullType::get() : $type->withoutNull()]
96+
);
97+
}
98+
if ($binaryOperationNode->operator === BinaryOperator::NOT_EQUAL) {
99+
return new InferredTypes(
100+
...[$comparedIdentifierValueToNull => $context->isTrue() ? $type->withoutNull() : NullType::get()]
101+
);
102+
}
103+
}
104+
105+
return new InferredTypes();
106+
}
107+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
/**
4+
* PackageFactory.ComponentEngine - Universal View Components for PHP
5+
* Copyright (C) 2022 Contributors of PackageFactory.ComponentEngine
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
namespace PackageFactory\ComponentEngine\TypeSystem\Inferrer;
24+
25+
enum TypeInferrerContext
26+
{
27+
case TRUTHY;
28+
29+
case FALSY;
30+
31+
public function isTrue(): bool
32+
{
33+
return $this === self::TRUTHY;
34+
}
35+
36+
public function isFalse(): bool
37+
{
38+
return $this === self::FALSY;
39+
}
40+
}

src/TypeSystem/Resolver/TernaryOperation/TernaryOperationTypeResolver.php

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
use PackageFactory\ComponentEngine\Parser\Ast\BooleanLiteralNode;
2626
use PackageFactory\ComponentEngine\Parser\Ast\TernaryOperationNode;
2727
use PackageFactory\ComponentEngine\TypeSystem\Resolver\Expression\ExpressionTypeResolver;
28-
use PackageFactory\ComponentEngine\TypeSystem\Scope\ShallowScope\TernaryBranchScope;
28+
use PackageFactory\ComponentEngine\TypeSystem\Scope\TernaryBranchScope\TernaryBranchScope;
2929
use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface;
3030
use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType;
3131
use PackageFactory\ComponentEngine\TypeSystem\TypeInterface;
@@ -39,34 +39,26 @@ public function __construct(
3939

4040
public function resolveTypeOf(TernaryOperationNode $ternaryOperationNode): TypeInterface
4141
{
42-
$expressionTypeResolver = new ExpressionTypeResolver(
43-
scope: $this->scope
44-
);
45-
$conditionNode = $ternaryOperationNode->condition;
46-
47-
// @todo for eager type checks?
48-
$expressionTypeResolver->resolveTypeOf($conditionNode);
49-
50-
if ($conditionNode->root instanceof BooleanLiteralNode) {
51-
return $conditionNode->root->value
52-
? $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->true)
53-
: $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->false);
54-
}
55-
5642
$trueExpressionTypeResolver = new ExpressionTypeResolver(
57-
scope: TernaryBranchScope::forTrueBranch(
43+
scope: TernaryBranchScope::forTruthyBranch(
5844
$ternaryOperationNode->condition,
5945
$this->scope
6046
)
6147
);
6248

6349
$falseExpressionTypeResolver = new ExpressionTypeResolver(
64-
scope: TernaryBranchScope::forFalseBranch(
50+
scope: TernaryBranchScope::forFalsyBranch(
6551
$ternaryOperationNode->condition,
6652
$this->scope
6753
)
6854
);
6955

56+
if ($ternaryOperationNode->condition->root instanceof BooleanLiteralNode) {
57+
return $ternaryOperationNode->condition->root->value
58+
? $trueExpressionTypeResolver->resolveTypeOf($ternaryOperationNode->true)
59+
: $falseExpressionTypeResolver->resolveTypeOf($ternaryOperationNode->false);
60+
}
61+
7062
return UnionType::of(
7163
$trueExpressionTypeResolver->resolveTypeOf($ternaryOperationNode->true),
7264
$falseExpressionTypeResolver->resolveTypeOf($ternaryOperationNode->false)

src/TypeSystem/Scope/ShallowScope/TernaryBranchScope.php

Lines changed: 0 additions & 101 deletions
This file was deleted.

0 commit comments

Comments
 (0)