Skip to content

Failing test for #4657 #477

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

Merged
merged 4 commits into from
Mar 17, 2021
Merged

Failing test for #4657 #477

merged 4 commits into from
Mar 17, 2021

Conversation

ruudk
Copy link
Contributor

@ruudk ruudk commented Mar 17, 2021

See phpstan/phpstan#4657

While using the debugger I found that the problem occurs while resolving is_null($value).

This code:

$sureType = reset($sureTypes);
if ($isSpecified($sureType[0])) {
return null;
}
if ($this->treatPhpDocTypesAsCertain) {
$argumentType = $scope->getType($sureType[0]);
} else {
$argumentType = $scope->getNativeType($sureType[0]);
}

gets $sureTypes = [ Node\Expr\Variable , Type\NullType ].

When treatPhpDocTypesAsCertain = false, it will call getNativeType on the variable. That resolves to null.

It's probably because the $value = null; declaration on the top.

It does not know that the value is also passed as reference to a callback.

Any advice how to debug this further?

While using the debugger I found that the problem occurs while resolving is_null($value).

This code:
https://github.com/phpstan/phpstan-src/blob/6d523028e399c15dc77aec3affd2ea97ff735925/src/Rules/Comparison/ImpossibleCheckTypeHelper.php#L177-L186
gets $sureTypes = [ Node\Expr\Variable  , Type\NullType ].

When treatPhpDocTypesAsCertain = false, it will call `getNativeType` on the variable. That resolves to `null`.

It's probably because the `$value = null;` declaration on the top.

It does not know that the value is also passed as reference to a callback.
@ruudk
Copy link
Contributor Author

ruudk commented Mar 17, 2021

Interesting: When I remove this:

if ($this->treatPhpDocTypesAsCertain) {
$argumentType = $scope->getType($sureType[0]);
} else {
$argumentType = $scope->getNativeType($sureType[0]);
}

with
$argumentType = $scope->getType($sureType[0]);

my test passes, but a lot other tests fail (obviously).

I wonder, my bug file does not contain any PHPDoc, so why does getNativeType give other results than getType?
https://github.com/phpstan/phpstan-src/blob/7c232be3b8bafda3f0be8bf1f791f79cbe03ebc1/tests/PHPStan/Rules/Comparison/data/bug-4657.php

@ondrejmirtes
Copy link
Member

@ruudk That's about wrong information being in MutatingScope::$nativeExpressionTypes.

