Skip to content

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

Draft
wants to merge 30 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7efcfd3
TASK: Put domain logic inside constructor of UnionType
mhsdesign Apr 9, 2023
2ffe099
TASK: Simple Nullable Handling
mhsdesign Apr 9, 2023
6293145
TASK: Infer types in arms of ternary
mhsdesign Apr 9, 2023
2a08474
TASK: Revert 7efcfd3cc179443912b5e06d1b913efde3cb5739
mhsdesign Apr 9, 2023
5e4d6ce
TASK: Union test that all members are deduplicated
mhsdesign Apr 9, 2023
5c08a4e
TASK: Union test isNullable and withoutNullable
mhsdesign Apr 9, 2023
8f0ba11
TASK: UnionType rename to containsNull, withoutNull
mhsdesign Apr 21, 2023
8a34ee1
TASK: Introduce TernaryBranchScope
mhsdesign Apr 21, 2023
1b476e5
TASK: Type inference for null comparison in ternary
mhsdesign Apr 21, 2023
57ee20c
TASK: UnionType RequiresAtLeastOneMember
mhsdesign Apr 21, 2023
3487bd9
TASK: TernaryBranchScope introduce static factories and dont throw bo…
mhsdesign Apr 21, 2023
1348ac1
TASK: Make type inference in TernaryBranchScope more explicit
mhsdesign Apr 22, 2023
492e05c
TASK: Adjust naming of $nonNullable to $typeWithoutNull
mhsdesign Apr 22, 2023
0c7061f
TASK: Solve #7 rudimentary
mhsdesign Apr 22, 2023
22df4ba
TASK: Introduce TypeInferrer inspired by phpstan to support future ad…
mhsdesign Apr 22, 2023
0adb4c0
TASK: Cleanup InferredTypes and extract duplicated logic to TypeInfer…
mhsdesign Apr 23, 2023
f61b896
TASK: Remove `@phpstan-ignore-next-line` by asserting that an array i…
mhsdesign Apr 23, 2023
6a8bd00
Merge remote-tracking branch 'origin/main' into task/simpleNullableHa…
mhsdesign Apr 23, 2023
d835fa5
TASK: UnionType::getIterator use `yield from`
mhsdesign Apr 26, 2023
88c6fd6
TASK: Rename `Inferrer` to `Narrower` and apply further suggestions f…
mhsdesign Apr 26, 2023
e0a913a
TASK: `Narrower` handle boolean literal comparisons
mhsdesign Apr 26, 2023
aaf6c49
TASK: `Narrower` null comparison against any expression that resolves…
mhsdesign Apr 26, 2023
560c97d
TASK: Add `ExpressionTypeNarrowerTest`
mhsdesign Apr 26, 2023
d00a194
TASK: Don't narrow `nullableString === true` as string
mhsdesign Apr 26, 2023
530b155
TASK: Narrow `nullableString && true`
mhsdesign Apr 26, 2023
331cdda
TASK: Correct namespace
mhsdesign Apr 26, 2023
bd28d87
Merge remote-tracking branch 'origin' into task/simpleNullableHandling
mhsdesign Apr 29, 2023
1027e16
TASK: Adjust to BinaryOperationNode api change
mhsdesign Apr 29, 2023
884b895
TASK: Apply suggestions from code review
mhsdesign Apr 29, 2023
0121247
TASK: ExpressionTypeNarrower support UnaryOperationNode
mhsdesign Apr 29, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
use PackageFactory\ComponentEngine\TypeSystem\Type\SlotType\SlotType;
use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType;
use PackageFactory\ComponentEngine\TypeSystem\Type\StructType\StructType;
use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType;
use PackageFactory\ComponentEngine\TypeSystem\TypeInterface;

