Repository: PHPCSStandards/PHP_CodeSniffer (php)

Issue:
PHP fatal error when user passes an invalid generator
## Describe the bug

PHPCS throws a fatal error when the user passes an invalid generator.

### To reproduce
Steps to reproduce the behavior:
1. Run `phpcs test.php --generator=InvalidGenerator --standard=Generic`
2. See the error message displayed
```bash
$ phpcs test.php --generator=InvalidGenerator --standard=Generic          
PHP Fatal error:  Uncaught Error: Class "PHP_CodeSniffer\Generators\InvalidGenerator" not found in src/Runner.php:98
Stack trace:
#0 bin/phpcs(14): PHP_CodeSniffer\Runner->runPHPCS()
#1 {main}
  thrown in src/Runner.php on line 98
```

## Expected behavior

PHPCS should display a user-friendly error message informing the user that the generator does not exist and potentially listing the available generators.

## Please confirm

- [x] I have searched the issue list and am not opening a duplicate issue.
- [x] I have read the [Contribution Guidelines](https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/.github/CONTRIBUTING.md) and this is not a [support question](https://github.com/PHPCSStandards/PHP_CodeSniffer/discussions).
- [x] I confirm that this bug is a bug in PHP_CodeSniffer and not in one of the external standards.
- [x] I have verified the issue still exists in the `master` branch of PHP_CodeSniffer.


Pull request description:
Config: display user-friendly message when invalid generator is passed
# Description

This PR improves how `Config::processLongArgument()` handles the `--generator` parameter to display a user-friendly message if an invalid generator name is passed. Before, an invalid generator name caused a fatal error.

I have checked, and it is not possible to add custom generators using third-party code (at least not without adding files to the PHPCS directory). This is because the `Runner` class only loads generators found in the `src/Generator/` directory:

https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/3d14d004265ac6e38f3e161316cf51b7b4812414/src/Runner.php#L97-L98

So, it should be fine to limit the list of valid generators added in this PR to only the three generators provided by PHPCS.

## Suggested changelog entry
Config: display a user-friendly message if an invalid generator name is passed in the command line

## Related issues/external references

Fixes #709 


## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix _(non-breaking change which fixes an issue)_
- [x] New feature _(non-breaking change which adds functionality)_
- [ ] Breaking change _(fix or feature that would cause existing functionality to change)_
    - [ ] This change is only breaking for integrators, not for external standards or end-users.
- [ ] Documentation improvement


## PR checklist
<!-- Go over all the following points, and put an `x` in all the boxes that apply. -->
- [x] I have checked there is no other PR open for the same change.
- [x] I have read the [Contribution Guidelines](https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/.github/CONTRIBUTING.md).
- [x] I grant the project the right to include and distribute the code under the BSD-3-Clause license (and I have the right to grant these rights).
- [x] I have added tests to cover my changes.
- [x] I have verified that the code complies with the projects coding standards.
- [ ] \[Required for new sniffs\] I have added XML documentation for the sniff.

Patch:
diff --git a/src/Config.php b/src/Config.php
index 8d57cd3f1..28d64835e 100644
--- a/src/Config.php
+++ b/src/Config.php
@@ -170,6 +170,21 @@ class Config
      */
     private $cliArgs = [];
 
+    /**
+     * A list of valid generators.
+     *
+     * {@internal Once support for PHP < 5.6 is dropped, this property should be refactored into a
+     * class constant.}
+     *
+     * @var array<string, string> Keys are the lowercase version of the generator name, while values
+     *                            are the associated PHP generator class.
+     */
+    private $validGenerators = [
+        'text'     => 'Text',
+        'html'     => 'HTML',
+        'markdown' => 'Markdown',
+    ];
+
     /**
      * Command line values that the user has supplied directly.
      *
@@ -1215,7 +1230,22 @@ class Config
                     break;
                 }
 
-                $this->generator = substr($arg, 10);
+                $generatorName          = substr($arg, 10);
+                $lowerCaseGeneratorName = strtolower($generatorName);
+
+                if (isset($this->validGenerators[$lowerCaseGeneratorName]) === false) {
+                    $validOptions = implode(', ', $this->validGenerators);
+                    $validOptions = substr_replace($validOptions, ' and', strrpos($validOptions, ','), 1);
+                    $error        = sprintf(
+                        'ERROR: "%s" is not a valid generator. The following generators are supported: %s.'.PHP_EOL.PHP_EOL,
+                        $generatorName,
+                        $validOptions
+                    );
+                    $error       .= $this->printShortUsage(true);
+                    throw new DeepExitException($error, 3);
+                }
+
+                $this->generator = $this->validGenerators[$lowerCaseGeneratorName];
                 self::$overriddenDefaults['generator'] = true;
             } else if (substr($arg, 0, 9) === 'encoding=') {
                 if (isset(self::$overriddenDefaults['encoding']) === true) {


Test patch:
diff --git a/tests/Core/Config/GeneratorArgTest.php b/tests/Core/Config/GeneratorArgTest.php
index f0b52db15..f0fffd033 100644
--- a/tests/Core/Config/GeneratorArgTest.php
+++ b/tests/Core/Config/GeneratorArgTest.php
@@ -23,37 +23,59 @@ final class GeneratorArgTest extends TestCase
     /**
      * Ensure that the generator property is set when the parameter is passed a valid value.
      *
-     * @param string $generatorName Generator name.
+     * @param string $argumentValue         Generator name passed on the command line.
+     * @param string $expectedPropertyValue Expected value of the generator property.
      *
-     * @dataProvider dataGeneratorNames
+     * @dataProvider dataValidGeneratorNames
      *
      * @return void
      */
-    public function testGenerators($generatorName)
+    public function testValidGenerators($argumentValue, $expectedPropertyValue)
     {
-        $config = new ConfigDouble(["--generator=$generatorName"]);
+        $config = new ConfigDouble(["--generator=$argumentValue"]);
 
-        $this->assertSame($generatorName, $config->generator);
+        $this->assertSame($expectedPropertyValue, $config->generator);
 
-    }//end testGenerators()
+    }//end testValidGenerators()
 
 
     /**
-     * Data provider for testGenerators().
+     * Data provider for testValidGenerators().
      *
-     * @see self::testGenerators()
+     * @see self::testValidGenerators()
      *
      * @return array<int, array<string>>
      */
