From 66f8a2f4bf57f864509c36abbf8eab2200440bd6 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Thu, 5 Sep 2024 17:48:56 +0200 Subject: [PATCH 1/3] add timeout in seconds --- composer.json | 2 +- src/Command/PrivatizeConstantsCommand.php | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 2ebb26e0a..4bad74c2e 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": ">=8.2", - "illuminate/container": "^11.0", + "illuminate/container": "^11.20", "nette/robot-loader": "^4.0", "nette/utils": "^4.0", "nikic/php-parser": "^4.19", diff --git a/src/Command/PrivatizeConstantsCommand.php b/src/Command/PrivatizeConstantsCommand.php index 1e84aacfb..5cc6fc26d 100644 --- a/src/Command/PrivatizeConstantsCommand.php +++ b/src/Command/PrivatizeConstantsCommand.php @@ -28,6 +28,11 @@ final class PrivatizeConstantsCommand extends Command */ 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, @@ -153,7 +158,7 @@ private function runPHPStanAnalyse(array $paths): array { $this->symfonyStyle->note('Running PHPStan to spot false-private class constants'); - $phpStanAnalyseProcess = new Process([ + $commandOptions = [ 'vendor/bin/phpstan', 'analyse', ...$paths, @@ -161,7 +166,9 @@ private function runPHPStanAnalyse(array $paths): array __DIR__ . '/../../config/privatize-constants-phpstan-ruleset.neon', '--error-format', 'json', - ]); + ]; + + $phpStanAnalyseProcess = new Process($commandOptions, null, null, null, self::TIMEOUT_IN_SECONDS); $phpStanAnalyseProcess->run(); $this->symfonyStyle->success('PHPStan analysis finished'); From 5d5bc5dc43fb9f05f60a5e9ec6b0ce8d409a1013 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Thu, 5 Sep 2024 17:57:53 +0200 Subject: [PATCH 2/3] [config] Move from privatize-constants --- README.md | 2 +- .../privatize-constants-phpstan-ruleset.neon | 5 -- src/Command/PrivatizeConstantsCommand.php | 67 ++----------------- 3 files changed, 5 insertions(+), 69 deletions(-) delete mode 100644 config/privatize-constants-phpstan-ruleset.neon diff --git a/README.md b/README.md index 55985b8f2..42ad9e5fd 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ PHPStan can report unused private class constants, but it skips all the public o 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 +vendor/bin/swiss-knife privatize-constants src test ``` This command will: diff --git a/config/privatize-constants-phpstan-ruleset.neon b/config/privatize-constants-phpstan-ruleset.neon deleted file mode 100644 index 66d258086..000000000 --- a/config/privatize-constants-phpstan-ruleset.neon +++ /dev/null @@ -1,5 +0,0 @@ -rules: - - PHPStan\Rules\Classes\ClassConstantRule - -parameters: - customRulesetUsed: true diff --git a/src/Command/PrivatizeConstantsCommand.php b/src/Command/PrivatizeConstantsCommand.php index 5cc6fc26d..65d546ef9 100644 --- a/src/Command/PrivatizeConstantsCommand.php +++ b/src/Command/PrivatizeConstantsCommand.php @@ -5,7 +5,6 @@ namespace Rector\SwissKnife\Command; use Nette\Utils\FileSystem; -use Nette\Utils\Strings; use Rector\SwissKnife\Finder\PhpFilesFinder; use Rector\SwissKnife\Helpers\ClassNameResolver; use Rector\SwissKnife\PHPStan\ClassConstantResultAnalyser; @@ -76,7 +75,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int return self::SUCCESS; } - $this->privatizeClassConstants($phpFileInfos); + $this->symfonyStyle->success('1. Finding all class constants...'); + + dump('testing'); + die; // special case of self::NAME, that should be protected - their children too $staticClassConstMatches = $this->staticClassConstResolver->resolve($phpFileInfos); @@ -121,67 +123,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int return self::SUCCESS; } - /** - * @param SplFileInfo[] $phpFileInfos - */ - private function privatizeClassConstants(array $phpFileInfos): void - { - $this->symfonyStyle->note(sprintf('Found %d PHP files, turning constants to private', count($phpFileInfos))); - - $privatizedFileCount = 0; - - foreach ($phpFileInfos as $phpFileInfo) { - $originalFileContent = $phpFileInfo->getContents(); - - $fileContent = $this->makeClassConstantsPrivate($originalFileContent); - if ($originalFileContent === $fileContent) { - continue; - } - - FileSystem::write($phpFileInfo->getRealPath(), $fileContent); - ++$privatizedFileCount; - } - - $this->symfonyStyle->success(sprintf('Constants in %d files turned to private', $privatizedFileCount)); - } - - private function makeClassConstantsPrivate(string $fileContents): string - { - return Strings::replace($fileContents, self::PUBLIC_CONST_REGEX, '$1private const '); - } - - /** - * @param string[] $paths - * @return array - */ - private function runPHPStanAnalyse(array $paths): array - { - $this->symfonyStyle->note('Running PHPStan to spot false-private class constants'); - - $commandOptions = [ - 'vendor/bin/phpstan', - 'analyse', - ...$paths, - '--configuration', - __DIR__ . '/../../config/privatize-constants-phpstan-ruleset.neon', - '--error-format', - 'json', - ]; - - $phpStanAnalyseProcess = new Process($commandOptions, null, null, null, self::TIMEOUT_IN_SECONDS); - $phpStanAnalyseProcess->run(); - - $this->symfonyStyle->success('PHPStan analysis finished'); - - // process output message - sleep(1); - - $this->symfonyStyle->newLine(); - - $resultOutput = $phpStanAnalyseProcess->getOutput() ?: $phpStanAnalyseProcess->getErrorOutput(); - return json_decode($resultOutput, true); - } - private function replacePrivateConstWith(ClassConstMatch $publicClassConstMatch, string $replaceString): void { $classFileContents = FileSystem::read($publicClassConstMatch->getClassFileName()); From 61dc30b8faff46817356d6e7f592e52ea298cc58 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Thu, 5 Sep 2024 18:03:14 +0200 Subject: [PATCH 3/3] refactor to php-parser approach without PHPStan, to make direct and more robuts --- README.md | 6 +- composer.json | 1 - phpstan.neon | 4 +- src/Command/NamespaceToPSR4Command.php | 5 +- src/Command/PrivatizeConstantsCommand.php | 171 ++++++++---------- src/Contract/ClassConstantFetchInterface.php | 16 ++ src/Exception/NotImplementedYetException.php | 11 ++ src/Exception/ShouldNotHappenException.php | 11 ++ src/Helpers/ClassNameResolver.php | 43 ----- src/PHPStan/ClassConstantResultAnalyser.php | 84 --------- .../FindClassConstFetchNodeVisitor.php | 157 ++++++++++++++++ .../FindNonPrivateClassConstNodeVisitor.php | 58 ++++++ src/Resolver/StaticClassConstResolver.php | 46 ----- src/ValueObject/ClassConstMatch.php | 61 ------- src/ValueObject/ClassConstant.php | 37 ++++ .../AbstractClassConstantFetch.php | 36 ++++ .../CurrentClassConstantFetch.php | 9 + .../ExternalClassAccessConstantFetch.php | 9 + .../ParentClassConstantFetch.php | 9 + .../PublicAndProtectedClassConstants.php | 49 ----- tests/Helpers/ClassNameResolverTest.php | 21 --- .../ClassConstantResultAnalyserTest.php | 47 ----- .../Fixture/FileWithStaticConstCalls.php | 15 -- .../StaticClassConstResolverTest.php | 36 ---- 24 files changed, 436 insertions(+), 506 deletions(-) create mode 100644 src/Contract/ClassConstantFetchInterface.php create mode 100644 src/Exception/NotImplementedYetException.php create mode 100644 src/Exception/ShouldNotHappenException.php delete mode 100644 src/Helpers/ClassNameResolver.php delete mode 100644 src/PHPStan/ClassConstantResultAnalyser.php create mode 100644 src/PhpParser/NodeVisitor/FindClassConstFetchNodeVisitor.php create mode 100644 src/PhpParser/NodeVisitor/FindNonPrivateClassConstNodeVisitor.php delete mode 100644 src/Resolver/StaticClassConstResolver.php delete mode 100644 src/ValueObject/ClassConstMatch.php create mode 100644 src/ValueObject/ClassConstant.php create mode 100644 src/ValueObject/ClassConstantFetch/AbstractClassConstantFetch.php create mode 100644 src/ValueObject/ClassConstantFetch/CurrentClassConstantFetch.php create mode 100644 src/ValueObject/ClassConstantFetch/ExternalClassAccessConstantFetch.php create mode 100644 src/ValueObject/ClassConstantFetch/ParentClassConstantFetch.php delete mode 100644 src/ValueObject/PublicAndProtectedClassConstants.php delete mode 100644 tests/Helpers/ClassNameResolverTest.php delete mode 100644 tests/PHPStan/ClassConstantResultAnalyserTest.php delete mode 100644 tests/Resolver/StaticClassConstResolver/Fixture/FileWithStaticConstCalls.php delete mode 100644 tests/Resolver/StaticClassConstResolver/StaticClassConstResolverTest.php diff --git a/README.md b/README.md index 42ad9e5fd..3789f9198 100644 --- a/README.md +++ b/README.md @@ -113,9 +113,9 @@ vendor/bin/swiss-knife privatize-constants src test 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` +* find all class constant usages +* scans classes and constants +* makes those constant used locally `private` That way all the constants not used outside will be made `private` safely. diff --git a/composer.json b/composer.json index 4bad74c2e..8aed49020 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,6 @@ "nette/utils": "^4.0", "nikic/php-parser": "^4.19", "symfony/console": "^6.4", - "symfony/process": "^6.4", "symfony/finder": "^6.4", "webmozart/assert": "^1.11" }, diff --git a/phpstan.neon b/phpstan.neon index 386e2e072..aa5d9fd58 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -16,5 +16,5 @@ parameters: constant: 99 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#' diff --git a/src/Command/NamespaceToPSR4Command.php b/src/Command/NamespaceToPSR4Command.php index 67aa0e5db..12bd7bb5c 100644 --- a/src/Command/NamespaceToPSR4Command.php +++ b/src/Command/NamespaceToPSR4Command.php @@ -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'); diff --git a/src/Command/PrivatizeConstantsCommand.php b/src/Command/PrivatizeConstantsCommand.php index 65d546ef9..b824eef3e 100644 --- a/src/Command/PrivatizeConstantsCommand.php +++ b/src/Command/PrivatizeConstantsCommand.php @@ -5,11 +5,14 @@ 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; @@ -17,25 +20,12 @@ 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(); } @@ -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(); + $findNonPrivateClassConstNodeVisitor = new FindNonPrivateClassConstNodeVisitor(); + $nodeTraverser->addVisitor($findNonPrivateClassConstNodeVisitor); + + $this->parseAndTraverseFile($phpFileInfo, $nodeTraverser); + + // nothing found + if ($findNonPrivateClassConstNodeVisitor->getClassConstants() === []) { return; } - foreach ($phpFileInfos as $phpFileInfo) { - $fullyQualifiedClassName = ClassNameResolver::resolveFromFileContents($phpFileInfo->getContents()); - - if ($fullyQualifiedClassName === null) { - // no class to process - continue; - } + foreach ($findNonPrivateClassConstNodeVisitor->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())); } } } diff --git a/src/Contract/ClassConstantFetchInterface.php b/src/Contract/ClassConstantFetchInterface.php new file mode 100644 index 000000000..f3d8c770c --- /dev/null +++ b/src/Contract/ClassConstantFetchInterface.php @@ -0,0 +1,16 @@ +[\w\\\\]+);#'; - - /** - * @var string - * @see https://regex101.com/r/B7LvXE/1 - */ - private const SHORT_CLASS_NAME_REGEX = '#\bclass\s+(?[A-Z][A-Za-z]+)#'; - - /** - * @return class-string|null - */ - public static function resolveFromFileContents(string $fileContents): ?string - { - // @todo use php-parser to make more reliable? - - $namespaceMatch = Strings::match($fileContents, self::NAMESPACE_REGEX); - $classMatch = Strings::match($fileContents, self::SHORT_CLASS_NAME_REGEX); - - // short class must exist - if (! isset($classMatch['short_class_name'])) { - return null; - } - - return ($namespaceMatch['namespace'] ?? '') . '\\' . $classMatch['short_class_name']; - } -} diff --git a/src/PHPStan/ClassConstantResultAnalyser.php b/src/PHPStan/ClassConstantResultAnalyser.php deleted file mode 100644 index 23c6bcda0..000000000 --- a/src/PHPStan/ClassConstantResultAnalyser.php +++ /dev/null @@ -1,84 +0,0 @@ -.*?) of class (?[\w\\\\]+)#'; - - /** - * @var string - * @see https://regex101.com/r/V1QOPN/1 - */ - private const PROTECTED_CONSTANT_MESSAGE_REGEX = '#Access to undefined constant (?[\w\\\\]+)::(?[\w\_]+)#'; - - /** - * @param mixed[] $phpstanResult - */ - public function analyseResult(array $phpstanResult): PublicAndProtectedClassConstants - { - $publicClassConstMatches = []; - $protectedClassConstMatches = []; - - Assert::keyExists($phpstanResult, 'files'); - - foreach ($phpstanResult['files'] as $fileDetail) { - Assert::keyExists($fileDetail, 'messages'); - - foreach ($fileDetail['messages'] as $messageError) { - Assert::keyExists($messageError, 'message'); - - $publicClassConstMatch = $this->matchPublicClassConstMatch($messageError['message']); - if ($publicClassConstMatch instanceof ClassConstMatch) { - $publicClassConstMatches[] = $publicClassConstMatch; - } - - $protectedClassConstMatch = $this->matchProtectedClassConstMatch($messageError['message']); - if ($protectedClassConstMatch instanceof ClassConstMatch) { - $protectedClassConstMatches[] = $protectedClassConstMatch; - } - } - } - - $publicClassConstMatches = array_unique($publicClassConstMatches); - $protectedClassConstMatches = array_unique($protectedClassConstMatches); - - return new PublicAndProtectedClassConstants($publicClassConstMatches, $protectedClassConstMatches); - } - - private function matchProtectedClassConstMatch(string $errorMessage): ?ClassConstMatch - { - return $this->matchClassConstMatchWithRegex($errorMessage, self::PROTECTED_CONSTANT_MESSAGE_REGEX); - } - - private function matchPublicClassConstMatch(string $errorMessage): ?ClassConstMatch - { - return $this->matchClassConstMatchWithRegex($errorMessage, self::PRIVATE_CONSTANT_MESSAGE_REGEX); - } - - private function matchClassConstMatchWithRegex(string $errorMessage, string $regex): ?ClassConstMatch - { - $match = Strings::match($errorMessage, $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']); - } -} diff --git a/src/PhpParser/NodeVisitor/FindClassConstFetchNodeVisitor.php b/src/PhpParser/NodeVisitor/FindClassConstFetchNodeVisitor.php new file mode 100644 index 000000000..039972760 --- /dev/null +++ b/src/PhpParser/NodeVisitor/FindClassConstFetchNodeVisitor.php @@ -0,0 +1,157 @@ +isAnonymous()) { + return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; + } + + $this->currentClass = $node; + return null; + } + + if (! $node instanceof ClassConstFetch) { + return null; + } + + // unable to resolve → skip + if (! $node->class instanceof Name) { + return null; + } + + $className = $node->class->toString(); + + if ($node->name instanceof Expr) { + // unable to resolve → skip + return null; + } + + $constantName = $node->name->toString(); + + // always public magic + if ($constantName === 'class') { + return null; + } + if ($className === 'self') { + $currentClassName = $this->getClassName(); + Assert::isInstanceOf($this->currentClass, Class_::class); + if ($this->isCurrentClassConstant($this->currentClass, $constantName)) { + $this->classConstantFetches[] = new CurrentClassConstantFetch($currentClassName, $constantName); + return $node; + } + // check if parent class is vendor + if ($this->currentClass->extends instanceof Name) { + $parentClassName = $this->currentClass->extends->toString(); + if ($this->isVendorClassName($parentClassName)) { + return null; + } + } + $this->classConstantFetches[] = new ParentClassConstantFetch($currentClassName, $constantName); + return $node; + } + + if ($className === 'static') { + throw new NotImplementedYetException('@todo'); + } + + if (class_exists($className) || interface_exists($className)) { + // is class from /vendor? we can skip it + if ($this->isVendorClassName($className)) { + return null; + } + + // is vendor fetch? skip + $this->classConstantFetches[] = new ExternalClassAccessConstantFetch($className, $constantName); + return null; + } + + throw new NotImplementedYetException(); + } + + public function leaveNode(Node $node): ?Node + { + if (! $node instanceof Class_) { + return null; + } + + // we've left class, lets reset its value + $this->currentClass = null; + return $node; + } + + /** + * @return ClassConstantFetchInterface[] + */ + public function getClassConstantFetches(): array + { + return $this->classConstantFetches; + } + + private function isVendorClassName(string $className): bool + { + if (! class_exists($className) && ! interface_exists($className) && ! trait_exists($className)) { + throw new ShouldNotHappenException(); + } + + $reflectionClass = new ReflectionClass($className); + return str_contains((string) $reflectionClass->getFileName(), 'vendor'); + } + + private function isCurrentClassConstant(Class_ $currentClass, string $constantName): bool + { + foreach ($currentClass->getConstants() as $classConstant) { + foreach ($classConstant->consts as $const) { + if ($const->name->toString() === $constantName) { + return true; + } + } + } + + return false; + } + + private function getClassName(): string + { + if (! $this->currentClass instanceof Class_) { + throw new ShouldNotHappenException(); + } + + $namespaceName = $this->currentClass->namespacedName; + if (! $namespaceName instanceof Name) { + throw new ShouldNotHappenException(); + } + + return $namespaceName->toString(); + } +} diff --git a/src/PhpParser/NodeVisitor/FindNonPrivateClassConstNodeVisitor.php b/src/PhpParser/NodeVisitor/FindNonPrivateClassConstNodeVisitor.php new file mode 100644 index 000000000..1c12e882e --- /dev/null +++ b/src/PhpParser/NodeVisitor/FindNonPrivateClassConstNodeVisitor.php @@ -0,0 +1,58 @@ +isAnonymous()) { + return null; + } + + Assert::isInstanceOf($node->namespacedName, Name::class); + + $className = $node->namespacedName->toString(); + + foreach ($node->getConstants() as $classConst) { + foreach ($classConst->consts as $constConst) { + $constantName = $constConst->name->toString(); + + // not interested in private constants + if ($classConst->isPrivate()) { + continue; + } + + $this->classConstants[] = new ClassConstant($className, $constantName, $classConst->getLine()); + } + } + + return $node; + } + + /** + * @return ClassConstant[] + */ + public function getClassConstants(): array + { + return $this->classConstants; + } +} diff --git a/src/Resolver/StaticClassConstResolver.php b/src/Resolver/StaticClassConstResolver.php deleted file mode 100644 index 597f383c8..000000000 --- a/src/Resolver/StaticClassConstResolver.php +++ /dev/null @@ -1,46 +0,0 @@ -[A-Z\_]+)#ms'; - - /** - * @param SplFileInfo[] $phpFileInfos - * @return ClassConstMatch[] - */ - public function resolve(array $phpFileInfos): array - { - $staticConstMatches = []; - foreach ($phpFileInfos as $phpFileInfo) { - $matches = Strings::matchAll($phpFileInfo->getContents(), self::STATIC_CONST_CALL_REGEX); - foreach ($matches as $match) { - $fullyQualifiedClassName = ClassNameResolver::resolveFromFileContents($phpFileInfo->getContents()); - if ($fullyQualifiedClassName === null) { - continue; - } - - $staticConstMatches[] = new ClassConstMatch($fullyQualifiedClassName, $match['constant_name']); - } - } - - return $staticConstMatches; - } -} diff --git a/src/ValueObject/ClassConstMatch.php b/src/ValueObject/ClassConstMatch.php deleted file mode 100644 index c287bb55e..000000000 --- a/src/ValueObject/ClassConstMatch.php +++ /dev/null @@ -1,61 +0,0 @@ -className . '_' . $this->constantName; - } - - public function getClassName(): string - { - return $this->className; - } - - public function getConstantName(): string - { - return $this->constantName; - } - - /** - * We have to use class const file path, - * as error file path only report use, does not contain the constant - */ - public function getClassFileName(): string - { - $reflectionClass = new ReflectionClass($this->className); - return (string) $reflectionClass->getFileName(); - } - - public function getParentClassConstMatch(): ?self - { - $reflectionClass = new ReflectionClass($this->className); - if ($reflectionClass->getParentClass() === false) { - return null; - } - - return new self($reflectionClass->getParentClass()->getName(), $this->constantName); - } -} diff --git a/src/ValueObject/ClassConstant.php b/src/ValueObject/ClassConstant.php new file mode 100644 index 000000000..64a59ed9b --- /dev/null +++ b/src/ValueObject/ClassConstant.php @@ -0,0 +1,37 @@ +className; + } + + public function getConstantName(): string + { + return $this->constantName; + } + + public function getLine(): int + { + return $this->line; + } +} diff --git a/src/ValueObject/ClassConstantFetch/AbstractClassConstantFetch.php b/src/ValueObject/ClassConstantFetch/AbstractClassConstantFetch.php new file mode 100644 index 000000000..ddeb50349 --- /dev/null +++ b/src/ValueObject/ClassConstantFetch/AbstractClassConstantFetch.php @@ -0,0 +1,36 @@ +className; + } + + public function getConstantName(): string + { + return $this->constantName; + } + + public function isClassConstantMatch(ClassConstant $classConstant): bool + { + if ($classConstant->getClassName() !== $this->className) { + return false; + } + + return $classConstant->getConstantName() === $this->constantName; + } +} diff --git a/src/ValueObject/ClassConstantFetch/CurrentClassConstantFetch.php b/src/ValueObject/ClassConstantFetch/CurrentClassConstantFetch.php new file mode 100644 index 000000000..1d8b58eab --- /dev/null +++ b/src/ValueObject/ClassConstantFetch/CurrentClassConstantFetch.php @@ -0,0 +1,9 @@ +publicClassConstMatches; - } - - /** - * @return ClassConstMatch[] - */ - public function getProtectedClassConstMatches(): array - { - return $this->protectedClassConstMatches; - } - - public function getProtectedCount(): int - { - return count($this->protectedClassConstMatches); - } - - public function getPublicCount(): int - { - return count($this->publicClassConstMatches); - } - - public function isEmpty(): bool - { - return $this->publicClassConstMatches === [] && $this->protectedClassConstMatches === []; - } -} diff --git a/tests/Helpers/ClassNameResolverTest.php b/tests/Helpers/ClassNameResolverTest.php deleted file mode 100644 index 10b2e2975..000000000 --- a/tests/Helpers/ClassNameResolverTest.php +++ /dev/null @@ -1,21 +0,0 @@ -assertSame(SomeClass::class, $resolvedFullyQualifiedName); - } -} diff --git a/tests/PHPStan/ClassConstantResultAnalyserTest.php b/tests/PHPStan/ClassConstantResultAnalyserTest.php deleted file mode 100644 index 839a7912b..000000000 --- a/tests/PHPStan/ClassConstantResultAnalyserTest.php +++ /dev/null @@ -1,47 +0,0 @@ -classConstantResultAnalyser = new ClassConstantResultAnalyser(); - } - - public function test(): void - { - $phpstanResult = [ - 'files' => [ - 'some_file_path.php' => [ - 'messages' => [ - [ - 'line' => 10, - 'message' => 'Access to undefined constant App\\SomeClass::SOME_CONSTANT', - ], - ], - ], - ], - ]; - - $publicAndProtectedClassConstants = $this->classConstantResultAnalyser->analyseResult($phpstanResult); - - $this->assertFalse($publicAndProtectedClassConstants->isEmpty()); - $this->assertSame(0, $publicAndProtectedClassConstants->getPublicCount()); - $this->assertSame(1, $publicAndProtectedClassConstants->getProtectedCount()); - - $onlyProtectedClassConstMatch = $publicAndProtectedClassConstants->getProtectedClassConstMatches()[0]; - $this->assertInstanceOf(ClassConstMatch::class, $onlyProtectedClassConstMatch); - - // $this->assertSame('SOME_CONST', $onlyProtectedClassConstMatch->getConstantName()); - $this->assertSame('App\SomeClass', $onlyProtectedClassConstMatch->getClassName()); - } -} diff --git a/tests/Resolver/StaticClassConstResolver/Fixture/FileWithStaticConstCalls.php b/tests/Resolver/StaticClassConstResolver/Fixture/FileWithStaticConstCalls.php deleted file mode 100644 index 75d7b4eff..000000000 --- a/tests/Resolver/StaticClassConstResolver/Fixture/FileWithStaticConstCalls.php +++ /dev/null @@ -1,15 +0,0 @@ -staticClassConstResolver = new StaticClassConstResolver(); - } - - public function test(): void - { - $splFileInfo = new SplFileInfo(__DIR__ . '/Fixture/FileWithStaticConstCalls.php', '', ''); - - $classConstMatches = $this->staticClassConstResolver->resolve([$splFileInfo]); - $this->assertCount(2, $classConstMatches); - - $firstClassConstMatch = $classConstMatches[0]; - $this->assertSame(FileWithStaticConstCalls::class, $firstClassConstMatch->getClassName()); - $this->assertSame('ITEM_NAME', $firstClassConstMatch->getConstantName()); - - $secondClassConstMatch = $classConstMatches[1]; - $this->assertSame(FileWithStaticConstCalls::class, $secondClassConstMatch->getClassName()); - $this->assertSame('ITEM_PRICE', $secondClassConstMatch->getConstantName()); - } -}