diff --git a/src/infrastructure/celerity/CelerityResourceMap.php b/src/infrastructure/celerity/CelerityResourceMap.php index 9137a0f433..de6dce674d 100644 --- a/src/infrastructure/celerity/CelerityResourceMap.php +++ b/src/infrastructure/celerity/CelerityResourceMap.php @@ -1,122 +1,154 @@ resourceMap = $resource_map; return $this; } public function resolveResources(array $symbols) { $map = array(); foreach ($symbols as $symbol) { if (!empty($map[$symbol])) { continue; } $this->resolveResource($map, $symbol); } return $map; } private function resolveResource(array &$map, $symbol) { if (empty($this->resourceMap[$symbol])) { throw new Exception( "Attempting to resolve unknown Celerity resource, '{$symbol}'."); } $info = $this->resourceMap[$symbol]; foreach ($info['requires'] as $requires) { if (!empty($map[$requires])) { continue; } $this->resolveResource($map, $requires); } $map[$symbol] = $info; } public function setPackageMap($package_map) { $this->packageMap = $package_map; return $this; } public function packageResources(array $resolved_map) { $packaged = array(); $handled = array(); foreach ($resolved_map as $symbol => $info) { if (isset($handled[$symbol])) { continue; } if (empty($this->packageMap['reverse'][$symbol])) { $packaged[$symbol] = $info; } else { $package = $this->packageMap['reverse'][$symbol]; $package_info = $this->packageMap['packages'][$package]; $packaged[$package_info['name']] = $package_info; foreach ($package_info['symbols'] as $packaged_symbol) { $handled[$packaged_symbol] = true; } } } return $packaged; } public function resolvePackage($package_hash) { $package = idx($this->packageMap['packages'], $package_hash); if (!$package) { return null; } $paths = array(); foreach ($package['symbols'] as $symbol) { $paths[] = $this->resourceMap[$symbol]['disk']; } return $paths; } public function lookupSymbolInformation($symbol) { return idx($this->resourceMap, $symbol); } - public function lookupFileInformation($path) { + private function lookupFileInformation($path) { if (empty($this->reverseMap)) { $this->reverseMap = array(); foreach ($this->resourceMap as $symbol => $data) { $data['provides'] = $symbol; $this->reverseMap[$data['disk']] = $data; } } return idx($this->reverseMap, $path); } + + /** + * Return the fully-qualified, absolute URI for the resource associated with + * a resource name. This method is fairly low-level and ignores packaging. + * + * @param string Resource name to lookup. + * @return string Fully-qualified resource URI. + */ + public function getFullyQualifiedURIForName($name) { + $info = $this->lookupFileInformation($name); + if ($info) { + return idx($info, 'uri'); + } + return null; + } + + + /** + * Return the resource symbols required by a named resource. + * + * @param string Resource name to lookup. + * @return list List of required symbols. + */ + public function getRequiredSymbolsForName($name) { + $info = $this->lookupFileInformation($name); + if ($info) { + return idx($info, 'requires', array()); + } + return null; + } + + } diff --git a/src/infrastructure/celerity/CelerityResourceTransformer.php b/src/infrastructure/celerity/CelerityResourceTransformer.php index 8ef35da29a..d6b74926c7 100644 --- a/src/infrastructure/celerity/CelerityResourceTransformer.php +++ b/src/infrastructure/celerity/CelerityResourceTransformer.php @@ -1,226 +1,226 @@ translateURICallback = $translate_uricallback; return $this; } public function setMinify($minify) { $this->minify = $minify; return $this; } public function setRawResourceMap(array $raw_resource_map) { $this->rawResourceMap = $raw_resource_map; return $this; } public function setCelerityMap(CelerityResourceMap $celerity_map) { $this->celerityMap = $celerity_map; return $this; } public function setRawURIMap(array $raw_urimap) { $this->rawURIMap = $raw_urimap; return $this; } public function getRawURIMap() { return $this->rawURIMap; } /** * @phutil-external-symbol function jsShrink */ public function transformResource($path, $data) { $type = self::getResourceType($path); switch ($type) { case 'css': $data = $this->replaceCSSPrintRules($path, $data); $data = $this->replaceCSSVariables($path, $data); $data = preg_replace_callback( '@url\s*\((\s*[\'"]?.*?)\)@s', nonempty( $this->translateURICallback, array($this, 'translateResourceURI')), $data); break; } if (!$this->minify) { return $data; } // Some resources won't survive minification (like Raphael.js), and are // marked so as not to be minified. if (strpos($data, '@'.'do-not-minify') !== false) { return $data; } switch ($type) { case 'css': // Remove comments. $data = preg_replace('@/\*.*?\*/@s', '', $data); // Remove whitespace around symbols. $data = preg_replace('@\s*([{}:;,])\s*@', '\1', $data); // Remove unnecessary semicolons. $data = preg_replace('@;}@', '}', $data); // Replace #rrggbb with #rgb when possible. $data = preg_replace( '@#([a-f0-9])\1([a-f0-9])\2([a-f0-9])\3@i', '#\1\2\3', $data); $data = trim($data); break; case 'js': // If `jsxmin` is available, use it. jsxmin is the Javelin minifier and // produces the smallest output, but is complicated to build. if (Filesystem::binaryExists('jsxmin')) { $future = new ExecFuture('jsxmin __DEV__:0'); $future->write($data); list($err, $result) = $future->resolve(); if (!$err) { $data = $result; break; } } // If `jsxmin` is not available, use `JsShrink`, which doesn't compress // quite as well but is always available. $root = dirname(phutil_get_library_root('phabricator')); require_once $root.'/externals/JsShrink/jsShrink.php'; $data = jsShrink($data); break; } return $data; } public static function getResourceType($path) { return last(explode('.', $path)); } public function translateResourceURI(array $matches) { $uri = trim($matches[1], "'\" \r\t\n"); if ($this->rawURIMap !== null) { if (isset($this->rawURIMap[$uri])) { $uri = $this->rawURIMap[$uri]; } } else if ($this->rawResourceMap) { if (isset($this->rawResourceMap[$uri]['uri'])) { $uri = $this->rawResourceMap[$uri]['uri']; } } else if ($this->celerityMap) { - $info = $this->celerityMap->lookupFileInformation($uri); - if ($info) { - $uri = $info['uri']; + $resource_uri = $this->celerityMap->getFullyQualifiedURIForName($uri); + if ($resource_uri) { + $uri = $resource_uri; } } return 'url('.$uri.')'; } private function replaceCSSVariables($path, $data) { $this->currentPath = $path; return preg_replace_callback( '/{\$([^}]+)}/', array($this, 'replaceCSSVariable'), $data); } private function replaceCSSPrintRules($path, $data) { $this->currentPath = $path; return preg_replace_callback( '/!print\s+(.+?{.+?})/s', array($this, 'replaceCSSPrintRule'), $data); } public static function getCSSVariableMap() { return array( // Base Colors 'red' => '#c0392b', 'lightred' => '#f4dddb', 'orange' => '#e67e22', 'lightorange' => '#f7e2d4', 'yellow' => '#f1c40f', 'lightyellow' => '#fdf5d4', 'green' => '#139543', 'lightgreen' => '#d7eddf', 'blue' => '#2980b9', 'lightblue' => '#daeaf3', 'sky' => '#3498db', 'lightsky' => '#ddeef9', 'indigo' => '#c6539d', 'lightindigo' => '#f5e2ef', 'violet' => '#8e44ad', 'lightviolet' => '#ecdff1', 'charcoal' => '#4b4d51', 'backdrop' => '#c4cde0', // Base Greys 'lightgreyborder' => '#C7CCD9', 'greyborder' => '#A1A6B0', 'darkgreyborder' => '#676A70', 'lightgreytext' => '#92969D', 'greytext' => '#74777D', 'darkgreytext' => '#4B4D51', 'lightgreybackground' => '#F7F7F7', 'greybackground' => '#EBECEE', // Base Blues 'thinblueborder' => '#DDE8EF', 'lightblueborder' => '#BFCFDA', 'blueborder' => '#8C98B8', 'darkblueborder' => '#626E82', 'lightbluebackground' => '#F8F9FC', 'bluebackground' => '#DAE7FF', 'lightbluetext' => '#8C98B8', 'bluetext' => '#6B748C', 'darkbluetext' => '#464C5C', ); } public function replaceCSSVariable($matches) { static $map; if (!$map) { $map = self::getCSSVariableMap(); } $var_name = $matches[1]; if (empty($map[$var_name])) { $path = $this->currentPath; throw new Exception( "CSS file '{$path}' has unknown variable '{$var_name}'."); } return $map[$var_name]; } public function replaceCSSPrintRule($matches) { $rule = $matches[1]; $rules = array(); $rules[] = '.printable '.$rule; $rules[] = "@media print {\n ".str_replace("\n", "\n ", $rule)."\n}\n"; return implode("\n\n", $rules); } } diff --git a/src/infrastructure/celerity/api.php b/src/infrastructure/celerity/api.php index fa615f32ad..d12f3c3f16 100644 --- a/src/infrastructure/celerity/api.php +++ b/src/infrastructure/celerity/api.php @@ -1,61 +1,61 @@ requireResource($symbol); } /** * Generate a node ID which is guaranteed to be unique for the current page, * even across Ajax requests. You should use this method to generate IDs for * nodes which require a uniqueness guarantee. * * @return string A string appropriate for use as an 'id' attribute on a DOM * node. It is guaranteed to be unique for the current page, even * if the current request is a subsequent Ajax request. * * @group celerity */ function celerity_generate_unique_node_id() { static $uniq = 0; $response = CelerityAPI::getStaticResourceResponse(); $block = $response->getMetadataBlock(); return 'UQ'.$block.'_'.($uniq++); } /** * Get the versioned URI for a raw resource, like an image. * * @param string Path to the raw image. * @return string Versioned path to the image, if one is available. * * @group celerity */ function celerity_get_resource_uri($resource) { $map = CelerityResourceMap::getInstance(); - $info = $map->lookupFileInformation($resource); - if ($info) { - return $info['uri']; - } else { - return $resource; + $uri = $map->getFullyQualifiedURIForName($resource); + if ($uri) { + return $uri; } + + return $resource; } diff --git a/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php b/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php index a1f9cd9974..f02a019231 100644 --- a/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php +++ b/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php @@ -1,266 +1,265 @@ 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 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::getInstance(); $path = preg_replace( '@^externals/javelinjs/src/@', 'webroot/rsrc/js/javelin/', $path); $need = $external_classes; - $info = $celerity->lookupFileInformation(substr($path, strlen('webroot'))); - if (!$info) { - $info = array(); + $resource_name = substr($path, strlen('webroot')); + $requires = $celerity->getRequiredSymbolsForName($resource_name); + if (!$requires) { + $requires = array(); } - $requires = idx($info, 'requires', array()); - - foreach ($requires as $key => $name) { - $symbol_info = $celerity->lookupSymbolInformation($name); + foreach ($requires as $key => $symbol_name) { + $symbol_info = $celerity->lookupSymbolInformation($symbol_name); if (!$symbol_info) { $this->raiseLintAtLine( 0, 0, self::LINT_UNKNOWN_DEPENDENCY, - "This file @requires component '{$name}', but it does not ". + "This file @requires component '{$symbol_name}', but it does not ". "exist. You may need to rebuild the Celerity map."); unset($requires[$key]); continue; } if (preg_match('/\\.css$/', $symbol_info['disk'])) { // If JS requires CSS, just assume everything is fine. unset($requires[$key]); } else { $symbol_path = 'webroot'.$symbol_info['disk']; 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); } }