diff --git a/build/phpstan.neon b/build/phpstan.neon index 1e79bf303a..017fd4f8fa 100644 --- a/build/phpstan.neon +++ b/build/phpstan.neon @@ -64,6 +64,7 @@ parameters: - 'PHPStan\Reflection\MissingPropertyFromReflectionException' - 'PHPStan\Reflection\MissingConstantFromReflectionException' - 'PHPStan\Type\CircularTypeAliasDefinitionException' + - 'PHPStan\Reflection\MissingStaticAccessorInstanceException' - 'LogicException' - 'Error' check: diff --git a/src/DependencyInjection/ContainerFactory.php b/src/DependencyInjection/ContainerFactory.php index 379b1fb741..c11a5fe8a1 100644 --- a/src/DependencyInjection/ContainerFactory.php +++ b/src/DependencyInjection/ContainerFactory.php @@ -30,7 +30,6 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\ShouldNotHappenException; -use PHPStan\Type\ObjectType; use function array_diff_key; use function array_intersect; use function array_key_exists; @@ -195,7 +194,6 @@ public static function postInitializeContainer(Container $container): void ReflectionProviderStaticAccessor::registerInstance($container->getByType(ReflectionProvider::class)); PhpVersionStaticAccessor::registerInstance($container->getByType(PhpVersion::class)); - ObjectType::resetCaches(); $container->getService('typeSpecifier'); BleedingEdgeToggle::setBleedingEdge($container->getParameter('featureToggles')['bleedingEdge']); diff --git a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php index b4f16e66f7..6be5278504 100644 --- a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php +++ b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php @@ -24,6 +24,7 @@ use PHPStan\PhpDocParser\Parser\TypeParser; use PHPStan\PhpDocParser\ParserConfig; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\MissingStaticAccessorInstanceException; use PHPStan\Reflection\PhpVersionStaticAccessor; use PHPStan\Reflection\ReflectionProvider\DirectReflectionProviderProvider; use PHPStan\Reflection\ReflectionProvider\DummyReflectionProvider; @@ -66,120 +67,143 @@ public function loadConfiguration(): void $parser = Llk::load(new Read(__DIR__ . '/../../resources/RegexGrammar.pp')); $reflectionProvider = new DummyReflectionProvider(); $reflectionProviderProvider = new DirectReflectionProviderProvider($reflectionProvider); + + try { + $originalReflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + } catch (MissingStaticAccessorInstanceException) { + $originalReflectionProvider = null; + } + + try { + $originalPhpVersion = PhpVersionStaticAccessor::getInstance(); + } catch (MissingStaticAccessorInstanceException) { + $originalPhpVersion = null; + } + ReflectionProviderStaticAccessor::registerInstance($reflectionProvider); PhpVersionStaticAccessor::registerInstance(new PhpVersion(PHP_VERSION_ID)); - $composerPhpVersionFactory = new ComposerPhpVersionFactory([]); - $constantResolver = new ConstantResolver($reflectionProviderProvider, [], null, $composerPhpVersionFactory, null); - - $phpDocParserConfig = new ParserConfig([]); - $ignoredRegexValidator = new IgnoredRegexValidator( - $parser, - new TypeStringResolver( - new Lexer($phpDocParserConfig), - new TypeParser($phpDocParserConfig, new ConstExprParser($phpDocParserConfig)), - new TypeNodeResolver( - new DirectTypeNodeResolverExtensionRegistryProvider( - new class implements TypeNodeResolverExtensionRegistry { - - public function getExtensions(): array + + try { + $composerPhpVersionFactory = new ComposerPhpVersionFactory([]); + $constantResolver = new ConstantResolver($reflectionProviderProvider, [], null, $composerPhpVersionFactory, null); + + $phpDocParserConfig = new ParserConfig([]); + $ignoredRegexValidator = new IgnoredRegexValidator( + $parser, + new TypeStringResolver( + new Lexer($phpDocParserConfig), + new TypeParser($phpDocParserConfig, new ConstExprParser($phpDocParserConfig)), + new TypeNodeResolver( + new DirectTypeNodeResolverExtensionRegistryProvider( + new class implements TypeNodeResolverExtensionRegistry { + + public function getExtensions(): array + { + return []; + } + + }, + ), + $reflectionProviderProvider, + new DirectTypeAliasResolverProvider(new class implements TypeAliasResolver { + + public function hasTypeAlias(string $aliasName, ?string $classNameScope): bool { - return []; + return false; } - }, - ), - $reflectionProviderProvider, - new DirectTypeAliasResolverProvider(new class implements TypeAliasResolver { - - public function hasTypeAlias(string $aliasName, ?string $classNameScope): bool - { - return false; - } - - public function resolveTypeAlias(string $aliasName, NameScope $nameScope): ?Type - { - return null; - } + public function resolveTypeAlias(string $aliasName, NameScope $nameScope): ?Type + { + return null; + } - }), - $constantResolver, - new InitializerExprTypeResolver($constantResolver, $reflectionProviderProvider, new PhpVersion(PHP_VERSION_ID), new class implements OperatorTypeSpecifyingExtensionRegistryProvider { + }), + $constantResolver, + new InitializerExprTypeResolver($constantResolver, $reflectionProviderProvider, new PhpVersion(PHP_VERSION_ID), new class implements OperatorTypeSpecifyingExtensionRegistryProvider { - public function getRegistry(): OperatorTypeSpecifyingExtensionRegistry - { - return new OperatorTypeSpecifyingExtensionRegistry([]); - } + public function getRegistry(): OperatorTypeSpecifyingExtensionRegistry + { + return new OperatorTypeSpecifyingExtensionRegistry([]); + } - }, new OversizedArrayBuilder(), true), + }, new OversizedArrayBuilder(), true), + ), ), - ), - ); + ); - $errors = []; - foreach ($ignoreErrors as $ignoreError) { - if (is_array($ignoreError)) { - if (isset($ignoreError['count'])) { - continue; // ignoreError coming from baseline will be correct - } - if (isset($ignoreError['messages'])) { - $ignoreMessages = $ignoreError['messages']; - } elseif (isset($ignoreError['message'])) { - $ignoreMessages = [$ignoreError['message']]; + $errors = []; + foreach ($ignoreErrors as $ignoreError) { + if (is_array($ignoreError)) { + if (isset($ignoreError['count'])) { + continue; // ignoreError coming from baseline will be correct + } + if (isset($ignoreError['messages'])) { + $ignoreMessages = $ignoreError['messages']; + } elseif (isset($ignoreError['message'])) { + $ignoreMessages = [$ignoreError['message']]; + } else { + continue; + } } else { - continue; + $ignoreMessages = [$ignoreError]; } - } else { - $ignoreMessages = [$ignoreError]; - } - foreach ($ignoreMessages as $ignoreMessage) { - $error = $this->validateMessage($ignoredRegexValidator, $ignoreMessage); - if ($error === null) { - continue; + foreach ($ignoreMessages as $ignoreMessage) { + $error = $this->validateMessage($ignoredRegexValidator, $ignoreMessage); + if ($error === null) { + continue; + } + $errors[] = $error; } - $errors[] = $error; } - } - $reportUnmatched = (bool) $builder->parameters['reportUnmatchedIgnoredErrors']; + $reportUnmatched = (bool) $builder->parameters['reportUnmatchedIgnoredErrors']; - if ($reportUnmatched) { - foreach ($ignoreErrors as $ignoreError) { - if (!is_array($ignoreError)) { - continue; - } + if ($reportUnmatched) { + foreach ($ignoreErrors as $ignoreError) { + if (!is_array($ignoreError)) { + continue; + } - if (isset($ignoreError['path'])) { - $ignorePaths = [$ignoreError['path']]; - } elseif (isset($ignoreError['paths'])) { - $ignorePaths = $ignoreError['paths']; - } else { - continue; - } + if (isset($ignoreError['path'])) { + $ignorePaths = [$ignoreError['path']]; + } elseif (isset($ignoreError['paths'])) { + $ignorePaths = $ignoreError['paths']; + } else { + continue; + } - foreach ($ignorePaths as $ignorePath) { - if (FileExcluder::isAbsolutePath($ignorePath)) { - if (is_dir($ignorePath)) { - continue; + foreach ($ignorePaths as $ignorePath) { + if (FileExcluder::isAbsolutePath($ignorePath)) { + if (is_dir($ignorePath)) { + continue; + } + if (is_file($ignorePath)) { + continue; + } } - if (is_file($ignorePath)) { + if (FileExcluder::isFnmatchPattern($ignorePath)) { continue; } - } - if (FileExcluder::isFnmatchPattern($ignorePath)) { - continue; - } - $errors[] = sprintf('Path "%s" is neither a directory, nor a file path, nor a fnmatch pattern.', $ignorePath); + $errors[] = sprintf('Path "%s" is neither a directory, nor a file path, nor a fnmatch pattern.', $ignorePath); + } } } - } - if (count($errors) === 0) { - return; - } + if (count($errors) === 0) { + return; + } - throw new InvalidIgnoredErrorPatternsException($errors); + throw new InvalidIgnoredErrorPatternsException($errors); + } finally { + if ($originalReflectionProvider !== null) { + ReflectionProviderStaticAccessor::registerInstance($originalReflectionProvider); + } + if ($originalPhpVersion !== null) { + PhpVersionStaticAccessor::registerInstance($originalPhpVersion); + } + } } private function validateMessage(IgnoredRegexValidator $ignoredRegexValidator, string $ignoreMessage): ?string diff --git a/src/PhpDoc/StubValidator.php b/src/PhpDoc/StubValidator.php index 43b7f79171..a9f5e925ba 100644 --- a/src/PhpDoc/StubValidator.php +++ b/src/PhpDoc/StubValidator.php @@ -97,7 +97,6 @@ use PHPStan\Rules\Properties\MissingPropertyTypehintRule; use PHPStan\Rules\Registry as RuleRegistry; use PHPStan\Type\FileTypeMapper; -use PHPStan\Type\ObjectType; use Throwable; use function array_fill_keys; use function count; @@ -125,56 +124,58 @@ public function validate(array $stubFiles, bool $debug): array $originalReflectionProvider = ReflectionProviderStaticAccessor::getInstance(); $originalPhpVersion = PhpVersionStaticAccessor::getInstance(); - $container = $this->derivativeContainerFactory->create([ - __DIR__ . '/../../conf/config.stubValidator.neon', - ]); - $ruleRegistry = $this->getRuleRegistry($container); - $collectorRegistry = $this->getCollectorRegistry($container); + try { + $container = $this->derivativeContainerFactory->create([ + __DIR__ . '/../../conf/config.stubValidator.neon', + ]); - $fileAnalyser = $container->getByType(FileAnalyser::class); + $ruleRegistry = $this->getRuleRegistry($container); + $collectorRegistry = $this->getCollectorRegistry($container); - $nodeScopeResolver = $container->getByType(NodeScopeResolver::class); - $nodeScopeResolver->setAnalysedFiles($stubFiles); + $fileAnalyser = $container->getByType(FileAnalyser::class); - $pathRoutingParser = $container->getService('pathRoutingParser'); - $pathRoutingParser->setAnalysedFiles($stubFiles); + $nodeScopeResolver = $container->getByType(NodeScopeResolver::class); + $nodeScopeResolver->setAnalysedFiles($stubFiles); - $analysedFiles = array_fill_keys($stubFiles, true); + $pathRoutingParser = $container->getService('pathRoutingParser'); + $pathRoutingParser->setAnalysedFiles($stubFiles); - $errors = []; - foreach ($stubFiles as $stubFile) { - try { - $tmpErrors = $fileAnalyser->analyseFile( - $stubFile, - $analysedFiles, - $ruleRegistry, - $collectorRegistry, - static function (): void { - }, - )->getErrors(); - foreach ($tmpErrors as $tmpError) { - $errors[] = $tmpError->withoutTip()->doNotIgnore(); - } - } catch (Throwable $e) { - if ($debug) { - throw $e; - } + $analysedFiles = array_fill_keys($stubFiles, true); + + $errors = []; + foreach ($stubFiles as $stubFile) { + try { + $tmpErrors = $fileAnalyser->analyseFile( + $stubFile, + $analysedFiles, + $ruleRegistry, + $collectorRegistry, + static function (): void { + }, + )->getErrors(); + foreach ($tmpErrors as $tmpError) { + $errors[] = $tmpError->withoutTip()->doNotIgnore(); + } + } catch (Throwable $e) { + if ($debug) { + throw $e; + } - $internalErrorMessage = sprintf('Internal error: %s', $e->getMessage()); - $errors[] = (new Error($internalErrorMessage, $stubFile, canBeIgnored: $e)) - ->withIdentifier('phpstan.internal') - ->withMetadata([ - InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), - InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), - ]); + $internalErrorMessage = sprintf('Internal error: %s', $e->getMessage()); + $errors[] = (new Error($internalErrorMessage, $stubFile, canBeIgnored: $e)) + ->withIdentifier('phpstan.internal') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); + } } + } finally { + ReflectionProviderStaticAccessor::registerInstance($originalReflectionProvider); + PhpVersionStaticAccessor::registerInstance($originalPhpVersion); } - ReflectionProviderStaticAccessor::registerInstance($originalReflectionProvider); - PhpVersionStaticAccessor::registerInstance($originalPhpVersion); - ObjectType::resetCaches(); - return $errors; } diff --git a/src/Reflection/MissingStaticAccessorInstanceException.php b/src/Reflection/MissingStaticAccessorInstanceException.php new file mode 100644 index 0000000000..94bf6a28e4 --- /dev/null +++ b/src/Reflection/MissingStaticAccessorInstanceException.php @@ -0,0 +1,10 @@ +overriddenReadOnlyProperty', ], [ - 'DOMElement|null', + PHP_VERSION_ID < 80100 ? 'string' : 'DOMElement|null', '$this->documentElement', ], ]; diff --git a/tests/PHPStan/DependencyInjection/InvalidIgnoredErrorExceptionTest.php b/tests/PHPStan/DependencyInjection/InvalidIgnoredErrorExceptionTest.php new file mode 100644 index 0000000000..a37a7a73ee --- /dev/null +++ b/tests/PHPStan/DependencyInjection/InvalidIgnoredErrorExceptionTest.php @@ -0,0 +1,64 @@ + + */ + public static function dataValidateIgnoreErrors(): iterable + { + yield [ + __DIR__ . '/invalidIgnoreErrors/message-and-messages.neon', + 'An ignoreErrors entry cannot contain both message and messages fields.', + ]; + yield [ + __DIR__ . '/invalidIgnoreErrors/rawMessage-and-message.neon', + 'An ignoreErrors entry cannot contain both rawMessage and message fields.', + ]; + yield [ + __DIR__ . '/invalidIgnoreErrors/rawMessage-and-messages.neon', + 'An ignoreErrors entry cannot contain both rawMessage and messages fields.', + ]; + yield [ + __DIR__ . '/invalidIgnoreErrors/identifier-and-identifiers.neon', + 'An ignoreErrors entry cannot contain both identifier and identifiers fields.', + ]; + yield [ + __DIR__ . '/invalidIgnoreErrors/path-and-paths.neon', + 'An ignoreErrors entry cannot contain both path and paths fields.', + ]; + yield [ + __DIR__ . '/invalidIgnoreErrors/missing-main-key.neon', + 'An ignoreErrors entry must contain at least one of the following fields: message, messages, rawMessage, identifier, identifiers, path, paths.', + ]; + yield [ + __DIR__ . '/invalidIgnoreErrors/count-without-path.neon', + 'An ignoreErrors entry with count field must also contain path field.', + ]; + } + + #[DataProvider('dataValidateIgnoreErrors')] + public function testValidateIgnoreErrors(string $file, string $expectedMessage): void + { + self::$configFile = $file; + $this->expectExceptionMessage($expectedMessage); + self::getContainer(); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + self::$configFile, + ]; + } + +} diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php index 0b0f721ca8..cb9dc524d9 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php @@ -130,12 +130,16 @@ public function testObjectShapes(): void public function testConflictingAnnotationProperty(): void { - $this->analyse([__DIR__ . '/data/conflicting-annotation-property.php'], [ - [ - 'Access to private property ConflictingAnnotationProperty\PropertyWithAnnotation::$test.', - 27, - ], - ]); + $errors = []; + if (PHP_VERSION_ID >= 80200) { + $errors = [ + [ + 'Access to private property ConflictingAnnotationProperty\PropertyWithAnnotation::$test.', + 27, + ], + ]; + } + $this->analyse([__DIR__ . '/data/conflicting-annotation-property.php'], $errors); } public function testBug10477(): void diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php index 94d296562c..26143e9a56 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php @@ -947,12 +947,17 @@ public function testConflictingAnnotationProperty(): void $this->checkThisOnly = false; $this->checkUnionTypes = true; $this->checkDynamicProperties = true; - $this->analyse([__DIR__ . '/data/conflicting-annotation-property.php'], [ - [ - 'Access to private property ConflictingAnnotationProperty\PropertyWithAnnotation::$test.', - 26, - ], - ]); + + $errors = []; + if (PHP_VERSION_ID >= 80200) { + $errors = [ + [ + 'Access to private property ConflictingAnnotationProperty\PropertyWithAnnotation::$test.', + 26, + ], + ]; + } + $this->analyse([__DIR__ . '/data/conflicting-annotation-property.php'], $errors); } #[RequiresPhp('>= 8.1')] diff --git a/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php index 07b195a07f..c46d35de67 100644 --- a/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php @@ -6,6 +6,7 @@ use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -80,12 +81,16 @@ public function testObjectShapes(): void public function testConflictingAnnotationProperty(): void { $this->checkThisOnly = false; - $this->analyse([__DIR__ . '/data/conflicting-annotation-property.php'], [ - /*[ - 'Property ConflictingAnnotationProperty\PropertyWithAnnotation::$test is not writable.', - 27, - ],*/ - ]); + $errors = []; + if (PHP_VERSION_ID < 80200) { + $errors = [ + [ + 'Property ConflictingAnnotationProperty\PropertyWithAnnotation::$test is not writable.', + 27, + ], + ]; + } + $this->analyse([__DIR__ . '/data/conflicting-annotation-property.php'], $errors); } #[RequiresPhp('>= 8.4')]