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 native types2 #1802

Closed
wants to merge 22 commits into from
Closed

Fix native types2 #1802

wants to merge 22 commits into from

Conversation

rajyan
Copy link
Contributor

@rajyan rajyan commented Oct 8, 2022

closes #1535
closes #1627

and a lot of native type related issues!

Native types should always be specified along phpdoc types.
Also, there should be only a single treatPhpDocTypesAsCertain handling to switch between variableTypes and nativeTypeExpression

if (!$this->treatPhpDocTypesAsCertain) {
if (!array_key_exists(sprintf('$%s', $variableName), $this->nativeExpressionTypes)) {
return new MixedType();
}
return $this->nativeExpressionTypes[sprintf('$%s', $variableName)];
}

With this pull request, native types and phpdoc types are completely separated by treatPhpDocTypesAsCertain.

I hope I've understood
#1372 (comment)

So - if you're interested in $treatPhpDocTypesAsCertain=true, call Scope::getType(), if false, call Scope::getNativeType(). Can you imagine this working? Thanks :)

this correctly...

@rajyan
Copy link
Contributor Author

rajyan commented Oct 8, 2022

I think it’s almost done. Several native type specification are missing.

@rajyan
Copy link
Contributor Author

rajyan commented Oct 8, 2022

Finally done it?
I hope this pull request makes sense:smile:

@rajyan rajyan changed the base branch from 1.8.x to 1.9.x October 8, 2022 15:08
@rajyan
Copy link
Contributor Author

rajyan commented Oct 8, 2022

I prefer heading to 1.9.x because it's gonna be a huge change for treatPhpDocTypesAsCertain: false

@rajyan
Copy link
Contributor Author

rajyan commented Oct 8, 2022

The failing tests are from treatPhpDocTypesAsCertain: false. I've haven't checked all errors yet, but looks mostly correct to me.

@rajyan rajyan marked this pull request as ready for review October 8, 2022 15:12
@herndlm
Copy link
Contributor

herndlm commented Oct 8, 2022

I don't want to disturb the discussion here too much, so it's not important, but do you maybe know how this relates to #1781 (review)? Would there be more changes needed or should that work already then?

@rajyan
Copy link
Contributor Author

rajyan commented Oct 8, 2022

@herndlm
Should be fixed:+1:

@rajyan
Copy link
Contributor Author

rajyan commented Oct 8, 2022

I’ll add some regression tests tomorrow:)

@ondrejmirtes
Copy link
Member

I'm looking forward to review this, looks promising 😊

@ondrejmirtes
Copy link
Member

Alright, I like some parts but don't like some others :)

I'd like to establish some ground rules that should be true when this PR is in progress and after it's merged:

  • Scope::getType() returns "complete type" or "full type", however you want to call it.
  • Scope::getNativeType() returns "native type".
  • Scope/MutatingScope shouldn't be interested in treatPhpDocTypesAsCertain at all. Meaning it's OK to delete doNotTreatPhpDocTypesAsCertain() method, even promoteNativeTypes(). Well, doNotTreatPhpDocTypesAsCertain() should be kept for backward compatibility, but deprecated in favour of calling Scope::getNativeType(). It's OK if it does return $this;.
  • MutatingScope::getNativeType() should mirror MutatingScope::getType(). At the end of the method (for unhandled expression types), it shouldn't call getType() (because that's lying), but instead return new MixedType().
  • Scope has to keep two sets of types (complete types and full types) for all operations. Besides $moreSpecificTypes property we'll also need $moreSpecificNativeTypes. For example filterBySpecifiedTypes() has to operate on these two. Let's take this example:
/**
 * @param positive-int|non-empty-string $a
 */
function foo(int|string $a): void
{
    assertType('positive-int|non-empty-string', $a);
    assertNativeType('int|string', $a);
    if (is_string($a)) {
        assertType('non-empty-string', $a);
        assertNativeType('string', $a);
    }
}

The implementation of MutatingScope::getNativeType() has to handle all the same expressions MutatingScope::getType() handles, but slightly differently. Here are some examples:

