From 16c26f71e411c805af17b24b214d5f347459f45e Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 5 Apr 2021 20:10:27 +0200 Subject: [PATCH] :sparkles: New `Utils\MessageHelper` class This class initially introduces five new utility methods for working with error/warning messages. The class currently contains the following methods: * `addMessage()` - simple method to add either an error or a warning to PHPCS based on an `$isError` parameter. Returns boolean (same as PHPCS natively). Supports all optional parameters supported by PHPCS. * `addFixableMessage()` - simple method to add either a fixable error or a fixable warning to PHPCS based on an `$isError` parameter. Returns boolean (same as PHPCS natively). Supports all optional parameters supported by PHPCS. * `stringToErrorcode()` - to convert an arbitrary text string to an alphanumeric string with underscores. Returns the adjusted text string. This method is intended to pre-empt issues in XML and PHP when arbitrary text strings are used as (part of) an error code. * `hasNewLineSupport()` - to check whether PHPCS can properly handle new lines in violation messages. Prior to PHPCS 3.3.1, new line support in error messages was buggy. Ref: https://github.com/squizlabs/PHP_CodeSniffer/pull/2093 * `showEscapeChars()` - to make the whitespace escape codes used in an arbitrary text string visible. Returns string. Includes dedicated unit tests for each method. --- PHPCSUtils/Utils/MessageHelper.php | 158 ++++++++++++ Tests/Utils/MessageHelper/AddMessageTest.inc | 13 + Tests/Utils/MessageHelper/AddMessageTest.php | 243 ++++++++++++++++++ .../MessageHelper/HasNewLineSupportTest.inc | 4 + .../MessageHelper/HasNewLineSupportTest.php | 166 ++++++++++++ .../MessageHelper/ShowEscapeCharsTest.php | 90 +++++++ .../MessageHelper/StringToErrorcodeTest.php | 59 +++++ 7 files changed, 733 insertions(+) create mode 100644 PHPCSUtils/Utils/MessageHelper.php create mode 100644 Tests/Utils/MessageHelper/AddMessageTest.inc create mode 100644 Tests/Utils/MessageHelper/AddMessageTest.php create mode 100644 Tests/Utils/MessageHelper/HasNewLineSupportTest.inc create mode 100644 Tests/Utils/MessageHelper/HasNewLineSupportTest.php create mode 100644 Tests/Utils/MessageHelper/ShowEscapeCharsTest.php create mode 100644 Tests/Utils/MessageHelper/StringToErrorcodeTest.php diff --git a/PHPCSUtils/Utils/MessageHelper.php b/PHPCSUtils/Utils/MessageHelper.php new file mode 100644 index 00000000..8b181458 --- /dev/null +++ b/PHPCSUtils/Utils/MessageHelper.php @@ -0,0 +1,158 @@ +addError($message, $stackPtr, $code, $data, $severity); + } + + return $phpcsFile->addWarning($message, $stackPtr, $code, $data, $severity); + } + + /** + * Add a PHPCS message to the output stack as either a fixable warning or a fixable error. + * + * @since 1.0.0-alpha4 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param string $message The message. + * @param int $stackPtr The position of the token + * the message relates to. + * @param bool $isError Whether to report the message as an + * 'error' or 'warning'. + * Defaults to true (error). + * @param string $code The error code for the message. + * Defaults to 'Found'. + * @param array $data Optional input for the data replacements. + * @param int $severity Optional. Severity level. Defaults to 0 which will + * translate to the PHPCS default severity level. + * + * @return bool + */ + public static function addFixableMessage( + File $phpcsFile, + $message, + $stackPtr, + $isError = true, + $code = 'Found', + $data = [], + $severity = 0 + ) { + if ($isError === true) { + return $phpcsFile->addFixableError($message, $stackPtr, $code, $data, $severity); + } + + return $phpcsFile->addFixableWarning($message, $stackPtr, $code, $data, $severity); + } + + /** + * Convert an arbitrary text string to an alphanumeric string with underscores. + * + * Pre-empt issues in XML and PHP when arbitrary strings are being used as error codes. + * + * @since 1.0.0-alpha4 + * + * @param string $text Arbitrary text string intended to be used in an error code. + * + * @return string + */ + public static function stringToErrorcode($text) + { + return \preg_replace('`[^a-z0-9_]`i', '_', $text); + } + + /** + * Check whether PHPCS can properly handle new lines in violation messages. + * + * @link https://github.com/squizlabs/PHP_CodeSniffer/pull/2093 + * + * @since 1.0.0-alpha4 + * + * @return bool + */ + public static function hasNewLineSupport() + { + static $supported; + if (isset($supported) === false) { + $supported = \version_compare(Helper::getVersion(), '3.3.1', '>='); + } + + return $supported; + } + + /** + * Make the whitespace escape codes used in an arbitrary text string visible. + * + * At times, it is useful to show a code snippet in an error message. + * If such a code snippet contains new lines and/or tab or space characters, those would be + * displayed as-is in the command-line report, often breaking the layout of the report + * or making the report harder to read. + * + * This method will convert these characters to their escape codes, making them visible in the + * display string without impacting the report layout. + * + * @see \PHPCSUtils\Utils\GetTokensToString Methods to retrieve a multi-token code snippet. + * @see \PHP_CodeSniffer\Util\Common\prepareForOutput() Similar PHPCS native method. + * + * @since 1.0.0-alpha4 + * + * @param string $text Arbitrary text string. + * + * @return string + */ + public static function showEscapeChars($text) + { + $search = ["\n", "\r", "\t"]; + $replace = ['\n', '\r', '\t']; + + return \str_replace($search, $replace, $text); + } +} diff --git a/Tests/Utils/MessageHelper/AddMessageTest.inc b/Tests/Utils/MessageHelper/AddMessageTest.inc new file mode 100644 index 00000000..ce27ac26 --- /dev/null +++ b/Tests/Utils/MessageHelper/AddMessageTest.inc @@ -0,0 +1,13 @@ +getTokens(); + $stackPtr = $this->getTargetToken($testMarker, \T_CONSTANT_ENCAPSED_STRING); + $severity = \mt_rand(5, 10); + $expected['severity'] = $severity; + + $return = MessageHelper::addMessage( + self::$phpcsFile, + 'Message added. Text: %s', + $stackPtr, + $isError, + static::CODE, + [$tokens[$stackPtr]['content']], + $severity + ); + + $this->assertTrue($return); + + $this->verifyRecordedMessages($stackPtr, $isError, $expected); + } + + /** + * Data Provider. + * + * @see testAddMessage() For the array format. + * + * @return array + */ + public function dataAddMessage() + { + return [ + 'add-error' => [ + '/* testAddErrorMessage */', + true, + [ + 'message' => "Message added. Text: 'test 1'", + 'source' => static::CODE, + 'fixable' => false, + ], + ], + 'add-warning' => [ + '/* testAddWarningMessage */', + false, + [ + 'message' => "Message added. Text: 'test 2'", + 'source' => static::CODE, + 'fixable' => false, + ], + ], + ]; + } + + /** + * Test the addFixableMessage wrapper. + * + * @dataProvider dataAddFixableMessage + * @covers \PHPCSUtils\Utils\MessageHelper::addFixableMessage + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param bool $isError Whether to test adding an error or a warning. + * @param array $expected Expected error details. + * + * @return void + */ + public function testAddFixableMessage($testMarker, $isError, $expected) + { + $tokens = self::$phpcsFile->getTokens(); + $stackPtr = $this->getTargetToken($testMarker, \T_CONSTANT_ENCAPSED_STRING); + $severity = \mt_rand(5, 10); + $expected['severity'] = $severity; + + $return = MessageHelper::addFixableMessage( + self::$phpcsFile, + 'Message added. Text: %s', + $stackPtr, + $isError, + static::CODE, + [$tokens[$stackPtr]['content']], + $severity + ); + + // Fixable message recording only returns true when the fixer is enabled (=phpcbf). + $this->assertFalse($return); + + $this->verifyRecordedMessages($stackPtr, $isError, $expected); + } + + /** + * Data Provider. + * + * @see testAddFixableMessage() For the array format. + * + * @return array + */ + public function dataAddFixableMessage() + { + return [ + 'add-fixable-error' => [ + '/* testAddFixableErrorMessage */', + true, + [ + 'message' => "Message added. Text: 'test 3'", + 'source' => static::CODE, + 'fixable' => true, + ], + ], + 'add-fixable-warning' => [ + '/* testAddFixableWarningMessage */', + false, + [ + 'message' => "Message added. Text: 'test 4'", + 'source' => static::CODE, + 'fixable' => true, + ], + ], + ]; + } + + /** + * Helper method to verify the expected message details. + * + * @param int $stackPtr The stack pointer on which the error/warning is expected. + * @param bool $isError Whether to test adding an error or a warning. + * @param array $expected Expected error details. + * + * @return void + */ + protected function verifyRecordedMessages($stackPtr, $isError, $expected) + { + $tokens = self::$phpcsFile->getTokens(); + $errors = self::$phpcsFile->getErrors(); + $warnings = self::$phpcsFile->getWarnings(); + $result = ($isError === true) ? $errors : $warnings; + + /* + * Make sure that no errors/warnings were recorded when the other type is set to be expected. + */ + if ($isError === true) { + $this->assertArrayNotHasKey( + $tokens[$stackPtr]['line'], + $warnings, + 'Expected no warnings on line ' . $tokens[$stackPtr]['line'] . '. At least one found.' + ); + } else { + $this->assertArrayNotHasKey( + $tokens[$stackPtr]['line'], + $errors, + 'Expected no errors on line ' . $tokens[$stackPtr]['line'] . '. At least one found.' + ); + } + + /* + * Make sure the expected array keys for the errors/warnings are available. + */ + $this->assertArrayHasKey( + $tokens[$stackPtr]['line'], + $result, + 'Expected a violation on line ' . $tokens[$stackPtr]['line'] . '. None found.' + ); + + $this->assertArrayHasKey( + $tokens[$stackPtr]['column'], + $result[$tokens[$stackPtr]['line']], + 'Expected a violation on line ' . $tokens[$stackPtr]['line'] . ', column ' + . $tokens[$stackPtr]['column'] . '. None found.' + ); + + $messages = $result[$tokens[$stackPtr]['line']][$tokens[$stackPtr]['column']]; + + // Expect one violation. + $this->assertCount(1, $messages, 'Expected 1 violation, found: ' . \count($messages)); + + $violation = $messages[0]; + + // PHPCS 2.x places `unknownSniff.` before the actual error code for utility tests with a dummy error code. + $violation['source'] = \str_replace('unknownSniff.', '', $violation['source']); + + /* + * Test the violation details. + */ + foreach ($expected as $key => $value) { + $this->assertSame($value, $violation[$key], \ucfirst($key) . ' comparison failed'); + } + } +} diff --git a/Tests/Utils/MessageHelper/HasNewLineSupportTest.inc b/Tests/Utils/MessageHelper/HasNewLineSupportTest.inc new file mode 100644 index 00000000..1c97c6ad --- /dev/null +++ b/Tests/Utils/MessageHelper/HasNewLineSupportTest.inc @@ -0,0 +1,4 @@ +markTestSkipped('PHPCS 2.x: skipping test as new line support won\'t be available anyway.'); + } + + /* + * Set up the expected output. + * phpcs:disable Generic.Files.LineLength.TooLong + */ + if (MessageHelper::hasNewLineSupport() === true) { + $expected = <<<'EOD' +------------------------------------------------------------------------------------------------------------------------ +FOUND 1 ERROR AFFECTING 1 LINE +------------------------------------------------------------------------------------------------------------------------ + 4 | ERROR | Lorem ipsum dolor sit amet, consectetur adipiscing elit. + | | Aenean felis urna, dictum vitae lobortis vitae, maximus nec enim. Etiam euismod placerat efficitur. Nulla + | | eu felis ipsum. + | | Cras vitae ultrices turpis. Ut consectetur ligula in justo tincidunt mattis. + | | + | | Aliquam fermentum magna id venenatis placerat. Curabitur lobortis nulla sit amet consequat fermentum. + | | Aenean malesuada tristique aliquam. Donec eget placerat nisl. + | | + | | Morbi mollis, risus vel venenatis accumsan, urna dolor faucibus risus, ut congue purus augue vel ipsum. + | | Curabitur nec dolor est. Suspendisse nec quam non ligula aliquam tempus. Donec laoreet maximus leo, in + | | eleifend odio interdum vitae. +------------------------------------------------------------------------------------------------------------------------ +EOD; + + // Make sure space on empty line is included (often removed by file editor). + $expected = \str_replace("|\n", "| \n", $expected); + } else { + $expected = <<<'EOD' +------------------------------------------------------------------------------------------------------------------------ +FOUND 1 ERROR AFFECTING 1 LINE +------------------------------------------------------------------------------------------------------------------------ + 4 | ERROR | Lorem ipsum dolor sit amet, consectetur adipiscing elit. + | | Aenean felis urna, dictum vitae + | | lobortis vitae, maximus nec enim. Etiam euismod placerat efficitur. Nulla eu felis ipsum. + | | + | | Cras vitae ultrices turpis. Ut consectetur ligula in justo tincidunt mattis. + | | + | | + | | Aliquam fermentum magna id venenatis placerat. Curabitur lobortis nulla sit amet consequat fermentum. + | | Aenean malesuada tristique aliquam. Donec eget placerat nisl. + | | + | | Morbi mollis, + | | risus vel venenatis accumsan, urna dolor faucibus risus, ut congue purus augue vel ipsum. + | | + | | Curabitur nec dolor est. Suspendisse nec quam non ligula aliquam tempus. Donec laoreet maximus leo, in + | | eleifend odio interdum vitae. +------------------------------------------------------------------------------------------------------------------------ +EOD; + + // Make sure space on select empty lines is included (often removed by file editor). + $expected = \str_replace("mattis.\n | |\n", "mattis.\n | | \n", $expected); + $expected = \str_replace("nisl.\n | |\n", "nisl.\n | | \n", $expected); + } + // phpcs:enable + + $this->expectOutputString($expected); + $this->setOutputCallback([$this, 'normalizeOutput']); + + /* + * Create the error. + */ + $stackPtr = $this->getTargetToken('/* testMessageWithNewLine */', \T_CONSTANT_ENCAPSED_STRING); + + self::$phpcsFile->addError( + // phpcs:ignore Generic.Files.LineLength.TooLong + "Lorem ipsum dolor sit amet, consectetur adipiscing elit.\nAenean felis urna, dictum vitae lobortis vitae, maximus nec enim. Etiam euismod placerat efficitur. Nulla eu felis ipsum.\nCras vitae ultrices turpis. Ut consectetur ligula in justo tincidunt mattis.\n\nAliquam fermentum magna id venenatis placerat. Curabitur lobortis nulla sit amet consequat fermentum. Aenean malesuada tristique aliquam. Donec eget placerat nisl.\n\nMorbi mollis, risus vel venenatis accumsan, urna dolor faucibus risus, ut congue purus augue vel ipsum.\nCurabitur nec dolor est. Suspendisse nec quam non ligula aliquam tempus. Donec laoreet maximus leo, in eleifend odio interdum vitae.", + $stackPtr, + static::CODE + ); + + /* + * Generate the actual output to test. + */ + $config = self::$phpcsFile->config; + $config->colors = false; + $config->reportWidth = 120; + $reporter = new Reporter($config); + $reportClass = new Full(); + $reportData = $reporter->prepareFileReport(self::$phpcsFile); + + $reportClass->generateFileReport( + $reportData, + self::$phpcsFile, + self::$phpcsFile->config->showSources, + $config->reportWidth + ); + } + + /** + * Normalize the output to allow for OS-independent comparison. + * + * @param string $output Generated output. + * + * @return string + */ + public function normalizeOutput($output) + { + // Remove potential color codes. + $output = \preg_replace('`\\033\[[0-9]+m`', '', $output); + + $output = \explode(\PHP_EOL, \trim($output)); + // Remove: line with filename. + \array_shift($output); + + return \implode("\n", $output); + } +} diff --git a/Tests/Utils/MessageHelper/ShowEscapeCharsTest.php b/Tests/Utils/MessageHelper/ShowEscapeCharsTest.php new file mode 100644 index 00000000..89daf0a4 --- /dev/null +++ b/Tests/Utils/MessageHelper/ShowEscapeCharsTest.php @@ -0,0 +1,90 @@ +assertSame($expected, MessageHelper::showEscapeChars($input)); + } + + /** + * Data provider. + * + * @see testShowEscapeChars() For the array format. + * + * @return array + */ + public function dataShowEscapeChars() + { + return [ + 'no-escape-chars' => [ + 'input' => 'if ($var === true) {', + 'expected' => 'if ($var === true) {', + ], + 'has-escape-chars-in-single-quoted-string' => [ + 'input' => 'if ($var === true) {\r\n\t// Do something.\r\n}', + 'expected' => 'if ($var === true) {\r\n\t// Do something.\r\n}', + ], + 'has-real-tabs' => [ + 'input' => '$var = 123;', + 'expected' => '$var\t\t= 123;', + ], + 'has-tab-escape-chars-in-double-quoted-string' => [ + 'input' => "\$var\t\t= 123;", + 'expected' => '$var\t\t= 123;', + ], + 'has-real-new-line' => [ + 'input' => '$foo = 123; +$bar = 456;', + 'expected' => '$foo = 123;\n$bar = 456;', + ], + 'has-new-line-escape-char-in-double-quoted-string' => [ + 'input' => "\$foo = 123;\n\$bar = 456;", + 'expected' => '$foo = 123;\n$bar = 456;', + ], + 'has-real-tab-and-new-lines' => [ + 'input' => 'if ($var === true) { + // Do something. +}', + 'expected' => 'if ($var === true) {\n\t// Do something.\n}', + ], + 'has-tab-and-new-lines-escape-chars-in-double-quoted-string' => [ + 'input' => "if (\$var === true) {\r\n\t// Do something.\r\n}", + 'expected' => 'if ($var === true) {\r\n\t// Do something.\r\n}', + ], + ]; + } +} diff --git a/Tests/Utils/MessageHelper/StringToErrorcodeTest.php b/Tests/Utils/MessageHelper/StringToErrorcodeTest.php new file mode 100644 index 00000000..939b3347 --- /dev/null +++ b/Tests/Utils/MessageHelper/StringToErrorcodeTest.php @@ -0,0 +1,59 @@ +assertSame($expected, MessageHelper::stringToErrorCode($input)); + } + + /** + * Data provider. + * + * @see testStringToErrorCode() For the array format. + * + * @return array + */ + public function dataStringToErrorCode() + { + return [ + 'no-special-chars' => ['dir_name', 'dir_name'], + 'full-stop' => ['soap.wsdl_cache', 'soap_wsdl_cache'], + 'dash-and-space' => ['arbitrary-string with space', 'arbitrary_string_with_space'], + 'no-alphanum-chars' => ['^%*&%*ۈ?', '____________'], + ]; + } +}