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
15 changes: 15 additions & 0 deletions .github/workflows/code_checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
2 changes: 1 addition & 1 deletion config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 8 additions & 34 deletions packages/PHPStanStaticTypeMapper/src/PHPStanStaticTypeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace Rector\PHPStanStaticTypeMapper\TypeMapper;

use PhpParser\Node;
use PhpParser\Node\Identifier;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\Type\Type;
use PHPStan\Type\VerbosityLevel;
use Rector\PHPStan\Type\SelfObjectType;
use Rector\PHPStanStaticTypeMapper\Contract\TypeMapperInterface;

final class SelfObjectTypeMapper implements TypeMapperInterface
{
public function getNodeClass(): string
{
return SelfObjectType::class;
}

/**
* @param SelfObjectType $type
*/
public function mapToPHPStanPhpDocTypeNode(Type $type): TypeNode
{
return new IdentifierTypeNode('self');
}

/**
* @param SelfObjectType $type
*/
public function mapToPhpParserNode(Type $type, ?string $kind = null): ?Node
{
return new Identifier('self');
}

/**
* @param SelfObjectType $type
*/
public function mapToDocString(Type $type, ?Type $parentType = null): string
{
return $type->describe(VerbosityLevel::typeOnly());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace Rector\PHPStanStaticTypeMapper\TypeMapper;

use PhpParser\Node;
use PhpParser\Node\Identifier;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\Type\StaticType;
use PHPStan\Type\ThisType;
use PHPStan\Type\Type;
use PHPStan\Type\VerbosityLevel;
use Rector\Exception\NotImplementedException;
use Rector\PHPStanStaticTypeMapper\Contract\TypeMapperInterface;

final class StaticTypeMapper implements TypeMapperInterface
{
public function getNodeClass(): string
{
return StaticType::class;
}

/**
* @param StaticType $type
*/
public function mapToPHPStanPhpDocTypeNode(Type $type): TypeNode
{
throw new NotImplementedException();
}

/**
* @param StaticType $type
*/
public function mapToPhpParserNode(Type $type, ?string $kind = null): ?Node
{
if ($type instanceof ThisType) {
return new Identifier('self');
}

return null;
}

/**
* @param StaticType $type
*/
public function mapToDocString(Type $type, ?Type $parentType = null): string
{
return $type->describe(VerbosityLevel::typeOnly());
}
}
8 changes: 8 additions & 0 deletions utils/PHPStanStaticTypeMapperChecker/config/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
services:
_defaults:
public: true
autowire: true
autoconfigure: true

Rector\Utils\PHPStanStaticTypeMapperChecker\:
resource: '../src'
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

declare(strict_types=1);

namespace Rector\Utils\PHPStanStaticTypeMapperChecker\Command;

use PHPStan\Type\NonexistentParentClassType;
use Rector\Console\Command\AbstractCommand;
use Rector\Console\Shell;
use Rector\PHPStanStaticTypeMapper\Contract\TypeMapperInterface;
use Rector\Utils\PHPStanStaticTypeMapperChecker\Finder\PHPStanTypeClassFinder;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symplify\PackageBuilder\Console\Command\CommandNaming;

final class CheckStaticTypeMappersCommand extends AbstractCommand
{
/**
* @var SymfonyStyle
*/
private $symfonyStyle;

/**
* @var TypeMapperInterface[]
*/
private $typeMappers = [];

/**
* @var PHPStanTypeClassFinder
*/
private $phpStanTypeClassFinder;

/**
* @param TypeMapperInterface[] $typeMappers
*/
public function __construct(
array $typeMappers,
SymfonyStyle $symfonyStyle,
PHPStanTypeClassFinder $phpStanTypeClassFinder
) {
$this->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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace Rector\Utils\PHPStanStaticTypeMapperChecker\Finder;

use Nette\Loaders\RobotLoader;
use Nette\Utils\Strings;

final class PHPStanTypeClassFinder
{
/**
* @return string[]
*/
public function find(): array
{
$robotLoader = new RobotLoader();
$robotLoader->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';
}
}