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
109 changes: 109 additions & 0 deletions build/PHPStan/Build/NoPhpVersionInjectionInScopeAwareExtensionRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php declare(strict_types = 1);

namespace PHPStan\Build;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClassNode;
use PHPStan\Php\PhpVersion;
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\ObjectType;
use PHPStan\Type\StaticMethodParameterClosureThisExtension;
use PHPStan\Type\StaticMethodParameterClosureTypeExtension;
use PHPStan\Type\StaticMethodParameterOutTypeExtension;
use PHPStan\Type\StaticMethodTypeSpecifyingExtension;
use PHPStan\Type\TypeCombinator;
use function sprintf;

/**
* Extensions that receive a Scope must ask Scope::getPhpVersion() about the analysed
* PHP version so that PHP_VERSION_ID checks in the analysed code narrow their answers.
*
* @implements Rule<InClassNode>
*/
final class NoPhpVersionInjectionInScopeAwareExtensionRule 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,
FunctionParameterClosureTypeExtension::class,
MethodParameterClosureTypeExtension::class,
StaticMethodParameterClosureTypeExtension::class,
FunctionParameterClosureThisExtension::class,
MethodParameterClosureThisExtension::class,
StaticMethodParameterClosureThisExtension::class,
FunctionParameterOutTypeExtension::class,
MethodParameterOutTypeExtension::class,
StaticMethodParameterOutTypeExtension::class,
ExpressionTypeResolverExtension::class,
];

public function getNodeType(): string
{
return InClassNode::class;
}

public function processNode(Node $node, Scope $scope): array
{
$classReflection = $node->getClassReflection();
$implementedExtension = null;
foreach (self::SCOPE_AWARE_EXTENSIONS as $extension) {
if ($classReflection->implementsInterface($extension)) {
$implementedExtension = $extension;
break;
}
}
if ($implementedExtension === null) {
return [];
}

if (!$classReflection->hasConstructor()) {
return [];
}

$phpVersionType = new ObjectType(PhpVersion::class);
$errors = [];
foreach ($classReflection->getConstructor()->getOnlyVariant()->getParameters() as $parameter) {
if (!$phpVersionType->isSuperTypeOf(TypeCombinator::removeNull($parameter->getType()))->yes()) {
continue;
}

$errors[] = RuleErrorBuilder::message(sprintf(
'%s implements %s and should not inject %s via constructor parameter $%s. Use Scope::getPhpVersion() instead.',
$classReflection->getDisplayName(),
$implementedExtension,
PhpVersion::class,
$parameter->getName(),
))
->identifier('phpstan.phpVersionInjection')
->build();
}

return $errors;
}

}
1 change: 1 addition & 0 deletions build/phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ rules:
- PHPStan\Build\SkipTestsWithRequiresPhpAttributeRule
- PHPStan\Build\MemoizationPropertyRule
- PHPStan\Build\OrChainIdenticalComparisonToInArrayRule
- PHPStan\Build\NoPhpVersionInjectionInScopeAwareExtensionRule

services:
-
Expand Down
6 changes: 6 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1542,6 +1542,12 @@ parameters:
count: 2
path: src/Type/Php/MinMaxFunctionReturnTypeExtension.php

-
rawMessage: 'PHPStan\Type\Php\PDOConnectReturnTypeExtension implements PHPStan\Type\DynamicStaticMethodReturnTypeExtension and should not inject PHPStan\Php\PhpVersion via constructor parameter $phpVersion. Use Scope::getPhpVersion() instead.'
identifier: phpstan.phpVersionInjection
count: 1
path: src/Type/Php/PDOConnectReturnTypeExtension.php

-
rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.'
identifier: phpstanApi.instanceofType
Expand Down
5 changes: 5 additions & 0 deletions src/Php/PhpVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,11 @@ public function isNullValidArgInMbSubstituteCharacter(): bool
return $this->versionId >= 80000;
}

public function isZeroValidCodePointInMbSubstituteCharacter(): bool
{
return $this->versionId >= 80000;
}

public function isInterfaceConstantImplicitlyFinal(): bool
{
return $this->versionId < 80100;
Expand Down
11 changes: 11 additions & 0 deletions src/Php/PhpVersions.php
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,17 @@ public function isNullValidArgInMbSubstituteCharacter(): TrinaryLogic
return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result;
}

public function isZeroValidCodePointInMbSubstituteCharacter(): TrinaryLogic
{
return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result;
}

/** On PHP 8.0+ the Reflection* classes extended by the BetterReflection enum adapters declare the correct return types. */
public function hasCorrectReflectionEnumAdapterReturnTypes(): TrinaryLogic
{
return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result;
}

