Skip to content

Commit 4292cd3

Browse files
committed
Merge remote-tracking branch 'origin/AC-3483' into Hammer-PlatformHealth-30sep24
2 parents 6430b3c + 162e923 commit 4292cd3

File tree

2 files changed

+231
-1
lines changed

2 files changed

+231
-1
lines changed

src/Analyzer/SystemXml/Analyzer.php

Lines changed: 197 additions & 1 deletion
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,142 @@ 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+
$baseDir = $this->getBaseDir($afterFile);
101+
foreach ($addedNodes as $nodeId => $node) {
102+
$newNodeData = $this->getNodeData($node);
103+
$nodePath = $newNodeData['path'];
104+
105+
// Extract section, group, and fieldId with error handling
106+
$extractedData = $this->extractSectionGroupField($nodePath);
107+
if ($extractedData === null) {
108+
// Skip the node if its path is invalid
109+
continue;
110+
}
111+
112+
// Extract section, group, and fieldId
113+
list($sectionId, $groupId, $fieldId) = $extractedData;
114+
115+
// Call function to check if this field is duplicated in other system.xml files
116+
$isDuplicated = $this->isDuplicatedFieldInXml($baseDir, $sectionId, $groupId, $fieldId, $afterFile);
117+
118+
foreach ($isDuplicated as $isDuplicatedItem) {
119+
if ($isDuplicatedItem['status'] === 'duplicate') {
120+
$this->reportDuplicateNodes($afterFile, [$nodeId => $node ]);
121+
} else {
122+
$this->reportAddedNodes($afterFile, [$nodeId => $node ]);
123+
}
124+
}
125+
}
98126
}
99127
}
100128
return $this->report;
101129
}
102130

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

297+
/**
298+
* Creates reports for <var>$nodes</var> considering that they have been duplicated.
299+
*
300+
* @param string $file
301+
* @param NodeInterface[] $nodes
302+
*/
303+
private function reportDuplicateNodes(string $file, array $nodes)
304+
{
305+
foreach ($nodes as $node) {
306+
switch (true) {
307+
case $node instanceof Field:
308+
$this->report->add('system', new DuplicateFieldAdded($file, $node->getPath()));
309+
break;
310+
}
311+
}
312+
}
313+
167314
/**
168315
* Creates reports for <var>$modules</var> considering that <kbd>system.xml</kbd> has been removed from them.
169316
*
@@ -202,4 +349,53 @@ private function reportRemovedNodes(string $file, array $nodes)
202349
}
203350
}
204351
}
352+
353+
/**
354+
* @param string|null $baseDir
355+
* @param string $sectionId
356+
* @param string $groupId
357+
* @param string $fieldId
358+
* @param string $afterFile
359+
* @return array
360+
* @throws \Exception
361+
*/
362+
private function isDuplicatedFieldInXml(?string $baseDir, string $sectionId, string $groupId, ?string $fieldId, string $afterFile): array
363+
{
364+
$hasDuplicate = false;
365+
366+
$result = [
367+
'status' => 'minor',
368+
'field' => $fieldId
369+
];
370+
371+
if ($baseDir) {
372+
$systemXmlFiles = $this->getSystemXmlFiles($baseDir, $afterFile);
373+
374+
foreach ($systemXmlFiles as $systemXmlFile) {
375+
$xmlContent = file_get_contents($systemXmlFile);
376+
try {
377+
$xml = new \SimpleXMLElement($xmlContent);
378+
} catch (\Exception $e) {
379+
continue; // Skip this file if there's a parsing error
380+
}
381+
// Find <field> nodes with the given field ID
382+
// XPath to search for <field> within a specific section and group
383+
$fields = $xml->xpath("//section[@id='$sectionId']/group[@id='$groupId']/field[@id='$fieldId']");
384+
if (!empty($fields)) {
385+
$hasDuplicate = true; // Set the duplicate flag to true if a match is found
386+
break; // Since we found a duplicate, we don't need to check further for this field
387+
}
388+
}
389+
if ($hasDuplicate) {
390+
return [
391+
[
392+
'status' => 'duplicate',
393+
'field' => $fieldId
394+
395+
]
396+
];
397+
}
398+
}
399+
return [$result];
400+
}
205401
}
Lines changed: 34 additions & 0 deletions
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+
}

0 commit comments

Comments
 (0)