diff --git a/composer.json b/composer.json index 6b913a83..c622dfc4 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "magento/magento-semver", "description": "Magento semantic version checker", - "version": "2.0.0", + "version": "3.0.0", "license": [ "OSL-3.0", "AFL-3.0" diff --git a/src/Analyzer/AbstractCodeAnalyzer.php b/src/Analyzer/AbstractCodeAnalyzer.php index 3c623c60..1bf4d16a 100644 --- a/src/Analyzer/AbstractCodeAnalyzer.php +++ b/src/Analyzer/AbstractCodeAnalyzer.php @@ -237,4 +237,17 @@ protected function getFileName($context, $nodeName, $isBefore = true) { return $isBefore ? $this->fileBefore : $this->fileAfter; } + + /** + * Gets the AbstractCodeAnalyzers for content analysis (ex: method analyzer for classes) + * + * @param string $context + * @param string $fileBefore + * @param string $fileAfter + * @return AbstractCodeAnalyzer[] + */ + protected function getContentAnalyzers($context, $fileBefore, $fileAfter) + { + return []; + } } diff --git a/src/Analyzer/ClassAnalyzer.php b/src/Analyzer/ClassAnalyzer.php index 1d9056a1..51b4b8a2 100644 --- a/src/Analyzer/ClassAnalyzer.php +++ b/src/Analyzer/ClassAnalyzer.php @@ -108,18 +108,7 @@ protected function reportChanged($report, $registryBefore, $registryAfter, $toVe $classAfter = $afterNameMap[$key]; if ($classBefore !== $classAfter) { - /** - * @var AnalyzerInterface[] $analyzers -*/ - $analyzers = [ - 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), - new ClassImplementsAnalyzer(static::CONTEXT, $fileBefore, $fileAfter), - new ClassExtendsAnalyzer(static::CONTEXT, $fileBefore, $fileAfter), - new ClassTraitAnalyzer(static::CONTEXT, $fileBefore, $fileAfter), - ]; + $analyzers = $this->getContentAnalyzers(static::CONTEXT, $fileBefore, $fileAfter); foreach ($analyzers as $analyzer) { $internalReport = $analyzer->analyze($classBefore, $classAfter); @@ -129,6 +118,27 @@ protected function reportChanged($report, $registryBefore, $registryAfter, $toVe } } + /** + * Get the list of content analyzers + * + * @param string $context + * @param string $fileBefore + * @param string $fileAfter + * @return AbstractCodeAnalyzer[] + */ + protected function getContentAnalyzers($context, $fileBefore, $fileAfter) + { + return [ + new ClassMethodAnalyzer($context, $fileBefore, $fileAfter, $this->dependencyGraph), + new PropertyAnalyzer($context, $fileBefore, $fileAfter), + new ClassConstantAnalyzer($context, $fileBefore, $fileAfter), + new ClassMethodExceptionAnalyzer($context, $fileBefore, $fileAfter), + new ClassImplementsAnalyzer($context, $fileBefore, $fileAfter), + new ClassExtendsAnalyzer($context, $fileBefore, $fileAfter), + new ClassTraitAnalyzer($context, $fileBefore, $fileAfter), + ]; + } + /** * Get the class node registry * diff --git a/src/Analyzer/InterfaceAnalyzer.php b/src/Analyzer/InterfaceAnalyzer.php index d03c4e36..165ef39f 100644 --- a/src/Analyzer/InterfaceAnalyzer.php +++ b/src/Analyzer/InterfaceAnalyzer.php @@ -118,13 +118,7 @@ protected function reportChanged($report, $registryBefore, $registryAfter, $toVe $interfaceAfter = $afterNameMap[$key]; if ($interfaceBefore !== $interfaceAfter) { - /** @var AnalyzerInterface[] $analyzers */ - $analyzers = [ - new ClassMethodAnalyzer(static::CONTEXT, $fileBefore, $fileAfter), - new ClassConstantAnalyzer(static::CONTEXT, $fileBefore, $fileAfter), - new ClassMethodExceptionAnalyzer(static::CONTEXT, $fileBefore, $fileAfter), - new InterfaceExtendsAnalyzer(static::CONTEXT, $fileBefore, $fileAfter) - ]; + $analyzers = $this->getContentAnalyzers(static::CONTEXT, $fileBefore, $fileAfter); foreach ($analyzers as $analyzer) { $internalReport = $analyzer->analyze($interfaceBefore, $interfaceAfter); @@ -133,4 +127,22 @@ protected function reportChanged($report, $registryBefore, $registryAfter, $toVe } } } + + /** + * Get the list of content analyzers + * + * @param string $context + * @param string $fileBefore + * @param string $fileAfter + * @return AbstractCodeAnalyzer[] + */ + protected function getContentAnalyzers($context, $fileBefore, $fileAfter) + { + return [ + new ClassMethodAnalyzer($context, $fileBefore, $fileAfter), + new ClassConstantAnalyzer($context, $fileBefore, $fileAfter), + new ClassMethodExceptionAnalyzer($context, $fileBefore, $fileAfter), + new InterfaceExtendsAnalyzer($context, $fileBefore, $fileAfter) + ]; + } } diff --git a/src/Analyzer/TraitAnalyzer.php b/src/Analyzer/TraitAnalyzer.php index 1f127274..084996a7 100644 --- a/src/Analyzer/TraitAnalyzer.php +++ b/src/Analyzer/TraitAnalyzer.php @@ -115,11 +115,7 @@ protected function reportChanged($report, $registryBefore, $registryAfter, $toVe if ($traitBefore !== $traitAfter) { $fileBefore = $this->getFileName($registryBefore, $traitName); $fileAfter = $this->getFileName($registryAfter, $traitName); - - /** @var AbstractCodeAnalyzer[] $analyzers */ - $analyzers = [ - new ClassMethodAnalyzer(static::CONTEXT, $fileBefore, $fileAfter), - ]; + $analyzers = $this->getContentAnalyzers(static::CONTEXT, $fileBefore, $fileAfter); foreach ($analyzers as $analyzer) { $internalReport = $analyzer->analyze($traitBefore, $traitAfter); @@ -128,4 +124,17 @@ protected function reportChanged($report, $registryBefore, $registryAfter, $toVe } } } + + /** + * Get the list of content analyzers + * + * @param string $context + * @param string $fileBefore + * @param string $fileAfter + * @return AbstractCodeAnalyzer[] + */ + protected function getContentAnalyzers($context, $fileBefore, $fileAfter) + { + return [new ClassMethodAnalyzer($context, $fileBefore, $fileAfter)]; + } } diff --git a/src/MergedReport.php b/src/MergedReport.php new file mode 100644 index 00000000..da9eab90 --- /dev/null +++ b/src/MergedReport.php @@ -0,0 +1,37 @@ +differences as $context => $levels) { + if (!key_exists($context, $this->differences)) { + $this->differences[$context] = $levels; + } else { + foreach ($levels as $level => $differences) { + $this->differences[$context][$level] = array_merge( + $this->differences[$context][$level], + $differences + ); + } + } + } + + return $this; + } +} diff --git a/src/ReportBuilder.php b/src/ReportBuilder.php index fc4c4820..3e7f93d9 100644 --- a/src/ReportBuilder.php +++ b/src/ReportBuilder.php @@ -44,9 +44,9 @@ class ReportBuilder /** * Define analyzer factory list for the different report types. - * @var array + * @var string[] */ - private $analyzerList = [ + private $analyzerFactoryClasses = [ ReportTypes::API => AnalyzerFactory::class, ReportTypes::ALL => NonApiAnalyzerFactory::class, ReportTypes::DB_SCHEMA => DbSchemaAnalyzerFactory::class, @@ -118,6 +118,16 @@ protected function makeVersionReport() return $report; } + /** + * Get the mapping of report type -> analyzer factory + * + * @return string[] + */ + protected function getAnalyzerFactoryClasses() + { + return $this->analyzerFactoryClasses; + } + /** * Create a report based on type * @@ -161,7 +171,7 @@ protected function buildReport() /** * @var AnalyzerFactoryInterface $factory */ - foreach ($this->analyzerList as $reportType => $factory) { + foreach ($this->getAnalyzerFactoryClasses() as $reportType => $factory) { /** @var AnalyzerInterface $analyzer */ $analyzer = (new $factory())->create($dependencyMap); $tmpReport = $analyzer->analyze( diff --git a/src/Reporter/BreakingChangeTableReporter.php b/src/Reporter/BreakingChangeTableReporter.php index 12160eb2..3ae55e7f 100644 --- a/src/Reporter/BreakingChangeTableReporter.php +++ b/src/Reporter/BreakingChangeTableReporter.php @@ -51,20 +51,15 @@ public function __construct($apiChangeReport, $apiMembershipReport, $targetFile) */ public function output(OutputInterface $output) { - $contexts = [ - 'class', - 'function', - 'interface', - 'trait', - 'database', - ]; + $reportContexts = array_keys($this->report->getDifferences()); + $membershipContexts = array_keys($this->membershipReport->getDifferences()); - foreach ($contexts as $context) { + foreach ($reportContexts as $context) { $header = static::formatSectionHeader($this->targetFile, $context, 'breaking-change'); $this->outputChangeReport($output, $this->report, $context, $header); } - foreach ($contexts as $context) { + foreach ($membershipContexts as $context) { $header = static::formatSectionHeader($this->targetFile, $context, 'api-membership'); $this->outputChangeReport($output, $this->membershipReport, $context, $header); } diff --git a/tests/Unit/MergedReportTest.php b/tests/Unit/MergedReportTest.php new file mode 100644 index 00000000..d2146445 --- /dev/null +++ b/tests/Unit/MergedReportTest.php @@ -0,0 +1,96 @@ +createMock(ClassConstructorOptionalParameterAdded::class)]; + $diffsWithDatabase['class'][Level::MINOR] = [ + $this->createMock(ClassExtendsAdded::class), + $this->createMock(ClassMethodOptionalParameterAdded::class) + ]; + $diffsWithDatabase['database'][Level::MAJOR] = [$this->createMock(DropForeignKey::class)]; + $databaseReport = new InjectableReport($diffsWithDatabase); + + $diffsWithXml = []; + $diffsWithXml['class'][Level::MAJOR] = [$this->createMock(ClassConstantRemoved::class)]; + $diffsWithXml['class'][Level::MINOR] = [$this->createMock(ClassImplementsAdded::class)]; + $diffsWithXml['xml'][Level::MINOR] = [$this->createMock(FieldAdded::class)]; + $xmlReport = new InjectableReport($diffsWithXml); + + /** @var MockObject|ClassTraitAdded $preMergeOperation */ + $preMergeOperation = $this->createMock(ClassTraitAdded::class); + $preMergeOperation->expects($this->any())->method('getLevel')->willReturn(Level::MINOR); + + $mergedReport = new MergedReport(); + $mergedReport->addClass($preMergeOperation); + $mergedReport->merge($databaseReport); + $mergedReport->merge($xmlReport); + + $mergedDiffs = $mergedReport->getDifferences(); + $this->assertTrue(key_exists('database', $mergedDiffs)); + $this->assertTrue(key_exists('xml', $mergedDiffs)); + $this->assertTrue($this->hasAllDifferences($mergedReport, $databaseReport)); + $this->assertTrue($this->hasAllDifferences($mergedReport, $xmlReport)); + $this->assertTrue(array_search($preMergeOperation, $mergedDiffs['class'][Level::MINOR]) !== false); + } + + /** + * Checks if a merged report contains all differences in another report + + * @param Report $mergedReport + * @param Report $inputReport + * @return bool + */ + private function hasAllDifferences($mergedReport, $inputReport) + { + $mergedDiffs = $mergedReport->getDifferences(); + $inputDiffs = $inputReport->getDifferences(); + foreach ($inputDiffs as $context => $levels) { + if (!key_exists($context, $mergedDiffs)) { + return false; + } + $mergedLevels = $mergedDiffs[$context]; + foreach ($levels as $level => $operations) { + if (!key_exists($level, $mergedLevels)) { + return false; + } + $mergedOperations = $mergedLevels[$level]; + foreach ($operations as $operation) { + if (array_search($operation, $mergedOperations) === false) { + return false; + } + } + } + } + return true; + } +}