[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/infrastructure/lint/linter/ -> PhabricatorJavelinLinter.php (source)

   1  <?php
   2  
   3  final class PhabricatorJavelinLinter extends ArcanistLinter {
   4  
   5    private $symbols = array();
   6  
   7    private $symbolsBinary;
   8    private $haveWarnedAboutBinary;
   9  
  10    const LINT_PRIVATE_ACCESS = 1;
  11    const LINT_MISSING_DEPENDENCY = 2;
  12    const LINT_UNNECESSARY_DEPENDENCY = 3;
  13    const LINT_UNKNOWN_DEPENDENCY = 4;
  14    const LINT_MISSING_BINARY = 5;
  15  
  16    public function getInfoName() {
  17      return 'Javelin Linter';
  18    }
  19  
  20    public function getInfoDescription() {
  21      return pht(
  22        'This linter is intended for use with the Javelin JS library and '.
  23        'extensions. Use `javelinsymbols` to run Javelin rules on Javascript '.
  24        'source files.');
  25    }
  26  
  27    private function getBinaryPath() {
  28      if ($this->symbolsBinary === null) {
  29        list($err, $stdout) = exec_manual('which javelinsymbols');
  30        $this->symbolsBinary = ($err ? false : rtrim($stdout));
  31      }
  32      return $this->symbolsBinary;
  33    }
  34  
  35    public function willLintPaths(array $paths) {
  36      if (!$this->getBinaryPath()) {
  37        return;
  38      }
  39  
  40      $root = dirname(phutil_get_library_root('phabricator'));
  41      require_once $root.'/scripts/__init_script__.php';
  42  
  43      $futures = array();
  44      foreach ($paths as $path) {
  45        if ($this->shouldIgnorePath($path)) {
  46          continue;
  47        }
  48  
  49        $future = $this->newSymbolsFuture($path);
  50        $futures[$path] = $future;
  51      }
  52  
  53      foreach (Futures($futures)->limit(8) as $path => $future) {
  54        $this->symbols[$path] = $future->resolvex();
  55      }
  56    }
  57  
  58    public function getLinterName() {
  59      return 'JAVELIN';
  60    }
  61  
  62    public function getLinterConfigurationName() {
  63      return 'javelin';
  64    }
  65  
  66    public function getLintSeverityMap() {
  67      return array(
  68        self::LINT_MISSING_BINARY => ArcanistLintSeverity::SEVERITY_WARNING,
  69      );
  70    }
  71  
  72    public function getLintNameMap() {
  73      return array(
  74        self::LINT_PRIVATE_ACCESS => 'Private Method/Member Access',
  75        self::LINT_MISSING_DEPENDENCY => 'Missing Javelin Dependency',
  76        self::LINT_UNNECESSARY_DEPENDENCY => 'Unnecessary Javelin Dependency',
  77        self::LINT_UNKNOWN_DEPENDENCY => 'Unknown Javelin Dependency',
  78        self::LINT_MISSING_BINARY => '`javelinsymbols` Not In Path',
  79      );
  80    }
  81  
  82    public function getCacheGranularity() {
  83      return ArcanistLinter::GRANULARITY_REPOSITORY;
  84    }
  85  
  86    public function getCacheVersion() {
  87      $version = '0';
  88      $binary_path = $this->getBinaryPath();
  89      if ($binary_path) {
  90        $version .= '-'.md5_file($binary_path);
  91      }
  92      return $version;
  93    }
  94  
  95    private function shouldIgnorePath($path) {
  96      return preg_match('@/__tests__/|externals/javelin/docs/@', $path);
  97    }
  98  
  99    public function lintPath($path) {
 100      if ($this->shouldIgnorePath($path)) {
 101        return;
 102      }
 103  
 104      if (!$this->symbolsBinary) {
 105        if (!$this->haveWarnedAboutBinary) {
 106          $this->haveWarnedAboutBinary = true;
 107          // TODO: Write build documentation for the Javelin binaries and point
 108          // the user at it.
 109          $this->raiseLintAtLine(
 110            1,
 111            0,
 112            self::LINT_MISSING_BINARY,
 113            "The 'javelinsymbols' binary in the Javelin project is not ".
 114            "available in \$PATH, so the Javelin linter can't run. This ".
 115            "isn't a big concern, but means some Javelin problems can't be ".
 116            "automatically detected.");
 117        }
 118        return;
 119      }
 120  
 121      list($uses, $installs) = $this->getUsedAndInstalledSymbolsForPath($path);
 122      foreach ($uses as $symbol => $line) {
 123        $parts = explode('.', $symbol);
 124        foreach ($parts as $part) {
 125          if ($part[0] == '_' && $part[1] != '_') {
 126            $base = implode('.', array_slice($parts, 0, 2));
 127            if (!array_key_exists($base, $installs)) {
 128              $this->raiseLintAtLine(
 129                $line,
 130                0,
 131                self::LINT_PRIVATE_ACCESS,
 132                "This file accesses private symbol '{$symbol}' across file ".
 133                "boundaries. You may only access private members and methods ".
 134                "from the file where they are defined.");
 135            }
 136            break;
 137          }
 138        }
 139      }
 140  
 141      if ($this->getEngine()->getCommitHookMode()) {
 142        // Don't do the dependency checks in commit-hook mode because we won't
 143        // have an available working copy.
 144        return;
 145      }
 146  
 147      $external_classes = array();
 148      foreach ($uses as $symbol => $line) {
 149        $parts = explode('.', $symbol);
 150        $class = implode('.', array_slice($parts, 0, 2));
 151        if (!array_key_exists($class, $external_classes) &&
 152            !array_key_exists($class, $installs)) {
 153          $external_classes[$class] = $line;
 154        }
 155      }
 156  
 157      $celerity = CelerityResourceMap::getNamedInstance('phabricator');
 158  
 159      $path = preg_replace(
 160        '@^externals/javelinjs/src/@',
 161        'webroot/rsrc/js/javelin/',
 162        $path);
 163      $need = $external_classes;
 164  
 165      $resource_name = substr($path, strlen('webroot/'));
 166      $requires = $celerity->getRequiredSymbolsForName($resource_name);
 167      if (!$requires) {
 168        $requires = array();
 169      }
 170  
 171      foreach ($requires as $key => $requires_symbol) {
 172        $requires_name = $celerity->getResourceNameForSymbol($requires_symbol);
 173        if ($requires_name === null) {
 174          $this->raiseLintAtLine(
 175            0,
 176            0,
 177            self::LINT_UNKNOWN_DEPENDENCY,
 178            "This file @requires component '{$requires_symbol}', but it does ".
 179            "not exist. You may need to rebuild the Celerity map.");
 180          unset($requires[$key]);
 181          continue;
 182        }
 183  
 184        if (preg_match('/\\.css$/', $requires_name)) {
 185          // If JS requires CSS, just assume everything is fine.
 186          unset($requires[$key]);
 187        } else {
 188          $symbol_path = 'webroot/'.$requires_name;
 189          list($ignored, $req_install) = $this->getUsedAndInstalledSymbolsForPath(
 190            $symbol_path);
 191          if (array_intersect_key($req_install, $external_classes)) {
 192            $need = array_diff_key($need, $req_install);
 193            unset($requires[$key]);
 194          }
 195        }
 196      }
 197  
 198      foreach ($need as $class => $line) {
 199        $this->raiseLintAtLine(
 200          $line,
 201          0,
 202          self::LINT_MISSING_DEPENDENCY,
 203          "This file uses '{$class}' but does not @requires the component ".
 204          "which installs it. You may need to rebuild the Celerity map.");
 205      }
 206  
 207      foreach ($requires as $component) {
 208        $this->raiseLintAtLine(
 209          0,
 210          0,
 211          self::LINT_UNNECESSARY_DEPENDENCY,
 212          "This file @requires component '{$component}' but does not use ".
 213          "anything it provides.");
 214      }
 215    }
 216  
 217    private function loadSymbols($path) {
 218      if (empty($this->symbols[$path])) {
 219        $this->symbols[$path] = $this->newSymbolsFuture($path)->resolvex();
 220      }
 221      return $this->symbols[$path];
 222    }
 223  
 224    private function newSymbolsFuture($path) {
 225      $future = new ExecFuture('javelinsymbols # %s', $path);
 226      $future->write($this->getData($path));
 227      return $future;
 228    }
 229  
 230    private function getUsedAndInstalledSymbolsForPath($path) {
 231      list($symbols) = $this->loadSymbols($path);
 232      $symbols = trim($symbols);
 233  
 234      $uses = array();
 235      $installs = array();
 236      if (empty($symbols)) {
 237        // This file has no symbols.
 238        return array($uses, $installs);
 239      }
 240  
 241      $symbols = explode("\n", trim($symbols));
 242      foreach ($symbols as $line) {
 243        $matches = null;
 244        if (!preg_match('/^([?+\*])([^:]*):(\d+)$/', $line, $matches)) {
 245          throw new Exception(
 246            'Received malformed output from `javelinsymbols`.');
 247        }
 248        $type = $matches[1];
 249        $symbol = $matches[2];
 250        $line = $matches[3];
 251  
 252        switch ($type) {
 253          case '?':
 254            $uses[$symbol] = $line;
 255            break;
 256          case '+':
 257            $installs['JX.'.$symbol] = $line;
 258            break;
 259        }
 260      }
 261  
 262      $contents = $this->getData($path);
 263  
 264      $matches = null;
 265      $count = preg_match_all(
 266        '/@javelin-installs\W+(\S+)/',
 267        $contents,
 268        $matches,
 269        PREG_PATTERN_ORDER);
 270  
 271      if ($count) {
 272        foreach ($matches[1] as $symbol) {
 273          $installs[$symbol] = 0;
 274        }
 275      }
 276  
 277      return array($uses, $installs);
 278    }
 279  
 280  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1