Skip to content

Report impossible class_exists/interface_exists/trait_exists/enum_exists#5754

Merged
staabm merged 6 commits into
phpstan:2.2.xfrom
janedbal:struct-exists-impossibility
Jun 2, 2026
Merged

Report impossible class_exists/interface_exists/trait_exists/enum_exists#5754
staabm merged 6 commits into
phpstan:2.2.xfrom
janedbal:struct-exists-impossibility

Conversation

@janedbal
Copy link
Copy Markdown
Contributor

Detect via reflection when the constant-string argument names a struct of the wrong kind (e.g. class_exists() on an interface name) and narrow the argument to never in the truthy context. Drop the ImpossibleCheckTypeHelper bailout for these four functions so the rule can pick up the impossibility, while still suppressing "always true" since runtime autoload can fail.

Closes phpstan/phpstan#14683

@phpstan-bot
Copy link
Copy Markdown
Collaborator

You've opened the pull request against the latest branch 2.2.x. PHPStan 2.2 is not going to be released for months. If your code is relevant on 2.1.x and you want it to be released sooner, please rebase your pull request and change its target to 2.1.x.

@janedbal janedbal force-pushed the struct-exists-impossibility branch from 3dc802f to 480fc5b Compare May 25, 2026 07:55
@janedbal janedbal changed the base branch from 2.2.x to 2.1.x May 25, 2026 07:55
@janedbal janedbal force-pushed the struct-exists-impossibility branch 2 times, most recently from 95f21da to d04d50c Compare May 25, 2026 10:19
@janedbal janedbal marked this pull request as ready for review May 25, 2026 12:18
@phpstan-bot
Copy link
Copy Markdown
Collaborator

This pull request has been marked as ready for review.

$argsCount = count($args);
if ($node->name instanceof Node\Name) {
$functionName = strtolower((string) $node->name);
if (in_array($functionName, [
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Given the fact this condition is now useless for 200 lines of code, I feel like it would be better moving the onlyReportFalse declaration.

This way we have

if ($node instanceof FuncCall) {
     // specific logic which could be moved into a private or a dedicated service
}

$onlyReportFalse = ...;

// genericLogic

Maybe one day the

$onlyReportFalse = ...;

will become a

$onlyReportValue = // true/false/null

WDYT @staabm ?

Copy link
Copy Markdown
Contributor

@staabm staabm May 30, 2026

Choose a reason for hiding this comment

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

I think we might get a more readable result by refactoring this into a separate method, which takes $expr and the to-be-returned ?bool.

the new method can decide based on the $expr whether the ?bool can/should be returned or not.

so
return $rootExprType->getValue(); gets return $this->decideSpecifiedTypeWasFound($expr, $rootExprType->getValue());

and
return $result->maybe() ? null : $result->yes(); gets return $this->decideSpecifiedTypeWasFound($expr, $result->maybe() ? null : $result->yes());

(name of the new method tbd)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in d201db6 — extracted into decideSpecifiedTypeWasFound() (name open to bikeshedding). This drops the $onlyReportFalse flag entirely, so it also covers @VincentLanglet's point about the early declaration.

— Claude

@VincentLanglet VincentLanglet requested a review from staabm May 25, 2026 18:53
@staabm
Copy link
Copy Markdown
Contributor

staabm commented May 30, 2026

please rebase on 2.2.x

janedbal added 2 commits June 1, 2026 09:22
Detect via reflection when the constant-string argument names a struct
of the wrong kind (e.g. class_exists() on an interface name) and
narrow the argument to never in the truthy context. Drop the
ImpossibleCheckTypeHelper bailout for these four functions so the rule
can pick up the impossibility, while still suppressing "always true"
since runtime autoload can fail.

Closes phpstan/phpstan#14683
@janedbal janedbal force-pushed the struct-exists-impossibility branch from d9ad04e to 9a08e13 Compare June 1, 2026 07:23
@janedbal janedbal changed the base branch from 2.1.x to 2.2.x June 1, 2026 07:24
Replace the early-declared $onlyReportFalse flag with a method that
post-processes the candidate result at each return site, deciding based
on the expression whether the "always true" result must be suppressed.

Co-Authored-By: Claude Code
return $result;
}

private function isAutoloadableExistenceCheck(Expr $node): bool
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would inline this function, but otherwise this LGTM

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Inlined in ba761bd. Thanks!

— Claude

@staabm staabm requested a review from VincentLanglet June 1, 2026 07:51
Comment thread src/Rules/Comparison/ImpossibleCheckTypeHelper.php Outdated
@staabm staabm requested a review from VincentLanglet June 2, 2026 05:48
@staabm staabm merged commit f79f07f into phpstan:2.2.x Jun 2, 2026
664 of 667 checks passed
@staabm
Copy link
Copy Markdown
Contributor

staabm commented Jun 2, 2026

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.

missing "is always false/true" errors on class_exists, method_exists, ..

4 participants