PageRenderTime 28ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/php/PHP_CodeSniffer/src/Tokenizers/Tokenizer.php

http://github.com/jonswar/perl-code-tidyall
PHP | 1646 lines | 1125 code | 229 blank | 292 comment | 331 complexity | c0005843c8ec6833f33296b2c32f1cec MD5 | raw file
Possible License(s): BSD-3-Clause, AGPL-1.0, 0BSD, MIT

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * The base tokenizer class.
  4. *
  5. * @author Greg Sherwood <gsherwood@squiz.net>
  6. * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
  7. * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
  8. */
  9. namespace PHP_CodeSniffer\Tokenizers;
  10. use PHP_CodeSniffer\Exceptions\RuntimeException;
  11. use PHP_CodeSniffer\Util;
  12. abstract class Tokenizer
  13. {
  14. /**
  15. * The config data for the run.
  16. *
  17. * @var \PHP_CodeSniffer\Config
  18. */
  19. protected $config = null;
  20. /**
  21. * The EOL char used in the content.
  22. *
  23. * @var string
  24. */
  25. protected $eolChar = [];
  26. /**
  27. * A token-based representation of the content.
  28. *
  29. * @var array
  30. */
  31. protected $tokens = [];
  32. /**
  33. * The number of tokens in the tokens array.
  34. *
  35. * @var integer
  36. */
  37. protected $numTokens = 0;
  38. /**
  39. * A list of tokens that are allowed to open a scope.
  40. *
  41. * @var array
  42. */
  43. public $scopeOpeners = [];
  44. /**
  45. * A list of tokens that end the scope.
  46. *
  47. * @var array
  48. */
  49. public $endScopeTokens = [];
  50. /**
  51. * Known lengths of tokens.
  52. *
  53. * @var array<int, int>
  54. */
  55. public $knownLengths = [];
  56. /**
  57. * A list of lines being ignored due to error suppression comments.
  58. *
  59. * @var array
  60. */
  61. public $ignoredLines = [];
  62. /**
  63. * Initialise and run the tokenizer.
  64. *
  65. * @param string $content The content to tokenize,
  66. * @param \PHP_CodeSniffer\Config | null $config The config data for the run.
  67. * @param string $eolChar The EOL char used in the content.
  68. *
  69. * @return void
  70. * @throws \PHP_CodeSniffer\Exceptions\TokenizerException If the file appears to be minified.
  71. */
  72. public function __construct($content, $config, $eolChar='\n')
  73. {
  74. $this->eolChar = $eolChar;
  75. $this->config = $config;
  76. $this->tokens = $this->tokenize($content);
  77. if ($config === null) {
  78. return;
  79. }
  80. $this->createPositionMap();
  81. $this->createTokenMap();
  82. $this->createParenthesisNestingMap();
  83. $this->createScopeMap();
  84. $this->createLevelMap();
  85. // Allow the tokenizer to do additional processing if required.
  86. $this->processAdditional();
  87. }//end __construct()
  88. /**
  89. * Checks the content to see if it looks minified.
  90. *
  91. * @param string $content The content to tokenize.
  92. * @param string $eolChar The EOL char used in the content.
  93. *
  94. * @return boolean
  95. */
  96. protected function isMinifiedContent($content, $eolChar='\n')
  97. {
  98. // Minified files often have a very large number of characters per line
  99. // and cause issues when tokenizing.
  100. $numChars = strlen($content);
  101. $numLines = (substr_count($content, $eolChar) + 1);
  102. $average = ($numChars / $numLines);
  103. if ($average > 100) {
  104. return true;
  105. }
  106. return false;
  107. }//end isMinifiedContent()
  108. /**
  109. * Gets the array of tokens.
  110. *
  111. * @return array
  112. */
  113. public function getTokens()
  114. {
  115. return $this->tokens;
  116. }//end getTokens()
  117. /**
  118. * Creates an array of tokens when given some content.
  119. *
  120. * @param string $string The string to tokenize.
  121. *
  122. * @return array
  123. */
  124. abstract protected function tokenize($string);
  125. /**
  126. * Performs additional processing after main tokenizing.
  127. *
  128. * @return void
  129. */
  130. abstract protected function processAdditional();
  131. /**
  132. * Sets token position information.
  133. *
  134. * Can also convert tabs into spaces. Each tab can represent between
  135. * 1 and $width spaces, so this cannot be a straight string replace.
  136. *
  137. * @return void
  138. */
  139. private function createPositionMap()
  140. {
  141. $currColumn = 1;
  142. $lineNumber = 1;
  143. $eolLen = strlen($this->eolChar);
  144. $ignoring = null;
  145. $inTests = defined('PHP_CODESNIFFER_IN_TESTS');
  146. $checkEncoding = false;
  147. if (function_exists('iconv_strlen') === true) {
  148. $checkEncoding = true;
  149. }
  150. $checkAnnotations = $this->config->annotations;
  151. $encoding = $this->config->encoding;
  152. $tabWidth = $this->config->tabWidth;
  153. $tokensWithTabs = [
  154. T_WHITESPACE => true,
  155. T_COMMENT => true,
  156. T_DOC_COMMENT => true,
  157. T_DOC_COMMENT_WHITESPACE => true,
  158. T_DOC_COMMENT_STRING => true,
  159. T_CONSTANT_ENCAPSED_STRING => true,
  160. T_DOUBLE_QUOTED_STRING => true,
  161. T_HEREDOC => true,
  162. T_NOWDOC => true,
  163. T_INLINE_HTML => true,
  164. ];
  165. $this->numTokens = count($this->tokens);
  166. for ($i = 0; $i < $this->numTokens; $i++) {
  167. $this->tokens[$i]['line'] = $lineNumber;
  168. $this->tokens[$i]['column'] = $currColumn;
  169. if (isset($this->knownLengths[$this->tokens[$i]['code']]) === true) {
  170. // There are no tabs in the tokens we know the length of.
  171. $length = $this->knownLengths[$this->tokens[$i]['code']];
  172. $currColumn += $length;
  173. } else if ($tabWidth === 0
  174. || isset($tokensWithTabs[$this->tokens[$i]['code']]) === false
  175. || strpos($this->tokens[$i]['content'], "\t") === false
  176. ) {
  177. // There are no tabs in this content, or we aren't replacing them.
  178. if ($checkEncoding === true) {
  179. // Not using the default encoding, so take a bit more care.
  180. $oldLevel = error_reporting();
  181. error_reporting(0);
  182. $length = iconv_strlen($this->tokens[$i]['content'], $encoding);
  183. error_reporting($oldLevel);
  184. if ($length === false) {
  185. // String contained invalid characters, so revert to default.
  186. $length = strlen($this->tokens[$i]['content']);
  187. }
  188. } else {
  189. $length = strlen($this->tokens[$i]['content']);
  190. }
  191. $currColumn += $length;
  192. } else {
  193. $this->replaceTabsInToken($this->tokens[$i]);
  194. $length = $this->tokens[$i]['length'];
  195. $currColumn += $length;
  196. }//end if
  197. $this->tokens[$i]['length'] = $length;
  198. if (isset($this->knownLengths[$this->tokens[$i]['code']]) === false
  199. && strpos($this->tokens[$i]['content'], $this->eolChar) !== false
  200. ) {
  201. $lineNumber++;
  202. $currColumn = 1;
  203. // Newline chars are not counted in the token length.
  204. $this->tokens[$i]['length'] -= $eolLen;
  205. }
  206. if ($this->tokens[$i]['code'] === T_COMMENT
  207. || $this->tokens[$i]['code'] === T_DOC_COMMENT_STRING
  208. || $this->tokens[$i]['code'] === T_DOC_COMMENT_TAG
  209. || ($inTests === true && $this->tokens[$i]['code'] === T_INLINE_HTML)
  210. ) {
  211. $commentText = ltrim($this->tokens[$i]['content'], " \t/*");
  212. $commentText = rtrim($commentText, " */\t\r\n");
  213. $commentTextLower = strtolower($commentText);
  214. if (strpos($commentText, '@codingStandards') !== false) {
  215. // If this comment is the only thing on the line, it tells us
  216. // to ignore the following line. If the line contains other content
  217. // then we are just ignoring this one single line.
  218. $ownLine = false;
  219. if ($i > 0) {
  220. for ($prev = ($i - 1); $prev >= 0; $prev--) {
  221. if ($this->tokens[$prev]['code'] === T_WHITESPACE) {
  222. continue;
  223. }
  224. break;
  225. }
  226. if ($this->tokens[$prev]['line'] !== $this->tokens[$i]['line']) {
  227. $ownLine = true;
  228. }
  229. }
  230. if ($ignoring === null
  231. && strpos($commentText, '@codingStandardsIgnoreStart') !== false
  232. ) {
  233. $ignoring = ['.all' => true];
  234. if ($ownLine === true) {
  235. $this->ignoredLines[$this->tokens[$i]['line']] = $ignoring;
  236. }
  237. } else if ($ignoring !== null
  238. && strpos($commentText, '@codingStandardsIgnoreEnd') !== false
  239. ) {
  240. if ($ownLine === true) {
  241. $this->ignoredLines[$this->tokens[$i]['line']] = ['.all' => true];
  242. } else {
  243. $this->ignoredLines[$this->tokens[$i]['line']] = $ignoring;
  244. }
  245. $ignoring = null;
  246. } else if ($ignoring === null
  247. && strpos($commentText, '@codingStandardsIgnoreLine') !== false
  248. ) {
  249. $ignoring = ['.all' => true];
  250. if ($ownLine === true) {
  251. $this->ignoredLines[$this->tokens[$i]['line']] = $ignoring;
  252. $this->ignoredLines[($this->tokens[$i]['line'] + 1)] = $ignoring;
  253. } else {
  254. $this->ignoredLines[$this->tokens[$i]['line']] = $ignoring;
  255. }
  256. $ignoring = null;
  257. }//end if
  258. } else if (substr($commentTextLower, 0, 6) === 'phpcs:'
  259. || substr($commentTextLower, 0, 7) === '@phpcs:'
  260. ) {
  261. // If the @phpcs: syntax is being used, strip the @ to make
  262. // comparisons easier.
  263. if ($commentText[0] === '@') {
  264. $commentText = substr($commentText, 1);
  265. $commentTextLower = strtolower($commentText);
  266. }
  267. // If there is a comment on the end, strip it off.
  268. $commentStart = strpos($commentTextLower, ' --');
  269. if ($commentStart !== false) {
  270. $commentText = substr($commentText, 0, $commentStart);
  271. $commentTextLower = strtolower($commentText);
  272. }
  273. // If this comment is the only thing on the line, it tells us
  274. // to ignore the following line. If the line contains other content
  275. // then we are just ignoring this one single line.
  276. $lineHasOtherContent = false;
  277. $lineHasOtherTokens = false;
  278. if ($i > 0) {
  279. for ($prev = ($i - 1); $prev > 0; $prev--) {
  280. if ($this->tokens[$prev]['line'] !== $this->tokens[$i]['line']) {
  281. // Changed lines.
  282. break;
  283. }
  284. if ($this->tokens[$prev]['code'] === T_WHITESPACE
  285. || ($this->tokens[$prev]['code'] === T_INLINE_HTML
  286. && trim($this->tokens[$prev]['content']) === '')
  287. ) {
  288. continue;
  289. }
  290. $lineHasOtherTokens = true;
  291. if ($this->tokens[$prev]['code'] === T_OPEN_TAG) {
  292. continue;
  293. }
  294. $lineHasOtherContent = true;
  295. break;
  296. }//end for
  297. $changedLines = false;
  298. for ($next = $i; $next < $this->numTokens; $next++) {
  299. if ($changedLines === true) {
  300. // Changed lines.
  301. break;
  302. }
  303. if (isset($this->knownLengths[$this->tokens[$next]['code']]) === false
  304. && strpos($this->tokens[$next]['content'], $this->eolChar) !== false
  305. ) {
  306. // Last token on the current line.
  307. $changedLines = true;
  308. }
  309. if ($next === $i) {
  310. continue;
  311. }
  312. if ($this->tokens[$next]['code'] === T_WHITESPACE
  313. || ($this->tokens[$next]['code'] === T_INLINE_HTML
  314. && trim($this->tokens[$next]['content']) === '')
  315. ) {
  316. continue;
  317. }
  318. $lineHasOtherTokens = true;
  319. if ($this->tokens[$next]['code'] === T_CLOSE_TAG) {
  320. continue;
  321. }
  322. $lineHasOtherContent = true;
  323. break;
  324. }//end for
  325. }//end if
  326. if (substr($commentTextLower, 0, 9) === 'phpcs:set') {
  327. // Ignore standards for complete lines that change sniff settings.
  328. if ($lineHasOtherTokens === false) {
  329. $this->ignoredLines[$this->tokens[$i]['line']] = ['.all' => true];
  330. }
  331. // Need to maintain case here, to get the correct sniff code.
  332. $parts = explode(' ', substr($commentText, 10));
  333. if (count($parts) >= 2) {
  334. $sniffParts = explode('.', $parts[0]);
  335. if (count($sniffParts) >= 3) {
  336. $this->tokens[$i]['sniffCode'] = array_shift($parts);
  337. $this->tokens[$i]['sniffProperty'] = array_shift($parts);
  338. $this->tokens[$i]['sniffPropertyValue'] = rtrim(implode(' ', $parts), " */\r\n");
  339. }
  340. }
  341. $this->tokens[$i]['code'] = T_PHPCS_SET;
  342. $this->tokens[$i]['type'] = 'T_PHPCS_SET';
  343. } else if (substr($commentTextLower, 0, 16) === 'phpcs:ignorefile') {
  344. // The whole file will be ignored, but at least set the correct token.
  345. $this->tokens[$i]['code'] = T_PHPCS_IGNORE_FILE;
  346. $this->tokens[$i]['type'] = 'T_PHPCS_IGNORE_FILE';
  347. } else if (substr($commentTextLower, 0, 13) === 'phpcs:disable') {
  348. if ($lineHasOtherContent === false) {
  349. // Completely ignore the comment line.
  350. $this->ignoredLines[$this->tokens[$i]['line']] = ['.all' => true];
  351. }
  352. if ($ignoring === null) {
  353. $ignoring = [];
  354. }
  355. $disabledSniffs = [];
  356. $additionalText = substr($commentText, 14);
  357. if ($additionalText === false) {
  358. $ignoring = ['.all' => true];
  359. } else {
  360. $parts = explode(',', substr($commentText, 13));
  361. foreach ($parts as $sniffCode) {
  362. $sniffCode = trim($sniffCode);
  363. $disabledSniffs[$sniffCode] = true;
  364. $ignoring[$sniffCode] = true;
  365. // This newly disabled sniff might be disabling an existing
  366. // enabled exception that we are tracking.
  367. if (isset($ignoring['.except']) === true) {
  368. foreach (array_keys($ignoring['.except']) as $ignoredSniffCode) {
  369. if ($ignoredSniffCode === $sniffCode
  370. || strpos($ignoredSniffCode, $sniffCode.'.') === 0
  371. ) {
  372. unset($ignoring['.except'][$ignoredSniffCode]);
  373. }
  374. }
  375. if (empty($ignoring['.except']) === true) {
  376. unset($ignoring['.except']);
  377. }
  378. }
  379. }//end foreach
  380. }//end if
  381. $this->tokens[$i]['code'] = T_PHPCS_DISABLE;
  382. $this->tokens[$i]['type'] = 'T_PHPCS_DISABLE';
  383. $this->tokens[$i]['sniffCodes'] = $disabledSniffs;
  384. } else if (substr($commentTextLower, 0, 12) === 'phpcs:enable') {
  385. if ($ignoring !== null) {
  386. $enabledSniffs = [];
  387. $additionalText = substr($commentText, 13);
  388. if ($additionalText === false) {
  389. $ignoring = null;
  390. } else {
  391. $parts = explode(',', substr($commentText, 13));
  392. foreach ($parts as $sniffCode) {
  393. $sniffCode = trim($sniffCode);
  394. $enabledSniffs[$sniffCode] = true;
  395. // This new enabled sniff might remove previously disabled
  396. // sniffs if it is actually a standard or category of sniffs.
  397. foreach (array_keys($ignoring) as $ignoredSniffCode) {
  398. if ($ignoredSniffCode === $sniffCode
  399. || strpos($ignoredSniffCode, $sniffCode.'.') === 0
  400. ) {
  401. unset($ignoring[$ignoredSniffCode]);
  402. }
  403. }
  404. // This new enabled sniff might be able to clear up
  405. // previously enabled sniffs if it is actually a standard or
  406. // category of sniffs.
  407. if (isset($ignoring['.except']) === true) {
  408. foreach (array_keys($ignoring['.except']) as $ignoredSniffCode) {
  409. if ($ignoredSniffCode === $sniffCode
  410. || strpos($ignoredSniffCode, $sniffCode.'.') === 0
  411. ) {
  412. unset($ignoring['.except'][$ignoredSniffCode]);
  413. }
  414. }
  415. }
  416. }//end foreach
  417. if (empty($ignoring) === true) {
  418. $ignoring = null;
  419. } else {
  420. if (isset($ignoring['.except']) === true) {
  421. $ignoring['.except'] += $enabledSniffs;
  422. } else {
  423. $ignoring['.except'] = $enabledSniffs;
  424. }
  425. }
  426. }//end if
  427. if ($lineHasOtherContent === false) {
  428. // Completely ignore the comment line.
  429. $this->ignoredLines[$this->tokens[$i]['line']] = ['.all' => true];
  430. } else {
  431. // The comment is on the same line as the code it is ignoring,
  432. // so respect the new ignore rules.
  433. $this->ignoredLines[$this->tokens[$i]['line']] = $ignoring;
  434. }
  435. $this->tokens[$i]['sniffCodes'] = $enabledSniffs;
  436. }//end if
  437. $this->tokens[$i]['code'] = T_PHPCS_ENABLE;
  438. $this->tokens[$i]['type'] = 'T_PHPCS_ENABLE';
  439. } else if (substr($commentTextLower, 0, 12) === 'phpcs:ignore') {
  440. $ignoreRules = [];
  441. $additionalText = substr($commentText, 13);
  442. if ($additionalText === false) {
  443. $ignoreRules = ['.all' => true];
  444. } else {
  445. $parts = explode(',', substr($commentText, 13));
  446. foreach ($parts as $sniffCode) {
  447. $ignoreRules[trim($sniffCode)] = true;
  448. }
  449. }
  450. $this->tokens[$i]['code'] = T_PHPCS_IGNORE;
  451. $this->tokens[$i]['type'] = 'T_PHPCS_IGNORE';
  452. $this->tokens[$i]['sniffCodes'] = $ignoreRules;
  453. if ($ignoring !== null) {
  454. $ignoreRules += $ignoring;
  455. }
  456. if ($lineHasOtherContent === false) {
  457. // Completely ignore the comment line, and set the following
  458. // line to include the ignore rules we've set.
  459. $this->ignoredLines[$this->tokens[$i]['line']] = ['.all' => true];
  460. $this->ignoredLines[($this->tokens[$i]['line'] + 1)] = $ignoreRules;
  461. } else {
  462. // The comment is on the same line as the code it is ignoring,
  463. // so respect the ignore rules it set.
  464. $this->ignoredLines[$this->tokens[$i]['line']] = $ignoreRules;
  465. }
  466. }//end if
  467. }//end if
  468. }//end if
  469. if ($ignoring !== null && isset($this->ignoredLines[$this->tokens[$i]['line']]) === false) {
  470. $this->ignoredLines[$this->tokens[$i]['line']] = $ignoring;
  471. }
  472. }//end for
  473. // If annotations are being ignored, we clear out all the ignore rules
  474. // but leave the annotations tokenized as normal.
  475. if ($checkAnnotations === false) {
  476. $this->ignoredLines = [];
  477. }
  478. }//end createPositionMap()
  479. /**
  480. * Replaces tabs in original token content with spaces.
  481. *
  482. * Each tab can represent between 1 and $config->tabWidth spaces,
  483. * so this cannot be a straight string replace. The original content
  484. * is placed into an orig_content index and the new token length is also
  485. * set in the length index.
  486. *
  487. * @param array $token The token to replace tabs inside.
  488. * @param string $prefix The character to use to represent the start of a tab.
  489. * @param string $padding The character to use to represent the end of a tab.
  490. * @param int $tabWidth The number of spaces each tab represents.
  491. *
  492. * @return void
  493. */
  494. public function replaceTabsInToken(&$token, $prefix=' ', $padding=' ', $tabWidth=null)
  495. {
  496. $checkEncoding = false;
  497. if (function_exists('iconv_strlen') === true) {
  498. $checkEncoding = true;
  499. }
  500. $currColumn = $token['column'];
  501. if ($tabWidth === null) {
  502. $tabWidth = $this->config->tabWidth;
  503. if ($tabWidth === 0) {
  504. $tabWidth = 1;
  505. }
  506. }
  507. if (rtrim($token['content'], "\t") === '') {
  508. // String only contains tabs, so we can shortcut the process.
  509. $numTabs = strlen($token['content']);
  510. $firstTabSize = ($tabWidth - (($currColumn - 1) % $tabWidth));
  511. $length = ($firstTabSize + ($tabWidth * ($numTabs - 1)));
  512. $newContent = $prefix.str_repeat($padding, ($length - 1));
  513. } else {
  514. // We need to determine the length of each tab.
  515. $tabs = explode("\t", $token['content']);
  516. $numTabs = (count($tabs) - 1);
  517. $tabNum = 0;
  518. $newContent = '';
  519. $length = 0;
  520. foreach ($tabs as $content) {
  521. if ($content !== '') {
  522. $newContent .= $content;
  523. if ($checkEncoding === true) {
  524. // Not using the default encoding, so take a bit more care.
  525. $oldLevel = error_reporting();
  526. error_reporting(0);
  527. $contentLength = iconv_strlen($content, $this->config->encoding);
  528. error_reporting($oldLevel);
  529. if ($contentLength === false) {
  530. // String contained invalid characters, so revert to default.
  531. $contentLength = strlen($content);
  532. }
  533. } else {
  534. $contentLength = strlen($content);
  535. }
  536. $currColumn += $contentLength;
  537. $length += $contentLength;
  538. }
  539. // The last piece of content does not have a tab after it.
  540. if ($tabNum === $numTabs) {
  541. break;
  542. }
  543. // Process the tab that comes after the content.
  544. $lastCurrColumn = $currColumn;
  545. $tabNum++;
  546. // Move the pointer to the next tab stop.
  547. if (($currColumn % $tabWidth) === 0) {
  548. // This is the first tab, and we are already at a
  549. // tab stop, so this tab counts as a single space.
  550. $currColumn++;
  551. } else {
  552. $currColumn++;
  553. while (($currColumn % $tabWidth) !== 0) {
  554. $currColumn++;
  555. }
  556. $currColumn++;
  557. }
  558. $length += ($currColumn - $lastCurrColumn);
  559. $newContent .= $prefix.str_repeat($padding, ($currColumn - $lastCurrColumn - 1));
  560. }//end foreach
  561. }//end if
  562. $token['orig_content'] = $token['content'];
  563. $token['content'] = $newContent;
  564. $token['length'] = $length;
  565. }//end replaceTabsInToken()
  566. /**
  567. * Creates a map of brackets positions.
  568. *
  569. * @return void
  570. */
  571. private function createTokenMap()
  572. {
  573. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  574. echo "\t*** START TOKEN MAP ***".PHP_EOL;
  575. }
  576. $squareOpeners = [];
  577. $curlyOpeners = [];
  578. $this->numTokens = count($this->tokens);
  579. $openers = [];
  580. $openOwner = null;
  581. for ($i = 0; $i < $this->numTokens; $i++) {
  582. /*
  583. Parenthesis mapping.
  584. */
  585. if (isset(Util\Tokens::$parenthesisOpeners[$this->tokens[$i]['code']]) === true) {
  586. $this->tokens[$i]['parenthesis_opener'] = null;
  587. $this->tokens[$i]['parenthesis_closer'] = null;
  588. $this->tokens[$i]['parenthesis_owner'] = $i;
  589. $openOwner = $i;
  590. } else if ($this->tokens[$i]['code'] === T_OPEN_PARENTHESIS) {
  591. $openers[] = $i;
  592. $this->tokens[$i]['parenthesis_opener'] = $i;
  593. if ($openOwner !== null) {
  594. $this->tokens[$openOwner]['parenthesis_opener'] = $i;
  595. $this->tokens[$i]['parenthesis_owner'] = $openOwner;
  596. $openOwner = null;
  597. }
  598. } else if ($this->tokens[$i]['code'] === T_CLOSE_PARENTHESIS) {
  599. // Did we set an owner for this set of parenthesis?
  600. $numOpeners = count($openers);
  601. if ($numOpeners !== 0) {
  602. $opener = array_pop($openers);
  603. if (isset($this->tokens[$opener]['parenthesis_owner']) === true) {
  604. $owner = $this->tokens[$opener]['parenthesis_owner'];
  605. $this->tokens[$owner]['parenthesis_closer'] = $i;
  606. $this->tokens[$i]['parenthesis_owner'] = $owner;
  607. }
  608. $this->tokens[$i]['parenthesis_opener'] = $opener;
  609. $this->tokens[$i]['parenthesis_closer'] = $i;
  610. $this->tokens[$opener]['parenthesis_closer'] = $i;
  611. }
  612. }//end if
  613. /*
  614. Bracket mapping.
  615. */
  616. switch ($this->tokens[$i]['code']) {
  617. case T_OPEN_SQUARE_BRACKET:
  618. $squareOpeners[] = $i;
  619. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  620. echo str_repeat("\t", count($squareOpeners));
  621. echo str_repeat("\t", count($curlyOpeners));
  622. echo "=> Found square bracket opener at $i".PHP_EOL;
  623. }
  624. break;
  625. case T_OPEN_CURLY_BRACKET:
  626. if (isset($this->tokens[$i]['scope_closer']) === false) {
  627. $curlyOpeners[] = $i;
  628. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  629. echo str_repeat("\t", count($squareOpeners));
  630. echo str_repeat("\t", count($curlyOpeners));
  631. echo "=> Found curly bracket opener at $i".PHP_EOL;
  632. }
  633. }
  634. break;
  635. case T_CLOSE_SQUARE_BRACKET:
  636. if (empty($squareOpeners) === false) {
  637. $opener = array_pop($squareOpeners);
  638. $this->tokens[$i]['bracket_opener'] = $opener;
  639. $this->tokens[$i]['bracket_closer'] = $i;
  640. $this->tokens[$opener]['bracket_opener'] = $opener;
  641. $this->tokens[$opener]['bracket_closer'] = $i;
  642. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  643. echo str_repeat("\t", count($squareOpeners));
  644. echo str_repeat("\t", count($curlyOpeners));
  645. echo "\t=> Found square bracket closer at $i for $opener".PHP_EOL;
  646. }
  647. }
  648. break;
  649. case T_CLOSE_CURLY_BRACKET:
  650. if (empty($curlyOpeners) === false
  651. && isset($this->tokens[$i]['scope_opener']) === false
  652. ) {
  653. $opener = array_pop($curlyOpeners);
  654. $this->tokens[$i]['bracket_opener'] = $opener;
  655. $this->tokens[$i]['bracket_closer'] = $i;
  656. $this->tokens[$opener]['bracket_opener'] = $opener;
  657. $this->tokens[$opener]['bracket_closer'] = $i;
  658. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  659. echo str_repeat("\t", count($squareOpeners));
  660. echo str_repeat("\t", count($curlyOpeners));
  661. echo "\t=> Found curly bracket closer at $i for $opener".PHP_EOL;
  662. }
  663. }
  664. break;
  665. default:
  666. continue 2;
  667. }//end switch
  668. }//end for
  669. // Cleanup for any openers that we didn't find closers for.
  670. // This typically means there was a syntax error breaking things.
  671. foreach ($openers as $opener) {
  672. unset($this->tokens[$opener]['parenthesis_opener']);
  673. unset($this->tokens[$opener]['parenthesis_owner']);
  674. }
  675. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  676. echo "\t*** END TOKEN MAP ***".PHP_EOL;
  677. }
  678. }//end createTokenMap()
  679. /**
  680. * Creates a map for the parenthesis tokens that surround other tokens.
  681. *
  682. * @return void
  683. */
  684. private function createParenthesisNestingMap()
  685. {
  686. $map = [];
  687. for ($i = 0; $i < $this->numTokens; $i++) {
  688. if (isset($this->tokens[$i]['parenthesis_opener']) === true
  689. && $i === $this->tokens[$i]['parenthesis_opener']
  690. ) {
  691. if (empty($map) === false) {
  692. $this->tokens[$i]['nested_parenthesis'] = $map;
  693. }
  694. if (isset($this->tokens[$i]['parenthesis_closer']) === true) {
  695. $map[$this->tokens[$i]['parenthesis_opener']]
  696. = $this->tokens[$i]['parenthesis_closer'];
  697. }
  698. } else if (isset($this->tokens[$i]['parenthesis_closer']) === true
  699. && $i === $this->tokens[$i]['parenthesis_closer']
  700. ) {
  701. array_pop($map);
  702. if (empty($map) === false) {
  703. $this->tokens[$i]['nested_parenthesis'] = $map;
  704. }
  705. } else {
  706. if (empty($map) === false) {
  707. $this->tokens[$i]['nested_parenthesis'] = $map;
  708. }
  709. }//end if
  710. }//end for
  711. }//end createParenthesisNestingMap()
  712. /**
  713. * Creates a scope map of tokens that open scopes.
  714. *
  715. * @return void
  716. * @see recurseScopeMap()
  717. */
  718. private function createScopeMap()
  719. {
  720. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  721. echo "\t*** START SCOPE MAP ***".PHP_EOL;
  722. }
  723. for ($i = 0; $i < $this->numTokens; $i++) {
  724. // Check to see if the current token starts a new scope.
  725. if (isset($this->scopeOpeners[$this->tokens[$i]['code']]) === true) {
  726. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  727. $type = $this->tokens[$i]['type'];
  728. $content = Util\Common::prepareForOutput($this->tokens[$i]['content']);
  729. echo "\tStart scope map at $i:$type => $content".PHP_EOL;
  730. }
  731. if (isset($this->tokens[$i]['scope_condition']) === true) {
  732. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  733. echo "\t* already processed, skipping *".PHP_EOL;
  734. }
  735. continue;
  736. }
  737. $i = $this->recurseScopeMap($i);
  738. }//end if
  739. }//end for
  740. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  741. echo "\t*** END SCOPE MAP ***".PHP_EOL;
  742. }
  743. }//end createScopeMap()
  744. /**
  745. * Recurses though the scope openers to build a scope map.
  746. *
  747. * @param int $stackPtr The position in the stack of the token that
  748. * opened the scope (eg. an IF token or FOR token).
  749. * @param int $depth How many scope levels down we are.
  750. * @param int $ignore How many curly braces we are ignoring.
  751. *
  752. * @return int The position in the stack that closed the scope.
  753. */
  754. private function recurseScopeMap($stackPtr, $depth=1, &$ignore=0)
  755. {
  756. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  757. echo str_repeat("\t", $depth);
  758. echo "=> Begin scope map recursion at token $stackPtr with depth $depth".PHP_EOL;
  759. }
  760. $opener = null;
  761. $currType = $this->tokens[$stackPtr]['code'];
  762. $startLine = $this->tokens[$stackPtr]['line'];
  763. // We will need this to restore the value if we end up
  764. // returning a token ID that causes our calling function to go back
  765. // over already ignored braces.
  766. $originalIgnore = $ignore;
  767. // If the start token for this scope opener is the same as
  768. // the scope token, we have already found our opener.
  769. if (isset($this->scopeOpeners[$currType]['start'][$currType]) === true) {
  770. $opener = $stackPtr;
  771. }
  772. for ($i = ($stackPtr + 1); $i < $this->numTokens; $i++) {
  773. $tokenType = $this->tokens[$i]['code'];
  774. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  775. $type = $this->tokens[$i]['type'];
  776. $line = $this->tokens[$i]['line'];
  777. $content = Util\Common::prepareForOutput($this->tokens[$i]['content']);
  778. echo str_repeat("\t", $depth);
  779. echo "Process token $i on line $line [";
  780. if ($opener !== null) {
  781. echo "opener:$opener;";
  782. }
  783. if ($ignore > 0) {
  784. echo "ignore=$ignore;";
  785. }
  786. echo "]: $type => $content".PHP_EOL;
  787. }//end if
  788. // Very special case for IF statements in PHP that can be defined without
  789. // scope tokens. E.g., if (1) 1; 1 ? (1 ? 1 : 1) : 1;
  790. // If an IF statement below this one has an opener but no
  791. // keyword, the opener will be incorrectly assigned to this IF statement.
  792. // The same case also applies to USE statements, which don't have to have
  793. // openers, so a following USE statement can cause an incorrect brace match.
  794. if (($currType === T_IF || $currType === T_ELSE || $currType === T_USE)
  795. && $opener === null
  796. && ($this->tokens[$i]['code'] === T_SEMICOLON
  797. || $this->tokens[$i]['code'] === T_CLOSE_TAG)
  798. ) {
  799. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  800. $type = $this->tokens[$stackPtr]['type'];
  801. echo str_repeat("\t", $depth);
  802. if ($this->tokens[$i]['code'] === T_SEMICOLON) {
  803. $closerType = 'semicolon';
  804. } else {
  805. $closerType = 'close tag';
  806. }
  807. echo "=> Found $closerType before scope opener for $stackPtr:$type, bailing".PHP_EOL;
  808. }
  809. return $i;
  810. }
  811. // Special case for PHP control structures that have no braces.
  812. // If we find a curly brace closer before we find the opener,
  813. // we're not going to find an opener. That closer probably belongs to
  814. // a control structure higher up.
  815. if ($opener === null
  816. && $ignore === 0
  817. && $tokenType === T_CLOSE_CURLY_BRACKET
  818. && isset($this->scopeOpeners[$currType]['end'][$tokenType]) === true
  819. ) {
  820. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  821. $type = $this->tokens[$stackPtr]['type'];
  822. echo str_repeat("\t", $depth);
  823. echo "=> Found curly brace closer before scope opener for $stackPtr:$type, bailing".PHP_EOL;
  824. }
  825. return ($i - 1);
  826. }
  827. if ($opener !== null
  828. && (isset($this->tokens[$i]['scope_opener']) === false
  829. || $this->scopeOpeners[$this->tokens[$stackPtr]['code']]['shared'] === true)
  830. && isset($this->scopeOpeners[$currType]['end'][$tokenType]) === true
  831. ) {
  832. if ($ignore > 0 && $tokenType === T_CLOSE_CURLY_BRACKET) {
  833. // The last opening bracket must have been for a string
  834. // offset or alike, so let's ignore it.
  835. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  836. echo str_repeat("\t", $depth);
  837. echo '* finished ignoring curly brace *'.PHP_EOL;
  838. }
  839. $ignore--;
  840. continue;
  841. } else if ($this->tokens[$opener]['code'] === T_OPEN_CURLY_BRACKET
  842. && $tokenType !== T_CLOSE_CURLY_BRACKET
  843. ) {
  844. // The opener is a curly bracket so the closer must be a curly bracket as well.
  845. // We ignore this closer to handle cases such as T_ELSE or T_ELSEIF being considered
  846. // a closer of T_IF when it should not.
  847. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  848. $type = $this->tokens[$stackPtr]['type'];
  849. echo str_repeat("\t", $depth);
  850. echo "=> Ignoring non-curly scope closer for $stackPtr:$type".PHP_EOL;
  851. }
  852. } else {
  853. $scopeCloser = $i;
  854. $todo = [
  855. $stackPtr,
  856. $opener,
  857. ];
  858. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  859. $type = $this->tokens[$stackPtr]['type'];
  860. $closerType = $this->tokens[$scopeCloser]['type'];
  861. echo str_repeat("\t", $depth);
  862. echo "=> Found scope closer ($scopeCloser:$closerType) for $stackPtr:$type".PHP_EOL;
  863. }
  864. $validCloser = true;
  865. if (($this->tokens[$stackPtr]['code'] === T_IF || $this->tokens[$stackPtr]['code'] === T_ELSEIF)
  866. && ($tokenType === T_ELSE || $tokenType === T_ELSEIF)
  867. ) {
  868. // To be a closer, this token must have an opener.
  869. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  870. echo str_repeat("\t", $depth);
  871. echo "* closer needs to be tested *".PHP_EOL;
  872. }
  873. $i = self::recurseScopeMap($i, ($depth + 1), $ignore);
  874. if (isset($this->tokens[$scopeCloser]['scope_opener']) === false) {
  875. $validCloser = false;
  876. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  877. echo str_repeat("\t", $depth);
  878. echo "* closer is not valid (no opener found) *".PHP_EOL;
  879. }
  880. } else if ($this->tokens[$this->tokens[$scopeCloser]['scope_opener']]['code'] !== $this->tokens[$opener]['code']) {
  881. $validCloser = false;
  882. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  883. echo str_repeat("\t", $depth);
  884. $type = $this->tokens[$this->tokens[$scopeCloser]['scope_opener']]['type'];
  885. $openerType = $this->tokens[$opener]['type'];
  886. echo "* closer is not valid (mismatched opener type; $type != $openerType) *".PHP_EOL;
  887. }
  888. } else if (PHP_CODESNIFFER_VERBOSITY > 1) {
  889. echo str_repeat("\t", $depth);
  890. echo "* closer was valid *".PHP_EOL;
  891. }
  892. } else {
  893. // The closer was not processed, so we need to
  894. // complete that token as well.
  895. $todo[] = $scopeCloser;
  896. }//end if
  897. if ($validCloser === true) {
  898. foreach ($todo as $token) {
  899. $this->tokens[$token]['scope_condition'] = $stackPtr;
  900. $this->tokens[$token]['scope_opener'] = $opener;
  901. $this->tokens[$token]['scope_closer'] = $scopeCloser;
  902. }
  903. if ($this->scopeOpeners[$this->tokens[$stackPtr]['code']]['shared'] === true) {
  904. // As we are going back to where we started originally, restore
  905. // the ignore value back to its original value.
  906. $ignore = $originalIgnore;
  907. return $opener;
  908. } else if ($scopeCloser === $i
  909. && isset($this->scopeOpeners[$tokenType]) === true
  910. ) {
  911. // Unset scope_condition here or else the token will appear to have
  912. // already been processed, and it will be skipped. Normally we want that,
  913. // but in this case, the token is both a closer and an opener, so
  914. // it needs to act like an opener. This is also why we return the
  915. // token before this one; so the closer has a chance to be processed
  916. // a second time, but as an opener.
  917. unset($this->tokens[$scopeCloser]['scope_condition']);
  918. return ($i - 1);
  919. } else {
  920. return $i;
  921. }
  922. } else {
  923. continue;
  924. }//end if
  925. }//end if
  926. }//end if
  927. // Is this an opening condition ?
  928. if (isset($this->scopeOpeners[$tokenType]) === true) {
  929. if ($opener === null) {
  930. if ($tokenType === T_USE) {
  931. // PHP use keywords are special because they can be
  932. // used as blocks but also inline in function definitions.
  933. // So if we find them nested inside another opener, just skip them.
  934. continue;
  935. }
  936. if ($tokenType === T_FUNCTION
  937. && $this->tokens[$stackPtr]['code'] !== T_FUNCTION
  938. ) {
  939. // Probably a closure, so process it manually.
  940. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  941. $type = $this->tokens[$stackPtr]['type'];
  942. echo str_repeat("\t", $depth);
  943. echo "=> Found function before scope opener for $stackPtr:$type, processing manually".PHP_EOL;
  944. }
  945. if (isset($this->tokens[$i]['scope_closer']) === true) {
  946. // We've already processed this closure.
  947. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  948. echo str_repeat("\t", $depth);
  949. echo '* already processed, skipping *'.PHP_EOL;
  950. }
  951. $i = $this->tokens[$i]['scope_closer'];
  952. continue;
  953. }
  954. $i = self::recurseScopeMap($i, ($depth + 1), $ignore);
  955. continue;
  956. }//end if
  957. if ($tokenType === T_CLASS) {
  958. // Probably an anonymous class inside another anonymous class,
  959. // so process it manually.
  960. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  961. $type = $this->tokens[$stackPtr]['type'];
  962. echo str_repeat("\t", $depth);
  963. echo "=> Found class before scope opener for $stackPtr:$type, processing manually".PHP_EOL;
  964. }
  965. if (isset($this->tokens[$i]['scope_closer']) === true) {
  966. // We've already processed this anon class.
  967. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  968. echo str_repeat("\t", $depth);
  969. echo '* already processed, skipping *'.PHP_EOL;
  970. }
  971. $i = $this->tokens[$i]['scope_closer'];
  972. continue;
  973. }
  974. $i = self::recurseScopeMap($i, ($depth + 1), $ignore);
  975. continue;
  976. }//end if
  977. // Found another opening condition but still haven't
  978. // found our opener, so we are never going to find one.
  979. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  980. $type = $this->tokens[$stackPtr]['type'];
  981. echo str_repeat("\t", $depth);
  982. echo "=> Found new opening condition before scope opener for $stackPtr:$type, ";
  983. }
  984. if (($this->tokens[$stackPtr]['code'] === T_IF
  985. || $this->tokens[$stackPtr]['code'] === T_ELSEIF
  986. || $this->tokens[$stackPtr]['code'] === T_ELSE)
  987. && ($this->tokens[$i]['code'] === T_ELSE
  988. || $this->tokens[$i]['code'] === T_ELSEIF)
  989. ) {
  990. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  991. echo "continuing".PHP_EOL;
  992. }
  993. return ($i - 1);
  994. } else {
  995. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  996. echo "backtracking".PHP_EOL;
  997. }
  998. return $stackPtr;
  999. }
  1000. }//end if
  1001. if (PHP_CODESNIFFER_VERBOSITY > 1) {
  1002. echo str_repeat("\t", $depth);
  1003. echo '* token is an opening condition *'.PHP

Large files files are truncated, but you can click here to view the full file