Skip to content

Commit

Permalink
[feat] support skipping rules by name via the --skip flag
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewbreksa committed Jul 7, 2023
1 parent cce1b07 commit 31ca5b6
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 60 deletions.
106 changes: 57 additions & 49 deletions src/PawfectPHPCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ class PawfectPHPCommand extends Command
* @param ContainerInterface $container
*/
public function __construct(
FileLoaderInterface $fileLoader,
RuleRepositoryInterface $ruleRegistry,
ReflectionClassLoaderInterface $reflectionClassLoader,
ContainerInterface $container
FileLoaderInterface $fileLoader,
RuleRepositoryInterface $ruleRegistry,
ReflectionClassLoaderInterface $reflectionClassLoader,
ContainerInterface $container
) {
parent::__construct();
$this->fileLoader = $fileLoader;
Expand All @@ -87,21 +87,22 @@ protected function configure(): void
{
$this->setDescription('Scans application code and runs discovered classes through the provided rules');
$this->addArgument(
'rules',
InputArgument::REQUIRED,
'The directory to inspect for rules'
'rules',
InputArgument::REQUIRED,
'The directory to inspect for rules'
);
$this->addArgument(
'paths',
InputArgument::IS_ARRAY,
'The list of directories and files to scan'
'paths',
InputArgument::IS_ARRAY,
'The list of directories and files to scan'
);
$this->addOption(
'dry-run',
'd',
InputOption::VALUE_NONE,
'If passed, the application will not return with a non-zero exit code if there are any rule failures'
'dry-run',
'd',
InputOption::VALUE_NONE,
'If passed, the application will not return with a non-zero exit code if there are any rule failures'
);
$this->addOption('skip', 's', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Do not run the rule passed');
}

/**
Expand All @@ -118,34 +119,34 @@ protected function execute(InputInterface $input, OutputInterface $output): int
/** @psalm-suppress PossiblyInvalidCast */
foreach ($this->fileLoader->yieldFiles([(string)$input->getArgument('rules')]) as $ruleFile) {
$symfonyStyle->writeln(
'inspecting ' . $ruleFile->getPathname() . ' for rules',
OutputInterface::VERBOSITY_DEBUG
'inspecting ' . $ruleFile->getPathname() . ' for rules',
OutputInterface::VERBOSITY_DEBUG
);
try {
$ruleReflectionClass = $this->reflectionClassLoader->load($ruleFile);
} catch (NoSupportedClassesFoundInFile $noSupportedClassesFoundInFile) {
$symfonyStyle->writeln(
sprintf('[*] no supported classes found in %s: %s', $ruleFile->getPathname(), $noSupportedClassesFoundInFile->getMessage()),
OutputInterface::VERBOSITY_DEBUG
sprintf('[*] no supported classes found in %s: %s', $ruleFile->getPathname(), $noSupportedClassesFoundInFile->getMessage()),
OutputInterface::VERBOSITY_DEBUG
);
continue;
} catch (Throwable $exception) {
$symfonyStyle->writeln('<fg=red>[!] exception inspecting ' . $ruleFile->getPathname() . ', skipping</>');
$symfonyStyle->writeln(
sprintf('[*] exception inspecting %s: %s', $ruleFile->getPathname(), $exception->getMessage()),
OutputInterface::VERBOSITY_DEBUG
sprintf('[*] exception inspecting %s: %s', $ruleFile->getPathname(), $exception->getMessage()),
OutputInterface::VERBOSITY_DEBUG
);
continue;
}
if (!$ruleReflectionClass->implementsInterface(RuleInterface::class) && !$ruleReflectionClass->implementsInterface(AnalysisAwareRule::class)) {
$symfonyStyle->writeln(
sprintf(
'%s does not implement one of %s, %s',
$ruleReflectionClass->getName(),
RuleInterface::class,
AnalysisAwareRule::class
),
OutputInterface::VERBOSITY_DEBUG
sprintf(
'%s does not implement one of %s, %s',
$ruleReflectionClass->getName(),
RuleInterface::class,
AnalysisAwareRule::class
),
OutputInterface::VERBOSITY_DEBUG
);
continue;
}
Expand All @@ -155,9 +156,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int
*/
$ruleInstance = $this->container->get($ruleReflectionClass->getName());
$symfonyStyle->writeln(
'registering ' . $ruleReflectionClass->getName() . ' as a rule',
OutputInterface::VERBOSITY_DEBUG
'registering ' . $ruleReflectionClass->getName() . ' as a rule',
OutputInterface::VERBOSITY_DEBUG
);

$symfonyStyle->writeln(sprintf('checking if %s is in the skipped rules array: %s', $ruleInstance->getName(), implode(',', $input->getOption('skip'))), OutputInterface::VERBOSITY_DEBUG);
if (in_array($ruleInstance->getName(), $input->getOption('skip') ?? [])) {
$symfonyStyle->writeln(sprintf('skipping rule %s as it is in the skipped rules array', $ruleInstance->getName()), OutputInterface::VERBOSITY_DEBUG);
continue;
}

$this->ruleRegistry->register($ruleInstance->getName(), $ruleInstance);
}

Expand All @@ -182,15 +190,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$reflectionClass = $this->reflectionClassLoader->load($classFile);
} catch (NoSupportedClassesFoundInFile $noSupportedClassesFoundInFile) {
$symfonyStyle->writeln(
sprintf('[*] no supported classes found in %s: %s', $classFile->getPathname(), $noSupportedClassesFoundInFile->getMessage()),
OutputInterface::VERBOSITY_DEBUG
sprintf('[*] no supported classes found in %s: %s', $classFile->getPathname(), $noSupportedClassesFoundInFile->getMessage()),
OutputInterface::VERBOSITY_DEBUG
);
continue;
} catch (Throwable $exception) {
$symfonyStyle->writeln('<fg=red>[!] exception inspecting ' . $classFile->getPathname() . ', skipping</>');
$symfonyStyle->writeln(
sprintf('[*] exception inspecting %s: %s', $classFile->getPathname(), $exception->getMessage()),
OutputInterface::VERBOSITY_DEBUG
sprintf('[*] exception inspecting %s: %s', $classFile->getPathname(), $exception->getMessage()),
OutputInterface::VERBOSITY_DEBUG
);
continue;
}
Expand Down Expand Up @@ -243,16 +251,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int

$symfonyStyle->newLine();
$symfonyStyle->writeln(sprintf(
"<fg=blue>Registered Rules: %s, Inspected Files: %s, Scanned Classes: %s, Applied Rules: %s, Passes: %s, Failures: %s, Exceptions: %s, Warnings: %s, Time: %s</>",
$analysis->getRegisteredRules(),
$analysis->getInspectedFiles(),
$analysis->getInspectedClasses(),
count(array_unique($appliedRuleNames)),
$analysis->getPassCount(),
$analysis->getFailCount(),
$analysis->getExceptionCount(),
$analysis->getWarnCount(),
sprintf('%02d:%02d:%02d', (int)($duration / 3600), ((int)($duration / 60) % 60), $duration % 60)
"<fg=blue>Registered Rules: %s, Inspected Files: %s, Scanned Classes: %s, Applied Rules: %s, Passes: %s, Failures: %s, Exceptions: %s, Warnings: %s, Time: %s</>",
$analysis->getRegisteredRules(),
$analysis->getInspectedFiles(),
$analysis->getInspectedClasses(),
count(array_unique($appliedRuleNames)),
$analysis->getPassCount(),
$analysis->getFailCount(),
$analysis->getExceptionCount(),
$analysis->getWarnCount(),
sprintf('%02d:%02d:%02d', (int)($duration / 3600), ((int)($duration / 60) % 60), $duration % 60)
));

if ($analysis->getFailCount() > 0) {
Expand All @@ -264,8 +272,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$symfonyStyle->writeln("\t" . '- ' . $rule . ':');
foreach ($failures as $failure) {
$symfonyStyle->writeln("\t\t" . '<fg=red>- ' . $failure[0]
. ($failure[1] !== null ? ' (line ' . $failure[1] . ')' : '')
. '</>');
. ($failure[1] !== null ? ' (line ' . $failure[1] . ')' : '')
. '</>');
}
}
}
Expand All @@ -280,8 +288,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$symfonyStyle->writeln("\t" . '- ' . $rule . ':');
foreach ($exceptions as $exception) {
$symfonyStyle->writeln("\t\t" . '<fg=red>- ' . $exception->getMessage()
. ' (' . $exception->getFile() . ':' . $exception->getLine() . ')'
. '</>');
. ' (' . $exception->getFile() . ':' . $exception->getLine() . ')'
. '</>');
}
}
}
Expand All @@ -296,8 +304,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$symfonyStyle->writeln("\t" . '- ' . $rule . ':');
foreach ($warnings as $warning) {
$symfonyStyle->writeln("\t\t" . '<fg=yellow>- ' . $warning[0]
. ($warning[1] !== null ? ' (line ' . $warning[1] . ')' : '')
. '</>');
. ($warning[1] !== null ? ' (line ' . $warning[1] . ')' : '')
. '</>');
}
}
}
Expand Down
87 changes: 76 additions & 11 deletions tests/PawfectPHPCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class PawfectPHPCommandTest extends TestCase
{
public function tearDown(): void
{
// Mockery::close();
Mockery::close();
parent::tearDown();
}

Expand Down Expand Up @@ -173,9 +173,9 @@ public function testNoClasses()

$ruleRegistry->expects('count')->andReturns(1);
$testRule = Mockery::mock(RuleInterface::class);
$testRule->expects('getName')->andReturns('test-rule');
$testRule->allows('getName')->andReturns('test-rule');
$ruleRegistry->expects('getAllRules')->andReturn([
'test-rule' => $testRule,
'test-rule' => $testRule,
]);
$testRuleReflectionClass = Mockery::mock(ReflectionClass::class);
$testRuleReflectionClass->expects('implementsInterface')->with(RuleInterface::class)->andReturns(true);
Expand Down Expand Up @@ -205,10 +205,75 @@ public function testNoClasses()

$commandTester = new CommandTester($command);
$commandTester->execute(
[
'rules' => __DIR__ . '/../examples/',
'paths' => [__DIR__ . '/../src'],
]
[
'rules' => __DIR__ . '/../examples/',
'paths' => [__DIR__ . '/../src'],
]
);

$output = $commandTester->getDisplay();
self::assertStringContainsString('all rules pass', $output);
self::assertEquals(0, $commandTester->getStatusCode());
}

