diff --git a/src/PawfectPHPCommand.php b/src/PawfectPHPCommand.php index 414d0d0..1315d08 100644 --- a/src/PawfectPHPCommand.php +++ b/src/PawfectPHPCommand.php @@ -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; @@ -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'); } /** @@ -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('[!] 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; } @@ -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); } @@ -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('[!] 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; } @@ -243,16 +251,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int $symfonyStyle->newLine(); $symfonyStyle->writeln(sprintf( - "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) + "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) { @@ -264,8 +272,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $symfonyStyle->writeln("\t" . '- ' . $rule . ':'); foreach ($failures as $failure) { $symfonyStyle->writeln("\t\t" . '- ' . $failure[0] - . ($failure[1] !== null ? ' (line ' . $failure[1] . ')' : '') - . ''); + . ($failure[1] !== null ? ' (line ' . $failure[1] . ')' : '') + . ''); } } } @@ -280,8 +288,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $symfonyStyle->writeln("\t" . '- ' . $rule . ':'); foreach ($exceptions as $exception) { $symfonyStyle->writeln("\t\t" . '- ' . $exception->getMessage() - . ' (' . $exception->getFile() . ':' . $exception->getLine() . ')' - . ''); + . ' (' . $exception->getFile() . ':' . $exception->getLine() . ')' + . ''); } } } @@ -296,8 +304,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $symfonyStyle->writeln("\t" . '- ' . $rule . ':'); foreach ($warnings as $warning) { $symfonyStyle->writeln("\t\t" . '- ' . $warning[0] - . ($warning[1] !== null ? ' (line ' . $warning[1] . ')' : '') - . ''); + . ($warning[1] !== null ? ' (line ' . $warning[1] . ')' : '') + . ''); } } } diff --git a/tests/PawfectPHPCommandTest.php b/tests/PawfectPHPCommandTest.php index bbc13e4..a05d36e 100644 --- a/tests/PawfectPHPCommandTest.php +++ b/tests/PawfectPHPCommandTest.php @@ -47,7 +47,7 @@ class PawfectPHPCommandTest extends TestCase { public function tearDown(): void { - // Mockery::close(); + Mockery::close(); parent::tearDown(); } @@ -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); @@ -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(); @@ -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');