diff --git a/composer.json b/composer.json index 19ed2473..c1d333fd 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "magento/magento-semver", "description": "Magento semantic version checker", - "version": "4.0.0", + "version": "5.0.0", "license": [ "OSL-3.0", "AFL-3.0" @@ -13,7 +13,8 @@ "symfony/console": "~4.1.0||~4.4.0", "tomzx/php-semver-checker": "^0.13.0", "wikimedia/less.php": "~1.8.0", - "zendframework/zend-stdlib": "^3.2.1" + "zendframework/zend-stdlib": "^3.2.1", + "nikic/php-parser": "^3.1" }, "require-dev": { "phpunit/phpunit": "^6.5.0", diff --git a/composer.lock b/composer.lock index a02995c1..647195a4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bcf7e3c8b1127bce7f252f930c24435e", + "content-hash": "97c29ab87137d8c0e984e439871455d0", "packages": [ { "name": "hassankhan/config", @@ -212,16 +212,16 @@ }, { "name": "symfony/console", - "version": "v4.4.4", + "version": "v4.4.5", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "f512001679f37e6a042b51897ed24a2f05eba656" + "reference": "4fa15ae7be74e53f6ec8c83ed403b97e23b665e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/f512001679f37e6a042b51897ed24a2f05eba656", - "reference": "f512001679f37e6a042b51897ed24a2f05eba656", + "url": "https://api.github.com/repos/symfony/console/zipball/4fa15ae7be74e53f6ec8c83ed403b97e23b665e9", + "reference": "4fa15ae7be74e53f6ec8c83ed403b97e23b665e9", "shasum": "" }, "require": { @@ -284,7 +284,7 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2020-01-25T12:44:29+00:00" + "time": "2020-02-24T13:10:00+00:00" }, { "name": "symfony/polyfill-ctype", @@ -521,16 +521,16 @@ }, { "name": "symfony/yaml", - "version": "v4.4.4", + "version": "v4.4.5", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "cd014e425b3668220adb865f53bff64b3ad21767" + "reference": "94d005c176db2080e98825d98e01e8b311a97a88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/cd014e425b3668220adb865f53bff64b3ad21767", - "reference": "cd014e425b3668220adb865f53bff64b3ad21767", + "url": "https://api.github.com/repos/symfony/yaml/zipball/94d005c176db2080e98825d98e01e8b311a97a88", + "reference": "94d005c176db2080e98825d98e01e8b311a97a88", "shasum": "" }, "require": { @@ -576,7 +576,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2020-01-21T11:12:16+00:00" + "time": "2020-02-03T10:46:43+00:00" }, { "name": "tomzx/finder", @@ -1156,16 +1156,16 @@ }, { "name": "phpspec/prophecy", - "version": "v1.10.2", + "version": "v1.10.3", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "b4400efc9d206e83138e2bb97ed7f5b14b831cd9" + "reference": "451c3cd1418cf640de218914901e51b064abb093" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/b4400efc9d206e83138e2bb97ed7f5b14b831cd9", - "reference": "b4400efc9d206e83138e2bb97ed7f5b14b831cd9", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093", + "reference": "451c3cd1418cf640de218914901e51b064abb093", "shasum": "" }, "require": { @@ -1215,7 +1215,7 @@ "spy", "stub" ], - "time": "2020-01-20T15:57:02+00:00" + "time": "2020-03-05T15:02:03+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/src/ClassHierarchy/DependencyInspectionVisitor.php b/src/ClassHierarchy/DependencyInspectionVisitor.php index 9a6c9ffe..48f8be92 100644 --- a/src/ClassHierarchy/DependencyInspectionVisitor.php +++ b/src/ClassHierarchy/DependencyInspectionVisitor.php @@ -15,8 +15,10 @@ use PhpParser\Node\Stmt\ClassLike; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Interface_ as InterfaceNode; -use PhpParser\Node\Stmt\Property; +use PhpParser\Node\Stmt\PropertyProperty; use PhpParser\Node\Stmt\Trait_ as TraitNode; +use PhpParser\Node\Stmt\TraitUse; +use PhpParser\NodeTraverser; use PhpParser\NodeVisitorAbstract; /** @@ -24,12 +26,20 @@ */ class DependencyInspectionVisitor extends NodeVisitorAbstract { + /** @var DependencyGraph */ private $dependencyGraph; /** @var NodeHelper */ private $nodeHelper; + /** + * @var Entity + * Holds current Entity. Stored so we can populate this entity in our dependency graph upon walking relevant child + * nodes. + */ + private $currentClassLike = null; + /** * Constructor. * @@ -43,135 +53,115 @@ public function __construct(DependencyGraph $dependencyGraph, NodeHelper $nodeHe } /** - * @inheritDoc + * Logic to process current node. We aggressively halt walking the AST since this may contain many nodes + * If we are visiting a Classlike node, set currentClassLike so we can populate this entity in our dependency graph + * upon walking relevant child nodes like PropertyProperty and ClassMethod. * - * Inspect nodes after all visitors have run since we need the fully qualified names of nodes. - */ - public function leaveNode(Node $node) - { - if ($node instanceof ClassNode) { - $this->addClassNode($node); - } elseif ($node instanceof InterfaceNode) { - $this->addInterfaceNode($node); - } elseif ($node instanceof TraitNode) { - $this->addTraitNode($node); - } - } - - /** - * Getter for {@link DependencyInspectionVisitor::$dependencyGraph}. + * Subparse tree we want to traverse will be something like: + * Namespace -> ClassLike -> ClassMethod + * -> TraitUse + * -> PropertyProperty * - * @return DependencyGraph - */ - public function getDependencyGraph(): DependencyGraph - { - return $this->dependencyGraph; - } - - /** - * @param ClassNode $node + * + * @inheritdoc + * + * @param Node $node + * @return int tells NodeTraverser whether to continue traversing */ - private function addClassNode(ClassNode $node) + public function enterNode(Node $node) { - // name is not set for anonymous classes, therefore they cannot be part of the dependency graph - if ($node->isAnonymous()) { - return; - } - - $className = (string)$node->namespacedName; - $class = $this->dependencyGraph->findOrCreateClass($className); - - [$methodList, $propertyList] = $this->fetchStmtsNodes($node); - $class->setMethodList($methodList); - $class->setPropertyList($propertyList); - $class->setIsApi($this->nodeHelper->isApiNode($node)); - - if ($node->extends) { - $parentClassName = (string)$node->extends; - $parentClassEntity = $this->dependencyGraph->findOrCreateClass($parentClassName); - $class->addExtends($parentClassEntity); - } - - foreach ($node->implements as $implement) { - $interfaceName = (string)$implement; - $interfaceEntity = $this->dependencyGraph->findOrCreateInterface($interfaceName); - $class->addImplements($interfaceEntity); - } - - foreach ($this->nodeHelper->getTraitUses($node) as $traitUse) { - foreach ($traitUse->traits as $trait) { - $traitName = (string)$trait; - $traitEntity = $this->dependencyGraph->findOrCreateTrait($traitName); - $class->addUses($traitEntity); - } + switch (true) { + case $node instanceof Node\Stmt\Namespace_: + return null; + case $node instanceof ClassLike: + //set currentClassLike entity + return $this->handleClassLike($node); + case $node instanceof ClassMethod: + $this->currentClassLike->addMethod($node); + return NodeTraverser::DONT_TRAVERSE_CHILDREN; + case $node instanceof TraitUse: + foreach ($node->traits as $trait) { + $traitName = (string)$trait; + $traitEntity = $this->dependencyGraph->findOrCreateTrait($traitName); + $this->currentClassLike->addUses($traitEntity); + } + return NodeTraverser::DONT_TRAVERSE_CHILDREN; + case $node instanceof PropertyProperty: + $this->currentClassLike->addProperty($node); + return NodeTraverser::DONT_TRAVERSE_CHILDREN; + default: + return NodeTraverser::DONT_TRAVERSE_CHILDREN; } - - $this->dependencyGraph->addEntity($class); } /** - * @param InterfaceNode $node + * Handles Class, Interface, and Traits nodes. Sets currentClassLike entity and will populate extends, implements, + * and API information + * + * @param ClassLike $node + * @return int|null */ - private function addInterfaceNode(InterfaceNode $node) + private function handleClassLike(ClassLike $node) { - $interfaceName = (string)$node->namespacedName; - $interface = $this->dependencyGraph->findOrCreateInterface($interfaceName); - - $interface->setIsApi($this->nodeHelper->isApiNode($node)); - [$methodList] = $this->fetchStmtsNodes($node); - $interface->setMethodList($methodList); - - foreach ($node->extends as $extend) { - $interfaceName = (string)$extend; - $interfaceEntity = $this->dependencyGraph->findOrCreateInterface($interfaceName); - $interface->addExtends($interfaceEntity); + /** + * @var \PhpParser\Node\Name $namespacedName + * This is set in the NamespaceResolver visitor + */ + $namespacedName = $node->namespacedName; + switch (true) { + case $node instanceof ClassNode: + if ($node->isAnonymous()) { + return NodeTraverser::STOP_TRAVERSAL; + } + $this->currentClassLike = $this->dependencyGraph->findOrCreateClass((string)$namespacedName); + if ($node->extends) { + $parentClassName = (string)$node->extends; + $parentClassEntity = $this->dependencyGraph->findOrCreateClass($parentClassName); + $this->currentClassLike->addExtends($parentClassEntity); + } + foreach ($node->implements as $implement) { + $interfaceName = (string)$implement; + $interfaceEntity = $this->dependencyGraph->findOrCreateInterface($interfaceName); + $this->currentClassLike->addImplements($interfaceEntity); + } + break; + case $node instanceof InterfaceNode: + $this->currentClassLike = $this->dependencyGraph->findOrCreateInterface((string)$namespacedName); + foreach ($node->extends as $extend) { + $interfaceName = (string)$extend; + $interfaceEntity = $this->dependencyGraph->findOrCreateInterface($interfaceName); + $this->currentClassLike->addExtends($interfaceEntity); + } + break; + case $node instanceof TraitNode: + $this->currentClassLike = $this->dependencyGraph->findOrCreateTrait((string)$namespacedName); + break; } - - $this->dependencyGraph->addEntity($interface); + $this->currentClassLike->setIsApi($this->nodeHelper->isApiNode($node)); + return null; } - /** - * @param TraitNode $node + /* + * Unsets currentClassLike upon exiting ClassLike node. This is for cleanup, although this is not necessary since + * Classmethod, PropertyProperty, and TraitUse nodes will only be traversed after Classlike + * + * @param Node $node + * @return false|int|Node|Node[]|void|null */ - private function addTraitNode(TraitNode $node) + public function leaveNode(Node $node) { - $traitName = (string)$node->namespacedName; - $trait = $this->dependencyGraph->findOrCreateTrait($traitName); - - [$methodList, $propertyList] = $this->fetchStmtsNodes($node); - $trait->setMethodList($methodList); - $trait->setPropertyList($propertyList); - $trait->setIsApi($this->nodeHelper->isApiNode($node)); - - foreach ($this->nodeHelper->getTraitUses($node) as $traitUse) { - foreach ($traitUse->traits as $parentTrait) { - $parentTraitName = (string)$parentTrait; - $parentTraitEntity = $this->dependencyGraph->findOrCreateTrait($parentTraitName); - $trait->addUses($parentTraitEntity); - } + if ($node instanceof ClassLike) { + $this->currentClassLike = null; } - - $this->dependencyGraph->addEntity($trait); } /** - * @param ClassLike $node - * @return array + * Getter for {@link DependencyInspectionVisitor::$dependencyGraph}. + * + * @return DependencyGraph */ - private function fetchStmtsNodes(ClassLike $node): array + public function getDependencyGraph(): DependencyGraph { - $methodList = []; - $propertyList = []; - foreach ($node->stmts as $stmt) { - if ($stmt instanceof ClassMethod) { - $methodList[$stmt->name] = $stmt; - } elseif ($stmt instanceof Property) { - foreach ($stmt->props as $prop) { - $propertyList[$prop->name] = $prop; - } - } - } - - return [$methodList, $propertyList]; + return $this->dependencyGraph; } } diff --git a/src/ClassHierarchy/Entity.php b/src/ClassHierarchy/Entity.php index 53c1e517..563bf236 100644 --- a/src/ClassHierarchy/Entity.php +++ b/src/ClassHierarchy/Entity.php @@ -11,6 +11,7 @@ use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Property; +use PhpParser\Node\Stmt\PropertyProperty; /** * Implements an entity that reflects a `class`, `interface` or `trait` and its dependencies. @@ -368,7 +369,29 @@ private function addUsedBy(Entity $entity) */ public function setMethodList(array $methodList): void { - $this->methodList = $methodList; + $this->methodList = []; + foreach ($methodList as $method) { + $this->addMethod($method); + } + } + + /** + * Also cleans method to prevent memory leaks. + * @param ClassMethod $method + */ + public function addMethod(ClassMethod $method): void + { + //remove stmts from Method + $method->stmts = []; + $this->methodList[$method->name] = $method; + } + + /** + * @param PropertyProperty $property + */ + public function addProperty(PropertyProperty $property): void + { + $this->propertyList[$property->name] = $property; } /** @@ -376,7 +399,10 @@ public function setMethodList(array $methodList): void */ public function setPropertyList(array $propertyList): void { - $this->propertyList = $propertyList; + $this->propertyList = []; + foreach ($propertyList as $property) { + $this->addProperty($property); + } } /** diff --git a/src/Helper/Node.php b/src/Helper/Node.php index f2a827e5..555841ba 100644 --- a/src/Helper/Node.php +++ b/src/Helper/Node.php @@ -31,29 +31,4 @@ public function isApiNode(PhpNode $node) return isset($comment[0]) && strpos($comment[0]->getText(), SemanticVersionChecker::ANNOTATION_API) !== false; } - - /** - * Workaround for different versions of `nikic/php-parser`. - * - * Later versions of the package `nikic/php-parser` add the convenience method `ClassLike::getTraitUses()`. If we - * happen to have such a version, that method is called and its result is returned, otherwise we extract the - * {@link \PhpParser\Node\Stmt\TraitUse} and return them. - * - * @param PhpNode $node - * @return TraitUse[] - */ - public function getTraitUses(PhpNode $node): array - { - if (method_exists($node, 'getTraitUses')) { - return $node->getTraitUses(); - } - - $traitUses = []; - foreach ($node->stmts as $stmt) { - if ($stmt instanceof TraitUse) { - $traitUses[] = $stmt; - } - } - return $traitUses; - } } diff --git a/src/ReportBuilder.php b/src/ReportBuilder.php index 3e7f93d9..6b3d55be 100644 --- a/src/ReportBuilder.php +++ b/src/ReportBuilder.php @@ -141,8 +141,14 @@ protected function buildReport() $sourceBeforeFiles = $fileIterator->findFromString($this->sourceBeforeDir, '', ''); $sourceAfterFiles = $fileIterator->findFromString($this->sourceAfterDir, '', ''); - //let static analyzer build a complete dependency graph + $staticAnalyzer = (new StaticAnalyzerFactory())->create(); + + /** + * Run dependency analysis over entire codebase. Necessary as we should parse parents and siblings of unchanged + * files. + */ + //MC-31705: Dependency graph get overwritten twice here. Document or fix this $staticAnalyzer->analyse($sourceBeforeFiles); $dependencyMap = $staticAnalyzer->analyse($sourceAfterFiles); @@ -151,6 +157,9 @@ protected function buildReport() $scannerBefore = new ScannerRegistry($scannerRegistryFactory->create($dependencyMap)); $scannerAfter = new ScannerRegistry($scannerRegistryFactory->create($dependencyMap)); + /** + * Filter unchanged files. (All json files will remain because of filter) + */ foreach ($this->getFilters($this->sourceBeforeDir, $this->sourceAfterDir) as $filter) { // filters modify arrays by reference $filter->filter($sourceBeforeFiles, $sourceAfterFiles); diff --git a/src/Visitor/AbstractApiVisitor.php b/src/Visitor/AbstractApiVisitor.php index 8f789f36..2d66e2be 100644 --- a/src/Visitor/AbstractApiVisitor.php +++ b/src/Visitor/AbstractApiVisitor.php @@ -10,6 +10,7 @@ use Magento\SemanticVersionChecker\ClassHierarchy\DependencyGraph; use Magento\SemanticVersionChecker\Helper\Node as NodeHelper; use PhpParser\Node; +use PhpParser\NodeTraverser; use PhpParser\NodeVisitorAbstract; use PHPSemVerChecker\Registry\Registry; @@ -43,8 +44,33 @@ public function __construct( } /** + * Halt walking when we reach Classlike node * @param Node $node - * @return void + * @return int + */ + public function enterNode(Node $node) + { + switch (true) { + case $node instanceof Node\Stmt\Namespace_: + case $node instanceof Node\Stmt\ClassLike: + return null; + default: + /* + * Note that by skipping traversal of ClassMethod children, NameResolver will not resolve namespaces on + * its method stmts. This will affect analyzing for ClassMethodImplementationChanged in + * src/Analyzer/ClassMethodAnalyzer.php + * For example changing: + * a = \Magento\Framework\App\ObjectManager::getInstance(); + * To: + * a = ObjectManager::getInstance(); + * will now be analyzed as a ClassMethodImplementationChanged (a PATCH change). + */ + return NodeTraverser::DONT_TRAVERSE_CHILDREN; + } + } + + /** + * @inheritdoc */ public function leaveNode(Node $node) {