diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 93f75ac..2b3d73c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ jobs: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: - php-version: '8.1' + php-version: '8.2' coverage: none - run: composer update --no-progress - run: composer update --no-progress --working-dir=dev-tools diff --git a/composer.json b/composer.json index 1317e34..29353d1 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ "php": "^7.0 || ^8.0", "ext-dom": "*", "ext-libxml": "*", - "phpunit/phpunit": "^6.5.14 || ^7.5.20 || ^8.0 || ^9.0 || ^10.0" + "phpunit/phpunit": "^6.5.14 || ^7.5.20 || ^8.0 || ^9.0 || ^10.0 || ^11.0" }, "autoload": { "psr-4": { diff --git a/dev-tools/composer.json b/dev-tools/composer.json index 1c2f9c1..b6475f3 100644 --- a/dev-tools/composer.json +++ b/dev-tools/composer.json @@ -1,6 +1,6 @@ { "require": { - "php": "^8.1" + "php": "^8.2" }, "require-dev": { "ergebnis/composer-normalize": "*", diff --git a/src/Constraint/XmlMatchesXsd.php b/src/Constraint/XmlMatchesXsd.php index f981ab5..5d7c6da 100644 --- a/src/Constraint/XmlMatchesXsd.php +++ b/src/Constraint/XmlMatchesXsd.php @@ -16,6 +16,8 @@ class_alias(XmlMatchesXsdForV5::class, XmlMatchesXsd::class); } elseif (version_compare(\PHPUnit\Runner\Version::id(), '8.0.0') < 0) { class_alias(XmlMatchesXsdForV7::class, XmlMatchesXsd::class); -} else { +} elseif (version_compare(\PHPUnit\Runner\Version::id(), '11.0.0') < 0) { class_alias(XmlMatchesXsdForV8::class, XmlMatchesXsd::class); +} else { + class_alias(XmlMatchesXsdForV11::class, XmlMatchesXsd::class); } diff --git a/src/Constraint/XmlMatchesXsdForV11.php b/src/Constraint/XmlMatchesXsdForV11.php new file mode 100644 index 0000000..23b9ea4 --- /dev/null +++ b/src/Constraint/XmlMatchesXsdForV11.php @@ -0,0 +1,114 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace PhpCsFixer\PhpunitConstraintXmlMatchesXsd\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; + +/** + * @internal + */ +final readonly class XmlMatchesXsdForV11 extends Constraint +{ + /** + * @var string[] + */ + private array $xmlConstraintErrors; + + public function __construct(private string $xsd) + { + // replace first only + $needle = 'http://www.w3.org/2001/xml.xsd'; + if (false !== $pos = strpos($xsd, $needle)) { + $xsd = substr_replace($xsd, 'file:///'.str_replace('\\', '/', __DIR__).'/xml.xsd', $pos, \strlen($needle)); + } + + $this->xsd = $xsd; + } + + public function toString(): string + { + return 'matches XSD'; + } + + protected function failureDescription($other): string + { + if (\is_string($other)) { + return sprintf("%s %s.\n%s", $other, $this->toString(), implode("\n", $this->xmlConstraintErrors)); + } + + if (\is_object($other)) { + $type = sprintf('%s#%s', \get_class($other), method_exists($other, '__toString') ? $other->__toString() : ''); + } elseif (null === $other) { + $type = 'null'; + } else { + $type = \gettype($other).'#'.$other; + } + + return $type.' '.$this->toString(); + } + + protected function matches($other): bool + { + return \is_string($other) + ? $this->stringMatches($other) + : false; + } + + private function stringMatches(string $other): bool + { + $internalErrors = libxml_use_internal_errors(true); + libxml_clear_errors(); + + $dom = new \DOMDocument(); + $dom->preserveWhiteSpace = false; + $dom->validateOnParse = true; + + if (!@$dom->loadXML($other, LIBXML_NONET | (\defined('LIBXML_COMPACT') ? LIBXML_COMPACT : 0))) { + $this->setXMLConstraintErrors(); + libxml_clear_errors(); + libxml_use_internal_errors($internalErrors); + + return false; + } + + $dom->normalizeDocument(); + + libxml_clear_errors(); + + if (false === $result = @$dom->schemaValidateSource($this->xsd)) { + $this->setXMLConstraintErrors(); + } + + libxml_clear_errors(); + libxml_use_internal_errors($internalErrors); + + return $result; + } + + private function setXMLConstraintErrors() + { + foreach (libxml_get_errors() as $error) { + if (LIBXML_ERR_WARNING === $error->level) { + $level = 'warning '; + } elseif (LIBXML_ERR_ERROR === $error->level) { + $level = 'error '; + } elseif (LIBXML_ERR_FATAL === $error->level) { + $level = 'fatal '; + } else { + $level = ''; + } + + $this->xmlConstraintErrors[] = sprintf('[%s%s] %s (line %d, column %d).', $level, $error->code, trim($error->message), $error->line, $error->column); + } + } +}