NewFlexibleHeredocNowdocSniff.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. <?php
  2. /**
  3. * PHPCompatibility, an external standard for PHP_CodeSniffer.
  4. *
  5. * @package PHPCompatibility
  6. * @copyright 2012-2019 PHPCompatibility Contributors
  7. * @license https://opensource.org/licenses/LGPL-3.0 LGPL3
  8. * @link https://github.com/PHPCompatibility/PHPCompatibility
  9. */
  10. namespace PHPCompatibility\Sniffs\Syntax;
  11. use PHPCompatibility\Sniff;
  12. use PHPCompatibility\PHPCSHelper;
  13. use PHP_CodeSniffer_File as File;
  14. /**
  15. * Detect usage of flexible heredoc/nowdoc and related cross-version incompatibilities.
  16. *
  17. * As of PHP 7.3:
  18. * - The body and the closing marker of a heredoc/nowdoc can be indented;
  19. * - The closing marker no longer needs to be on a line by itself;
  20. * - The heredoc/nowdoc body may no longer contain the closing marker at the
  21. * start of any of its lines.
  22. *
  23. * PHP version 7.3
  24. *
  25. * @link https://www.php.net/manual/en/migration73.new-features.php#migration73.new-features.core.heredoc
  26. * @link https://wiki.php.net/rfc/flexible_heredoc_nowdoc_syntaxes
  27. *
  28. * @since 9.0.0
  29. */
  30. class NewFlexibleHeredocNowdocSniff extends Sniff
  31. {
  32. /**
  33. * Returns an array of tokens this test wants to listen for.
  34. *
  35. * @since 9.0.0
  36. *
  37. * @return array
  38. */
  39. public function register()
  40. {
  41. $targets = array(
  42. \T_END_HEREDOC,
  43. \T_END_NOWDOC,
  44. );
  45. if (version_compare(\PHP_VERSION_ID, '70299', '>') === false) {
  46. // Start identifier of a PHP 7.3 flexible heredoc/nowdoc.
  47. $targets[] = \T_STRING;
  48. }
  49. return $targets;
  50. }
  51. /**
  52. * Processes this test, when one of its tokens is encountered.
  53. *
  54. * @since 9.0.0
  55. *
  56. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  57. * @param int $stackPtr The position of the current token in the
  58. * stack passed in $tokens.
  59. *
  60. * @return void
  61. */
  62. public function process(File $phpcsFile, $stackPtr)
  63. {
  64. /*
  65. * Due to a tokenizer bug which gets hit when the PHP 7.3 heredoc/nowdoc syntax
  66. * is used, this part of the sniff cannot possibly work on PHPCS < 2.6.0.
  67. * See upstream issue #928.
  68. */
  69. if ($this->supportsBelow('7.2') === true && version_compare(PHPCSHelper::getVersion(), '2.6.0', '>=')) {
  70. $this->detectIndentedNonStandAloneClosingMarker($phpcsFile, $stackPtr);
  71. }
  72. $tokens = $phpcsFile->getTokens();
  73. if ($this->supportsAbove('7.3') === true && $tokens[$stackPtr]['code'] !== \T_STRING) {
  74. $this->detectClosingMarkerInBody($phpcsFile, $stackPtr);
  75. }
  76. }
  77. /**
  78. * Detect indented and/or non-stand alone closing markers.
  79. *
  80. * @since 9.0.0
  81. *
  82. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  83. * @param int $stackPtr The position of the current token in the
  84. * stack passed in $tokens.
  85. *
  86. * @return void
  87. */
  88. protected function detectIndentedNonStandAloneClosingMarker(File $phpcsFile, $stackPtr)
  89. {
  90. $tokens = $phpcsFile->getTokens();
  91. $indentError = 'Heredoc/nowdoc with an indented closing marker is not supported in PHP 7.2 or earlier.';
  92. $indentErrorCode = 'IndentedClosingMarker';
  93. $trailingError = 'Having code - other than a semi-colon or new line - after the closing marker of a heredoc/nowdoc is not supported in PHP 7.2 or earlier.';
  94. $trailingErrorCode = 'ClosingMarkerNoNewLine';
  95. if (version_compare(\PHP_VERSION_ID, '70299', '>') === true) {
  96. /*
  97. * Check for indented closing marker.
  98. */
  99. if (ltrim($tokens[$stackPtr]['content']) !== $tokens[$stackPtr]['content']) {
  100. $phpcsFile->addError($indentError, $stackPtr, $indentErrorCode);
  101. }
  102. /*
  103. * Check for tokens after the closing marker.
  104. */
  105. $nextNonWhitespace = $phpcsFile->findNext(array(\T_WHITESPACE, \T_SEMICOLON), ($stackPtr + 1), null, true);
  106. if ($tokens[$stackPtr]['line'] === $tokens[$nextNonWhitespace]['line']) {
  107. $phpcsFile->addError($trailingError, $stackPtr, $trailingErrorCode);
  108. }
  109. } else {
  110. // For PHP < 7.3, we're only interested in T_STRING tokens.
  111. if ($tokens[$stackPtr]['code'] !== \T_STRING) {
  112. return;
  113. }
  114. if (preg_match('`^<<<([\'"]?)([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\1[\r\n]+`', $tokens[$stackPtr]['content'], $matches) !== 1) {
  115. // Not the start of a PHP 7.3 flexible heredoc/nowdoc.
  116. return;
  117. }
  118. $identifier = $matches[2];
  119. for ($i = ($stackPtr + 1); $i <= $phpcsFile->numTokens; $i++) {
  120. if ($tokens[$i]['code'] !== \T_ENCAPSED_AND_WHITESPACE) {
  121. continue;
  122. }
  123. $trimmed = ltrim($tokens[$i]['content']);
  124. if (strpos($trimmed, $identifier) !== 0) {
  125. continue;
  126. }
  127. // OK, we've found the PHP 7.3 flexible heredoc/nowdoc closing marker.
  128. /*
  129. * Check for indented closing marker.
  130. */
  131. if ($trimmed !== $tokens[$i]['content']) {
  132. // Indent found before closing marker.
  133. $phpcsFile->addError($indentError, $i, $indentErrorCode);
  134. }
  135. /*
  136. * Check for tokens after the closing marker.
  137. */
  138. // Remove the identifier.
  139. $afterMarker = substr($trimmed, \strlen($identifier));
  140. // Remove a potential semi-colon at the beginning of what's left of the string.
  141. $afterMarker = ltrim($afterMarker, ';');
  142. // Remove new line characters at the end of the string.
  143. $afterMarker = rtrim($afterMarker, "\r\n");
  144. if ($afterMarker !== '') {
  145. $phpcsFile->addError($trailingError, $i, $trailingErrorCode);
  146. }
  147. break;
  148. }
  149. }
  150. }
  151. /**
  152. * Detect heredoc/nowdoc identifiers at the start of lines in the heredoc/nowdoc body.
  153. *
  154. * @since 9.0.0
  155. *
  156. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  157. * @param int $stackPtr The position of the current token in the
  158. * stack passed in $tokens.
  159. *
  160. * @return void
  161. */
  162. protected function detectClosingMarkerInBody(File $phpcsFile, $stackPtr)
  163. {
  164. $tokens = $phpcsFile->getTokens();
  165. $error = 'The body of a heredoc/nowdoc can not contain the heredoc/nowdoc closing marker as text at the start of a line since PHP 7.3.';
  166. $errorCode = 'ClosingMarkerNoNewLine';
  167. if (version_compare(\PHP_VERSION_ID, '70299', '>') === true) {
  168. $nextNonWhitespace = $phpcsFile->findNext(\T_WHITESPACE, ($stackPtr + 1), null, true, null, true);
  169. if ($nextNonWhitespace === false
  170. || $tokens[$nextNonWhitespace]['code'] === \T_SEMICOLON
  171. || (($tokens[$nextNonWhitespace]['code'] === \T_COMMA
  172. || $tokens[$nextNonWhitespace]['code'] === \T_STRING_CONCAT)
  173. && $tokens[$nextNonWhitespace]['line'] !== $tokens[$stackPtr]['line'])
  174. ) {
  175. // This is most likely a correctly identified closing marker.
  176. return;
  177. }
  178. // The real closing tag has to be before the next heredoc/nowdoc.
  179. $nextHereNowDoc = $phpcsFile->findNext(array(\T_START_HEREDOC, \T_START_NOWDOC), ($stackPtr + 1));
  180. if ($nextHereNowDoc === false) {
  181. $nextHereNowDoc = null;
  182. }
  183. $identifier = trim($tokens[$stackPtr]['content']);
  184. $realClosingMarker = $stackPtr;
  185. while (($realClosingMarker = $phpcsFile->findNext(\T_STRING, ($realClosingMarker + 1), $nextHereNowDoc, false, $identifier)) !== false) {
  186. $prevNonWhitespace = $phpcsFile->findPrevious(\T_WHITESPACE, ($realClosingMarker - 1), null, true);
  187. if ($prevNonWhitespace === false
  188. || $tokens[$prevNonWhitespace]['line'] === $tokens[$realClosingMarker]['line']
  189. ) {
  190. // Marker text found, but not at the start of the line.
  191. continue;
  192. }
  193. // The original T_END_HEREDOC/T_END_NOWDOC was most likely incorrect as we've found
  194. // a possible alternative closing marker.
  195. $phpcsFile->addError($error, $stackPtr, $errorCode);
  196. break;
  197. }
  198. } else {
  199. if (isset($tokens[$stackPtr]['scope_closer'], $tokens[$stackPtr]['scope_opener']) === true
  200. && $tokens[$stackPtr]['scope_closer'] === $stackPtr
  201. ) {
  202. $opener = $tokens[$stackPtr]['scope_opener'];
  203. } else {
  204. // PHPCS < 3.0.2 did not add scope_* values for Nowdocs.
  205. $opener = $phpcsFile->findPrevious(\T_START_NOWDOC, ($stackPtr - 1));
  206. if ($opener === false) {
  207. return;
  208. }
  209. }
  210. $quotedIdentifier = preg_quote($tokens[$stackPtr]['content'], '`');
  211. // Throw an error for each line in the body which starts with the identifier.
  212. for ($i = ($opener + 1); $i < $stackPtr; $i++) {
  213. if (preg_match('`^[ \t]*' . $quotedIdentifier . '\b`', $tokens[$i]['content']) === 1) {
  214. $phpcsFile->addError($error, $i, $errorCode);
  215. }
  216. }
  217. }
  218. }
  219. }