diff --git a/src/lint/linter/ArcanistJSHintLinter.php b/src/lint/linter/ArcanistJSHintLinter.php index ac6ecc31..9a066dae 100644 --- a/src/lint/linter/ArcanistJSHintLinter.php +++ b/src/lint/linter/ArcanistJSHintLinter.php @@ -1,163 +1,171 @@ ArcanistLintSeverity::SEVERITY_ERROR ); } public function getLintNameMap() { return array( self::JSHINT_ERROR => "JSHint Error" ); } public function getJSHintOptions() { $working_copy = $this->getEngine()->getWorkingCopy(); $options = '--reporter '.dirname(realpath(__FILE__)).'/reporter.js'; $config = $working_copy->getConfig('lint.jshint.config'); if ($config !== null) { $config = Filesystem::resolvePath( $config, $working_copy->getProjectRoot()); if (!Filesystem::pathExists($config)) { throw new ArcanistUsageException( "Unable to find custom options file defined by ". "'lint.jshint.config'. Make sure that the path is correct."); } $options .= ' --config '.$config; } return $options; } private function getJSHintPath() { $working_copy = $this->getEngine()->getWorkingCopy(); $prefix = $working_copy->getConfig('lint.jshint.prefix'); $bin = $working_copy->getConfig('lint.jshint.bin'); if ($bin === null) { $bin = "jshint"; } if ($prefix !== null) { $bin = $prefix."/".$bin; if (!Filesystem::pathExists($bin)) { throw new ArcanistUsageException( "Unable to find JSHint binary in a specified directory. Make sure ". "that 'lint.jshint.prefix' and 'lint.jshint.bin' keys are set ". "correctly. If you'd rather use a copy of JSHint installed ". "globally, you can just remove these keys from your .arcconfig"); } return $bin; } // Look for globally installed JSHint list($err) = (phutil_is_windows() ? exec_manual('where %s', $bin) : exec_manual('which %s', $bin)); if ($err) { throw new ArcanistUsageException( "JSHint does not appear to be installed on this system. Install it ". "(e.g., with 'npm install jshint -g') or configure ". "'lint.jshint.prefix' in your .arcconfig to point to the directory ". "where it resides."); } return $bin; } public function willLintPaths(array $paths) { + if (!$this->isCodeEnabled(self::JSHINT_ERROR)) { + return; + } + $jshint_bin = $this->getJSHintPath(); $jshint_options = $this->getJSHintOptions(); $futures = array(); foreach ($paths as $path) { $filepath = $this->getEngine()->getFilePathOnDisk($path); $futures[$path] = new ExecFuture( "%s %s %C", $jshint_bin, $filepath, $jshint_options); } foreach (Futures($futures)->limit(8) as $path => $future) { $this->results[$path] = $future->resolve(); } } public function lintPath($path) { + if (!$this->isCodeEnabled(self::JSHINT_ERROR)) { + return; + } + list($rc, $stdout, $stderr) = $this->results[$path]; if ($rc === 0) { return; } $errors = json_decode($stdout); if (!is_array($errors)) { // Something went wrong and we can't decode the output. Exit abnormally. throw new ArcanistUsageException( "JSHint returned unparseable output.\n". "stdout:\n\n{$stdout}". "stderr:\n\n{$stderr}"); } foreach ($errors as $err) { $this->raiseLintAtLine( $err->line, $err->col, self::JSHINT_ERROR, $err->reason); } } } diff --git a/src/lint/linter/ArcanistPEP8Linter.php b/src/lint/linter/ArcanistPEP8Linter.php index 735cfcfd..3e4a51a0 100644 --- a/src/lint/linter/ArcanistPEP8Linter.php +++ b/src/lint/linter/ArcanistPEP8Linter.php @@ -1,119 +1,125 @@ getPEP8Path()); return $stdout.$this->getPEP8Options(); } public function getPEP8Options() { $working_copy = $this->getEngine()->getWorkingCopy(); $options = $working_copy->getConfig('lint.pep8.options'); if ($options === null) { $options = $this->getConfig('options'); } return $options; } public function getPEP8Path() { $working_copy = $this->getEngine()->getWorkingCopy(); $prefix = $working_copy->getConfig('lint.pep8.prefix'); $bin = $working_copy->getConfig('lint.pep8.bin'); if ($bin === null && $prefix === null) { $bin = csprintf('/usr/bin/env python %s', phutil_get_library_root('arcanist'). '/../externals/pep8/pep8.py'); } else { if ($bin === null) { $bin = 'pep8'; } if ($prefix !== null) { if (!Filesystem::pathExists($prefix.'/'.$bin)) { throw new ArcanistUsageException( "Unable to find PEP8 binary in a specified directory. Make sure ". "that 'lint.pep8.prefix' and 'lint.pep8.bin' keys are set ". "correctly. If you'd rather use a copy of PEP8 installed ". "globally, you can just remove these keys from your .arcconfig."); } $bin = csprintf("%s/%s", $prefix, $bin); return $bin; } // Look for globally installed PEP8 list($err) = exec_manual('which %s', $bin); if ($err) { throw new ArcanistUsageException( "PEP8 does not appear to be installed on this system. Install it ". "(e.g., with 'easy_install pep8') or configure ". "'lint.pep8.prefix' in your .arcconfig to point to the directory ". "where it resides."); } } return $bin; } public function lintPath($path) { + $severity = ArcanistLintSeverity::SEVERITY_WARNING; + + if (!$this->getEngine()->isSeverityEnabled($severity)) { + return; + } + $pep8_bin = $this->getPEP8Path(); $options = $this->getPEP8Options(); list($rc, $stdout) = exec_manual( "%C %C %s", $pep8_bin, $options, $this->getEngine()->getFilePathOnDisk($path)); $lines = explode("\n", $stdout); $messages = array(); foreach ($lines as $line) { $matches = null; if (!preg_match('/^(.*?):(\d+):(\d+): (\S+) (.*)$/', $line, $matches)) { continue; } foreach ($matches as $key => $match) { $matches[$key] = trim($match); } if (!$this->isMessageEnabled($matches[4])) { continue; } $message = new ArcanistLintMessage(); $message->setPath($path); $message->setLine($matches[2]); $message->setChar($matches[3]); $message->setCode($matches[4]); $message->setName('PEP8 '.$matches[4]); $message->setDescription($matches[5]); - $message->setSeverity(ArcanistLintSeverity::SEVERITY_WARNING); + $message->setSeverity($severity); $this->addLintMessage($message); } } } diff --git a/src/lint/linter/ArcanistPhutilXHPASTLinter.php b/src/lint/linter/ArcanistPhutilXHPASTLinter.php index bd98b3da..7d03bbbf 100644 --- a/src/lint/linter/ArcanistPhutilXHPASTLinter.php +++ b/src/lint/linter/ArcanistPhutilXHPASTLinter.php @@ -1,202 +1,213 @@ xhpastLinter = $linter; return $this; } public function setDeprecatedFunctions($map) { $this->deprecatedFunctions = $map; return $this; } public function setDynamicStringFunctions($map) { $this->dynamicStringFunctions = $map; return $this; } public function setDynamicStringClasses($map) { $this->dynamicStringClasses = $map; return $this; } public function setEngine(ArcanistLintEngine $engine) { if (!$this->xhpastLinter) { throw new Exception( 'Call setXHPASTLinter() before using ArcanistPhutilXHPASTLinter.'); } $this->xhpastLinter->setEngine($engine); return parent::setEngine($engine); } public function getLintNameMap() { return array( self::LINT_ARRAY_COMBINE => 'array_combine() Unreliable', self::LINT_DEPRECATED_FUNCTION => 'Use of Deprecated Function', self::LINT_UNSAFE_DYNAMIC_STRING => 'Unsafe Usage of Dynamic String', ); } public function getLintSeverityMap() { $warning = ArcanistLintSeverity::SEVERITY_WARNING; return array( self::LINT_ARRAY_COMBINE => $warning, self::LINT_DEPRECATED_FUNCTION => $warning, self::LINT_UNSAFE_DYNAMIC_STRING => $warning, ); } public function getLinterName() { return 'PHLXHP'; } public function getCacheVersion() { return 2; } public function willLintPaths(array $paths) { $this->xhpastLinter->willLintPaths($paths); } public function lintPath($path) { $tree = $this->xhpastLinter->getXHPASTTreeForPath($path); if (!$tree) { return; } $root = $tree->getRootNode(); - $this->lintArrayCombine($root); - $this->lintUnsafeDynamicString($root); - $this->lintDeprecatedFunctions($root); + $method_codes = array( + 'lintArrayCombine' => self::LINT_ARRAY_COMBINE, + 'lintUnsafeDynamicString' => self::LINT_UNSAFE_DYNAMIC_STRING, + 'lintDeprecatedFunctions' => self::LINT_DEPRECATED_FUNCTION, + ); + + foreach ($method_codes as $method => $codes) { + foreach ((array)$codes as $code) { + if ($this->isCodeEnabled($code)) { + call_user_func(array($this, $method), $root); + break; + } + } + } } private function lintUnsafeDynamicString($root) { $safe = $this->dynamicStringFunctions + array( 'pht' => 0, 'hsprintf' => 0, 'csprintf' => 0, 'vcsprintf' => 0, 'execx' => 0, 'exec_manual' => 0, 'phutil_passthru' => 0, 'qsprintf' => 1, 'vqsprintf' => 1, 'queryfx' => 1, 'vqueryfx' => 1, 'queryfx_all' => 1, 'vqueryfx_all' => 1, 'queryfx_one' => 1, ); $calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); $this->lintUnsafeDynamicStringCall($calls, $safe); $safe = $this->dynamicStringClasses + array( 'ExecFuture' => 0, ); $news = $root->selectDescendantsOfType('n_NEW'); $this->lintUnsafeDynamicStringCall($news, $safe); } private function lintUnsafeDynamicStringCall( AASTNodeList $calls, array $safe) { $safe = array_combine( array_map('strtolower', array_keys($safe)), $safe); foreach ($calls as $call) { $name = $call->getChildByIndex(0)->getConcreteString(); $param = idx($safe, strtolower($name)); if ($param === null) { continue; } $parameters = $call->getChildByIndex(1); if (count($parameters->getChildren()) <= $param) { continue; } $identifier = $parameters->getChildByIndex($param); if (!$identifier->isConstantString()) { $this->raiseLintAtNode( $call, self::LINT_UNSAFE_DYNAMIC_STRING, "Parameter ".($param + 1)." of {$name}() should be a scalar string, ". "otherwise it's not safe."); } } } private function lintArrayCombine($root) { $function_calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); foreach ($function_calls as $call) { $name = $call->getChildByIndex(0)->getConcreteString(); if (strcasecmp($name, 'array_combine') == 0) { $parameter_list = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST'); if (count($parameter_list->getChildren()) !== 2) { // Wrong number of parameters, but raise that elsewhere if we want. continue; } $first = $parameter_list->getChildByIndex(0); $second = $parameter_list->getChildByIndex(1); if ($first->getConcreteString() == $second->getConcreteString()) { $this->raiseLintAtNode( $call, self::LINT_ARRAY_COMBINE, 'Prior to PHP 5.4, array_combine() fails when given empty '. 'arrays. Prefer to write array_combine(x, x) as array_fuse(x).'); } } } } private function lintDeprecatedFunctions($root) { $map = $this->deprecatedFunctions + array( 'phutil_render_tag' => 'The phutil_render_tag() function is deprecated and unsafe. '. 'Use phutil_tag() instead.', ); $function_calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); foreach ($function_calls as $call) { $name = $call->getChildByIndex(0)->getConcreteString(); $name = strtolower($name); if (empty($map[$name])) { continue; } $this->raiseLintAtNode( $call, self::LINT_DEPRECATED_FUNCTION, $map[$name]); } } } diff --git a/src/lint/linter/ArcanistSpellingLinter.php b/src/lint/linter/ArcanistSpellingLinter.php index 032e51e1..928bad43 100644 --- a/src/lint/linter/ArcanistSpellingLinter.php +++ b/src/lint/linter/ArcanistSpellingLinter.php @@ -1,157 +1,164 @@ severity = $severity; $this->wholeWordRules = ArcanistSpellingDefaultData::getFullWordRules(); $this->partialWordRules = ArcanistSpellingDefaultData::getPartialWordRules(); } public function willLintPaths(array $paths) { return; } public function getLinterName() { return 'SPELL'; } public function removeLintRule($word) { foreach ($this->partialWordRules as $severity=>&$wordlist) { unset($wordlist[$word]); } foreach ($this->wholeWordRules as $severity=>&$wordlist) { unset($wordlist[$word]); } } public function addPartialWordRule( $incorrect_word, $correct_word, $severity=self::LINT_SPELLING_IMPORTANT) { $this->partialWordRules[$severity][$incorrect_word] = $correct_word; } public function addWholeWordRule( $incorrect_word, $correct_word, $severity=self::LINT_SPELLING_IMPORTANT) { $this->wholeWordRules[$severity][$incorrect_word] = $correct_word; } public function getLintSeverityMap() { return array( self::LINT_SPELLING_PICKY => ArcanistLintSeverity::SEVERITY_WARNING, self::LINT_SPELLING_IMPORTANT => ArcanistLintSeverity::SEVERITY_ERROR, ); } public function getLintNameMap() { return array( self::LINT_SPELLING_PICKY => 'Possible spelling mistake', self::LINT_SPELLING_IMPORTANT => 'Possible spelling mistake', ); } public function lintPath($path) { foreach ($this->partialWordRules as $severity => $wordlist) { if ($severity >= $this->severity) { + if (!$this->isCodeEnabled($severity)) { + continue; + } foreach ($wordlist as $misspell => $correct) { $this->checkPartialWord($path, $misspell, $correct, $severity); } } } + foreach ($this->wholeWordRules as $severity => $wordlist) { if ($severity >= $this->severity) { + if (!$this->isCodeEnabled($severity)) { + continue; + } foreach ($wordlist as $misspell => $correct) { $this->checkWholeWord($path, $misspell, $correct, $severity); } } } } protected function checkPartialWord($path, $word, $correct_word, $severity) { $text = $this->getData($path); $pos = 0; while ($pos < strlen($text)) { $next = stripos($text, $word, $pos); if ($next === false) { return; } $original = substr($text, $next, strlen($word)); $replacement = self::fixLetterCase($correct_word, $original); $this->raiseLintAtOffset( $next, $severity, sprintf( "Possible spelling error. You wrote '%s', but did you mean '%s'?", $word, $correct_word ), $original, $replacement ); $pos = $next + 1; } } protected function checkWholeWord($path, $word, $correct_word, $severity) { $text = $this->getData($path); $matches = array(); $num_matches = preg_match_all( '#\b' . preg_quote($word, '#') . '\b#i', $text, $matches, PREG_OFFSET_CAPTURE ); if (!$num_matches) { return; } foreach ($matches[0] as $match) { $original = $match[0]; $replacement = self::fixLetterCase($correct_word, $original); $this->raiseLintAtOffset( $match[1], $severity, sprintf( "Possible spelling error. You wrote '%s', but did you mean '%s'?", $word, $correct_word ), $original, $replacement ); } } public static function fixLetterCase($string, $case) { if ($case == strtolower($case)) { return strtolower($string); } if ($case == strtoupper($case)) { return strtoupper($string); } if ($case == ucwords(strtolower($case))) { return ucwords(strtolower($string)); } return null; } }