diff --git a/src/Analyser/DirectInternalScopeFactory.php b/src/Analyser/DirectInternalScopeFactory.php index 07d7fbe09c..e79aada21f 100644 --- a/src/Analyser/DirectInternalScopeFactory.php +++ b/src/Analyser/DirectInternalScopeFactory.php @@ -14,6 +14,7 @@ use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Rules\RuleLevelHelper; use PHPStan\ShouldNotHappenException; use function is_a; @@ -38,6 +39,7 @@ public function __construct( private bool $explicitMixedInUnknownGenericNew, private bool $explicitMixedForGlobalVariables, private ConstantResolver $constantResolver, + private RuleLevelHelper $ruleLevelHelper, ) { } @@ -86,6 +88,7 @@ public function create( $this->parser, $this->nodeScopeResolver, $this->constantResolver, + $this->ruleLevelHelper, $context, $this->phpVersion, $declareStrictTypes, diff --git a/src/Analyser/LazyInternalScopeFactory.php b/src/Analyser/LazyInternalScopeFactory.php index 0d74666e85..da2ec75428 100644 --- a/src/Analyser/LazyInternalScopeFactory.php +++ b/src/Analyser/LazyInternalScopeFactory.php @@ -14,6 +14,7 @@ use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Rules\RuleLevelHelper; use PHPStan\ShouldNotHappenException; use function is_a; @@ -80,6 +81,7 @@ public function create( $this->container->getService('currentPhpVersionSimpleParser'), $this->container->getByType(NodeScopeResolver::class), $this->container->getByType(ConstantResolver::class), + $this->container->getByType(RuleLevelHelper::class), $context, $this->container->getByType(PhpVersion::class), $declareStrictTypes, diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index e920b8b03f..75e4beeacc 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -66,6 +66,7 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Rules\RuleLevelHelper; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; @@ -188,6 +189,7 @@ public function __construct( private Parser $parser, private NodeScopeResolver $nodeScopeResolver, private ConstantResolver $constantResolver, + private RuleLevelHelper $ruleLevelHelper, private ScopeContext $context, private PhpVersion $phpVersion, private bool $declareStrictTypes = false, @@ -2772,7 +2774,7 @@ private function enterFunctionLike( } } $parameterNode = new Variable($parameter->getName()); - $expressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameterNode, $parameterType); + $expressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameterNode, $this->ruleLevelHelper->transformCommonType($parameterType)); $nativeParameterType = $parameter->getNativeType(); if ($parameter->isVariadic()) { diff --git a/src/Rules/RuleLevelHelper.php b/src/Rules/RuleLevelHelper.php index 5efd65b20e..3ba9687ab3 100644 --- a/src/Rules/RuleLevelHelper.php +++ b/src/Rules/RuleLevelHelper.php @@ -55,7 +55,7 @@ public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTyp return $this->acceptsWithReason($acceptingType, $acceptedType, $strictTypes)->result; } - private function transformCommonType(Type $type): Type + public function transformCommonType(Type $type): Type { if (!$this->checkExplicitMixed && !$this->checkImplicitMixed) { return $type; diff --git a/src/Testing/PHPStanTestCase.php b/src/Testing/PHPStanTestCase.php index 14efcc7480..ca52d644c1 100644 --- a/src/Testing/PHPStanTestCase.php +++ b/src/Testing/PHPStanTestCase.php @@ -2,6 +2,7 @@ namespace PHPStan\Testing; +use Nette\Neon\Neon; use PHPStan\Analyser\ConstantResolver; use PHPStan\Analyser\DirectInternalScopeFactory; use PHPStan\Analyser\Error; @@ -32,6 +33,7 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ReflectionProvider\DirectReflectionProviderProvider; use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\Constant\OversizedArrayBuilder; use PHPStan\Type\TypeAliasResolver; use PHPStan\Type\UsefulTypeAliasResolver; @@ -39,11 +41,16 @@ use PHPUnit\Framework\TestCase; use function array_merge; use function count; +use function file_put_contents; use function implode; +use function is_array; +use function ksort; +use function rand; use function rtrim; use function sha1; use function sprintf; use function sys_get_temp_dir; +use function unlink; use const DIRECTORY_SEPARATOR; use const PHP_VERSION_ID; @@ -57,12 +64,36 @@ abstract class PHPStanTestCase extends TestCase /** @var array */ private static array $containers = []; + private static ?string $testCaseConfig = null; + + protected function tearDown(): void + { + parent::tearDown(); + + self::$testCaseConfig = null; + } + + /** @param array|null $config */ + protected function setTestCaseConfig(?array $config): void + { + if ($config === null) { + self::$testCaseConfig = null; + return; + } + // Normalize the config to avoid creating unnecessary containers + if (isset($config['parameters']) && is_array($config['parameters'])) { + ksort($config['parameters']); + } + + self::$testCaseConfig = Neon::encode($config); + } + /** @api */ public static function getContainer(): Container { $additionalConfigFiles = static::getAdditionalConfigFiles(); $additionalConfigFiles[] = __DIR__ . '/TestCase.neon'; - $cacheKey = sha1(implode("\n", $additionalConfigFiles)); + $cacheKey = sha1(implode("\n", $additionalConfigFiles) . self::$testCaseConfig); if (!isset(self::$containers[$cacheKey])) { $tmpDir = sys_get_temp_dir() . '/phpstan-tests'; @@ -72,6 +103,15 @@ public static function getContainer(): Container self::fail($e->getMessage()); } + $testCaseConfigFile = null; + if (self::$testCaseConfig !== null) { + $testCaseConfigFile = $tmpDir . '/test_case_config_' . rand() . '.neon'; + if (file_put_contents($testCaseConfigFile, self::$testCaseConfig) === false) { + self::fail('Failed to write test case config to temp file.'); + } + $additionalConfigFiles[] = $testCaseConfigFile; + } + $rootDir = __DIR__ . '/../..'; $fileHelper = new FileHelper($rootDir); $rootDir = $fileHelper->normalizePath($rootDir, '/'); @@ -94,6 +134,10 @@ public static function getContainer(): Container require_once __DIR__ . '/../../stubs/runtime/Enum/ReflectionEnumUnitCase.php'; require_once __DIR__ . '/../../stubs/runtime/Enum/ReflectionEnumBackedCase.php'; } + + if ($testCaseConfigFile !== null) { + unlink($testCaseConfigFile); + } } else { ContainerFactory::postInitializeContainer(self::$containers[$cacheKey]); } @@ -191,6 +235,7 @@ public static function createScopeFactory(ReflectionProvider $reflectionProvider $container->getParameter('featureToggles')['explicitMixedInUnknownGenericNew'], $container->getParameter('featureToggles')['explicitMixedForGlobalVariables'], $constantResolver, + self::getContainer()->getByType(RuleLevelHelper::class), ), ); } diff --git a/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php index 023d00e9f4..f423d45380 100644 --- a/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php @@ -18,6 +18,8 @@ class AnalyserTraitsIntegrationTest extends PHPStanTestCase protected function setUp(): void { + parent::setUp(); + $this->fileHelper = self::getContainer()->getByType(FileHelper::class); } diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index 245ba3d554..21c77610b7 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -57,6 +57,8 @@ class TypeSpecifierTest extends PHPStanTestCase protected function setUp(): void { + parent::setUp(); + $reflectionProvider = $this->createReflectionProvider(); $this->printer = new Printer(); $this->typeSpecifier = self::getContainer()->getService('typeSpecifier'); diff --git a/tests/PHPStan/Analyser/traitsCachingIssue/TraitsCachingIssueIntegrationTest.php b/tests/PHPStan/Analyser/traitsCachingIssue/TraitsCachingIssueIntegrationTest.php index ee7dd1f215..11869d8601 100644 --- a/tests/PHPStan/Analyser/traitsCachingIssue/TraitsCachingIssueIntegrationTest.php +++ b/tests/PHPStan/Analyser/traitsCachingIssue/TraitsCachingIssueIntegrationTest.php @@ -23,6 +23,8 @@ class TraitsCachingIssueIntegrationTest extends PHPStanTestCase public function tearDown(): void { + parent::tearDown(); + $this->deleteCache(); if ($this->originalTraitOneContents !== null) { diff --git a/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php index 5232f164ff..631ca0c28c 100644 --- a/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php @@ -18,11 +18,15 @@ class TableErrorFormatterTest extends ErrorFormatterTestCase protected function setUp(): void { + parent::setUp(); + putenv('GITHUB_ACTIONS'); } protected function tearDown(): void { + parent::tearDown(); + putenv('COLUMNS'); } diff --git a/tests/PHPStan/PhpDoc/DefaultStubFilesProviderTest.php b/tests/PHPStan/PhpDoc/DefaultStubFilesProviderTest.php index e7ea48c599..83a372092a 100644 --- a/tests/PHPStan/PhpDoc/DefaultStubFilesProviderTest.php +++ b/tests/PHPStan/PhpDoc/DefaultStubFilesProviderTest.php @@ -12,6 +12,8 @@ class DefaultStubFilesProviderTest extends PHPStanTestCase protected function setUp(): void { + parent::setUp(); + $this->currentWorkingDirectory = $this->getContainer()->getParameter('currentWorkingDirectory'); } diff --git a/tests/PHPStan/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRuleTest.php b/tests/PHPStan/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRuleTest.php index 4c50e26ccc..fbf26fa675 100644 --- a/tests/PHPStan/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRuleTest.php +++ b/tests/PHPStan/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRuleTest.php @@ -21,6 +21,8 @@ protected function getRule(): Rule protected function tearDown(): void { + parent::tearDown(); + @unlink(__DIR__ . '/composer.json'); } diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index 670e24f5be..6d00ef5032 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -19,14 +19,11 @@ class CallStaticMethodsRuleTest extends RuleTestCase { - private bool $checkThisOnly; - - private bool $checkExplicitMixed = false; - protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); - $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, $this->checkThisOnly, true, $this->checkExplicitMixed, false, true, false); + $ruleLevelHelper = $this->getContainer()->getByType(RuleLevelHelper::class); + return new CallStaticMethodsRule( new StaticMethodCallCheck($reflectionProvider, $ruleLevelHelper, new ClassCaseSensitivityCheck($reflectionProvider, true), true, true), new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, true), @@ -35,7 +32,6 @@ protected function getRule(): Rule public function testCallStaticMethods(): void { - $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/call-static-methods.php'], [ [ 'Call to an undefined static method CallStaticMethods\Foo::bar().', @@ -232,7 +228,6 @@ public function testCallStaticMethods(): void public function testCallInterfaceMethods(): void { - $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/call-interface-methods.php'], [ [ 'Cannot call abstract static method InterfaceMethods\Foo::fooStaticMethod().', @@ -247,7 +242,6 @@ public function testCallInterfaceMethods(): void public function testCallToIncorrectCaseMethodName(): void { - $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/incorrect-static-method-case.php'], [ [ 'Call to static method IncorrectStaticMethodCase\Foo::fooBar() with incorrect case: foobar', @@ -258,7 +252,6 @@ public function testCallToIncorrectCaseMethodName(): void public function testStaticCallsToInstanceMethods(): void { - $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/static-calls-to-instance-methods.php'], [ [ 'Static call to instance method StaticCallsToInstanceMethods\Foo::doFoo().', @@ -293,7 +286,6 @@ public function testStaticCallsToInstanceMethods(): void public function testStaticCallOnExpression(): void { - $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/static-call-on-expression.php'], [ [ 'Call to an undefined static method StaticCallOnExpression\Foo::doBar().', @@ -304,13 +296,16 @@ public function testStaticCallOnExpression(): void public function testStaticCallOnExpressionWithCheckDisabled(): void { - $this->checkThisOnly = true; + $this->setTestCaseConfig([ + 'parameters' => [ + 'checkThisOnly' => true, + ], + ]); $this->analyse([__DIR__ . '/data/static-call-on-expression.php'], []); } public function testReturnStatic(): void { - $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/return-static-static-method.php'], [ [ 'Call to an undefined static method ReturnStaticStaticMethod\Bar::doBaz().', @@ -321,7 +316,6 @@ public function testReturnStatic(): void public function testCallParentAbstractMethod(): void { - $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/call-parent-abstract-method.php'], [ [ 'Cannot call abstract method CallParentAbstractMethod\Baz::uninstall().', @@ -344,13 +338,11 @@ public function testCallParentAbstractMethod(): void public function testClassExists(): void { - $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/static-methods-class-exists.php'], []); } public function testBug3448(): void { - $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/bug-3448.php'], [ [ 'Parameter #1 $lall of static method Bug3448\Foo::add() expects int, string given.', @@ -365,7 +357,6 @@ public function testBug3448(): void public function testBug3641(): void { - $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/bug-3641.php'], [ [ 'Static method Bug3641\Foo::bar() invoked with 1 parameter, 0 required.', @@ -376,7 +367,6 @@ public function testBug3641(): void public function testBug2164(): void { - $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/bug-2164.php'], [ [ 'Parameter #1 $arg of static method Bug2164\A::staticTest() expects static(Bug2164\B)|string, Bug2164\B|string given.', @@ -387,8 +377,6 @@ public function testBug2164(): void public function testNamedArguments(): void { - $this->checkThisOnly = false; - $this->analyse([__DIR__ . '/data/static-method-named-arguments.php'], [ [ 'Missing parameter $j (int) in call to static method StaticMethodNamedArguments\Foo::doFoo().', @@ -403,13 +391,11 @@ public function testNamedArguments(): void public function testBug577(): void { - $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/bug-577.php'], []); } public function testBug4550(): void { - $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/bug-4550.php'], [ [ 'Parameter #1 $class of static method Bug4550\Test::valuesOf() expects class-string, string given.', @@ -428,7 +414,6 @@ public function testBug1971(): void $this->markTestSkipped('Test requires PHP 7.x'); } - $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/bug-1971.php'], [ [ 'Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{class-string, \'sayHello2\'} given.', @@ -443,7 +428,6 @@ public function testBug1971Php8(): void $this->markTestSkipped('Test requires PHP 8.0'); } - $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/bug-1971.php'], [ [ 'Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{\'Bug1971\\\HelloWorld\', \'sayHello\'} given.', @@ -462,63 +446,53 @@ public function testBug1971Php8(): void public function testBug5259(): void { - $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/bug-5259.php'], []); } public function testBug5536(): void { - $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/bug-5536.php'], []); } public function testBug4886(): void { - $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/bug-4886.php'], []); } public function testFirstClassCallables(): void { - $this->checkThisOnly = false; - // handled by a different rule $this->analyse([__DIR__ . '/data/first-class-static-method-callable.php'], []); } public function testBug5893(): void { - $this->checkThisOnly = false; - $this->checkExplicitMixed = true; $this->analyse([__DIR__ . '/data/bug-5893.php'], []); } public function testBug6249(): void { // discussion https://github.com/phpstan/phpstan/discussions/6249 - $this->checkThisOnly = false; - $this->checkExplicitMixed = true; $this->analyse([__DIR__ . '/data/bug-6249.php'], []); } public function testBug5749(): void { - $this->checkThisOnly = false; - $this->checkExplicitMixed = true; $this->analyse([__DIR__ . '/data/bug-5749.php'], []); } public function testBug5757(): void { - $this->checkThisOnly = false; - $this->checkExplicitMixed = true; $this->analyse([__DIR__ . '/data/bug-5757.php'], []); } public function testDiscussion7004(): void { - $this->checkThisOnly = false; - $this->checkExplicitMixed = true; + $this->setTestCaseConfig([ + 'parameters' => [ + 'checkExplicitMixed' => true, + ], + ]); $this->analyse([__DIR__ . '/data/discussion-7004.php'], [ [ 'Parameter #1 $data of static method Discussion7004\Foo::fromArray1() expects array, array given.', @@ -537,8 +511,6 @@ public function testDiscussion7004(): void public function testTemplateTypeInOneBranchOfConditional(): void { - $this->checkThisOnly = false; - $this->checkExplicitMixed = true; $this->analyse([__DIR__ . '/data/template-type-in-one-branch-of-conditional.php'], [ [ 'Parameter #1 $params of static method TemplateTypeInOneBranchOfConditional\DriverManager::getConnection() expects array{wrapperClass?: class-string}, array{wrapperClass: \'stdClass\'} given.', @@ -555,15 +527,11 @@ public function testTemplateTypeInOneBranchOfConditional(): void public function testBug7489(): void { - $this->checkThisOnly = false; - $this->checkExplicitMixed = true; $this->analyse([__DIR__ . '/data/bug-7489.php'], []); } public function testHasMethodStaticCall(): void { - $this->checkThisOnly = false; - $this->checkExplicitMixed = false; $this->analyse([__DIR__ . '/data/static-has-method.php'], [ [ 'Call to an undefined static method StaticHasMethodCall\rex_var::doesNotExist().', @@ -578,22 +546,16 @@ public function testHasMethodStaticCall(): void public function testBug1267(): void { - $this->checkThisOnly = false; - $this->checkExplicitMixed = false; $this->analyse([__DIR__ . '/data/bug-1267.php'], []); } public function testBug6147(): void { - $this->checkThisOnly = false; - $this->checkExplicitMixed = false; $this->analyse([__DIR__ . '/data/bug-6147.php'], []); } public function testBug5781(): void { - $this->checkThisOnly = false; - $this->checkExplicitMixed = false; $this->analyse([__DIR__ . '/data/bug-5781.php'], [ [ 'Parameter #1 $param of static method Bug5781\Foo::bar() expects array{a: bool, b: bool, c: bool, d: bool, e: bool, f: bool, g: bool, h: bool, ...}, array{} given.', @@ -605,9 +567,6 @@ public function testBug5781(): void public function testRequireExtends(): void { - $this->checkThisOnly = false; - $this->checkExplicitMixed = false; - $this->analyse([__DIR__ . '/../Properties/data/require-extends.php'], [ [ 'Call to an undefined static method RequireExtends\MyInterface::doesNotExistStatic().', @@ -618,9 +577,6 @@ public function testRequireExtends(): void public function testRequireImplements(): void { - $this->checkThisOnly = false; - $this->checkExplicitMixed = false; - $this->analyse([__DIR__ . '/../Properties/data/require-implements.php'], [ [ 'Call to an undefined static method RequireImplements\MyBaseClass::doesNotExistStatic().', @@ -629,4 +585,36 @@ public function testRequireImplements(): void ]); } + public function testGenericInstanceofEnum(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1'); + } + + $this->setTestCaseConfig([ + 'parameters' => [ + 'checkExplicitMixed' => true, + 'checkImplicitMixed' => true, + ], + ]); + $this->analyse([__DIR__ . '/data/generic-instanceof-enum.php'], [ + [ + 'Call to an undefined static method T of mixed&UnitEnum::from().', + 14, + ], + [ + 'Call to an undefined static method T of mixed&UnitEnum::from().', + 25, + ], + [ + 'Call to an undefined static method T of object&UnitEnum::from().', + 36, + ], + [ + 'Call to an undefined static method UnitEnum::from().', + 43, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/generic-instanceof-enum.php b/tests/PHPStan/Rules/Methods/data/generic-instanceof-enum.php new file mode 100644 index 0000000000..186ff57b53 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/generic-instanceof-enum.php @@ -0,0 +1,46 @@ += 8.1 + +namespace GenericInstanceofEnum; + +class HelloWorld +{ + /** + * @template T + * @param T $val + */ + public function foo(mixed $val): void + { + if ($val instanceof \UnitEnum) { + $val::from('a'); + } + } + + /** + * @template T of mixed + * @param T $val + */ + public function foo2(mixed $val): void + { + if ($val instanceof \UnitEnum) { + $val::from('a'); + } + } + + /** + * @template T of object + * @param T $val + */ + public function foo3(mixed $val): void + { + if ($val instanceof \UnitEnum) { + $val::from('a'); + } + } + + public function foo4(mixed $val): void + { + if ($val instanceof \UnitEnum) { + $val::from('a'); + } + } +}