From 56d33c68c943302b86d8419daabea27fe27c59a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 16:19:34 +0000 Subject: [PATCH 1/4] Initial plan From f839dbf476136280ecc53c0379308c7c8d1f4d86 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 16:27:54 +0000 Subject: [PATCH 2/4] Fix PHP 8.5 observer deprecation handling Agent-Logs-Url: https://github.com/voku/HtmlMin/sessions/1a74d871-5b13-42d4-b172-ee87a4776089 Co-authored-by: voku <264695+voku@users.noreply.github.com> --- .github/workflows/ci.yml | 15 ++++++++++---- infection.json5 | 11 ++++++++++ phpstan.neon | 1 - src/voku/helper/HtmlMin.php | 2 +- tests/HtmlMinTest.php | 41 +++++++++++++++++++++++++++++++++++++ 5 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 infection.json5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3d691c..179fb1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: php-version: ${{ matrix.php }} coverage: xdebug extensions: zip - tools: composer + tools: composer, infection - name: Determine composer cache directory id: composer-cache @@ -57,8 +57,8 @@ jobs: - name: Install dependencies run: | - if [[ "${{ matrix.php }}" == "8.1" ]]; then - composer require phpstan/phpstan --no-update + if [[ "${{ matrix.php }}" == "8.5" ]]; then + composer require --dev phpstan/phpstan --no-update fi; if [[ "${{ matrix.composer }}" == "lowest" ]]; then @@ -78,10 +78,17 @@ jobs: - name: Run phpstan continue-on-error: true - if: ${{ matrix.php == '8.1' }} + if: ${{ matrix.php == '8.5' }} run: | php vendor/bin/phpstan analyse + - name: Run infection + if: ${{ matrix.php == '8.5' }} + env: + XDEBUG_MODE: coverage + run: | + infection --threads=max --min-msi=0 --min-covered-msi=0 --only-covering-test-cases --no-progress + - name: Upload coverage results to Coveralls continue-on-error: true env: diff --git a/infection.json5 b/infection.json5 new file mode 100644 index 0000000..0bac9c9 --- /dev/null +++ b/infection.json5 @@ -0,0 +1,11 @@ +{ + "$schema": "https://infection.github.io/schema.json", + "source": { + "directories": [ + "src" + ] + }, + "logs": { + "summary": "build/logs/infection-summary.log" + } +} diff --git a/phpstan.neon b/phpstan.neon index b58c3b4..5d10cdb 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,4 @@ parameters: level: 8 - checkMissingIterableValueType: false paths: - %currentWorkingDirectory%/src/ diff --git a/src/voku/helper/HtmlMin.php b/src/voku/helper/HtmlMin.php index 42b66f8..53f5cb1 100644 --- a/src/voku/helper/HtmlMin.php +++ b/src/voku/helper/HtmlMin.php @@ -375,7 +375,7 @@ public function __construct() */ public function attachObserverToTheDomLoop(HtmlMinDomObserverInterface $observer) { - $this->domLoopObservers[$observer] = $observer; + $this->domLoopObservers->offsetSet($observer, $observer); } /** diff --git a/tests/HtmlMinTest.php b/tests/HtmlMinTest.php index bb69e2b..f67c627 100644 --- a/tests/HtmlMinTest.php +++ b/tests/HtmlMinTest.php @@ -21,6 +21,47 @@ public function testEmptyResult() static::assertSame('', (new HtmlMin())->minify('')); } + public function testAttachObserverToTheDomLoopDoesNotTriggerDeprecation() + { + $observer = new class implements \voku\helper\HtmlMinDomObserverInterface { + /** @var int */ + public $beforeCalls = 0; + + /** @var int */ + public $afterCalls = 0; + + public function domElementBeforeMinification(\voku\helper\SimpleHtmlDomInterface $element, \voku\helper\HtmlMinInterface $htmlMin) + { + ++$this->beforeCalls; + } + + public function domElementAfterMinification(\voku\helper\SimpleHtmlDomInterface $element, \voku\helper\HtmlMinInterface $htmlMin) + { + ++$this->afterCalls; + } + }; + + \set_error_handler(static function (int $severity, string $message, string $file, int $line): bool { + if (($severity & \E_DEPRECATED) !== 0) { + throw new \ErrorException($message, 0, $severity, $file, $line); + } + + return false; + }); + + try { + $minifier = new HtmlMin(); + $minifier->attachObserverToTheDomLoop($observer); + + static::assertSame('
test
', $minifier->minify('
test
')); + } finally { + \restore_error_handler(); + } + + static::assertGreaterThan(0, $observer->beforeCalls); + static::assertGreaterThan(0, $observer->afterCalls); + } + /** * @return array */ From 021bcb52f72e391939943809f2cd0d76d7225c7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 17:38:59 +0000 Subject: [PATCH 3/4] Integrate Infection PHPStan analysis in CI Agent-Logs-Url: https://github.com/voku/HtmlMin/sessions/c4646820-ee2c-4ae4-99b6-2d713d8aece6 Co-authored-by: voku <264695+voku@users.noreply.github.com> --- .github/workflows/ci.yml | 3 +- infection.json5 | 1 + src/voku/helper/HtmlMin.php | 60 ++++++++++--------- .../HtmlMinDomObserverOptimizeAttributes.php | 2 +- src/voku/helper/HtmlMinInterface.php | 2 +- tests/HtmlMinTest.php | 16 +++++ 6 files changed, 53 insertions(+), 31 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 179fb1d..2d401f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: - name: Install dependencies run: | if [[ "${{ matrix.php }}" == "8.5" ]]; then - composer require --dev phpstan/phpstan --no-update + composer require --dev phpstan/phpstan:^2.1 --no-update fi; if [[ "${{ matrix.composer }}" == "lowest" ]]; then @@ -77,7 +77,6 @@ jobs: php vendor/bin/phpunit -c phpunit.xml --coverage-clover=build/logs/clover.xml - name: Run phpstan - continue-on-error: true if: ${{ matrix.php == '8.5' }} run: | php vendor/bin/phpstan analyse diff --git a/infection.json5 b/infection.json5 index 0bac9c9..5b91d70 100644 --- a/infection.json5 +++ b/infection.json5 @@ -1,5 +1,6 @@ { "$schema": "https://infection.github.io/schema.json", + "staticAnalysisTool": "phpstan", "source": { "directories": [ "src" diff --git a/src/voku/helper/HtmlMin.php b/src/voku/helper/HtmlMin.php index 53f5cb1..8d81699 100644 --- a/src/voku/helper/HtmlMin.php +++ b/src/voku/helper/HtmlMin.php @@ -25,17 +25,6 @@ class HtmlMin implements HtmlMinInterface */ private static $regExSpace = "/[[:space:]]{2,}|[\r\n]/u"; - /** - * @var string[] - * - * @psalm-var list - */ - private static $optional_end_tags = [ - 'html', - 'head', - 'body', - ]; - /** * @var string[] * @@ -91,7 +80,7 @@ class HtmlMin implements HtmlMinInterface ]; /** - * @var array + * @var array */ private static $booleanAttributes = [ 'allowfullscreen' => '', @@ -138,7 +127,7 @@ class HtmlMin implements HtmlMinInterface ]; /** - * @var array + * @var list */ private static $skipTagsForRemoveWhitespace = [ 'code', @@ -149,7 +138,7 @@ class HtmlMin implements HtmlMinInterface ]; /** - * @var array + * @var array */ private $protectedChildNodes = []; @@ -1230,7 +1219,7 @@ private function domNodeOpeningTagOptional(\DOMNode $node): bool } return !\in_array( - $firstChild->tagName, + $firstChild->nodeName, [ 'meta', 'link', @@ -1586,7 +1575,7 @@ private function minifyJsonString(string $json): string } /** - * @return array + * @return string[] */ public function getDomainsToRemoveHttpPrefixFromAttributes(): array { @@ -2266,9 +2255,9 @@ private function protectTagHelper(HtmlDomParser $dom, string $selector): HtmlDom } $parentNode = $element->parentNode(); - if ($parentNode !== null && $parentNode->nodeValue !== null) { + if ($parentNode !== null && $parentNode->getNode()->nodeValue !== null) { $this->protectedChildNodes[$this->protected_tags_counter] = $parentNode->innerHtml(); - $parentNode->nodeValue = '<' . $this->protectedChildNodesHelper . ' data-' . $this->protectedChildNodesHelper . '="' . $this->protected_tags_counter . '">protectedChildNodesHelper . '>'; + $parentNode->getNode()->nodeValue = '<' . $this->protectedChildNodesHelper . ' data-' . $this->protectedChildNodesHelper . '="' . $this->protected_tags_counter . '">protectedChildNodesHelper . '>'; } ++$this->protected_tags_counter; @@ -2301,7 +2290,7 @@ private function protectTags(HtmlDomParser $dom): HtmlDomParser } } - $innerHtml = $element->innerhtml; + $innerHtml = $element->innerHtml(); // On PHP < 8.0 the simplevokubroken-hash mechanism restores content // (including surrounding newlines/spaces) AFTER fixHtmlOutput's trim @@ -2338,7 +2327,8 @@ private function protectTags(HtmlDomParser $dom): HtmlDomParser ) { $originalInnerHtml = $innerHtml; try { - $innerHtml = \JShrink\Minifier::minify($innerHtml); + $minifiedInnerHtml = \JShrink\Minifier::minify($innerHtml); + $innerHtml = \is_string($minifiedInnerHtml) ? $minifiedInnerHtml : $originalInnerHtml; } catch (\Exception $e) { $innerHtml = $originalInnerHtml; } @@ -2411,6 +2401,9 @@ private function removeComments(HtmlDomParser $dom): HtmlDomParser foreach ($dom->findMulti('//comment()') as $commentWrapper) { $comment = $commentWrapper->getNode(); $val = $comment->nodeValue; + if ($val === null) { + continue; + } if (\strpos($val, '[') === false) { $parentNode = $comment->parentNode; if ($parentNode !== null) { @@ -2449,6 +2442,9 @@ private function removeCommentsOnlyFromHtmlString(string $html): string foreach ($dom->findMulti('//comment()') as $commentWrapper) { $comment = $commentWrapper->getNode(); $commentValue = $comment->nodeValue; + if ($commentValue === null) { + continue; + } if ( $this->isConditionalComment($commentValue) || @@ -2508,7 +2504,7 @@ private function removeWhitespaceAroundTags(SimpleHtmlDomInterface $element) /** * Callback function for preg_replace_callback use. * - * @param array $matches PREG matches + * @param array $matches PREG matches * * @return string */ @@ -2516,7 +2512,7 @@ private function restoreProtectedHtml($matches): string { \preg_match('/=["\']*(?\d+)/', $matches['attributes'], $matchesInner); - return $this->protectedChildNodes[$matchesInner['id']] ?? ''; + return isset($matchesInner['id']) ? ($this->protectedChildNodes[(int) $matchesInner['id']] ?? '') : ''; } /** @@ -2574,6 +2570,10 @@ private function sumUpWhitespace(HtmlDomParser $dom): HtmlDomParser continue; } + if ($text_node->nodeValue === null) { + continue; + } + $nodeValueTmp = \preg_replace(self::$regExSpace, ' ', $text_node->nodeValue); if ($nodeValueTmp !== null) { $text_node->nodeValue = $nodeValueTmp; @@ -2600,38 +2600,44 @@ public function useKeepBrokenHtml(bool $keepBrokenHtml): self } /** - * @param string[] $templateLogicSyntaxInSpecialScriptTags + * @param array $templateLogicSyntaxInSpecialScriptTags * * @return HtmlMin */ public function overwriteTemplateLogicSyntaxInSpecialScriptTags(array $templateLogicSyntaxInSpecialScriptTags): self { + $validatedTemplateLogicSyntaxInSpecialScriptTags = []; foreach ($templateLogicSyntaxInSpecialScriptTags as $tmp) { if (!\is_string($tmp)) { throw new \InvalidArgumentException('setTemplateLogicSyntaxInSpecialScriptTags only allows string[]'); } + + $validatedTemplateLogicSyntaxInSpecialScriptTags[] = $tmp; } - $this->templateLogicSyntaxInSpecialScriptTags = $templateLogicSyntaxInSpecialScriptTags; + $this->templateLogicSyntaxInSpecialScriptTags = $validatedTemplateLogicSyntaxInSpecialScriptTags; return $this; } /** - * @param string[] $specialScriptTags + * @param array $specialScriptTags * - * @return HtmlDomParser + * @return HtmlMin */ public function overwriteSpecialScriptTags(array $specialScriptTags): self { + $validatedSpecialScriptTags = []; foreach ($specialScriptTags as $tag) { if (!\is_string($tag)) { throw new \InvalidArgumentException('SpecialScriptTags only allows string[]'); } + + $validatedSpecialScriptTags[] = $tag; } - $this->specialScriptTags = $specialScriptTags; + $this->specialScriptTags = $validatedSpecialScriptTags; return $this; } diff --git a/src/voku/helper/HtmlMinDomObserverOptimizeAttributes.php b/src/voku/helper/HtmlMinDomObserverOptimizeAttributes.php index edfa2da..e94ff1f 100644 --- a/src/voku/helper/HtmlMinDomObserverOptimizeAttributes.php +++ b/src/voku/helper/HtmlMinDomObserverOptimizeAttributes.php @@ -162,7 +162,7 @@ public function domElementAfterMinification(SimpleHtmlDomInterface $element, Htm * @param string $tag * @param string $attrName * @param string $attrValue - * @param array $allAttr + * @param array $allAttr * @param HtmlMinInterface $htmlMin * * @return bool diff --git a/src/voku/helper/HtmlMinInterface.php b/src/voku/helper/HtmlMinInterface.php index 7a0cfe9..56a3833 100644 --- a/src/voku/helper/HtmlMinInterface.php +++ b/src/voku/helper/HtmlMinInterface.php @@ -163,7 +163,7 @@ public function isXHTML(): bool; public function getLocalDomains(): array; /** - * @return array + * @return string[] */ public function getDomainsToRemoveHttpPrefixFromAttributes(): array; } diff --git a/tests/HtmlMinTest.php b/tests/HtmlMinTest.php index f67c627..f26a323 100644 --- a/tests/HtmlMinTest.php +++ b/tests/HtmlMinTest.php @@ -62,6 +62,22 @@ public function domElementAfterMinification(\voku\helper\SimpleHtmlDomInterface static::assertGreaterThan(0, $observer->afterCalls); } + public function testOverwriteTemplateLogicSyntaxInSpecialScriptTagsRejectsNonStringValues() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('setTemplateLogicSyntaxInSpecialScriptTags only allows string[]'); + + (new HtmlMin())->overwriteTemplateLogicSyntaxInSpecialScriptTags(['{%', 123]); + } + + public function testOverwriteSpecialScriptTagsRejectsNonStringValues() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('SpecialScriptTags only allows string[]'); + + (new HtmlMin())->overwriteSpecialScriptTags(['text/html', 123]); + } + /** * @return array */ From f94d6681c7f3680b9defb69f894542fc38c9e7b6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 18:26:53 +0000 Subject: [PATCH 4/4] Align HtmlMin exception messages with method names Agent-Logs-Url: https://github.com/voku/HtmlMin/sessions/6d4cb1d3-82e6-4d7b-a893-c981c65a02af Co-authored-by: voku <264695+voku@users.noreply.github.com> --- src/voku/helper/HtmlMin.php | 4 ++-- tests/HtmlMinTest.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/voku/helper/HtmlMin.php b/src/voku/helper/HtmlMin.php index 8d81699..b93e29a 100644 --- a/src/voku/helper/HtmlMin.php +++ b/src/voku/helper/HtmlMin.php @@ -2609,7 +2609,7 @@ public function overwriteTemplateLogicSyntaxInSpecialScriptTags(array $templateL $validatedTemplateLogicSyntaxInSpecialScriptTags = []; foreach ($templateLogicSyntaxInSpecialScriptTags as $tmp) { if (!\is_string($tmp)) { - throw new \InvalidArgumentException('setTemplateLogicSyntaxInSpecialScriptTags only allows string[]'); + throw new \InvalidArgumentException('overwriteTemplateLogicSyntaxInSpecialScriptTags only allows string[]'); } $validatedTemplateLogicSyntaxInSpecialScriptTags[] = $tmp; @@ -2631,7 +2631,7 @@ public function overwriteSpecialScriptTags(array $specialScriptTags): self $validatedSpecialScriptTags = []; foreach ($specialScriptTags as $tag) { if (!\is_string($tag)) { - throw new \InvalidArgumentException('SpecialScriptTags only allows string[]'); + throw new \InvalidArgumentException('overwriteSpecialScriptTags only allows string[]'); } $validatedSpecialScriptTags[] = $tag; diff --git a/tests/HtmlMinTest.php b/tests/HtmlMinTest.php index f26a323..adb58d2 100644 --- a/tests/HtmlMinTest.php +++ b/tests/HtmlMinTest.php @@ -65,7 +65,7 @@ public function domElementAfterMinification(\voku\helper\SimpleHtmlDomInterface public function testOverwriteTemplateLogicSyntaxInSpecialScriptTagsRejectsNonStringValues() { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('setTemplateLogicSyntaxInSpecialScriptTags only allows string[]'); + $this->expectExceptionMessage('overwriteTemplateLogicSyntaxInSpecialScriptTags only allows string[]'); (new HtmlMin())->overwriteTemplateLogicSyntaxInSpecialScriptTags(['{%', 123]); } @@ -73,7 +73,7 @@ public function testOverwriteTemplateLogicSyntaxInSpecialScriptTagsRejectsNonStr public function testOverwriteSpecialScriptTagsRejectsNonStringValues() { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('SpecialScriptTags only allows string[]'); + $this->expectExceptionMessage('overwriteSpecialScriptTags only allows string[]'); (new HtmlMin())->overwriteSpecialScriptTags(['text/html', 123]); }