Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions src/Analyser/ExprHandler/StaticCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,18 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
} else {
$throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr);
}
} elseif ($expr->class instanceof Expr) {
$classType = $scope->getType($expr->class)->getObjectTypeOrClassStringObjectType();
$methodName = $expr->name->name;
$methodReflection = $scope->getMethodReflection($classType, $methodName);
if ($methodReflection !== null) {
$parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
$scope,
$expr->getArgs(),
$methodReflection->getVariants(),
$methodReflection->getNamedArgumentsVariants(),
);
}
}
} else {
$nameResult = $nodeScopeResolver->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep());
Expand Down Expand Up @@ -202,7 +214,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
}

if (
$methodReflection !== null
$expr->class instanceof Name
&& $methodReflection !== null
&& (
(
!$methodReflection->isStatic()
Expand All @@ -215,7 +228,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
) {
$scope = $scope->invalidateExpression(new Variable('this'), true, $methodReflection->getDeclaringClass());
} elseif (
$methodReflection !== null
$expr->class instanceof Name
&& $methodReflection !== null
&& $this->rememberPossiblyImpureFunctionValues
&& $scope->isInClass()
&& $scope->getClassReflection()->is($methodReflection->getDeclaringClass()->getName())
Expand All @@ -230,7 +244,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
}

if (
$methodReflection !== null
$expr->class instanceof Name
&& $methodReflection !== null
&& !$methodReflection->isStatic()
&& $methodReflection->getName() === '__construct'
&& $scopeFunction instanceof MethodReflection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,10 @@ public function testBug6822(): void
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-6822.php'], []);
}

public function testBug5020(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-5020.php'], []);
}

}
33 changes: 33 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-5020.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Bug5020;

interface ITransformer
{
public static function Transform(string $theInput, bool &$theErrorEncountered): string;
}

class Transformer implements ITransformer
{
public static function Transform(string $theInput, bool &$theErrorEncountered): string
{
if ($theInput === 'invalid') {
$theErrorEncountered = true;
return '';
}
return strtoupper(trim($theInput));
}
}

/**
* @param class-string<Transformer> $transformer
*/
function foo(string $transformer): void
{
$input = ' asdasda asdasd ';
$error = false;
$output = $transformer::Transform($input, $error);
if ($error) {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ public function testRulePhp7(): void
'Call to static method DateTimeImmutable::createFromFormat() on a separate line has no effect.',
12,
],
[
'Call to static method DateTimeImmutable::createFromFormat() on a separate line has no effect.',
13,
],
[
'Call to method DateTime::format() on a separate line has no effect.',
23,
Expand Down Expand Up @@ -132,7 +136,20 @@ public function testBug10819(): void

public function testDynamicStaticCall(): void
{
$this->analyse([__DIR__ . '/data/dynamic-static-call.php'], []);
$this->analyse([__DIR__ . '/data/dynamic-static-call.php'], [
[
'Call to static method DynamicStaticCall\Foo::doFoo() on a separate line has no effect.',
32,
],
[
'Call to static method DynamicStaticCall\FinalFoo::doFoo() on a separate line has no effect.',
33,
],
[
'Call to static method DynamicStaticCall\Bar::finalFoo() on a separate line has no effect.',
34,
],
]);
}

#[RequiresPhp('>= 8.5.0')]
Expand Down
6 changes: 3 additions & 3 deletions tests/PHPStan/Rules/Methods/data/dynamic-static-call.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ final static public function finalFoo():int

class Baz {
function doBaz(Foo $foo, FinalFoo $finalFoo, Bar $bar):void {
$foo::doFoo(); // no error, subclass could override static method with impure impl
$finalFoo::doFoo(); // could be "Call to static method .. on a separate line has no effect", because final class
$bar::finalFoo(); // could be "Call to static method .. on a separate line has no effect", because final method
$foo::doFoo();
$finalFoo::doFoo();
$bar::finalFoo();
}
}
6 changes: 6 additions & 0 deletions tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -219,4 +219,10 @@ public function testBug14511(): void
$this->analyse([__DIR__ . '/data/bug-14511.php'], []);
}

#[RequiresPhp('>= 8.1.0')]
public function testBug14557(): void
{
$this->analyse([__DIR__ . '/data/bug-14557-function.php'], []);
}

}
12 changes: 12 additions & 0 deletions tests/PHPStan/Rules/Pure/PureMethodRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -389,4 +389,16 @@ public function testBug14511(): void
$this->analyse([__DIR__ . '/data/bug-14511-method.php'], []);
}

