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 @@ += 7.5. + $this->assertIsBool($result); + } else { + // PHPUnit < 7.5. + $this->assertInternalType('bool', $result); + } + + if ($result === false) { + return; + } + + /* + * Test the actual message returned for PHPCS versions which have proper new line support. + */ + + /* + * Set up the expected output. + * phpcs:disable Generic.Files.LineLength.TooLong + */ + $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); + + $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' => ['^%*&%*ۈ?', '____________'], + ]; + } +}