From 27bcd61538a03e9b2f312a53265b3fb95774713d Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 21 Jul 2026 05:44:05 +0700 Subject: [PATCH 1/2] fix: ignore expanded entity nodes in source-backed sniffs --- phpunit.xml.dist | 3 + src/Runner/EntityExpansionMarker.php | 63 ++++++++++++++++ src/Runner/EntityPreprocessor.php | 31 ++++++-- src/Runner/XmlFileProcessor.php | 2 +- src/Sniff/AbstractSniff.php | 6 ++ src/Sniff/ExceptionNameSniff.php | 4 ++ src/Sniff/SimparaSniff.php | 4 ++ .../Runner/EntityExpansionMarkerTest.php | 72 +++++++++++++++++++ .../Sniff/EntityExpandedSniffTest.php | 62 ++++++++++++++++ tests/Unit/Sniff/ExceptionNameSniffTest.php | 4 ++ tests/Unit/Sniff/SimparaSniffTest.php | 4 ++ 11 files changed, 247 insertions(+), 8 deletions(-) create mode 100644 src/Runner/EntityExpansionMarker.php create mode 100644 tests/Integration/Runner/EntityExpansionMarkerTest.php create mode 100644 tests/Integration/Sniff/EntityExpandedSniffTest.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f173b58..eaa454f 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -18,6 +18,9 @@ tests/Unit + + tests/Integration + diff --git a/src/Runner/EntityExpansionMarker.php b/src/Runner/EntityExpansionMarker.php new file mode 100644 index 0000000..841fe67 --- /dev/null +++ b/src/Runner/EntityExpansionMarker.php @@ -0,0 +1,63 @@ +' + . $content + . ''; + } + + public static function contains(\DOMNode $node): bool + { + for ($current = $node; $current->parentNode !== null; $current = $current->parentNode) { + if (self::isBetweenMarkers($current)) { + return true; + } + } + + return false; + } + + private static function isBetweenMarkers(\DOMNode $node): bool + { + $nestedMarkers = 0; + + for ($sibling = $node->previousSibling; $sibling !== null; $sibling = $sibling->previousSibling) { + if (self::isEnd($sibling)) { + $nestedMarkers++; + continue; + } + + if (!self::isStart($sibling)) { + continue; + } + + if ($nestedMarkers === 0) { + return true; + } + + $nestedMarkers--; + } + + return false; + } + + private static function isStart(\DOMNode $node): bool + { + return $node instanceof \DOMComment && $node->textContent === self::START; + } + + private static function isEnd(\DOMNode $node): bool + { + return $node instanceof \DOMComment && $node->textContent === self::END; + } +} diff --git a/src/Runner/EntityPreprocessor.php b/src/Runner/EntityPreprocessor.php index a603684..f7d80b8 100644 --- a/src/Runner/EntityPreprocessor.php +++ b/src/Runner/EntityPreprocessor.php @@ -25,7 +25,14 @@ public function process(string $xml): string return $this->expandEntities($xml); } - private function expandEntities(string $content): string + public function processForParsing(string $xml): string + { + $xml = $this->stripDoctype($xml); + + return $this->expandEntities($xml, markXmlExpansions: true); + } + + private function expandEntities(string $content, bool $markXmlExpansions = false): string { $maxDepth = 20; @@ -33,10 +40,13 @@ private function expandEntities(string $content): string $changed = false; $content = preg_replace_callback( - '/|' . self::ENTITY_PATTERN . '/', - function (array $matches) use (&$changed): string { - // If this is a comment, return as is - if (str_starts_with($matches[0], '||<\?[\s\S]*?\?>|' . self::ENTITY_PATTERN . '/', + function (array $matches) use (&$changed, $markXmlExpansions): string { + if ( + str_starts_with($matches[0], '&value;'; + + self::assertSame( + 'expanded', + $preprocessor->process($source), + ); + } + + private function parse(string $xml): \DOMDocument + { + $document = new \DOMDocument(); + $document->loadXML($xml); + + return $document; + } +} diff --git a/tests/Integration/Sniff/EntityExpandedSniffTest.php b/tests/Integration/Sniff/EntityExpandedSniffTest.php new file mode 100644 index 0000000..a6b1925 --- /dev/null +++ b/tests/Integration/Sniff/EntityExpandedSniffTest.php @@ -0,0 +1,62 @@ +Source&expanded;'; + $document = $this->processedDocument($source, 'Expanded'); + + $violations = new SimparaSniff()->process($document, $source, 'file.xml'); + + self::assertCount(1, $violations); + self::assertSame(1, $violations[0]->line); + } + + #[Test] + public function exceptionNameIgnoresExpandedElements(): void + { + $source = 'RuntimeException&expanded;'; + $document = $this->processedDocument( + $source, + 'ExpandedException', + ); + + $violations = new ExceptionNameSniff()->process($document, $source, 'file.xml'); + + self::assertCount(1, $violations); + self::assertSame(1, $violations[0]->line); + } + + private function processedDocument(string $source, string $expanded): \DOMDocument + { + $content = new EntityPreprocessor(['expanded' => $expanded])->processForParsing($source); + $document = new \DOMDocument(); + $document->loadXML($content); + + return $document; + } +} diff --git a/tests/Unit/Sniff/ExceptionNameSniffTest.php b/tests/Unit/Sniff/ExceptionNameSniffTest.php index 99ecd73..5e749c6 100644 --- a/tests/Unit/Sniff/ExceptionNameSniffTest.php +++ b/tests/Unit/Sniff/ExceptionNameSniffTest.php @@ -5,14 +5,18 @@ namespace DocbookCS\Tests\Unit\Sniff; use DocbookCS\Report\Violation; +use DocbookCS\Runner\EntityExpansionMarker; use DocbookCS\Sniff\ExceptionNameSniff; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; #[ CoversClass(ExceptionNameSniff::class), CoversClass(Violation::class), + // + UsesClass(EntityExpansionMarker::class), ] final class ExceptionNameSniffTest extends TestCase { diff --git a/tests/Unit/Sniff/SimparaSniffTest.php b/tests/Unit/Sniff/SimparaSniffTest.php index d6c616a..036d374 100644 --- a/tests/Unit/Sniff/SimparaSniffTest.php +++ b/tests/Unit/Sniff/SimparaSniffTest.php @@ -5,14 +5,18 @@ namespace DocbookCS\Tests\Unit\Sniff; use DocbookCS\Report\Violation; +use DocbookCS\Runner\EntityExpansionMarker; use DocbookCS\Sniff\SimparaSniff; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; #[ CoversClass(SimparaSniff::class), CoversClass(Violation::class), + // + UsesClass(EntityExpansionMarker::class), ] final class SimparaSniffTest extends TestCase { From 4798a986aa53dbf3d53a68a437e61af47af7b367 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Wed, 22 Jul 2026 12:32:02 +0700 Subject: [PATCH 2/2] chore: formatting --- src/Runner/EntityExpansionMarker.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Runner/EntityExpansionMarker.php b/src/Runner/EntityExpansionMarker.php index 841fe67..ded0b3b 100644 --- a/src/Runner/EntityExpansionMarker.php +++ b/src/Runner/EntityExpansionMarker.php @@ -11,9 +11,7 @@ final class EntityExpansionMarker public static function wrap(string $content): string { - return '' - . $content - . ''; + return sprintf('%s', self::START, $content, self::END); } public static function contains(\DOMNode $node): bool