Repository navigation
Resolve PHP version checks in return-type, throw-type and type-specifying extensions from Scope::getPhpVersion()
#6529
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
Changes from 16 commits
8b4783e
c006f1f
56816b0
037ddad
add8984
5ee6d34
6dd96dd
f6b2836
b74fdd4
892a037
365835c
e9d2c95
80619fa
6d24a23
b2db476
a69e753
3565169
ddc2b6f
0d8d599
c778a5d
8cd6733
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| <?php declare(strict_types = 1); | ||
|
|
||
| namespace PHPStan\Build; | ||
|
|
||
| use PhpParser\Node; | ||
| use PHPStan\Analyser\IgnoreErrorExtension; | ||
| use PHPStan\Analyser\Scope; | ||
| use PHPStan\Collectors\Collector; | ||
| use PHPStan\Node\InClassNode; | ||
| use PHPStan\Php\PhpVersion; | ||
| use PHPStan\Rules\RestrictedUsage\RestrictedClassConstantUsageExtension; | ||
| use PHPStan\Rules\RestrictedUsage\RestrictedClassNameUsageExtension; | ||
| use PHPStan\Rules\RestrictedUsage\RestrictedFunctionUsageExtension; | ||
| use PHPStan\Rules\RestrictedUsage\RestrictedMethodUsageExtension; | ||
| use PHPStan\Rules\RestrictedUsage\RestrictedPropertyUsageExtension; | ||
| use PHPStan\Rules\Rule; | ||
| use PHPStan\Rules\RuleErrorBuilder; | ||
| use PHPStan\Type\DynamicFunctionReturnTypeExtension; | ||
| use PHPStan\Type\DynamicFunctionThrowTypeExtension; | ||
| use PHPStan\Type\DynamicMethodReturnTypeExtension; | ||
| use PHPStan\Type\DynamicMethodThrowTypeExtension; | ||
| use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; | ||
| use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; | ||
| use PHPStan\Type\ExpressionTypeResolverExtension; | ||
| use PHPStan\Type\FunctionParameterClosureThisExtension; | ||
| use PHPStan\Type\FunctionParameterClosureTypeExtension; | ||
| use PHPStan\Type\FunctionParameterOutTypeExtension; | ||
| use PHPStan\Type\FunctionTypeSpecifyingExtension; | ||
| use PHPStan\Type\MethodParameterClosureThisExtension; | ||
| use PHPStan\Type\MethodParameterClosureTypeExtension; | ||
| use PHPStan\Type\MethodParameterOutTypeExtension; | ||
| use PHPStan\Type\MethodTypeSpecifyingExtension; | ||
| use PHPStan\Type\StaticMethodParameterClosureThisExtension; | ||
| use PHPStan\Type\StaticMethodParameterClosureTypeExtension; | ||
| use PHPStan\Type\StaticMethodParameterOutTypeExtension; | ||
| use PHPStan\Type\StaticMethodTypeSpecifyingExtension; | ||
| use function sprintf; | ||
|
|
||
| /** | ||
| * Every one of these extension interfaces hands the call's Scope to the extension, | ||
| * so the analysed PHP version must be read from Scope::getPhpVersion(). A DI-injected | ||
| * PhpVersion always answers with the configured version and silently ignores both | ||
| * PHP_VERSION_ID narrowing in the analysed code and configured version ranges. | ||
| * | ||
| * The list holds every extension interface with a method taking the public Scope. | ||
| * Interfaces taking the engine-internal MutatingScope (ExprHandler, StmtHandler) are not | ||
| * extensions in this sense, and OperatorTypeSpecifyingExtension gets no Scope at all, | ||
| * so both are absent on purpose. An extension interface added later belongs here too. | ||
| * | ||
| * @implements Rule<InClassNode> | ||
| */ | ||
| final class NoInjectedPhpVersionInScopeAwareExtensionRule implements Rule | ||
| { | ||
|
|
||
| private const SCOPE_AWARE_EXTENSIONS = [ | ||
| DynamicFunctionReturnTypeExtension::class, | ||
| DynamicMethodReturnTypeExtension::class, | ||
| DynamicStaticMethodReturnTypeExtension::class, | ||
| DynamicFunctionThrowTypeExtension::class, | ||
| DynamicMethodThrowTypeExtension::class, | ||
| DynamicStaticMethodThrowTypeExtension::class, | ||
| FunctionTypeSpecifyingExtension::class, | ||
| MethodTypeSpecifyingExtension::class, | ||
| StaticMethodTypeSpecifyingExtension::class, | ||
| FunctionParameterOutTypeExtension::class, | ||
| MethodParameterOutTypeExtension::class, | ||
| StaticMethodParameterOutTypeExtension::class, | ||
| FunctionParameterClosureTypeExtension::class, | ||
| MethodParameterClosureTypeExtension::class, | ||
| StaticMethodParameterClosureTypeExtension::class, | ||
| FunctionParameterClosureThisExtension::class, | ||
| MethodParameterClosureThisExtension::class, | ||
| StaticMethodParameterClosureThisExtension::class, | ||
| RestrictedClassConstantUsageExtension::class, | ||
| RestrictedClassNameUsageExtension::class, | ||
| RestrictedFunctionUsageExtension::class, | ||
| RestrictedMethodUsageExtension::class, | ||
| RestrictedPropertyUsageExtension::class, | ||
| ExpressionTypeResolverExtension::class, | ||
| IgnoreErrorExtension::class, | ||
| Collector::class, | ||
| ]; | ||
|
|
||
| public function getNodeType(): string | ||
| { | ||
| return InClassNode::class; | ||
| } | ||
|
|
||
| public function processNode(Node $node, Scope $scope): array | ||
| { | ||
| $classReflection = $node->getClassReflection(); | ||
| if (!$classReflection->hasConstructor()) { | ||
| return []; | ||
| } | ||
|
|
||
| $implementedExtension = null; | ||
| foreach (self::SCOPE_AWARE_EXTENSIONS as $extensionInterface) { | ||
| if (!$classReflection->is($extensionInterface)) { | ||
| continue; | ||
| } | ||
|
|
||
| $implementedExtension = $extensionInterface; | ||
| break; | ||
| } | ||
|
|
||
| if ($implementedExtension === null) { | ||
| return []; | ||
| } | ||
|
|
||
| $constructorVariant = $classReflection->getConstructor()->getOnlyVariant(); | ||
| $errors = []; | ||
| foreach ($constructorVariant->getParameters() as $parameter) { | ||
| foreach ($parameter->getType()->getObjectClassNames() as $className) { | ||
| if ($className !== PhpVersion::class) { | ||
| continue; | ||
| } | ||
|
|
||
| $errors[] = RuleErrorBuilder::message(sprintf( | ||
| '%s implements %s and must not inject %s - read the analysed PHP version from Scope::getPhpVersion() instead.', | ||
| $classReflection->getDisplayName(), | ||
| $implementedExtension, | ||
| PhpVersion::class, | ||
| ))->identifier('phpstanBuild.phpVersionInExtension')->build(); | ||
| } | ||
| } | ||
|
|
||
| return $errors; | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1548,6 +1548,12 @@ parameters: | |
| count: 2 | ||
| path: src/Type/Php/MinMaxFunctionReturnTypeExtension.php | ||
|
|
||
| - | ||
| rawMessage: 'PHPStan\Type\Php\PDOConnectReturnTypeExtension implements PHPStan\Type\DynamicStaticMethodReturnTypeExtension and must not inject PHPStan\Php\PhpVersion - read the analysed PHP version from Scope::getPhpVersion() instead.' | ||
| identifier: phpstanBuild.phpVersionInExtension | ||
| count: 1 | ||
| path: src/Type/Php/PDOConnectReturnTypeExtension.php | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PDOConnectReturnTypeExtension requires php-src builtin classes, only available in PHP 8.4+ (its functionality does not depend on a configured php version but runtime capabilities)
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've made the change and committed it. Summary of what I did for the review comment:
One behaviour change fell out, in the right direction. Restricting to the declaring class stops the extension overriding a |
||
| - | ||
| rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.' | ||
| identifier: phpstanApi.instanceofType | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.