From add2ed3fd5c5481998136cfafd352be1e540a6ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Sat, 23 May 2026 17:11:00 +0200 Subject: [PATCH 1/4] Implement configuration file management and initialization commands - Added `InitCommand` to create a default `cca.yaml` configuration file with options for silent and interactive modes. - Introduced `ConfigFileResolver` to handle automatic discovery of configuration files in the working directory. - Updated `ChurnCommandContext` and `CognitiveMetricsCommandContext` to utilize the new configuration file management. - Enhanced documentation in `README.md` and `Configuration.md` to reflect the new configuration setup process. - Added unit tests for the new configuration functionalities and command behaviors. --- docs/Configuration.md | 14 ++ readme.md | 11 + src/Application.php | 26 ++- src/Command/ChurnCommand.php | 6 +- .../ChurnCommandContext.php | 6 +- src/Command/CognitiveMetricsCommand.php | 6 +- .../CognitiveMetricsCommandContext.php | 6 +- src/Command/InitCommand.php | 210 ++++++++++++++++++ src/Config/ConfigFileResolver.php | 26 +++ src/Config/ConfigInitializer.php | 50 +++++ .../Command/ChurnSpecificationPatternTest.php | 27 ++- ...gnitiveMetricsSpecificationPatternTest.php | 41 ++-- .../Unit/Command/ConfigAutoDiscoveryTest.php | 187 ++++++++++++++++ tests/Unit/Command/InitCommandTest.php | 192 ++++++++++++++++ tests/Unit/Config/ConfigFileResolverTest.php | 81 +++++++ tests/Unit/Config/ConfigInitializerTest.php | 107 +++++++++ 16 files changed, 966 insertions(+), 30 deletions(-) create mode 100644 src/Command/InitCommand.php create mode 100644 src/Config/ConfigFileResolver.php create mode 100644 src/Config/ConfigInitializer.php create mode 100644 tests/Unit/Command/ConfigAutoDiscoveryTest.php create mode 100644 tests/Unit/Command/InitCommandTest.php create mode 100644 tests/Unit/Config/ConfigFileResolverTest.php create mode 100644 tests/Unit/Config/ConfigInitializerTest.php diff --git a/docs/Configuration.md b/docs/Configuration.md index 3654cf8..5bffc01 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -1,5 +1,17 @@ # Configuration +## Creating a configuration file + +Run `init` to create a `cca.yaml` in your project: + +```bash +bin/phpcca init +bin/phpcca init --silent +bin/phpcca init --path=/path/to/cca.yaml +``` + +When `cca.yaml` exists in the working directory, `analyse` and `churn` pick it up automatically. You only need `--config` for a custom path or filename. + ## Passing a configuration file You can specify another configuration file by passing it to the config options: @@ -8,6 +20,8 @@ You can specify another configuration file by passing it to the config options: bin/phpcca analyse --config= ``` +When both `cca.yaml` and `--config` are available, the explicit `--config` path takes precedence. + ## Excluding Classes and Methods You can exclude classes and methods via a regex in the configuration. diff --git a/readme.md b/readme.md index 4575936..7ee359f 100644 --- a/readme.md +++ b/readme.md @@ -34,10 +34,21 @@ composer require --dev phauthentic/cognitive-code-analysis ## Running it 🧑‍💻 +Create a project configuration file: + +```bash +bin/phpcca init # interactive setup → creates cca.yaml in current directory +bin/phpcca init --silent # non-interactive, all defaults +bin/phpcca init --path=/path/to/cca.yaml +``` + +When `cca.yaml` exists in the current working directory, `analyse` and `churn` load it automatically. Use `--config` to specify a different file. + Cognitive Complexity Analysis ```bash bin/phpcca analyse +bin/phpcca analyse --config=custom.yaml # explicit config overrides auto-discovery ``` Generate a report, supported types are `json`, `csv`, `html`, `markdown`, `checkstyle`, `junit`, `sarif`, `gitlab-codequality`, `github-actions`. diff --git a/src/Application.php b/src/Application.php index 3e0ca29..0f543b5 100644 --- a/src/Application.php +++ b/src/Application.php @@ -28,6 +28,7 @@ use Phauthentic\CognitiveCodeAnalysis\Command\CognitiveMetricsCommand; use Phauthentic\CognitiveCodeAnalysis\Command\CognitiveMetricsSpecifications\CognitiveMetricsValidationSpecificationFactory; use Phauthentic\CognitiveCodeAnalysis\Command\CognitiveMetricsSpecifications\CompositeCognitiveMetricsValidationSpecification; +use Phauthentic\CognitiveCodeAnalysis\Command\InitCommand; use Phauthentic\CognitiveCodeAnalysis\Command\EventHandler\ParserErrorHandler; use Phauthentic\CognitiveCodeAnalysis\Command\EventHandler\ProgressBarHandler; use Phauthentic\CognitiveCodeAnalysis\Command\EventHandler\VerboseHandler; @@ -51,6 +52,8 @@ use Phauthentic\CognitiveCodeAnalysis\Command\Presentation\ChurnTextRenderer; use Phauthentic\CognitiveCodeAnalysis\Command\Presentation\CognitiveMetricTextRenderer; use Phauthentic\CognitiveCodeAnalysis\Command\Presentation\CognitiveMetricTextRendererInterface; +use Phauthentic\CognitiveCodeAnalysis\Config\ConfigFileResolver; +use Phauthentic\CognitiveCodeAnalysis\Config\ConfigInitializer; use Phauthentic\CognitiveCodeAnalysis\Config\ConfigLoader; use Phauthentic\CognitiveCodeAnalysis\Config\ConfigService; use PhpParser\NodeTraverser; @@ -178,6 +181,17 @@ private function registerUtilityServices(): void $this->containerBuilder->register(ConfigLoader::class, ConfigLoader::class) ->setPublic(true); + $this->containerBuilder->register(ConfigFileResolver::class, ConfigFileResolver::class) + ->setPublic(true); + + $this->containerBuilder->register(ConfigInitializer::class, ConfigInitializer::class) + ->setArguments([ + new Reference(Processor::class), + new Reference(ConfigLoader::class), + __DIR__ . '/../config.yml', + ]) + ->setPublic(true); + $this->containerBuilder->register(ParserFactory::class, ParserFactory::class) ->setPublic(true); @@ -416,12 +430,21 @@ private function registerCommands(): void $this->containerBuilder->register(CognitiveMetricsCommand::class, CognitiveMetricsCommand::class) ->setArguments([ new Reference(CommandPipelineFactory::class), + new Reference(ConfigFileResolver::class), ]) ->setPublic(true); $this->containerBuilder->register(ChurnCommand::class, ChurnCommand::class) ->setArguments([ new Reference(ChurnPipelineFactory::class), + new Reference(ConfigFileResolver::class), + ]) + ->setPublic(true); + + $this->containerBuilder->register(InitCommand::class, InitCommand::class) + ->setArguments([ + new Reference(ConfigInitializer::class), + new Reference(ConfigFileResolver::class), ]) ->setPublic(true); } @@ -435,7 +458,8 @@ private function configureApplication(): void ]) ->setPublic(true) ->addMethodCall('add', [new Reference(CognitiveMetricsCommand::class)]) - ->addMethodCall('add', [new Reference(ChurnCommand::class)]); + ->addMethodCall('add', [new Reference(ChurnCommand::class)]) + ->addMethodCall('add', [new Reference(InitCommand::class)]); } public function run(): void diff --git a/src/Command/ChurnCommand.php b/src/Command/ChurnCommand.php index c1826ef..2d05999 100644 --- a/src/Command/ChurnCommand.php +++ b/src/Command/ChurnCommand.php @@ -7,6 +7,7 @@ use Phauthentic\CognitiveCodeAnalysis\Command\Pipeline\ChurnExecutionContext; use Phauthentic\CognitiveCodeAnalysis\Command\Pipeline\ChurnPipelineFactory; use Phauthentic\CognitiveCodeAnalysis\Command\ChurnSpecifications\ChurnCommandContext; +use Phauthentic\CognitiveCodeAnalysis\Config\ConfigFileResolver; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -30,7 +31,8 @@ class ChurnCommand extends Command public const OPTION_COVERAGE_CLOVER = 'coverage-clover'; public function __construct( - readonly private ChurnPipelineFactory $pipelineFactory + readonly private ChurnPipelineFactory $pipelineFactory, + readonly private ConfigFileResolver $configFileResolver, ) { parent::__construct(); } @@ -106,7 +108,7 @@ protected function configure(): void */ protected function execute(InputInterface $input, OutputInterface $output): int { - $commandContext = new ChurnCommandContext($input); + $commandContext = new ChurnCommandContext($input, $this->configFileResolver); $executionContext = new ChurnExecutionContext($commandContext, $output); // Build pipeline with stages diff --git a/src/Command/ChurnSpecifications/ChurnCommandContext.php b/src/Command/ChurnSpecifications/ChurnCommandContext.php index 431d413..92e558b 100644 --- a/src/Command/ChurnSpecifications/ChurnCommandContext.php +++ b/src/Command/ChurnSpecifications/ChurnCommandContext.php @@ -4,18 +4,20 @@ namespace Phauthentic\CognitiveCodeAnalysis\Command\ChurnSpecifications; +use Phauthentic\CognitiveCodeAnalysis\Config\ConfigFileResolver; use Symfony\Component\Console\Input\InputInterface; class ChurnCommandContext { public function __construct( - private InputInterface $input + private InputInterface $input, + private ConfigFileResolver $configFileResolver, ) { } public function getConfigFile(): ?string { - return $this->input->getOption('config'); + return $this->configFileResolver->resolve($this->input->getOption('config')); } public function hasConfigFile(): bool diff --git a/src/Command/CognitiveMetricsCommand.php b/src/Command/CognitiveMetricsCommand.php index 783b564..85ec129 100644 --- a/src/Command/CognitiveMetricsCommand.php +++ b/src/Command/CognitiveMetricsCommand.php @@ -7,6 +7,7 @@ use Phauthentic\CognitiveCodeAnalysis\Command\Pipeline\CommandPipelineFactory; use Phauthentic\CognitiveCodeAnalysis\Command\Pipeline\ExecutionContext; use Phauthentic\CognitiveCodeAnalysis\Command\CognitiveMetricsSpecifications\CognitiveMetricsCommandContext; +use Phauthentic\CognitiveCodeAnalysis\Config\ConfigFileResolver; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -38,7 +39,8 @@ class CognitiveMetricsCommand extends Command private const ARGUMENT_PATH = 'path'; public function __construct( - readonly private CommandPipelineFactory $pipelineFactory + readonly private CommandPipelineFactory $pipelineFactory, + readonly private ConfigFileResolver $configFileResolver, ) { parent::__construct(); } @@ -125,7 +127,7 @@ protected function configure(): void */ protected function execute(InputInterface $input, OutputInterface $output): int { - $commandContext = new CognitiveMetricsCommandContext($input); + $commandContext = new CognitiveMetricsCommandContext($input, $this->configFileResolver); $executionContext = new ExecutionContext($commandContext, $output); // Build pipeline with stages diff --git a/src/Command/CognitiveMetricsSpecifications/CognitiveMetricsCommandContext.php b/src/Command/CognitiveMetricsSpecifications/CognitiveMetricsCommandContext.php index b3d9adb..d2c204e 100644 --- a/src/Command/CognitiveMetricsSpecifications/CognitiveMetricsCommandContext.php +++ b/src/Command/CognitiveMetricsSpecifications/CognitiveMetricsCommandContext.php @@ -4,18 +4,20 @@ namespace Phauthentic\CognitiveCodeAnalysis\Command\CognitiveMetricsSpecifications; +use Phauthentic\CognitiveCodeAnalysis\Config\ConfigFileResolver; use Symfony\Component\Console\Input\InputInterface; class CognitiveMetricsCommandContext { public function __construct( - private InputInterface $input + private InputInterface $input, + private ConfigFileResolver $configFileResolver, ) { } public function getConfigFile(): ?string { - return $this->input->getOption('config'); + return $this->configFileResolver->resolve($this->input->getOption('config')); } public function hasConfigFile(): bool diff --git a/src/Command/InitCommand.php b/src/Command/InitCommand.php new file mode 100644 index 0000000..d80484f --- /dev/null +++ b/src/Command/InitCommand.php @@ -0,0 +1,210 @@ +addOption( + name: self::OPTION_PATH, + shortcut: 'p', + mode: InputOption::VALUE_OPTIONAL, + description: 'Target config file path', + default: null, + ) + ->addOption( + name: self::OPTION_SILENT, + mode: InputOption::VALUE_NONE, + description: 'Skip interactive prompts and use default values', + ) + ->addOption( + name: self::OPTION_FORCE, + shortcut: 'f', + mode: InputOption::VALUE_NONE, + description: 'Overwrite an existing config file', + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $targetPath = $this->resolveTargetPath($input); + + if (is_file($targetPath) && !$input->getOption(self::OPTION_FORCE)) { + $io->error(sprintf( + 'Config file already exists at %s. Use --force to overwrite.', + $targetPath + )); + + return Command::FAILURE; + } + + try { + $overrides = $this->collectSettings($input, $io); + $config = $this->configInitializer->createDefaultConfig($overrides); + $this->configInitializer->writeConfigFile($targetPath, $config); + } catch (CognitiveAnalysisException $exception) { + $io->error($exception->getMessage()); + + return Command::FAILURE; + } + + $io->success(sprintf('Created config at: %s', $targetPath)); + + return Command::SUCCESS; + } + + private function resolveTargetPath(InputInterface $input): string + { + $path = $input->getOption(self::OPTION_PATH); + + if ($path !== null) { + return $path; + } + + return $this->configFileResolver->getDefaultPath(); + } + + /** + * @return array + */ + private function collectSettings(InputInterface $input, SymfonyStyle $io): array + { + if ($input->getOption(self::OPTION_SILENT) || !$input->isInteractive()) { + return $this->getDefaultOverrides(); + } + + return [ + 'cognitive' => [ + 'scoreThreshold' => $this->askScoreThreshold($io), + 'showOnlyMethodsExceedingThreshold' => $this->askBooleanSetting( + $io, + 'When enabled, only methods whose cognitive score exceeds the threshold are shown in output. ' + . 'Useful to focus on the most complex methods.', + 'Show only methods exceeding the threshold?', + self::DEFAULT_SHOW_ONLY_METHODS_EXCEEDING_THRESHOLD, + ), + 'showHalsteadComplexity' => $this->askBooleanSetting( + $io, + 'When enabled, Halstead complexity metrics are included in output. ' + . 'Halstead measures program length and vocabulary as a different aspect of complexity.', + 'Show Halstead complexity metrics?', + self::DEFAULT_SHOW_HALSTEAD_COMPLEXITY, + ), + 'showCyclomaticComplexity' => $this->askBooleanSetting( + $io, + 'When enabled, cyclomatic complexity metrics are included in output. ' + . 'Cyclomatic complexity measures the number of independent paths through code.', + 'Show cyclomatic complexity metrics?', + self::DEFAULT_SHOW_CYCLOMATIC_COMPLEXITY, + ), + 'showDetailedCognitiveMetrics' => $this->askBooleanSetting( + $io, + 'When enabled, individual metric columns (line count, arguments, returns, etc.) ' + . 'are shown in table output. When disabled, only the overall cognitive score is displayed.', + 'Show detailed cognitive metrics?', + self::DEFAULT_SHOW_DETAILED_COGNITIVE_METRICS, + ), + 'groupByClass' => $this->askBooleanSetting( + $io, + 'When enabled, results are grouped by class in separate tables. ' + . 'When disabled, all methods appear in one flat table sorted by complexity score.', + 'Group results by class?', + self::DEFAULT_GROUP_BY_CLASS, + ), + ], + ]; + } + + /** + * @return array + */ + private function getDefaultOverrides(): array + { + return [ + 'cognitive' => [ + 'scoreThreshold' => self::DEFAULT_SCORE_THRESHOLD, + 'showOnlyMethodsExceedingThreshold' => self::DEFAULT_SHOW_ONLY_METHODS_EXCEEDING_THRESHOLD, + 'showHalsteadComplexity' => self::DEFAULT_SHOW_HALSTEAD_COMPLEXITY, + 'showCyclomaticComplexity' => self::DEFAULT_SHOW_CYCLOMATIC_COMPLEXITY, + 'showDetailedCognitiveMetrics' => self::DEFAULT_SHOW_DETAILED_COGNITIVE_METRICS, + 'groupByClass' => self::DEFAULT_GROUP_BY_CLASS, + ], + ]; + } + + private function askScoreThreshold(SymfonyStyle $io): float + { + $io->writeln( + 'Methods with a score above this threshold are considered complex. ' + . 'It is used for highlighting in reports and for filtering when only showing methods ' + . 'exceeding the threshold.' + ); + + $value = $io->ask( + 'Score threshold', + (string) self::DEFAULT_SCORE_THRESHOLD, + static function (mixed $answer): float { + if (!is_numeric($answer)) { + throw new CognitiveAnalysisException('Score threshold must be a number.'); + } + + $threshold = (float) $answer; + if ($threshold <= 0) { + throw new CognitiveAnalysisException('Score threshold must be greater than 0.'); + } + + return $threshold; + } + ); + + return (float) $value; + } + + private function askBooleanSetting( + SymfonyStyle $io, + string $explanation, + string $question, + bool $default, + ): bool { + $io->writeln($explanation); + + return (bool) $io->confirm($question, $default); + } +} diff --git a/src/Config/ConfigFileResolver.php b/src/Config/ConfigFileResolver.php new file mode 100644 index 0000000..11ceb31 --- /dev/null +++ b/src/Config/ConfigFileResolver.php @@ -0,0 +1,26 @@ + $overrides + * @return array + */ + public function createDefaultConfig(array $overrides = []): array + { + $defaultConfig = Yaml::parseFile($this->bundledConfigPath); + + if ($overrides !== []) { + $defaultConfig = array_replace_recursive($defaultConfig, $overrides); + } + + return $this->processor->processConfiguration($this->configLoader, [$defaultConfig]); + } + + /** + * @param array $config + */ + public function writeConfigFile(string $path, array $config): void + { + $directory = dirname($path); + if (!is_dir($directory) && !mkdir($directory, 0755, true) && !is_dir($directory)) { + throw new CognitiveAnalysisException("Failed to create directory: {$directory}"); + } + + $yaml = Yaml::dump($config, 4, 2); + if (file_put_contents($path, $yaml) === false) { + throw new CognitiveAnalysisException("Failed to write config file: {$path}"); + } + } +} diff --git a/tests/Unit/Command/ChurnSpecificationPatternTest.php b/tests/Unit/Command/ChurnSpecificationPatternTest.php index 13517b4..1e23590 100644 --- a/tests/Unit/Command/ChurnSpecificationPatternTest.php +++ b/tests/Unit/Command/ChurnSpecificationPatternTest.php @@ -8,6 +8,7 @@ use Phauthentic\CognitiveCodeAnalysis\Command\ChurnSpecifications\CompositeChurnSpecification; use Phauthentic\CognitiveCodeAnalysis\Command\ChurnSpecifications\CoverageFileExists; use Phauthentic\CognitiveCodeAnalysis\Command\ChurnSpecifications\CoverageFormatExclusivity; +use Phauthentic\CognitiveCodeAnalysis\Config\ConfigFileResolver; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputArgument; @@ -16,6 +17,18 @@ class ChurnSpecificationPatternTest extends TestCase { + private ConfigFileResolver $configFileResolver; + + protected function setUp(): void + { + $this->configFileResolver = new ConfigFileResolver(); + } + + private function createContext(ArrayInput $input): ChurnCommandContext + { + return new ChurnCommandContext($input, $this->configFileResolver); + } + private function createInput(array $parameters): ArrayInput { $definition = new InputDefinition([ @@ -40,7 +53,7 @@ public function testCoverageFormatExclusivitySpecification(): void 'path' => '/test', '--coverage-cobertura' => 'coverage.xml' ]); - $context1 = new ChurnCommandContext($input1); + $context1 = $this->createContext($input1); $this->assertTrue($spec->isSatisfiedBy($context1)); // Test valid case - only clover @@ -48,7 +61,7 @@ public function testCoverageFormatExclusivitySpecification(): void 'path' => '/test', '--coverage-clover' => 'coverage.xml' ]); - $context2 = new ChurnCommandContext($input2); + $context2 = $this->createContext($input2); $this->assertTrue($spec->isSatisfiedBy($context2)); // Test invalid case - both formats @@ -57,7 +70,7 @@ public function testCoverageFormatExclusivitySpecification(): void '--coverage-cobertura' => 'cobertura.xml', '--coverage-clover' => 'clover.xml' ]); - $context3 = new ChurnCommandContext($input3); + $context3 = $this->createContext($input3); $this->assertFalse($spec->isSatisfiedBy($context3)); $this->assertEquals('Only one coverage format can be specified at a time.', $spec->getErrorMessage()); } @@ -68,7 +81,7 @@ public function testCoverageFileExistsSpecification(): void // Test valid case - no coverage file $input1 = $this->createInput(['path' => '/test']); - $context1 = new ChurnCommandContext($input1); + $context1 = $this->createContext($input1); $this->assertTrue($spec->isSatisfiedBy($context1)); // Test invalid case - non-existent file @@ -76,7 +89,7 @@ public function testCoverageFileExistsSpecification(): void 'path' => '/test', '--coverage-cobertura' => '/non/existent/file.xml' ]); - $context2 = new ChurnCommandContext($input2); + $context2 = $this->createContext($input2); $this->assertFalse($spec->isSatisfiedBy($context2)); $this->assertStringContainsString('Coverage file not found', $spec->getErrorMessage()); } @@ -90,7 +103,7 @@ public function testCompositeValidationSpecification(): void // Test valid case $input1 = $this->createInput(['path' => '/test']); - $context1 = new ChurnCommandContext($input1); + $context1 = $this->createContext($input1); $this->assertTrue($spec->isSatisfiedBy($context1)); // Test invalid case - both coverage formats @@ -99,7 +112,7 @@ public function testCompositeValidationSpecification(): void '--coverage-cobertura' => 'cobertura.xml', '--coverage-clover' => 'clover.xml' ]); - $context2 = new ChurnCommandContext($input2); + $context2 = $this->createContext($input2); $this->assertFalse($spec->isSatisfiedBy($context2)); $failedSpec = $spec->getFirstFailedSpecification($context2); diff --git a/tests/Unit/Command/CognitiveMetricsSpecifications/CognitiveMetricsSpecificationPatternTest.php b/tests/Unit/Command/CognitiveMetricsSpecifications/CognitiveMetricsSpecificationPatternTest.php index 7539b6f..27b6897 100644 --- a/tests/Unit/Command/CognitiveMetricsSpecifications/CognitiveMetricsSpecificationPatternTest.php +++ b/tests/Unit/Command/CognitiveMetricsSpecifications/CognitiveMetricsSpecificationPatternTest.php @@ -10,6 +10,7 @@ use Phauthentic\CognitiveCodeAnalysis\Command\CognitiveMetricsSpecifications\CoverageFormatExclusivity; use Phauthentic\CognitiveCodeAnalysis\Command\CognitiveMetricsSpecifications\SortFieldValid; use Phauthentic\CognitiveCodeAnalysis\Command\CognitiveMetricsSpecifications\SortOrderValid; +use Phauthentic\CognitiveCodeAnalysis\Config\ConfigFileResolver; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputArgument; @@ -18,6 +19,18 @@ class CognitiveMetricsSpecificationPatternTest extends TestCase { + private ConfigFileResolver $configFileResolver; + + protected function setUp(): void + { + $this->configFileResolver = new ConfigFileResolver(); + } + + private function createContext(ArrayInput $input): CognitiveMetricsCommandContext + { + return new CognitiveMetricsCommandContext($input, $this->configFileResolver); + } + private function createInput(array $parameters): ArrayInput { $definition = new InputDefinition([ @@ -44,7 +57,7 @@ public function testCoverageFormatExclusivitySpecification(): void 'path' => '/test', '--coverage-cobertura' => 'coverage.xml' ]); - $context1 = new CognitiveMetricsCommandContext($input1); + $context1 = $this->createContext($input1); $this->assertTrue($spec->isSatisfiedBy($context1)); // Test valid case - only clover @@ -52,7 +65,7 @@ public function testCoverageFormatExclusivitySpecification(): void 'path' => '/test', '--coverage-clover' => 'coverage.xml' ]); - $context2 = new CognitiveMetricsCommandContext($input2); + $context2 = $this->createContext($input2); $this->assertTrue($spec->isSatisfiedBy($context2)); // Test invalid case - both formats @@ -61,7 +74,7 @@ public function testCoverageFormatExclusivitySpecification(): void '--coverage-cobertura' => 'cobertura.xml', '--coverage-clover' => 'clover.xml' ]); - $context3 = new CognitiveMetricsCommandContext($input3); + $context3 = $this->createContext($input3); $this->assertFalse($spec->isSatisfiedBy($context3)); $this->assertEquals('Only one coverage format can be specified at a time.', $spec->getErrorMessage()); } @@ -72,7 +85,7 @@ public function testCoverageFileExistsSpecification(): void // Test valid case - no coverage file $input1 = $this->createInput(['path' => '/test']); - $context1 = new CognitiveMetricsCommandContext($input1); + $context1 = $this->createContext($input1); $this->assertTrue($spec->isSatisfiedBy($context1)); // Test invalid case - non-existent file @@ -80,7 +93,7 @@ public function testCoverageFileExistsSpecification(): void 'path' => '/test', '--coverage-cobertura' => '/non/existent/file.xml' ]); - $context2 = new CognitiveMetricsCommandContext($input2); + $context2 = $this->createContext($input2); $this->assertFalse($spec->isSatisfiedBy($context2)); $this->assertStringContainsString('Coverage file not found', $spec->getErrorMessage()); } @@ -91,7 +104,7 @@ public function testSortFieldValidSpecification(): void // Test valid case - no sort field $input1 = $this->createInput(['path' => '/test']); - $context1 = new CognitiveMetricsCommandContext($input1); + $context1 = $this->createContext($input1); $this->assertTrue($spec->isSatisfiedBy($context1)); // Test valid case - valid sort field @@ -99,7 +112,7 @@ public function testSortFieldValidSpecification(): void 'path' => '/test', '--sort-by' => 'score' ]); - $context2 = new CognitiveMetricsCommandContext($input2); + $context2 = $this->createContext($input2); $this->assertTrue($spec->isSatisfiedBy($context2)); // Test invalid case - invalid sort field @@ -107,7 +120,7 @@ public function testSortFieldValidSpecification(): void 'path' => '/test', '--sort-by' => 'invalid_field' ]); - $context3 = new CognitiveMetricsCommandContext($input3); + $context3 = $this->createContext($input3); $this->assertFalse($spec->isSatisfiedBy($context3)); $this->assertEquals('Invalid sort field provided.', $spec->getErrorMessage()); @@ -125,7 +138,7 @@ public function testSortOrderValidSpecification(): void 'path' => '/test', '--sort-order' => 'asc' ]); - $context1 = new CognitiveMetricsCommandContext($input1); + $context1 = $this->createContext($input1); $this->assertTrue($spec->isSatisfiedBy($context1)); // Test valid case - desc @@ -133,7 +146,7 @@ public function testSortOrderValidSpecification(): void 'path' => '/test', '--sort-order' => 'desc' ]); - $context2 = new CognitiveMetricsCommandContext($input2); + $context2 = $this->createContext($input2); $this->assertTrue($spec->isSatisfiedBy($context2)); // Test invalid case - invalid sort order @@ -141,7 +154,7 @@ public function testSortOrderValidSpecification(): void 'path' => '/test', '--sort-order' => 'invalid' ]); - $context3 = new CognitiveMetricsCommandContext($input3); + $context3 = $this->createContext($input3); $this->assertFalse($spec->isSatisfiedBy($context3)); $this->assertEquals('Sort order must be "asc" or "desc"', $spec->getErrorMessage()); @@ -159,7 +172,7 @@ public function testCompositeValidationSpecification(): void // Test valid case $input1 = $this->createInput(['path' => '/test']); - $context1 = new CognitiveMetricsCommandContext($input1); + $context1 = $this->createContext($input1); $this->assertTrue($spec->isSatisfiedBy($context1)); // Test invalid case - both coverage formats @@ -168,7 +181,7 @@ public function testCompositeValidationSpecification(): void '--coverage-cobertura' => 'cobertura.xml', '--coverage-clover' => 'clover.xml' ]); - $context2 = new CognitiveMetricsCommandContext($input2); + $context2 = $this->createContext($input2); $this->assertFalse($spec->isSatisfiedBy($context2)); $failedSpec = $spec->getFirstFailedSpecification($context2); @@ -192,7 +205,7 @@ public function testCognitiveMetricsCommandContext(): void '--debug' => true ]); - $context = new CognitiveMetricsCommandContext($input); + $context = $this->createContext($input); $this->assertEquals('/test/path', $context->getPaths()[0]); $this->assertTrue($context->hasConfigFile()); diff --git a/tests/Unit/Command/ConfigAutoDiscoveryTest.php b/tests/Unit/Command/ConfigAutoDiscoveryTest.php new file mode 100644 index 0000000..a19bfe8 --- /dev/null +++ b/tests/Unit/Command/ConfigAutoDiscoveryTest.php @@ -0,0 +1,187 @@ +originalWorkingDirectory = (string) getcwd(); + $this->projectRoot = dirname(__DIR__, 3); + $this->tempDir = sys_get_temp_dir() . '/config-auto-discovery-' . uniqid('', true); + mkdir($this->tempDir); + } + + protected function tearDown(): void + { + chdir($this->originalWorkingDirectory); + $this->removeDirectory($this->tempDir); + } + + #[Test] + public function analyseAutoLoadsCcaYamlFromWorkingDirectory(): void + { + chdir($this->tempDir); + $this->writeAutoDiscoveryConfig(0.75); + + $application = new Application(); + $command = $application->getContainer()->get(CognitiveMetricsCommand::class); + $tester = new CommandTester($command); + + $tempOutputFile = tempnam(sys_get_temp_dir(), 'auto_discovery_report_') . '.txt'; + + $tester->execute([ + 'path' => $this->projectRoot . '/tests/TestCode', + '--report-type' => 'configtext', + '--report-file' => $tempOutputFile, + ]); + + $this->assertSame(Command::SUCCESS, $tester->getStatusCode()); + $this->assertStringContainsString('Score Threshold: 0.75', file_get_contents($tempOutputFile)); + + unlink($tempOutputFile); + } + + #[Test] + public function analyseExplicitConfigOverridesAutoDiscoveredCcaYaml(): void + { + chdir($this->tempDir); + $this->writeAutoDiscoveryConfig(0.75); + + $explicitConfigPath = $this->tempDir . '/explicit-config.yaml'; + $this->writeConfigFile($explicitConfigPath, 0.80); + + $application = new Application(); + $command = $application->getContainer()->get(CognitiveMetricsCommand::class); + $tester = new CommandTester($command); + + $tempOutputFile = tempnam(sys_get_temp_dir(), 'auto_discovery_override_report_') . '.txt'; + + $tester->execute([ + 'path' => $this->projectRoot . '/tests/TestCode', + '--config' => $explicitConfigPath, + '--report-type' => 'configtext', + '--report-file' => $tempOutputFile, + ]); + + $this->assertSame(Command::SUCCESS, $tester->getStatusCode()); + $this->assertStringContainsString('Score Threshold: 0.80', file_get_contents($tempOutputFile)); + + unlink($tempOutputFile); + } + + #[Test] + public function churnCommandContextAutoDiscoversCcaYamlFromWorkingDirectory(): void + { + chdir($this->tempDir); + file_put_contents( + $this->tempDir . DIRECTORY_SEPARATOR . ConfigFileResolver::DEFAULT_FILENAME, + "cognitive:\n scoreThreshold: 0.75\n" + ); + + $input = new ArrayInput( + ['path' => $this->projectRoot . '/src'], + new InputDefinition([ + new InputArgument('path', InputArgument::REQUIRED), + new InputOption('config', 'c', InputOption::VALUE_OPTIONAL), + ]) + ); + + $context = new ChurnCommandContext($input, new ConfigFileResolver()); + + $this->assertTrue($context->hasConfigFile()); + $this->assertSame( + $this->tempDir . DIRECTORY_SEPARATOR . ConfigFileResolver::DEFAULT_FILENAME, + $context->getConfigFile() + ); + } + + private function writeAutoDiscoveryConfig(float $scoreThreshold): void + { + $this->writeConfigFile( + $this->tempDir . DIRECTORY_SEPARATOR . ConfigFileResolver::DEFAULT_FILENAME, + $scoreThreshold + ); + } + + private function writeConfigFile(string $path, float $scoreThreshold): void + { + $config = [ + 'cognitive' => [ + 'excludeFilePatterns' => [], + 'excludePatterns' => [], + 'scoreThreshold' => $scoreThreshold, + 'showOnlyMethodsExceedingThreshold' => true, + 'showHalsteadComplexity' => false, + 'showCyclomaticComplexity' => false, + 'showDetailedCognitiveMetrics' => true, + 'groupByClass' => false, + 'metrics' => [ + 'lineCount' => [ + 'threshold' => 60, + 'scale' => 25.0, + 'enabled' => true, + ], + ], + 'customReporters' => [ + 'cognitive' => [ + 'configtext' => [ + 'class' => 'Tests\Fixtures\CustomReporters\ConfigAwareTextReporter', + 'file' => $this->projectRoot . '/tests/Fixtures/CustomReporters/ConfigAwareTextReporter.php', + ], + ], + ], + ], + ]; + + file_put_contents($path, Yaml::dump($config, 4, 2)); + } + + private function removeDirectory(string $directory): void + { + if (!is_dir($directory)) { + return; + } + + $items = scandir($directory); + if ($items === false) { + return; + } + + foreach ($items as $item) { + if ($item === '.' || $item === '..') { + continue; + } + + $path = $directory . DIRECTORY_SEPARATOR . $item; + if (is_dir($path)) { + $this->removeDirectory($path); + continue; + } + + unlink($path); + } + + rmdir($directory); + } +} diff --git a/tests/Unit/Command/InitCommandTest.php b/tests/Unit/Command/InitCommandTest.php new file mode 100644 index 0000000..c93f93c --- /dev/null +++ b/tests/Unit/Command/InitCommandTest.php @@ -0,0 +1,192 @@ +tempDir = sys_get_temp_dir() . '/init-command-' . uniqid('', true); + mkdir($this->tempDir); + } + + protected function tearDown(): void + { + $this->removeDirectory($this->tempDir); + } + + #[Test] + public function silentModeCreatesConfigWithDefaultsAtGivenPath(): void + { + $targetPath = $this->tempDir . '/cca.yaml'; + $tester = $this->createCommandTester(); + + $statusCode = $tester->execute([ + '--silent' => true, + '--path' => $targetPath, + ]); + + $this->assertSame(Command::SUCCESS, $statusCode); + $this->assertFileExists($targetPath); + + $config = Yaml::parseFile($targetPath); + $this->assertSame(0.5, $config['cognitive']['scoreThreshold']); + $this->assertTrue($config['cognitive']['groupByClass']); + $this->assertFalse($config['cognitive']['showOnlyMethodsExceedingThreshold']); + $this->assertFalse($config['cognitive']['showHalsteadComplexity']); + $this->assertFalse($config['cognitive']['showCyclomaticComplexity']); + $this->assertTrue($config['cognitive']['showDetailedCognitiveMetrics']); + } + + #[Test] + public function silentModeUsesDefaultPathInWorkingDirectory(): void + { + $originalWorkingDirectory = getcwd(); + chdir($this->tempDir); + + try { + $tester = $this->createCommandTester(); + $statusCode = $tester->execute(['--silent' => true]); + + $this->assertSame(Command::SUCCESS, $statusCode); + $this->assertFileExists($this->tempDir . '/cca.yaml'); + } finally { + chdir($originalWorkingDirectory); + } + } + + #[Test] + public function interactiveModeWritesSelectedSettings(): void + { + $targetPath = $this->tempDir . '/interactive-cca.yaml'; + $tester = $this->createCommandTester(); + $tester->setInputs([ + '0.8', + 'yes', + 'yes', + 'no', + 'yes', + 'no', + ]); + + $statusCode = $tester->execute([ + '--path' => $targetPath, + ]); + + $this->assertSame(Command::SUCCESS, $statusCode); + + $config = Yaml::parseFile($targetPath); + $this->assertSame(0.8, $config['cognitive']['scoreThreshold']); + $this->assertTrue($config['cognitive']['showOnlyMethodsExceedingThreshold']); + $this->assertTrue($config['cognitive']['showHalsteadComplexity']); + $this->assertFalse($config['cognitive']['showCyclomaticComplexity']); + $this->assertTrue($config['cognitive']['showDetailedCognitiveMetrics']); + $this->assertFalse($config['cognitive']['groupByClass']); + } + + #[Test] + public function failsWhenFileExistsWithoutForce(): void + { + $targetPath = $this->tempDir . '/existing.yaml'; + file_put_contents($targetPath, 'existing: true'); + $tester = $this->createCommandTester(); + + $statusCode = $tester->execute([ + '--silent' => true, + '--path' => $targetPath, + ]); + + $this->assertSame(Command::FAILURE, $statusCode); + $this->assertSame('existing: true', file_get_contents($targetPath)); + } + + #[Test] + public function forceOverwritesExistingFile(): void + { + $targetPath = $this->tempDir . '/overwrite.yaml'; + file_put_contents($targetPath, 'existing: true'); + $tester = $this->createCommandTester(); + + $statusCode = $tester->execute([ + '--silent' => true, + '--force' => true, + '--path' => $targetPath, + ]); + + $this->assertSame(Command::SUCCESS, $statusCode); + + $config = Yaml::parseFile($targetPath); + $this->assertArrayHasKey('cognitive', $config); + } + + #[Test] + public function rejectsInvalidScoreThreshold(): void + { + $targetPath = $this->tempDir . '/invalid-threshold.yaml'; + $tester = $this->createCommandTester(); + $tester->setInputs([ + 'invalid', + '0.5', + 'no', + 'no', + 'no', + 'yes', + 'yes', + ]); + + $statusCode = $tester->execute([ + '--path' => $targetPath, + ]); + + $this->assertSame(Command::SUCCESS, $statusCode); + $this->assertSame(0.5, Yaml::parseFile($targetPath)['cognitive']['scoreThreshold']); + } + + private function createCommandTester(): CommandTester + { + $application = new Application(); + $command = $application->getContainer()->get(InitCommand::class); + + return new CommandTester($command); + } + + private function removeDirectory(string $directory): void + { + if (!is_dir($directory)) { + return; + } + + $items = scandir($directory); + if ($items === false) { + return; + } + + foreach ($items as $item) { + if ($item === '.' || $item === '..') { + continue; + } + + $path = $directory . DIRECTORY_SEPARATOR . $item; + if (is_dir($path)) { + $this->removeDirectory($path); + continue; + } + + unlink($path); + } + + rmdir($directory); + } +} diff --git a/tests/Unit/Config/ConfigFileResolverTest.php b/tests/Unit/Config/ConfigFileResolverTest.php new file mode 100644 index 0000000..25ac4d8 --- /dev/null +++ b/tests/Unit/Config/ConfigFileResolverTest.php @@ -0,0 +1,81 @@ +originalWorkingDirectory = (string) getcwd(); + } + + protected function tearDown(): void + { + chdir($this->originalWorkingDirectory); + } + + #[Test] + public function resolveReturnsExplicitPathWhenProvided(): void + { + $resolver = new ConfigFileResolver(); + + $this->assertSame('/custom/config.yaml', $resolver->resolve('/custom/config.yaml')); + } + + #[Test] + public function resolveAutoDiscoversCcaYamlInWorkingDirectory(): void + { + $tempDir = sys_get_temp_dir() . '/cca-resolver-' . uniqid('', true); + mkdir($tempDir); + chdir($tempDir); + + $configPath = $tempDir . DIRECTORY_SEPARATOR . ConfigFileResolver::DEFAULT_FILENAME; + file_put_contents($configPath, 'cognitive: {}'); + + $resolver = new ConfigFileResolver(); + + $this->assertSame($configPath, $resolver->resolve(null)); + + unlink($configPath); + rmdir($tempDir); + } + + #[Test] + public function resolveReturnsNullWhenNoExplicitPathOrDefaultFile(): void + { + $tempDir = sys_get_temp_dir() . '/cca-resolver-empty-' . uniqid('', true); + mkdir($tempDir); + chdir($tempDir); + + $resolver = new ConfigFileResolver(); + + $this->assertNull($resolver->resolve(null)); + + rmdir($tempDir); + } + + #[Test] + public function getDefaultPathReturnsCcaYamlInWorkingDirectory(): void + { + $tempDir = sys_get_temp_dir() . '/cca-resolver-default-' . uniqid('', true); + mkdir($tempDir); + chdir($tempDir); + + $resolver = new ConfigFileResolver(); + + $this->assertSame( + $tempDir . DIRECTORY_SEPARATOR . ConfigFileResolver::DEFAULT_FILENAME, + $resolver->getDefaultPath() + ); + + rmdir($tempDir); + } +} diff --git a/tests/Unit/Config/ConfigInitializerTest.php b/tests/Unit/Config/ConfigInitializerTest.php new file mode 100644 index 0000000..efdad8f --- /dev/null +++ b/tests/Unit/Config/ConfigInitializerTest.php @@ -0,0 +1,107 @@ +tempDir = sys_get_temp_dir() . '/config-initializer-' . uniqid('', true); + mkdir($this->tempDir); + } + + protected function tearDown(): void + { + $this->removeDirectory($this->tempDir); + } + + #[Test] + public function createDefaultConfigReturnsSchemaValidDefaults(): void + { + $initializer = $this->createInitializer(); + $config = $initializer->createDefaultConfig(); + + $this->assertArrayHasKey('cognitive', $config); + $this->assertSame(0.5, $config['cognitive']['scoreThreshold']); + $this->assertTrue($config['cognitive']['groupByClass']); + $this->assertArrayHasKey('metrics', $config['cognitive']); + $this->assertArrayHasKey('lineCount', $config['cognitive']['metrics']); + } + + #[Test] + public function createDefaultConfigAppliesOverrides(): void + { + $initializer = $this->createInitializer(); + $config = $initializer->createDefaultConfig([ + 'cognitive' => [ + 'scoreThreshold' => 0.8, + 'groupByClass' => false, + ], + ]); + + $this->assertSame(0.8, $config['cognitive']['scoreThreshold']); + $this->assertFalse($config['cognitive']['groupByClass']); + } + + #[Test] + public function writeConfigFileCreatesFileAndParentDirectory(): void + { + $initializer = $this->createInitializer(); + $config = $initializer->createDefaultConfig(); + $targetPath = $this->tempDir . '/nested/cca.yaml'; + + $initializer->writeConfigFile($targetPath, $config); + + $this->assertFileExists($targetPath); + $parsed = Yaml::parseFile($targetPath); + $this->assertSame(0.5, $parsed['cognitive']['scoreThreshold']); + } + + private function createInitializer(): ConfigInitializer + { + return new ConfigInitializer( + new Processor(), + new ConfigLoader(), + __DIR__ . '/../../../config.yml', + ); + } + + private function removeDirectory(string $directory): void + { + if (!is_dir($directory)) { + return; + } + + $items = scandir($directory); + if ($items === false) { + return; + } + + foreach ($items as $item) { + if ($item === '.' || $item === '..') { + continue; + } + + $path = $directory . DIRECTORY_SEPARATOR . $item; + if (is_dir($path)) { + $this->removeDirectory($path); + continue; + } + + unlink($path); + } + + rmdir($directory); + } +} From 94438fbcc4e3a4392c207a818c7ccb3b45f37517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Sat, 23 May 2026 17:13:45 +0200 Subject: [PATCH 2/4] Update CI configuration to include PHP version 8.5 --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6e22e0a..9f79122 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - php-version: ['8.2', '8.3', '8.4'] + php-version: ['8.2', '8.3', '8.4', '8.5'] steps: - uses: actions/checkout@v4 From cba6e220c6f84a524ee8eafd2d5b6ebf3d803203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Sat, 23 May 2026 17:13:52 +0200 Subject: [PATCH 3/4] Optimize Dockerfile for PHP 8.3 environment - Consolidated apt-get commands to reduce image size by cleaning up package lists. - Updated xdebug configuration commands to a single RUN instruction for efficiency. - Changed git configuration to system-wide for better accessibility. - Enhanced user creation command to ensure home directory and permissions are set correctly. - Set the working directory to /app for improved container organization. --- docker/php/Dockerfile | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index cfe1efb..d6bcacf 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -1,7 +1,8 @@ FROM php:8.3-cli -RUN apt-get update -RUN apt-get install -y libzip-dev zip git wget gpg +RUN apt-get update \ + && apt-get install -y libzip-dev zip git wget gpg \ + && rm -rf /var/lib/apt/lists/* RUN pear update-channels \ && pecl update-channels \ @@ -9,16 +10,17 @@ RUN pear update-channels \ RUN docker-php-ext-enable xdebug -RUN echo "xdebug.mode=coverage,debug" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini -RUN echo "xdebug.start_with_request=yes" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini +RUN echo "xdebug.mode=coverage,debug" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \ + && echo "xdebug.start_with_request=yes" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini -# Install Composer globally RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer -RUN git config --global --add safe.directory /app +RUN git config --system --add safe.directory /app -# Create a new user -RUN useradd -m php +RUN useradd --create-home --no-log-init --shell /bin/bash php \ + && mkdir -p /app \ + && chown -R php:php /app + +WORKDIR /app -# Set the new user as the default user for the container USER php From 260edd4d0c25ca236c4ba5741e16715473eb18aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Sat, 23 May 2026 17:16:09 +0200 Subject: [PATCH 4/4] Refactor InitCommand to use consistent variable naming for SymfonyStyle - Changed variable name from `$io` to `$style` for clarity and consistency. - Updated all references to the SymfonyStyle instance within the `InitCommand` class. - Improved readability of the code by maintaining a uniform naming convention. --- src/Command/InitCommand.php | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Command/InitCommand.php b/src/Command/InitCommand.php index d80484f..53c1cb0 100644 --- a/src/Command/InitCommand.php +++ b/src/Command/InitCommand.php @@ -63,11 +63,11 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { - $io = new SymfonyStyle($input, $output); + $style = new SymfonyStyle($input, $output); $targetPath = $this->resolveTargetPath($input); if (is_file($targetPath) && !$input->getOption(self::OPTION_FORCE)) { - $io->error(sprintf( + $style->error(sprintf( 'Config file already exists at %s. Use --force to overwrite.', $targetPath )); @@ -76,16 +76,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int } try { - $overrides = $this->collectSettings($input, $io); + $overrides = $this->collectSettings($input, $style); $config = $this->configInitializer->createDefaultConfig($overrides); $this->configInitializer->writeConfigFile($targetPath, $config); } catch (CognitiveAnalysisException $exception) { - $io->error($exception->getMessage()); + $style->error($exception->getMessage()); return Command::FAILURE; } - $io->success(sprintf('Created config at: %s', $targetPath)); + $style->success(sprintf('Created config at: %s', $targetPath)); return Command::SUCCESS; } @@ -104,7 +104,7 @@ private function resolveTargetPath(InputInterface $input): string /** * @return array */ - private function collectSettings(InputInterface $input, SymfonyStyle $io): array + private function collectSettings(InputInterface $input, SymfonyStyle $style): array { if ($input->getOption(self::OPTION_SILENT) || !$input->isInteractive()) { return $this->getDefaultOverrides(); @@ -112,37 +112,37 @@ private function collectSettings(InputInterface $input, SymfonyStyle $io): array return [ 'cognitive' => [ - 'scoreThreshold' => $this->askScoreThreshold($io), + 'scoreThreshold' => $this->askScoreThreshold($style), 'showOnlyMethodsExceedingThreshold' => $this->askBooleanSetting( - $io, + $style, 'When enabled, only methods whose cognitive score exceeds the threshold are shown in output. ' . 'Useful to focus on the most complex methods.', 'Show only methods exceeding the threshold?', self::DEFAULT_SHOW_ONLY_METHODS_EXCEEDING_THRESHOLD, ), 'showHalsteadComplexity' => $this->askBooleanSetting( - $io, + $style, 'When enabled, Halstead complexity metrics are included in output. ' . 'Halstead measures program length and vocabulary as a different aspect of complexity.', 'Show Halstead complexity metrics?', self::DEFAULT_SHOW_HALSTEAD_COMPLEXITY, ), 'showCyclomaticComplexity' => $this->askBooleanSetting( - $io, + $style, 'When enabled, cyclomatic complexity metrics are included in output. ' . 'Cyclomatic complexity measures the number of independent paths through code.', 'Show cyclomatic complexity metrics?', self::DEFAULT_SHOW_CYCLOMATIC_COMPLEXITY, ), 'showDetailedCognitiveMetrics' => $this->askBooleanSetting( - $io, + $style, 'When enabled, individual metric columns (line count, arguments, returns, etc.) ' . 'are shown in table output. When disabled, only the overall cognitive score is displayed.', 'Show detailed cognitive metrics?', self::DEFAULT_SHOW_DETAILED_COGNITIVE_METRICS, ), 'groupByClass' => $this->askBooleanSetting( - $io, + $style, 'When enabled, results are grouped by class in separate tables. ' . 'When disabled, all methods appear in one flat table sorted by complexity score.', 'Group results by class?', @@ -169,15 +169,15 @@ private function getDefaultOverrides(): array ]; } - private function askScoreThreshold(SymfonyStyle $io): float + private function askScoreThreshold(SymfonyStyle $style): float { - $io->writeln( + $style->writeln( 'Methods with a score above this threshold are considered complex. ' . 'It is used for highlighting in reports and for filtering when only showing methods ' . 'exceeding the threshold.' ); - $value = $io->ask( + $value = $style->ask( 'Score threshold', (string) self::DEFAULT_SCORE_THRESHOLD, static function (mixed $answer): float { @@ -198,13 +198,13 @@ static function (mixed $answer): float { } private function askBooleanSetting( - SymfonyStyle $io, + SymfonyStyle $style, string $explanation, string $question, bool $default, ): bool { - $io->writeln($explanation); + $style->writeln($explanation); - return (bool) $io->confirm($question, $default); + return (bool) $style->confirm($question, $default); } }