final class TypeReferenceTranspiler
{
Expand All @@ -43,22 +45,46 @@ public function __construct(
public function transpile(TypeReferenceNode $typeReferenceNode): string
{
$type = $this->scope->resolveTypeReference($typeReferenceNode);
$phpTypeReference = match ($type::class) {

return match ($type::class) {
UnionType::class => $this->transpileUnionType($type, $typeReferenceNode),
default => $this->transpileNonUnionType($type, $typeReferenceNode)
};
}

private function transpileUnionType(UnionType $unionType, TypeReferenceNode $typeReferenceNode): string
{
if ($unionType->isNullable()) {
$nonNullable = $unionType->withoutNullable();
if ($nonNullable instanceof UnionType) {
throw new \Exception('@TODO Transpilation of nullable union types with more non null members is not implemented');
}
return $this->transpileNullableType($nonNullable, $typeReferenceNode);
}

throw new \Exception('@TODO Transpilation of complex union types is not implemented');
}

private function transpileNonUnionType(TypeInterface $type, TypeReferenceNode $typeReferenceNode): string
{
return match ($type::class) {
NumberType::class => 'int|float',
StringType::class => 'string',
BooleanType::class => 'bool',
SlotType::class => $this->strategy->getPhpTypeReferenceForSlotType($type, $typeReferenceNode),
ComponentType::class => $this->strategy->getPhpTypeReferenceForComponentType($type, $typeReferenceNode),
EnumType::class => $this->strategy->getPhpTypeReferenceForEnumType($type, $typeReferenceNode),
StructType::class => $this->strategy->getPhpTypeReferenceForStructType($type, $typeReferenceNode),
UnionType::class => throw new \Exception("There is no such thing as nested unions, think again."),
default => $this->strategy->getPhpTypeReferenceForCustomType($type, $typeReferenceNode)
};
}

return $typeReferenceNode->isOptional
? match ($phpTypeReference) {
'int|float' => 'null|int|float',
default => '?' . $phpTypeReference
}
: $phpTypeReference;
private function transpileNullableType(TypeInterface $type, TypeReferenceNode $typeReferenceNode): string
{
if ($type->is(NumberType::get())) {
return 'null|int|float';
}
return '?' . $this->transpileNonUnionType($type, $typeReferenceNode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@
namespace PackageFactory\ComponentEngine\TypeSystem\Resolver\TernaryOperation;

use PackageFactory\ComponentEngine\Parser\Ast\BooleanLiteralNode;
use PackageFactory\ComponentEngine\Parser\Ast\IdentifierNode;
use PackageFactory\ComponentEngine\Parser\Ast\TernaryOperationNode;
use PackageFactory\ComponentEngine\TypeSystem\Resolver\Expression\ExpressionTypeResolver;
use PackageFactory\ComponentEngine\TypeSystem\Scope\ShallowScope\ShallowScope;
use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface;
use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType;
use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType;
use PackageFactory\ComponentEngine\TypeSystem\TypeInterface;

Expand All @@ -41,14 +44,39 @@ public function resolveTypeOf(TernaryOperationNode $ternaryOperationNode): TypeI
$expressionTypeResolver = new ExpressionTypeResolver(
scope: $this->scope
);
$conditionNode = $ternaryOperationNode->condition->root;
$conditionNode = $ternaryOperationNode->condition;

if ($conditionNode instanceof BooleanLiteralNode) {
return $conditionNode->value
$rootType = $expressionTypeResolver->resolveTypeOf($conditionNode);

if ($conditionNode->root instanceof BooleanLiteralNode) {
return $conditionNode->root->value
? $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->true)
: $expressionTypeResolver->resolveTypeOf($ternaryOperationNode->false);
}

if ($conditionNode->root instanceof IdentifierNode && $rootType instanceof UnionType && $rootType->isNullable()) {
$trueExpressionTypeResolver = new ExpressionTypeResolver(
scope: new ShallowScope(
$conditionNode->root->value,
$rootType->withoutNullable(),
$this->scope
)
);

$falseExpressionTypeResolver = new ExpressionTypeResolver(
scope: new ShallowScope(
$conditionNode->root->value,
NullType::get(),
$this->scope
)
);

return UnionType::of(
$trueExpressionTypeResolver->resolveTypeOf($ternaryOperationNode->true),
$falseExpressionTypeResolver->resolveTypeOf($ternaryOperationNode->false)
);
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It pains me to object to this, because I really, really like the idea. Yet, I think the cost of introducing ShallowScope for this is too high.

My reasoning here goes as follows:

The problem that the code above attempts to solve is that of deduction of a non-nullable subtype from a nullable union type given its falsiness has been determined for a ternary expression branch. The conditions in line 57 further narrow the specificity of the problem thusly:

  1. The ternary condition must consist of exactly one identifier
  2. The type of the ternary condition must be a nullable union-type

I would say that this is a very specific case. (Indeed a tad too specific, but more on that later)

Now, to make the deduction visible to the ternary branches, their respective type resolvers are instantiated each with a ShallowScope instance that overrides the type that the identifier in the ternary condition points to.

I would say that ShallowScope is a very general abstraction.

The point of the scope-system is to give total control over the mode of type resolution to specific language constructs that in the mental model of the reader/writer intuitively invoke the notion of "scope". Conceptually, I would say this prohibits the existence of more abstract scope implementations that would have no correspondence to said mental model.

Fortunately, this could be remedied rather easily, by renaming ShallowScope to TernaryBranchScope. (Since in the above scenario ternary branches would carry deduced type information, I find it absolutely fair to say that there is such a thing as a TernaryBranchScope)

This leaves us with the problem of specificity introduced by the condition in line 57. Take these two examples:

(1)

component Example {
  a: ?string

  return a ? "a is not null" : "a is null"
}

(2)

component Example {
  a: ?string
  b: boolean

  return a && b ? "a is not null" : "a may yet be null"
}

In both cases, reader's intuition would be able to tell that a is not null in the true branch. But due to the limitation through the condition in line 57, the type system would acknowledge this in case (1) but not in case (2). I would argue that this runs counter reader's intuition, who would expect this behavior to be consistent.

The more I think about it, the more this deduction problem sounds like a job for the aforementioned TernaryBranchScope :)

TernaryBranchScope may very well be aware of the entire expression of the ternary condition. Then if asked for the type of an identifier, it could then and there deduce a narrower type.

So, to conclude this: I'd like to suggest the introduction of TernaryBranchScope (as a replacement for ShallowScope) in the scope of this PR. It should take on the task of deduction as I've described it, but for now it would be fine to keep the current limitations on that algorithm and leave a TODO-comment, so it may be refined later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented :D But now im too tired to type things down that i encountered, see you tomorrow.

(And thanks for the tipp with TernaryBranchScope works like a charm and even better ^^)

Also i cant thank you enough for your most precise code reviews and comments - they are extremely helpful and well guiding me! Thank you really so much!!!

Copy link
Contributor Author

@mhsdesign mhsdesign Apr 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes youre right, my implementation is only limited for super simple things like foo ? foo : ""

i thought about how we can i implement a universal solution, which also handles any thinkable more complex case, but it seems like a hard challenge

for example given foo is a nullable string.

those expressions will be resolved as of now correctly:

foo ? foo : ""
foo !== null ? foo : ""

but here we loose it:

(foo) ? foo : ""
((foo)) ? foo : ""
foo !== null === true ? foo : ""
!foo ? "a" : "b"
!!foo ? "a" : "b"
foo !== nullVariable ? foo : ""
(foo !== null) ? foo : ""
foo && true ? foo : ""
(foo ? foo : foo) ? foo : ""
((foo ? foo : foo) ? foo : foo) ? foo : ""

I checked all the examples with typescript, and it does a good job (unsurprisingly). I even went as far as trying to have a look at the ts codebase for some inspiration, but i couldnt even find the right unit tests to start with xD

Some of the above are surely not a real world use case but its a simple demonstration how easy it is to confuse the implemented type resolution (or type inference?).

I think i need to find out while resolving the condition part, which variable participated and how it affected the truthyness. Im not really sure if this would be the right approach in the long run and if it can be easily implemented.

But for now im also quite happy with my current implementation as a start.

Copy link
Contributor Author

@mhsdesign mhsdesign Apr 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found the place where phpstan does it:
https://github.com/phpstan/phpstan-src/blob/07bb4aa2d5e39dafa78f56c5df132c763c2d1b67/src/Analyser/MutatingScope.php#L1616

// $node is a TernaryExpression with the fields: cond, if and else

$booleanConditionType = $this->getType($node->cond)->toBoolean();
if ($booleanConditionType->isTrue()->yes()) {
     return $this->filterByTruthyValue($node->cond)->getType($node->if);
}

if ($booleanConditionType->isFalse()->yes()) {
    return $this->filterByFalseyValue($node->cond)->getType($node->else);
}

return TypeCombinator::union(
     $this->filterByTruthyValue($node->cond)->getType($node->if),
     $this->filterByFalseyValue($node->cond)->getType($node->else),
);

interestingly phpstan's code is quite clever but not clever to not fail for the bait (foo ? foo : foo) ? foo : "":
https://phpstan.org/r/3b012ae8-4dd9-4901-95c0-45587f70134e

also psalm fails for the bait
https://psalm.dev/r/c536f4dab4

but psalm correctly works with your example:

/** @var bool $bool */
$bool = true;
$bar = $foo && $bool ? PHPStan\Testing\assertType("non-falsy-string", $foo) : PHPStan\Testing\assertType("string|null", $foo);

and i couldnt find another way (except the ternary thing) to confuse the type resolution - its really good.


with 22df4ba

i kind of stole the basic idea and structure of phpstan and it should be capable to implement in the future more complex inference - but first i need to build support for negation !foo


it came just to my mind that it will be fun to implement type inference for nullable struct members ... foo.bar ? 1 : 0 or even foo.bar?.buz ? 1 : 0

return UnionType::of(
$expressionTypeResolver->resolveTypeOf($ternaryOperationNode->true),
$expressionTypeResolver->resolveTypeOf($ternaryOperationNode->false)
Expand Down
9 changes: 7 additions & 2 deletions src/TypeSystem/Scope/GlobalScope/GlobalScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@

namespace PackageFactory\ComponentEngine\TypeSystem\Scope\GlobalScope;

use PackageFactory\ComponentEngine\Parser\Ast\ComponentDeclarationNode;
use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode;
use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface;
use PackageFactory\ComponentEngine\TypeSystem\Type\BooleanType\BooleanType;
use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType;
use PackageFactory\ComponentEngine\TypeSystem\Type\NumberType\NumberType;
use PackageFactory\ComponentEngine\TypeSystem\Type\SlotType\SlotType;
use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType;
use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType;
use PackageFactory\ComponentEngine\TypeSystem\TypeInterface;

final class GlobalScope implements ScopeInterface
Expand All @@ -51,12 +52,16 @@ public function lookupTypeFor(string $name): ?TypeInterface

public function resolveTypeReference(TypeReferenceNode $typeReferenceNode): TypeInterface
{
return match ($typeReferenceNode->name) {
$type = match ($typeReferenceNode->name) {
'string' => StringType::get(),
'number' => NumberType::get(),
'boolean' => BooleanType::get(),
'slot' => SlotType::get(),
default => throw new \Exception('@TODO: Unknown Type ' . $typeReferenceNode->name)
};
if ($typeReferenceNode->isOptional) {
$type = UnionType::of($type, NullType::get());
}
return $type;
}
}
8 changes: 7 additions & 1 deletion src/TypeSystem/Scope/ModuleScope/ModuleScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
use PackageFactory\ComponentEngine\Parser\Ast\ModuleNode;
use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode;
use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface;
use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType;
use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType;
use PackageFactory\ComponentEngine\TypeSystem\TypeInterface;

final class ModuleScope implements ScopeInterface
Expand All @@ -45,7 +47,11 @@ public function lookupTypeFor(string $name): ?TypeInterface
public function resolveTypeReference(TypeReferenceNode $typeReferenceNode): TypeInterface
{
if ($importNode = $this->moduleNode->imports->get($typeReferenceNode->name)) {
return $this->loader->resolveTypeOfImport($importNode);
$type = $this->loader->resolveTypeOfImport($importNode);
if ($typeReferenceNode->isOptional) {
$type = UnionType::of($type, NullType::get());
}
return $type;
}

if ($this->parentScope) {
Expand Down
51 changes: 51 additions & 0 deletions src/TypeSystem/Scope/ShallowScope/ShallowScope.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?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\Scope\ShallowScope;

use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode;
use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface;
use PackageFactory\ComponentEngine\TypeSystem\TypeInterface;

final class ShallowScope implements ScopeInterface
{
public function __construct(
private readonly string $overriddenName,
private readonly TypeInterface $overriddenType,
private readonly ScopeInterface $parentScope
) {
}

public function lookupTypeFor(string $name): ?TypeInterface
{
if ($this->overriddenName === $name) {
return $this->overriddenType;
}

return $this->parentScope->lookupTypeFor($name);
}

public function resolveTypeReference(TypeReferenceNode $typeReferenceNode): TypeInterface
{
return $this->parentScope->resolveTypeReference($typeReferenceNode);
}
}
53 changes: 48 additions & 5 deletions src/TypeSystem/Type/UnionType/UnionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,24 @@

namespace PackageFactory\ComponentEngine\TypeSystem\Type\UnionType;

use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType;
use PackageFactory\ComponentEngine\TypeSystem\TypeInterface;

final class UnionType implements TypeInterface
/**
* @implements \IteratorAggregate<int, TypeInterface>
*/
final class UnionType implements TypeInterface, \IteratorAggregate, \Countable
{
/**
* @var TypeInterface[]
*/
private array $members;
private readonly array $members;

private function __construct(TypeInterface ...$members)
{
if (count($members) < 1) {
throw new \Exception('UnionType can only hold more than one different members');
}
$this->members = $members;
}

Expand All @@ -53,7 +60,32 @@ public static function of(TypeInterface ...$members): TypeInterface
return $uniqueMembers[0];
}

return new self(...$members);
return new self(...$uniqueMembers);
}

public function isNullable(): bool
{
foreach ($this->members as $member) {
if ($member->is(NullType::get())) {
return true;
}
}
return false;
}

public function withoutNullable(): TypeInterface
{
$nonNullMembers = [];
foreach ($this->members as $member) {
if ($member->is(NullType::get())) {
continue;
}
$nonNullMembers[] = $member;
}
if (count($nonNullMembers) === 1) {
return $nonNullMembers[0];
}
return self::of(...$nonNullMembers);
}

public function is(TypeInterface $other): bool
Expand All @@ -67,15 +99,26 @@ public function is(TypeInterface $other): bool
break;
}
}

if (!$match) {
return false;
}
}

return true;
} else {
return false;
}
}

/** @return \ArrayIterator<int, TypeInterface> */
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->members);
}

