Skip to content

Commit

Permalink
refactor to php-parser approach without PHPStan, to make direct and m…
Browse files Browse the repository at this point in the history
…ore robuts
  • Loading branch information
TomasVotruba committed Sep 5, 2024
1 parent 5d5bc5d commit ad016eb
Show file tree
Hide file tree
Showing 19 changed files with 427 additions and 391 deletions.
3 changes: 3 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ parameters:
ignoreErrors:
# known class-string type
- '#Method Rector\\SwissKnife\\Helpers\\ClassNameResolver::resolveFromFileContents\(\) should return class-string\|null but returns string#'

# unrelated
- '#Parameter \#1 \$className of class Rector\\SwissKnife\\ValueObject\\ClassConstant constructor expects class-string, string given#'
5 changes: 4 additions & 1 deletion src/Command/NamespaceToPSR4Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ protected function configure(): void
);
}

protected function execute(InputInterface $input, OutputInterface $output)
/**
* @return self::*
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$path = (string) $input->getArgument('path');
$namespaceRoot = (string) $input->getOption('namespace-root');
Expand Down
171 changes: 74 additions & 97 deletions src/Command/PrivatizeConstantsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,27 @@
namespace Rector\SwissKnife\Command;

use Nette\Utils\FileSystem;
use Nette\Utils\Strings;
use PhpParser\NodeTraverser;
use Rector\SwissKnife\Contract\ClassConstantFetchInterface;
use Rector\SwissKnife\Finder\PhpFilesFinder;
use Rector\SwissKnife\Helpers\ClassNameResolver;
use Rector\SwissKnife\PHPStan\ClassConstantResultAnalyser;
use Rector\SwissKnife\Resolver\StaticClassConstResolver;
use Rector\SwissKnife\ValueObject\ClassConstMatch;
use Rector\SwissKnife\PhpParser\CachedPhpParser;
use Rector\SwissKnife\PhpParser\NodeVisitor\FindClassConstFetchNodeVisitor;
use Rector\SwissKnife\PhpParser\NodeVisitor\FindNonPrivateClassConstNodeVisitor;
use Rector\SwissKnife\ValueObject\ClassConstantFetch\CurrentClassConstantFetch;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Component\Process\Process;

final class PrivatizeConstantsCommand extends Command
{
/**
* @var string
* @see https://regex101.com/r/wkHZwX/1
*/
private const PUBLIC_CONST_REGEX = '#( |\t)(public )?const #ms';

/**
* @var int
*/
private const TIMEOUT_IN_SECONDS = 300;

public function __construct(
private readonly SymfonyStyle $symfonyStyle,
private readonly ClassConstantResultAnalyser $classConstantResultAnalyser,
private readonly StaticClassConstResolver $staticClassConstResolver,
private readonly CachedPhpParser $cachedPhpParser
) {
parent::__construct();
}
Expand Down Expand Up @@ -75,114 +65,101 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return self::SUCCESS;
}

$this->symfonyStyle->success('1. Finding all class constants...');
$this->symfonyStyle->note('1. Finding class const fetches...');
$classConstantFetches = $this->findClassConstantFetches($phpFileInfos);

dump('testing');
die;
$this->symfonyStyle->newLine(2);
$this->symfonyStyle->success(sprintf('Found %d class constant fetches', count($classConstantFetches)));

// special case of self::NAME, that should be protected - their children too
$staticClassConstMatches = $this->staticClassConstResolver->resolve($phpFileInfos);

$phpstanResult = $this->runPHPStanAnalyse($sources);

$publicAndProtectedClassConstants = $this->classConstantResultAnalyser->analyseResult($phpstanResult);
if ($publicAndProtectedClassConstants->isEmpty()) {
$this->symfonyStyle->success('No class constant visibility to change');
return self::SUCCESS;
}

// make public first, to avoid override to protected
foreach ($publicAndProtectedClassConstants->getPublicClassConstMatches() as $publicClassConstMatch) {
$this->replacePrivateConstWith($publicClassConstMatch, 'public const');
// go file by file and deal with public + protected constants
foreach ($phpFileInfos as $phpFileInfo) {
$this->processFileInfo($phpFileInfo, $classConstantFetches);
}

foreach ($publicAndProtectedClassConstants->getProtectedClassConstMatches() as $publicClassConstMatch) {
$this->replacePrivateConstWith($publicClassConstMatch, 'protected const');
}
return self::SUCCESS;
}

