diff --git a/.github/workflows/code_checks.yml b/.github/workflows/code_checks.yml index 7643e37b9cbd..72d15523db17 100644 --- a/.github/workflows/code_checks.yml +++ b/.github/workflows/code_checks.yml @@ -100,8 +100,23 @@ jobs: - run: composer install --no-progress - run: | php ci/check_services_in_yaml_configs.php + - run: | php ci/run_all_sets.php + + phpstan_types_checks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - + uses: shivammathur/setup-php@v1 + with: + php-version: 7.2 + coverage: none # disable xdebug, pcov + - run: composer install --no-progress + - run: | bin/rector sync-types + - run: | + bin/rector check-static-type-mappers fatal_scan: runs-on: ubuntu-latest diff --git a/composer.json b/composer.json index 631fd6ec1af0..c88bd0e32861 100644 --- a/composer.json +++ b/composer.json @@ -173,6 +173,7 @@ "Rector\\Phalcon\\Tests\\": "packages/Phalcon/tests", "Rector\\DoctrineGedmoToKnplabs\\Tests\\": "packages/DoctrineGedmoToKnplabs/tests", "Rector\\MinimalScope\\Tests\\": "packages/MinimalScope/tests", + "Rector\\Utils\\PHPStanStaticTypeMapperChecker\\": "utils/PHPStanStaticTypeMapperChecker/src", "Rector\\Polyfill\\Tests\\": "packages/Polyfill/tests" }, "classmap": [ diff --git a/config/config.yaml b/config/config.yaml index aefa05240554..1c911199205a 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -2,7 +2,7 @@ imports: - { resource: '../packages/**/config/config.yaml' } - { resource: 'services.yaml' } # only in local repository - - { resource: '../utils/**/config/config.yaml', ignore_errors: true } + - { resource: '../utils/**/config/config.yaml' } parameters: # processed paths diff --git a/packages/PHPStanStaticTypeMapper/src/PHPStanStaticTypeMapper.php b/packages/PHPStanStaticTypeMapper/src/PHPStanStaticTypeMapper.php index 327bc7b8b0d2..41fd5aa9552d 100644 --- a/packages/PHPStanStaticTypeMapper/src/PHPStanStaticTypeMapper.php +++ b/packages/PHPStanStaticTypeMapper/src/PHPStanStaticTypeMapper.php @@ -7,13 +7,10 @@ use PhpParser\Node; use PhpParser\Node\Identifier; use PhpParser\Node\Name; -use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\NullableType; use PhpParser\Node\UnionType as PhpParserUnionType; use PHPStan\PhpDocParser\Ast\Type\TypeNode; -use PHPStan\Type\StaticType; use PHPStan\Type\Type; -use PHPStan\Type\TypeWithClassName; use Rector\Exception\NotImplementedException; use Rector\PHPStanStaticTypeMapper\Contract\TypeMapperInterface; @@ -48,52 +45,29 @@ public function mapToPHPStanPhpDocTypeNode(Type $type): TypeNode /** * @return Identifier|Name|NullableType|PhpParserUnionType|null */ - public function mapToPhpParserNode(Type $phpStanType, ?string $kind = null): ?Node + public function mapToPhpParserNode(Type $type, ?string $kind = null): ?Node { foreach ($this->typeMappers as $typeMapper) { - // it cannot be is_a for SelfObjectType, because type classes inherit from each other - if (! is_a($phpStanType, $typeMapper->getNodeClass(), true)) { + if (! is_a($type, $typeMapper->getNodeClass(), true)) { continue; } - return $typeMapper->mapToPhpParserNode($phpStanType, $kind); - } - - if ($phpStanType instanceof StaticType) { - return null; - } - - if ($phpStanType instanceof TypeWithClassName) { - $lowerCasedClassName = strtolower($phpStanType->getClassName()); - if ($lowerCasedClassName === 'self') { - return new Identifier('self'); - } - - if ($lowerCasedClassName === 'static') { - return null; - } - - if ($lowerCasedClassName === 'mixed') { - return null; - } - - return new FullyQualified($phpStanType->getClassName()); + return $typeMapper->mapToPhpParserNode($type, $kind); } - throw new NotImplementedException(__METHOD__ . ' for ' . get_class($phpStanType)); + throw new NotImplementedException(__METHOD__ . ' for ' . get_class($type)); } - public function mapToDocString(Type $phpStanType, ?Type $parentType = null): string + public function mapToDocString(Type $type, ?Type $parentType = null): string { foreach ($this->typeMappers as $typeMapper) { - // it cannot be is_a for SelfObjectType, because type classes inherit from each other - if (! is_a($phpStanType, $typeMapper->getNodeClass(), true)) { + if (! is_a($type, $typeMapper->getNodeClass(), true)) { continue; } - return $typeMapper->mapToDocString($phpStanType, $parentType); + return $typeMapper->mapToDocString($type, $parentType); } - throw new NotImplementedException(__METHOD__ . ' for ' . get_class($phpStanType)); + throw new NotImplementedException(__METHOD__ . ' for ' . get_class($type)); } } diff --git a/packages/PHPStanStaticTypeMapper/src/TypeMapper/SelfObjectTypeMapper.php b/packages/PHPStanStaticTypeMapper/src/TypeMapper/SelfObjectTypeMapper.php new file mode 100644 index 000000000000..d9d4f97d1b94 --- /dev/null +++ b/packages/PHPStanStaticTypeMapper/src/TypeMapper/SelfObjectTypeMapper.php @@ -0,0 +1,46 @@ +describe(VerbosityLevel::typeOnly()); + } +} diff --git a/packages/PHPStanStaticTypeMapper/src/TypeMapper/StaticTypeMapper.php b/packages/PHPStanStaticTypeMapper/src/TypeMapper/StaticTypeMapper.php new file mode 100644 index 000000000000..d50747c61152 --- /dev/null +++ b/packages/PHPStanStaticTypeMapper/src/TypeMapper/StaticTypeMapper.php @@ -0,0 +1,51 @@ +describe(VerbosityLevel::typeOnly()); + } +} diff --git a/utils/PHPStanStaticTypeMapperChecker/config/config.yaml b/utils/PHPStanStaticTypeMapperChecker/config/config.yaml new file mode 100644 index 000000000000..80dc8ad4bc57 --- /dev/null +++ b/utils/PHPStanStaticTypeMapperChecker/config/config.yaml @@ -0,0 +1,8 @@ +services: + _defaults: + public: true + autowire: true + autoconfigure: true + + Rector\Utils\PHPStanStaticTypeMapperChecker\: + resource: '../src' diff --git a/utils/PHPStanStaticTypeMapperChecker/src/Command/CheckStaticTypeMappersCommand.php b/utils/PHPStanStaticTypeMapperChecker/src/Command/CheckStaticTypeMappersCommand.php new file mode 100644 index 000000000000..e5ccfb1a9b45 --- /dev/null +++ b/utils/PHPStanStaticTypeMapperChecker/src/Command/CheckStaticTypeMappersCommand.php @@ -0,0 +1,108 @@ +typeMappers = $typeMappers; + $this->symfonyStyle = $symfonyStyle; + $this->phpStanTypeClassFinder = $phpStanTypeClassFinder; + + parent::__construct(); + } + + protected function configure(): void + { + $this->setName(CommandNaming::classToName(self::class)); + $this->setDescription('[Dev] check PHPStan types to TypeMappers'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $missingNodeClasses = $this->getMissingNodeClasses(); + if ($missingNodeClasses === []) { + $this->symfonyStyle->success('All PHPStan Types are covered by TypeMapper'); + + return Shell::CODE_SUCCESS; + } + + $this->symfonyStyle->error('Some classes are missing nodes'); + $this->symfonyStyle->listing($missingNodeClasses); + + die; + + return Shell::CODE_ERROR; + } + + /** + * @return string[] + */ + private function getMissingNodeClasses(): array + { + $phpStanTypeClasses = $this->phpStanTypeClassFinder->find(); + $supportedPHPStanTypeClasses = $this->getSupportedTypeClasses(); + + $unsupportedTypeClasses = []; + foreach ($phpStanTypeClasses as $phpStanTypeClass) { + foreach ($supportedPHPStanTypeClasses as $supportedPHPStanTypeClass) { + if (is_a($phpStanTypeClass, $supportedPHPStanTypeClass, true)) { + continue 2; + } + } + + $unsupportedTypeClasses[] = $phpStanTypeClass; + } + + $typesToRemove = [NonexistentParentClassType::class]; + + return array_diff($unsupportedTypeClasses, $typesToRemove); + } + + /** + * @return string[] + */ + private function getSupportedTypeClasses(): array + { + $supportedPHPStanTypeClasses = []; + foreach ($this->typeMappers as $typeMappers) { + $supportedPHPStanTypeClasses[] = $typeMappers->getNodeClass(); + } + + return $supportedPHPStanTypeClasses; + } +} diff --git a/utils/PHPStanStaticTypeMapperChecker/src/Finder/PHPStanTypeClassFinder.php b/utils/PHPStanStaticTypeMapperChecker/src/Finder/PHPStanTypeClassFinder.php new file mode 100644 index 000000000000..789ef462ee33 --- /dev/null +++ b/utils/PHPStanStaticTypeMapperChecker/src/Finder/PHPStanTypeClassFinder.php @@ -0,0 +1,60 @@ +addDirectory($this->getPHPStanPharSrcTypeDirectoryPath()); + + $robotLoader->setTempDirectory(sys_get_temp_dir() . '/_phpstan_types'); + $robotLoader->acceptFiles = ['*Type.php']; + $robotLoader->rebuild(); + + $classLikesToFilePaths = $robotLoader->getIndexedClasses(); + $classLikes = array_keys($classLikesToFilePaths); + + return $this->filterClassesOnly($classLikes); + } + + /** + * @param string[] $classLikes + * @return string[] + */ + private function filterClassesOnly(array $classLikes): array + { + $classes = []; + foreach ($classLikes as $classLike) { + if (! class_exists($classLike)) { + continue; + } + + if (Strings::match($classLike, '#\\\\Accessory\\\\#')) { + continue; + } + + $classes[] = $classLike; + } + return $classes; + } + + /** + * @see https://github.com/dg/nette-robot-loader/blob/593c0e40e511c0b0700610a6a3964a210219139f/tests/Loaders/RobotLoader.phar.phpt#L33 + */ + private function getPHPStanPharSrcTypeDirectoryPath(): string + { + $phpstanPharRealpath = realpath(__DIR__ . '/../../../../vendor/phpstan/phpstan/phpstan.phar'); + + return 'phar://' . $phpstanPharRealpath . '/src/Type'; + } +}