diff --git a/.arcconfig b/.arcconfig index f112192e5..0ca12325b 100644 --- a/.arcconfig +++ b/.arcconfig @@ -1,9 +1,8 @@ { "project.name" : "phabricator", "phabricator.uri" : "https://secure.phabricator.com/", - "lint.engine" : "PhabricatorLintEngine", "unit.engine" : "PhutilUnitTestEngine", "load" : ["src/"], "lint.xhpast.naminghook" : "PhabricatorSymbolNameLinter", "lint.jshint.config" : "support/jshint/jshintconfig" } diff --git a/.arclint b/.arclint new file mode 100644 index 000000000..809327d32 --- /dev/null +++ b/.arclint @@ -0,0 +1,61 @@ +{ + "exclude": [ + "(^externals/)", + "(\\.lint-test$)" + ], + "linters": { + "filename": { + "type": "filename" + }, + "javelin": { + "type": "javelin", + "include": "(\\.js$)", + "exclude": [ + "(^externals/JsShrink/)", + "(^support/aphlict/)", + "(^webroot/rsrc/externals/raphael/)" + ] + }, + "jshint": { + "type": "jshint", + "include": "(\\.js$)", + "exclude": [ + "(^externals/JsShrink/)", + "(^webroot/rsrc/externals/raphael/)" + ] + }, + "generated": { + "type": "generated" + }, + "merge-conflict": { + "type": "merge-conflict" + }, + "nolint": { + "type": "nolint" + }, + "phutil-xhpast": { + "type": "phutil-xhpast", + "include": "(\\.php$)", + "phutil-xhpast.deprecated.functions": { + "phutil_escape_html": "The phutil_escape_html() function is deprecated. Raw strings passed to phutil_tag() or hsprintf() are escaped automatically." + } + }, + "text": { + "type": "text" + }, + "spelling": { + "type": "spelling" + }, + "xhpast": { + "type": "xhpast", + "include": "(\\.php$)", + "severity": { + "16": "advice", + "29": "warning", + "31": "error", + "34": "error", + "35": "error" + } + } + } +} diff --git a/src/infrastructure/lint/PhabricatorLintEngine.php b/src/infrastructure/lint/PhabricatorLintEngine.php deleted file mode 100644 index 37722ba99..000000000 --- a/src/infrastructure/lint/PhabricatorLintEngine.php +++ /dev/null @@ -1,66 +0,0 @@ -setDeprecatedFunctions(array( - 'phutil_escape_html' => - 'The phutil_escape_html() function is deprecated. Raw strings '. - 'passed to phutil_tag() or hsprintf() are escaped automatically.', - )); - } - } - - $paths = $this->getPaths(); - - foreach ($paths as $key => $path) { - if (!$this->pathExists($path)) { - unset($paths[$key]); - } - } - - $javelin_linter = new PhabricatorJavelinLinter(); - $linters[] = $javelin_linter; - - $jshint_linter = new ArcanistJSHintLinter(); - $linters[] = $jshint_linter; - - foreach ($paths as $path) { - if (!preg_match('/\.js$/', $path)) { - continue; - } - - if (strpos($path, 'externals/JsShrink') !== false) { - // Ignore warnings in JsShrink tests. - continue; - } - - if (strpos($path, 'externals/raphael') !== false) { - // Ignore Raphael. - continue; - } - - $jshint_linter->addPath($path); - $jshint_linter->addData($path, $this->loadData($path)); - - if (strpos($path, 'support/aphlict/') !== false) { - // This stuff is Node.js, not Javelin, so don't apply the Javelin - // linter. - continue; - } - - $javelin_linter->addPath($path); - $javelin_linter->addData($path, $this->loadData($path)); - } - - return $linters; - } - -} diff --git a/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php b/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php index 4c73e10d8..d4d04b555 100644 --- a/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php +++ b/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php @@ -1,265 +1,269 @@ symbolsBinary === null) { list($err, $stdout) = exec_manual('which javelinsymbols'); $this->symbolsBinary = ($err ? false : rtrim($stdout)); } return $this->symbolsBinary; } public function willLintPaths(array $paths) { if (!$this->getBinaryPath()) { return; } $root = dirname(phutil_get_library_root('phabricator')); require_once $root.'/scripts/__init_script__.php'; $futures = array(); foreach ($paths as $path) { if ($this->shouldIgnorePath($path)) { continue; } $future = $this->newSymbolsFuture($path); $futures[$path] = $future; } foreach (Futures($futures)->limit(8) as $path => $future) { $this->symbols[$path] = $future->resolvex(); } } public function getLinterName() { return 'JAVELIN'; } + public function getLinterConfigurationName() { + return 'javelin'; + } + public function getLintSeverityMap() { return array( self::LINT_MISSING_BINARY => ArcanistLintSeverity::SEVERITY_WARNING, ); } public function getLintNameMap() { return array( self::LINT_PRIVATE_ACCESS => 'Private Method/Member Access', self::LINT_MISSING_DEPENDENCY => 'Missing Javelin Dependency', self::LINT_UNNECESSARY_DEPENDENCY => 'Unnecessary Javelin Dependency', self::LINT_UNKNOWN_DEPENDENCY => 'Unknown Javelin Dependency', self::LINT_MISSING_BINARY => '`javelinsymbols` Not In Path', ); } public function getCacheGranularity() { return ArcanistLinter::GRANULARITY_REPOSITORY; } public function getCacheVersion() { $version = '0'; $binary_path = $this->getBinaryPath(); if ($binary_path) { $version .= '-'.md5_file($binary_path); } return $version; } private function shouldIgnorePath($path) { return preg_match('@/__tests__/|externals/javelin/docs/@', $path); } public function lintPath($path) { if ($this->shouldIgnorePath($path)) { return; } if (!$this->symbolsBinary) { if (!$this->haveWarnedAboutBinary) { $this->haveWarnedAboutBinary = true; // TODO: Write build documentation for the Javelin binaries and point // the user at it. $this->raiseLintAtLine( 1, 0, self::LINT_MISSING_BINARY, "The 'javelinsymbols' binary in the Javelin project is not ". "available in \$PATH, so the Javelin linter can't run. This ". "isn't a big concern, but means some Javelin problems can't be ". "automatically detected."); } return; } list($uses, $installs) = $this->getUsedAndInstalledSymbolsForPath($path); foreach ($uses as $symbol => $line) { $parts = explode('.', $symbol); foreach ($parts as $part) { if ($part[0] == '_' && $part[1] != '_') { $base = implode('.', array_slice($parts, 0, 2)); if (!array_key_exists($base, $installs)) { $this->raiseLintAtLine( $line, 0, self::LINT_PRIVATE_ACCESS, "This file accesses private symbol '{$symbol}' across file ". "boundaries. You may only access private members and methods ". "from the file where they are defined."); } break; } } } if ($this->getEngine()->getCommitHookMode()) { // Don't do the dependency checks in commit-hook mode because we won't // have an available working copy. return; } $external_classes = array(); foreach ($uses as $symbol => $line) { $parts = explode('.', $symbol); $class = implode('.', array_slice($parts, 0, 2)); if (!array_key_exists($class, $external_classes) && !array_key_exists($class, $installs)) { $external_classes[$class] = $line; } } $celerity = CelerityResourceMap::getNamedInstance('phabricator'); $path = preg_replace( '@^externals/javelinjs/src/@', 'webroot/rsrc/js/javelin/', $path); $need = $external_classes; $resource_name = substr($path, strlen('webroot/')); $requires = $celerity->getRequiredSymbolsForName($resource_name); if (!$requires) { $requires = array(); } foreach ($requires as $key => $requires_symbol) { $requires_name = $celerity->getResourceNameForSymbol($requires_symbol); if ($requires_name === null) { $this->raiseLintAtLine( 0, 0, self::LINT_UNKNOWN_DEPENDENCY, "This file @requires component '{$requires_symbol}', but it does ". "not exist. You may need to rebuild the Celerity map."); unset($requires[$key]); continue; } if (preg_match('/\\.css$/', $requires_name)) { // If JS requires CSS, just assume everything is fine. unset($requires[$key]); } else { $symbol_path = 'webroot/'.$requires_name; list($ignored, $req_install) = $this->getUsedAndInstalledSymbolsForPath( $symbol_path); if (array_intersect_key($req_install, $external_classes)) { $need = array_diff_key($need, $req_install); unset($requires[$key]); } } } foreach ($need as $class => $line) { $this->raiseLintAtLine( $line, 0, self::LINT_MISSING_DEPENDENCY, "This file uses '{$class}' but does not @requires the component ". "which installs it. You may need to rebuild the Celerity map."); } foreach ($requires as $component) { $this->raiseLintAtLine( 0, 0, self::LINT_UNNECESSARY_DEPENDENCY, "This file @requires component '{$component}' but does not use ". "anything it provides."); } } private function loadSymbols($path) { if (empty($this->symbols[$path])) { $this->symbols[$path] = $this->newSymbolsFuture($path)->resolvex(); } return $this->symbols[$path]; } private function newSymbolsFuture($path) { $future = new ExecFuture('javelinsymbols # %s', $path); $future->write($this->getData($path)); return $future; } private function getUsedAndInstalledSymbolsForPath($path) { list($symbols) = $this->loadSymbols($path); $symbols = trim($symbols); $uses = array(); $installs = array(); if (empty($symbols)) { // This file has no symbols. return array($uses, $installs); } $symbols = explode("\n", trim($symbols)); foreach ($symbols as $line) { $matches = null; if (!preg_match('/^([?+\*])([^:]*):(\d+)$/', $line, $matches)) { throw new Exception( "Received malformed output from `javelinsymbols`."); } $type = $matches[1]; $symbol = $matches[2]; $line = $matches[3]; switch ($type) { case '?': $uses[$symbol] = $line; break; case '+': $installs['JX.'.$symbol] = $line; break; } } $contents = $this->getData($path); $matches = null; $count = preg_match_all( '/@javelin-installs\W+(\S+)/', $contents, $matches, PREG_PATTERN_ORDER); if ($count) { foreach ($matches[1] as $symbol) { $installs[$symbol] = 0; } } return array($uses, $installs); } }