diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3d691c..2d401f0 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:^2.1 --no-update fi; if [[ "${{ matrix.composer }}" == "lowest" ]]; then @@ -77,11 +77,17 @@ 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.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..5b91d70 --- /dev/null +++ b/infection.json5 @@ -0,0 +1,12 @@ +{ + "$schema": "https://infection.github.io/schema.json", + "staticAnalysisTool": "phpstan", + "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..b93e29a 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 = []; @@ -375,7 +364,7 @@ public function __construct() */ public function attachObserverToTheDomLoop(HtmlMinDomObserverInterface $observer) { - $this->domLoopObservers[$observer] = $observer; + $this->domLoopObservers->offsetSet($observer, $observer); } /** @@ -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[]'); + throw new \InvalidArgumentException('overwriteTemplateLogicSyntaxInSpecialScriptTags 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[]'); + throw new \InvalidArgumentException('overwriteSpecialScriptTags 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 bb69e2b..adb58d2 100644 --- a/tests/HtmlMinTest.php +++ b/tests/HtmlMinTest.php @@ -21,6 +21,63 @@ 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); + } + + public function testOverwriteTemplateLogicSyntaxInSpecialScriptTagsRejectsNonStringValues() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('overwriteTemplateLogicSyntaxInSpecialScriptTags only allows string[]'); + + (new HtmlMin())->overwriteTemplateLogicSyntaxInSpecialScriptTags(['{%', 123]); + } + + public function testOverwriteSpecialScriptTagsRejectsNonStringValues() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('overwriteSpecialScriptTags only allows string[]'); + + (new HtmlMin())->overwriteSpecialScriptTags(['text/html', 123]); + } + /** * @return array */