Skip to content

Commit 1fac324

Browse files
committed
Skip deprecated function errors when PHP_VERSION_ID guard excludes deprecation version
- Added DeprecatedSinceVersionHelper to extract the `since` version from #[Deprecated] attributes and compare it against the scope's narrowed PHP version - In NativeFunctionReflectionProvider, convert JetBrains\PhpStorm\Deprecated stub attributes to Deprecated AttributeReflection so the version info is accessible - Applied version-aware deprecation filtering in all RestrictedUsage rules: RestrictedFunctionUsageRule, RestrictedMethodUsageRule, RestrictedStaticMethodUsageRule, RestrictedPropertyUsageRule, RestrictedStaticPropertyUsageRule, RestrictedClassConstantUsageRule, and their callable variants - New regression test in tests/PHPStan/Rules/RestrictedUsage/Bug14366Test.php
1 parent b6c48a0 commit 1fac324

14 files changed

Lines changed: 245 additions & 1 deletion

src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,22 @@
1111
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
1212
use PHPStan\PhpDoc\StubPhpDocProvider;
1313
use PHPStan\Reflection\Assertions;
14+
use PHPStan\Reflection\AttributeReflection;
1415
use PHPStan\Reflection\AttributeReflectionFactory;
1516
use PHPStan\Reflection\ExtendedFunctionVariant;
1617
use PHPStan\Reflection\InitializerExprContext;
1718
use PHPStan\Reflection\Native\ExtendedNativeParameterReflection;
1819
use PHPStan\Reflection\Native\NativeFunctionReflection;
1920
use PHPStan\TrinaryLogic;
21+
use PHPStan\Type\Constant\ConstantStringType;
2022
use PHPStan\Type\FileTypeMapper;
2123
use PHPStan\Type\Generic\TemplateTypeMap;
2224
use PHPStan\Type\MixedType;
2325
use PHPStan\Type\Type;
2426
use PHPStan\Type\TypehintHelper;
2527
use function array_key_exists;
2628
use function array_map;
29+
use function is_string;
2730
use function str_contains;
2831
use function strtolower;
2932

@@ -160,6 +163,11 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef
160163
$hasSideEffects = TrinaryLogic::createMaybe();
161164
}
162165

166+
$phpstanAttributes = $this->attributeReflectionFactory->fromNativeReflection($attributes, InitializerExprContext::fromFunction($realFunctionName, $fileName));
167+
if ($reflectionFunctionAdapter !== null) {
168+
$phpstanAttributes = $this->addDeprecatedSinceAttribute($reflectionFunctionAdapter, $phpstanAttributes);
169+
}
170+
163171
$functionReflection = new NativeFunctionReflection(
164172
$realFunctionName,
165173
$variantsByType['positional'],
@@ -171,7 +179,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef
171179
$docComment,
172180
$returnsByReference,
173181
$acceptsNamedArguments,
174-
$this->attributeReflectionFactory->fromNativeReflection($attributes, InitializerExprContext::fromFunction($realFunctionName, $fileName)),
182+
$phpstanAttributes,
175183
);
176184
$this->functionMap[$lowerCasedFunctionName] = $functionReflection;
177185

@@ -199,4 +207,38 @@ private static function getParamOutTypeFromPhpDoc(string $paramName, ResolvedPhp
199207
return null;
200208
}
201209

