[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |