diff --git a/src/__tests__/PhutilLibraryTestCase.php b/src/__tests__/PhutilLibraryTestCase.php index 26fb89d..0e5c626 100644 --- a/src/__tests__/PhutilLibraryTestCase.php +++ b/src/__tests__/PhutilLibraryTestCase.php @@ -1,146 +1,191 @@ setLibrary($this->getLibraryName()) ->selectAndLoadSymbols(); $this->assertTrue(true); } /** * This is more of an acceptance test case instead of a unit test. It verifies * that all the library map is up-to-date. */ public function testLibraryMap() { $root = $this->getLibraryRoot(); $library = phutil_get_library_name_for_root($root); $new_library_map = id(new PhutilLibraryMapBuilder($root)) ->buildMap(); $bootloader = PhutilBootloader::getInstance(); $old_library_map = $bootloader->getLibraryMapWithoutExtensions($library); unset($old_library_map[PhutilLibraryMapBuilder::LIBRARY_MAP_VERSION_KEY]); - $this->assertEqual( - $new_library_map, - $old_library_map, + $identical = ($new_library_map === $old_library_map); + if (!$identical) { + $differences = $this->getMapDifferences( + $old_library_map, + $new_library_map); + sort($differences); + } else { + $differences = array(); + } + + $this->assertTrue( + $identical, pht( - 'The library map does not appear to be up-to-date. Try '. - 'rebuilding the map with `%s`.', - 'arc liberate')); + "The library map is out of date. Rebuild it with `%s`.\n". + "These entries differ: %s.", + 'arc liberate', + implode(', ', $differences))); + } + + + private function getMapDifferences($old, $new) { + $changed = array(); + + $all = $old + $new; + foreach ($all as $key => $value) { + $old_exists = array_key_exists($key, $old); + $new_exists = array_key_exists($key, $new); + + // One map has it and the other does not, so mark it as changed. + if ($old_exists != $new_exists) { + $changed[] = $key; + continue; + } + + $oldv = idx($old, $key); + $newv = idx($new, $key); + if ($oldv === $newv) { + continue; + } + + if (is_array($oldv) && is_array($newv)) { + $child_changed = $this->getMapDifferences($oldv, $newv); + foreach ($child_changed as $child) { + $changed[] = $key.'.'.$child; + } + } else { + $changed[] = $key; + } + } + + return $changed; } + /** * This is more of an acceptance test case instead of a unit test. It verifies * that methods in subclasses have the same visibility as the method in the * parent class. */ public function testMethodVisibility() { $symbols = id(new PhutilSymbolLoader()) ->setLibrary($this->getLibraryName()) ->selectSymbolsWithoutLoading(); $classes = array(); foreach ($symbols as $symbol) { if ($symbol['type'] == 'class') { $classes[$symbol['name']] = new ReflectionClass($symbol['name']); } } $failures = array(); foreach ($classes as $class_name => $class) { $parents = array(); $parent = $class; while ($parent = $parent->getParentClass()) { $parents[] = $parent; } $interfaces = $class->getInterfaces(); foreach ($class->getMethods() as $method) { $method_name = $method->getName(); foreach (array_merge($parents, $interfaces) as $extends) { if ($extends->hasMethod($method_name)) { $xmethod = $extends->getMethod($method_name); if (!$this->compareVisibility($xmethod, $method)) { $failures[] = pht( 'Class "%s" implements method "%s" with the wrong visibility. '. 'The method has visibility "%s", but it is defined in parent '. '"%s" with visibility "%s". In Phabricator, a method which '. 'overrides another must always have the same visibility.', $class_name, $method_name, $this->getVisibility($method), $extends->getName(), $this->getVisibility($xmethod)); } // We found a declaration somewhere, so stop looking. break; } } } } $this->assertTrue( empty($failures), "\n\n".implode("\n\n", $failures)); } /** * Get the name of the library currently being tested. */ protected function getLibraryName() { return phutil_get_library_name_for_root($this->getLibraryRoot()); } /** * Get the root directory for the library currently being tested. */ protected function getLibraryRoot() { $caller = id(new ReflectionClass($this))->getFileName(); return phutil_get_library_root_for_path($caller); } private function compareVisibility( ReflectionMethod $parent_method, ReflectionMethod $method) { static $bitmask; if ($bitmask === null) { $bitmask = ReflectionMethod::IS_PUBLIC; $bitmask += ReflectionMethod::IS_PROTECTED; $bitmask += ReflectionMethod::IS_PRIVATE; } $parent_modifiers = $parent_method->getModifiers(); $modifiers = $method->getModifiers(); return !(($parent_modifiers ^ $modifiers) & $bitmask); } private function getVisibility(ReflectionMethod $method) { if ($method->isPrivate()) { return 'private'; } else if ($method->isProtected()) { return 'protected'; } else { return 'public'; } } } diff --git a/src/parser/aast/api/AASTNode.php b/src/parser/aast/api/AASTNode.php index c4ac24c..7b20224 100644 --- a/src/parser/aast/api/AASTNode.php +++ b/src/parser/aast/api/AASTNode.php @@ -1,403 +1,404 @@ id = $id; $this->typeID = $data[0]; if (isset($data[1])) { $this->l = $data[1]; } else { $this->l = -1; } if (isset($data[2])) { $this->r = $data[2]; } else { $this->r = -1; } $this->tree = $tree; } final public function getParentNode() { return $this->parentNode; } final public function setParentNode(AASTNode $node = null) { $this->parentNode = $node; return $this; } final public function getPreviousSibling() { return $this->previousSibling; } final public function setPreviousSibling(AASTNode $node = null) { $this->previousSibling = $node; return $this; } final public function getNextSibling() { return $this->nextSibling; } final public function setNextSibling(AASTNode $node = null) { $this->nextSibling = $node; return $this; } final public function getID() { return $this->id; } final public function getTypeID() { return $this->typeID; } final public function getTree() { return $this->tree; } final public function getTypeName() { if (empty($this->typeName)) { $this->typeName = $this->tree->getNodeTypeNameFromTypeID($this->getTypeID()); } return $this->typeName; } final public function getChildren() { return $this->children; } final public function setChildren(array $children) { // We don't call `assert_instances_of($children, 'AASTNode')` because doing // so would incur a significant performance penalty. $this->children = $children; return $this; } public function getChildrenOfType($type) { $nodes = array(); foreach ($this->children as $child) { if ($child->getTypeName() == $type) { $nodes[] = $child; } } return $nodes; } public function getChildOfType($index, $type) { $child = $this->getChildByIndex($index); if ($child->getTypeName() != $type) { throw new Exception( pht( "Child in position '%d' is not of type '%s': %s", $index, $type, $this->getDescription())); } return $child; } public function getChildByIndex($index) { // NOTE: Microoptimization to avoid calls like array_values() or idx(). $idx = 0; foreach ($this->children as $child) { if ($idx == $index) { return $child; } ++$idx; } throw new Exception(pht("No child with index '%d'.", $index)); } /** * Build a cache to improve the performance of * @{method:selectDescendantsOfType}. This cache makes a time/memory tradeoff * by aggressively caching node descendants. It may improve the tree's query * performance substantially if you make a large number of queries, but also * requires a significant amount of memory. * * This builds a cache for the entire tree and improves performance of all * @{method:selectDescendantsOfType} calls. */ public function buildSelectCache() { $cache = array(); foreach ($this->getChildren() as $id => $child) { $type_id = $child->getTypeID(); if (empty($cache[$type_id])) { $cache[$type_id] = array(); } $cache[$type_id][$id] = $child; foreach ($child->buildSelectCache() as $type_id => $nodes) { if (empty($cache[$type_id])) { $cache[$type_id] = array(); } $cache[$type_id] += $nodes; } } $this->selectCache = $cache; return $this->selectCache; } /** * Build a cache to improve the performance of @{method:selectTokensOfType}. * This cache makes a time/memory tradeoff by aggressively caching token * types. It may improve the tree's query performance substantially if you * make a large number of queries, but also requires a significant amount of * memory. * * This builds a cache for this node only. */ public function buildTokenCache() { $cache = array(); foreach ($this->getTokens() as $id => $token) { $cache[$token->getTypeName()][$id] = $token; } $this->tokenCache = $cache; return $this->tokenCache; } public function selectTokensOfType($type_name) { return $this->selectTokensOfTypes(array($type_name)); } /** * Select all tokens of any given types. */ public function selectTokensOfTypes(array $type_names) { $tokens = array(); foreach ($type_names as $type_name) { if (isset($this->tokenCache)) { $cached_tokens = idx($this->tokenCache, $type_name, array()); foreach ($cached_tokens as $id => $cached_token) { $tokens[$id] = $cached_token; } } else { foreach ($this->getTokens() as $id => $token) { if ($token->getTypeName() == $type_name) { $tokens[$id] = $token; } } } } return $tokens; } final public function isDescendantOf(AASTNode $node) { for ($it = $this; $it !== null; $it = $it->getParentNode()) { if ($it === $node) { return true; } } return false; } public function selectDescendantsOfType($type_name) { return $this->selectDescendantsOfTypes(array($type_name)); } public function selectDescendantsOfTypes(array $type_names) { $nodes = array(); foreach ($type_names as $type_name) { $type = $this->getTypeIDFromTypeName($type_name); if (isset($this->selectCache)) { if (isset($this->selectCache[$type])) { $nodes = $nodes + $this->selectCache[$type]; } } else { $nodes = $nodes + $this->executeSelectDescendantsOfType($this, $type); } } return AASTNodeList::newFromTreeAndNodes($this->tree, $nodes); } protected function executeSelectDescendantsOfType($node, $type) { $results = array(); foreach ($node->getChildren() as $id => $child) { if ($child->getTypeID() == $type) { $results[$id] = $child; } $results += $this->executeSelectDescendantsOfType($child, $type); } return $results; } public function getTokens() { if ($this->l == -1 || $this->r == -1) { return array(); } $tokens = $this->tree->getRawTokenStream(); $result = array(); foreach (range($this->l, $this->r) as $token_id) { $result[$token_id] = $tokens[$token_id]; } return $result; } public function getConcreteString() { $values = array(); foreach ($this->getTokens() as $token) { $values[] = $token->getValue(); } return implode('', $values); } public function getSemanticString() { $tokens = $this->getTokens(); foreach ($tokens as $id => $token) { if ($token->isComment()) { unset($tokens[$id]); } } return implode('', mpull($tokens, 'getValue')); } public function getIndentation() { $tokens = $this->getTokens(); $left = head($tokens); while ($left && (!$left->isAnyWhitespace() || strpos($left->getValue(), "\n") === false)) { $left = $left->getPrevToken(); } if (!$left) { return null; } return preg_replace("/^.*\n/s", '', $left->getValue()); } public function getDescription() { $concrete = $this->getConcreteString(); if (strlen($concrete) > 75) { $concrete = substr($concrete, 0, 36).'...'.substr($concrete, -36); } $concrete = addcslashes($concrete, "\\\n\""); return pht('a node of type %s: "%s"', $this->getTypeName(), $concrete); } final protected function getTypeIDFromTypeName($type_name) { return $this->tree->getNodeTypeIDFromTypeName($type_name); } final public function getOffset() { $stream = $this->tree->getRawTokenStream(); if (empty($stream[$this->l])) { return null; } return $stream[$this->l]->getOffset(); } final public function getLength() { $stream = $this->tree->getRawTokenStream(); if (empty($stream[$this->r])) { return null; } return $stream[$this->r]->getOffset() - $this->getOffset(); } public function getSurroundingNonsemanticTokens() { $before = array(); $after = array(); $tokens = $this->tree->getRawTokenStream(); if ($this->l != -1) { $before = $tokens[$this->l]->getNonsemanticTokensBefore(); } if ($this->r != -1) { $after = $tokens[$this->r]->getNonsemanticTokensAfter(); } return array($before, $after); } final public function getLineNumber() { return idx($this->tree->getOffsetToLineNumberMap(), $this->getOffset()); } final public function getEndLineNumber() { return idx( $this->tree->getOffsetToLineNumberMap(), $this->getOffset() + $this->getLength()); } /** * Determines whether the current node appears //after// a specified node in * the tree. * * @param AASTNode * @return bool */ final public function isAfter(AASTNode $node) { return head($this->getTokens())->getOffset() > last($node->getTokens())->getOffset(); } /** * Determines whether the current node appears //before// a specified node in * the tree. * * @param AASTNode * @return bool */ final public function isBefore(AASTNode $node) { return last($this->getTokens())->getOffset() < head($node->getTokens())->getOffset(); } /** * Determines whether a specified node is a descendant of the current node. * * @param AASTNode * @return bool */ final public function containsDescendant(AASTNode $node) { return !$this->isAfter($node) && !$this->isBefore($node); } public function dispose() { foreach ($this->getChildren() as $child) { $child->dispose(); } unset($this->selectCache); } }