diff --git a/src/Config.php b/src/Config.php index 0fe06904be..46dd0e43f9 100644 --- a/src/Config.php +++ b/src/Config.php @@ -17,6 +17,7 @@ use PHP_CodeSniffer\Exceptions\DeepExitException; use PHP_CodeSniffer\Exceptions\RuntimeException; use PHP_CodeSniffer\Util\Common; +use PHP_CodeSniffer\Util\Help; use PHP_CodeSniffer\Util\Standards; /** @@ -1395,71 +1396,21 @@ public function printShortUsage($return=false) */ public function printPHPCSUsage() { - echo 'Usage: phpcs [-nwlsaepqvi] [-d key[=value]] [--colors] [--no-colors]'.PHP_EOL; - echo ' [--cache[=]] [--no-cache] [--tab-width=]'.PHP_EOL; - echo ' [--report=] [--report-file=] [--report-=]'.PHP_EOL; - echo ' [--report-width=] [--basepath=] [--bootstrap=]'.PHP_EOL; - echo ' [--severity=] [--error-severity=] [--warning-severity=]'.PHP_EOL; - echo ' [--runtime-set key value] [--config-set key value] [--config-delete key] [--config-show]'.PHP_EOL; - echo ' [--standard=] [--sniffs=] [--exclude=]'.PHP_EOL; - echo ' [--encoding=] [--parallel=] [--generator=]'.PHP_EOL; - echo ' [--extensions=] [--ignore=] [--ignore-annotations]'.PHP_EOL; - echo ' [--stdin-path=] [--file-list=] [--filter=] - ...'.PHP_EOL; - echo PHP_EOL; - echo ' - Check STDIN instead of local files and directories'.PHP_EOL; - echo ' -n Do not print warnings (shortcut for --warning-severity=0)'.PHP_EOL; - echo ' -w Print both warnings and errors (this is the default)'.PHP_EOL; - echo ' -l Local directory only, no recursion'.PHP_EOL; - echo ' -s Show error codes in all reports'.PHP_EOL; - echo ' -a Run interactively'.PHP_EOL; - echo ' -e Explain a standard by showing the sniffs it includes'.PHP_EOL; - echo ' -p Show progress of the run'.PHP_EOL; - echo ' -q Quiet mode; disables progress and verbose output'.PHP_EOL; - echo ' -m Stop error messages from being recorded'.PHP_EOL; - echo ' (saves a lot of memory, but stops many reports from being used)'.PHP_EOL; - echo ' -v Print processed files'.PHP_EOL; - echo ' -vv Print ruleset and token output'.PHP_EOL; - echo ' -vvv Print sniff processing information'.PHP_EOL; - echo ' -i Show a list of installed coding standards'.PHP_EOL; - echo ' -d Set the [key] php.ini value to [value] or [true] if value is omitted'.PHP_EOL; - echo PHP_EOL; - echo ' --help Print this help message'.PHP_EOL; - echo ' --version Print version information'.PHP_EOL; - echo ' --colors Use colors in output'.PHP_EOL; - echo ' --no-colors Do not use colors in output (this is the default)'.PHP_EOL; - echo ' --cache Cache results between runs'.PHP_EOL; - echo ' --no-cache Do not cache results between runs (this is the default)'.PHP_EOL; - echo ' --ignore-annotations Ignore all phpcs: annotations in code comments'.PHP_EOL; - echo PHP_EOL; - echo ' Use a specific file for caching (uses a temporary file by default)'.PHP_EOL; - echo ' A path to strip from the front of file paths inside reports'.PHP_EOL; - echo ' A comma separated list of files to run before processing begins'.PHP_EOL; - echo ' The encoding of the files being checked (default is utf-8)'.PHP_EOL; - echo ' A comma separated list of file extensions to check'.PHP_EOL; - echo ' The type of the file can be specified using: ext/type'.PHP_EOL; - echo ' e.g., module/php,es/js'.PHP_EOL; - echo ' One or more files and/or directories to check'.PHP_EOL; - echo ' A file containing a list of files and/or directories to check (one per line)'.PHP_EOL; - echo ' Use either the "GitModified" or "GitStaged" filter,'.PHP_EOL; - echo ' or specify the path to a custom filter class'.PHP_EOL; - echo ' Use either the "HTML", "Markdown" or "Text" generator'.PHP_EOL; - echo ' (forces documentation generation instead of checking)'.PHP_EOL; - echo ' A comma separated list of patterns to ignore files and directories'.PHP_EOL; - echo ' How many files should be checked simultaneously (default is 1)'.PHP_EOL; - echo ' Print either the "full", "xml", "checkstyle", "csv"'.PHP_EOL; - echo ' "json", "junit", "emacs", "source", "summary", "diff"'.PHP_EOL; - echo ' "svnblame", "gitblame", "hgblame", "notifysend" or "performance",'.PHP_EOL; - echo ' report or specify the path to a custom report class'.PHP_EOL; - echo ' (the "full" report is printed by default)'.PHP_EOL; - echo ' Write the report to the specified file path'.PHP_EOL; - echo ' How many columns wide screen reports should be printed'.PHP_EOL; - echo ' or set to "auto" to use current screen width, where supported'.PHP_EOL; - echo ' The minimum severity required to display an error or warning'.PHP_EOL; - echo ' A comma separated list of sniff codes to include or exclude from checking'.PHP_EOL; - echo ' (all sniffs must be part of the specified standard)'.PHP_EOL; - echo ' The name or path of the coding standard to use'.PHP_EOL; - echo ' If processing STDIN, the file path that STDIN will be processed as'.PHP_EOL; - echo ' The number of spaces each tab represents'.PHP_EOL; + $longOptions = explode(',', Help::DEFAULT_LONG_OPTIONS); + $longOptions[] = 'cache'; + $longOptions[] = 'no-cache'; + $longOptions[] = 'report'; + $longOptions[] = 'report-file'; + $longOptions[] = 'report-report'; + $longOptions[] = 'config-explain'; + $longOptions[] = 'config-set'; + $longOptions[] = 'config-delete'; + $longOptions[] = 'config-show'; + $longOptions[] = 'generator'; + + $shortOptions = Help::DEFAULT_SHORT_OPTIONS.'aems'; + + (new Help($this, $longOptions, $shortOptions))->display(); }//end printPHPCSUsage() @@ -1471,49 +1422,11 @@ public function printPHPCSUsage() */ public function printPHPCBFUsage() { - echo 'Usage: phpcbf [-nwli] [-d key[=value]] [--ignore-annotations] [--bootstrap=]'.PHP_EOL; - echo ' [--standard=] [--sniffs=] [--exclude=] [--suffix=]'.PHP_EOL; - echo ' [--severity=] [--error-severity=] [--warning-severity=]'.PHP_EOL; - echo ' [--tab-width=] [--encoding=] [--parallel=]'.PHP_EOL; - echo ' [--basepath=] [--extensions=] [--ignore=]'.PHP_EOL; - echo ' [--stdin-path=] [--file-list=] [--filter=] - ...'.PHP_EOL; - echo PHP_EOL; - echo ' - Fix STDIN instead of local files and directories'.PHP_EOL; - echo ' -n Do not fix warnings (shortcut for --warning-severity=0)'.PHP_EOL; - echo ' -w Fix both warnings and errors (on by default)'.PHP_EOL; - echo ' -l Local directory only, no recursion'.PHP_EOL; - echo ' -p Show progress of the run'.PHP_EOL; - echo ' -q Quiet mode; disables progress and verbose output'.PHP_EOL; - echo ' -v Print processed files'.PHP_EOL; - echo ' -vv Print ruleset and token output'.PHP_EOL; - echo ' -vvv Print sniff processing information'.PHP_EOL; - echo ' -i Show a list of installed coding standards'.PHP_EOL; - echo ' -d Set the [key] php.ini value to [value] or [true] if value is omitted'.PHP_EOL; - echo PHP_EOL; - echo ' --help Print this help message'.PHP_EOL; - echo ' --version Print version information'.PHP_EOL; - echo ' --ignore-annotations Ignore all phpcs: annotations in code comments'.PHP_EOL; - echo PHP_EOL; - echo ' A path to strip from the front of file paths inside reports'.PHP_EOL; - echo ' A comma separated list of files to run before processing begins'.PHP_EOL; - echo ' The encoding of the files being fixed (default is utf-8)'.PHP_EOL; - echo ' A comma separated list of file extensions to fix'.PHP_EOL; - echo ' The type of the file can be specified using: ext/type'.PHP_EOL; - echo ' e.g., module/php,es/js'.PHP_EOL; - echo ' One or more files and/or directories to fix'.PHP_EOL; - echo ' A file containing a list of files and/or directories to fix (one per line)'.PHP_EOL; - echo ' Use either the "GitModified" or "GitStaged" filter,'.PHP_EOL; - echo ' or specify the path to a custom filter class'.PHP_EOL; - echo ' A comma separated list of patterns to ignore files and directories'.PHP_EOL; - echo ' How many files should be fixed simultaneously (default is 1)'.PHP_EOL; - echo ' The minimum severity required to fix an error or warning'.PHP_EOL; - echo ' A comma separated list of sniff codes to include or exclude from fixing'.PHP_EOL; - echo ' (all sniffs must be part of the specified standard)'.PHP_EOL; - echo ' The name or path of the coding standard to use'.PHP_EOL; - echo ' If processing STDIN, the file path that STDIN will be processed as'.PHP_EOL; - echo ' Write modified files to a filename using this suffix'.PHP_EOL; - echo ' ("diff" and "patch" are not used in this mode)'.PHP_EOL; - echo ' The number of spaces each tab represents'.PHP_EOL; + $longOptions = explode(',', Help::DEFAULT_LONG_OPTIONS); + $longOptions[] = 'suffix'; + $shortOptions = Help::DEFAULT_SHORT_OPTIONS; + + (new Help($this, $longOptions, $shortOptions))->display(); }//end printPHPCBFUsage() diff --git a/src/Util/Help.php b/src/Util/Help.php new file mode 100644 index 0000000000..a7602b7d9d --- /dev/null +++ b/src/Util/Help.php @@ -0,0 +1,626 @@ + + * @copyright 2024 Juliette Reinders Folmer. All rights reserved. + * @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Util; + +use InvalidArgumentException; +use PHP_CodeSniffer\Config; +use PHP_CodeSniffer\Util\Common; + +final class Help +{ + + + /** + * Short options which are available for both the `phpcs` as well as the `phpcbf` command. + * + * @var string + */ + const DEFAULT_SHORT_OPTIONS = '-hilnpqvw'; + + /** + * Long options which are available for both the `phpcs` as well as the `phpcbf` command. + * + * {@internal This should be a constant array, but those aren't supported until PHP 5.6.} + * + * @var string Comma-separated list of the option names. + */ + const DEFAULT_LONG_OPTIONS = 'basepath,bootstrap,colors,encoding,error-severity,exclude,extensions,file,file-list,filter,ignore,ignore-annotations,no-colors,parallel,php-ini,report-width,runtime-set,severity,sniffs,standard,stdin-path,tab-width,version,vv,vvv,warning-severity'; + + /** + * Minimum screen width. + * + * The help info needs room to display, so this is the minimum acceptable width. + * + * @var integer + */ + const MIN_WIDTH = 60; + + /** + * Indent option lines. + * + * @var string + */ + const INDENT = ' '; + + /** + * Gutter spacing for between the option argument info and the option description. + * + * @var string + */ + const GUTTER = ' '; + + /** + * The current PHPCS Configuration. + * + * @var \PHP_CodeSniffer\Config + */ + private $config; + + /** + * The options which should be shown for this help screen. + * + * @var array + */ + private $requestedOptions = []; + + /** + * Active options per category (after filtering). + * + * @var array>> + */ + private $activeOptions = []; + + /** + * Width of the indent for option lines. + * + * @var integer + */ + private $indentWidth = 0; + + /** + * Width of the gutter spacing. + * + * @var integer + */ + private $gutterWidth = 0; + + /** + * Width of longest option argument info entry. + * + * @var integer + */ + private $maxOptionNameLength = 0; + + + /** + * Constructor. + * + * @param \PHP_CodeSniffer\Config $config Configuration object. + * @param array $longOptions The long options which should be shown. + * @param string $shortOptions The short options which should be shown. + * + * @throws \InvalidArgumentException When $shortOptions is not a string. + */ + public function __construct(Config $config, array $longOptions, $shortOptions='') + { + if (is_string($shortOptions) === false) { + throw new InvalidArgumentException('The $shortOptions parameter must be a string'); + } + + $this->config = $config; + $this->requestedOptions = array_merge($longOptions, str_split($shortOptions)); + + $this->filterOptions(); + + $this->indentWidth = strlen(self::INDENT); + $this->gutterWidth = strlen(self::GUTTER); + + $this->setMaxOptionNameLength(); + + }//end __construct() + + + /** + * Display the help info. + * + * @return void + */ + public function display() + { + $this->printUsage(); + $this->printCategories(); + + }//end display() + + + /** + * Filter the available options based on the requested options. + * + * @return void + */ + private function filterOptions() + { + $filteredOptions = $this->getAllOptions(); + + foreach ($filteredOptions as $category => $options) { + // Initial state set to "true" to prevent a spacer at the start of an array. + $lastWasSpacer = true; + $spacerCount = 0; + + foreach ($options as $name => $option) { + if ($lastWasSpacer !== true && strpos($name, 'blank-line') === 0) { + ++$spacerCount; + $lastWasSpacer = true; + continue; + } + + if (in_array($name, $this->requestedOptions, true) === false) { + unset($filteredOptions[$category][$name]); + continue; + } + + $lastWasSpacer = false; + } + + // Make sure the final array doesn't contain a spacer at the end. + if (empty($filteredOptions[$category]) === false) { + end($filteredOptions[$category]); + $key = key($filteredOptions[$category]); + if (strpos($key, 'blank-line') === 0) { + unset($filteredOptions[$category][$key]); + --$spacerCount; + } + } + + // Remove categories now left empty. + if (empty($filteredOptions[$category]) === true || count($filteredOptions[$category]) === $spacerCount) { + unset($filteredOptions[$category]); + } + }//end foreach + + $this->activeOptions = $filteredOptions; + + }//end filterOptions() + + + /** + * Determine the length of the longest option argument and store it. + * + * @return void + */ + private function setMaxOptionNameLength() + { + $lengths = []; + foreach ($this->activeOptions as $category => $options) { + foreach ($options as $option) { + if (isset($option['argument']) === false) { + continue; + } + + $lengths[] = strlen($option['argument']); + } + } + + if (empty($lengths) === false) { + $this->maxOptionNameLength = max($lengths); + } + + }//end setMaxOptionNameLength() + + + /** + * Get the maximum width which can be used to display the help info. + * + * Independently of user preference/auto-determined width of the current screen, + * a minimum width is needed to display information, so don't allow this to get too low. + * + * @return int + */ + private function getMaxWidth() + { + return max(self::MIN_WIDTH, $this->config->reportWidth); + + }//end getMaxWidth() + + + /** + * Get the maximum width for the text in the option description column. + * + * @return int + */ + private function getDescriptionColumnWidth() + { + return ($this->getMaxWidth() - $this->maxOptionNameLength - $this->indentWidth - $this->gutterWidth); + + }//end getDescriptionColumnWidth() + + + /** + * Get the length of the indentation needed for follow up lines when the description does not fit on one line. + * + * @return int + */ + private function getDescriptionFollowupLineIndentLength() + { + return ($this->maxOptionNameLength + $this->indentWidth + $this->gutterWidth); + + }//end getDescriptionFollowupLineIndentLength() + + + /** + * Print basic usage information to the screen. + * + * @return void + */ + private function printUsage() + { + $command = 'phpcs'; + if (defined('PHP_CODESNIFFER_CBF') === true && PHP_CODESNIFFER_CBF === true) { + // @codeCoverageIgnore + $command = 'phpcbf'; + } + + $this->printCategoryHeader('Usage'); + + echo self::INDENT.$command.' [options] '.PHP_EOL; + + }//end printUsage() + + + /** + * Print details of all the requested options to the screen, sorted by category. + * + * @return void + */ + private function printCategories() + { + foreach ($this->activeOptions as $category => $options) { + $this->printCategoryHeader($category); + $this->printCategoryOptions($options); + } + + }//end printCategories() + + + /** + * Print a category header. + * + * @param string $header The header text. + * + * @return void + */ + private function printCategoryHeader($header) + { + $header .= ':'; + if ($this->config->colors === true) { + $header = "\033[33m{$header}\033[0m"; + } + + echo PHP_EOL.$header.PHP_EOL; + + }//end printCategoryHeader() + + + /** + * Print the options for a category. + * + * @param array> $options The options to display. + * + * @return void + */ + private function printCategoryOptions(array $options) + { + $maxDescriptionWidth = $this->getDescriptionColumnWidth(); + $maxTextWidth = ($this->getMaxWidth() - $this->indentWidth); + $secondLineIndent = str_repeat(' ', $this->getDescriptionFollowupLineIndentLength()); + + $output = ''; + foreach ($options as $option) { + if (isset($option['spacer']) === true) { + $output .= PHP_EOL; + } + + if (isset($option['text']) === true) { + $text = wordwrap($option['text'], $maxTextWidth, "\n"); + $output .= self::INDENT.implode(PHP_EOL.self::INDENT, explode("\n", $text)).PHP_EOL; + } + + if (isset($option['argument'], $option['description']) === true) { + $argument = str_pad($option['argument'], $this->maxOptionNameLength); + $argument = $this->colorizeVariableInput($argument); + $output .= self::INDENT."\033[32m{$argument}\033[0m"; + $output .= self::GUTTER; + + $description = wordwrap($option['description'], $maxDescriptionWidth, "\n"); + $output .= implode(PHP_EOL.$secondLineIndent, explode("\n", $description)).PHP_EOL; + } + } + + if ($this->config->colors === false) { + $output = Common::stripColors($output); + } + + echo $output; + + }//end printCategoryOptions() + + + /** + * Colorize "variable" input in the option argument info. + * + * For the purposes of this method, "variable" input is text between <> brackets. + * The regex allows for multiple tags and nested tags. + * + * @param string $text The text to process. + * + * @return string + */ + private function colorizeVariableInput($text) + { + return preg_replace('`(<(?:(?>[^<>]+)|(?R))*>)`', "\033[36m".'$1'."\033[32m", $text); + + }//end colorizeVariableInput() + + + /** + * Retrieve the help details for all supported CLI arguments per category. + * + * @return array>> + */ + private function getAllOptions() + { + $options = []; + + // phpcs:disable Squiz.Strings.ConcatenationSpacing.PaddingFound -- Readability is more important. + $options['Scan targets'] = [ + 'file' => [ + 'argument' => '', + 'description' => 'One or more files and/or directories to check, space separated.', + ], + '-' => [ + 'argument' => '-', + 'description' => 'Check STDIN instead of local files and directories.', + ], + 'stdin-path' => [ + 'argument' => '--stdin-path=', + 'description' => 'If processing STDIN, the file path that STDIN will be processed as.', + ], + 'file-list' => [ + 'argument' => '--file-list=', + 'description' => 'Check the files and/or directories which are defined in the file to which the path is provided (one per line).', + ], + 'filter' => [ + 'argument' => '--filter=', + 'description' => 'Check based on a predefined file filter. Use either the "GitModified" or "GitStaged" filter, or specify the path to a custom filter class.', + ], + 'ignore' => [ + 'argument' => '--ignore=', + 'description' => 'Ignore files based on a comma-separated list of patterns matching files and/or directories.', + ], + 'extensions' => [ + 'argument' => '--extensions=', + 'description' => 'Check files with the specified file extensions (comma-separated list). Defaults to php,inc/php,js,css.'."\n" + .'The type of the file can be specified using: ext/type; e.g. module/php,es/js.', + ], + 'l' => [ + 'argument' => '-l', + 'description' => 'Check local directory only, no recursion.', + ], + ]; + + $options['Rule Selection Options'] = [ + 'standard' => [ + 'argument' => '--standard=', + 'description' => 'The name of, or the path to, the coding standard to use. Can be a comma-separated list specifying multiple standards. If no standard is specified, PHP_CodeSniffer will look for a [.]phpcs.xml[.dist] custom ruleset file in the current directory and those above it.', + ], + 'sniffs' => [ + 'argument' => '--sniffs=', + 'description' => 'A comma-separated list of sniff codes to limit the scan to. All sniffs must be part of the standard in use.', + ], + 'exclude' => [ + 'argument' => '--exclude=', + 'description' => 'A comma-separated list of sniff codes to exclude from the scan. All sniffs must be part of the standard in use.', + ], + 'blank-line' => ['spacer' => ''], + + 'i' => [ + 'argument' => '-i', + 'description' => 'Show a list of installed coding standards.', + ], + 'e' => [ + 'argument' => '-e', + 'description' => 'Explain a standard by showing the names of all the sniffs it includes.', + ], + 'generator' => [ + 'argument' => '--generator=', + 'description' => 'Show documentation for a standard. Use either the "HTML", "Markdown" or "Text" generator.', + ], + ]; + + $options['Run Options'] = [ + 'a' => [ + 'argument' => '-a', + 'description' => 'Run in interactive mode, pausing after each file.', + ], + 'bootstrap' => [ + 'argument' => '--bootstrap=', + 'description' => 'Run the specified file(s) before processing begins. A list of files can be provided, separated by commas.', + ], + 'cache' => [ + 'argument' => '--cache[=]', + 'description' => 'Cache results between runs. Optionally, can be provided to use a specific file for caching. Otherwise, a temporary file is used.', + ], + 'no-cache' => [ + 'argument' => '--no-cache', + 'description' => 'Do not cache results between runs (default).', + ], + 'parallel' => [ + 'argument' => '--parallel=', + 'description' => 'The number of files to be checked simultaneously. Defaults to 1 (no parallel processing).'."\n" + .'If enabled, this option only takes effect if the PHP PCNTL (Process Control) extension is available.', + ], + 'suffix' => [ + 'argument' => '--suffix=', + 'description' => 'Write modified files to a filename using this suffix ("diff" and "patch" are not used in this mode).', + ], + 'blank-line' => ['spacer' => ''], + + 'php-ini' => [ + 'argument' => '-d ', + 'description' => 'Set the [key] php.ini value to [value] or set to [true] if value is omitted.'."\n" + .'Note: only php.ini settings which can be changed at runtime are supported.', + ], + ]; + + $options['Reporting Options'] = [ + 'report' => [ + 'argument' => '--report=', + 'description' => 'Print either the "full", "xml", "checkstyle", "csv", "json", "junit", "emacs", "source", "summary", "diff", "svnblame", "gitblame", "hgblame", "notifysend" or "performance" report or specify the path to a custom report class. By default, the "full" report is displayed.', + ], + 'report-file' => [ + 'argument' => '--report-file=', + 'description' => 'Write the report to the specified file path.', + ], + 'report-report' => [ + 'argument' => '--report-=', + 'description' => 'Write the report specified in to the specified file path.', + ], + 'report-width' => [ + 'argument' => '--report-width=', + 'description' => 'How many columns wide screen reports should be. Set to "auto" to use current screen width, where supported.', + ], + 'basepath' => [ + 'argument' => '--basepath=', + 'description' => 'Strip a path from the front of file paths inside reports.', + ], + 'blank-line-1' => ['spacer' => ''], + + 'w' => [ + 'argument' => '-w', + 'description' => 'Include both warnings and errors (default).', + ], + 'n' => [ + 'argument' => '-n', + 'description' => 'Do not include warnings. Shortcut for "--warning-severity=0".', + ], + 'severity' => [ + 'argument' => '--severity=', + 'description' => 'The minimum severity required to display an error or warning. Defaults to 5.', + ], + 'error-severity' => [ + 'argument' => '--error-severity=', + 'description' => 'The minimum severity required to display an error. Defaults to 5.', + ], + 'warning-severity' => [ + 'argument' => '--warning-severity=', + 'description' => 'The minimum severity required to display a warning. Defaults to 5.', + ], + 'blank-line-2' => ['spacer' => ''], + + 's' => [ + 'argument' => '-s', + 'description' => 'Show sniff error codes in all reports.', + ], + 'ignore-annotations' => [ + 'argument' => '--ignore-annotations', + 'description' => 'Ignore all "phpcs:..." annotations in code comments.', + ], + 'colors' => [ + 'argument' => '--colors', + 'description' => 'Use colors in screen output.', + ], + 'no-colors' => [ + 'argument' => '--no-colors', + 'description' => 'Do not use colors in screen output (default).', + ], + 'p' => [ + 'argument' => '-p', + 'description' => 'Show progress of the run.', + ], + 'q' => [ + 'argument' => '-q', + 'description' => 'Quiet mode; disables progress and verbose output.', + ], + 'm' => [ + 'argument' => '-m', + 'description' => 'Stop error messages from being recorded. This saves a lot of memory but stops many reports from being used.', + ], + ]; + + $options['Configuration Options'] = [ + 'encoding' => [ + 'argument' => '--encoding=', + 'description' => 'The encoding of the files being checked. Defaults to "utf-8".', + ], + 'tab-width' => [ + 'argument' => '--tab-width=', + 'description' => 'The number of spaces each tab represents.', + ], + 'blank-line' => ['spacer' => ''], + + 'config-explain' => [ + 'text' => 'Default values for a selection of options can be stored in a user-specific CodeSniffer.conf configuration file.'."\n" + .'This applies to the following options: "default_standard", "report_format", "tab_width", "encoding", "severity", "error_severity", "warning_severity", "show_warnings", "report_width", "show_progress", "quiet", "colors", "cache", "parallel".', + ], + 'config-show' => [ + 'argument' => '--config-show', + 'description' => 'Show the configuration options which are currently stored in the applicable CodeSniffer.conf file.', + ], + 'config-set' => [ + 'argument' => '--config-set ', + 'description' => 'Save a configuration option to the CodeSniffer.conf file.', + ], + 'config-delete' => [ + 'argument' => '--config-delete ', + 'description' => 'Delete a configuration option from the CodeSniffer.conf file.', + ], + 'runtime-set' => [ + 'argument' => '--runtime-set ', + 'description' => 'Set a configuration option to be applied to the current scan run only.', + ], + ]; + + $options['Miscellaneous Options'] = [ + 'h' => [ + 'argument' => '-h, -?, --help', + 'description' => 'Print this help message.', + ], + 'version' => [ + 'argument' => '--version', + 'description' => 'Print version information.', + ], + 'v' => [ + 'argument' => '-v', + 'description' => 'Verbose output: Print processed files.', + ], + 'vv' => [ + 'argument' => '-vv', + 'description' => 'Verbose output: Print ruleset and token output.', + ], + 'vvv' => [ + 'argument' => '-vvv', + 'description' => 'Verbose output: Print sniff processing information.', + ], + ]; + // phpcs:enable + + return $options; + + }//end getAllOptions() + + +}//end class diff --git a/tests/Core/Util/HelpTest.php b/tests/Core/Util/HelpTest.php new file mode 100644 index 0000000000..29b47e18d4 --- /dev/null +++ b/tests/Core/Util/HelpTest.php @@ -0,0 +1,725 @@ + + * @copyright 2024 Juliette Reinders Folmer. All rights reserved. + * @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Util; + +use PHP_CodeSniffer\Tests\ConfigDouble; +use PHP_CodeSniffer\Util\Help; +use PHPUnit\Framework\TestCase; +use ReflectionMethod; +use ReflectionProperty; + +/** + * Test the Help class. + * + * @covers \PHP_CodeSniffer\Util\Help + */ +final class HelpTest extends TestCase +{ + + + /** + * QA check: verify that the category names are at most the minimum screen width + * and that option argument names are always at most half the length of the minimum screen width. + * + * If this test would start failing, either wrapping of argument info would need to be implemented + * or the minimum screen width needs to be upped. + * + * @coversNothing + * + * @return void + */ + public function testQaArgumentNamesAreWithinAcceptableBounds() + { + $help = new Help(new ConfigDouble(), []); + + $reflMethod = new ReflectionMethod($help, 'getAllOptions'); + $reflMethod->setAccessible(true); + $allOptions = $reflMethod->invoke($help); + $reflMethod->setAccessible(false); + + $this->assertGreaterThan(0, count($allOptions), 'No categories found'); + + $minScreenWidth = Help::MIN_WIDTH; + $maxArgWidth = ($minScreenWidth / 2); + + foreach ($allOptions as $category => $options) { + $this->assertLessThanOrEqual( + Help::MIN_WIDTH, + strlen($category), + "Category name $category is longer than the minimum screen width of $minScreenWidth" + ); + + foreach ($options as $option) { + if (isset($option['argument']) === false) { + continue; + } + + $this->assertLessThanOrEqual( + $maxArgWidth, + strlen($option['argument']), + "Option name {$option['argument']} is longer than the half the minimum screen width of $minScreenWidth" + ); + } + } + + }//end testQaArgumentNamesAreWithinAcceptableBounds() + + + /** + * QA check: verify that each option only contains a spacer, text or argument + description combo. + * + * @coversNothing + * + * @return void + */ + public function testQaValidCategoryOptionDefinitions() + { + $help = new Help(new ConfigDouble(), []); + + $reflMethod = new ReflectionMethod($help, 'getAllOptions'); + $reflMethod->setAccessible(true); + $allOptions = $reflMethod->invoke($help); + $reflMethod->setAccessible(false); + + $this->assertGreaterThan(0, count($allOptions), 'No categories found'); + + foreach ($allOptions as $category => $options) { + $this->assertGreaterThan(0, count($options), "No options found in category $category"); + + foreach ($options as $name => $option) { + if (isset($option['spacer']) === true) { + $this->assertStringStartsWith('blank-line', $name, 'The name for spacer items should start with "blank-line"'); + } + + $this->assertFalse( + isset($option['spacer'], $option['text']), + "Option $name: spacer and text should not be combined in one option" + ); + $this->assertFalse( + isset($option['spacer'], $option['argument']), + "Option $name: spacer and argument should not be combined in one option" + ); + $this->assertFalse( + isset($option['spacer'], $option['description']), + "Option $name: spacer and description should not be combined in one option" + ); + $this->assertFalse( + isset($option['text'], $option['argument']), + "Option $name: text and argument should not be combined in one option" + ); + $this->assertFalse( + isset($option['text'], $option['description']), + "Option $name: text and description should not be combined in one option" + ); + + if (isset($option['argument']) === true) { + $this->assertArrayHasKey( + 'description', + $option, + "Option $name: an argument should always be accompanied by a description" + ); + } + + if (isset($option['description']) === true) { + $this->assertArrayHasKey( + 'argument', + $option, + "Option $name: a description should always be accompanied by an argument" + ); + } + }//end foreach + }//end foreach + + }//end testQaValidCategoryOptionDefinitions() + + + /** + * Test receiving an expected exception when the shortOptions parameter is not passed a string value. + * + * @return void + */ + public function testConstructorInvalidArgumentException() + { + $exception = 'InvalidArgumentException'; + $message = 'The $shortOptions parameter must be a string'; + + if (method_exists($this, 'expectException') === true) { + // PHPUnit 5+. + $this->expectException($exception); + $this->expectExceptionMessage($message); + } else { + // PHPUnit 4. + $this->setExpectedException($exception, $message); + } + + new Help(new ConfigDouble(), [], []); + + }//end testConstructorInvalidArgumentException() + + + /** + * Test filtering of the options by requested options. + * + * Tests that: + * - Options not explicitly requested are removed. + * - Short options passed via the longOptions array are still respected. + * - A category gets removed if all options are removed, even if the category still has spacers. + * + * @param array $longOptions The long options which should be displayed. + * @param string $shortOptions The short options which should be displayed. + * @param array $expected The categories expected after filtering with the number + * of expected help items per category. + * + * @dataProvider dataOptionFiltering + * + * @return void + */ + public function testOptionFiltering($longOptions, $shortOptions, $expected) + { + $help = new Help(new ConfigDouble(), $longOptions, $shortOptions); + + $reflProperty = new ReflectionProperty($help, 'activeOptions'); + $reflProperty->setAccessible(true); + $activeOptions = $reflProperty->getValue($help); + $reflProperty->setAccessible(false); + + // Simplify the value to make it comparible. + foreach ($activeOptions as $category => $options) { + $activeOptions[$category] = count($options); + } + + $this->assertSame($expected, $activeOptions, 'Option count per category does not match'); + + }//end testOptionFiltering() + + + /** + * Data provider. + * + * @return array|array>> + */ + public static function dataOptionFiltering() + { + $allLongOptions = explode(',', Help::DEFAULT_LONG_OPTIONS); + $allLongOptions[] = 'cache'; + $allLongOptions[] = 'no-cache'; + $allLongOptions[] = 'report'; + $allLongOptions[] = 'report-file'; + $allLongOptions[] = 'report-report'; + $allLongOptions[] = 'runtime-set'; + $allLongOptions[] = 'config-explain'; + $allLongOptions[] = 'config-set'; + $allLongOptions[] = 'config-delete'; + $allLongOptions[] = 'config-show'; + $allLongOptions[] = 'generator'; + $allLongOptions[] = 'suffix'; + + $allShortOptions = Help::DEFAULT_SHORT_OPTIONS.'saem'; + + return [ + 'No options' => [ + 'longOptions' => [], + 'shortOptions' => '', + 'expected' => [], + ], + 'Invalid options have no influence' => [ + 'longOptions' => [ + 'doesnotexist', + 'invalid', + ], + 'shortOptions' => 'bjrz', + 'expected' => [], + ], + 'Short options passed as long options works fine' => [ + 'longOptions' => [ + 's', + 'suffix', + 'a', + 'e', + 'colors', + ], + 'shortOptions' => '', + 'expected' => [ + 'Rule Selection Options' => 1, + 'Run Options' => 2, + 'Reporting Options' => 2, + ], + ], + 'All options' => [ + 'longOptions' => $allLongOptions, + 'shortOptions' => $allShortOptions, + 'expected' => [ + 'Scan targets' => 8, + 'Rule Selection Options' => 7, + 'Run Options' => 8, + 'Reporting Options' => 19, + 'Configuration Options' => 8, + 'Miscellaneous Options' => 5, + ], + ], + 'Default options only' => [ + 'longOptions' => explode(',', Help::DEFAULT_LONG_OPTIONS), + 'shortOptions' => Help::DEFAULT_SHORT_OPTIONS, + 'expected' => [ + 'Scan targets' => 8, + 'Rule Selection Options' => 5, + 'Run Options' => 4, + 'Reporting Options' => 14, + 'Configuration Options' => 4, + 'Miscellaneous Options' => 5, + ], + ], + 'Only one category' => [ + 'longOptions' => [ + 'file', + 'stdin-path', + 'file-list', + 'filter', + 'ignore', + 'extensions', + ], + 'shortOptions' => '-l', + 'expected' => [ + 'Scan targets' => 8, + ], + ], + 'All except one category' => [ + 'longOptions' => array_diff($allLongOptions, ['version', 'vv', 'vvv']), + 'shortOptions' => str_replace(['h', 'v'], '', $allShortOptions), + 'expected' => [ + 'Scan targets' => 8, + 'Rule Selection Options' => 7, + 'Run Options' => 8, + 'Reporting Options' => 19, + 'Configuration Options' => 8, + ], + ], + ]; + + }//end dataOptionFiltering() + + + /** + * Test filtering of the options by requested options does not leave stray spacers at the start + * or end of a category and that a category does not contain two consecutive spacers. + * + * {@internal Careful! This test may need updates to still test what it is supposed to test + * if/when the defined options in Help::getAllOptions() change.} + * + * @param array $longOptions The long options which should be displayed. + * @param string $shortOptions The short options which should be displayed. + * + * @dataProvider dataOptionFilteringSpacerHandling + * + * @return void + */ + public function testOptionFilteringSpacerHandling($longOptions, $shortOptions) + { + $help = new Help(new ConfigDouble(), $longOptions, $shortOptions); + + $reflProperty = new ReflectionProperty($help, 'activeOptions'); + $reflProperty->setAccessible(true); + $activeOptions = $reflProperty->getValue($help); + $reflProperty->setAccessible(false); + + $this->assertNotEmpty($activeOptions, 'Active options is empty, test is invalid'); + + foreach ($activeOptions as $options) { + $first = reset($options); + $this->assertArrayNotHasKey('spacer', $first, 'Found spacer at start of category'); + + $last = end($options); + $this->assertArrayNotHasKey('spacer', $last, 'Found spacer at end of category'); + + $previousWasSpacer = false; + foreach ($options as $option) { + $this->assertFalse((isset($option['spacer']) && $previousWasSpacer === true), 'Consecutive spacers found'); + $previousWasSpacer = isset($option['spacer']); + } + } + + }//end testOptionFilteringSpacerHandling() + + + /** + * Data provider. + * + * @return array>> + */ + public static function dataOptionFilteringSpacerHandling() + { + return [ + 'No spacer at start of category' => [ + 'longOptions' => ['generator'], + 'shortOptions' => 'ie', + ], + 'No spacer at end of category' => [ + 'longOptions' => [ + 'encoding', + 'tab-width', + ], + 'shortOptions' => '', + ], + 'No consecutive spacers within category' => [ + 'longOptions' => [ + 'report', + 'report-file', + 'report-report', + 'report-width', + 'basepath', + 'ignore-annotations', + 'colors', + 'no-colors', + ], + 'shortOptions' => 'spqm', + ], + ]; + + }//end dataOptionFilteringSpacerHandling() + + + /** + * Test that if no short/long options are passed, only usage information is displayed (and displayed correctly). + * + * @param array $cliArgs Command line arguments. + * @param string $expectedRegex Regex to validate expected output. + * + * @dataProvider dataDisplayUsage + * + * @return void + */ + public function testDisplayUsage($cliArgs, $expectedRegex) + { + $help = new Help(new ConfigDouble($cliArgs), []); + + $this->expectOutputRegex($expectedRegex); + + $help->display(); + + }//end testDisplayUsage() + + + /** + * Data provider. + * + * @return array>> + */ + public static function dataDisplayUsage() + { + return [ + 'Usage without colors' => [ + 'cliArgs' => ['--no-colors'], + 'expectedRegex' => '`^\s*Usage:\s+phpc(bf|s) \[options\] \\s+$`', + ], + 'Usage with colors' => [ + 'cliArgs' => ['--colors'], + 'expectedRegex' => '`^\s*\\033\[33mUsage:\\033\[0m\s+phpc(bf|s) \[options\] \\s+$`', + ], + ]; + + }//end dataDisplayUsage() + + + /** + * Test the column width calculations. + * + * This tests the following aspects: + * 1. That the report width is never less than Help::MIN_WIDTH, even when a smaller width is passed. + * 2. That the first column width is calculated correctly and is based on the longest argument. + * 3. That the word wrapping of the description respects the maximum report width. + * 4. That if the description is being wrapped, the indent for the second line is calculated correctly. + * + * @param int $reportWidth Report width for the test. + * @param array $longOptions The long options which should be displayed. + * @param string $expectedOutput Expected output. + * + * @dataProvider dataReportWidthCalculations + * + * @return void + */ + public function testReportWidthCalculations($reportWidth, $longOptions, $expectedOutput) + { + $config = new ConfigDouble(["--report-width=$reportWidth", '--no-colors']); + $help = new Help($config, $longOptions); + + $reflMethod = new ReflectionMethod($help, 'printCategories'); + $reflMethod->setAccessible(true); + $reflMethod->invoke($help); + $reflMethod->setAccessible(false); + + $this->expectOutputString($expectedOutput); + + }//end testReportWidthCalculations() + + + /** + * Data provider. + * + * @return array> + */ + public static function dataReportWidthCalculations() + { + $longOptions = [ + 'e', + 'generator', + ]; + + // phpcs:disable Squiz.Strings.ConcatenationSpacing.PaddingFound -- Test readability is more important. + return [ + 'Report width small: 40; forces report width to minimum width of 60' => [ + 'reportWidth' => 40, + 'longOptions' => $longOptions, + 'expectedOutput' => PHP_EOL.'Rule Selection Options:'.PHP_EOL + .' -e Explain a standard by showing the'.PHP_EOL + .' names of all the sniffs it'.PHP_EOL + .' includes.'.PHP_EOL + .' --generator= Show documentation for a standard.'.PHP_EOL + .' Use either the "HTML", "Markdown"'.PHP_EOL + .' or "Text" generator.'.PHP_EOL, + ], + 'Report width is minimum: 60 (= self::MIN_WIDTH)' => [ + 'reportWidth' => Help::MIN_WIDTH, + 'longOptions' => $longOptions, + 'expectedOutput' => PHP_EOL.'Rule Selection Options:'.PHP_EOL + .' -e Explain a standard by showing the'.PHP_EOL + .' names of all the sniffs it'.PHP_EOL + .' includes.'.PHP_EOL + .' --generator= Show documentation for a standard.'.PHP_EOL + .' Use either the "HTML", "Markdown"'.PHP_EOL + .' or "Text" generator.'.PHP_EOL, + ], + 'Report width matches length for one line, not the other: 96; only one should wrap' => [ + 'reportWidth' => 96, + 'longOptions' => $longOptions, + 'expectedOutput' => PHP_EOL.'Rule Selection Options:'.PHP_EOL + .' -e Explain a standard by showing the names of all the sniffs it includes.'.PHP_EOL + .' --generator= Show documentation for a standard. Use either the "HTML", "Markdown"'.PHP_EOL + .' or "Text" generator.'.PHP_EOL, + ], + 'Report width matches longest line: 119; the messages should not wrap and there should be no stray new line at the end' => [ + 'reportWidth' => 119, + 'longOptions' => $longOptions, + 'expectedOutput' => PHP_EOL.'Rule Selection Options:'.PHP_EOL + .' -e Explain a standard by showing the names of all the sniffs it includes.'.PHP_EOL + .' --generator= Show documentation for a standard. Use either the "HTML", "Markdown" or "Text" generator.'.PHP_EOL, + ], + ]; + // phpcs:enable + + }//end dataReportWidthCalculations() + + + /** + * Verify that variable elements in an argument specification get colorized correctly. + * + * @param string $input String to colorize. + * @param string $expected Expected function output. + * + * @dataProvider dataColorizeVariableInput + * + * @return void + */ + public function testColorizeVariableInput($input, $expected) + { + $help = new Help(new ConfigDouble(), []); + + $reflMethod = new ReflectionMethod($help, 'colorizeVariableInput'); + $reflMethod->setAccessible(true); + $result = $reflMethod->invoke($help, $input); + $reflMethod->setAccessible(false); + + $this->assertSame($expected, $result); + + }//end testColorizeVariableInput() + + + /** + * Data provider. + * + * @return array>> + */ + public static function dataColorizeVariableInput() + { + return [ + 'Empty string' => [ + 'input' => '', + 'expected' => '', + ], + 'String without variable element(s)' => [ + 'input' => 'This is text', + 'expected' => 'This is text', + ], + 'String with variable element' => [ + 'input' => 'This text', + 'expected' => "This \033[36m\033[32m text", + ], + 'String with multiple variable elements' => [ + 'input' => ' is ', + 'expected' => "\033[36m\033[32m is \033[36m\033[32m", + ], + 'String with unclosed variable element' => [ + 'input' => 'This 'This [ + 'input' => ' text>', + 'expected' => "\033[36m text>\033[32m", + ], + 'String with nested elements and surrounding text' => [ + 'input' => 'Start text> end', + 'expected' => "Start \033[36m text>\033[32m end", + ], + ]; + + }//end dataColorizeVariableInput() + + + /** + * Test the various option types within a category get displayed correctly. + * + * @param array $input The options to print. + * @param array $expectedRegex Regexes to validate expected output. + * + * @dataProvider dataPrintCategoryOptions + * + * @return void + */ + public function testPrintCategoryOptionsNoColor($input, $expectedRegex) + { + $config = new ConfigDouble(['--no-colors']); + $help = new Help($config, []); + + $reflProperty = new ReflectionProperty($help, 'activeOptions'); + $reflProperty->setAccessible(true); + $reflProperty->setValue($help, ['cat' => $input]); + $reflProperty->setAccessible(false); + + $reflMethod = new ReflectionMethod($help, 'setMaxOptionNameLength'); + $reflMethod->setAccessible(true); + $reflMethod->invoke($help); + $reflMethod->setAccessible(false); + + $reflMethod = new ReflectionMethod($help, 'printCategoryOptions'); + $reflMethod->setAccessible(true); + $reflMethod->invoke($help, $input); + $reflMethod->setAccessible(false); + + $this->expectOutputRegex($expectedRegex['no-color']); + + }//end testPrintCategoryOptionsNoColor() + + + /** + * Test the various option types within a category get displayed correctly. + * + * @param array $input The options to print. + * @param array $expectedRegex Regexes to validate expected output. + * + * @dataProvider dataPrintCategoryOptions + * + * @return void + */ + public function testPrintCategoryOptionsColor($input, $expectedRegex) + { + $config = new ConfigDouble(['--colors']); + $help = new Help($config, []); + + $reflProperty = new ReflectionProperty($help, 'activeOptions'); + $reflProperty->setAccessible(true); + $reflProperty->setValue($help, ['cat' => $input]); + $reflProperty->setAccessible(false); + + $reflMethod = new ReflectionMethod($help, 'setMaxOptionNameLength'); + $reflMethod->setAccessible(true); + $reflMethod->invoke($help); + $reflMethod->setAccessible(false); + + $reflMethod = new ReflectionMethod($help, 'printCategoryOptions'); + $reflMethod->setAccessible(true); + $reflMethod->invoke($help, $input); + $reflMethod->setAccessible(false); + + $this->expectOutputRegex($expectedRegex['color']); + + }//end testPrintCategoryOptionsColor() + + + /** + * Data provider. + * + * @return array>|array>> + */ + public static function dataPrintCategoryOptions() + { + $indentLength = strlen(Help::INDENT); + $gutterLength = strlen(Help::GUTTER); + + // phpcs:disable Squiz.Strings.ConcatenationSpacing.PaddingFound -- Test readability is more important. + // phpcs:disable Generic.Strings.UnnecessaryStringConcat.Found -- Test readability is more important. + return [ + 'Input: arg, spacer, arg; new lines in description get preserved' => [ + 'input' => [ + 'short-option' => [ + 'argument' => '-a', + 'description' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + ], + 'blank-line' => [ + 'spacer' => '', + ], + 'long-option-multi-line-description' => [ + 'argument' => '--something=', + 'description' => 'Proin sit amet malesuada libero, finibus bibendum tortor. Nulla vitae quam nec orci finibus pharetra.' + ."\n".'Nam eget blandit dui.', + ], + ], + 'expectedRegex' => [ + 'no-color' => '`^ {'.$indentLength.'}-a {15} {'.$gutterLength.'}Lorem ipsum dolor sit amet, consectetur adipiscing elit\.\R' + .'\R' + .' {'.$indentLength.'}--something= {'.$gutterLength.'}Proin sit amet malesuada libero, finibus bibendum tortor\.\R' + .' {'.($indentLength + 17).'} {'.$gutterLength.'}Nulla vitae quam nec orci finibus pharetra\.\R' + .' {'.($indentLength + 17).'} {'.$gutterLength.'}Nam eget blandit dui\.\R$`', + 'color' => '`^ {'.$indentLength.'}\\033\[32m-a {15}\\033\[0m {'.$gutterLength.'}Lorem ipsum dolor sit amet, consectetur adipiscing elit\.\R' + .'\R' + .' {'.$indentLength.'}\\033\[32m--something=\\033\[36m\\033\[32m\\033\[0m {'.$gutterLength.'}Proin sit amet malesuada libero, finibus bibendum tortor\.\R' + .' {'.($indentLength + 17).'} {'.$gutterLength.'}Nulla vitae quam nec orci finibus pharetra\.\R' + .' {'.($indentLength + 17).'} {'.$gutterLength.'}Nam eget blandit dui\.\R$`', + ], + ], + 'Input: text, arg, text; multi-line text gets wrapped' => [ + 'input' => [ + 'single-line-text' => [ + 'text' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + ], + 'argument-description' => [ + 'argument' => '--something', + 'description' => 'Fusce dapibus sodales est eu sodales.', + ], + 'multi-line-text-gets-wrapped' => [ + 'text' => 'Maecenas vulputate ligula vel feugiat finibus. Mauris sem dui, pretium in turpis auctor, consectetur ultrices lorem.', + ], + ], + 'expectedRegex' => [ + 'no-color' => '`^ {'.$indentLength.'}Lorem ipsum dolor sit amet, consectetur adipiscing elit\.\R' + .' {'.$indentLength.'}--something {'.$gutterLength.'}Fusce dapibus sodales est eu sodales\.\R' + .' {'.$indentLength.'}Maecenas vulputate ligula vel feugiat finibus. Mauris sem dui, pretium in\R' + .' {'.$indentLength.'}turpis auctor, consectetur ultrices lorem\.\R$`', + 'color' => '`^ {'.$indentLength.'}Lorem ipsum dolor sit amet, consectetur adipiscing elit\.\R' + .' {'.$indentLength.'}\\033\[32m--something\\033\[0m {'.$gutterLength.'}Fusce dapibus sodales est eu sodales\.\R' + .' {'.$indentLength.'}Maecenas vulputate ligula vel feugiat finibus. Mauris sem dui, pretium in\R' + .' {'.$indentLength.'}turpis auctor, consectetur ultrices lorem\.\R$`', + ], + ], + ]; + // phpcs:enable + + }//end dataPrintCategoryOptions() + + +}//end class