Skip to content

Commit

Permalink
Add validate command to ensure rule definitions are valid (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomasVotruba authored Aug 8, 2024
1 parent 976e505 commit 7716f1a
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 20 deletions.
85 changes: 85 additions & 0 deletions src/Command/ValidateCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

declare(strict_types=1);

namespace Symplify\RuleDocGenerator\Command;

use ReflectionClass;
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 Symplify\RuleDocGenerator\Contract\DocumentedRuleInterface;
use Symplify\RuleDocGenerator\FileSystem\RuleDefinitionClassesFinder;
use Symplify\RuleDocGenerator\ValueObject\Option;

final class ValidateCommand extends Command
{
public function __construct(
private readonly SymfonyStyle $symfonyStyle,
private readonly RuleDefinitionClassesFinder $ruleDefinitionClassesFinder,
) {
parent::__construct();
}

protected function configure(): void
{
$this->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;
}
}
21 changes: 8 additions & 13 deletions src/DirectoryToMarkdownPrinter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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,
Expand All @@ -31,32 +30,28 @@ 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);

// 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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>
*/
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');
Expand All @@ -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;
}

Expand All @@ -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(
Expand All @@ -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');
}
}

0 comments on commit 7716f1a

Please sign in to comment.