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

Native type does not know anything about purity #3797

Merged
merged 8 commits into from
Feb 26, 2025

Conversation

VincentLanglet
Copy link
Contributor

Copy link
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

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

I disagree with this on a conceptual level. With a closure like fn () => sleep(5), only native types are involved and we still know it's impure.

With one-off fixes like that, we'd be chaising our own tail for a long time (finding more stuff to fix all the time, which is frustrating). I propose a different fix. In https://github.com/phpstan/phpstan-src/blob/2.1.x/src/Rules/PhpDoc/VarTagTypeRuleHelper.php there are about 9 isSuperTypeOf calls.

  1. Instead of calling them directly on a type, introduce a private method that accepts $type, $varTagType and calls isSuperTypeOf inside. That way we only need to change a single place.
  2. After calling this isSuperTypeOf, in case an error would be reported, run one more check. Take the $type (the native type of the expression), do Type::toPhpDocNode() and then create a Type again with TypeNodeResolver. This will "clean up" everything that's not expressible with PHPDocs.
  3. Call isSuperTypeOf again with the new $type that's the result of operation from 2).

@VincentLanglet
Copy link
Contributor Author

  1. After calling this isSuperTypeOf, in case an error would be reported, run one more check. Take the $type (the native type of the expression), do Type::toPhpDocNode() and then create a Type again with TypeNodeResolver. This will "clean up" everything that's not expressible with PHPDocs.

TypeNodeResolver::resolve needs a NameScope. How do I get one here ?

@ondrejmirtes
Copy link
Member

I think null is fine? Or an empty one. We won't be resolving any relative names, I hope.

@ondrejmirtes
Copy link
Member

Anyway, this is just a guess what could work, maybe it's still going to break for some cases.

@VincentLanglet
Copy link
Contributor Author

I think null is fine? Or an empty one. We won't be resolving any relative names, I hope.

Indeed.

I saw there was 5 occurences of new NameScope(null, []) do you think it could be useful to provide a
NameScope::createEmpty() method ?

image