#[RequiresPhp('>= 8.1.0')]
public function testBug14557(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-14557.php'], [
[
'Impure call to method Bug14557\SomeClass::impureStaticMethod() in pure method Bug14557\Foo::impureViaClassString().',
93,
],
]);
}

}
55 changes: 55 additions & 0 deletions tests/PHPStan/Rules/Pure/data/bug-14557-function.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php // lint >= 8.1

declare(strict_types = 1);

namespace Bug14557Function;

enum MyEnum: string
{
case Foo = 'foo';
case Bar = 'bar';
}

/**
* @param enum-string<MyEnum> $enum
* @phpstan-pure
*/
function fromEnumString(string $enum): MyEnum
{
return $enum::from('foo');
}

/**
* @param enum-string<MyEnum> $enum
* @phpstan-pure
*/
function tryFromEnumString(string $enum): ?MyEnum
{
return $enum::tryFrom('foo');
}

/**
* @param class-string<MyEnum> $enum
* @phpstan-pure
*/
function fromClassString(string $enum): MyEnum
{
return $enum::from('foo');
}

/**
* @param class-string<MyEnum> $enum
* @phpstan-pure
*/
function tryFromClassString(string $enum): ?MyEnum
Copy link
Copy Markdown
Contributor

@staabm staabm May 2, 2026

Choose a reason for hiding this comment

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

add another test for

/**
 * @phpstan-pure
 */
function fromEnum(MyEnum $enum): MyEnum
{
	return $enum::from('foo');
}

which unexpectedly errors before this PR
https://phpstan.org/r/1dccccac-c269-4df9-9cb1-d7715a501d5b

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. Added the test case staabm requested — a pure function/method calling $enum::from('foo') where $enum is typed as MyEnum (an object instance, not a class-string or enum-string). Added to both the function-level and method-level test files, updated the error line expectation, and all tests + make phpstan pass.

{
return $enum::tryFrom('foo');
}

/**
* @phpstan-pure
*/
function fromEnum(MyEnum $enum): MyEnum
{
return $enum::from('foo');
}
96 changes: 96 additions & 0 deletions tests/PHPStan/Rules/Pure/data/bug-14557.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php // lint >= 8.1

declare(strict_types = 1);

namespace Bug14557;

enum MyEnum: string
{
case Foo = 'foo';
case Bar = 'bar';
}

class SomeClass
{

/** @phpstan-pure */
public static function pureStaticMethod(): int
{
return 1;
}

/** @phpstan-impure */
public static function impureStaticMethod(): int
{
echo 'hello';
return 1;
}

}

class Foo
{

/**
* @param enum-string<MyEnum> $enum
* @phpstan-pure
*/
public function doFoo(string $enum): MyEnum
{
return $enum::from('foo');
}

/**
* @param enum-string<MyEnum> $enum
* @phpstan-pure
*/
public function doBar(string $enum): ?MyEnum
{
return $enum::tryFrom('foo');
}

/**
* @param class-string<MyEnum> $enum
* @phpstan-pure
*/
public function doBaz(string $enum): MyEnum
{
return $enum::from('foo');
}

/**
* @param class-string<MyEnum> $enum
* @phpstan-pure
*/
public function doLorem(string $enum): ?MyEnum
{
return $enum::tryFrom('foo');
}

/**
* @phpstan-pure
*/
public function fromEnum(MyEnum $enum): MyEnum
{
return $enum::from('foo');
}

/**
* @param class-string<SomeClass> $class
* @phpstan-pure
*/
public function pureViaClassString(string $class): int
{
return $class::pureStaticMethod();
}

/**
* @param class-string<SomeClass> $class
* @phpstan-pure
*/
public function impureViaClassString(string $class): int
{
return $class::impureStaticMethod(); // error
}

}
Loading