Path: blob/master/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php
13466 views
<?php12final class PhabricatorJavelinLinter extends ArcanistLinter {34private $symbols = array();56private $symbolsBinary;7private $haveWarnedAboutBinary;89const LINT_PRIVATE_ACCESS = 1;10const LINT_MISSING_DEPENDENCY = 2;11const LINT_UNNECESSARY_DEPENDENCY = 3;12const LINT_UNKNOWN_DEPENDENCY = 4;13const LINT_MISSING_BINARY = 5;1415public function getInfoName() {16return pht('Javelin Linter');17}1819public function getInfoDescription() {20return pht(21'This linter is intended for use with the Javelin JS library and '.22'extensions. Use `%s` to run Javelin rules on Javascript source files.',23'javelinsymbols');24}2526private function getBinaryPath() {27if ($this->symbolsBinary === null) {28list($err, $stdout) = exec_manual('which javelinsymbols');29$this->symbolsBinary = ($err ? false : rtrim($stdout));30}31return $this->symbolsBinary;32}3334public function willLintPaths(array $paths) {35if (!$this->getBinaryPath()) {36return;37}3839$root = dirname(phutil_get_library_root('phabricator'));40require_once $root.'/scripts/__init_script__.php';4142$futures = array();43foreach ($paths as $path) {44if ($this->shouldIgnorePath($path)) {45continue;46}4748$future = $this->newSymbolsFuture($path);49$futures[$path] = $future;50}5152foreach (id(new FutureIterator($futures))->limit(8) as $path => $future) {53$this->symbols[$path] = $future->resolvex();54}55}5657public function getLinterName() {58return 'JAVELIN';59}6061public function getLinterConfigurationName() {62return 'javelin';63}6465public function getLintSeverityMap() {66return array(67self::LINT_MISSING_BINARY => ArcanistLintSeverity::SEVERITY_WARNING,68);69}7071public function getLintNameMap() {72return array(73self::LINT_PRIVATE_ACCESS =>74pht('Private Method/Member Access'),75self::LINT_MISSING_DEPENDENCY =>76pht('Missing Javelin Dependency'),77self::LINT_UNNECESSARY_DEPENDENCY =>78pht('Unnecessary Javelin Dependency'),79self::LINT_UNKNOWN_DEPENDENCY =>80pht('Unknown Javelin Dependency'),81self::LINT_MISSING_BINARY =>82pht('`%s` Not In Path', 'javelinsymbols'),83);84}8586public function getCacheGranularity() {87return parent::GRANULARITY_REPOSITORY;88}8990public function getCacheVersion() {91$version = '0';92$binary_path = $this->getBinaryPath();93if ($binary_path) {94$version .= '-'.md5_file($binary_path);95}96return $version;97}9899private function shouldIgnorePath($path) {100return preg_match('@/__tests__/|externals/javelin/docs/@', $path);101}102103public function lintPath($path) {104if ($this->shouldIgnorePath($path)) {105return;106}107108if (!$this->symbolsBinary) {109if (!$this->haveWarnedAboutBinary) {110$this->haveWarnedAboutBinary = true;111// TODO: Write build documentation for the Javelin binaries and point112// the user at it.113$this->raiseLintAtLine(1141,1150,116self::LINT_MISSING_BINARY,117pht(118"The '%s' binary in the Javelin project is not available in %s, ".119"so the Javelin linter can't run. This isn't a big concern, ".120"but means some Javelin problems can't be automatically detected.",121'javelinsymbols',122'$PATH'));123}124return;125}126127list($uses, $installs) = $this->getUsedAndInstalledSymbolsForPath($path);128foreach ($uses as $symbol => $line) {129$parts = explode('.', $symbol);130foreach ($parts as $part) {131if ($part[0] == '_' && $part[1] != '_') {132$base = implode('.', array_slice($parts, 0, 2));133if (!array_key_exists($base, $installs)) {134$this->raiseLintAtLine(135$line,1360,137self::LINT_PRIVATE_ACCESS,138pht(139"This file accesses private symbol '%s' across file ".140"boundaries. You may only access private members and methods ".141"from the file where they are defined.",142$symbol));143}144break;145}146}147}148149$external_classes = array();150foreach ($uses as $symbol => $line) {151$parts = explode('.', $symbol);152$class = implode('.', array_slice($parts, 0, 2));153if (!array_key_exists($class, $external_classes) &&154!array_key_exists($class, $installs)) {155$external_classes[$class] = $line;156}157}158159$celerity = CelerityResourceMap::getNamedInstance('phabricator');160161$path = preg_replace(162'@^externals/javelinjs/src/@',163'webroot/rsrc/js/javelin/',164$path);165$need = $external_classes;166167$resource_name = substr($path, strlen('webroot/'));168$requires = $celerity->getRequiredSymbolsForName($resource_name);169if (!$requires) {170$requires = array();171}172173foreach ($requires as $key => $requires_symbol) {174$requires_name = $celerity->getResourceNameForSymbol($requires_symbol);175if ($requires_name === null) {176$this->raiseLintAtLine(1770,1780,179self::LINT_UNKNOWN_DEPENDENCY,180pht(181"This file %s component '%s', but it does not exist. ".182"You may need to rebuild the Celerity map.",183'@requires',184$requires_symbol));185unset($requires[$key]);186continue;187}188189if (preg_match('/\\.css$/', $requires_name)) {190// If JS requires CSS, just assume everything is fine.191unset($requires[$key]);192} else {193$symbol_path = 'webroot/'.$requires_name;194list($ignored, $req_install) = $this->getUsedAndInstalledSymbolsForPath(195$symbol_path);196if (array_intersect_key($req_install, $external_classes)) {197$need = array_diff_key($need, $req_install);198unset($requires[$key]);199}200}201}202203foreach ($need as $class => $line) {204$this->raiseLintAtLine(205$line,2060,207self::LINT_MISSING_DEPENDENCY,208pht(209"This file uses '%s' but does not @requires the component ".210"which installs it. You may need to rebuild the Celerity map.",211$class));212}213214foreach ($requires as $component) {215$this->raiseLintAtLine(2160,2170,218self::LINT_UNNECESSARY_DEPENDENCY,219pht(220"This file %s component '%s' but does not use anything it provides.",221'@requires',222$component));223}224}225226private function loadSymbols($path) {227if (empty($this->symbols[$path])) {228$this->symbols[$path] = $this->newSymbolsFuture($path)->resolvex();229}230return $this->symbols[$path];231}232233private function newSymbolsFuture($path) {234$future = new ExecFuture('javelinsymbols # %s', $path);235$future->write($this->getData($path));236return $future;237}238239private function getUsedAndInstalledSymbolsForPath($path) {240list($symbols) = $this->loadSymbols($path);241$symbols = trim($symbols);242243$uses = array();244$installs = array();245if (empty($symbols)) {246// This file has no symbols.247return array($uses, $installs);248}249250$symbols = explode("\n", trim($symbols));251foreach ($symbols as $line) {252$matches = null;253if (!preg_match('/^([?+\*])([^:]*):(\d+)$/', $line, $matches)) {254throw new Exception(255pht('Received malformed output from `%s`.', 'javelinsymbols'));256}257$type = $matches[1];258$symbol = $matches[2];259$line = $matches[3];260261switch ($type) {262case '?':263$uses[$symbol] = $line;264break;265case '+':266$installs['JX.'.$symbol] = $line;267break;268}269}270271$contents = $this->getData($path);272273$matches = null;274$count = preg_match_all(275'/@javelin-installs\W+(\S+)/',276$contents,277$matches,278PREG_PATTERN_ORDER);279280if ($count) {281foreach ($matches[1] as $symbol) {282$installs[$symbol] = 0;283}284}285286return array($uses, $installs);287}288289}290291292