if ($node instanceof Expr\BinaryOp\Smaller) {
    return $this->getNativeType($node->left)->isSmallerThan($this->getNativeType($node->right))->toBooleanType();
}
if ($node instanceof Node\Expr\BitwiseNot) {
    return $this->initializerExprTypeResolver->getBitwiseNotType($node->expr, fn (Expr $expr): Type => $this->getNativeType($expr));
}
  • For FuncCall/MethodCall/StaticCall/New_, MutatingScope::getType() does a lot of complicated things, like calling dynamic return type extensions and resolving generics. These expressions in MutatingScope::getNativeType() will be handled completely differently. For example, MethodCall has to get ClassReflection from ReflectionProvider, ask about hasNativeMethod(), get the getNativeMethod() and ask for native return type. (This will need getNativeReturnType() added on ExtendedMethodReflection - should be a separate PR that's gonna be merged/reviewed first. Right now the only ExtendedMethodReflection impl that has this method is PhpMethodReflection.)
  • What's a bit worrying for me now is that you changed and written a lot of code but there isn't a single new test using assertNativeType. This is gonna have to be tested a lot. Like the things around array dim fetch assign (NodeScopeResolver around line 3400), it's ideal to test them with assertNativeType.
  • I don't understand how the rules can still do their job if they don't know what the value of $treatPhpDocTypesAsCertain is. I imagine that ConstantConditionRuleHelper should continue to use $treatPhpDocTypesAsCertain and do their job with logic as $this->treatPhpDocTypesAsCertain ? $this->getType($expr) : $this->getNativeType($expr). If you deleted this logic, I don't know where it went and why (if) it still works as it should.

$this->analyse([__DIR__ . '/data/bug-7622.php'], [
[
'Match expression does not handle remaining value: string',
21,
Copy link
Member

Choose a reason for hiding this comment

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

This is wrong

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this? I'm not sure if native type can know that the match is exhaustive.

Copy link
Member

Choose a reason for hiding this comment

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

But the rule always asks getType, not getNativeType. The pupose of the setting treat... to false is to remove errors like "this if is always false" (when code checks again something that's only expressed in PHPDocs and not checked at runtime), not add new errors.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok, I think

The pupose of the setting treat... to false is to remove errors like "this if is always false"

was what I was totally misunderstanding.

My thinking was to switch between native types and full types by treatPhpDocTypesAsCertain

like,
treatPhpDocTypesAsCertain: true
getNativeType() => native type
getType() => full types

treatPhpDocTypesAsCertain: false
getNativeType() = getType() => native type

I now understood that when a rule error is

- full type native type
1 no error no error
2 error no error
3 no error error
4 error error

purpose of treatPhpDocTypesAsCertain is to relax cases like 2.

Copy link
Member

Choose a reason for hiding this comment

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

I think the docs are clear on this 😊 "relaxes" https://phpstan.org/config-reference#treatphpdoctypesascertain

@@ -123,89 +114,8 @@ public function testRule(): void
]);
}

public function testRuleWithoutTreatPhpDocTypesAsCertain(): void
Copy link
Member

Choose a reason for hiding this comment

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

Deleting a test is a no-go :) It should continue working as before.

@ondrejmirtes
Copy link
Member

It's interesting to know that the example with type narrowing on variables already works: https://phpstan.org/r/59b890fb-466d-4926-b060-15859b520c63

That's because today there's MutatingScope::$variableTypes and MutatingScope::$nativeExpressionTypes. It should actually be called MutatingScope::$variableNativeTypes because that's what it is.

The test wouldn't work for other expressions like property fetches and array dim fetches. That's why we need MutatingScope::$moreSpecificNativeTypes alongside MutatingScope::$moreSpecificTypes.

@rajyan
Copy link
Contributor Author

rajyan commented Oct 8, 2022

@rajyan
Copy link
Contributor Author

rajyan commented Oct 8, 2022

@ondrejmirtes
Thank you for the review!

Scope/MutatingScope shouldn't be interested in treatPhpDocTypesAsCertain at all. Meaning it's OK to delete doNotTreatPhpDocTypesAsCertain() method, even promoteNativeTypes(). Well, doNotTreatPhpDocTypesAsCertain() should be kept for backward compatibility, but deprecated in favour of calling Scope::getNativeType(). It's OK if it does return $this;.

You mean that Scope always have both native and phpdoc type, and treatPhpDocTypesAsCertain should be handled in the Rule side to switch between native and phpdoc types?

@ondrejmirtes
Copy link
Member

Yes 😊

@ondrejmirtes
Copy link
Member

Another important test is this:

function doFoo(): string
{
}

function (): void {
    /** @var non-empty-string $a */
    $a = doFoo();
    assertType('non-empty-string', $a);
    assertNativeType('string', $a);
};

Meaning that everything around processStmtVarAnnotation and processVarAnnotation in NodeScopeResolver has to be skipped for native types.

rajyan added a commit to rajyan/phpstan-src that referenced this pull request Oct 9, 2022
rajyan added a commit to rajyan/phpstan-src that referenced this pull request Oct 9, 2022
rajyan added a commit to rajyan/phpstan-src that referenced this pull request Oct 9, 2022
rajyan added a commit to rajyan/phpstan-src that referenced this pull request Oct 9, 2022
@rajyan
Copy link
Contributor Author

rajyan commented Oct 9, 2022

@ondrejmirtes
Added some tests to see that the cases you mentioned
#1802
is working with the current implementation.

i'm going to fix the comments in
#1802
👍

@ondrejmirtes
Copy link
Member

To give you a bit more context, thanks to more precise Scope::getNativeType(), I'll be able to do two awesome things:

Validate inline @var

Let's take this example:

function doFoo(): string
{
}

function (): void {
    /** @var int $a */
   $a = doFoo();
};

I'll be able to report "Type int in PHPDoc tag @var is not a subtype of native type string". Right now we can't do this because @var is used for different purposes:

  • To narrow down types. This is better served by actually fixing the PHPDocs, using generics, and conditional return types.
  • To fix wrong 3rd party PHPDocs. This is better served by using stub files.

Because of this our options of reporting wrong code are pretty limited. But I came up with this rule (That depends on correct Scope::getNativeType()) and we'll be able to report cases that are definitely wrong thanks to native types.

Redesign rule levels

Right now the design of levels mirrors how I proceeded with PHPStan development. The rules I implemented first are checked on lower levels.

Calling a function with wrong argument types is reported on level 5.

But what would make more sense is to report "this is definitely an error" on lower levels. And Scope::getNativeType() will help us with that.

In continuous delivery fashion, I'll be able to do this on 1.x while keeping PHPStan stable. Because these new levels will have different names. Right now you set a level with --level 2 for example. These new levels will have some prefix or suffix, so people would be able to switch to them by doing --level x2 or similar.

@ondrejmirtes
Copy link
Member

One thing I can suggest here: We can finish the current impact of this PR once we're happy with it and it's tested in sufficient manner. And merge it.

We don't have to implement every Expr type in getNativeType() right now, it's a lot of code and I worry it would become unmanagable. That can be done in the next PRs.

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'm quite happy with this PR already. These few comments resolved + green build is all it takes for me to merge it 😊 And we can continue the work in further PRs, like moreSpecificNativeTypes, and fully implementing getNativeType for all exprs.

@@ -506,6 +506,13 @@ public function hasVariableType(string $variableName): TrinaryLogic
/** @api */
public function getVariableType(string $variableName): Type
{
if (!$this->treatPhpDocTypesAsCertain) {
Copy link
Member

Choose a reason for hiding this comment

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

The behaviour shouldn't change here - MutatingScope shouldn't be interested in the treat option.

There could be getVariableNativeType method, but I don't think we need it.

@@ -2147,7 +2113,7 @@ public function getNativeType(Expr $expr): Type
);
}

return $this->getType($expr);
return $this->doNotTreatPhpDocTypesAsCertain()->getType($expr);
Copy link
Member

Choose a reason for hiding this comment

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

This lind should just return new MixedType.

ondrejmirtes pushed a commit that referenced this pull request Oct 12, 2022
@rajyan
Copy link
Contributor Author

rajyan commented Oct 12, 2022

TypeSpecifier/SpecifiedTypes, BUT the name might be misleading, because the usual Scope that's used since the start of the analysis must have $treatPhpDocTypesAsCertain=true REGARDLESS of the project setting in phpstan.neon.
So I'm inclining towards something like bool $nativeTypesPromoted or something like that. I'll try to prototype it.

Totally agreed.

@ondrejmirtes
Copy link
Member

I've written down some context about this and my plan: phpstan/phpstan#8191

@ondrejmirtes
Copy link
Member

Everything from this PR is either already in 1.9.x or in #1943 :)

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