diff --git a/src/Command/ValidateCommand.php b/src/Command/ValidateCommand.php new file mode 100644 index 0000000..bb2b96c --- /dev/null +++ b/src/Command/ValidateCommand.php @@ -0,0 +1,85 @@ +setName('validate'); + + $this->setDescription('Make sure all rule definitions are not empty and have at least one code sample'); + + $this->addArgument( + Option::PATHS, + InputArgument::REQUIRED | InputArgument::IS_ARRAY, + 'Path to directory of your project' + ); + + $this->addOption(Option::SKIP_TYPE, null, InputOption::VALUE_REQUIRED, 'Skip specific type in filter'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $paths = (array) $input->getArgument(Option::PATHS); + $input->getOption(Option::SKIP_TYPE); + + // 1. collect documented rules in provided path + $classesByFilePaths = $this->ruleDefinitionClassesFinder->findInDirectories($paths); + + $isValid = true; + + foreach ($classesByFilePaths as $ruleClass) { + $ruleClassReflection = new ReflectionClass($ruleClass); + + $documentedRule = $ruleClassReflection->newInstanceWithoutConstructor(); + /** @var DocumentedRuleInterface $documentedRule */ + $ruleDefinition = $documentedRule->getRuleDefinition(); + + if (strlen($ruleDefinition->getDescription()) < 10) { + $this->symfonyStyle->error(sprintf( + 'Rule definition "%s" of "%s" is too short. Make it at least 10 chars', + $ruleDefinition->getDescription(), + $ruleDefinition->getRuleClass(), + )); + + $isValid = false; + } + + if (count($ruleDefinition->getCodeSamples()) < 1) { + $this->symfonyStyle->error(sprintf( + 'Rule "%s" does not have any code samples. Ad at least one so documentation is clear', + $ruleDefinition->getRuleClass(), + )); + + $isValid = false; + } + } + + if ($isValid) { + return self::FAILURE; + } + + return self::SUCCESS; + } +} diff --git a/src/DirectoryToMarkdownPrinter.php b/src/DirectoryToMarkdownPrinter.php index c76a249..74b7798 100644 --- a/src/DirectoryToMarkdownPrinter.php +++ b/src/DirectoryToMarkdownPrinter.php @@ -5,9 +5,8 @@ namespace Symplify\RuleDocGenerator; use Symfony\Component\Console\Style\SymfonyStyle; -use Symplify\RuleDocGenerator\Contract\DocumentedRuleInterface; use Symplify\RuleDocGenerator\Exception\ShouldNotHappenException; -use Symplify\RuleDocGenerator\FileSystem\ClassByTypeFinder; +use Symplify\RuleDocGenerator\FileSystem\RuleDefinitionClassesFinder; use Symplify\RuleDocGenerator\Printer\RuleDefinitionsPrinter; use Symplify\RuleDocGenerator\ValueObject\RuleClassWithFilePath; @@ -17,7 +16,7 @@ final class DirectoryToMarkdownPrinter { public function __construct( - private readonly ClassByTypeFinder $classByTypeFinder, + private readonly RuleDefinitionClassesFinder $classByTypeFinder, private readonly SymfonyStyle $symfonyStyle, private readonly RuleDefinitionsResolver $ruleDefinitionsResolver, private readonly RuleDefinitionsPrinter $ruleDefinitionsPrinter, @@ -31,24 +30,20 @@ public function __construct( public function print(string $workingDirectory, array $directories, ?int $categorizeLevel, array $skipTypes): string { // 1. collect documented rules in provided path - $documentedRuleClasses = $this->classByTypeFinder->findByType( - $workingDirectory, - $directories, - DocumentedRuleInterface::class - ); + $ruleWithFilePaths = $this->classByTypeFinder->findAndCreateRuleWithFilePaths($directories, $workingDirectory); - $documentedRuleClasses = $this->filterOutSkippedTypes($documentedRuleClasses, $skipTypes); - if ($documentedRuleClasses === []) { + $ruleWithFilePaths = $this->filterOutSkippedTypes($ruleWithFilePaths, $skipTypes); + if ($ruleWithFilePaths === []) { // we need at least some classes throw new ShouldNotHappenException(sprintf('No documented classes found in "%s" directories', implode('","', $directories))); } - $message = sprintf('Found %d documented rule classes', count($documentedRuleClasses)); + $message = sprintf('Found %d documented rule classes', count($ruleWithFilePaths)); $this->symfonyStyle->note($message); $classes = array_map( static fn (RuleClassWithFilePath $ruleClassWithFilePath): string => $ruleClassWithFilePath->getClass(), - $documentedRuleClasses + $ruleWithFilePaths ); $this->symfonyStyle->listing($classes); @@ -56,7 +51,7 @@ public function print(string $workingDirectory, array $directories, ?int $catego // 2. create rule definition collection $this->symfonyStyle->note('Resolving rule definitions'); - $ruleDefinitions = $this->ruleDefinitionsResolver->resolveFromClassNames($documentedRuleClasses); + $ruleDefinitions = $this->ruleDefinitionsResolver->resolveFromClassNames($ruleWithFilePaths); // 3. print rule definitions to markdown lines $this->symfonyStyle->note('Printing rule definitions'); diff --git a/src/FileSystem/ClassByTypeFinder.php b/src/FileSystem/RuleDefinitionClassesFinder.php similarity index 54% rename from src/FileSystem/ClassByTypeFinder.php rename to src/FileSystem/RuleDefinitionClassesFinder.php index 2ba7572..61290a2 100644 --- a/src/FileSystem/ClassByTypeFinder.php +++ b/src/FileSystem/RuleDefinitionClassesFinder.php @@ -6,15 +6,21 @@ use Nette\Loaders\RobotLoader; use ReflectionClass; +use Symplify\RuleDocGenerator\Contract\DocumentedRuleInterface; use Symplify\RuleDocGenerator\ValueObject\RuleClassWithFilePath; -final class ClassByTypeFinder +final class RuleDefinitionClassesFinder { + /** + * @var string + */ + private const DOCUMENTED_RULE_INTERFACE = DocumentedRuleInterface::class; + /** * @param string[] $directories - * @return RuleClassWithFilePath[] + * @return array */ - public function findByType(string $workingDirectory, array $directories, string $type): array + public function findInDirectories(array $directories): array { $robotLoader = new RobotLoader(); $robotLoader->setTempDirectory(sys_get_temp_dir() . '/robot_loader_temp'); @@ -25,9 +31,9 @@ public function findByType(string $workingDirectory, array $directories, string $robotLoader->rebuild(); - $desiredClasses = []; + $classesByFilePath = []; foreach ($robotLoader->getIndexedClasses() as $class => $filePath) { - if (! is_a($class, $type, true)) { + if (! is_a($class, self::DOCUMENTED_RULE_INTERFACE, true)) { continue; } @@ -37,10 +43,26 @@ public function findByType(string $workingDirectory, array $directories, string continue; } - $isDeprecated = str_contains((string) $reflectionClass->getDocComment(), '@deprecated'); + $classesByFilePath[$filePath] = $class; + } + + return $classesByFilePath; + } + + /** + * @param string[] $directories + * @return RuleClassWithFilePath[] + */ + public function findAndCreateRuleWithFilePaths(array $directories, string $workingDirectory): array + { + $classesByFilePath = $this->findInDirectories($directories); + + $desiredClasses = []; + foreach ($classesByFilePath as $filePath => $class) { + $isClassDeprecated = $this->isClassDeprecated($class); $relativeFilePath = PathsHelper::relativeFromDirectory($filePath, $workingDirectory); - $desiredClasses[] = new RuleClassWithFilePath($class, $relativeFilePath, $isDeprecated); + $desiredClasses[] = new RuleClassWithFilePath($class, $relativeFilePath, $isClassDeprecated); } usort( @@ -50,4 +72,11 @@ public function findByType(string $workingDirectory, array $directories, string return $desiredClasses; } + + private function isClassDeprecated(string $class): bool + { + $reflectionClass = new ReflectionClass($class); + + return str_contains((string) $reflectionClass->getDocComment(), '@deprecated'); + } }