private function isSuperTypeOfVarType(Type $type, Type $varTagType): TrinaryLogic
{
$type = $this->typeNodeResolver->resolve($type->toPhpDocNode(), new NameScope(null, []));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

After calling this isSuperTypeOf, in case an error would be reported, run one more check.

Seems like if I do this every time it works. I would say it's avoid two calls to isSuperTypeOf (?)

Also since sometimes checks are

!$this->isSuperTypeOfVarType($type, $varTagType)->yes()

and sometimes it's

$this->isSuperTypeOfVarType($type, $varTagType)->no()

It was unclear to me if I should have call toPhpDocNode for maybe results. So I did it every time.

Copy link
Member

Choose a reason for hiding this comment

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

Please do what I suggested. I don't want to break any existing use-cases, that's why I want to do the "new" thing right before when the original error message would appear. To decrease the number of reported errors, not to potentially incraese it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, but I don't understand how I'll know "an error would be reported" easily since sometime the usage will be
!$this->isSuperTypeOfVarType($type, $varTagType)->yes() and sometimes it will be
$this->isSuperTypeOfVarType($type, $varTagType)->no().

The following implementation

private function isSuperTypeOfVarType(Type $type, Type $varTagType): TrinaryLogic
	{
	    $result = $type->isSuperTypeOf($varTagType);
        if ($result->yes()) {
             return $result;
        }
	
		$type = $this->typeNodeResolver->resolve($type->toPhpDocNode(), new NameScope(null, []));

		return $type->isSuperTypeOf($varTagType);
	}

is not satisfying since for case where the usage is

$this->isSuperTypeOfVarType($type, $varTagType)->no()

I use the strategy toPhpDocNode for nothing since the first isSuperTypeOf wouldn't have report any error.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have the implementation
d8b9c48

But I dunno if it's satisfying enough


private function isSuperTypeOfVarType(Type $type, Type $varTagType): TrinaryLogic
{
$type = $this->typeNodeResolver->resolve($type->toPhpDocNode(), new NameScope(null, []));
Copy link
Member

Choose a reason for hiding this comment

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

Please do what I suggested. I don't want to break any existing use-cases, that's why I want to do the "new" thing right before when the original error message would appear. To decrease the number of reported errors, not to potentially incraese it.

@ondrejmirtes
Copy link
Member

We don't need to be too smart. We csn have two different methods - one for !yes(), one for no ().

@VincentLanglet
Copy link
Contributor Author

We don't need to be too smart. We csn have two different methods - one for !yes(), one for no ().

I created isSuperTypeOfVarType and isAtLeastMaybeSuperTypeOfVarType

@VincentLanglet
Copy link
Contributor Author

Friendly ping @ondrejmirtes ; this PR is ready to be re-reviewed :)

Copy link
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

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

I worry the empty NameScope might cause problems for generics. Can you please test it with @template T above a method and then using the T in inline @var PHPDoc tag?

With a failing test I'll come up with a solution to the problem.

@ondrejmirtes
Copy link
Member

We need a test that executes the line with $type:

		if ($type->isSuperTypeOf($varTagType)->yes()) {
			return true;
		}

		$type = $this->typeNodeResolver->resolve($type->toPhpDocNode(), new NameScope(null, []));

		return $type->isSuperTypeOf($varTagType)->yes();

Please debug what $type is before and after, ideally with $type->describe(VerbosityLevel::precise()).

@VincentLanglet
Copy link
Contributor Author

With a failing test I'll come up with a solution to the problem.

I fixed the test, it's failing now.

var_dump($type->describe(VerbosityLevel::precise()));
$type = $this->typeNodeResolver->resolve($type->toPhpDocNode(), new NameScope(null, []));
var_dump($type->describe(VerbosityLevel::precise()));

gives Closure(): list<T> before and after

@ondrejmirtes
Copy link
Member

Now instead of new NameScope(null, []), you need to get the right NameScope by doing:

			$function = $scope->getFunction();
			$nameScope = $this->fileTypeMapper->getNameScope(
				$scope->getFile(),
				$scope->isInClass() ? $scope->getClassReflection()->getName() : null,
				$scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
				$function !== null ? $function->getName() : null,
			);

It's a new method I just added in 1.12.x.

@VincentLanglet VincentLanglet force-pushed the nativeTypePure branch 2 times, most recently from dd605c7 to f0d2c7a Compare February 26, 2025 13:24
@VincentLanglet
Copy link
Contributor Author

Now instead of new NameScope(null, []), you need to get the right NameScope by doing:

			$function = $scope->getFunction();
			$nameScope = $this->fileTypeMapper->getNameScope(
				$scope->getFile(),
				$scope->isInClass() ? $scope->getClassReflection()->getName() : null,
				$scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
				$function !== null ? $function->getName() : null,
			);

It's a new method I just added in 1.12.x.

Thanks ! It works well.

try {
$type = $this->typeNodeResolver->resolve($type->toPhpDocNode(), $this->createNameScope($scope));
} catch (NameScopeAlreadyBeingCreatedException) {
return false;
Copy link
Member

Choose a reason for hiding this comment

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

Does this mean it's going to be reported, or it's not going to be reported? I'd rather opt for failing silently (to not report an error).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I didn't know exactly what this exception means.

Before my PR we were doing just $type->isSuperTypeOf($varTagType)->no().

Does this mean it's going to be reported,

Yes. But the error was also reported before this PR. This won't introduce new error.
That's why I didn't chose the failing silently solution.

Now, we doing

  • If $type->isSuperTypeOf($varTagType)->no() was OK before, early return
  • Try to create a namescope
    • If we cannot create a namescope => Report an error (But this error is currently already reported by PHPStan because the check $type->isSuperTypeOf($varTagType)->no() was not ok).
    • If we can create a namescope => use it to remove some false-positive.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you still prefer, I return true ?

Copy link
Member

Choose a reason for hiding this comment

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

Yes. This would be an error the user cannot do with much. So I don't want to report it.

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 5668c05 into phpstan:1.12.x Feb 26, 2025
309 checks passed
@ondrejmirtes
Copy link
Member

Perfect, thank you! From the method naming to the logic, this is top-notch!

@VincentLanglet
Copy link
Contributor Author

Sure, I opened #3841

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.

@var with type Closure(): list<HelloWorld> is not subtype of native type Closure(): array
2 participants