$this->replaceClassAndChildWithProtected($phpFileInfos, $staticClassConstMatches);
/**
* @param SplFileInfo[] $phpFileInfos
* @return ClassConstantFetchInterface[]
*/
private function findClassConstantFetches(array $phpFileInfos): array
{
$nodeTraverser = new NodeTraverser();

if ($publicAndProtectedClassConstants->getPublicCount() !== 0) {
$this->symfonyStyle->success(
sprintf('%d constant made public', $publicAndProtectedClassConstants->getPublicCount())
);
}
$findClassConstFetchNodeVisitor = new FindClassConstFetchNodeVisitor();
$nodeTraverser->addVisitor($findClassConstFetchNodeVisitor);

if ($publicAndProtectedClassConstants->getProtectedCount() !== 0) {
$this->symfonyStyle->success(
sprintf('%d constant made protected', $publicAndProtectedClassConstants->getProtectedCount())
);
$progressBar = $this->symfonyStyle->createProgressBar(count($phpFileInfos));
foreach ($phpFileInfos as $phpFileInfo) {
$this->parseAndTraverseFile($phpFileInfo, $nodeTraverser);
$progressBar->advance();
}

if ($staticClassConstMatches !== []) {
$this->symfonyStyle->success(
\sprintf('%d constants made protected for static access', count($staticClassConstMatches))
);
}
$progressBar->finish();

return self::SUCCESS;
return $findClassConstFetchNodeVisitor->getClassConstantFetches();
}

private function replacePrivateConstWith(ClassConstMatch $publicClassConstMatch, string $replaceString): void
private function parseAndTraverseFile(SplFileInfo $phpFileInfo, NodeTraverser $nodeTraverser): void
{
$classFileContents = FileSystem::read($publicClassConstMatch->getClassFileName());

// replace "private const NAME" with "private const NAME"
$classFileContents = str_replace(
'private const ' . $publicClassConstMatch->getConstantName(),
$replaceString . ' ' . $publicClassConstMatch->getConstantName(),
$classFileContents
);

FileSystem::write($publicClassConstMatch->getClassFileName(), $classFileContents);

// @todo handle case when "AppBundle\Rpc\BEItem\BeItemPackage::ITEM_TYPE_NAME_PACKAGE" constant is in parent class
$parentClassConstMatch = $publicClassConstMatch->getParentClassConstMatch();
if (! $parentClassConstMatch instanceof ClassConstMatch) {
return;
}

$this->replacePrivateConstWith($parentClassConstMatch, $replaceString);
$fileStmts = $this->cachedPhpParser->parseFile($phpFileInfo->getRealPath());
$nodeTraverser->traverse($fileStmts);
}

/**
* @param SplFileInfo[] $phpFileInfos
* @param ClassConstMatch[] $staticClassConstsMatches
* @param ClassConstantFetchInterface[] $classConstantFetches
*/
private function replaceClassAndChildWithProtected(array $phpFileInfos, array $staticClassConstsMatches): void
private function processFileInfo(SplFileInfo $phpFileInfo, array $classConstantFetches): void
{
if ($staticClassConstsMatches === []) {
$nodeTraverser = new NodeTraverser();
$findClassConstNodeVisitor = new FindNonPrivateClassConstNodeVisitor();
$nodeTraverser->addVisitor($findClassConstNodeVisitor);

$this->parseAndTraverseFile($phpFileInfo, $nodeTraverser);

// nothing found
if ($findClassConstNodeVisitor->getClassConstants() === []) {
return;
}

foreach ($phpFileInfos as $phpFileInfo) {
$fullyQualifiedClassName = ClassNameResolver::resolveFromFileContents($phpFileInfo->getContents());

if ($fullyQualifiedClassName === null) {
// no class to process
continue;
}
foreach ($findClassConstNodeVisitor->getClassConstants() as $classConstant) {
$isPublic = false;
foreach ($classConstantFetches as $classConstantFetch) {
if (! $classConstantFetch->isClassConstantMatch($classConstant)) {
continue;
}

foreach ($staticClassConstsMatches as $staticClassConstMatch) {
// update current and all hcildren
if (! is_a($fullyQualifiedClassName, $staticClassConstMatch->getClassName(), true)) {
if ($classConstantFetch instanceof CurrentClassConstantFetch) {
continue;
}

$classFileContents = \str_replace(
'private const ' . $staticClassConstMatch->getConstantName(),
'protected const ' . $staticClassConstMatch->getConstantName(),
$phpFileInfo->getContents()
// used externally, make public
$isPublic = true;
}

if ($isPublic) {
$changedFileContents = Strings::replace(
$phpFileInfo->getContents(),
'#(public\s+)?const\s+' . $classConstant->getConstantName() . '#',
'public const ' . $classConstant->getConstantName()
);

$this->symfonyStyle->warning(sprintf(
'The "%s" constant in "%s" made protected to allow static access. Consider refactoring to better design',
$staticClassConstMatch->getConstantName(),
$staticClassConstMatch->getClassName(),
));
FileSystem::write($phpFileInfo->getRealPath(), $changedFileContents);

FileSystem::write($phpFileInfo->getRealPath(), $classFileContents);
$this->symfonyStyle->note(sprintf('Constant %s changed to public', $classConstant->getConstantName()));
continue;
}

// make private
$changedFileContents = Strings::replace(
$phpFileInfo->getContents(),
'#(public\s+)?const\s+' . $classConstant->getConstantName() . '#',
'private const ' . $classConstant->getConstantName()
);
FileSystem::write($phpFileInfo->getRealPath(), $changedFileContents);

$this->symfonyStyle->note(sprintf('Constant %s changed to private', $classConstant->getConstantName()));
}
}
}
16 changes: 16 additions & 0 deletions src/Contract/ClassConstantFetchInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Rector\SwissKnife\Contract;

use Rector\SwissKnife\ValueObject\ClassConstant;

interface ClassConstantFetchInterface
{
public function getClassName(): string;

public function getConstantName(): string;

public function isClassConstantMatch(ClassConstant $classConstant): bool;
}
9 changes: 9 additions & 0 deletions src/Exception/NotImplementedYetException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Rector\SwissKnife\Exception;

final class NotImplementedYetException extends \Exception
{
}
84 changes: 0 additions & 84 deletions src/PHPStan/ClassConstantResultAnalyser.php

This file was deleted.

Loading

0 comments on commit ad016eb

Please sign in to comment.