210+
/**
211+
* @param list<AttributeReflection> $phpstanAttributes
212+
* @return list<AttributeReflection>
213+
*/
214+
private function addDeprecatedSinceAttribute(ReflectionFunction $reflectionFunction, array $phpstanAttributes): array
215+
{
216+
foreach ($phpstanAttributes as $attr) {
217+
if (strtolower($attr->getName()) === 'deprecated') {
218+
return $phpstanAttributes;
219+
}
220+
}
221+
222+
foreach ($reflectionFunction->getAttributes() as $brAttr) {
223+
if ($brAttr->getName() !== 'JetBrains\\PhpStorm\\Deprecated') {
224+
continue;
225+
}
226+
$arguments = $brAttr->getArguments();
227+
if (!isset($arguments['since']) || !is_string($arguments['since'])) {
228+
continue;
229+
}
230+
231+
$argTypes = ['since' => new ConstantStringType($arguments['since'])];
232+
if (isset($arguments['message']) && is_string($arguments['message'])) {
233+
$argTypes['message'] = new ConstantStringType($arguments['message']);
234+
}
235+
236+
$phpstanAttributes[] = new AttributeReflection('Deprecated', $argTypes);
237+
238+
return $phpstanAttributes;
239+
}
240+
241+
return $phpstanAttributes;
242+
}
243+
202244
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\RestrictedUsage;
4+
5+
use Nette\Utils\Strings;
6+
use PHPStan\Php\PhpVersions;
7+
use PHPStan\Reflection\AttributeReflection;
8+
use PHPStan\Type\IntegerRangeType;
9+
use function sprintf;
10+
use function strtolower;
11+
12+
final class DeprecatedSinceVersionHelper
13+
{
14+
15+
/**
16+
* @param list<AttributeReflection> $attributes
17+
*/
18+
public static function isScopeVersionBeforeDeprecation(array $attributes, PhpVersions $phpVersions): bool
19+
{
20+
$sinceVersionId = self::getDeprecatedSincePhpVersionId($attributes);
21+
if ($sinceVersionId === null) {
22+
return false;
23+
}
24+
25+
return !IntegerRangeType::fromInterval($sinceVersionId, null)->isSuperTypeOf($phpVersions->getType())->yes();
26+
}
27+
28+
/**
29+
* @param list<AttributeReflection> $attributes
30+
*/
31+
private static function getDeprecatedSincePhpVersionId(array $attributes): ?int
32+
{
33+
foreach ($attributes as $attribute) {
34+
if (strtolower($attribute->getName()) !== 'deprecated') {
35+
continue;
36+
}
37+
$argumentTypes = $attribute->getArgumentTypes();
38+
if (!isset($argumentTypes['since'])) {
39+
continue;
40+
}
41+
$sinceType = $argumentTypes['since'];
42+
foreach ($sinceType->getConstantStrings() as $constantString) {
43+
$matches = Strings::match($constantString->getValue(), '#^(\d+)\.(\d+)(?:\.(\d+))?$#');
44+
if ($matches !== null) {
45+
$major = (int) $matches[1];
46+
$minor = (int) $matches[2];
47+
$patch = (int) ($matches[3] ?? 0);
48+
return (int) sprintf('%d%02d%02d', $major, $minor, $patch);
49+
}
50+
}
51+
}
52+
53+
return null;
54+
}
55+
56+
}

src/Rules/RestrictedUsage/RestrictedClassConstantUsageRule.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHPStan\Rules\RuleLevelHelper;
1515
use PHPStan\Type\ErrorType;
1616
use PHPStan\Type\Type;
17+
use function str_ends_with;
1718