This is all the code that's related to processing closures:

  • private function processClosureNode(
    Expr\Closure $expr,
    MutatingScope $scope,
    callable $nodeCallback,
    ExpressionContext $context,
    ?Type $passedToType
    ): ExpressionResult
    {
    foreach ($expr->params as $param) {
    $this->processParamNode($param, $scope, $nodeCallback);
    }
    $byRefUses = [];
    if ($passedToType !== null && !$passedToType->isCallable()->no()) {
    $callableParameters = null;
    $acceptors = $passedToType->getCallableParametersAcceptors($scope);
    if (count($acceptors) === 1) {
    $callableParameters = $acceptors[0]->getParameters();
    }
    } else {
    $callableParameters = null;
    }
    $useScope = $scope;
    foreach ($expr->uses as $use) {
    if ($use->byRef) {
    $byRefUses[] = $use;
    $useScope = $useScope->enterExpressionAssign($use->var);
    $inAssignRightSideVariableName = $context->getInAssignRightSideVariableName();
    $inAssignRightSideType = $context->getInAssignRightSideType();
    if (
    $inAssignRightSideVariableName === $use->var->name
    && $inAssignRightSideType !== null
    ) {
    if ($inAssignRightSideType instanceof ClosureType) {
    $variableType = $inAssignRightSideType;
    } else {
    $alreadyHasVariableType = $scope->hasVariableType($inAssignRightSideVariableName);
    if ($alreadyHasVariableType->no()) {
    $variableType = TypeCombinator::union(new NullType(), $inAssignRightSideType);
    } else {
    $variableType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideType);
    }
    }
    $scope = $scope->assignVariable($inAssignRightSideVariableName, $variableType);
    }
    }
    $this->processExprNode($use, $useScope, $nodeCallback, $context);
    if (!$use->byRef) {
    continue;
    }
    $useScope = $useScope->exitExpressionAssign($use->var);
    }
    if ($expr->returnType !== null) {
    $nodeCallback($expr->returnType, $scope);
    }
    $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters);
    $closureScope = $closureScope->processClosureScope($scope, null, $byRefUses);
    $nodeCallback(new InClosureNode($expr), $closureScope);
    $gatheredReturnStatements = [];
    $gatheredYieldStatements = [];
    $closureStmtsCallback = static function (\PhpParser\Node $node, Scope $scope) use ($nodeCallback, &$gatheredReturnStatements, &$gatheredYieldStatements, &$closureScope): void {
    $nodeCallback($node, $scope);
    if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) {
    return;
    }
    if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) {
    $gatheredYieldStatements[] = $node;
    }
    if (!$node instanceof Return_) {
    return;
    }
    $gatheredReturnStatements[] = new ReturnStatement($scope, $node);
    };
    if (count($byRefUses) === 0) {
    $statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback);
    $nodeCallback(new ClosureReturnStatementsNode(
    $expr,
    $gatheredReturnStatements,
    $gatheredYieldStatements,
    $statementResult
    ), $closureScope);
    return new ExpressionResult($scope, false);
    }
    $count = 0;
    do {
    $prevScope = $closureScope;
    $intermediaryClosureScopeResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, static function (): void {
    });
    $intermediaryClosureScope = $intermediaryClosureScopeResult->getScope();
    foreach ($intermediaryClosureScopeResult->getExitPoints() as $exitPoint) {
    $intermediaryClosureScope = $intermediaryClosureScope->mergeWith($exitPoint->getScope());
    }
    $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters);
    $closureScope = $closureScope->processClosureScope($intermediaryClosureScope, $prevScope, $byRefUses);
    if ($closureScope->equals($prevScope)) {
    break;
    }
    $count++;
    } while ($count < self::LOOP_SCOPE_ITERATIONS);
    $statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback);
    $nodeCallback(new ClosureReturnStatementsNode(
    $expr,
    $gatheredReturnStatements,
    $gatheredYieldStatements,
    $statementResult
    ), $closureScope);
    return new ExpressionResult($scope->processClosureScope($closureScope, null, $byRefUses), false);
    }
  • public function enterAnonymousFunction(
    Expr\Closure $closure,
    ?array $callableParameters = null
    ): self
    {
    $anonymousFunctionReflection = $this->getType($closure);
    if (!$anonymousFunctionReflection instanceof ClosureType) {
    throw new \PHPStan\ShouldNotHappenException();
    }
    $scope = $this->enterAnonymousFunctionWithoutReflection($closure, $callableParameters);
    return $this->scopeFactory->create(
    $scope->context,
    $scope->isDeclareStrictTypes(),
    $scope->constantTypes,
    $scope->getFunction(),
    $scope->getNamespace(),
    $scope->variableTypes,
    $scope->moreSpecificTypes,
    [],
    $scope->inClosureBindScopeClass,
    $anonymousFunctionReflection,
    true,
    [],
    $scope->nativeExpressionTypes,
    [],
    false,
    $this
    );
    }
    /**
    * @param \PhpParser\Node\Expr\Closure $closure
    * @param \PHPStan\Reflection\ParameterReflection[]|null $callableParameters
    * @return self
    */
    private function enterAnonymousFunctionWithoutReflection(
    Expr\Closure $closure,
    ?array $callableParameters = null
    ): self
    {
    $variableTypes = [];
    foreach ($closure->params as $i => $parameter) {
    $isNullable = $this->isParameterValueNullable($parameter);
    $parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic);
    if ($callableParameters !== null) {
    if (isset($callableParameters[$i])) {
    $parameterType = TypehintHelper::decideType($parameterType, $callableParameters[$i]->getType());
    } elseif (count($callableParameters) > 0) {
    $lastParameter = $callableParameters[count($callableParameters) - 1];
    if ($lastParameter->isVariadic()) {
    $parameterType = TypehintHelper::decideType($parameterType, $lastParameter->getType());
    } else {
    $parameterType = TypehintHelper::decideType($parameterType, new MixedType());
    }
    } else {
    $parameterType = TypehintHelper::decideType($parameterType, new MixedType());
    }
    }
    if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) {
    throw new \PHPStan\ShouldNotHappenException();
    }
    $variableTypes[$parameter->var->name] = VariableTypeHolder::createYes(
    $parameterType
    );
    }
    $nativeTypes = [];
    $moreSpecificTypes = [];
    foreach ($closure->uses as $use) {
    if (!is_string($use->var->name)) {
    throw new \PHPStan\ShouldNotHappenException();
    }
    if ($use->byRef) {
    continue;
    }
    $variableName = $use->var->name;
    if ($this->hasVariableType($variableName)->no()) {
    $variableType = new ErrorType();
    } else {
    $variableType = $this->getVariableType($variableName);
    $nativeTypes[sprintf('$%s', $variableName)] = $this->getNativeType($use->var);
    }
    $variableTypes[$variableName] = VariableTypeHolder::createYes($variableType);
    foreach ($this->moreSpecificTypes as $exprString => $moreSpecificType) {
    $matches = \Nette\Utils\Strings::matchAll((string) $exprString, '#^\$([a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*)#');
    if ($matches === []) {
    continue;
    }
    $matches = array_column($matches, 1);
    if (!in_array($variableName, $matches, true)) {
    continue;
    }
    $moreSpecificTypes[$exprString] = $moreSpecificType;
    }
    }
    if ($this->hasVariableType('this')->yes() && !$closure->static) {
    $variableTypes['this'] = VariableTypeHolder::createYes($this->getVariableType('this'));
    }
    return $this->scopeFactory->create(
    $this->context,
    $this->isDeclareStrictTypes(),
    $this->constantTypes,
    $this->getFunction(),
    $this->getNamespace(),
    $variableTypes,
    $moreSpecificTypes,
    [],
    $this->inClosureBindScopeClass,
    new TrivialParametersAcceptor(),
    true,
    [],
    $nativeTypes,
    [],
    false,
    $this
    );
    }
  • public function processClosureScope(
    self $closureScope,
    ?self $prevScope,
    array $byRefUses
    ): self
    {
    $variableTypes = $this->variableTypes;
    if (count($byRefUses) === 0) {
    return $this;
    }
    foreach ($byRefUses as $use) {
    if (!is_string($use->var->name)) {
    throw new \PHPStan\ShouldNotHappenException();
    }
    $variableName = $use->var->name;
    if (!$closureScope->hasVariableType($variableName)->yes()) {
    $variableTypes[$variableName] = VariableTypeHolder::createYes(new NullType());
    continue;
    }
    $variableType = $closureScope->getVariableType($variableName);
    if ($prevScope !== null) {
    $prevVariableType = $prevScope->getVariableType($variableName);
    if (!$variableType->equals($prevVariableType)) {
    $variableType = TypeCombinator::union($variableType, $prevVariableType);
    $variableType = self::generalizeType($variableType, $prevVariableType);
    }
    }
    $variableTypes[$variableName] = VariableTypeHolder::createYes($variableType);
    }
    return $this->scopeFactory->create(
    $this->context,
    $this->isDeclareStrictTypes(),
    $this->constantTypes,
    $this->getFunction(),
    $this->getNamespace(),
    $variableTypes,
    $this->moreSpecificTypes,
    $this->conditionalExpressions,
    $this->inClosureBindScopeClass,
    $this->anonymousFunctionReflection,
    $this->inFirstLevelStatement,
    [],
    $this->nativeExpressionTypes,
    $this->inFunctionCallsStack,
    $this->afterExtractCall,
    $this->parentScope
    );
    }

@ruudk
Copy link
Contributor Author

ruudk commented Mar 17, 2021

@ondrejmirtes Thanks for pointing this out. I believe I solved it. Is it the way to go?

@ondrejmirtes
Copy link
Member

Yeah, looks fine, just do all the necessary assertNativeType() tests to verify it :)

@ruudk
Copy link
Contributor Author

ruudk commented Mar 17, 2021

Added them to my test file, are there more places where I should add it?

@ondrejmirtes
Copy link
Member

@ruudk If you write some nonsense there you'll realized those asserts aren't executed. You need to add the file as a dataProvider in NodeScopeResolver::testFileAsserts().

@ondrejmirtes ondrejmirtes merged commit feb2a5a into phpstan:master Mar 17, 2021
@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.

2 participants