public function testRuleSkipped()
{
$fileLoader = Mockery::mock(FileLoaderInterface::class);
$ruleRegistry = Mockery::mock(RuleRepositoryInterface::class);
$reflectionClassLoader = Mockery::mock(ReflectionClassLoaderInterface::class);
$container = Mockery::mock(ContainerInterface::class);

$ruleRegistry->expects('count')->andReturns(1);
$testRule = Mockery::mock(RuleInterface::class);
$testRule->allows('getName')->andReturns('TestRule');
$testRuleNotSkipped = Mockery::mock(AnalysisAwareRule::class);
$testRuleNotSkipped->allows('getName')->andReturns('TestRuleNotSkipped');
$ruleRegistry->expects('getAllRules')->andReturn([
'TestRuleNotSkipped' => $testRuleNotSkipped,
]);
$testRuleReflectionClass = Mockery::mock(ReflectionClass::class);
$testRuleReflectionClass->expects('implementsInterface')->with(RuleInterface::class)->andReturns(true);
$testRuleReflectionClass->allows('getName')->andReturns('TestRule');
$testRuleFile = Mockery::mock(SplFileInfo::class);
$testRuleFile->allows('getPathname')->andReturns('TestRule.php');

$testRuleNoTskippedReflectionClass = Mockery::mock(ReflectionClass::class);
$testRuleNoTskippedReflectionClass->expects('implementsInterface')->with(RuleInterface::class)->andReturns(false);
$testRuleNoTskippedReflectionClass->expects('implementsInterface')->with(AnalysisAwareRule::class)->andReturns(true);
$testRuleNoTskippedReflectionClass->allows('getName')->andReturns('TestRuleNotSkipped');
$testRuleNotSkippedFile = Mockery::mock(SplFileInfo::class);
$testRuleNotSkippedFile->allows('getPathname')->andReturns('TestRuleNotSkipped.php');

$container->expects('get')->with('TestRule')->andReturns($testRule);
$container->expects('get')->with('TestRuleNotSkipped')->andReturns($testRuleNotSkipped);

$reflectionClassLoader->expects('load')->with($testRuleFile)->andReturns($testRuleReflectionClass);
$reflectionClassLoader->expects('load')->with($testRuleNotSkippedFile)->andReturns($testRuleNoTskippedReflectionClass);

$ruleRegistry->expects('register')->with('TestRuleNotSkipped', $testRuleNotSkipped);

$fileLoader->expects('yieldFiles')->with([__DIR__ . '/../examples/'])->andReturns([
$testRuleFile,
$testRuleNotSkippedFile
]);

$fileLoader->expects('yieldFiles')->with([__DIR__ . '/../src'])->andReturns([]);

$command = new PawfectPHPCommand(
$fileLoader,
$ruleRegistry,
$reflectionClassLoader,
$container
);


$commandTester = new CommandTester($command);
$commandTester->execute(
[
'rules' => __DIR__ . '/../examples/',
'paths' => [__DIR__ . '/../src'],
'--skip' => ['TestRule']
]
);

$output = $commandTester->getDisplay();
Expand All @@ -218,14 +283,14 @@ public function testNoClasses()

public function testNoRulesForClass()
{
$fileLoader = Mockery::mock(FileLoaderInterface::class);
$ruleRegistry = Mockery::mock(RuleRepositoryInterface::class);
$fileLoader = Mockery::mock(FileLoaderInterface::class);
$ruleRegistry = Mockery::mock(RuleRepositoryInterface::class);
$reflectionClassLoader = Mockery::mock(ReflectionClassLoaderInterface::class);
$container = Mockery::mock(ContainerInterface::class);
$container = Mockery::mock(ContainerInterface::class);

$ruleRegistry->expects('count')->andReturns(1);
$testRule = Mockery::mock(RuleInterface::class);
$testRule->expects('getName')->andReturns('test-rule');
$testRule->allows('getName')->andReturns('test-rule');
$testRuleReflectionClass = Mockery::mock(ReflectionClass::class);
$testRuleReflectionClass->expects('implementsInterface')->with(RuleInterface::class)->andReturns(true);
$testRuleReflectionClass->allows('getName')->andReturns('TestRule');
Expand Down

0 comments on commit 31ca5b6

Please sign in to comment.