Skip to content
Closed
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
67 changes: 67 additions & 0 deletions src/Rules/BuiltinClassMemberAvailabilityHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules;

use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Type;
use function count;

/**
* Members of built-in classes (and of classes extending them) come from stubs filtered for the analysed PHP version.
* When the scope is narrowed (e.g. by a `PHP_VERSION_ID` check) to PHP versions
* that exclude the analysed one, a member missing from the stubs might exist there.
*/
#[AutowiredService]
final class BuiltinClassMemberAvailabilityHelper
{

public function __construct(
private ReflectionProvider $reflectionProvider,
private PhpVersion $phpVersion,
)
{
}

public function mightBeAvailableInScopePhpVersion(Scope $scope, Type $classType): bool
{
$classNames = $classType->getObjectClassNames();
if (count($classNames) === 0) {
return false;
}

foreach ($classNames as $className) {
if (!$this->reflectionProvider->hasClass($className)) {
return false;
}

if (!$this->hasBuiltinClassInHierarchy($this->reflectionProvider->getClass($className))) {
return false;
}
}

return $scope->getPhpVersion()->getType()->isSuperTypeOf(
new ConstantIntegerType($this->phpVersion->getVersionId()),
)->no();
}

private function hasBuiltinClassInHierarchy(ClassReflection $classReflection): bool
{
if ($classReflection->isBuiltin()) {
return true;
}

foreach ($classReflection->getParents() as $parentReflection) {
if ($parentReflection->isBuiltin()) {
return true;
}
}

return false;
}

}
6 changes: 6 additions & 0 deletions src/Rules/Classes/ClassConstantRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\Internal\SprintfHelper;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper;
use PHPStan\Rules\ClassNameCheck;
use PHPStan\Rules\ClassNameNodePair;
use PHPStan\Rules\ClassNameUsageLocation;
Expand Down Expand Up @@ -40,6 +41,7 @@ final class ClassConstantRule implements Rule
public function __construct(
private ReflectionProvider $reflectionProvider,
private RuleLevelHelper $ruleLevelHelper,
private BuiltinClassMemberAvailabilityHelper $builtinClassMemberAvailabilityHelper,
private ClassNameCheck $classCheck,
private NonStringableDynamicAccessCheck $nonStringableDynamicAccessCheck,
)
Expand Down Expand Up @@ -239,6 +241,10 @@ private function processSingleClassConstFetch(Scope $scope, ClassConstFetch $nod
}

