From 76d4fd4e48120a0a5bee29b46b1d743214f37b8f Mon Sep 17 00:00:00 2001 From: Tom Erskine Date: Wed, 13 Nov 2019 13:17:03 -0600 Subject: [PATCH] Updating To latest Infra SVC code changes --- src/Analyzer/AbstractCodeAnalyzer.php | 40 +- src/Analyzer/ClassAnalyzer.php | 28 +- src/Analyzer/ClassMethodAnalyzer.php | 105 +++++- src/Analyzer/Factory/AnalyzerFactory.php | 8 +- .../Factory/AnalyzerFactoryInterface.php | 6 +- .../Factory/DbSchemaAnalyzerFactory.php | 6 +- src/Analyzer/Factory/DiAnalyzerFactory.php | 6 +- .../Factory/LayoutAnalyzerFactory.php | 6 +- src/Analyzer/Factory/LessAnalyzerFactory.php | 6 +- .../Factory/NonApiAnalyzerFactory.php | 6 +- .../Factory/SystemXmlAnalyzerFactory.php | 6 +- src/Analyzer/Factory/XsdAnalyzerFactory.php | 6 +- src/Analyzer/Xsd/Analyzer.php | 4 +- .../DependencyInspectionVisitor.php | 32 +- src/ClassHierarchy/Entity.php | 47 ++- src/Filter/SourceFilter.php | 2 +- src/Operation/ClassMethodOverwriteAdded.php | 43 +++ src/Operation/ClassMethodOverwriteRemoved.php | 43 +++ src/ReportBuilder.php | 10 +- src/Reporter/HtmlDbSchemaReporter.php | 4 + .../CompareSourceCommandApiClassesTest.php | 19 +- .../source-code-after/TestClass.php | 357 ++++++++++-------- .../source-code-before/TestClass.php | 340 +++++++++-------- 23 files changed, 729 insertions(+), 401 deletions(-) create mode 100644 src/Operation/ClassMethodOverwriteAdded.php create mode 100644 src/Operation/ClassMethodOverwriteRemoved.php diff --git a/src/Analyzer/AbstractCodeAnalyzer.php b/src/Analyzer/AbstractCodeAnalyzer.php index cb0f5d9a..b819a2a7 100644 --- a/src/Analyzer/AbstractCodeAnalyzer.php +++ b/src/Analyzer/AbstractCodeAnalyzer.php @@ -7,6 +7,7 @@ namespace Magento\SemanticVersionChecker\Analyzer; +use Magento\SemanticVersionChecker\ClassHierarchy\DependencyGraph; use Magento\SemanticVersionChecker\Helper\ClassParser; use PhpParser\Node; use PhpParser\Node\Stmt\Class_; @@ -22,59 +23,59 @@ abstract class AbstractCodeAnalyzer implements AnalyzerInterface * @var null|string */ protected $context; - /** * File path before changes. * * @var null|string */ protected $fileBefore; - /** * File path after changes. * * @var null|string */ protected $fileAfter; - + /** + * @var null|DependencyGraph + */ + protected $dependencyGraph; /** * @param string $context * @param string $fileBefore * @param string $fileAfter + * @param DependencyGraph|null $dependencyGraph */ - public function __construct($context = null, $fileBefore = null, $fileAfter = null) - { + public function __construct( + $context = null, + $fileBefore = null, + $fileAfter = null, + DependencyGraph $dependencyGraph = null + ) { $this->context = $context; $this->fileBefore = $fileBefore; $this->fileAfter = $fileAfter; + $this->dependencyGraph = $dependencyGraph; } - public function analyze($registryBefore, $registryAfter) { $report = new Report(); - $beforeNameMap = $this->getNodeNameMap($registryBefore); $afterNameMap = $this->getNodeNameMap($registryAfter); - $namesBefore = array_keys($beforeNameMap); $namesAfter = array_keys($afterNameMap); $added = array_diff($namesAfter, $namesBefore); $removed = array_diff($namesBefore, $namesAfter); $toVerify = array_intersect($namesBefore, $namesAfter); - $this->reportAdded($report, $registryAfter, $added); $this->reportMovedOrRemoved($report, $registryBefore, $registryAfter, $removed); - $this->reportChanged( $report, $registryBefore, $registryAfter, $toVerify ); - return $report; } - /** * Gets the appropriate nodes from the context and maps them to their names * @@ -90,7 +91,6 @@ protected function getNodeNameMap($context) } return $keyed; } - /** * Has the node been moved to parent class. * @@ -101,13 +101,11 @@ protected function getNodeNameMap($context) protected function isMovedToParent($parsedClass, $removedNode) { $parentClass = $parsedClass->getParentClass(); - if ($removedNode instanceof ClassLike || $parentClass === null || (property_exists($removedNode, 'flags') && in_array($removedNode->flags, [Class_::MODIFIER_PRIVATE]))) { return false; } - $parentNodes = $parentClass->getNodesOfType($this->getNodeClass()); foreach ($parentNodes as $parentNode) { $parentNodeName = $this->getNodeName($parentNode); @@ -117,10 +115,8 @@ protected function isMovedToParent($parsedClass, $removedNode) return true; } } - return $this->isMovedToParent($parentClass, $removedNode); } - /** * Get the name for a given node of the type analyzed * @@ -128,14 +124,12 @@ protected function isMovedToParent($parsedClass, $removedNode) * @return string */ abstract protected function getNodeName($node); - /** * The class of the nodes to analyze * * @return string */ abstract protected function getNodeClass(); - /** * Create and report a NodeAdded operation * @@ -146,7 +140,6 @@ abstract protected function getNodeClass(); * @return void */ abstract protected function reportAddedNode($report, $fileAfter, $contextAfter, $nodeAfter); - /** * Create and report a NodeRemoved operation * @@ -157,7 +150,6 @@ abstract protected function reportAddedNode($report, $fileAfter, $contextAfter, * @return void */ abstract protected function reportRemovedNode($report, $fileBefore, $contextBefore, $nodeBefore); - /** * Create and report a NodeMoved operation * @@ -171,7 +163,6 @@ protected function reportMovedNode($report, $fileBefore, $contextBefore, $nodeBe { // ClassLike nodes do not have Moved operations, so do not enforce implementing this method } - /** * Report the list of added nodes * @@ -190,7 +181,6 @@ protected function reportAdded($report, $contextAfter, $addedNames) $this->reportAddedNode($report, $fileAfter, $contextAfter, $node); } } - /** * Report moved or removed nodes * @@ -214,7 +204,6 @@ protected function reportMovedOrRemoved($report, $contextBefore, $contextAfter, } } } - /** * Find changes to nodes that exist in both before and after states and add them to the report * @@ -228,7 +217,6 @@ protected function reportChanged($report, $contextBefore, $contextAfter, $toVeri { // Not all types have changes beyond add/remove } - /** * Get the filename to use in the report. * @@ -243,4 +231,4 @@ protected function getFileName($context, $nodeName, $isBefore = true) { return $isBefore ? $this->fileBefore : $this->fileAfter; } -} +} \ No newline at end of file diff --git a/src/Analyzer/ClassAnalyzer.php b/src/Analyzer/ClassAnalyzer.php index eb1812ef..eb5e82e1 100644 --- a/src/Analyzer/ClassAnalyzer.php +++ b/src/Analyzer/ClassAnalyzer.php @@ -7,6 +7,7 @@ namespace Magento\SemanticVersionChecker\Analyzer; +use Magento\SemanticVersionChecker\ClassHierarchy\DependencyGraph; use PhpParser\Node\Stmt\Class_; use PHPSemVerChecker\Operation\ClassAdded; use PHPSemVerChecker\Operation\ClassRemoved; @@ -45,18 +46,6 @@ protected function getNodeClass() return Class_::class; } - - /** - * Get the class node registry - * - * @param Registry $registry - * @return Class_[] - */ - protected function getNodeNameMap($registry) - { - return $registry->data[static::CONTEXT]; - } - /** * Get the filename from the registry * @@ -120,7 +109,7 @@ protected function reportChanged($report, $registryBefore, $registryAfter, $toVe if ($classBefore !== $classAfter) { /** @var AnalyzerInterface[] $analyzers */ $analyzers = [ - new ClassMethodAnalyzer(static::CONTEXT, $fileBefore, $fileAfter), + new ClassMethodAnalyzer(static::CONTEXT, $fileBefore, $fileAfter, $this->dependencyGraph), new PropertyAnalyzer(static::CONTEXT, $fileBefore, $fileAfter), new ClassConstantAnalyzer(static::CONTEXT, $fileBefore, $fileAfter), new ClassMethodExceptionAnalyzer(static::CONTEXT, $fileBefore, $fileAfter), @@ -136,4 +125,15 @@ protected function reportChanged($report, $registryBefore, $registryAfter, $toVe } } } -} + + /** + * Get the class node registry + * + * @param Registry $registry + * @return Class_[] + */ + protected function getNodeNameMap($registry) + { + return $registry->data[static::CONTEXT]; + } +} \ No newline at end of file diff --git a/src/Analyzer/ClassMethodAnalyzer.php b/src/Analyzer/ClassMethodAnalyzer.php index 08851685..5fa13f1a 100644 --- a/src/Analyzer/ClassMethodAnalyzer.php +++ b/src/Analyzer/ClassMethodAnalyzer.php @@ -15,16 +15,18 @@ use Magento\SemanticVersionChecker\Operation\ClassMethodLastParameterRemoved; use Magento\SemanticVersionChecker\Operation\ClassMethodMoved; use Magento\SemanticVersionChecker\Operation\ClassMethodOptionalParameterAdded; +use Magento\SemanticVersionChecker\Operation\ClassMethodOverwriteAdded; +use Magento\SemanticVersionChecker\Operation\ClassMethodOverwriteRemoved; use Magento\SemanticVersionChecker\Operation\ClassMethodParameterTypingChanged; use Magento\SemanticVersionChecker\Operation\ClassMethodReturnTypingChanged; use Magento\SemanticVersionChecker\Operation\ExtendableClassConstructorOptionalParameterAdded; use Magento\SemanticVersionChecker\Operation\Visibility\MethodDecreased as VisibilityMethodDecreased; use Magento\SemanticVersionChecker\Operation\Visibility\MethodIncreased as VisibilityMethodIncreased; +use PhpParser\Node\NullableType; use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\ClassLike; use PhpParser\Node\Stmt\ClassMethod; use PHPSemVerChecker\Comparator\Implementation; -use PHPSemVerChecker\Comparator\Type; use PHPSemVerChecker\Operation\ClassMethodAdded; use PHPSemVerChecker\Operation\ClassMethodImplementationChanged; use PHPSemVerChecker\Operation\ClassMethodOperationUnary; @@ -35,6 +37,12 @@ use PHPSemVerChecker\Operation\ClassMethodParameterTypingRemoved; use PHPSemVerChecker\Operation\ClassMethodRemoved; use PHPSemVerChecker\Report\Report; +use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; +use PHPStan\PhpDocParser\Lexer\Lexer; +use PHPStan\PhpDocParser\Parser\ConstExprParser; +use PHPStan\PhpDocParser\Parser\PhpDocParser; +use PHPStan\PhpDocParser\Parser\TokenIterator; +use PHPStan\PhpDocParser\Parser\TypeParser; /** * Class method analyzer. @@ -105,6 +113,27 @@ protected function getNodeClass() */ protected function reportAddedNode($report, $fileAfter, $classAfter, $methodAfter) { + if ($this->dependencyGraph === null) { + $report->add($this->context, new ClassMethodAdded($this->context, $fileAfter, $classAfter, $methodAfter)); + return; + } + + $class = $this->dependencyGraph->findEntityByName((string) $classAfter->namespacedName); + + if ($class !== null) { + foreach ($class->getExtends() as $entity) { + $methods = $entity->getMethodList(); + // checks if the method is already exiting in parent class + if (isset($methods[$methodAfter->name])) { + $report->add( + $this->context, + new ClassMethodOverwriteAdded($this->context, $fileAfter, $classAfter, $methodAfter) + ); + return; + } + } + } + $report->add($this->context, new ClassMethodAdded($this->context, $fileAfter, $classAfter, $methodAfter)); } @@ -331,12 +360,76 @@ protected function reportChanged($report, $contextBefore, $contextAfter, $method */ private function isReturnTypeChanged(ClassMethod $methodBefore, ClassMethod $methodAfter): bool { - $hasPHP7ReturnDeclarationChanged = !Type::isSame($methodBefore->returnType, $methodAfter->returnType); + return $this->isDocBlockAnnotationReturnTypeChanged($methodBefore, $methodAfter) || $this->isDeclarationReturnTypeChanged($methodBefore, $methodAfter); + } + + /** + * @param ClassMethod $methodBefore + * @param ClassMethod $methodAfter + * + * @return bool + */ + private function isDocBlockAnnotationReturnTypeChanged(ClassMethod $methodBefore, ClassMethod $methodAfter) + { + $returnBefore = $this->getDocReturnDeclaration($methodBefore); + $returnAfter = $this->getDocReturnDeclaration($methodAfter); - $returnBefore = $this->methodDocBlockAnalyzer->getMethodDocDeclarationByTag($methodBefore, $this->methodDocBlockAnalyzer::DOC_RETURN_TAG); - $returnAfter = $this->methodDocBlockAnalyzer->getMethodDocDeclarationByTag($methodAfter, $this->methodDocBlockAnalyzer::DOC_RETURN_TAG); + return $returnBefore !== $returnAfter; + } - return $hasPHP7ReturnDeclarationChanged || $returnBefore !== $returnAfter; + /** + * @param ClassMethod $methodBefore + * @param ClassMethod $methodAfter + * + * @return bool + */ + private function isDeclarationReturnTypeChanged(ClassMethod $methodBefore, ClassMethod $methodAfter) + { + if (!$this->isReturnsEqualByNullability($methodBefore, $methodAfter)) { + return true; + } + $beforeMethodReturnType = $methodBefore->returnType instanceof NullableType ? (string) $methodBefore->returnType->type : (string) $methodBefore->returnType; + $afterMethodReturnType = $methodAfter->returnType instanceof NullableType ? (string) $methodAfter->returnType->type : (string) $methodAfter->returnType; + + return $beforeMethodReturnType !== $afterMethodReturnType; + } + + /** + * checks if both return types has same nullable status + * + * @param ClassMethod $before + * @param ClassMethod $after + * + * @return bool + */ + private function isReturnsEqualByNullability(ClassMethod $before, ClassMethod $after): bool + { + return ($before instanceof NullableType) === ($after instanceof NullableType); + } + + /** + * Analyses the Method doc block and returns the return type declaration + * + * @param ClassMethod $method + * + * @return string + */ + private function getDocReturnDeclaration(ClassMethod $method) + { + if ($method->getDocComment() !== null) { + $lexer = new Lexer(); + $typeParser = new TypeParser(); + $constExprParser = new ConstExprParser(); + $phpDocParser = new PhpDocParser($typeParser, $constExprParser); + + $tokens = $lexer->tokenize((string)$method->getDocComment()); + $tokenIterator = new TokenIterator($tokens); + $phpDocNode = $phpDocParser->parse($tokenIterator); + $tags = $phpDocNode->getTagsByName('@return'); + /** @var PhpDocTagNode $tag */ + $tag = array_shift($tags); + } + return isset($tag) ? (string)$tag->value : ' '; } /** @@ -415,4 +508,4 @@ private function analyzeRemainingMethodParams($contextAfter, $methodAfter, $rema return $data; } -} +} \ No newline at end of file diff --git a/src/Analyzer/Factory/AnalyzerFactory.php b/src/Analyzer/Factory/AnalyzerFactory.php index 07defed5..255971ef 100644 --- a/src/Analyzer/Factory/AnalyzerFactory.php +++ b/src/Analyzer/Factory/AnalyzerFactory.php @@ -12,6 +12,7 @@ use Magento\SemanticVersionChecker\Analyzer\ClassAnalyzer; use Magento\SemanticVersionChecker\Analyzer\InterfaceAnalyzer; use Magento\SemanticVersionChecker\Analyzer\TraitAnalyzer; +use Magento\SemanticVersionChecker\ClassHierarchy\DependencyGraph; /** * Builds a PHP File Analyzer @@ -20,16 +21,17 @@ class AnalyzerFactory implements AnalyzerFactoryInterface { /** + * @param DependencyGraph|null $dependencyGraph * @return AnalyzerInterface */ - public function create(): AnalyzerInterface + public function create(DependencyGraph $dependencyGraph = null): AnalyzerInterface { $analyzers = [ - new ClassAnalyzer(), + new ClassAnalyzer(null, null, null, $dependencyGraph), new InterfaceAnalyzer(), new TraitAnalyzer(), ]; return new Analyzer($analyzers); } -} +} \ No newline at end of file diff --git a/src/Analyzer/Factory/AnalyzerFactoryInterface.php b/src/Analyzer/Factory/AnalyzerFactoryInterface.php index e5d09212..c9d01b9f 100644 --- a/src/Analyzer/Factory/AnalyzerFactoryInterface.php +++ b/src/Analyzer/Factory/AnalyzerFactoryInterface.php @@ -8,6 +8,7 @@ namespace Magento\SemanticVersionChecker\Analyzer\Factory; use Magento\SemanticVersionChecker\Analyzer\AnalyzerInterface; +use Magento\SemanticVersionChecker\ClassHierarchy\DependencyGraph; /** * Defines an interface for analyzer factory to build analyzer. @@ -15,7 +16,8 @@ interface AnalyzerFactoryInterface { /** + * @param DependencyGraph|null $dependencyGraph * @return AnalyzerInterface */ - public function create(): AnalyzerInterface; -} + public function create(DependencyGraph $dependencyGraph = null): AnalyzerInterface; +} \ No newline at end of file diff --git a/src/Analyzer/Factory/DbSchemaAnalyzerFactory.php b/src/Analyzer/Factory/DbSchemaAnalyzerFactory.php index 8ddac947..5301e39a 100644 --- a/src/Analyzer/Factory/DbSchemaAnalyzerFactory.php +++ b/src/Analyzer/Factory/DbSchemaAnalyzerFactory.php @@ -16,6 +16,7 @@ use Magento\SemanticVersionChecker\Analyzer\DBSchema\DbSchemaColumnAnalyzer; use Magento\SemanticVersionChecker\Analyzer\DBSchema\DbSchemaWhitelistAnalyzer; use Magento\SemanticVersionChecker\Analyzer\DBSchema\DbSchemaWhitelistReductionOrRemovalAnalyzer; +use Magento\SemanticVersionChecker\ClassHierarchy\DependencyGraph; use PHPSemVerChecker\Report\Report; /** @@ -24,9 +25,10 @@ class DbSchemaAnalyzerFactory implements AnalyzerFactoryInterface { /** + * @param DependencyGraph|null $dependencyGraph * @return AnalyzerInterface */ - public function create(): AnalyzerInterface + public function create(DependencyGraph $dependencyGraph = null): AnalyzerInterface { $report = new Report(); $analyzers = [ @@ -41,4 +43,4 @@ public function create(): AnalyzerInterface return new Analyzer($analyzers); } -} +} \ No newline at end of file diff --git a/src/Analyzer/Factory/DiAnalyzerFactory.php b/src/Analyzer/Factory/DiAnalyzerFactory.php index 9cd1c6c9..3b03106a 100644 --- a/src/Analyzer/Factory/DiAnalyzerFactory.php +++ b/src/Analyzer/Factory/DiAnalyzerFactory.php @@ -10,6 +10,7 @@ use Magento\SemanticVersionChecker\Analyzer\Analyzer; use Magento\SemanticVersionChecker\Analyzer\AnalyzerInterface; use Magento\SemanticVersionChecker\Analyzer\DiXml\VirtualTypeAnalyzer; +use Magento\SemanticVersionChecker\ClassHierarchy\DependencyGraph; use Magento\SemanticVersionChecker\DbSchemaReport; /** @@ -18,9 +19,10 @@ class DiAnalyzerFactory implements AnalyzerFactoryInterface { /** + * @param DependencyGraph|null $dependencyGraph * @return AnalyzerInterface */ - public function create(): AnalyzerInterface + public function create(DependencyGraph $dependencyGraph = null): AnalyzerInterface { $report = new DbSchemaReport(); $analyzers = [ @@ -29,4 +31,4 @@ public function create(): AnalyzerInterface return new Analyzer($analyzers); } -} +} \ No newline at end of file diff --git a/src/Analyzer/Factory/LayoutAnalyzerFactory.php b/src/Analyzer/Factory/LayoutAnalyzerFactory.php index 0ccef009..e70bf00c 100644 --- a/src/Analyzer/Factory/LayoutAnalyzerFactory.php +++ b/src/Analyzer/Factory/LayoutAnalyzerFactory.php @@ -10,6 +10,7 @@ use Magento\SemanticVersionChecker\Analyzer\Analyzer; use Magento\SemanticVersionChecker\Analyzer\AnalyzerInterface; use Magento\SemanticVersionChecker\Analyzer\Layout\Analyzer as LayoutAnalyzer; +use Magento\SemanticVersionChecker\ClassHierarchy\DependencyGraph; use Magento\SemanticVersionChecker\DbSchemaReport; /** @@ -18,9 +19,10 @@ class LayoutAnalyzerFactory implements AnalyzerFactoryInterface { /** + * @param DependencyGraph|null $dependencyGraph * @return AnalyzerInterface */ - public function create(): AnalyzerInterface + public function create(DependencyGraph $dependencyGraph = null): AnalyzerInterface { $report = new DbSchemaReport(); $analyzers = [ @@ -29,4 +31,4 @@ public function create(): AnalyzerInterface return new Analyzer($analyzers); } -} +} \ No newline at end of file diff --git a/src/Analyzer/Factory/LessAnalyzerFactory.php b/src/Analyzer/Factory/LessAnalyzerFactory.php index f3d98682..bb78e6ac 100644 --- a/src/Analyzer/Factory/LessAnalyzerFactory.php +++ b/src/Analyzer/Factory/LessAnalyzerFactory.php @@ -10,6 +10,7 @@ use Magento\SemanticVersionChecker\Analyzer\Analyzer; use Magento\SemanticVersionChecker\Analyzer\AnalyzerInterface; use Magento\SemanticVersionChecker\Analyzer\Less\Analyzer as LessAnalyzer; +use Magento\SemanticVersionChecker\ClassHierarchy\DependencyGraph; use Magento\SemanticVersionChecker\DbSchemaReport; /** @@ -18,9 +19,10 @@ class LessAnalyzerFactory implements AnalyzerFactoryInterface { /** + * @param DependencyGraph|null $dependencyGraph * @return AnalyzerInterface */ - public function create(): AnalyzerInterface + public function create(DependencyGraph $dependencyGraph = null): AnalyzerInterface { $report = new DbSchemaReport(); $analyzers = [ @@ -29,4 +31,4 @@ public function create(): AnalyzerInterface return new Analyzer($analyzers); } -} +} \ No newline at end of file diff --git a/src/Analyzer/Factory/NonApiAnalyzerFactory.php b/src/Analyzer/Factory/NonApiAnalyzerFactory.php index 1115ddde..7c1d948b 100644 --- a/src/Analyzer/Factory/NonApiAnalyzerFactory.php +++ b/src/Analyzer/Factory/NonApiAnalyzerFactory.php @@ -12,6 +12,7 @@ use Magento\SemanticVersionChecker\Analyzer\InterfaceAnalyzer; use Magento\SemanticVersionChecker\Analyzer\NonApiAnalyzer; use Magento\SemanticVersionChecker\Analyzer\TraitAnalyzer; +use Magento\SemanticVersionChecker\ClassHierarchy\DependencyGraph; /** * Implements a factory for {@link NonApiAnalyzer}. @@ -19,9 +20,10 @@ class NonApiAnalyzerFactory implements AnalyzerFactoryInterface { /** + * @param DependencyGraph|null $dependencyGraph * @return AnalyzerInterface */ - public function create(): AnalyzerInterface + public function create(DependencyGraph $dependencyGraph = null): AnalyzerInterface { $analyzers = [ new ClassAnalyzer(), @@ -31,4 +33,4 @@ public function create(): AnalyzerInterface return new NonApiAnalyzer($analyzers); } -} +} \ No newline at end of file diff --git a/src/Analyzer/Factory/SystemXmlAnalyzerFactory.php b/src/Analyzer/Factory/SystemXmlAnalyzerFactory.php index 1166e032..c89a48df 100644 --- a/src/Analyzer/Factory/SystemXmlAnalyzerFactory.php +++ b/src/Analyzer/Factory/SystemXmlAnalyzerFactory.php @@ -10,6 +10,7 @@ use Magento\SemanticVersionChecker\Analyzer\Analyzer; use Magento\SemanticVersionChecker\Analyzer\AnalyzerInterface; use Magento\SemanticVersionChecker\Analyzer\SystemXml\Analyzer as SystemXmlAnalyzer; +use Magento\SemanticVersionChecker\ClassHierarchy\DependencyGraph; use Magento\SemanticVersionChecker\DbSchemaReport; /** @@ -19,9 +20,10 @@ class SystemXmlAnalyzerFactory implements AnalyzerFactoryInterface { /** + * @param DependencyGraph|null $dependencyGraph * @return AnalyzerInterface */ - public function create(): AnalyzerInterface + public function create(DependencyGraph $dependencyGraph = null): AnalyzerInterface { $report = new DbSchemaReport(); $analyzers = [ @@ -30,4 +32,4 @@ public function create(): AnalyzerInterface return new Analyzer($analyzers); } -} +} \ No newline at end of file diff --git a/src/Analyzer/Factory/XsdAnalyzerFactory.php b/src/Analyzer/Factory/XsdAnalyzerFactory.php index 7eaeda2e..23e8428d 100644 --- a/src/Analyzer/Factory/XsdAnalyzerFactory.php +++ b/src/Analyzer/Factory/XsdAnalyzerFactory.php @@ -10,6 +10,7 @@ use Magento\SemanticVersionChecker\Analyzer\Analyzer; use Magento\SemanticVersionChecker\Analyzer\AnalyzerInterface; use Magento\SemanticVersionChecker\Analyzer\Xsd\Analyzer as XsdAnalyzer; +use Magento\SemanticVersionChecker\ClassHierarchy\DependencyGraph; use Magento\SemanticVersionChecker\DbSchemaReport; /** @@ -18,9 +19,10 @@ class XsdAnalyzerFactory implements AnalyzerFactoryInterface { /** + * @param DependencyGraph|null $dependencyGraph * @return AnalyzerInterface */ - public function create(): AnalyzerInterface + public function create(DependencyGraph $dependencyGraph = null): AnalyzerInterface { $report = new DbSchemaReport(); $analyzers = [ @@ -29,4 +31,4 @@ public function create(): AnalyzerInterface return new Analyzer($analyzers); } -} +} \ No newline at end of file diff --git a/src/Analyzer/Xsd/Analyzer.php b/src/Analyzer/Xsd/Analyzer.php index f9a8c2e0..2018dc94 100644 --- a/src/Analyzer/Xsd/Analyzer.php +++ b/src/Analyzer/Xsd/Analyzer.php @@ -78,8 +78,8 @@ public function analyze($registryBefore, $registryAfter) //process common modules foreach ($commonModules as $moduleName) { - $nodesBeforeByModule = $nodesBefore[$moduleName] ?: []; - $nodesAfterByModule = $nodesAfter[$moduleName] ?: []; + $nodesBeforeByModule = array_key_exists($moduleName, $nodesBefore) ? $nodesBefore[$moduleName]: []; + $nodesAfterByModule = array_key_exists($moduleName, $nodesAfter) ? $nodesAfter[$moduleName] : []; $filesBefore = array_keys($nodesBeforeByModule); $filesAfter = array_keys($nodesAfterByModule); diff --git a/src/ClassHierarchy/DependencyInspectionVisitor.php b/src/ClassHierarchy/DependencyInspectionVisitor.php index 9b2c22ba..5376624e 100644 --- a/src/ClassHierarchy/DependencyInspectionVisitor.php +++ b/src/ClassHierarchy/DependencyInspectionVisitor.php @@ -10,7 +10,10 @@ use Magento\SemanticVersionChecker\Helper\Node as NodeHelper; use PhpParser\Node; use PhpParser\Node\Stmt\Class_ as ClassNode; +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\Trait_ as TraitNode; use PhpParser\NodeVisitorAbstract; @@ -76,6 +79,9 @@ private function addClassNode(ClassNode $node) $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) { @@ -110,6 +116,8 @@ private function addInterfaceNode(InterfaceNode $node) $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; @@ -128,6 +136,9 @@ private function addTraitNode(TraitNode $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) { @@ -140,4 +151,23 @@ private function addTraitNode(TraitNode $node) $this->dependencyGraph->addEntity($trait); } -} + + /** + * @param ClassLike $node + * @return array + */ + private function fetchStmtsNodes(ClassLike $node): array + { + $methodList = []; + $propertyList = []; + foreach ($node->stmts as $stmt) { + if ($stmt instanceof ClassMethod) { + $methodList[$stmt->name] = $stmt; + } elseif ($stmt instanceof Property) { + $propertyList[$stmt->name] = $stmt; + } + } + + return [$methodList, $propertyList]; + } +} \ No newline at end of file diff --git a/src/ClassHierarchy/Entity.php b/src/ClassHierarchy/Entity.php index bc5eb123..41c44ace 100644 --- a/src/ClassHierarchy/Entity.php +++ b/src/ClassHierarchy/Entity.php @@ -8,6 +8,9 @@ namespace Magento\SemanticVersionChecker\ClassHierarchy; +use PhpParser\Node\Stmt\ClassMethod; +use PhpParser\Node\Stmt\Property; + /** * Implements an entity that reflects a `class`, `interface` or `trait` and its dependencies. */ @@ -86,6 +89,16 @@ class Entity */ private $usedBy = []; + /** + * @var ClassMethod[] + */ + private $methodList = []; + + /** + * @var Property[] + */ + private $propertyList = []; + /** * Constructor. * @@ -348,4 +361,36 @@ private function addUsedBy(Entity $entity) $key = $entity->getName(); $this->usedBy[$key] = $entity; } -} + + /** + * @param ClassMethod[] $methodList + */ + public function setMethodList(array $methodList): void + { + $this->methodList = $methodList; + } + + /** + * @param Property[] $propertyList + */ + public function setPropertyList(array $propertyList): void + { + $this->propertyList = $propertyList; + } + + /** + * @return ClassMethod[] + */ + public function getMethodList():array + { + return $this->methodList; + } + + /** + * @return Property[] + */ + public function getPropertyList():array + { + return $this->propertyList; + } +} \ No newline at end of file diff --git a/src/Filter/SourceFilter.php b/src/Filter/SourceFilter.php index 08a10e7a..f168f265 100644 --- a/src/Filter/SourceFilter.php +++ b/src/Filter/SourceFilter.php @@ -13,7 +13,7 @@ class SourceFilter { /** - * Filters not changed files + * Filters unchanged files * * @param array $filesBefore * @param array $filesAfter diff --git a/src/Operation/ClassMethodOverwriteAdded.php b/src/Operation/ClassMethodOverwriteAdded.php new file mode 100644 index 00000000..beae0554 --- /dev/null +++ b/src/Operation/ClassMethodOverwriteAdded.php @@ -0,0 +1,43 @@ + ['V028', 'V028', 'V028'] + ]; + + /** + * @var string + */ + protected $reason = 'Method overwrite has been added.'; + + /** + * @var array + */ + private $mapping = [ + 'V028' => Level::PATCH, + ]; + + /** + * Returns level of error. + * + * @return mixed + */ + public function getLevel() + { + return $this->mapping[$this->getCode()]; + } +} \ No newline at end of file diff --git a/src/Operation/ClassMethodOverwriteRemoved.php b/src/Operation/ClassMethodOverwriteRemoved.php new file mode 100644 index 00000000..4a315399 --- /dev/null +++ b/src/Operation/ClassMethodOverwriteRemoved.php @@ -0,0 +1,43 @@ + ['V029', 'V029', 'V029'] + ]; + + /** + * @var string + */ + protected $reason = 'Method overwrite has been removed.'; + + /** + * @var array + */ + private $mapping = [ + 'V029' => Level::PATCH + ]; + + /** + * Returns level of error. + * + * @return mixed + */ + public function getLevel() + { + return $this->mapping[$this->getCode()]; + } +} \ No newline at end of file diff --git a/src/ReportBuilder.php b/src/ReportBuilder.php index b007bc32..01fe7b2c 100644 --- a/src/ReportBuilder.php +++ b/src/ReportBuilder.php @@ -109,10 +109,10 @@ protected function makeVersionReport() ); // try { - $report = $this->buildReport(); + $report = $this->buildReport(); // } finally { - // Restore original severity levels - LevelMapping::setOverrides($originalMapping); + // Restore original severity levels + LevelMapping::setOverrides($originalMapping); // } return $report; @@ -163,7 +163,7 @@ protected function buildReport() */ foreach ($this->analyzerList as $reportType => $factory) { /** @var AnalyzerInterface $analyzer */ - $analyzer = (new $factory())->create(); + $analyzer = (new $factory())->create($dependencyMap); $tmpReport = $analyzer->analyze( $beforeRegistryList[$reportType], $afterRegistryList[$reportType] @@ -204,4 +204,4 @@ protected function getFilters($sourceBeforeDir, $sourceAfterDir): array return $filters; } -} +} \ No newline at end of file diff --git a/src/Reporter/HtmlDbSchemaReporter.php b/src/Reporter/HtmlDbSchemaReporter.php index a235e2fc..6927b0cb 100644 --- a/src/Reporter/HtmlDbSchemaReporter.php +++ b/src/Reporter/HtmlDbSchemaReporter.php @@ -54,6 +54,10 @@ public function output(OutputInterface $output) 'interface', 'trait', 'database', + 'di', + 'system', + 'xsd', + 'less', ]; foreach ($contexts as $context) { diff --git a/tests/Unit/Console/Command/CompareSourceCommandApiClassesTest.php b/tests/Unit/Console/Command/CompareSourceCommandApiClassesTest.php index fbf3db60..f566aa87 100644 --- a/tests/Unit/Console/Command/CompareSourceCommandApiClassesTest.php +++ b/tests/Unit/Console/Command/CompareSourceCommandApiClassesTest.php @@ -3,9 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Magento\SemanticVersionChecker\Test\Unit\Console\Command; +namespace Magento\Tools\SemanticVersionChecker\Test\Unit\Console\Command; -use Magento\SemanticVersionChecker\Test\Unit\Console\Command\CompareSourceCommandTest\AbstractTestCase; +use Magento\Tools\SemanticVersionChecker\Test\Unit\Console\Command\CompareSourceCommandTest\AbstractTestCase; /** * Test semantic version checker CLI command dealing with API classes. @@ -249,11 +249,15 @@ public function changesDataProvider() 'Test\Vcs\TestClass::annotationChangedPrivate | [private] Method return typing changed. | M122 ', 'Test\Vcs\TestClass::declarationRemovedPrivate | [private] Method return typing changed. | M122 ', 'Test\Vcs\TestClass::annotationRemovedPrivate | [private] Method return typing changed. | M122 ', + 'Test\Vcs\TestClass::toBeChangedClassReturnTypeInlineDeclaration | [public] Method return typing changed. | M120', + 'Test\Vcs\TestClass::nullableToBeChangedClassReturnTypeInlineDeclaration | [public] Method return typing changed. | M120' ], 'Major change is detected.', [ 'Test\Vcs\TestClass::declarationFcqnNotChangedPublic | [public] Method return typing changed. | M120 ', 'Test\Vcs\TestClass::declarationSelfNotChangedProtected | [protected] Method return typing changed. | M121 ', + 'Test\Vcs\TestClass::classReturnTypeInlineDeclaration | [public] Method return typing changed. | M120 ', + 'Test\Vcs\TestClass::nullableClassReturnTypeInlineDeclaration | [public] Method return typing changed. | M120 ' ] ], 'api-class-new-method-parameter-type' => [ @@ -445,7 +449,16 @@ public function changesDataProvider() 'Test\Vcs\TestClass::movedNativeTypePrivate | [private] Method variable typehint was moved from in-line to doc block annotation. | M174', ], 'Major change is detected.' + ], + 'api-class-added-method-subclass-overwrite' => [ + $pathToFixtures . '/added-method-subclass-overwrite/source-code-before', + $pathToFixtures . '/added-method-subclass-overwrite/source-code-after', + [ + 'Class (PATCH)', + 'Test\Vcs\ApiClass::testFunction | [public] Method overwrite has been added. | V028' + ], + 'Patch change is detected.' ] ]; } -} +} \ No newline at end of file diff --git a/tests/Unit/Console/Command/CompareSourceCommandTest/_files/api-class/changed-method-return-type/source-code-after/TestClass.php b/tests/Unit/Console/Command/CompareSourceCommandTest/_files/api-class/changed-method-return-type/source-code-after/TestClass.php index 74182e3a..9b412db3 100644 --- a/tests/Unit/Console/Command/CompareSourceCommandTest/_files/api-class/changed-method-return-type/source-code-after/TestClass.php +++ b/tests/Unit/Console/Command/CompareSourceCommandTest/_files/api-class/changed-method-return-type/source-code-after/TestClass.php @@ -4,179 +4,206 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Test\Vcs; -/** - * @api - */ -class TestClass -{ - /** - * @param int $int - */ - public function declarationAddedPublic( $int): ?int - { - return $int; - } - - /** - * @param int $int - * @return void - */ - public function declarationFcqnNotChangedPublic($int): ?TestClass - { - } - - /** - * @param int $int - * @return void - */ - protected function declarationSelfNotChangedProtected($int): self - { - } - - /** - * @param int $int - * @return int|null - */ - public function annotationAddedPublic( $int) - { - return $int; - } - - /** - * @param int $int - */ - protected function declarationAddedProtected( $int): ?int - { - return $int; - } - - /** - * @param int $int - * @return int|null - */ - protected function annotationAddedProtected( $int) - { - return $int; - } - - /** - * @param int $int - */ - private function declarationAddedPrivate( $int): ?int - { - return $int; - } - - /** - * @param int $int - * @return int|null - */ - private function annotationAddedPrivate( $int) - { - return $int; - } - - /** - * @param int $int - */ - public function declarationChangedPublic( $int): ?int - { - return $int; - } - - /** - * @param int $int - * @return int|null - */ - public function annotationChangedPublic( $int) - { - return $int; - } - /** - * @param int $int - */ - protected function declarationChangedProtected( $int): ?int - { - return $int; - } - - /** - * @param int $int - * @return int|null - */ - protected function annotationChangedProtected( $int) - { - return $int; - } - /** - * @param int $int - */ - private function declarationChangedPrivate( $int): ?int - { - return $int; - } - - /** - * @param int $int - * @return int|null - */ - private function annotationChangedPrivate( $int) - { - return $int; - } - - /** - * @param int $int - */ - public function declarationRemovedPublic( $int) - { - return $int; - } - - /** - * @param int $int - */ - public function annotationRemovedPublic( $int) - { - return $int; - } - - /** - * @param int $int - */ - protected function declarationRemovedProtected( $int) - { - return $int; - } - - /** - * @param int $int - */ - protected function annotationRemovedProtected( $int) - { - return $int; +namespace Test\Vcs { + + use Foo\Bar\Test; + use Foo\Buzz\Baz; + + /** + * @api + */ + class TestClass + { + /** + * @param int $int + */ + public function declarationAddedPublic($int): ?int + { + return $int; + } + + /** + * @param int $int + * @return int|null + */ + public function annotationAddedPublic($int) + { + return $int; + } + + /** + * @param int $int + */ + protected function declarationAddedProtected($int): ?int + { + return $int; + } + + /** + * @param int $int + * @return int|null + */ + protected function annotationAddedProtected($int) + { + return $int; + } + + /** + * @param int $int + */ + private function declarationAddedPrivate($int): ?int + { + return $int; + } + + /** + * @param int $int + * @return int|null + */ + private function annotationAddedPrivate($int) + { + return $int; + } + + /** + * @param int $int + */ + public function declarationChangedPublic($int): ?int + { + return $int; + } + + /** + * @param int $int + * @return int|null + */ + public function annotationChangedPublic($int) + { + return $int; + } + + /** + * @param int $int + */ + protected function declarationChangedProtected($int): ?int + { + return $int; + } + + /** + * @param int $int + * @return int|null + */ + protected function annotationChangedProtected($int) + { + return $int; + } + + /** + * @param int $int + */ + private function declarationChangedPrivate($int): ?int + { + return $int; + } + + /** + * @param int $int + * @return int|null + */ + private function annotationChangedPrivate($int) + { + return $int; + } + + /** + * @param int $int + */ + public function declarationRemovedPublic($int) + { + return $int; + } + + /** + * @param int $int + */ + public function annotationRemovedPublic($int) + { + return $int; + } + + /** + * @param int $int + */ + protected function declarationRemovedProtected($int) + { + return $int; + } + + /** + * @param int $int + */ + protected function annotationRemovedProtected($int) + { + return $int; + } + + /** + * @param int $int + */ + private function declarationRemovedPrivate($int) + { + return $int; + } + + /** + * @param int $int + */ + private function annotationRemovedPrivate($int) + { + return $int; + } + + public function php7RemoveAnnotationWithoutDoc(int $int1, int $int2) + { + return $int1 + $int2; + } + + public function classReturnTypeInlineDeclaration(): Test + { + return new Test(); + } + + public function nullableClassReturnTypeInlineDeclaration(): ?Test + { + return null; + } + + public function toBeChangedClassReturnTypeInlineDeclaration(): Baz + { + return new \Foo\Buzz\Baz(); + } + + public function nullableToBeChangedClassReturnTypeInlineDeclaration(): ?Baz + { + return null; + } } +} - /** - * @param int $int - */ - private function declarationRemovedPrivate( $int) +namespace Foo\Bar { + class Test { - return $int; } - /** - * @param int $int - */ - private function annotationRemovedPrivate( $int) + class Baz { - return $int; } +} - public function php7RemoveAnnotationWithoutDoc(int $int1, int $int2) +namespace Foo\Buzz { + class Baz { - return $int1 + $int2; } -} +} \ No newline at end of file diff --git a/tests/Unit/Console/Command/CompareSourceCommandTest/_files/api-class/changed-method-return-type/source-code-before/TestClass.php b/tests/Unit/Console/Command/CompareSourceCommandTest/_files/api-class/changed-method-return-type/source-code-before/TestClass.php index bf9e0461..f508fdac 100644 --- a/tests/Unit/Console/Command/CompareSourceCommandTest/_files/api-class/changed-method-return-type/source-code-before/TestClass.php +++ b/tests/Unit/Console/Command/CompareSourceCommandTest/_files/api-class/changed-method-return-type/source-code-before/TestClass.php @@ -4,172 +4,196 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Test\Vcs; -/** - * @api - */ -class TestClass -{ - /** - * @param array $int - */ - public function declarationAddedPublic( $int) - { - return $int; - } - - /** - * @param int $int - * @return void - */ - public function declarationFcqnNotChangedPublic($int): ?TestClass - { - } - - /** - * @param int $int - * @return void - */ - protected function declarationSelfNotChangedProtected($int): self - { - } - - public function annotationAddedPublic( $int) - { - return $int; - } - - /** - * @param array $int - */ - protected function declarationAddedProtected( $int) - { - return $int; - } - - protected function annotationAddedProtected( $int) - { - return $int; - } - - /** - * @param array $int - */ - private function declarationAddedPrivate($int) - { - return $int; - } - - private function annotationAddedPrivate($int) - { - return $int; - } - - /** - * @param array $int - */ - public function declarationChangedPublic( $int): array - { - return $int; - } - - /** - * @param array $int - * @return array - */ - public function annotationChangedPublic( $int) - { - return $int; - } - - /** - * @param array $int - */ - protected function declarationChangedProtected( $int): array - { - return $int; - } - - /** - * @param array $int - * @return array - */ - protected function annotationChangedProtected( $int) - { - return $int; - } - - /** - * @param array $int - */ - private function declarationChangedPrivate( $int): array - { - return $int; - } - - /** - * @param array $int - * @return array - */ - private function annotationChangedPrivate( $int) - { - return $int; - } - - /** - * @param array $int - */ - public function declarationRemovedPublic( $int): array - { - return $int; - } - - /** - * @param array $int - * @return array - */ - public function annotationRemovedPublic( $int) - { - return $int; - } - - /** - * @param array $int - */ - protected function declarationRemovedProtected( $int): array - { - return $int; - } - - /** - * @param array $int - * @return array - */ - protected function annotationRemovedProtected( $int) - { - return $int; +namespace Test\Vcs { + + use Foo\Bar\Baz; + + /** + * @api + */ + class TestClass + { + /** + * @param array $int + */ + public function declarationAddedPublic($int) + { + return $int; + } + + public function annotationAddedPublic($int) + { + return $int; + } + + /** + * @param array $int + */ + protected function declarationAddedProtected($int) + { + return $int; + } + + protected function annotationAddedProtected($int) + { + return $int; + } + + /** + * @param array $int + */ + private function declarationAddedPrivate($int) + { + return $int; + } + + private function annotationAddedPrivate($int) + { + return $int; + } + + /** + * @param array $int + */ + public function declarationChangedPublic($int): array + { + return $int; + } + + /** + * @param array $int + * @return array + */ + public function annotationChangedPublic($int) + { + return $int; + } + + /** + * @param array $int + */ + protected function declarationChangedProtected($int): array + { + return $int; + } + + /** + * @param array $int + * @return array + */ + protected function annotationChangedProtected($int) + { + return $int; + } + + /** + * @param array $int + */ + private function declarationChangedPrivate($int): array + { + return $int; + } + + /** + * @param array $int + * @return array + */ + private function annotationChangedPrivate($int) + { + return $int; + } + + /** + * @param array $int + */ + public function declarationRemovedPublic($int): array + { + return $int; + } + + /** + * @param array $int + * @return array + */ + public function annotationRemovedPublic($int) + { + return $int; + } + + /** + * @param array $int + */ + protected function declarationRemovedProtected($int): array + { + return $int; + } + + /** + * @param array $int + * @return array + */ + protected function annotationRemovedProtected($int) + { + return $int; + } + + /** + * @param array $int + */ + private function declarationRemovedPrivate($int): array + { + return $int; + } + + /** + * @param array $int + * @return array + */ + private function annotationRemovedPrivate($int) + { + return $int; + } + + public function php7RemoveAnnotationWithoutDoc(int $int1, int $int2): int + { + return $int1 + $int2; + } + + public function classReturnTypeInlineDeclaration(): \Foo\Bar\Test + { + return new \Foo\Bar\Test(); + } + + public function nullableClassReturnTypeInlineDeclaration(): ?\Foo\Bar\Test + { + return null; + } + + public function toBeChangedClassReturnTypeInlineDeclaration(): Baz + { + return new \Foo\Bar\Baz(); + } + + public function nullableToBeChangedClassReturnTypeInlineDeclaration(): ?Baz + { + return null; + } } +} - /** - * @param array $int - */ - private function declarationRemovedPrivate( $int): array +namespace Foo\Bar { + class Test { - return $int; } - /** - * @param array $int - * @return array - */ - private function annotationRemovedPrivate( $int) + class Baz { - return $int; } +} - public function php7RemoveAnnotationWithoutDoc(int $int1, int $int2) : int +namespace Foo\Buzz { + class Baz { - return $int1 + $int2; } -} +} \ No newline at end of file