-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add privatize-constants to automate private class const (#29)
* add privatize constants command * misc
- Loading branch information
1 parent
438e082
commit d1a530d
Showing
4 changed files
with
218 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
rules: | ||
- PHPStan\Rules\Classes\ClassConstantRule | ||
|
||
parameters: | ||
customRulesetUsed: true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Rector\SwissKnife\Command; | ||
|
||
use Nette\Utils\FileSystem; | ||
use Nette\Utils\Strings; | ||
use Rector\SwissKnife\Finder\FilesFinder; | ||
use Rector\SwissKnife\ValueObject\ClassConstMatch; | ||
use Symfony\Component\Console\Command\Command; | ||
use Symfony\Component\Console\Input\InputArgument; | ||
use Symfony\Component\Console\Input\InputInterface; | ||
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/VR8VUD/1 | ||
*/ | ||
private const CONSTANT_MESSAGE_REGEX = '#constant (?<constant_name>.*?) of class (?<class_name>[\w\\\\]+)#'; | ||
public function __construct( | ||
private readonly SymfonyStyle $symfonyStyle | ||
) { | ||
parent::__construct(); | ||
} | ||
protected function configure(): void | ||
{ | ||
$this->setName('privatize-constants'); | ||
|
||
$this->addArgument( | ||
'sources', | ||
InputArgument::REQUIRED | InputArgument::IS_ARRAY, | ||
'One or more paths to check' | ||
); | ||
$this->setDescription('Make class constants private if not used outside'); | ||
} | ||
|
||
/** | ||
* @return Command::* | ||
*/ | ||
protected function execute(InputInterface $input, OutputInterface $output): int | ||
{ | ||
$sources = (array) $input->getArgument('sources'); | ||
$phpFileInfos = FilesFinder::findPhpFiles($sources); | ||
|
||
$this->privatizeClassConstants($phpFileInfos); | ||
|
||
$phpstanResult = $this->runPHPStanAnalyse($sources); | ||
|
||
foreach ($phpstanResult['files'] as $detail) { | ||
foreach ($detail['messages'] as $messageError) { | ||
// @todo check non-existing constants on child/parent access as well | ||
|
||
// resolve errorMessage error details | ||
$classConstMatch = $this->resolveClassConstMatch($messageError['errorMessage']); | ||
if (! $classConstMatch instanceof ClassConstMatch) { | ||
continue; | ||
} | ||
|
||
$classFileContents = FileSystem::read($classConstMatch->getClassFileName()); | ||
|
||
// replace "private const NAME" with "public const NAME" | ||
$changedFileContent = str_replace( | ||
'private const ' . $classConstMatch->getConstantName(), | ||
'public const ' . $classConstMatch->getConstantName(), | ||
$classFileContents | ||
); | ||
|
||
if ($changedFileContent === $classFileContents) { | ||
continue; | ||
} | ||
|
||
FileSystem::write($classConstMatch->getClassFileName(), $changedFileContent); | ||
|
||
$this->symfonyStyle->note(sprintf( | ||
'Updated "%s" constant in "%s" file to public as used outside', | ||
$classConstMatch->getConstantName(), | ||
$classConstMatch->getClassFileName() | ||
)); | ||
} | ||
} | ||
|
||
return self::SUCCESS; | ||
} | ||
|
||
/** | ||
* @param SplFileInfo[] $phpFileInfos | ||
*/ | ||
private function privatizeClassConstants(array $phpFileInfos): void | ||
{ | ||
foreach ($phpFileInfos as $phpFileInfo) { | ||
$originalFileContent = $phpFileInfo->getContents(); | ||
|
||
$fileContent = $this->makeClassConstantsPrivate($originalFileContent); | ||
if ($originalFileContent === $fileContent) { | ||
continue; | ||
} | ||
|
||
FileSystem::write($phpFileInfo->getRealPath(), $fileContent); | ||
|
||
$this->symfonyStyle->note( | ||
sprintf('Constants in "%s" file privatized', $phpFileInfo->getRelativePathname()) | ||
); | ||
} | ||
} | ||
|
||
private function makeClassConstantsPrivate(string $fileContents): string | ||
{ | ||
$fileContent = Strings::replace($fileContents, '#^( |\t)const #', '$1private const '); | ||
|
||
return str_replace('public const ', 'private const ', $fileContent); | ||
} | ||
|
||
/** | ||
* @param string[] $paths | ||
* @return array<string, mixed> | ||
*/ | ||
private function runPHPStanAnalyse(array $paths): array | ||
{ | ||
$phpStanAnalyseProcess = new Process([ | ||
'vendor/bin/phpstan', | ||
'analyse', | ||
...$paths, | ||
'--configuration', | ||
__DIR__ . '/../../config/privatize-constants-phpstan-ruleset.neon', | ||
'--error-format', | ||
'json', | ||
]); | ||
$phpStanAnalyseProcess->run(); | ||
|
||
$resultOutput = $phpStanAnalyseProcess->getOutput() ?: $phpStanAnalyseProcess->getErrorOutput(); | ||
return json_decode($resultOutput, true); | ||
} | ||
|
||
private function resolveClassConstMatch(string $errorMessage): ?ClassConstMatch | ||
{ | ||
if (! str_contains($errorMessage, 'Access to private constant')) { | ||
return null; | ||
} | ||
|
||
$match = Strings::match($errorMessage, self::CONSTANT_MESSAGE_REGEX); | ||
|
||
if (! isset($match['constant_name'], $match['class_name'])) { | ||
return null; | ||
} | ||
|
||
/** @var class-string $className */ | ||
$className = (string) $match['class_name']; | ||
|
||
return new ClassConstMatch($className, (string) $match['constant_name']); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Rector\SwissKnife\ValueObject; | ||
|
||
use ReflectionClass; | ||
|
||
final readonly class ClassConstMatch | ||
{ | ||
/** | ||
* @param class-string $className | ||
*/ | ||
public function __construct( | ||
private string $className, | ||
private string $constantName | ||
) { | ||
} | ||
|
||
public function getClassName(): string | ||
{ | ||
return $this->className; | ||
} | ||
|
||
public function getConstantName(): string | ||
{ | ||
return $this->constantName; | ||
} | ||
|
||
public function getClassFileName(): string | ||
{ | ||
$classReflection = new ReflectionClass($this->className); | ||
return (string) $classReflection->getFileName(); | ||
} | ||
} |