if (!$classType->hasConstant($constantName)->yes()) {
if ($this->builtinClassMemberAvailabilityHelper->mightBeAvailableInScopePhpVersion($scope, $classType)) {
return $messages;
}

return array_merge($messages, [
RuleErrorBuilder::message(sprintf(
'Access to undefined constant %s::%s.',
Expand Down
6 changes: 6 additions & 0 deletions src/Rules/Methods/MethodCallCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use PHPStan\Internal\SprintfHelper;
use PHPStan\Reflection\ExtendedMethodReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Rules\RuleLevelHelper;
Expand All @@ -31,6 +32,7 @@ final class MethodCallCheck
public function __construct(
private ReflectionProvider $reflectionProvider,
private RuleLevelHelper $ruleLevelHelper,
private BuiltinClassMemberAvailabilityHelper $builtinClassMemberAvailabilityHelper,
#[AutowiredParameter]
private bool $checkFunctionNameCase,
#[AutowiredParameter]
Expand Down Expand Up @@ -133,6 +135,10 @@ public function check(
}
}

if ($this->builtinClassMemberAvailabilityHelper->mightBeAvailableInScopePhpVersion($scope, $type)) {
return [[], null];
}

return [
[
RuleErrorBuilder::message(sprintf(
Expand Down
6 changes: 6 additions & 0 deletions src/Rules/Methods/StaticMethodCallCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use PHPStan\Reflection\Native\NativeMethodReflection;
use PHPStan\Reflection\Php\PhpMethodReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper;
use PHPStan\Rules\ClassNameCheck;
use PHPStan\Rules\ClassNameNodePair;
use PHPStan\Rules\ClassNameUsageLocation;
Expand Down Expand Up @@ -43,6 +44,7 @@ final class StaticMethodCallCheck
public function __construct(
private ReflectionProvider $reflectionProvider,
private RuleLevelHelper $ruleLevelHelper,
private BuiltinClassMemberAvailabilityHelper $builtinClassMemberAvailabilityHelper,
private ClassNameCheck $classCheck,
#[AutowiredParameter]
private bool $checkFunctionNameCase,
Expand Down Expand Up @@ -240,6 +242,10 @@ public function check(
}
}

if ($this->builtinClassMemberAvailabilityHelper->mightBeAvailableInScopePhpVersion($scope, $classType)) {
return [$errors, null];
}

return [
array_merge($errors, [
RuleErrorBuilder::message(sprintf(
Expand Down
6 changes: 6 additions & 0 deletions src/Rules/Properties/AccessPropertiesCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use PHPStan\Reflection\ExtendedPropertyReflection;
use PHPStan\Reflection\MissingPropertyFromReflectionException;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\NonStringableDynamicAccessCheck;
use PHPStan\Rules\RuleErrorBuilder;
Expand All @@ -39,6 +40,7 @@ final class AccessPropertiesCheck
public function __construct(
private ReflectionProvider $reflectionProvider,
private RuleLevelHelper $ruleLevelHelper,
private BuiltinClassMemberAvailabilityHelper $builtinClassMemberAvailabilityHelper,
private PhpVersion $phpVersion,
private NonStringableDynamicAccessCheck $nonStringableDynamicAccessCheck,
#[AutowiredParameter]
Expand Down Expand Up @@ -212,6 +214,10 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string
];
}

if ($this->builtinClassMemberAvailabilityHelper->mightBeAvailableInScopePhpVersion($scope, $type)) {
return [];
}

$ruleErrorBuilder = RuleErrorBuilder::message(sprintf(
'Access to an undefined property %s::$%s.',
$typeForDescribe->describe(VerbosityLevel::typeOnly()),
Expand Down
6 changes: 6 additions & 0 deletions src/Rules/Properties/AccessStaticPropertiesCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use PHPStan\Internal\SprintfHelper;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper;
use PHPStan\Rules\ClassNameCheck;
use PHPStan\Rules\ClassNameNodePair;
use PHPStan\Rules\ClassNameUsageLocation;
Expand Down Expand Up @@ -47,6 +48,7 @@ final class AccessStaticPropertiesCheck
public function __construct(
private ReflectionProvider $reflectionProvider,
private RuleLevelHelper $ruleLevelHelper,
private BuiltinClassMemberAvailabilityHelper $builtinClassMemberAvailabilityHelper,
private ClassNameCheck $classCheck,
private PhpVersion $phpVersion,
private NonStringableDynamicAccessCheck $nonStringableDynamicAccessCheck,
Expand Down Expand Up @@ -287,6 +289,10 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node,
return [];
}

if ($this->builtinClassMemberAvailabilityHelper->mightBeAvailableInScopePhpVersion($scope, $classType)) {
return $messages;
}

return array_merge($messages, [
RuleErrorBuilder::message(sprintf(
'Access to an undefined static property %s::$%s.',
Expand Down
2 changes: 2 additions & 0 deletions tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Analyser;

use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper;
use PHPStan\Rules\FunctionCallParametersCheck;
use PHPStan\Rules\Methods\CallMethodsRule;
use PHPStan\Rules\Methods\MethodCallCheck;
Expand Down Expand Up @@ -37,6 +38,7 @@ protected function getRule(): Rule
new MethodCallCheck(
$reflectionProvider,
$ruleLevelHelper,
self::getContainer()->getByType(BuiltinClassMemberAvailabilityHelper::class),
checkFunctionNameCase: true,
reportMagicMethods: true,
),
Expand Down
2 changes: 2 additions & 0 deletions tests/PHPStan/Rules/Classes/ClassConstantPhp74RuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Rules\Classes;

use PHPStan\Classes\ForbiddenClassNameExtension;
use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper;
use PHPStan\Rules\ClassCaseSensitivityCheck;
use PHPStan\Rules\ClassForbiddenNameCheck;
use PHPStan\Rules\ClassNameCheck;
Expand Down Expand Up @@ -35,6 +36,7 @@ protected function getRule(): Rule
return new ClassConstantRule(
$reflectionProvider,
$ruleLevelHelper,
self::getContainer()->getByType(BuiltinClassMemberAvailabilityHelper::class),
new ClassNameCheck(
new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: true, checkImportedClassNameCase: true),
new ClassForbiddenNameCheck($container->getExtensionsCollection(ForbiddenClassNameExtension::class)),
Expand Down
16 changes: 16 additions & 0 deletions tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Rules\Classes;

use PHPStan\Classes\ForbiddenClassNameExtension;
use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper;
use PHPStan\Rules\ClassCaseSensitivityCheck;
use PHPStan\Rules\ClassForbiddenNameCheck;
use PHPStan\Rules\ClassNameCheck;
Expand Down Expand Up @@ -40,6 +41,7 @@ protected function getRule(): Rule
return new ClassConstantRule(
$reflectionProvider,
$ruleLevelHelper,
self::getContainer()->getByType(BuiltinClassMemberAvailabilityHelper::class),
new ClassNameCheck(
new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: true, checkImportedClassNameCase: $this->checkImportedClassNameCase),
new ClassForbiddenNameCheck($container->getExtensionsCollection(ForbiddenClassNameExtension::class)),
Expand Down Expand Up @@ -636,4 +638,18 @@ public function testClosureAttributeArguments(): void
]);
}

public function testBug15109(): void
{
$this->analyse([__DIR__ . '/data/bug-15109.php'], [
[
'Access to undefined constant ReflectionProperty::FUTURE_CONSTANT.',
12,
],
[
'Access to undefined constant Bug15109ClassConstant\\Foo::DOES_NOT_EXIST.',
16,
],
]);
}

}
18 changes: 18 additions & 0 deletions tests/PHPStan/Rules/Classes/data/bug-15109.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php declare(strict_types = 1);

namespace Bug15109ClassConstant;

class Foo
{

}

function doFoo(): void
{
echo \ReflectionProperty::FUTURE_CONSTANT;

if (PHP_VERSION_ID >= 90000) {
echo \ReflectionProperty::FUTURE_CONSTANT;
echo Foo::DOES_NOT_EXIST;
}
}
23 changes: 23 additions & 0 deletions tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Rules\Methods;

use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper;
use PHPStan\Rules\FunctionCallParametersCheck;
use PHPStan\Rules\NonStringableDynamicAccessCheck;
use PHPStan\Rules\NullsafeCheck;
Expand Down Expand Up @@ -48,6 +49,7 @@ protected function getRule(): Rule
new MethodCallCheck(
$reflectionProvider,
$ruleLevelHelper,
self::getContainer()->getByType(BuiltinClassMemberAvailabilityHelper::class),
checkFunctionNameCase: true,
reportMagicMethods: true,
),
Expand Down Expand Up @@ -4650,4 +4652,25 @@ public function testDeadCatch(): void
]);
}

public function testBug15109(): void
{
$this->checkThisOnly = false;
$this->checkNullables = true;
$this->checkUnionTypes = true;
$this->analyse([__DIR__ . '/data/bug-15109.php'], [
[
'Call to an undefined method ReflectionMethod::futureMethod().',
26,
],
[
'Call to an undefined method Bug15109\\Foo::doesNotExist().',
34,
],
[
'Call to an undefined method Bug15109\\Foo::doesNotExist().',
41,
],
]);
}

}
21 changes: 21 additions & 0 deletions tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Rules\Methods;

use PHPStan\Classes\ForbiddenClassNameExtension;
use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper;
use PHPStan\Rules\ClassCaseSensitivityCheck;
use PHPStan\Rules\ClassForbiddenNameCheck;
use PHPStan\Rules\ClassNameCheck;
Expand Down Expand Up @@ -53,6 +54,7 @@ protected function getRule(): Rule
new StaticMethodCallCheck(
$reflectionProvider,
$ruleLevelHelper,
self::getContainer()->getByType(BuiltinClassMemberAvailabilityHelper::class),
new ClassNameCheck(
new ClassCaseSensitivityCheck(
$reflectionProvider,
Expand Down Expand Up @@ -1158,4 +1160,23 @@ public function testBug11421(): void
]);
}

public function testBug15109(): void
{
$this->checkThisOnly = false;
$this->analyse([__DIR__ . '/data/bug-15109.php'], [
[
'Call to an undefined static method ReflectionMethod::futureStaticMethod().',
25,
],
[
'Call to an undefined static method Bug15109\\Foo::doesNotExist().',
33,
],
[
'Call to an undefined static method Bug15109\\Foo::doesNotExist().',
40,
],
]);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\Php\PhpClassReflectionExtension;
use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper;
use PHPStan\Rules\FunctionCallParametersCheck;
use PHPStan\Rules\NonStringableDynamicAccessCheck;
use PHPStan\Rules\NullsafeCheck;
Expand Down Expand Up @@ -33,7 +34,7 @@ protected function getRule(): Rule
// @phpstan-ignore argument.type
return new CompositeRule([
new CallMethodsRule(
new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true),
new MethodCallCheck($reflectionProvider, $ruleLevelHelper, self::getContainer()->getByType(BuiltinClassMemberAvailabilityHelper::class), true, true),
new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), $reflectionProvider, self::getContainer()->getByType(ParameterClosureTypeHelper::class), true, true, true, true),
new NonStringableDynamicAccessCheck($ruleLevelHelper, true),
),
Expand Down
Loading
Loading