/** * Processes this test, when one of its tokens is encountered. * * @param PHP_CodeSniffer_File $phpcsFile The current file being processed. * @param int $stackPtr The position of the current token in the * stack passed in $tokens. * * @return void */ public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); if (isset($tokens[$stackPtr]['scope_opener']) === false) { $error = 'Possible parse error: %s missing opening or closing brace'; $data = array($tokens[$stackPtr]['content']); $phpcsFile->addWarning($error, $stackPtr, 'MissingBrace', $data); return; } // Determine the name of the class or interface. Note that we cannot // simply look for the first T_STRING because a class name // starting with the number will be multiple tokens. $opener = $tokens[$stackPtr]['scope_opener']; $nameStart = $phpcsFile->findNext(T_WHITESPACE, $stackPtr + 1, $opener, true); $nameEnd = $phpcsFile->findNext(T_WHITESPACE, $nameStart, $opener); if ($nameEnd === false) { $name = $tokens[$nameStart]['content']; } else { $name = trim($phpcsFile->getTokensAsString($nameStart, $nameEnd - $nameStart)); } // Check for camel caps format. $valid = Common::isCamelCaps($name, true, true, false); if ($valid === false) { $type = ucfirst($tokens[$stackPtr]['content']); $error = '%s name "%s" is not in camel caps format'; $data = array($type, $name); $phpcsFile->addError($error, $stackPtr, 'NotCamelCaps', $data); $phpcsFile->recordMetric($stackPtr, 'CamelCase class name', 'no'); } else { $phpcsFile->recordMetric($stackPtr, 'CamelCase class name', 'yes'); } }
/** * Processes the tokens within the scope. * * @param PHP_CodeSniffer_File $phpcsFile The file being processed. * @param int $stackPtr The position where this token was * found. * @param int $currScope The position of the current scope. * * @return void */ protected function processTokenWithinScope(File $phpcsFile, $stackPtr, $currScope) { $methodName = $phpcsFile->getDeclarationName($stackPtr); if ($methodName === null) { // Ignore closures. return; } // Ignore magic methods. if (preg_match('|^__|', $methodName) !== 0) { $magicPart = strtolower(substr($methodName, 2)); if (isset($this->magicMethods[$magicPart]) === true || isset($this->methodsDoubleUnderscore[$magicPart]) === true) { return; } } $testName = ltrim($methodName, '_'); if ($testName !== '' && Common::isCamelCaps($testName, false, true, false) === false) { $error = 'Method name "%s" is not in camel caps format'; $className = $phpcsFile->getDeclarationName($currScope); $errorData = array($className . '::' . $methodName); $phpcsFile->addError($error, $stackPtr, 'NotCamelCaps', $errorData); $phpcsFile->recordMetric($stackPtr, 'CamelCase method name', 'no'); } else { $phpcsFile->recordMetric($stackPtr, 'CamelCase method name', 'yes'); } }
/** * Constructs a file list and loads in an array of file paths to process. * * @param \PHP_CodeSniffer\Config $config The config data for the run. * @param \PHP_CodeSniffer\Ruleset $ruleset The ruleset used for the run. * * @return void */ public function __construct(Config $config, Ruleset $ruleset) { $this->ruleset = $ruleset; $this->config = $config; $paths = $config->files; foreach ($paths as $path) { $isPharFile = Util\Common::isPharFile($path); if (is_dir($path) === true || $isPharFile === true) { if ($isPharFile === true) { $path = 'phar://' . $path; } $filterClass = $this->getFilterClass(); $di = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS); $filter = new $filterClass($di, $path, $config, $ruleset); $iterator = new \RecursiveIteratorIterator($filter); foreach ($iterator as $file) { $this->files[$file->getPathname()] = null; } } else { $this->addFile($path); } //end if } //end foreach reset($this->files); $this->numFiles = count($this->files); }
/** * Processes the tokens outside the scope. * * @param PHP_CodeSniffer_File $phpcsFile The file being processed. * @param int $stackPtr The position where this token was * found. * * @return void */ protected function processTokenOutsideScope(File $phpcsFile, $stackPtr) { $functionName = $phpcsFile->getDeclarationName($stackPtr); if ($functionName === null) { return; } $errorData = array($functionName); // Does this function claim to be magical? if (preg_match('|^__|', $functionName) !== 0) { $error = 'Function name "%s" is invalid; only PHP magic methods should be prefixed with a double underscore'; $phpcsFile->addError($error, $stackPtr, 'DoubleUnderscore', $errorData); return; } if (Common::isCamelCaps($functionName, false, true, false) === false) { $error = 'Function name "%s" is not in camel caps format'; $phpcsFile->addError($error, $stackPtr, 'NotCamelCaps', $errorData); } }
/** * Get a list of whitelisted file paths. * * @return array */ protected function getWhitelist() { $modified = array(); $cmd = 'git ls-files -o -m --exclude-standard -- ' . $this->basedir; $output = array(); exec($cmd, $output); $basedir = $this->basedir; if (is_dir($basedir) === false) { $basedir = dirname($basedir); } foreach ($output as $path) { $path = Util\Common::realpath($path); do { $modified[$path] = true; $path = dirname($path); } while ($path !== $basedir); } return $modified; }
/** * Check whether the current element of the iterator is acceptable. * * Files are checked for allowed extensions and ignore patterns. * Directories are checked for ignore patterns only. * * @return bool */ public function accept() { $filePath = Util\Common::realpath($this->current()); if ($filePath === false) { return false; } if (is_dir($filePath) === true) { if ($this->config->local === true) { return false; } } else { if ($this->shouldProcessFile($filePath) === false) { return false; } } if ($this->shouldIgnorePath($filePath) === true) { return false; } return true; }
/** * Check whether the current element of the iterator is acceptable. * * If a file is both blacklisted and whitelisted, it will be deemed unacceptable. * * @return bool */ public function accept() { if (parent::accept() === false) { return false; } if ($this->blacklist === null) { $this->blacklist = $this->getblacklist(); } if ($this->whitelist === null) { $this->whitelist = $this->getwhitelist(); } $filePath = Util\Common::realpath($this->current()); // If file is both blacklisted and whitelisted, the blacklist takes precedence. if (isset($this->blacklist[$filePath]) === true) { return false; } if (empty($this->whitelist) === true && empty($this->blacklist) === false) { // We are only checking a blacklist, so everything else should be whitelisted. return true; } return isset($this->whitelist[$filePath]); }
/** * Processes the tokens outside the scope. * * @param PHP_CodeSniffer_File $phpcsFile The file being processed. * @param int $stackPtr The position where this token was * found. * * @return void */ protected function processTokenOutsideScope(File $phpcsFile, $stackPtr) { $functionName = $phpcsFile->getDeclarationName($stackPtr); if ($functionName === null) { // Ignore closures. return; } $errorData = array($functionName); // Is this a magic function. i.e., it is prefixed with "__". if (preg_match('|^__|', $functionName) !== 0) { $magicPart = strtolower(substr($functionName, 2)); if (isset($this->magicFunctions[$magicPart]) === false) { $error = 'Function name "%s" is invalid; only PHP magic methods should be prefixed with a double underscore'; $phpcsFile->addError($error, $stackPtr, 'FunctionDoubleUnderscore', $errorData); } return; } // Ignore first underscore in functions prefixed with "_". $functionName = ltrim($functionName, '_'); if (Common::isCamelCaps($functionName, false, true, $this->strict) === false) { $error = 'Function name "%s" is not in camel caps format'; $phpcsFile->addError($error, $stackPtr, 'NotCamelCaps', $errorData); $phpcsFile->recordMetric($stackPtr, 'CamelCase function name', 'no'); } else { $phpcsFile->recordMetric($stackPtr, 'CamelCase method name', 'yes'); } }
/** * Return the path of an installed coding standard. * * Coding standards are directories located in the * CodeSniffer/Standards directory. Valid coding standards * include a ruleset.xml file. * * @param string $standard The name of the coding standard. * * @return string|null */ public static function getInstalledStandardPath($standard) { $installedPaths = self::getInstalledStandardPaths(); foreach ($installedPaths as $installedPath) { if (basename($installedPath) === $standard) { $standardPath = $installedPath; } else { $standardPath = $installedPath . DIRECTORY_SEPARATOR . $standard; } $path = Common::realpath($standardPath . DIRECTORY_SEPARATOR . 'ruleset.xml'); if (is_file($path) === true) { return $path; } else { if (Common::isPharFile($standardPath) === true) { $path = Common::realpath($standardPath); if ($path !== false) { return $path; } } } } return null; }
/** * Populates the array of PHP_CodeSniffer_Sniff's for this file. * * @return void * @throws RuntimeException If sniff registration fails. */ public function populateTokenListeners() { // Construct a list of listeners indexed by token being listened for. $this->tokenListeners = array(); foreach ($this->sniffs as $sniffClass => $sniffObject) { $this->sniffs[$sniffClass] = null; $this->sniffs[$sniffClass] = new $sniffClass(); $sniffCode = Util\Common::getSniffCode($sniffClass); $this->sniffCodes[$sniffCode] = $sniffClass; // Set custom properties. if (isset($this->ruleset[$sniffCode]['properties']) === true) { foreach ($this->ruleset[$sniffCode]['properties'] as $name => $value) { $this->setSniffProperty($sniffClass, $name, $value); } } $tokenizers = array(); $vars = get_class_vars($sniffClass); if (isset($vars['supportedTokenizers']) === true) { foreach ($vars['supportedTokenizers'] as $tokenizer) { $tokenizers[$tokenizer] = $tokenizer; } } else { $tokenizers = array('PHP' => 'PHP'); } $tokens = $this->sniffs[$sniffClass]->register(); if (is_array($tokens) === false) { $msg = "Sniff {$sniffClass} register() method must return an array"; throw new RuntimeException($msg); } $ignorePatterns = array(); $patterns = $this->getIgnorePatterns($sniffCode); foreach ($patterns as $pattern => $type) { $replacements = array('\\,' => ',', '*' => '.*'); $ignorePatterns[] = strtr($pattern, $replacements); } $includePatterns = array(); $patterns = $this->getIncludePatterns($sniffCode); foreach ($patterns as $pattern => $type) { $replacements = array('\\,' => ',', '*' => '.*'); $includePatterns[] = strtr($pattern, $replacements); } foreach ($tokens as $token) { if (isset($this->tokenListeners[$token]) === false) { $this->tokenListeners[$token] = array(); } if (isset($this->tokenListeners[$token][$sniffClass]) === false) { $this->tokenListeners[$token][$sniffClass] = array('class' => $sniffClass, 'source' => $sniffCode, 'tokenizers' => $tokenizers, 'ignore' => $ignorePatterns, 'include' => $includePatterns); } } } //end foreach }
/** * Processes a file path and add it to the file list. * * @param string $path The path to the file to add. * * @return void */ public function processFilePath($path) { // If we are processing STDIN, don't record any files to check. if ($this->stdin === true) { return; } $file = Util\Common::realpath($path); if (file_exists($file) === false) { if ($this->dieOnUnknownArg === false) { return; } echo 'ERROR: The file "' . $path . '" does not exist.' . PHP_EOL . PHP_EOL; $this->printUsage(); exit(3); } else { $files = $this->files; $files[] = $file; $this->files = $files; $this->overriddenDefaults['files'] = true; } }
/** * Performs additional processing after main tokenizing. * * This additional processing looks for properties, closures, labels and objects. * * @return void */ public function processAdditional() { if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t*** START ADDITIONAL JS PROCESSING ***" . PHP_EOL; } $numTokens = count($this->tokens); $classStack = array(); for ($i = 0; $i < $numTokens; $i++) { if (PHP_CODESNIFFER_VERBOSITY > 1) { $type = $this->tokens[$i]['type']; $content = Util\Common::prepareForOutput($this->tokens[$i]['content']); echo str_repeat("\t", count($classStack)); echo "\tProcess token {$i}: {$type} => {$content}" . PHP_EOL; } // Looking for functions that are actually closures. if ($this->tokens[$i]['code'] === T_FUNCTION && isset($this->tokens[$i]['scope_opener']) === true) { for ($x = $i + 1; $x < $numTokens; $x++) { if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === false) { break; } } if ($this->tokens[$x]['code'] === T_OPEN_PARENTHESIS) { $this->tokens[$i]['code'] = T_CLOSURE; $this->tokens[$i]['type'] = 'T_CLOSURE'; if (PHP_CODESNIFFER_VERBOSITY > 1) { $line = $this->tokens[$i]['line']; echo str_repeat("\t", count($classStack)); echo "\t* token {$i} on line {$line} changed from T_FUNCTION to T_CLOSURE" . PHP_EOL; } for ($x = $this->tokens[$i]['scope_opener'] + 1; $x < $this->tokens[$i]['scope_closer']; $x++) { if (isset($this->tokens[$x]['conditions'][$i]) === false) { continue; } $this->tokens[$x]['conditions'][$i] = T_CLOSURE; if (PHP_CODESNIFFER_VERBOSITY > 1) { $type = $this->tokens[$x]['type']; echo str_repeat("\t", count($classStack)); echo "\t\t* cleaned {$x} ({$type}) *" . PHP_EOL; } } } //end if continue; } else { if ($this->tokens[$i]['code'] === T_OPEN_CURLY_BRACKET && isset($this->tokens[$i]['scope_condition']) === false) { $classStack[] = $i; $closer = $this->tokens[$i]['bracket_closer']; $this->tokens[$i]['code'] = T_OBJECT; $this->tokens[$i]['type'] = 'T_OBJECT'; $this->tokens[$closer]['code'] = T_CLOSE_OBJECT; $this->tokens[$closer]['type'] = 'T_CLOSE_OBJECT'; if (PHP_CODESNIFFER_VERBOSITY > 1) { echo str_repeat("\t", count($classStack)); echo "\t* token {$i} converted from T_OPEN_CURLY_BRACKET to T_OBJECT *" . PHP_EOL; echo str_repeat("\t", count($classStack)); echo "\t* token {$closer} converted from T_CLOSE_CURLY_BRACKET to T_CLOSE_OBJECT *" . PHP_EOL; } for ($x = $i + 1; $x < $closer; $x++) { $this->tokens[$x]['conditions'][$i] = T_OBJECT; ksort($this->tokens[$x]['conditions'], SORT_NUMERIC); if (PHP_CODESNIFFER_VERBOSITY > 1) { $type = $this->tokens[$x]['type']; echo str_repeat("\t", count($classStack)); echo "\t\t* added T_OBJECT condition to {$x} ({$type}) *" . PHP_EOL; } } } else { if ($this->tokens[$i]['code'] === T_CLOSE_OBJECT) { $opener = array_pop($classStack); } else { if ($this->tokens[$i]['code'] === T_COLON) { // If it is a scope opener, it belongs to a // DEFAULT or CASE statement. if (isset($this->tokens[$i]['scope_condition']) === true) { continue; } // Make sure this is not part of an inline IF statement. for ($x = $i - 1; $x >= 0; $x--) { if ($this->tokens[$x]['code'] === T_INLINE_THEN) { $this->tokens[$i]['code'] = T_INLINE_ELSE; $this->tokens[$i]['type'] = 'T_INLINE_ELSE'; if (PHP_CODESNIFFER_VERBOSITY > 1) { echo str_repeat("\t", count($classStack)); echo "\t* token {$i} converted from T_COLON to T_INLINE_THEN *" . PHP_EOL; } continue 2; } else { if ($this->tokens[$x]['line'] < $this->tokens[$i]['line']) { break; } } } // The string to the left of the colon is either a property or label. for ($label = $i - 1; $label >= 0; $label--) { if (isset(Util\Tokens::$emptyTokens[$this->tokens[$label]['code']]) === false) { break; } } if ($this->tokens[$label]['code'] !== T_STRING && $this->tokens[$label]['code'] !== T_CONSTANT_ENCAPSED_STRING) { continue; } if (empty($classStack) === false) { $this->tokens[$label]['code'] = T_PROPERTY; $this->tokens[$label]['type'] = 'T_PROPERTY'; if (PHP_CODESNIFFER_VERBOSITY > 1) { echo str_repeat("\t", count($classStack)); echo "\t* token {$label} converted from T_STRING to T_PROPERTY *" . PHP_EOL; } } else { $this->tokens[$label]['code'] = T_LABEL; $this->tokens[$label]['type'] = 'T_LABEL'; if (PHP_CODESNIFFER_VERBOSITY > 1) { echo str_repeat("\t", count($classStack)); echo "\t* token {$label} converted from T_STRING to T_LABEL *" . PHP_EOL; } } //end if } } } } //end if } //end for if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t*** END ADDITIONAL JS PROCESSING ***" . PHP_EOL; } }
/** * Performs the run. * * @return int The number of errors and warnings found. */ private function run() { // The class that manages all reporters for the run. $this->reporter = new Reporter($this->config); // Include bootstrap files. foreach ($this->config->bootstrap as $bootstrap) { include $bootstrap; } if ($this->config->stdin === true) { $fileContents = $this->config->stdinContent; if ($fileContents === null) { $handle = fopen('php://stdin', 'r'); stream_set_blocking($handle, true); $fileContents = stream_get_contents($handle); fclose($handle); } $todo = new FileList($this->config, $this->ruleset); $dummy = new DummyFile($fileContents, $this->ruleset, $this->config); $todo->addFile($dummy->path, $dummy); $numFiles = 1; } else { if (empty($this->config->files) === true) { echo 'ERROR: You must supply at least one file or directory to process.' . PHP_EOL . PHP_EOL; $this->config->printUsage(); exit(0); } if (PHP_CODESNIFFER_VERBOSITY > 0) { echo 'Creating file list... '; } $todo = new FileList($this->config, $this->ruleset); $numFiles = count($todo); if (PHP_CODESNIFFER_VERBOSITY > 0) { echo "DONE ({$numFiles} files in queue)" . PHP_EOL; } if ($this->config->cache === true) { if (PHP_CODESNIFFER_VERBOSITY > 0) { echo 'Loading cache... '; } Cache::load($this->ruleset, $this->config); if (PHP_CODESNIFFER_VERBOSITY > 0) { $size = Cache::getSize(); echo "DONE ({$size} files in cache)" . PHP_EOL; } } } //end if // Turn all sniff errors into exceptions. set_error_handler(array($this, 'handleErrors')); // If verbosity is too high, turn off parallelism so the // debug output is clean. if (PHP_CODESNIFFER_VERBOSITY > 1) { $this->config->parallel = 1; } // If the PCNTL extension isn't installed, we can't fork. if (function_exists('pcntl_fork') === false) { $this->config->parallel = 1; } $lastDir = ''; if ($this->config->parallel === 1) { // Running normally. $numProcessed = 0; foreach ($todo as $path => $file) { $currDir = dirname($path); if ($lastDir !== $currDir) { if (PHP_CODESNIFFER_VERBOSITY > 0) { echo 'Changing into directory ' . Common::stripBasepath($currDir, $this->config->basepath) . PHP_EOL; } $lastDir = $currDir; } $this->processFile($file); $numProcessed++; $this->printProgress($file, $numFiles, $numProcessed); } } else { // Batching and forking. $childProcs = array(); $numFiles = count($todo); $numPerBatch = ceil($numFiles / $this->config->parallel); for ($batch = 0; $batch < $this->config->parallel; $batch++) { $startAt = $batch * $numPerBatch; if ($startAt >= $numFiles) { break; } $endAt = $startAt + $numPerBatch; if ($endAt > $numFiles) { $endAt = $numFiles; } $childOutFilename = tempnam(sys_get_temp_dir(), 'phpcs-child'); $pid = pcntl_fork(); if ($pid === -1) { throw new RuntimeException('Failed to create child process'); } else { if ($pid !== 0) { $childProcs[] = array('pid' => $pid, 'out' => $childOutFilename); } else { // Move forward to the start of the batch. $todo->rewind(); for ($i = 0; $i < $startAt; $i++) { $todo->next(); } // Reset the reporter to make sure only figures from this // file batch are recorded. $this->reporter->totalFiles = 0; $this->reporter->totalErrors = 0; $this->reporter->totalWarnings = 0; $this->reporter->totalFixable = 0; // Process the files. $pathsProcessed = array(); ob_start(); for ($i = $startAt; $i < $endAt; $i++) { $path = $todo->key(); $file = $todo->current(); $currDir = dirname($path); if ($lastDir !== $currDir) { if (PHP_CODESNIFFER_VERBOSITY > 0) { echo 'Changing into directory ' . Common::stripBasepath($currDir, $this->config->basepath) . PHP_EOL; } $lastDir = $currDir; } $this->processFile($file); $pathsProcessed[] = $path; $todo->next(); } $debugOutput = ob_get_contents(); ob_end_clean(); // Write information about the run to the filesystem // so it can be picked up by the main process. $childOutput = array('totalFiles' => $this->reporter->totalFiles, 'totalErrors' => $this->reporter->totalErrors, 'totalWarnings' => $this->reporter->totalWarnings, 'totalFixable' => $this->reporter->totalFixable, 'totalFixed' => $this->reporter->totalFixed); $output = '<' . '?php' . "\n" . ' $childOutput = '; $output .= var_export($childOutput, true); $output .= ";\n\$debugOutput = "; $output .= var_export($debugOutput, true); if ($this->config->cache === true) { $childCache = array(); foreach ($pathsProcessed as $path) { $childCache[$path] = Cache::get($path); } $output .= ";\n\$childCache = "; $output .= var_export($childCache, true); } $output .= ";\n?" . '>'; file_put_contents($childOutFilename, $output); exit($pid); } } //end if } //end for $this->processChildProcs($childProcs); } //end if restore_error_handler(); if (PHP_CODESNIFFER_VERBOSITY === 0 && $this->config->interactive === false && $this->config->showProgress === true) { echo PHP_EOL . PHP_EOL; } if ($this->config->cache === true) { Cache::save(); } $ignoreWarnings = Config::getConfigData('ignore_warnings_on_exit'); $ignoreErrors = Config::getConfigData('ignore_errors_on_exit'); $return = $this->reporter->totalErrors + $this->reporter->totalWarnings; if ($ignoreErrors !== null) { $ignoreErrors = (bool) $ignoreErrors; if ($ignoreErrors === true) { $return -= $this->reporter->totalErrors; } } if ($ignoreWarnings !== null) { $ignoreWarnings = (bool) $ignoreWarnings; if ($ignoreWarnings === true) { $return -= $this->reporter->totalWarnings; } } return $return; }
/** * Generate summary information to be used during report generation. * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file that has been processed. * * @return array */ public function prepareFileReport(File $phpcsFile) { $report = array('filename' => Common::stripBasepath($phpcsFile->getFilename(), $this->config->basepath), 'errors' => $phpcsFile->getErrorCount(), 'warnings' => $phpcsFile->getWarningCount(), 'fixable' => $phpcsFile->getFixableCount(), 'messages' => array()); if ($report['errors'] === 0 && $report['warnings'] === 0) { // Prefect score! return $report; } if ($this->config->recordErrors === false) { $message = 'Errors are not being recorded but this report requires error messages. '; $message .= 'This report will not show the correct information.'; $report['messages'][1][1] = array(array('message' => $message, 'source' => 'Internal.RecordErrors', 'severity' => 5, 'fixable' => false, 'type' => 'ERROR')); return $report; } $errors = array(); // Merge errors and warnings. foreach ($phpcsFile->getErrors() as $line => $lineErrors) { foreach ($lineErrors as $column => $colErrors) { $newErrors = array(); foreach ($colErrors as $data) { $newErrors[] = array('message' => $data['message'], 'source' => $data['source'], 'severity' => $data['severity'], 'fixable' => $data['fixable'], 'type' => 'ERROR'); } $errors[$line][$column] = $newErrors; } ksort($errors[$line]); } //end foreach foreach ($phpcsFile->getWarnings() as $line => $lineWarnings) { foreach ($lineWarnings as $column => $colWarnings) { $newWarnings = array(); foreach ($colWarnings as $data) { $newWarnings[] = array('message' => $data['message'], 'source' => $data['source'], 'severity' => $data['severity'], 'fixable' => $data['fixable'], 'type' => 'WARNING'); } if (isset($errors[$line]) === false) { $errors[$line] = array(); } if (isset($errors[$line][$column]) === true) { $errors[$line][$column] = array_merge($newWarnings, $errors[$line][$column]); } else { $errors[$line][$column] = $newWarnings; } } //end foreach ksort($errors[$line]); } //end foreach ksort($errors); $report['messages'] = $errors; return $report; }
/** * Processes the tokens outside the scope. * * @param PHP_CodeSniffer_File $phpcsFile The file being processed. * @param int $stackPtr The position where this token was * found. * * @return void */ protected function processTokenOutsideScope(File $phpcsFile, $stackPtr) { $functionName = $phpcsFile->getDeclarationName($stackPtr); if ($functionName === null) { // Ignore closures. return; } if (ltrim($functionName, '_') === '') { // Ignore special functions. return; } $errorData = array($functionName); // Is this a magic function. i.e., it is prefixed with "__". if (preg_match('|^__|', $functionName) !== 0) { $magicPart = strtolower(substr($functionName, 2)); if (isset($this->magicFunctions[$magicPart]) === false) { $error = 'Function name "%s" is invalid; only PHP magic methods should be prefixed with a double underscore'; $phpcsFile->addError($error, $stackPtr, 'FunctionDoubleUnderscore', $errorData); } return; } // Function names can be in two parts; the package name and // the function name. $packagePart = ''; $camelCapsPart = ''; $underscorePos = strrpos($functionName, '_'); if ($underscorePos === false) { $camelCapsPart = $functionName; } else { $packagePart = substr($functionName, 0, $underscorePos); $camelCapsPart = substr($functionName, $underscorePos + 1); // We don't care about _'s on the front. $packagePart = ltrim($packagePart, '_'); } // If it has a package part, make sure the first letter is a capital. if ($packagePart !== '') { if ($functionName[0] === '_') { $error = 'Function name "%s" is invalid; only private methods should be prefixed with an underscore'; $phpcsFile->addError($error, $stackPtr, 'FunctionUnderscore', $errorData); return; } if ($functionName[0] !== strtoupper($functionName[0])) { $error = 'Function name "%s" is prefixed with a package name but does not begin with a capital letter'; $phpcsFile->addError($error, $stackPtr, 'FunctionNoCapital', $errorData); return; } } // If it doesn't have a camel caps part, it's not valid. if (trim($camelCapsPart) === '') { $error = 'Function name "%s" is not valid; name appears incomplete'; $phpcsFile->addError($error, $stackPtr, 'FunctionInvalid', $errorData); return; } $validName = true; $newPackagePart = $packagePart; $newCamelCapsPart = $camelCapsPart; // Every function must have a camel caps part, so check that first. if (Common::isCamelCaps($camelCapsPart, false, true, false) === false) { $validName = false; $newCamelCapsPart = strtolower($camelCapsPart[0]) . substr($camelCapsPart, 1); } if ($packagePart !== '') { // Check that each new word starts with a capital. $nameBits = explode('_', $packagePart); foreach ($nameBits as $bit) { if ($bit[0] !== strtoupper($bit[0])) { $newPackagePart = ''; foreach ($nameBits as $bit) { $newPackagePart .= strtoupper($bit[0]) . substr($bit, 1) . '_'; } $validName = false; break; } } } if ($validName === false) { $newName = rtrim($newPackagePart, '_') . '_' . $newCamelCapsPart; if ($newPackagePart === '') { $newName = $newCamelCapsPart; } else { $newName = rtrim($newPackagePart, '_') . '_' . $newCamelCapsPart; } $error = 'Function name "%s" is invalid; consider "%s" instead'; $data = $errorData; $data[] = $newName; $phpcsFile->addError($error, $stackPtr, 'FunctionNameInvalid', $data); } }
/** * Creates an array of tokens when given some CSS code. * * Uses the PHP tokenizer to do all the tricky work * * @param string $string The string to tokenize. * * @return array */ public function tokenize($string) { if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t*** START CSS TOKENIZING 1ST PASS ***" . PHP_EOL; } // If the content doesn't have an EOL char on the end, add one so // the open and close tags we add are parsed correctly. $eolAdded = false; if (substr($string, strlen($this->eolChar) * -1) !== $this->eolChar) { $string .= $this->eolChar; $eolAdded = true; } $string = str_replace('<?php', '^PHPCS_CSS_T_OPEN_TAG^', $string); $string = str_replace('?>', '^PHPCS_CSS_T_CLOSE_TAG^', $string); $tokens = parent::tokenize('<?php ' . $string . '?>'); $finalTokens = array(); $finalTokens[0] = array('code' => T_OPEN_TAG, 'type' => 'T_OPEN_TAG', 'content' => ''); $newStackPtr = 1; $numTokens = count($tokens); $multiLineComment = false; for ($stackPtr = 1; $stackPtr < $numTokens; $stackPtr++) { $token = $tokens[$stackPtr]; // CSS files don't have lists, breaks etc, so convert these to // standard strings early so they can be converted into T_STYLE // tokens and joined with other strings if needed. if ($token['code'] === T_BREAK || $token['code'] === T_LIST || $token['code'] === T_DEFAULT || $token['code'] === T_SWITCH || $token['code'] === T_FOR || $token['code'] === T_FOREACH || $token['code'] === T_WHILE) { $token['type'] = 'T_STRING'; $token['code'] = T_STRING; } if (PHP_CODESNIFFER_VERBOSITY > 1) { $type = $token['type']; $content = Util\Common::prepareForOutput($token['content']); echo "\tProcess token {$stackPtr}: {$type} => {$content}" . PHP_EOL; } if ($token['code'] === T_BITWISE_XOR && $tokens[$stackPtr + 1]['content'] === 'PHPCS_CSS_T_OPEN_TAG') { $content = '<?php'; for ($stackPtr = $stackPtr + 3; $stackPtr < $numTokens; $stackPtr++) { if ($tokens[$stackPtr]['code'] === T_BITWISE_XOR && $tokens[$stackPtr + 1]['content'] === 'PHPCS_CSS_T_CLOSE_TAG') { // Add the end tag and ignore the * we put at the end. $content .= '?>'; $stackPtr += 2; break; } else { $content .= $tokens[$stackPtr]['content']; } } if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t\t=> Found embedded PHP code: "; $cleanContent = Util\Common::prepareForOutput($content); echo $cleanContent . PHP_EOL; } $finalTokens[$newStackPtr] = array('type' => 'T_EMBEDDED_PHP', 'code' => T_EMBEDDED_PHP, 'content' => $content); $newStackPtr++; continue; } //end if if ($token['code'] === T_GOTO_LABEL) { // Convert these back to T_STRING followed by T_COLON so we can // more easily process style definitions. $finalTokens[$newStackPtr] = array('type' => 'T_STRING', 'code' => T_STRING, 'content' => substr($token['content'], 0, -1)); $newStackPtr++; $finalTokens[$newStackPtr] = array('type' => 'T_COLON', 'code' => T_COLON, 'content' => ':'); $newStackPtr++; continue; } if ($token['code'] === T_FUNCTION) { // There are no functions in CSS, so convert this to a string. $finalTokens[$newStackPtr] = array('type' => 'T_STRING', 'code' => T_STRING, 'content' => $token['content']); $newStackPtr++; continue; } if ($token['code'] === T_COMMENT && substr($token['content'], 0, 2) === '/*') { // Multi-line comment. Record it so we can ignore other // comment tags until we get out of this one. $multiLineComment = true; } if ($token['code'] === T_COMMENT && $multiLineComment === false && (substr($token['content'], 0, 2) === '//' || $token['content'][0] === '#')) { $content = ltrim($token['content'], '#/'); // Guard against PHP7+ syntax errors by stripping // leading zeros so the content doesn't look like an invalid int. $leadingZero = false; if ($content[0] === '0') { $content = '1' . $content; $leadingZero = true; } $commentTokens = parent::tokenize('<?php ' . $content . '?>'); // The first and last tokens are the open/close tags. array_shift($commentTokens); array_pop($commentTokens); if ($leadingZero === true) { $commentTokens[0]['content'] = substr($commentTokens[0]['content'], 1); $content = substr($content, 1); } if ($token['content'][0] === '#') { // The # character is not a comment in CSS files, so // determine what it means in this context. $firstContent = $commentTokens[0]['content']; // If the first content is just a number, it is probably a // colour like 8FB7DB, which PHP splits into 8 and FB7DB. if (($commentTokens[0]['code'] === T_LNUMBER || $commentTokens[0]['code'] === T_DNUMBER) && $commentTokens[1]['code'] === T_STRING) { $firstContent .= $commentTokens[1]['content']; array_shift($commentTokens); } // If the first content looks like a colour and not a class // definition, join the tokens together. if (preg_match('/^[ABCDEF0-9]+$/i', $firstContent) === 1 && $commentTokens[1]['content'] !== '-') { array_shift($commentTokens); // Work out what we trimmed off above and remember to re-add it. $trimmed = substr($token['content'], 0, strlen($token['content']) - strlen($content)); $finalTokens[$newStackPtr] = array('type' => 'T_COLOUR', 'code' => T_COLOUR, 'content' => $trimmed . $firstContent); } else { $finalTokens[$newStackPtr] = array('type' => 'T_HASH', 'code' => T_HASH, 'content' => '#'); } } else { $finalTokens[$newStackPtr] = array('type' => 'T_STRING', 'code' => T_STRING, 'content' => '//'); } //end if $newStackPtr++; array_splice($tokens, $stackPtr, 1, $commentTokens); $numTokens = count($tokens); $stackPtr--; continue; } //end if if ($token['code'] === T_COMMENT && substr($token['content'], -2) === '*/') { // Multi-line comment is done. $multiLineComment = false; } $finalTokens[$newStackPtr] = $token; $newStackPtr++; } //end for if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t*** END CSS TOKENIZING 1ST PASS ***" . PHP_EOL; echo "\t*** START CSS TOKENIZING 2ND PASS ***" . PHP_EOL; } // A flag to indicate if we are inside a style definition, // which is defined using curly braces. $inStyleDef = false; // A flag to indicate if an At-rule like "@media" is used, which will result // in nested curly brackets. $asperandStart = false; $numTokens = count($finalTokens); for ($stackPtr = 0; $stackPtr < $numTokens; $stackPtr++) { $token = $finalTokens[$stackPtr]; if (PHP_CODESNIFFER_VERBOSITY > 1) { $type = $token['type']; $content = Util\Common::prepareForOutput($token['content']); echo "\tProcess token {$stackPtr}: {$type} => {$content}" . PHP_EOL; } switch ($token['code']) { case T_OPEN_CURLY_BRACKET: // Opening curly brackets for an At-rule do not start a style // definition. We also reset the asperand flag here because the next // opening curly bracket could be indeed the start of a style // definition. if ($asperandStart === true) { if (PHP_CODESNIFFER_VERBOSITY > 1) { if ($inStyleDef === true) { echo "\t\t* style definition closed *" . PHP_EOL; } if ($asperandStart === true) { echo "\t\t* at-rule definition closed *" . PHP_EOL; } } $inStyleDef = false; $asperandStart = false; } else { $inStyleDef = true; if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t\t* style definition opened *" . PHP_EOL; } } break; case T_CLOSE_CURLY_BRACKET: if (PHP_CODESNIFFER_VERBOSITY > 1) { if ($inStyleDef === true) { echo "\t\t* style definition closed *" . PHP_EOL; } if ($asperandStart === true) { echo "\t\t* at-rule definition closed *" . PHP_EOL; } } $inStyleDef = false; $asperandStart = false; break; case T_MINUS: // Minus signs are often used instead of spaces inside // class names, IDs and styles. if ($finalTokens[$stackPtr + 1]['code'] === T_STRING) { if ($finalTokens[$stackPtr - 1]['code'] === T_STRING) { $newContent = $finalTokens[$stackPtr - 1]['content'] . '-' . $finalTokens[$stackPtr + 1]['content']; if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t\t* token is a string joiner; ignoring this and previous token" . PHP_EOL; $old = Util\Common::prepareForOutput($finalTokens[$stackPtr + 1]['content']); $new = Util\Common::prepareForOutput($newContent); echo "\t\t=> token " . ($stackPtr + 1) . " content changed from \"{$old}\" to \"{$new}\"" . PHP_EOL; } $finalTokens[$stackPtr + 1]['content'] = $newContent; unset($finalTokens[$stackPtr]); unset($finalTokens[$stackPtr - 1]); } else { $newContent = '-' . $finalTokens[$stackPtr + 1]['content']; $finalTokens[$stackPtr + 1]['content'] = $newContent; unset($finalTokens[$stackPtr]); } } else { if ($finalTokens[$stackPtr + 1]['code'] === T_LNUMBER) { // They can also be used to provide negative numbers. if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t\t* token is part of a negative number; adding content to next token and ignoring *" . PHP_EOL; $content = Util\Common::prepareForOutput($finalTokens[$stackPtr + 1]['content']); echo "\t\t=> token " . ($stackPtr + 1) . " content changed from \"{$content}\" to \"-{$content}\"" . PHP_EOL; } $finalTokens[$stackPtr + 1]['content'] = '-' . $finalTokens[$stackPtr + 1]['content']; unset($finalTokens[$stackPtr]); } } //end if break; case T_COLON: // Only interested in colons that are defining styles. if ($inStyleDef === false) { break; } for ($x = $stackPtr - 1; $x >= 0; $x--) { if (isset(Util\Tokens::$emptyTokens[$finalTokens[$x]['code']]) === false) { break; } } if (PHP_CODESNIFFER_VERBOSITY > 1) { $type = $finalTokens[$x]['type']; echo "\t\t=> token {$x} changed from {$type} to T_STYLE" . PHP_EOL; } $finalTokens[$x]['type'] = 'T_STYLE'; $finalTokens[$x]['code'] = T_STYLE; break; case T_STRING: if (strtolower($token['content']) === 'url') { // Find the next content. for ($x = $stackPtr + 1; $x < $numTokens; $x++) { if (isset(Util\Tokens::$emptyTokens[$finalTokens[$x]['code']]) === false) { break; } } // Needs to be in the format "url(" for it to be a URL. if ($finalTokens[$x]['code'] !== T_OPEN_PARENTHESIS) { continue; } // Make sure the content isn't empty. for ($y = $x + 1; $y < $numTokens; $y++) { if (isset(Util\Tokens::$emptyTokens[$finalTokens[$y]['code']]) === false) { break; } } if ($finalTokens[$y]['code'] === T_CLOSE_PARENTHESIS) { continue; } if (PHP_CODESNIFFER_VERBOSITY > 1) { for ($i = $stackPtr + 1; $i <= $y; $i++) { $type = $finalTokens[$i]['type']; $content = Util\Common::prepareForOutput($finalTokens[$i]['content']); echo "\tProcess token {$i}: {$type} => {$content}" . PHP_EOL; } echo "\t\t* token starts a URL *" . PHP_EOL; } // Join all the content together inside the url() statement. $newContent = ''; for ($i = $x + 2; $i < $numTokens; $i++) { if ($finalTokens[$i]['code'] === T_CLOSE_PARENTHESIS) { break; } $newContent .= $finalTokens[$i]['content']; if (PHP_CODESNIFFER_VERBOSITY > 1) { $content = Util\Common::prepareForOutput($finalTokens[$i]['content']); echo "\t\t=> token {$i} added to URL string and ignored: {$content}" . PHP_EOL; } unset($finalTokens[$i]); } $stackPtr = $i; // If the content inside the "url()" is in double quotes // there will only be one token and so we don't have to do // anything except change its type. If it is not empty, // we need to do some token merging. $finalTokens[$x + 1]['type'] = 'T_URL'; $finalTokens[$x + 1]['code'] = T_URL; if ($newContent !== '') { $finalTokens[$x + 1]['content'] .= $newContent; if (PHP_CODESNIFFER_VERBOSITY > 1) { $content = Util\Common::prepareForOutput($finalTokens[$x + 1]['content']); echo "\t\t=> token content changed to: {$content}" . PHP_EOL; } } } //end if break; case T_ASPERAND: $asperandStart = true; if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t\t* at-rule definition opened *" . PHP_EOL; } break; default: // Nothing special to be done with this token. break; } //end switch } //end for // Reset the array keys to avoid gaps. $finalTokens = array_values($finalTokens); $numTokens = count($finalTokens); // Blank out the content of the end tag. $finalTokens[$numTokens - 1]['content'] = ''; if ($eolAdded === true) { // Strip off the extra EOL char we added for tokenizing. $finalTokens[$numTokens - 2]['content'] = substr($finalTokens[$numTokens - 2]['content'], 0, strlen($this->eolChar) * -1); if ($finalTokens[$numTokens - 2]['content'] === '') { unset($finalTokens[$numTokens - 2]); $finalTokens = array_values($finalTokens); $numTokens = count($finalTokens); } } if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t*** END CSS TOKENIZING 2ND PASS ***" . PHP_EOL; } return $finalTokens; }
/** * Loads existing cache data for the run, if any. * * @param \PHP_CodeSniffer\Ruleset $ruleset The ruleset used for the run. * @param \PHP_CodeSniffer\Config $config The config data for the run. * * @return void */ public static function load(Ruleset $ruleset, Config $config) { // Look at every loaded sniff class so far and use their file contents // to generate a hash for the code used during the run. // At this point, the loaded class list contains the core PHPCS code // and all sniffs that have been loaded as part of the run. if (PHP_CODESNIFFER_VERBOSITY > 1) { echo PHP_EOL . "\tGenerating loaded file list for code hash" . PHP_EOL; } $codeHash = ''; $classes = array_keys(Autoload::getLoadedClasses()); sort($classes); $installDir = dirname(__DIR__); $installDirLen = strlen($installDir); $standardDir = $installDir . DIRECTORY_SEPARATOR . 'Standards'; $standardDirLen = strlen($standardDir); foreach ($classes as $file) { if (substr($file, 0, $standardDirLen) !== $standardDir) { if (substr($file, 0, $installDirLen) === $installDir) { // We are only interested in sniffs here. continue; } if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t\t=> external file: {$file}" . PHP_EOL; } } else { if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t\t=> internal sniff: {$file}" . PHP_EOL; } } $codeHash .= md5_file($file); } // Add the content of the used rulesets to the hash so that sniff setting // changes in the ruleset invalidate the cache. $rulesets = $ruleset->paths; sort($rulesets); foreach ($rulesets as $file) { if (substr($file, 0, $standardDirLen) !== $standardDir) { if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t\t=> external ruleset: {$file}" . PHP_EOL; } } else { if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t\t=> internal ruleset: {$file}" . PHP_EOL; } } $codeHash .= md5_file($file); } // Go through the core PHPCS code and add those files to the file // hash. This ensures that core PHPCS changes will also invalidate the cache. // Note that we ignore sniffs here, and any files that don't affect // the outcome of the run. $di = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($installDir), 0, \RecursiveIteratorIterator::CATCH_GET_CHILD); $di = new \RecursiveDirectoryIterator($installDir); $filter = new \RecursiveCallbackFilterIterator($di, function ($file, $key, $iterator) { // Skip hidden files. $filename = $file->getFilename(); if (substr($filename, 0, 1) === '.') { return false; } $filePath = Common::realpath($file->getPathname()); if ($filePath === false) { return false; } if (is_dir($filePath) === true && ($filename === 'Standards' || $filename === 'Exceptions' || $filename === 'Reports' || $filename === 'Generators')) { return false; } return true; }); $iterator = new \RecursiveIteratorIterator($filter); foreach ($iterator as $file) { if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t\t=> core file: {$file}" . PHP_EOL; } $codeHash .= md5_file($file); } $codeHash = md5($codeHash); // Along with the code hash, use various settings that can affect // the results of a run to create a new hash. This hash will be used // in the cache file name. $rulesetHash = md5(var_export($ruleset->ignorePatterns, true) . var_export($ruleset->includePatterns, true)); $configData = array('tabWidth' => $config->tabWidth, 'encoding' => $config->encoding, 'recordErrors' => $config->recordErrors, 'codeHash' => $codeHash, 'rulesetHash' => $rulesetHash); $configString = implode(',', $configData); $cacheHash = substr(sha1($configString), 0, 12); if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\tGenerating cache key data" . PHP_EOL; echo "\t\t=> tabWidth: " . $configData['tabWidth'] . PHP_EOL; echo "\t\t=> encoding: " . $configData['encoding'] . PHP_EOL; echo "\t\t=> recordErrors: " . (int) $configData['recordErrors'] . PHP_EOL; echo "\t\t=> codeHash: " . $configData['codeHash'] . PHP_EOL; echo "\t\t=> rulesetHash: " . $configData['rulesetHash'] . PHP_EOL; echo "\t\t=> cacheHash: {$cacheHash}" . PHP_EOL; } if ($config->cacheFile !== null) { $cacheFile = $config->cacheFile; } else { // Determine the common paths for all files being checked. // We can use this to locate an existing cache file, or to // determine where to create a new one. if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\tChecking possible cache file paths" . PHP_EOL; } $paths = array(); foreach ($config->files as $file) { $file = Common::realpath($file); while ($file !== DIRECTORY_SEPARATOR) { if (isset($paths[$file]) === false) { $paths[$file] = 1; } else { $paths[$file]++; } $lastFile = $file; $file = dirname($file); if ($file === $lastFile) { // Just in case something went wrong, // we don't want to end up in an infinite loop. break; } } } ksort($paths); $paths = array_reverse($paths); $numFiles = count($config->files); $tmpDir = sys_get_temp_dir(); $cacheFile = null; foreach ($paths as $file => $count) { if ($count !== $numFiles) { unset($paths[$file]); continue; } $fileHash = substr(sha1($file), 0, 12); $testFile = $tmpDir . DIRECTORY_SEPARATOR . "phpcs.{$fileHash}.{$cacheHash}.cache"; if ($cacheFile === null) { // This will be our default location if we can't find // an existing file. $cacheFile = $testFile; } if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t\t=> {$testFile}" . PHP_EOL; echo "\t\t\t * based on shared location: {$file} *" . PHP_EOL; } if (file_exists($testFile) === true) { $cacheFile = $testFile; break; } } //end foreach if ($cacheFile === null) { // Unlikely, but just in case $paths is empty for some reason. $cacheFile = $tmpDir . DIRECTORY_SEPARATOR . "phpcs.{$cacheHash}.cache"; } } //end if self::$path = $cacheFile; if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t=> Using cache file: " . self::$path . PHP_EOL; } if (file_exists(self::$path) === true) { self::$cache = json_decode(file_get_contents(self::$path), true); // Verify the contents of the cache file. if (self::$cache['config'] !== $configData) { self::$cache = array(); if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t* cache was invalid and has been cleared *" . PHP_EOL; } } } else { if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t* cache file does not exist *" . PHP_EOL; } } self::$cache['config'] = $configData; }
/** * Processes the variable found within a double quoted string. * * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. * @param int $stackPtr The position of the double quoted * string. * * @return void */ protected function processVariableInString(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); $phpReservedVars = array('_SERVER', '_GET', '_POST', '_REQUEST', '_SESSION', '_ENV', '_COOKIE', '_FILES', 'GLOBALS', 'http_response_header', 'HTTP_RAW_POST_DATA', 'php_errormsg'); if (preg_match_all('|[^\\\\]\\$([a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*)|', $tokens[$stackPtr]['content'], $matches) !== 0) { foreach ($matches[1] as $varName) { // If it's a php reserved var, then its ok. if (in_array($varName, $phpReservedVars) === true) { continue; } if (Common::isCamelCaps($varName, false, true, false) === false) { $error = 'Variable "%s" is not in valid camel caps format'; $data = array($varName); $phpcsFile->addError($error, $stackPtr, 'StringVarNotCamelCaps', $data); } else { if (preg_match('|\\d|', $varName) === 1) { $warning = 'Variable "%s" contains numbers but this is discouraged'; $data = array($varName); $phpcsFile->addWarning($warning, $stackPtr, 'StringVarContainsNumbers', $data); } } } //end foreach } //end if }
/** * Reverts the previous fix made to a token. * * @param int $stackPtr The position of the token in the token stack. * * @return bool If a change was reverted. */ public function revertToken($stackPtr) { if (isset($this->fixedTokens[$stackPtr]) === false) { return false; } if (PHP_CODESNIFFER_VERBOSITY > 1) { $bt = debug_backtrace(); if ($bt[1]['class'] === 'PHP_CodeSniffer_Fixer') { $sniff = $bt[2]['class']; $line = $bt[1]['line']; } else { $sniff = $bt[1]['class']; $line = $bt[0]['line']; } $tokens = $this->currentFile->getTokens(); $type = $tokens[$stackPtr]['type']; $oldContent = Common::prepareForOutput($this->tokens[$stackPtr]); $newContent = Common::prepareForOutput($this->fixedTokens[$stackPtr]); if (trim($this->tokens[$stackPtr]) === '' && isset($tokens[$stackPtr + 1]) === true) { // Add some context for whitespace only changes. $append = Common::prepareForOutput($this->tokens[$stackPtr + 1]); $oldContent .= $append; $newContent .= $append; } } //end if $this->tokens[$stackPtr] = $this->fixedTokens[$stackPtr]; unset($this->fixedTokens[$stackPtr]); $this->numFixes--; if (PHP_CODESNIFFER_VERBOSITY > 1) { $indent = "\t"; if (empty($this->changeset) === false) { $indent .= "\tR: "; } @ob_end_clean(); echo "{$indent}{$sniff} (line {$line}) reverted token {$stackPtr} ({$type}) \"{$oldContent}\" => \"{$newContent}\"" . PHP_EOL; ob_start(); } return true; }
/** * Process the function parameter comments. * * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. * @param int $stackPtr The position of the current token * in the stack passed in $tokens. * @param int $commentStart The position in the stack where the comment started. * * @return void */ protected function processParams(File $phpcsFile, $stackPtr, $commentStart) { if ($this->phpVersion === null) { $this->phpVersion = Config::getConfigData('php_version'); if ($this->phpVersion === null) { $this->phpVersion = PHP_VERSION_ID; } } $tokens = $phpcsFile->getTokens(); $params = array(); $maxType = 0; $maxVar = 0; foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) { if ($tokens[$tag]['content'] !== '@param') { continue; } $type = ''; $typeSpace = 0; $var = ''; $varSpace = 0; $comment = ''; $commentLines = array(); if ($tokens[$tag + 2]['code'] === T_DOC_COMMENT_STRING) { $matches = array(); preg_match('/([^$&.]+)(?:((?:\\.\\.\\.)?(?:\\$|&)[^\\s]+)(?:(\\s+)(.*))?)?/', $tokens[$tag + 2]['content'], $matches); if (empty($matches) === false) { $typeLen = strlen($matches[1]); $type = trim($matches[1]); $typeSpace = $typeLen - strlen($type); $typeLen = strlen($type); if ($typeLen > $maxType) { $maxType = $typeLen; } } if (isset($matches[2]) === true) { $var = $matches[2]; $varLen = strlen($var); if ($varLen > $maxVar) { $maxVar = $varLen; } if (isset($matches[4]) === true) { $varSpace = strlen($matches[3]); $comment = $matches[4]; $commentLines[] = array('comment' => $comment, 'token' => $tag + 2, 'indent' => $varSpace); // Any strings until the next tag belong to this comment. if (isset($tokens[$commentStart]['comment_tags'][$pos + 1]) === true) { $end = $tokens[$commentStart]['comment_tags'][$pos + 1]; } else { $end = $tokens[$commentStart]['comment_closer']; } for ($i = $tag + 3; $i < $end; $i++) { if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) { $indent = 0; if ($tokens[$i - 1]['code'] === T_DOC_COMMENT_WHITESPACE) { $indent = strlen($tokens[$i - 1]['content']); } $comment .= ' ' . $tokens[$i]['content']; $commentLines[] = array('comment' => $tokens[$i]['content'], 'token' => $i, 'indent' => $indent); } } } else { $error = 'Missing parameter comment'; $phpcsFile->addError($error, $tag, 'MissingParamComment'); $commentLines[] = array('comment' => ''); } //end if } else { $error = 'Missing parameter name'; $phpcsFile->addError($error, $tag, 'MissingParamName'); } //end if } else { $error = 'Missing parameter type'; $phpcsFile->addError($error, $tag, 'MissingParamType'); } //end if $params[] = array('tag' => $tag, 'type' => $type, 'var' => $var, 'comment' => $comment, 'commentLines' => $commentLines, 'type_space' => $typeSpace, 'var_space' => $varSpace); } //end foreach $realParams = $phpcsFile->getMethodParameters($stackPtr); $foundParams = array(); // We want to use ... for all variable length arguments, so added // this prefix to the variable name so comparisons are easier. foreach ($realParams as $pos => $param) { if ($param['variable_length'] === true) { $realParams[$pos]['name'] = '...' . $realParams[$pos]['name']; } } foreach ($params as $pos => $param) { // If the type is empty, the whole line is empty. if ($param['type'] === '') { continue; } // Check the param type value. $typeNames = explode('|', $param['type']); foreach ($typeNames as $typeName) { $suggestedName = Common::suggestType($typeName); if ($typeName !== $suggestedName) { $error = 'Expected "%s" but found "%s" for parameter type'; $data = array($suggestedName, $typeName); $fix = $phpcsFile->addFixableError($error, $param['tag'], 'IncorrectParamVarName', $data); if ($fix === true) { $content = $suggestedName; $content .= str_repeat(' ', $param['type_space']); $content .= $param['var']; $content .= str_repeat(' ', $param['var_space']); if (isset($param['commentLines'][0]) === true) { $content .= $param['commentLines'][0]['comment']; } $phpcsFile->fixer->replaceToken($param['tag'] + 2, $content); } } else { if (count($typeNames) === 1) { // Check type hint for array and custom type. $suggestedTypeHint = ''; if (strpos($suggestedName, 'array') !== false || substr($suggestedName, -2) === '[]') { $suggestedTypeHint = 'array'; } else { if (strpos($suggestedName, 'callable') !== false) { $suggestedTypeHint = 'callable'; } else { if (strpos($suggestedName, 'callback') !== false) { $suggestedTypeHint = 'callable'; } else { if (in_array($typeName, Common::$allowedTypes) === false) { $suggestedTypeHint = $suggestedName; } else { if ($this->phpVersion >= 70000) { if ($typeName === 'string') { $suggestedTypeHint = 'string'; } else { if ($typeName === 'int' || $typeName === 'integer') { $suggestedTypeHint = 'int'; } else { if ($typeName === 'float') { $suggestedTypeHint = 'float'; } else { if ($typeName === 'bool' || $typeName === 'boolean') { $suggestedTypeHint = 'bool'; } } } } } } } } } if ($suggestedTypeHint !== '' && isset($realParams[$pos]) === true) { $typeHint = $realParams[$pos]['type_hint']; if ($typeHint === '') { $error = 'Type hint "%s" missing for %s'; $data = array($suggestedTypeHint, $param['var']); $errorCode = 'TypeHintMissing'; if ($suggestedTypeHint === 'string' || $suggestedTypeHint === 'int' || $suggestedTypeHint === 'float' || $suggestedTypeHint === 'bool') { $errorCode = 'Scalar' . $errorCode; } $phpcsFile->addError($error, $stackPtr, $errorCode, $data); } else { if ($typeHint !== substr($suggestedTypeHint, strlen($typeHint) * -1)) { $error = 'Expected type hint "%s"; found "%s" for %s'; $data = array($suggestedTypeHint, $typeHint, $param['var']); $phpcsFile->addError($error, $stackPtr, 'IncorrectTypeHint', $data); } } //end if } else { if ($suggestedTypeHint === '' && isset($realParams[$pos]) === true) { $typeHint = $realParams[$pos]['type_hint']; if ($typeHint !== '') { $error = 'Unknown type hint "%s" found for %s'; $data = array($typeHint, $param['var']); $phpcsFile->addError($error, $stackPtr, 'InvalidTypeHint', $data); } } } //end if } } //end if } //end foreach if ($param['var'] === '') { continue; } $foundParams[] = $param['var']; // Check number of spaces after the type. $spaces = $maxType - strlen($param['type']) + 1; if ($param['type_space'] !== $spaces) { $error = 'Expected %s spaces after parameter type; %s found'; $data = array($spaces, $param['type_space']); $fix = $phpcsFile->addFixableError($error, $param['tag'], 'SpacingAfterParamType', $data); if ($fix === true) { $phpcsFile->fixer->beginChangeset(); $content = $param['type']; $content .= str_repeat(' ', $spaces); $content .= $param['var']; $content .= str_repeat(' ', $param['var_space']); $content .= $param['commentLines'][0]['comment']; $phpcsFile->fixer->replaceToken($param['tag'] + 2, $content); // Fix up the indent of additional comment lines. foreach ($param['commentLines'] as $lineNum => $line) { if ($lineNum === 0 || $param['commentLines'][$lineNum]['indent'] === 0) { continue; } $newIndent = $param['commentLines'][$lineNum]['indent'] + $spaces - $param['type_space']; $phpcsFile->fixer->replaceToken($param['commentLines'][$lineNum]['token'] - 1, str_repeat(' ', $newIndent)); } $phpcsFile->fixer->endChangeset(); } //end if } //end if // Make sure the param name is correct. if (isset($realParams[$pos]) === true) { $realName = $realParams[$pos]['name']; if ($realName !== $param['var']) { $code = 'ParamNameNoMatch'; $data = array($param['var'], $realName); $error = 'Doc comment for parameter %s does not match '; if (strtolower($param['var']) === strtolower($realName)) { $error .= 'case of '; $code = 'ParamNameNoCaseMatch'; } $error .= 'actual variable name %s'; $phpcsFile->addError($error, $param['tag'], $code, $data); } } else { if (substr($param['var'], -4) !== ',...') { // We must have an extra parameter comment. $error = 'Superfluous parameter comment'; $phpcsFile->addError($error, $param['tag'], 'ExtraParamComment'); } } //end if if ($param['comment'] === '') { continue; } // Check number of spaces after the var name. $spaces = $maxVar - strlen($param['var']) + 1; if ($param['var_space'] !== $spaces) { $error = 'Expected %s spaces after parameter name; %s found'; $data = array($spaces, $param['var_space']); $fix = $phpcsFile->addFixableError($error, $param['tag'], 'SpacingAfterParamName', $data); if ($fix === true) { $phpcsFile->fixer->beginChangeset(); $content = $param['type']; $content .= str_repeat(' ', $param['type_space']); $content .= $param['var']; $content .= str_repeat(' ', $spaces); $content .= $param['commentLines'][0]['comment']; $phpcsFile->fixer->replaceToken($param['tag'] + 2, $content); // Fix up the indent of additional comment lines. foreach ($param['commentLines'] as $lineNum => $line) { if ($lineNum === 0 || $param['commentLines'][$lineNum]['indent'] === 0) { continue; } $newIndent = $param['commentLines'][$lineNum]['indent'] + $spaces - $param['var_space']; $phpcsFile->fixer->replaceToken($param['commentLines'][$lineNum]['token'] - 1, str_repeat(' ', $newIndent)); } $phpcsFile->fixer->endChangeset(); } //end if } //end if // Param comments must start with a capital letter and end with the full stop. if (preg_match('/^(\\p{Ll}|\\P{L})/u', $param['comment']) === 1) { $error = 'Parameter comment must start with a capital letter'; $phpcsFile->addError($error, $param['tag'], 'ParamCommentNotCapital'); } $lastChar = substr($param['comment'], -1); if ($lastChar !== '.') { $error = 'Parameter comment must end with a full stop'; $phpcsFile->addError($error, $param['tag'], 'ParamCommentFullStop'); } } //end foreach $realNames = array(); foreach ($realParams as $realParam) { $realNames[] = $realParam['name']; } // Report missing comments. $diff = array_diff($realNames, $foundParams); foreach ($diff as $neededParam) { $error = 'Doc comment for parameter "%s" missing'; $data = array($neededParam); $phpcsFile->addError($error, $commentStart, 'MissingParamTag', $data); } }
/** * Creates an array of tokens when given some PHP code. * * Starts by using token_get_all() but does a lot of extra processing * to insert information about the context of the token. * * @param string $string The string to tokenize. * @param string $eolChar The EOL character to use for splitting strings. * @param int $stackPtr The position of the first token in the file. * * @return array */ public function tokenizeString($string, $eolChar, $stackPtr) { if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t\t*** START COMMENT TOKENIZING ***" . PHP_EOL; } $tokens = array(); $numChars = strlen($string); /* Doc block comments start with /*, but typically contain an extra star when they are used for function and class comments. */ $char = $numChars - strlen(ltrim($string, '/*')); $openTag = substr($string, 0, $char); $string = ltrim($string, '/*'); $tokens[$stackPtr] = array('content' => $openTag, 'code' => T_DOC_COMMENT_OPEN_TAG, 'type' => 'T_DOC_COMMENT_OPEN_TAG', 'comment_tags' => array()); $openPtr = $stackPtr; $stackPtr++; if (PHP_CODESNIFFER_VERBOSITY > 1) { $content = Util\Common::prepareForOutput($openTag); echo "\t\tCreate comment token: T_DOC_COMMENT_OPEN_TAG => {$content}" . PHP_EOL; } /* Strip off the close tag so it doesn't interfere with any of our comment line processing. The token will be added to the stack just before we return it. */ $closeTag = array('content' => substr($string, strlen(rtrim($string, '/*'))), 'code' => T_DOC_COMMENT_CLOSE_TAG, 'type' => 'T_DOC_COMMENT_CLOSE_TAG', 'comment_opener' => $openPtr); $string = rtrim($string, '/*'); /* Process each line of the comment. */ $lines = explode($eolChar, $string); $numLines = count($lines); foreach ($lines as $lineNum => $string) { if ($lineNum !== $numLines - 1) { $string .= $eolChar; } $char = 0; $numChars = strlen($string); // We've started a new line, so process the indent. $space = $this->collectWhitespace($string, $char, $numChars); if ($space !== null) { $tokens[$stackPtr] = $space; $stackPtr++; if (PHP_CODESNIFFER_VERBOSITY > 1) { $content = Util\Common::prepareForOutput($space['content']); echo "\t\tCreate comment token: T_DOC_COMMENT_WHITESPACE => {$content}" . PHP_EOL; } $char += strlen($space['content']); if ($char === $numChars) { break; } } if ($string === '') { continue; } if ($string[$char] === '*') { // This is a function or class doc block line. $char++; $tokens[$stackPtr] = array('content' => '*', 'code' => T_DOC_COMMENT_STAR, 'type' => 'T_DOC_COMMENT_STAR'); $stackPtr++; if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t\tCreate comment token: T_DOC_COMMENT_STAR => *" . PHP_EOL; } } // Now we are ready to process the actual content of the line. $lineTokens = $this->processLine($string, $eolChar, $char, $numChars); foreach ($lineTokens as $lineToken) { $tokens[$stackPtr] = $lineToken; if (PHP_CODESNIFFER_VERBOSITY > 1) { $content = Util\Common::prepareForOutput($lineToken['content']); $type = $lineToken['type']; echo "\t\tCreate comment token: {$type} => {$content}" . PHP_EOL; } if ($lineToken['code'] === T_DOC_COMMENT_TAG) { $tokens[$openPtr]['comment_tags'][] = $stackPtr; } $stackPtr++; } } //end foreach $tokens[$stackPtr] = $closeTag; $tokens[$openPtr]['comment_closer'] = $stackPtr; if (PHP_CODESNIFFER_VERBOSITY > 1) { $content = Util\Common::prepareForOutput($closeTag['content']); echo "\t\tCreate comment token: T_DOC_COMMENT_CLOSE_TAG => {$content}" . PHP_EOL; } if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t\t*** END COMMENT TOKENIZING ***" . PHP_EOL; } return $tokens; }
/** * Constructs the level map. * * The level map adds a 'level' index to each token which indicates the * depth that a token within a set of scope blocks. It also adds a * 'condition' index which is an array of the scope conditions that opened * each of the scopes - position 0 being the first scope opener. * * @return void */ private function createLevelMap() { if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t*** START LEVEL MAP ***" . PHP_EOL; } $this->numTokens = count($this->tokens); $level = 0; $conditions = array(); $lastOpener = null; $openers = array(); for ($i = 0; $i < $this->numTokens; $i++) { if (PHP_CODESNIFFER_VERBOSITY > 1) { $type = $this->tokens[$i]['type']; $line = $this->tokens[$i]['line']; $len = $this->tokens[$i]['length']; $col = $this->tokens[$i]['column']; $content = Util\Common::prepareForOutput($this->tokens[$i]['content']); echo str_repeat("\t", $level + 1); echo "Process token {$i} on line {$line} [col:{$col};len:{$len};lvl:{$level};"; if (empty($conditions) !== true) { $condString = 'conds;'; foreach ($conditions as $condition) { $condString .= token_name($condition) . ','; } echo rtrim($condString, ',') . ';'; } echo "]: {$type} => {$content}" . PHP_EOL; } //end if $this->tokens[$i]['level'] = $level; $this->tokens[$i]['conditions'] = $conditions; if (isset($this->tokens[$i]['scope_condition']) === true) { // Check to see if this token opened the scope. if ($this->tokens[$i]['scope_opener'] === $i) { $stackPtr = $this->tokens[$i]['scope_condition']; if (PHP_CODESNIFFER_VERBOSITY > 1) { $type = $this->tokens[$stackPtr]['type']; echo str_repeat("\t", $level + 1); echo "=> Found scope opener for {$stackPtr}:{$type}" . PHP_EOL; } $stackPtr = $this->tokens[$i]['scope_condition']; // If we find a scope opener that has a shared closer, // then we need to go back over the condition map that we // just created and fix ourselves as we just added some // conditions where there was none. This happens for T_CASE // statements that are using the same break statement. if ($lastOpener !== null && $this->tokens[$lastOpener]['scope_closer'] === $this->tokens[$i]['scope_closer']) { // This opener shares its closer with the previous opener, // but we still need to check if the two openers share their // closer with each other directly (like CASE and DEFAULT) // or if they are just sharing because one doesn't have a // closer (like CASE with no BREAK using a SWITCHes closer). $thisType = $this->tokens[$this->tokens[$i]['scope_condition']]['code']; $opener = $this->tokens[$lastOpener]['scope_condition']; $isShared = isset($this->scopeOpeners[$thisType]['with'][$this->tokens[$opener]['code']]); reset($this->scopeOpeners[$thisType]['end']); reset($this->scopeOpeners[$this->tokens[$opener]['code']]['end']); $sameEnd = current($this->scopeOpeners[$thisType]['end']) === current($this->scopeOpeners[$this->tokens[$opener]['code']]['end']); if ($isShared === true && $sameEnd === true) { $badToken = $opener; if (PHP_CODESNIFFER_VERBOSITY > 1) { $type = $this->tokens[$badToken]['type']; echo str_repeat("\t", $level + 1); echo "* shared closer, cleaning up {$badToken}:{$type} *" . PHP_EOL; } for ($x = $this->tokens[$i]['scope_condition']; $x <= $i; $x++) { $oldConditions = $this->tokens[$x]['conditions']; $oldLevel = $this->tokens[$x]['level']; $this->tokens[$x]['level']--; unset($this->tokens[$x]['conditions'][$badToken]); if (PHP_CODESNIFFER_VERBOSITY > 1) { $type = $this->tokens[$x]['type']; $oldConds = ''; foreach ($oldConditions as $condition) { $oldConds .= token_name($condition) . ','; } $oldConds = rtrim($oldConds, ','); $newConds = ''; foreach ($this->tokens[$x]['conditions'] as $condition) { $newConds .= token_name($condition) . ','; } $newConds = rtrim($newConds, ','); $newLevel = $this->tokens[$x]['level']; echo str_repeat("\t", $level + 1); echo "* cleaned {$x}:{$type} *" . PHP_EOL; echo str_repeat("\t", $level + 2); echo "=> level changed from {$oldLevel} to {$newLevel}" . PHP_EOL; echo str_repeat("\t", $level + 2); echo "=> conditions changed from {$oldConds} to {$newConds}" . PHP_EOL; } //end if } //end for unset($conditions[$badToken]); if (PHP_CODESNIFFER_VERBOSITY > 1) { $type = $this->tokens[$badToken]['type']; echo str_repeat("\t", $level + 1); echo "* token {$badToken}:{$type} removed from conditions array *" . PHP_EOL; } unset($openers[$lastOpener]); $level--; if (PHP_CODESNIFFER_VERBOSITY > 1) { echo str_repeat("\t", $level + 2); echo '* level decreased *' . PHP_EOL; } } //end if } //end if $level++; if (PHP_CODESNIFFER_VERBOSITY > 1) { echo str_repeat("\t", $level + 1); echo '* level increased *' . PHP_EOL; } $conditions[$stackPtr] = $this->tokens[$stackPtr]['code']; if (PHP_CODESNIFFER_VERBOSITY > 1) { $type = $this->tokens[$stackPtr]['type']; echo str_repeat("\t", $level + 1); echo "* token {$stackPtr}:{$type} added to conditions array *" . PHP_EOL; } $lastOpener = $this->tokens[$i]['scope_opener']; if ($lastOpener !== null) { $openers[$lastOpener] = $lastOpener; } } else { if ($lastOpener !== null && $this->tokens[$lastOpener]['scope_closer'] === $i) { foreach (array_reverse($openers) as $opener) { if ($this->tokens[$opener]['scope_closer'] === $i) { $oldOpener = array_pop($openers); if (empty($openers) === false) { $lastOpener = array_pop($openers); $openers[$lastOpener] = $lastOpener; } else { $lastOpener = null; } if (PHP_CODESNIFFER_VERBOSITY > 1) { $type = $this->tokens[$oldOpener]['type']; echo str_repeat("\t", $level + 1); echo "=> Found scope closer for {$oldOpener}:{$type}" . PHP_EOL; } $oldCondition = array_pop($conditions); if (PHP_CODESNIFFER_VERBOSITY > 1) { echo str_repeat("\t", $level + 1); echo '* token ' . token_name($oldCondition) . ' removed from conditions array *' . PHP_EOL; } // Make sure this closer actually belongs to us. // Either the condition also has to think this is the // closer, or it has to allow sharing with us. $condition = $this->tokens[$this->tokens[$i]['scope_condition']]['code']; if ($condition !== $oldCondition) { if (isset($this->scopeOpeners[$oldCondition]['with'][$condition]) === false) { $badToken = $this->tokens[$oldOpener]['scope_condition']; if (PHP_CODESNIFFER_VERBOSITY > 1) { $type = token_name($oldCondition); echo str_repeat("\t", $level + 1); echo "* scope closer was bad, cleaning up {$badToken}:{$type} *" . PHP_EOL; } for ($x = $oldOpener + 1; $x <= $i; $x++) { $oldConditions = $this->tokens[$x]['conditions']; $oldLevel = $this->tokens[$x]['level']; $this->tokens[$x]['level']--; unset($this->tokens[$x]['conditions'][$badToken]); if (PHP_CODESNIFFER_VERBOSITY > 1) { $type = $this->tokens[$x]['type']; $oldConds = ''; foreach ($oldConditions as $condition) { $oldConds .= token_name($condition) . ','; } $oldConds = rtrim($oldConds, ','); $newConds = ''; foreach ($this->tokens[$x]['conditions'] as $condition) { $newConds .= token_name($condition) . ','; } $newConds = rtrim($newConds, ','); $newLevel = $this->tokens[$x]['level']; echo str_repeat("\t", $level + 1); echo "* cleaned {$x}:{$type} *" . PHP_EOL; echo str_repeat("\t", $level + 2); echo "=> level changed from {$oldLevel} to {$newLevel}" . PHP_EOL; echo str_repeat("\t", $level + 2); echo "=> conditions changed from {$oldConds} to {$newConds}" . PHP_EOL; } //end if } //end for } //end if } //end if $level--; if (PHP_CODESNIFFER_VERBOSITY > 1) { echo str_repeat("\t", $level + 2); echo '* level decreased *' . PHP_EOL; } $this->tokens[$i]['level'] = $level; $this->tokens[$i]['conditions'] = $conditions; } //end if } //end foreach } } //end if } //end if } //end for if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t*** END LEVEL MAP ***" . PHP_EOL; } }
/** * Adds an error to the error stack. * * @param boolean $error Is this an error message? * @param string $message The text of the message. * @param int $line The line on which the message occurred. * @param int $column The column at which the message occurred. * @param string $code A violation code unique to the sniff message. * @param array $data Replacements for the message. * @param int $severity The severity level for this message. A value of 0 * will be converted into the default severity level. * @param boolean $fixable Can the problem be fixed by the sniff? * * @return boolean */ protected function addMessage($error, $message, $line, $column, $code, $data, $severity, $fixable) { if (isset($this->tokenizer->ignoredLines[$line]) === true) { return false; } $includeAll = true; if ($this->configCache['cache'] === false || $this->configCache['recordErrors'] === false) { $includeAll = false; } // Work out which sniff generated the message. $parts = explode('.', $code); if ($parts[0] === 'Internal') { // An internal message. $listenerCode = Util\Common::getSniffCode($this->activeListener); $sniffCode = $code; $checkCodes = array($sniffCode); } else { if ($parts[0] !== $code) { // The full message code has been passed in. $sniffCode = $code; $listenerCode = substr($sniffCode, 0, strrpos($sniffCode, '.')); } else { $listenerCode = Util\Common::getSniffCode($this->activeListener); $sniffCode = $listenerCode . '.' . $code; $parts = explode('.', $sniffCode); } $checkCodes = array($sniffCode, $parts[0] . '.' . $parts[1] . '.' . $parts[2], $parts[0] . '.' . $parts[1], $parts[0]); } //end if // Filter out any messages for sniffs that shouldn't have run // due to the use of the --sniffs command line argument. if ($includeAll === false && (empty($this->configCache['sniffs']) === false && in_array($listenerCode, $this->configCache['sniffs']) === false || empty($this->configCache['exclude']) === false && in_array($listenerCode, $this->configCache['exclude']) === true)) { return false; } // If we know this sniff code is being ignored for this file, return early. foreach ($checkCodes as $checkCode) { if (isset($this->ignoredCodes[$checkCode]) === true) { return false; } } $oppositeType = 'warning'; if ($error === false) { $oppositeType = 'error'; } foreach ($checkCodes as $checkCode) { // Make sure this message type has not been set to the opposite message type. if (isset($this->ruleset->ruleset[$checkCode]['type']) === true && $this->ruleset->ruleset[$checkCode]['type'] === $oppositeType) { $error = !$error; break; } } if ($error === true) { $configSeverity = $this->configCache['errorSeverity']; $messageCount =& $this->errorCount; $messages =& $this->errors; } else { $configSeverity = $this->configCache['warningSeverity']; $messageCount =& $this->warningCount; $messages =& $this->warnings; } if ($includeAll === false && $configSeverity === 0) { // Don't bother doing any processing as these messages are just going to // be hidden in the reports anyway. return false; } if ($severity === 0) { $severity = 5; } foreach ($checkCodes as $checkCode) { // Make sure we are interested in this severity level. if (isset($this->ruleset->ruleset[$checkCode]['severity']) === true) { $severity = $this->ruleset->ruleset[$checkCode]['severity']; break; } } if ($includeAll === false && $configSeverity > $severity) { return false; } // Make sure we are not ignoring this file. foreach ($checkCodes as $checkCode) { if (isset($this->configCache['ignorePatterns'][$checkCode]) === false) { continue; } foreach ($this->configCache['ignorePatterns'][$checkCode] as $pattern => $type) { // While there is support for a type of each pattern // (absolute or relative) we don't actually support it here. $replacements = array('\\,' => ',', '*' => '.*'); // We assume a / directory separator, as do the exclude rules // most developers write, so we need a special case for any system // that is different. if (DIRECTORY_SEPARATOR === '\\') { $replacements['/'] = '\\\\'; } $pattern = '`' . strtr($pattern, $replacements) . '`i'; if (preg_match($pattern, $this->path) === 1) { $this->ignoredCodes[$checkCode] = true; return false; } } //end foreach } //end foreach $messageCount++; if ($fixable === true) { $this->fixableCount++; } if ($this->configCache['recordErrors'] === false && $includeAll === false) { return true; } // Work out the error message. if (isset($this->ruleset->ruleset[$sniffCode]['message']) === true) { $message = $this->ruleset->ruleset[$sniffCode]['message']; } if (empty($data) === false) { $message = vsprintf($message, $data); } if (isset($messages[$line]) === false) { $messages[$line] = array(); } if (isset($messages[$line][$column]) === false) { $messages[$line][$column] = array(); } $messages[$line][$column][] = array('message' => $message, 'source' => $sniffCode, 'listener' => $this->activeListener, 'severity' => $severity, 'fixable' => $fixable); if (PHP_CODESNIFFER_VERBOSITY > 1 && $this->fixer->enabled === true && $fixable === true) { @ob_end_clean(); echo "\tE: [Line {$line}] {$message} ({$sniffCode})" . PHP_EOL; ob_start(); } return true; }
/** * Generate a partial report for a single processed file. * * Function should return TRUE if it printed or stored data about the file * and FALSE if it ignored the file. Returning TRUE indicates that the file and * its data should be counted in the grand totals. * * @param array $report Prepared report data. * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on. * @param bool $showSources Show sources? * @param int $width Maximum allowed line width. * * @return bool */ public function generateFileReport($report, File $phpcsFile, $showSources = false, $width = 80) { if ($report['errors'] === 0 && $report['warnings'] === 0) { // Nothing to print. return false; } // How many lines to show about and below the error line. $surroundingLines = 2; $file = $report['filename']; $tokens = $phpcsFile->getTokens(); if (empty($tokens) === true) { if (PHP_CODESNIFFER_VERBOSITY === 1) { $startTime = microtime(true); echo 'CODE report is parsing ' . basename($file) . ' '; } else { if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "CODE report is forcing parse of {$file}" . PHP_EOL; } } $phpcsFile->parse(); if (PHP_CODESNIFFER_VERBOSITY === 1) { $timeTaken = (microtime(true) - $startTime) * 1000; if ($timeTaken < 1000) { $timeTaken = round($timeTaken); echo "DONE in {$timeTaken}ms"; } else { $timeTaken = round($timeTaken / 1000, 2); echo "DONE in {$timeTaken} secs"; } echo PHP_EOL; } $tokens = $phpcsFile->getTokens(); } //end if // Create an array that maps lines to the first token on the line. $lineTokens = array(); $lastLine = 0; foreach ($tokens as $stackPtr => $token) { if ($token['line'] !== $lastLine) { if ($lastLine > 0) { $lineTokens[$lastLine]['end'] = $stackPtr - 1; } $lastLine++; $lineTokens[$lastLine] = array('start' => $stackPtr, 'end' => null); } } // Make sure the last token in the file sits on an imaginary // last line so it is easier to generate code snippets at the // end of the file. $lineTokens[$lastLine]['end'] = $stackPtr; // Determine the longest code line we will be showing. $maxSnippetLength = 0; $eolLen = strlen($phpcsFile->eolChar); foreach ($report['messages'] as $line => $lineErrors) { $startLine = max($line - $surroundingLines, 1); $endLine = min($line + $surroundingLines, $lastLine); $maxLineNumLength = strlen($endLine); for ($i = $startLine; $i <= $endLine; $i++) { if ($i === 1) { continue; } $lineLength = $tokens[$lineTokens[$i]['start'] - 1]['column'] + $tokens[$lineTokens[$i]['start'] - 1]['length'] - $eolLen; $maxSnippetLength = max($lineLength, $maxSnippetLength); } } $maxSnippetLength += $maxLineNumLength + 8; // Determine the longest error message we will be showing. $maxErrorLength = 0; foreach ($report['messages'] as $line => $lineErrors) { foreach ($lineErrors as $column => $colErrors) { foreach ($colErrors as $error) { $length = strlen($error['message']); if ($showSources === true) { $length += strlen($error['source']) + 3; } $maxErrorLength = max($maxErrorLength, $length + 1); } } } // The padding that all lines will require that are printing an error message overflow. if ($report['warnings'] > 0) { $typeLength = 7; } else { $typeLength = 5; } $errorPadding = str_repeat(' ', $maxLineNumLength + 7); $errorPadding .= str_repeat(' ', $typeLength); $errorPadding .= ' '; if ($report['fixable'] > 0) { $errorPadding .= ' '; } $errorPaddingLength = strlen($errorPadding); // The maximum amount of space an error message can use. $maxErrorSpace = $width - $errorPaddingLength; if ($showSources === true) { // Account for the chars used to print colors. $maxErrorSpace += 8; } // Figure out the max report width we need and can use. $fileLength = strlen($file); $maxWidth = max($fileLength + 6, $maxErrorLength + $errorPaddingLength); $width = max(min($width, $maxWidth), $maxSnippetLength); if ($width < 70) { $width = 70; } // Print the file header. echo PHP_EOL . "[1mFILE: "; if ($fileLength <= $width - 6) { echo $file; } else { echo '...' . substr($file, $fileLength - ($width - 6)); } echo "[0m" . PHP_EOL; echo str_repeat('-', $width) . PHP_EOL; echo "[1m" . 'FOUND ' . $report['errors'] . ' ERROR'; if ($report['errors'] !== 1) { echo 'S'; } if ($report['warnings'] > 0) { echo ' AND ' . $report['warnings'] . ' WARNING'; if ($report['warnings'] !== 1) { echo 'S'; } } echo ' AFFECTING ' . count($report['messages']) . ' LINE'; if (count($report['messages']) !== 1) { echo 'S'; } echo "[0m" . PHP_EOL; foreach ($report['messages'] as $line => $lineErrors) { $startLine = max($line - $surroundingLines, 1); $endLine = min($line + $surroundingLines, $lastLine); $snippet = ''; for ($i = $lineTokens[$startLine]['start']; $i <= $lineTokens[$endLine]['end']; $i++) { $snippetLine = $tokens[$i]['line']; if ($lineTokens[$snippetLine]['start'] === $i) { // Starting a new line. if ($snippetLine === $line) { $snippet .= "[1m" . '>> '; } else { $snippet .= ' '; } $snippet .= str_repeat(' ', $maxLineNumLength - strlen($snippetLine)); $snippet .= $snippetLine . ': '; if ($snippetLine === $line) { $snippet .= "[0m"; } } if (isset($tokens[$i]['orig_content']) === true) { $tokenContent = $tokens[$i]['orig_content']; } else { $tokenContent = $tokens[$i]['content']; } if (strpos($tokenContent, "\t") !== false) { $token = $tokens[$i]; $token['content'] = $tokenContent; if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { $tab = ""; } else { $tab = "[30;1m»[0m"; } $phpcsFile->tokenizer->replaceTabsInToken($token, $tab, ""); $tokenContent = $token['content']; } $tokenContent = Util\Common::prepareForOutput($tokenContent, array("\r", "\n", "\t")); $tokenContent = str_replace("", ' ', $tokenContent); $underline = false; if ($snippetLine === $line && isset($lineErrors[$tokens[$i]['column']]) === true) { $underline = true; } // Underline invisible characters as well. if ($underline === true && trim($tokenContent) === '') { $snippet .= "[4m" . ' ' . "[0m" . $tokenContent; } else { if ($underline === true) { $snippet .= "[4m"; } $snippet .= $tokenContent; if ($underline === true) { $snippet .= "[0m"; } } } //end for echo str_repeat('-', $width) . PHP_EOL; foreach ($lineErrors as $column => $colErrors) { foreach ($colErrors as $error) { $padding = $maxLineNumLength - strlen($line); echo 'LINE ' . str_repeat(' ', $padding) . $line . ': '; if ($error['type'] === 'ERROR') { echo "[31mERROR[0m"; if ($report['warnings'] > 0) { echo ' '; } } else { echo "[33mWARNING[0m"; } echo ' '; if ($report['fixable'] > 0) { echo '['; if ($error['fixable'] === true) { echo 'x'; } else { echo ' '; } echo '] '; } $message = $error['message']; $message = str_replace("\n", "\n" . $errorPadding, $message); if ($showSources === true) { $message = "[1m" . $message . "[0m" . ' (' . $error['source'] . ')'; } $errorMsg = wordwrap($message, $maxErrorSpace, PHP_EOL . $errorPadding); echo $errorMsg . PHP_EOL; } //end foreach } //end foreach echo str_repeat('-', $width) . PHP_EOL; echo rtrim($snippet) . PHP_EOL; } //end foreach echo str_repeat('-', $width) . PHP_EOL; if ($report['fixable'] > 0) { echo "[1m" . 'PHPCBF CAN FIX THE ' . $report['fixable'] . ' MARKED SNIFF VIOLATIONS AUTOMATICALLY' . "[0m" . PHP_EOL; echo str_repeat('-', $width) . PHP_EOL; } return true; }
/** * Called to process class member vars. * * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. * @param int $stackPtr The position of the current token * in the stack passed in $tokens. * * @return void */ public function processMemberVar(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); $ignore = array(T_PUBLIC, T_PRIVATE, T_PROTECTED, T_VAR, T_STATIC, T_WHITESPACE); $commentEnd = $phpcsFile->findPrevious($ignore, $stackPtr - 1, null, true); if ($commentEnd === false || $tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG && $tokens[$commentEnd]['code'] !== T_COMMENT) { $phpcsFile->addError('Missing member variable doc comment', $stackPtr, 'Missing'); return; } if ($tokens[$commentEnd]['code'] === T_COMMENT) { $phpcsFile->addError('You must use "/**" style comments for a member variable comment', $stackPtr, 'WrongStyle'); return; } $commentStart = $tokens[$commentEnd]['comment_opener']; $foundVar = null; foreach ($tokens[$commentStart]['comment_tags'] as $tag) { if ($tokens[$tag]['content'] === '@var') { if ($foundVar !== null) { $error = 'Only one @var tag is allowed in a member variable comment'; $phpcsFile->addError($error, $tag, 'DuplicateVar'); } else { $foundVar = $tag; } } else { if ($tokens[$tag]['content'] === '@see') { // Make sure the tag isn't empty. $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd); if ($string === false || $tokens[$string]['line'] !== $tokens[$tag]['line']) { $error = 'Content missing for @see tag in member variable comment'; $phpcsFile->addError($error, $tag, 'EmptySees'); } } else { $error = '%s tag is not allowed in member variable comment'; $data = array($tokens[$tag]['content']); $phpcsFile->addWarning($error, $tag, 'TagNotAllowed', $data); } } //end if } //end foreach // The @var tag is the only one we require. if ($foundVar === null) { $error = 'Missing @var tag in member variable comment'; $phpcsFile->addError($error, $commentEnd, 'MissingVar'); return; } $firstTag = $tokens[$commentStart]['comment_tags'][0]; if ($foundVar !== null && $tokens[$firstTag]['content'] !== '@var') { $error = 'The @var tag must be the first tag in a member variable comment'; $phpcsFile->addError($error, $foundVar, 'VarOrder'); } // Make sure the tag isn't empty and has the correct padding. $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $foundVar, $commentEnd); if ($string === false || $tokens[$string]['line'] !== $tokens[$foundVar]['line']) { $error = 'Content missing for @var tag in member variable comment'; $phpcsFile->addError($error, $foundVar, 'EmptyVar'); return; } $varType = $tokens[$foundVar + 2]['content']; $suggestedType = Common::suggestType($varType); if ($varType !== $suggestedType) { $error = 'Expected "%s" but found "%s" for @var tag in member variable comment'; $data = array($suggestedType, $varType); $fix = $phpcsFile->addFixableError($error, $foundVar + 2, 'IncorrectVarType', $data); if ($fix === true) { $phpcsFile->fixer->replaceToken($foundVar + 2, $suggestedType); } } }
/** * Creates an array of tokens when given some PHP code. * * Starts by using token_get_all() but does a lot of extra processing * to insert information about the context of the token. * * @param string $string The string to tokenize. * * @return array */ protected function tokenize($string) { if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t*** START PHP TOKENIZING ***" . PHP_EOL; $isWin = false; if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { $isWin = true; } } $tokens = @token_get_all($string); $finalTokens = array(); $newStackPtr = 0; $numTokens = count($tokens); $lastNotEmptyToken = 0; $insideInlineIf = array(); $insideUseGroup = false; $commentTokenizer = new Comment(); for ($stackPtr = 0; $stackPtr < $numTokens; $stackPtr++) { $token = (array) $tokens[$stackPtr]; $tokenIsArray = isset($token[1]); if (PHP_CODESNIFFER_VERBOSITY > 1) { if ($tokenIsArray === true) { $type = token_name($token[0]); $content = Util\Common::prepareForOutput($token[1]); } else { $newToken = self::resolveSimpleToken($token[0]); $type = $newToken['type']; $content = Util\Common::prepareForOutput($token[0]); } echo "\tProcess token "; if ($tokenIsArray === true) { echo "[{$stackPtr}]"; } else { echo " {$stackPtr} "; } echo ": {$type} => {$content}"; } //end if if ($newStackPtr > 0 && $finalTokens[$newStackPtr - 1]['code'] !== T_WHITESPACE) { $lastNotEmptyToken = $newStackPtr - 1; } /* If we are using \r\n newline characters, the \r and \n are sometimes split over two tokens. This normally occurs after comments. We need to merge these two characters together so that our line endings are consistent for all lines. */ if ($tokenIsArray === true && substr($token[1], -1) === "\r") { if (isset($tokens[$stackPtr + 1]) === true && is_array($tokens[$stackPtr + 1]) === true && $tokens[$stackPtr + 1][1][0] === "\n") { $token[1] .= "\n"; if (PHP_CODESNIFFER_VERBOSITY > 1) { if ($isWin === true) { echo '\\n'; } else { echo "[30;1m\\n[0m"; } } if ($tokens[$stackPtr + 1][1] === "\n") { // This token's content has been merged into the previous, // so we can skip it. $tokens[$stackPtr + 1] = ''; } else { $tokens[$stackPtr + 1][1] = substr($tokens[$stackPtr + 1][1], 1); } } } //end if if (PHP_CODESNIFFER_VERBOSITY > 1) { echo PHP_EOL; } /* Parse doc blocks into something that can be easily iterated over. */ if ($tokenIsArray === true && $token[0] === T_DOC_COMMENT) { $commentTokens = $commentTokenizer->tokenizeString($token[1], $this->eolChar, $newStackPtr); foreach ($commentTokens as $commentToken) { $finalTokens[$newStackPtr] = $commentToken; $newStackPtr++; } continue; } /* If this is a double quoted string, PHP will tokenize the whole thing which causes problems with the scope map when braces are within the string. So we need to merge the tokens together to provide a single string. */ if ($tokenIsArray === false && ($token[0] === '"' || $token[0] === 'b"')) { // Binary casts need a special token. if ($token[0] === 'b"') { $finalTokens[$newStackPtr] = array('code' => T_BINARY_CAST, 'type' => 'T_BINARY_CAST', 'content' => 'b'); $newStackPtr++; } $tokenContent = '"'; $nestedVars = array(); for ($i = $stackPtr + 1; $i < $numTokens; $i++) { $subToken = (array) $tokens[$i]; $subTokenIsArray = isset($subToken[1]); if ($subTokenIsArray === true) { $tokenContent .= $subToken[1]; if ($subToken[1] === '{' && $subToken[0] !== T_ENCAPSED_AND_WHITESPACE) { $nestedVars[] = $i; } } else { $tokenContent .= $subToken[0]; if ($subToken[0] === '}') { array_pop($nestedVars); } } if ($subTokenIsArray === false && $subToken[0] === '"' && empty($nestedVars) === true) { // We found the other end of the double quoted string. break; } } //end for $stackPtr = $i; // Convert each line within the double quoted string to a // new token, so it conforms with other multiple line tokens. $tokenLines = explode($this->eolChar, $tokenContent); $numLines = count($tokenLines); $newToken = array(); for ($j = 0; $j < $numLines; $j++) { $newToken['content'] = $tokenLines[$j]; if ($j === $numLines - 1) { if ($tokenLines[$j] === '') { break; } } else { $newToken['content'] .= $this->eolChar; } $newToken['code'] = T_DOUBLE_QUOTED_STRING; $newToken['type'] = 'T_DOUBLE_QUOTED_STRING'; $finalTokens[$newStackPtr] = $newToken; $newStackPtr++; } // Continue, as we're done with this token. continue; } //end if /* If this is a heredoc, PHP will tokenize the whole thing which causes problems when heredocs don't contain real PHP code, which is almost never. We want to leave the start and end heredoc tokens alone though. */ if ($tokenIsArray === true && $token[0] === T_START_HEREDOC) { // Add the start heredoc token to the final array. $finalTokens[$newStackPtr] = self::standardiseToken($token); // Check if this is actually a nowdoc and use a different token // to help the sniffs. $nowdoc = false; if ($token[1][3] === "'") { $finalTokens[$newStackPtr]['code'] = T_START_NOWDOC; $finalTokens[$newStackPtr]['type'] = 'T_START_NOWDOC'; $nowdoc = true; } $tokenContent = ''; for ($i = $stackPtr + 1; $i < $numTokens; $i++) { $subTokenIsArray = is_array($tokens[$i]); if ($subTokenIsArray === true && $tokens[$i][0] === T_END_HEREDOC) { // We found the other end of the heredoc. break; } if ($subTokenIsArray === true) { $tokenContent .= $tokens[$i][1]; } else { $tokenContent .= $tokens[$i]; } } if ($i === $numTokens) { // We got to the end of the file and never // found the closing token, so this probably wasn't // a heredoc. if (PHP_CODESNIFFER_VERBOSITY > 1) { $type = $finalTokens[$newStackPtr]['type']; echo "\t\t* failed to find the end of the here/nowdoc" . PHP_EOL; echo "\t\t* token {$stackPtr} changed from {$type} to T_STRING" . PHP_EOL; } $finalTokens[$newStackPtr]['code'] = T_STRING; $finalTokens[$newStackPtr]['type'] = 'T_STRING'; $newStackPtr++; continue; } $stackPtr = $i; $newStackPtr++; // Convert each line within the heredoc to a // new token, so it conforms with other multiple line tokens. $tokenLines = explode($this->eolChar, $tokenContent); $numLines = count($tokenLines); $newToken = array(); for ($j = 0; $j < $numLines; $j++) { $newToken['content'] = $tokenLines[$j]; if ($j === $numLines - 1) { if ($tokenLines[$j] === '') { break; } } else { $newToken['content'] .= $this->eolChar; } if ($nowdoc === true) { $newToken['code'] = T_NOWDOC; $newToken['type'] = 'T_NOWDOC'; } else { $newToken['code'] = T_HEREDOC; $newToken['type'] = 'T_HEREDOC'; } $finalTokens[$newStackPtr] = $newToken; $newStackPtr++; } //end for // Add the end heredoc token to the final array. $finalTokens[$newStackPtr] = self::standardiseToken($tokens[$stackPtr]); if ($nowdoc === true) { $finalTokens[$newStackPtr]['code'] = T_END_NOWDOC; $finalTokens[$newStackPtr]['type'] = 'T_END_NOWDOC'; $nowdoc = true; } $newStackPtr++; // Continue, as we're done with this token. continue; } //end if /* Before PHP 5.6, the ... operator was tokenized as three T_STRING_CONCAT tokens in a row. So look for and combine these tokens in earlier versions. */ if ($tokenIsArray === false && $token[0] === '.' && isset($tokens[$stackPtr + 1]) === true && isset($tokens[$stackPtr + 2]) === true && $tokens[$stackPtr + 1] === '.' && $tokens[$stackPtr + 2] === '.') { $newToken = array(); $newToken['code'] = T_ELLIPSIS; $newToken['type'] = 'T_ELLIPSIS'; $newToken['content'] = '...'; $finalTokens[$newStackPtr] = $newToken; $newStackPtr++; $stackPtr += 2; continue; } /* Before PHP 5.6, the ** operator was tokenized as two T_MULTIPLY tokens in a row. So look for and combine these tokens in earlier versions. */ if ($tokenIsArray === false && $token[0] === '*' && isset($tokens[$stackPtr + 1]) === true && $tokens[$stackPtr + 1] === '*') { $newToken = array(); $newToken['code'] = T_POW; $newToken['type'] = 'T_POW'; $newToken['content'] = '**'; $finalTokens[$newStackPtr] = $newToken; $newStackPtr++; $stackPtr++; continue; } /* Before PHP 5.6, the **= operator was tokenized as T_MULTIPLY followed by T_MUL_EQUAL. So look for and combine these tokens in earlier versions. */ if ($tokenIsArray === false && $token[0] === '*' && isset($tokens[$stackPtr + 1]) === true && is_array($tokens[$stackPtr + 1]) === true && $tokens[$stackPtr + 1][1] === '*=') { $newToken = array(); $newToken['code'] = T_POW_EQUAL; $newToken['type'] = 'T_POW_EQUAL'; $newToken['content'] = '**='; $finalTokens[$newStackPtr] = $newToken; $newStackPtr++; $stackPtr++; continue; } /* Before PHP 7, the ?? operator was tokenized as T_INLINE_THEN followed by T_INLINE_THEN. So look for and combine these tokens in earlier versions. */ if ($tokenIsArray === false && $token[0] === '?' && isset($tokens[$stackPtr + 1]) === true && $tokens[$stackPtr + 1][0] === '?') { $newToken = array(); $newToken['code'] = T_COALESCE; $newToken['type'] = 'T_COALESCE'; $newToken['content'] = '??'; $finalTokens[$newStackPtr] = $newToken; $newStackPtr++; $stackPtr++; continue; } /* Before PHP 7, the <=> operator was tokenized as T_IS_SMALLER_OR_EQUAL followed by T_GREATER_THAN. So look for and combine these tokens in earlier versions. */ if ($tokenIsArray === true && $token[0] === T_IS_SMALLER_OR_EQUAL && isset($tokens[$stackPtr + 1]) === true && $tokens[$stackPtr + 1][0] === '>') { $newToken = array(); $newToken['code'] = T_SPACESHIP; $newToken['type'] = 'T_SPACESHIP'; $newToken['content'] = '<=>'; $finalTokens[$newStackPtr] = $newToken; $newStackPtr++; $stackPtr++; continue; } /* Emulate traits in PHP versions less than 5.4. */ if ($tokenIsArray === true && $token[0] === T_STRING && strtolower($token[1]) === 'trait' && $tokens[$stackPtr - 1][0] !== T_OBJECT_OPERATOR) { $finalTokens[$newStackPtr] = array('content' => $token[1], 'code' => T_TRAIT, 'type' => 'T_TRAIT'); if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t\t* token {$stackPtr} changed from T_STRING to T_TRAIT" . PHP_EOL; } $newStackPtr++; continue; } /* PHP doesn't assign a token to goto labels, so we have to. These are just string tokens with a single colon after them. Double colons are already tokenized and so don't interfere with this check. But we do have to account for CASE statements, that look just like goto labels. */ if ($tokenIsArray === true && $token[0] === T_STRING && isset($tokens[$stackPtr + 1]) === true && $tokens[$stackPtr + 1] === ':' && $tokens[$stackPtr - 1][0] !== T_PAAMAYIM_NEKUDOTAYIM) { $stopTokens = array(T_CASE => true, T_SEMICOLON => true, T_OPEN_CURLY_BRACKET => true, T_INLINE_THEN => true); for ($x = $newStackPtr - 1; $x > 0; $x--) { if (isset($stopTokens[$finalTokens[$x]['code']]) === true) { break; } } if ($finalTokens[$x]['code'] !== T_CASE && $finalTokens[$x]['code'] !== T_INLINE_THEN) { $finalTokens[$newStackPtr] = array('content' => $token[1] . ':', 'code' => T_GOTO_LABEL, 'type' => 'T_GOTO_LABEL'); if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t\t* token {$stackPtr} changed from T_STRING to T_GOTO_LABEL" . PHP_EOL; echo "\t\t* skipping T_COLON token " . ($stackPtr + 1) . PHP_EOL; } $newStackPtr++; $stackPtr++; continue; } } //end if /* HHVM 3.5 tokenizes "else[\s]+if" as a T_ELSEIF token while PHP proper only tokenizes "elseif" as a T_ELSEIF token. So split up the HHVM token to make it looks like proper PHP. */ if ($tokenIsArray === true && $token[0] === T_ELSEIF && strtolower($token[1]) !== 'elseif') { $finalTokens[$newStackPtr] = array('content' => substr($token[1], 0, 4), 'code' => T_ELSE, 'type' => 'T_ELSE'); $newStackPtr++; $finalTokens[$newStackPtr] = array('content' => substr($token[1], 4, -2), 'code' => T_WHITESPACE, 'type' => 'T_WHITESPACE'); $newStackPtr++; $finalTokens[$newStackPtr] = array('content' => substr($token[1], -2), 'code' => T_IF, 'type' => 'T_IF'); if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t\t* token {$stackPtr} changed from T_ELSEIF to T_ELSE/T_WHITESPACE/T_IF" . PHP_EOL; } $newStackPtr++; continue; } //end if /* HHVM 3.5 and 3.6 tokenizes a hashbang line such as #!/usr/bin/php as T_HASHBANG while PHP proper uses T_INLINE_HTML. */ if ($tokenIsArray === true && token_name($token[0]) === 'T_HASHBANG') { $finalTokens[$newStackPtr] = array('content' => $token[1], 'code' => T_INLINE_HTML, 'type' => 'T_INLINE_HTML'); if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t\t* token {$stackPtr} changed from T_HASHBANG to T_INLINE_HTML" . PHP_EOL; } $newStackPtr++; continue; } //end if /* If this token has newlines in its content, split each line up and create a new token for each line. We do this so it's easier to ascertain where errors occur on a line. Note that $token[1] is the token's content. */ if ($tokenIsArray === true && strpos($token[1], $this->eolChar) !== false) { $tokenLines = explode($this->eolChar, $token[1]); $numLines = count($tokenLines); $newToken = array('type' => token_name($token[0]), 'code' => $token[0], 'content' => ''); for ($i = 0; $i < $numLines; $i++) { $newToken['content'] = $tokenLines[$i]; if ($i === $numLines - 1) { if ($tokenLines[$i] === '') { break; } } else { $newToken['content'] .= $this->eolChar; } $finalTokens[$newStackPtr] = $newToken; $newStackPtr++; } } else { if ($tokenIsArray === true && $token[0] === T_STRING) { // Some T_STRING tokens should remain that way // due to their context. $context = array(T_OBJECT_OPERATOR => true, T_FUNCTION => true, T_CLASS => true, T_EXTENDS => true, T_IMPLEMENTS => true, T_NEW => true, T_CONST => true, T_NS_SEPARATOR => true, T_USE => true, T_NAMESPACE => true, T_PAAMAYIM_NEKUDOTAYIM => true); if (isset($context[$finalTokens[$lastNotEmptyToken]['code']]) === true) { $finalTokens[$newStackPtr] = array('content' => $token[1], 'code' => T_STRING, 'type' => 'T_STRING'); $newStackPtr++; continue; } } //end if $newToken = null; if ($tokenIsArray === false) { if (isset(self::$resolveTokenCache[$token[0]]) === true) { $newToken = self::$resolveTokenCache[$token[0]]; } } else { $cacheKey = null; if ($token[0] === T_STRING) { $cacheKey = strtolower($token[1]); } else { if ($token[0] !== T_CURLY_OPEN) { $cacheKey = $token[0]; } } if ($cacheKey !== null && isset(self::$resolveTokenCache[$cacheKey]) === true) { $newToken = self::$resolveTokenCache[$cacheKey]; $newToken['content'] = $token[1]; } } if ($newToken === null) { $newToken = self::standardiseToken($token); } // Convert colons that are actually the ELSE component of an // inline IF statement. if ($newToken['code'] === T_INLINE_THEN) { $insideInlineIf[] = $stackPtr; } else { if (empty($insideInlineIf) === false && $newToken['code'] === T_COLON) { array_pop($insideInlineIf); $newToken['code'] = T_INLINE_ELSE; $newToken['type'] = 'T_INLINE_ELSE'; } } // This is a special condition for T_ARRAY tokens used for // type hinting function arguments as being arrays. We want to keep // the parenthesis map clean, so let's tag these tokens as // T_ARRAY_HINT. if ($newToken['code'] === T_ARRAY) { for ($i = $stackPtr; $i < $numTokens; $i++) { if ($tokens[$i] === '(') { break; } else { if ($tokens[$i][0] === T_VARIABLE) { $newToken['code'] = T_ARRAY_HINT; $newToken['type'] = 'T_ARRAY_HINT'; break; } } } } // This is a special case when checking PHP 5.5+ code in PHP < 5.5 // where "finally" should be T_FINALLY instead of T_STRING. if ($newToken['code'] === T_STRING && strtolower($newToken['content']) === 'finally') { $newToken['code'] = T_FINALLY; $newToken['type'] = 'T_FINALLY'; } // This is a special case for the PHP 5.5 classname::class syntax // where "class" should be T_STRING instead of T_CLASS. if ($newToken['code'] === T_CLASS && $finalTokens[$newStackPtr - 1]['code'] === T_DOUBLE_COLON) { $newToken['code'] = T_STRING; $newToken['type'] = 'T_STRING'; } // This is a special case for PHP 5.6 use function and use const // where "function" and "const" should be T_STRING instead of T_FUNCTION // and T_CONST. if (($newToken['code'] === T_FUNCTION || $newToken['code'] === T_CONST) && $finalTokens[$lastNotEmptyToken]['code'] === T_USE) { $newToken['code'] = T_STRING; $newToken['type'] = 'T_STRING'; } // This is a special case for use groups in PHP 7+ where leaving // the curly braces as their normal tokens would confuse // the scope map and sniffs. if ($newToken['code'] === T_OPEN_CURLY_BRACKET && $finalTokens[$lastNotEmptyToken]['code'] === T_NS_SEPARATOR) { $newToken['code'] = T_OPEN_USE_GROUP; $newToken['type'] = 'T_OPEN_USE_GROUP'; $insideUseGroup = true; } if ($insideUseGroup === true && $newToken['code'] === T_CLOSE_CURLY_BRACKET) { $newToken['code'] = T_CLOSE_USE_GROUP; $newToken['type'] = 'T_CLOSE_USE_GROUP'; $insideUseGroup = false; } $finalTokens[$newStackPtr] = $newToken; $newStackPtr++; } //end if } //end for if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t*** END PHP TOKENIZING ***" . PHP_EOL; } return $finalTokens; }
/** * Process the subpackage tag. * * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. * @param array $tags The tokens for these tags. * * @return void */ protected function processSubpackage($phpcsFile, array $tags) { $tokens = $phpcsFile->getTokens(); foreach ($tags as $tag) { if ($tokens[$tag + 2]['code'] !== T_DOC_COMMENT_STRING) { // No content. continue; } $content = $tokens[$tag + 2]['content']; if (Common::isUnderscoreName($content) === true) { continue; } $newContent = str_replace(' ', '_', $content); $nameBits = explode('_', $newContent); $firstBit = array_shift($nameBits); $newName = strtoupper($firstBit[0]) . substr($firstBit, 1) . '_'; foreach ($nameBits as $bit) { if ($bit !== '') { $newName .= strtoupper($bit[0]) . substr($bit, 1) . '_'; } } $error = 'Subpackage name "%s" is not valid; consider "%s" instead'; $validName = trim($newName, '_'); $data = array($content, $validName); $phpcsFile->addError($error, $tag, 'InvalidSubpackage', $data); } //end foreach }