-
Notifications
You must be signed in to change notification settings - Fork 1
WIP: Ternary Expression TypeNarrower for Nullables #14
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
base: main
Are you sure you want to change the base?
Changes from 17 commits
7efcfd3
2ffe099
6293145
2a08474
5e4d6ce
5c08a4e
8f0ba11
8a34ee1
1b476e5
57ee20c
3487bd9
1348ac1
492e05c
0c7061f
22df4ba
0adb4c0
f61b896
6a8bd00
d835fa5
88c6fd6
e0a913a
aaf6c49
560c97d
d00a194
530b155
331cdda
bd28d87
1027e16
884b895
0121247
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,72 @@ | ||||||
<?php | ||||||
|
||||||
/** | ||||||
* PackageFactory.ComponentEngine - Universal View Components for PHP | ||||||
* Copyright (C) 2022 Contributors of PackageFactory.ComponentEngine | ||||||
* | ||||||
* This program is free software: you can redistribute it and/or modify | ||||||
* it under the terms of the GNU General Public License as published by | ||||||
* the Free Software Foundation, either version 3 of the License, or | ||||||
* (at your option) any later version. | ||||||
* | ||||||
* This program is distributed in the hope that it will be useful, | ||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||
* GNU General Public License for more details. | ||||||
* | ||||||
* You should have received a copy of the GNU General Public License | ||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||||
*/ | ||||||
|
||||||
declare(strict_types=1); | ||||||
|
||||||
namespace PackageFactory\ComponentEngine\TypeSystem\Inferrer; | ||||||
|
||||||
use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; | ||||||
|
||||||
class InferredTypes | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
It might be sensible to move this class to a namespace There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Still not sure ;) maybe another day ^^ It will fall all into place sometime... |
||||||
{ | ||||||
/** | ||||||
* Map of identifierName to the corresponding inferred type | ||||||
* @var array<string,TypeInterface> | ||||||
*/ | ||||||
private readonly array $types; | ||||||
|
||||||
private function __construct( | ||||||
TypeInterface ...$types | ||||||
) { | ||||||
assert(self::isAssociativeArray($types), '$types must be an associative array'); | ||||||
mhsdesign marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
$this->types = $types; | ||||||
} | ||||||
|
||||||
public static function empty(): self | ||||||
{ | ||||||
return new self(); | ||||||
} | ||||||
|
||||||
public static function fromType(string $identifierName, TypeInterface $type): self | ||||||
mhsdesign marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
{ | ||||||
return new self(...[$identifierName => $type]); | ||||||
} | ||||||
|
||||||
public function getType(string $identifierName): ?TypeInterface | ||||||
{ | ||||||
return $this->types[$identifierName] ?? null; | ||||||
} | ||||||
|
||||||
/** | ||||||
* @template T | ||||||
* @param array<string|int,T> $array | ||||||
* @phpstan-assert-if-true array<string,T> $array | ||||||
*/ | ||||||
private static function isAssociativeArray(array $array): bool | ||||||
{ | ||||||
foreach ($array as $key => $value) { | ||||||
if (is_string($key)) { | ||||||
continue; | ||||||
} | ||||||
return false; | ||||||
} | ||||||
return true; | ||||||
} | ||||||
mhsdesign marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
<?php | ||
|
||
/** | ||
* PackageFactory.ComponentEngine - Universal View Components for PHP | ||
* Copyright (C) 2022 Contributors of PackageFactory.ComponentEngine | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU General Public License as published by | ||
* the Free Software Foundation, either version 3 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU General Public License | ||
* along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace PackageFactory\ComponentEngine\TypeSystem\Inferrer; | ||
mhsdesign marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
use PackageFactory\ComponentEngine\Definition\BinaryOperator; | ||
use PackageFactory\ComponentEngine\Parser\Ast\BinaryOperationNode; | ||
use PackageFactory\ComponentEngine\Parser\Ast\ExpressionNode; | ||
use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode; | ||
use PackageFactory\ComponentEngine\Parser\Ast\NullLiteralNode; | ||
use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface; | ||
|
||
/** | ||
* This class handles the analysis of identifier types that are used in a condition | ||
* and based on the requested branch: truthy or falsy, will predict the types a variable will have in the respective branch | ||
* so it matches the expected runtime behaviour | ||
* | ||
* For example given this expression: `nullableString ? "nullableString is not null" : "nullableString is null"` based on the condition `nullableString` | ||
* 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 | ||
* | ||
* The structure is partially inspired by phpstan | ||
* https://github.com/phpstan/phpstan-src/blob/07bb4aa2d5e39dafa78f56c5df132c763c2d1b67/src/Analyser/TypeSpecifier.php#L111 | ||
*/ | ||
class TypeInferrer | ||
mhsdesign marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
public function __construct( | ||
private readonly ScopeInterface $scope | ||
) { | ||
} | ||
|
||
public function inferTypesInCondition(ExpressionNode $conditionNode, TypeInferrerContext $context): InferredTypes | ||
grebaldi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
if ($conditionNode->root instanceof IdentifierNode) { | ||
$type = $this->scope->lookupTypeFor($conditionNode->root->value); | ||
if (!$type) { | ||
return InferredTypes::empty(); | ||
} | ||
// case `nullableString ? "nullableString is not null" : "nullableString is null"` | ||
return InferredTypes::fromType($conditionNode->root->value, $context->narrowDownType($type)); | ||
} | ||
|
||
if (($binaryOperationNode = $conditionNode->root) instanceof BinaryOperationNode) { | ||
// cases | ||
// `nullableString === null ? "nullableString is null" : "nullableString is not null"` | ||
// `nullableString !== null ? "nullableString is not null" : "nullableString is null"` | ||
if (count($binaryOperationNode->operands->rest) !== 1) { | ||
return InferredTypes::empty(); | ||
} | ||
$first = $binaryOperationNode->operands->first; | ||
$second = $binaryOperationNode->operands->rest[0]; | ||
|
||
$comparedIdentifierValueToNull = match (true) { | ||
// case `nullableString === null` | ||
$first->root instanceof IdentifierNode && $second->root instanceof NullLiteralNode => $first->root->value, | ||
// yodas case `null === nullableString` | ||
$first->root instanceof NullLiteralNode && $second->root instanceof IdentifierNode => $second->root->value, | ||
default => null | ||
}; | ||
|
||
if ($comparedIdentifierValueToNull === null) { | ||
return InferredTypes::empty(); | ||
} | ||
$type = $this->scope->lookupTypeFor($comparedIdentifierValueToNull); | ||
if (!$type) { | ||
return InferredTypes::empty(); | ||
} | ||
|
||
if ($binaryOperationNode->operator === BinaryOperator::EQUAL) { | ||
return InferredTypes::fromType($comparedIdentifierValueToNull, $context->negate()->narrowDownType($type)); | ||
} | ||
if ($binaryOperationNode->operator === BinaryOperator::NOT_EQUAL) { | ||
return InferredTypes::fromType($comparedIdentifierValueToNull, $context->narrowDownType($type)); | ||
} | ||
} | ||
mhsdesign marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return InferredTypes::empty(); | ||
} | ||
} |
grebaldi marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
<?php | ||
|
||
/** | ||
* PackageFactory.ComponentEngine - Universal View Components for PHP | ||
* Copyright (C) 2022 Contributors of PackageFactory.ComponentEngine | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU General Public License as published by | ||
* the Free Software Foundation, either version 3 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU General Public License | ||
* along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace PackageFactory\ComponentEngine\TypeSystem\Inferrer; | ||
|
||
use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType; | ||
use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType; | ||
use PackageFactory\ComponentEngine\TypeSystem\TypeInterface; | ||
|
||
enum TypeInferrerContext | ||
{ | ||
case TRUTHY; | ||
|
||
case FALSY; | ||
|
||
public function negate(): self | ||
{ | ||
return match ($this) { | ||
self::TRUTHY => self::FALSY, | ||
self::FALSY => self::TRUTHY | ||
}; | ||
} | ||
|
||
public function narrowDownType(TypeInterface $type): TypeInterface | ||
{ | ||
if (!$type instanceof UnionType || !$type->containsNull()) { | ||
return $type; | ||
} | ||
return match ($this) { | ||
self::TRUTHY => $type->withoutNull(), | ||
self::FALSY => NullType::get() | ||
}; | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.