-    public static function dataGeneratorNames()
+    public static function dataValidGeneratorNames()
     {
         return [
-            ['Text'],
-            ['HTML'],
-            ['Markdown'],
+            'Text generator passed'            => [
+                'argumentValue'         => 'Text',
+                'expectedPropertyValue' => 'Text',
+            ],
+            'HTML generator passed'            => [
+                'argumentValue'         => 'HTML',
+                'expectedPropertyValue' => 'HTML',
+            ],
+            'Markdown generator passed'        => [
+                'argumentValue'         => 'Markdown',
+                'expectedPropertyValue' => 'Markdown',
+            ],
+            'Uppercase Text generator passed'  => [
+                'argumentValue'         => 'TEXT',
+                'expectedPropertyValue' => 'Text',
+            ],
+            'Mixed case Text generator passed' => [
+                'argumentValue'         => 'tEXt',
+                'expectedPropertyValue' => 'Text',
+            ],
+            'Lowercase HTML generator passed'  => [
+                'argumentValue'         => 'html',
+                'expectedPropertyValue' => 'HTML',
+            ],
         ];
 
-    }//end dataGeneratorNames()
+    }//end dataValidGeneratorNames()
 
 
     /**
@@ -76,4 +98,50 @@ final class GeneratorArgTest extends TestCase
     }//end testOnlySetOnce()
 
 
+    /**
+     * Ensure that an exception is thrown for an invalid generator.
+     *
+     * @param string $generatorName Generator name.
+     *
+     * @dataProvider dataInvalidGeneratorNames
+     *
+     * @return void
+     */
+    public function testInvalidGenerator($generatorName)
+    {
+        $exception = 'PHP_CodeSniffer\Exceptions\DeepExitException';
+        $message   = 'ERROR: "'.$generatorName.'" is not a valid generator. The following generators are supported: Text, HTML and Markdown.';
+
+        if (method_exists($this, 'expectException') === true) {
+            // PHPUnit 5+.
+            $this->expectException($exception);
+            $this->expectExceptionMessage($message);
+        } else {
+            // PHPUnit 4.
+            $this->setExpectedException($exception, $message);
+        }
+
+        new ConfigDouble(["--generator={$generatorName}"]);
+
+    }//end testInvalidGenerator()
+
+
+    /**
+     * Data provider for testInvalidGenerator().
+     *
+     * @see self::testInvalidGenerator()
+     *
+     * @return array<int, array<string>>
+     */
+    public static function dataInvalidGeneratorNames()
+    {
+        return [
+            ['InvalidGenerator'],
+            ['Text,HTML'],
+            [''],
+        ];
+
+    }//end dataInvalidGeneratorNames()
+
+
 }//end class