public function count(): int
{
return count($this->members);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use PackageFactory\ComponentEngine\Parser\Ast\TernaryOperationNode;
use PackageFactory\ComponentEngine\Test\Unit\TypeSystem\Scope\Fixtures\DummyScope;
use PackageFactory\ComponentEngine\TypeSystem\Resolver\TernaryOperation\TernaryOperationTypeResolver;
use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType;
use PackageFactory\ComponentEngine\TypeSystem\Type\NumberType\NumberType;
use PackageFactory\ComponentEngine\TypeSystem\Type\StringType\StringType;
use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType;
Expand All @@ -46,7 +47,13 @@ public function ternaryOperationExamples(): array
'1 < 2 ? variableOfTypeString : variableOfTypeNumber' => [
'1 < 2 ? variableOfTypeString : variableOfTypeNumber',
UnionType::of(NumberType::get(), StringType::get())
]
],
'nullableString ? nullableString : "fallback"' => [
'nullableString ? nullableString : "fallback"', StringType::get()
],
'nullableString ? null : nullableString' => [
'nullableString ? null : nullableString', NullType::get()
],
];
}

Expand All @@ -62,6 +69,7 @@ public function resolvesTernaryOperationToResultingType(string $ternaryExpressio
$scope = new DummyScope([
'variableOfTypeString' => StringType::get(),
'variableOfTypeNumber' => NumberType::get(),
'nullableString' => UnionType::of(StringType::get(), NullType::get())
]);
$ternaryOperationTypeResolver = new TernaryOperationTypeResolver(
scope: $scope
Expand Down
5 changes: 5 additions & 0 deletions test/Unit/TypeSystem/Scope/Fixtures/DummyScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@

use PackageFactory\ComponentEngine\Parser\Ast\TypeReferenceNode;
use PackageFactory\ComponentEngine\TypeSystem\ScopeInterface;
use PackageFactory\ComponentEngine\TypeSystem\Type\NullType\NullType;
use PackageFactory\ComponentEngine\TypeSystem\Type\UnionType\UnionType;
use PackageFactory\ComponentEngine\TypeSystem\TypeInterface;

final class DummyScope implements ScopeInterface
Expand All @@ -46,6 +48,9 @@ public function lookupTypeFor(string $name): ?TypeInterface
public function resolveTypeReference(TypeReferenceNode $typeReferenceNode): TypeInterface
{
if ($type = $this->typeNameToTypeMap[$typeReferenceNode->name] ?? null) {
if ($typeReferenceNode->isOptional) {
$type = UnionType::of($type, NullType::get());
}
return $type;
}

Expand Down
Loading