Skip to content

Commit

Permalink
Add privatize-constants to automate private class const (#29)
Browse files Browse the repository at this point in the history
* add privatize constants command

* misc
  • Loading branch information
TomasVotruba committed May 24, 2024
1 parent 438e082 commit d1a530d
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 0 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,23 @@ This will keep mocked classes non-final, so PHPUnit can extend them internally.
<br>
### 5. Privatize local class constants
PHPStan can report unused private clsas constants, but it skips all the public ones.
Do you have lots of class constants, all of them public but want to narrow scope to privates?
```bash
vendor/bin/swiss-knife privatize-constants src
```
This command will:
* make all constants private
* runs PHPStan to find out, which of them are used
* restores only the used constants back to `public`
That way all the constants not used outside will be made `private` safely.
<br>
Happy coding!
5 changes: 5 additions & 0 deletions config/privatize-constants-phpstan-ruleset.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
rules:
- PHPStan\Rules\Classes\ClassConstantRule

parameters:
customRulesetUsed: true
159 changes: 159 additions & 0 deletions src/Command/PrivatizeConstantsCommand.php
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']);
}
}
35 changes: 35 additions & 0 deletions src/ValueObject/ClassConstMatch.php
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();
}
}

0 comments on commit d1a530d

Please sign in to comment.