Skip to content
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

Fix issue with template of union #3535

Merged
merged 4 commits into from
Nov 6, 2024

Conversation

VincentLanglet
Copy link
Contributor

Closes phpstan/phpstan#11663

Cf phpstan/phpstan#11663 (comment) I dunno if there is a already-known better way to handle such situation.

@@ -5602,8 +5603,10 @@ private function filterTypeWithMethod(Type $typeWithMethod, string $methodName):
{
if ($typeWithMethod instanceof UnionType) {
$newTypes = [];
$oneTypeHasBeenRemoved = false;
Copy link
Contributor

@staabm staabm Oct 4, 2024

Choose a reason for hiding this comment

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

I think whether this is a problem which should be solved with a Type method.

maybe we can use a Type method in which you pass in a callable and it returns a preserved union type (maybe such method even exists...?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The whole method filterTypeWithMethod could be introduce as something like Type::filter if wanted but this would "only" be a refacto. (Which may be in another PR)

There is UnionType::traverse

public function traverse(callable $cb): Type
	{
		$types = [];
		$changed = false;

		foreach ($this->types as $type) {
			$newType = $cb($type);
			if ($type !== $newType) {
				$changed = true;
			}
			$types[] = $newType;
		}

		if ($changed) {
			return TypeCombinator::union(...$types);
		}

		return $this;
	}

but it doesn't do the exact same thing ; we can still notice a changed check is done the same way.

Copy link
Contributor

@herndlm herndlm Oct 4, 2024

Choose a reason for hiding this comment

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

don't want to disturb you guys too much, but another idea to throw in - does TypeTraverser::map() help in any way here maybe? e.g. the types you don't want you could replace with a NeverType potentially? that should leave the outer TemplateUnionType untouched at least. that could be a replacement for the foreach, AFAIK TypeCombinator::union() should keep the TemplateUnionType then and remove the never I suppose.

but not sure if that makes it even more complicated potentially 😅

Copy link
Member

Choose a reason for hiding this comment

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

I get it, but the variable name should be different. Something like $unionTypeHasChanged.

Also it's a lot of repeated code. I'd welcome some refactoring first, and then the change should be done in a single place.

@VincentLanglet
Copy link
Contributor Author

Hi, what do you think about this PR @ondrejmirtes ?

Thanks

@ondrejmirtes
Copy link
Member

I have no idea how $oneTypeHasBeenRemoved solves a problem so I don't want to merge it right now.

@VincentLanglet
Copy link
Contributor Author

I have no idea how $oneTypeHasBeenRemoved solves a problem so I don't want to merge it right now.

The idea was kinda similar to UnionType::traverse

public function traverse(callable $cb): Type
	{
		$types = [];
		$changed = false;

		foreach ($this->types as $type) {
			$newType = $cb($type);
			if ($type !== $newType) {
				$changed = true;
			}
			$types[] = $newType;
		}

		if ($changed) {
			return TypeCombinator::union(...$types);
		}

		return $this;
	}

the goal is to keep the same object when nothing change (here when no type is removed).

Because the existing code

if ($typeWithMethod instanceof UnionType) {
			$newTypes = [];
            foreach ($typeWithMethod->getTypes() as $innerType) {
				if (!$innerType->hasMethod($methodName)->yes()) {
					continue;
				}

				$newTypes[] = $innerType;
			}
			if (count($newTypes) === 0) {
				return null;
			}
			$typeWithMethod = TypeCombinator::union(...$newTypes);

always change the instance of typeWithMethod when it's an UnionType, which means:

  • BenevolentUnion become an UnionType
  • TemplateUnion become an UnionType

But when oneTypeHasBeenRemoved = false we could keep the initial instance of UnionType since we keep all the subTypes. This way, for the existing issue, we keep the TemplateUnion we all the needed informations.

@@ -5602,8 +5603,10 @@ private function filterTypeWithMethod(Type $typeWithMethod, string $methodName):
{
if ($typeWithMethod instanceof UnionType) {
$newTypes = [];
$oneTypeHasBeenRemoved = false;
Copy link
Member

Choose a reason for hiding this comment

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

I get it, but the variable name should be different. Something like $unionTypeHasChanged.

Also it's a lot of repeated code. I'd welcome some refactoring first, and then the change should be done in a single place.

@VincentLanglet
Copy link
Contributor Author

PR is simplified now

@ondrejmirtes
Copy link
Member

We could improve this further, right? Even if something changes in UnionType subtype, in this method we could make sure the correct class is created.

The subtypes from the top of my head are:

  • BenevolentUnionType
  • TemplateUnionType
  • TemplateBenevolentUnionType

@VincentLanglet
Copy link
Contributor Author

We could improve this further, right? Even if something changes in UnionType subtype, in this method we could make sure the correct class is created.

The subtypes from the top of my head are:

  • BenevolentUnionType
  • TemplateUnionType
  • TemplateBenevolentUnionType

I understand the use case for BenevolentUnion (__benevolent(A|B|C) become __benevolent(A|B)), but it's complicated for me to see the use case for TemplateUnionType and TemplateBenevolentUnionType.
If we have T of A|B|C it feel wrong to change this to T of A|B isn't it ?

Do you have an example to add to the tests ?

@ondrejmirtes
Copy link
Member

Examples:

if ($this instanceof BenevolentUnionType) {
$result = TypeUtils::toBenevolentUnion($result);
}
if ($this instanceof TemplateType) {
Copy link
Member

Choose a reason for hiding this comment

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

I'd rather have this logic in the subtypes in overridden methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@ondrejmirtes ondrejmirtes merged commit d999117 into phpstan:1.12.x Nov 6, 2024
451 checks passed
@ondrejmirtes
Copy link
Member

Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants