diff --git a/phpunit.xml.dist b/phpunit.xml.dist index aa12943..cf769b1 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,15 +1,17 @@ + backupGlobals="false" + backupStaticAttributes="false" + colors="true" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + processIsolation="true" + stopOnFailure="false" + bootstrap="tests/bootstrap.php" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" + cacheResultFile="./tests/data/cache/.phpunit.result.cache" +> ./src diff --git a/src/Helper/OutputHelper.php b/src/Helper/OutputHelper.php index 9c68d62..75eb6fc 100644 --- a/src/Helper/OutputHelper.php +++ b/src/Helper/OutputHelper.php @@ -61,11 +61,24 @@ class OutputHelper { use InflectsString; + /** + * The output writer instance used to write formatted output. + * + * @var Writer + */ protected Writer $writer; - - /** @var int Max width of command name */ + /** + * Max width of command name. + * + * @var int + */ protected int $maxCmdName = 0; + /** + * Class constructor. + * + * @param Writer|null $writer The output writer instance used to write formatted output. + */ public function __construct(?Writer $writer = null) { $this->writer = $writer ?? new Writer; @@ -79,7 +92,7 @@ public function printTrace(Throwable $e): void $eClass = get_class($e); $this->writer->colors( - "{$eClass} {$e->getMessage()}" . + "$eClass {$e->getMessage()}" . '(' . t('thrown in') . " {$e->getFile()}:{$e->getLine()})" ); @@ -107,6 +120,19 @@ public function printTrace(Throwable $e): void $this->writer->colors($traceStr); } + /** + * Converts an array of arguments into a string representation. + * + * Each array element is converted based on its type: + * - Scalar values (int, float, string, bool) are var_exported + * - Objects are converted using __toString() if available, otherwise class name is used + * - Arrays are recursively processed and wrapped in square brackets + * - Other types are converted to their type name + * + * @param array $args Array of arguments to be stringified + * + * @return string The comma-separated string representation of all arguments + */ public function stringifyArgs(array $args): string { $holder = []; @@ -118,7 +144,14 @@ public function stringifyArgs(array $args): string return implode(', ', $holder); } - protected function stringifyArg($arg): string + /** + * Converts the provided argument into a string representation. + * + * @param mixed $arg The argument to be converted into a string. This can be of any type. + * + * @return string A string representation of the provided argument. + */ + protected function stringifyArg(mixed $arg): string { if (is_scalar($arg)) { return var_export($arg, true); @@ -196,15 +229,17 @@ protected function showHelp(string $for, array $items, string $header = '', stri return; } - $space = 4; - $group = $lastGroup = null; + $space = 4; + $lastGroup = null; $withDefault = $for === 'Options' || $for === 'Arguments'; foreach (array_values($this->sortItems($items, $padLen, $for)) as $idx => $item) { $name = $this->getName($item); if ($for === 'Commands' && $lastGroup !== $group = $item->group()) { - $this->writer->help_group($group ?: '*', true); $lastGroup = $group; + if ($group !== '') { + $this->writer->help_group($group, true); + } } $desc = str_replace(["\r\n", "\n"], str_pad("\n", $padLen + $space + 3), $item->desc($withDefault)); @@ -254,12 +289,21 @@ public function showUsage(string $usage): self return $this; } + /** + * Shows an error message when a command is not found and suggests similar commands. + * Uses levenshtein distance to find commands that are similar to the attempted one. + * + * @param string $attempted The command name that was attempted to be executed + * @param array $available List of available command names + * + * @return OutputHelper For method chaining + */ public function showCommandNotFound(string $attempted, array $available): self { $closest = []; foreach ($available as $cmd) { $lev = levenshtein($attempted, $cmd); - if ($lev > 0 || $lev < 5) { + if ($lev > 0 && $lev < 5) { $closest[$cmd] = $lev; } } @@ -278,12 +322,12 @@ public function showCommandNotFound(string $attempted, array $available): self * Sort items by name. As a side effect sets max length of all names. * * @param Parameter[]|Command[] $items - * @param int $max + * @param int|null $max * @param string $for * * @return array */ - protected function sortItems(array $items, &$max = 0, string $for = ''): array + protected function sortItems(array $items, ?int &$max = 0, string $for = ''): array { $max = max(array_map(fn ($item) => strlen($this->getName($item)), $items)); @@ -292,8 +336,10 @@ protected function sortItems(array $items, &$max = 0, string $for = ''): array } uasort($items, static function ($a, $b) { - $aName = $a instanceof Groupable ? $a->group() . $a->name() : $a->name(); - $bName = $b instanceof Groupable ? $b->group() . $b->name() : $b->name(); + // Items in the default group (where group() returns empty/falsy) are prefixed with '__' + // to ensure they appear at the top of the sorted list, whilst grouped items follow after + $aName = $a instanceof Groupable ? ($a->group() ?: '__') . $a->name() : $a->name(); + $bName = $b instanceof Groupable ? ($b->group() ?: '__') . $b->name() : $b->name(); return $aName <=> $bName; }); @@ -308,7 +354,7 @@ protected function sortItems(array $items, &$max = 0, string $for = ''): array * * @return string */ - protected function getName($item): string + protected function getName(Parameter|Command $item): string { $name = $item->name(); diff --git a/tests/Helper/OutputHelperTest.php b/tests/Helper/OutputHelperTest.php index 29ce36a..2706fbd 100644 --- a/tests/Helper/OutputHelperTest.php +++ b/tests/Helper/OutputHelperTest.php @@ -28,7 +28,7 @@ class OutputHelperTest extends TestCase { - protected static $ou = __DIR__ . '/output'; + protected static string $ou = __DIR__ . '/output'; public function setUp(): void { @@ -88,16 +88,16 @@ public function test_show_commands() new Command('group:mkdir', 'Make a folder'), ], 'Cmd Header', 'Cmd Footer'); + // If the default group exists, we expect visually to be rendered at the very top. $this->assertSame([ 'Cmd Header', '', 'Commands:', + ' mkdir Make a folder', + ' rm Remove file or folder', 'group', ' group:mkdir Make a folder', ' group:rm Remove file or folder', - '*', - ' mkdir Make a folder', - ' rm Remove file or folder', '', 'Cmd Footer', ], $this->output()); @@ -150,7 +150,7 @@ public function test_stringify() $this->assertSame("[NULL, 'string', 10000, 12.345, DateTime]", $str); } - public function newHelper() + public function newHelper(): OutputHelper { return new OutputHelper(new Writer(static::$ou, new class extends Color { protected string $format = ':txt:';