1819
/**
1920
* @implements Rule<Node\Expr\ClassConstFetch>
@@ -91,6 +92,13 @@ public function processNode(Node $node, Scope $scope): array
9192
continue;
9293
}
9394

95+
if (
96+
str_ends_with($restrictedUsage->identifier, '.deprecated')
97+
&& DeprecatedSinceVersionHelper::isScopeVersionBeforeDeprecation($constantReflection->getAttributes(), $scope->getPhpVersion())
98+
) {
99+
continue;
100+
}
101+
94102
if ($classReflection->getName() !== $constantReflection->getDeclaringClass()->getName()) {
95103
$rewrittenConstantReflection = new RewrittenDeclaringClassClassConstantReflection($classReflection, $constantReflection);
96104
$rewrittenRestrictedUsage = $extension->isRestrictedClassConstantUsage($rewrittenConstantReflection, $scope);

src/Rules/RestrictedUsage/RestrictedFunctionCallableUsageRule.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\Reflection\ReflectionProvider;
1212
use PHPStan\Rules\Rule;
1313
use PHPStan\Rules\RuleErrorBuilder;
14+
use function str_ends_with;
1415

1516
/**
1617
* @implements Rule<FunctionCallableNode>
@@ -59,6 +60,13 @@ public function processNode(Node $node, Scope $scope): array
5960
continue;
6061
}
6162

63+
if (
64+
str_ends_with($restrictedUsage->identifier, '.deprecated')
65+
&& DeprecatedSinceVersionHelper::isScopeVersionBeforeDeprecation($functionReflection->getAttributes(), $scope->getPhpVersion())
66+
) {
67+
continue;
68+
}
69+
6270
$errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage)
6371
->identifier($restrictedUsage->identifier)
6472
->build();

src/Rules/RestrictedUsage/RestrictedFunctionUsageRule.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PHPStan\Reflection\ReflectionProvider;
1111
use PHPStan\Rules\Rule;
1212
use PHPStan\Rules\RuleErrorBuilder;
13+
use function str_ends_with;
1314

1415
/**
1516
* @implements Rule<Node\Expr\FuncCall>
@@ -58,6 +59,13 @@ public function processNode(Node $node, Scope $scope): array
5859
continue;
5960
}
6061

62+
if (
63+
str_ends_with($restrictedUsage->identifier, '.deprecated')
64+
&& DeprecatedSinceVersionHelper::isScopeVersionBeforeDeprecation($functionReflection->getAttributes(), $scope->getPhpVersion())
65+
) {
66+
continue;
67+
}
68+
6169
$errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage)
6270
->identifier($restrictedUsage->identifier)
6371
->build();

src/Rules/RestrictedUsage/RestrictedMethodCallableUsageRule.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\Reflection\ReflectionProvider;
1212
use PHPStan\Rules\Rule;
1313
use PHPStan\Rules\RuleErrorBuilder;
14+
use function str_ends_with;
1415

1516
/**
1617
* @implements Rule<MethodCallableNode>
@@ -72,6 +73,13 @@ public function processNode(Node $node, Scope $scope): array
7273
continue;
7374
}
7475

76+
if (
77+
str_ends_with($restrictedUsage->identifier, '.deprecated')
78+
&& DeprecatedSinceVersionHelper::isScopeVersionBeforeDeprecation($methodReflection->getAttributes(), $scope->getPhpVersion())
79+
) {
80+
continue;
81+
}
82+
7583
$errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage)
7684
->identifier($restrictedUsage->identifier)
7785
->build();

src/Rules/RestrictedUsage/RestrictedMethodUsageRule.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\Reflection\ReflectionProvider;
1212
use PHPStan\Rules\Rule;
1313
use PHPStan\Rules\RuleErrorBuilder;
14+
use function str_ends_with;
1415

1516
/**
1617
* @implements Rule<MethodCall>
@@ -72,6 +73,13 @@ public function processNode(Node $node, Scope $scope): array
7273
continue;
7374
}
7475

76+
if (
77+
str_ends_with($restrictedUsage->identifier, '.deprecated')
78+
&& DeprecatedSinceVersionHelper::isScopeVersionBeforeDeprecation($methodReflection->getAttributes(), $scope->getPhpVersion())
79+
) {
80+
continue;
81+
}
82+
7583
$errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage)
7684
->identifier($restrictedUsage->identifier)
7785
->build();

src/Rules/RestrictedUsage/RestrictedPropertyUsageRule.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PHPStan\Reflection\ReflectionProvider;
1111
use PHPStan\Rules\Rule;
1212
use PHPStan\Rules\RuleErrorBuilder;
13+
use function str_ends_with;
1314

1415
/**
1516
* @implements Rule<Node\Expr\PropertyFetch>
@@ -71,6 +72,13 @@ public function processNode(Node $node, Scope $scope): array
7172
continue;
7273
}
7374

75+
if (
76+
str_ends_with($restrictedUsage->identifier, '.deprecated')
77+
&& DeprecatedSinceVersionHelper::isScopeVersionBeforeDeprecation($propertyReflection->getAttributes(), $scope->getPhpVersion())
78+
) {
79+
continue;
80+
}
81+
7482
$errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage)
7583
->identifier($restrictedUsage->identifier)
7684
->build();

src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use PHPStan\Rules\RuleLevelHelper;
1616
use PHPStan\Type\ErrorType;
1717
use PHPStan\Type\Type;
18+
use function str_ends_with;
1819

1920
/**
2021
* @implements Rule<StaticMethodCallableNode>
@@ -92,6 +93,13 @@ public function processNode(Node $node, Scope $scope): array
9293
continue;
9394
}
9495

96+
if (
97+
str_ends_with($restrictedUsage->identifier, '.deprecated')
98+
&& DeprecatedSinceVersionHelper::isScopeVersionBeforeDeprecation($methodReflection->getAttributes(), $scope->getPhpVersion())
99+
) {
100+
continue;
101+
}
102+
95103
if ($classReflection->getName() !== $methodReflection->getDeclaringClass()->getName()) {
96104
$rewrittenMethodReflection = new RewrittenDeclaringClassMethodReflection($classReflection, $methodReflection);
97105
$rewrittenRestrictedUsage = $extension->isRestrictedMethodUsage($rewrittenMethodReflection, $scope);

src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHPStan\Rules\RuleLevelHelper;
1515
use PHPStan\Type\ErrorType;
1616
use PHPStan\Type\Type;
17+
use function str_ends_with;
1718

1819
/**
1920
* @implements Rule<Node\Expr\StaticCall>
@@ -91,6 +92,13 @@ public function processNode(Node $node, Scope $scope): array
9192
continue;
9293
}
9394

95+
if (
96+
str_ends_with($restrictedUsage->identifier, '.deprecated')
97+
&& DeprecatedSinceVersionHelper::isScopeVersionBeforeDeprecation($methodReflection->getAttributes(), $scope->getPhpVersion())
98+
) {
99+
continue;
100+
}
101+
94102
if ($classReflection->getName() !== $methodReflection->getDeclaringClass()->getName()) {
95103
$rewrittenMethodReflection = new RewrittenDeclaringClassMethodReflection($classReflection, $methodReflection);
96104
$rewrittenRestrictedUsage = $extension->isRestrictedMethodUsage($rewrittenMethodReflection, $scope);

0 commit comments

Comments
 (0)