Skip to content

Commit 6358cc8

Browse files
authored
Merge pull request #58 from magento-gl/Hammer-PlatformHealth-30sep24
Hammer platform health 30sep24
2 parents 753f3c3 + 86777d7 commit 6358cc8

File tree

8 files changed

+417
-168
lines changed

8 files changed

+417
-168
lines changed

Diff for: .github/workflows/php.yml

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ jobs:
1717
- "8.1"
1818
- "8.2"
1919
- "8.3"
20+
- "8.4"
2021
dependencies:
2122
- "lowest"
2223
- "highest"

Diff for: composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
],
99
"bin": ["bin/svc"],
1010
"require": {
11-
"php": "~8.1.0||~8.2.0||~8.3.0",
11+
"php": "~8.1.0||~8.2.0||~8.3.0||~8.4.0",
1212
"ext-json": "*",
1313
"laminas/laminas-stdlib": "^3.18",
1414
"nikic/php-parser": "^4.15",

Diff for: composer.lock

+163-160
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: dev/tests/Unit/Console/Command/CompareSourceCommandApiClassesTest.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,10 @@ public static function changesDataProvider()
5959
$pathToFixtures . '/new-method/source-code-before',
6060
$pathToFixtures . '/new-method/source-code-after',
6161
[
62-
'Class (PATCH)',
62+
'Class (MINOR)',
6363
'Test\Vcs\TestClass::testMethod | [public] Method has been added. | V015'
6464
],
65-
'Patch change is detected.'
65+
'Minor change is detected.'
6666
],
6767
'api-class-removed-class' => [
6868
$pathToFixtures . '/removed-class/source-code-before',

Diff for: dev/tests/Unit/bootstrap.php

-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ function ($errNo, $errStr, $errFile, $errLine) {
3737
E_USER_ERROR => 'User Error',
3838
E_USER_WARNING => 'User Warning',
3939
E_USER_NOTICE => 'User Notice',
40-
E_STRICT => 'Strict',
4140
E_RECOVERABLE_ERROR => 'Recoverable Error',
4241
E_DEPRECATED => 'Deprecated',
4342
E_USER_DEPRECATED => 'User Deprecated',

Diff for: src/Analyzer/SystemXml/Analyzer.php

+215-3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
use Magento\SemanticVersionChecker\Registry\XmlRegistry;
2626
use PHPSemVerChecker\Registry\Registry;
2727
use PHPSemVerChecker\Report\Report;
28+
use Magento\SemanticVersionChecker\Operation\SystemXml\DuplicateFieldAdded;
29+
use RecursiveDirectoryIterator;
2830

2931
/**
3032
* Analyzes <kbd>system.xml</kbd> files:
@@ -92,14 +94,152 @@ public function analyze($registryBefore, $registryAfter)
9294
$beforeFile = $registryBefore->mapping[XmlRegistry::NODES_KEY][$moduleName];
9395
$this->reportRemovedNodes($beforeFile, $removedNodes);
9496
}
97+
9598
if ($addedNodes) {
9699
$afterFile = $registryAfter->mapping[XmlRegistry::NODES_KEY][$moduleName];
97-
$this->reportAddedNodes($afterFile, $addedNodes);
100+
if (strpos($afterFile, '_files') !== false) {
101+
$this->reportAddedNodes($afterFile, $addedNodes);
102+
} else {
103+
$baseDir = $this->getBaseDir($afterFile);
104+
foreach ($addedNodes as $nodeId => $node) {
105+
$newNodeData = $this->getNodeData($node);
106+
$nodePath = $newNodeData['path'];
107+
108+
// Extract section, group, and fieldId with error handling
109+
$extractedData = $this->extractSectionGroupField($nodePath);
110+
if ($extractedData === null) {
111+
// Skip the node if its path is invalid
112+
continue;
113+
}
114+
115+
// Extract section, group, and fieldId
116+
list($sectionId, $groupId, $fieldId) = $extractedData;
117+
118+
// Call function to check if this field is duplicated in other system.xml files
119+
$isDuplicated = $this->isDuplicatedFieldInXml(
120+
$baseDir,
121+
$sectionId,
122+
$groupId,
123+
$fieldId,
124+
$afterFile
125+
);
126+
127+
foreach ($isDuplicated as $isDuplicatedItem) {
128+
if ($isDuplicatedItem['status'] === 'duplicate') {
129+
$this->reportDuplicateNodes($afterFile, [$nodeId => $node]);
130+
} else {
131+
$this->reportAddedNodes($afterFile, [$nodeId => $node]);
132+
}
133+
}
134+
}
135+
}
98136
}
99137
}
100138
return $this->report;
101139
}
102140

141+
/**
142+
* Get Magento Base directory from the path
143+
*
144+
* @param string $filePath
145+
* @return string|null
146+
*/
147+
private function getBaseDir(string $filePath): ?string
148+
{
149+
$currentDir = dirname($filePath);
150+
while ($currentDir !== '/' && $currentDir !== false) {
151+
// Check if current directory contains files unique to Magento root
152+
if (file_exists($currentDir . '/SECURITY.md')) {
153+
return $currentDir; // Found the Magento base directory
154+
}
155+
$currentDir = dirname($currentDir);
156+
}
157+
return null;
158+
}
159+
160+
/**
161+
* Search for system.xml files in both app/code and vendor directories, excluding the provided file.
162+
*
163+
* @param string $magentoBaseDir The base directory of Magento.
164+
* @param string|null $excludeFile The file to exclude from the search.
165+
* @return array An array of paths to system.xml files, excluding the specified file.
166+
*/
167+
private function getSystemXmlFiles(string $magentoBaseDir, ?string $excludeFile = null): array
168+
{
169+
$systemXmlFiles = [];
170+
$directoryToSearch = [
171+
$magentoBaseDir . '/app/code'
172+
];
173+
174+
// Check if 'vendor' directory exists, and only add it if it does
175+
if (is_dir($magentoBaseDir . '/vendor')) {
176+
$directoriesToSearch[] = $magentoBaseDir . '/vendor';
177+
}
178+
foreach ($directoryToSearch as $directory) {
179+
$iterator = new \RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory));
180+
foreach ($iterator as $file) {
181+
if ($file->getfileName() === 'system.xml') {
182+
$filePath = $file->getRealPath();
183+
if ($filePath !== $excludeFile) {
184+
$systemXmlFiles[] = $file->getRealPath();
185+
}
186+
}
187+
}
188+
}
189+
return $systemXmlFiles;
190+
}
191+
192+
/**
193+
* Method to extract section, group and field from the Node
194+
*
195+
* @param string $nodePath
196+
* @return array|null
197+
*/
198+
private function extractSectionGroupField(string $nodePath): ?array
199+
{
200+
$parts = explode('/', $nodePath);
201+
202+
if (count($parts) < 3) {
203+
// Invalid path if there are fewer than 3 parts
204+
return null;
205+
}
206+
207+
$sectionId = $parts[0];
208+
$groupId = $parts[1];
209+
$fieldId = $parts[2];
210+
211+
return [$sectionId, $groupId, $fieldId];
212+
}
213+
214+
/**
215+
* Method to get Node Data using reflection class
216+
*
217+
* @param object|string $node
218+
* @return array
219+
* @throws \ReflectionException
220+
*/
221+
private function getNodeData(object|string $node): array
222+
{
223+
$data = [];
224+
225+
// Use reflection to get accessible properties
226+
$reflection = new \ReflectionClass($node);
227+
foreach ($reflection->getMethods() as $method) {
228+
// Skip 'getId' and 'getParent' methods for comparison
229+
if ($method->getName() === 'getId' || $method->getName() === 'getParent') {
230+
continue;
231+
}
232+
233+
// Dynamically call the getter methods
234+
if (strpos($method->getName(), 'get') === 0) {
235+
$propertyName = lcfirst(str_replace('get', '', $method->getName()));
236+
$data[$propertyName] = $method->invoke($node);
237+
}
238+
}
239+
240+
return $data;
241+
}
242+
103243
/**
104244
* Extracts the node from <var>$registry</var> as an associative array.
105245
*
@@ -164,13 +304,32 @@ private function reportAddedNodes(string $file, array $nodes)
164304
}
165305
}
166306

307+
/**
308+
* Creates reports for <var>$nodes</var> considering that they have been duplicated.
309+
*
310+
* @param string $file
311+
* @param NodeInterface[] $nodes
312+
* @return void
313+
*/
314+
private function reportDuplicateNodes(string $file, array $nodes): void
315+
{
316+
foreach ($nodes as $node) {
317+
switch (true) {
318+
case $node instanceof Field:
319+
$this->report->add('system', new DuplicateFieldAdded($file, $node->getPath()));
320+
break;
321+
}
322+
}
323+
}
324+
167325
/**
168326
* Creates reports for <var>$modules</var> considering that <kbd>system.xml</kbd> has been removed from them.
169327
*
170328
* @param array $modules
171329
* @param XmlRegistry $registryBefore
330+
* @return void
172331
*/
173-
private function reportRemovedFiles(array $modules, XmlRegistry $registryBefore)
332+
private function reportRemovedFiles(array $modules, XmlRegistry $registryBefore): void
174333
{
175334
foreach ($modules as $module) {
176335
$beforeFile = $registryBefore->mapping[XmlRegistry::NODES_KEY][$module];
@@ -183,8 +342,9 @@ private function reportRemovedFiles(array $modules, XmlRegistry $registryBefore)
183342
*
184343
* @param string $file
185344
* @param NodeInterface[] $nodes
345+
* @return void
186346
*/
187-
private function reportRemovedNodes(string $file, array $nodes)
347+
private function reportRemovedNodes(string $file, array $nodes): void
188348
{
189349
foreach ($nodes as $node) {
190350
switch (true) {
@@ -202,4 +362,56 @@ private function reportRemovedNodes(string $file, array $nodes)
202362
}
203363
}
204364
}
365+
366+
/**
367+
* @param string|null $baseDir
368+
* @param string $sectionId
369+
* @param string $groupId
370+
* @param string|null $fieldId
371+
* @param string $afterFile
372+
* @return array
373+
*/
374+
private function isDuplicatedFieldInXml(
375+
?string $baseDir,
376+
string $sectionId,
377+
string $groupId,
378+
?string $fieldId,
379+
string $afterFile
380+
): array {
381+
$hasDuplicate = false;
382+
383+
$result = [
384+
'status' => 'minor',
385+
'field' => $fieldId
386+
];
387+
388+
if ($baseDir) {
389+
$systemXmlFiles = $this->getSystemXmlFiles($baseDir, $afterFile);
390+
391+
foreach ($systemXmlFiles as $systemXmlFile) {
392+
$xmlContent = file_get_contents($systemXmlFile);
393+
try {
394+
$xml = new \SimpleXMLElement($xmlContent);
395+
} catch (\Exception $e) {
396+
continue; // Skip this file if there's a parsing error
397+
}
398+
// Find <field> nodes with the given field ID
399+
// XPath to search for <field> within a specific section and group
400+
$fields = $xml->xpath("//section[@id='$sectionId']/group[@id='$groupId']/field[@id='$fieldId']");
401+
if (!empty($fields)) {
402+
$hasDuplicate = true; // Set the duplicate flag to true if a match is found
403+
break; // Since we found a duplicate, we don't need to check further for this field
404+
}
405+
}
406+
if ($hasDuplicate) {
407+
return [
408+
[
409+
'status' => 'duplicate',
410+
'field' => $fieldId
411+
]
412+
];
413+
}
414+
}
415+
return [$result];
416+
}
205417
}

Diff for: src/Operation/SystemXml/DuplicateFieldAdded.php

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/**
4+
* Copyright © Magento, Inc. All rights reserved.
5+
* See COPYING.txt for license details.
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Magento\SemanticVersionChecker\Operation\SystemXml;
11+
12+
use Magento\SemanticVersionChecker\Operation\AbstractOperation;
13+
use PHPSemVerChecker\SemanticVersioning\Level;
14+
15+
/**
16+
* When a <kbd>field</kbd> node is added.
17+
*/
18+
class DuplicateFieldAdded extends AbstractOperation
19+
{
20+
/**
21+
* @var string
22+
*/
23+
protected $code = 'M302';
24+
25+
/**
26+
* @var int
27+
*/
28+
protected $level = Level::PATCH;
29+
30+
/**
31+
* @var string
32+
*/
33+
protected $reason = 'A field-node was duplicated';
34+
}

Diff for: src/ReportBuilder.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ protected function makeVersionReport()
126126
// Customize severity level of some @api changes
127127
LevelMapping::setOverrides(
128128
[
129-
'V015' => Level::PATCH, // Add public method
129+
'V015' => Level::MINOR, // Add public method
130130
'V016' => Level::PATCH, // Add protected method
131131
'V019' => Level::MINOR, // Add public property
132132
'V020' => Level::MINOR, // Add protected property

0 commit comments

Comments
 (0)