public function isNumericStringValidArgInMbSubstituteCharacter(): TrinaryLogic
{
return IntegerRangeType::fromInterval(null, 79999)->isSuperTypeOf($this->phpVersions)->result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\BetterReflection\Reflection\Adapter\ReflectionType;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
Expand All @@ -22,7 +21,7 @@ final class AdapterReflectionEnumCaseDynamicReturnTypeExtension implements Dynam
/**
* @param class-string $class
*/
public function __construct(private PhpVersion $phpVersion, private string $class)
public function __construct(private string $class)
{
}

Expand All @@ -41,7 +40,7 @@ public function isMethodSupported(MethodReflection $methodReflection): bool

public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
{
if ($this->phpVersion->getVersionId() >= 80000) {
if ($scope->getPhpVersion()->hasCorrectReflectionEnumAdapterReturnTypes()->yes()) {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnum;
use PHPStan\BetterReflection\Reflection\Adapter\ReflectionNamedType;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
use PHPStan\Type\Constant\ConstantBooleanType;
Expand All @@ -27,10 +26,6 @@
final class AdapterReflectionEnumDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{

public function __construct(private PhpVersion $phpVersion)
{
}

public function getClass(): string
{
return ReflectionEnum::class;
Expand All @@ -52,7 +47,7 @@ public function isMethodSupported(MethodReflection $methodReflection): bool

public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
{
if ($this->phpVersion->getVersionId() >= 80000) {
if ($scope->getPhpVersion()->hasCorrectReflectionEnumAdapterReturnTypes()->yes()) {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\ObjectType;
Expand All @@ -17,7 +16,7 @@ final class NativeReflectionEnumReturnDynamicReturnTypeExtension implements Dyna
/**
* @param class-string $className
*/
public function __construct(private PhpVersion $phpVersion, private string $className, private string $methodName)
public function __construct(private string $className, private string $methodName)
{
}

Expand All @@ -33,7 +32,7 @@ public function isMethodSupported(MethodReflection $methodReflection): bool

public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
{
if ($this->phpVersion->getVersionId() >= 80000) {
if ($scope->getPhpVersion()->hasCorrectReflectionEnumAdapterReturnTypes()->yes()) {
return null;
}

Expand Down
9 changes: 2 additions & 7 deletions src/Type/Php/MbFunctionsReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\BooleanType;
Expand Down Expand Up @@ -38,10 +37,6 @@ final class MbFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeE
'mb_ord' => 2,
];

public function __construct(private PhpVersion $phpVersion)
{
}

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return array_key_exists($functionReflection->getName(), $this->encodingPositionMap);
Expand All @@ -62,15 +57,15 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
}

$strings = $scope->getType($args[$positionEncodingParam - 1]->value)->getConstantStrings();
$results = array_unique(array_map(fn (ConstantStringType $encoding): bool => $this->isSupportedEncoding($encoding->getValue()), $strings));
$results = array_unique(array_map(fn (ConstantStringType $encoding): bool => $this->isSupportedEncoding($encoding->getValue(), $scope->getPhpVersion()), $strings));

if ($returnType->equals(new UnionType([new StringType(), new BooleanType()]))) {
return count($results) === 1 ? new ConstantBooleanType($results[0]) : new BooleanType();
}

if (count($results) === 1) {
$invalidEncodingReturn = new ConstantBooleanType(false);
if ($this->phpVersion->throwsOnInvalidMbStringEncoding()) {
if ($scope->getPhpVersion()->throwsOnInvalidMbStringEncoding()->yes()) {
$invalidEncodingReturn = new NeverType();
}

Expand Down
39 changes: 20 additions & 19 deletions src/Type/Php/MbFunctionsReturnTypeExtensionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Type\Php;

use PHPStan\Php\PhpVersions;
use PHPStan\ShouldNotHappenException;
use function array_filter;
use function array_map;
Expand All @@ -19,39 +20,39 @@ trait MbFunctionsReturnTypeExtensionTrait
/** @var string[]|null */
private ?array $supportedEncodings = null;

private function isSupportedEncoding(string $encoding): bool
private function isSupportedEncoding(string $encoding, PhpVersions $phpVersion): bool
{
return in_array(strtoupper($encoding), $this->getSupportedEncodings(), true);
return in_array(strtoupper($encoding), $this->getSupportedEncodings($phpVersion), true);
}

/** @return string[] */
private function getSupportedEncodings(): array
private function getSupportedEncodings(PhpVersions $phpVersion): array
Comment thread
staabm marked this conversation as resolved.
{
if (!is_null($this->supportedEncodings)) {
return $this->supportedEncodings;
}

$supportedEncodings = [];
if (function_exists('mb_list_encodings')) {
foreach (mb_list_encodings() as $encoding) {
$aliases = @mb_encoding_aliases($encoding);
if ($aliases === false) {
throw new ShouldNotHappenException();
if (is_null($this->supportedEncodings)) {
$supportedEncodings = [];
if (function_exists('mb_list_encodings')) {
foreach (mb_list_encodings() as $encoding) {
$aliases = @mb_encoding_aliases($encoding);
if ($aliases === false) {
throw new ShouldNotHappenException();
}
$supportedEncodings = array_merge($supportedEncodings, $aliases, [$encoding]);
}
$supportedEncodings = array_merge($supportedEncodings, $aliases, [$encoding]);
}
$this->supportedEncodings = array_map('strtoupper', $supportedEncodings);
}
$this->supportedEncodings = array_map('strtoupper', $supportedEncodings);

$supportedEncodings = $this->supportedEncodings;

// PHP 7.3 and 7.4 claims 'pass' and its alias 'none' to be supported, but actually 'pass' was removed in 7.3
if (!$this->phpVersion->supportsPassNoneEncodings()) {
$this->supportedEncodings = array_filter(
$this->supportedEncodings,
if ($phpVersion->supportsPassNoneEncodings()->no()) {
$supportedEncodings = array_filter(
$supportedEncodings,
static fn (string $enc) => !in_array($enc, ['PASS', 'NONE'], true),
);
}

return $this->supportedEncodings;
return $supportedEncodings;
}

}
Loading
Loading