From b5a8841d2459024c8f28620f7e84c8e1097133e4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 20 Nov 2025 16:50:07 +0100 Subject: [PATCH] Drop DOM dependency --- composer.json | 132 +++++++++--------- src/Report/Xml/BuildInformation.php | 52 +++---- src/Report/Xml/Coverage.php | 26 ++-- src/Report/Xml/Facade.php | 89 ++++++++---- src/Report/Xml/File.php | 54 ++----- src/Report/Xml/Method.php | 24 ++-- src/Report/Xml/Node.php | 56 ++------ src/Report/Xml/Project.php | 56 +++----- src/Report/Xml/Report.php | 70 +++------- src/Report/Xml/Source.php | 17 +-- src/Report/Xml/Tests.php | 24 ++-- src/Report/Xml/Totals.php | 94 +++++-------- src/Report/Xml/Unit.php | 42 ++---- .../BankAccount.php.xml | 2 +- ..._with_class_and_anonymous_function.php.xml | 2 +- .../source_with_ignore.php.xml | 2 +- 16 files changed, 301 insertions(+), 441 deletions(-) diff --git a/composer.json b/composer.json index 744507de0..259a58caa 100644 --- a/composer.json +++ b/composer.json @@ -1,67 +1,73 @@ { - "name": "phpunit/php-code-coverage", - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "type": "library", - "keywords": [ - "coverage", - "testing", - "xunit" - ], - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", - "license": "BSD-3-Clause", - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy" - }, - "config": { - "platform": { - "php": "8.3.0" - }, - "optimize-autoloader": true, - "sort-packages": true - }, - "prefer-stable": true, - "require": { - "php": ">=8.3", - "ext-dom": "*", - "ext-libxml": "*", - "ext-xmlwriter": "*", - "nikic/php-parser": "^5.6.1", - "phpunit/php-file-iterator": "^6.0", - "phpunit/php-text-template": "^5.0", - "sebastian/complexity": "^5.0", - "sebastian/environment": "^8.0.3", - "sebastian/lines-of-code": "^4.0", - "sebastian/version": "^6.0", - "theseer/tokenizer": "^1.2.3" - }, - "require-dev": { - "phpunit/phpunit": "^12.3.7" - }, - "suggest": { - "ext-pcov": "PHP extension that provides line coverage", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "autoload-dev": { - "classmap": [ - "tests/" - ] + "name": "phpunit/php-code-coverage", + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "type": "library", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "license": "BSD-3-Clause", + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy" + }, + "config": { + "platform": { + "php": "8.3.0" }, - "extra": { - "branch-alias": { - "dev-main": "12.4.x-dev" - } + "optimize-autoloader": true, + "sort-packages": true + }, + "prefer-stable": true, + "require": { + "php": ">=8.3", + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.6.1", + "phpunit/php-file-iterator": "^6.0", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.0.3", + "sebastian/lines-of-code": "^4.0", + "sebastian/version": "^6.0", + "theseer/tokenizer": "dev-writer as 1.3-dev" + }, + "require-dev": { + "phpunit/phpunit": "^12.3.7" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "autoload-dev": { + "classmap": [ + "tests/" + ] + }, + "extra": { + "branch-alias": { + "dev-main": "12.4.x-dev" + } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/staabm/tokenizer" } + ] } diff --git a/src/Report/Xml/BuildInformation.php b/src/Report/Xml/BuildInformation.php index 654eecb31..b9375f228 100644 --- a/src/Report/Xml/BuildInformation.php +++ b/src/Report/Xml/BuildInformation.php @@ -9,63 +9,47 @@ */ namespace SebastianBergmann\CodeCoverage\Report\Xml; -use function assert; use function phpversion; use DateTimeImmutable; -use DOMElement; use SebastianBergmann\Environment\Runtime; +use XMLWriter; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage */ final readonly class BuildInformation { - private DOMElement $contextNode; - public function __construct( - DOMElement $contextNode, + XMLWriter $xmlWriter, Runtime $runtime, DateTimeImmutable $buildDate, string $phpUnitVersion, string $coverageVersion ) { - $this->contextNode = $contextNode; - - $runtimeNode = $this->nodeByName('runtime'); + $xmlWriter->startElement('build'); + $xmlWriter->writeAttribute('time', $buildDate->format('D M j G:i:s T Y')); + $xmlWriter->writeAttribute('phpunit', $phpUnitVersion); + $xmlWriter->writeAttribute('coverage', $coverageVersion); - $runtimeNode->setAttribute('name', $runtime->getName()); - $runtimeNode->setAttribute('version', $runtime->getVersion()); - $runtimeNode->setAttribute('url', $runtime->getVendorUrl()); + $xmlWriter->startElement('runtime'); + $xmlWriter->writeAttribute('name', $runtime->getName()); + $xmlWriter->writeAttribute('version', $runtime->getVersion()); + $xmlWriter->writeAttribute('url', $runtime->getVendorUrl()); + $xmlWriter->endElement(); - $driverNode = $this->nodeByName('driver'); + $xmlWriter->startElement('driver'); if ($runtime->hasXdebug()) { - $driverNode->setAttribute('name', 'xdebug'); - $driverNode->setAttribute('version', phpversion('xdebug')); + $xmlWriter->writeAttribute('name', 'xdebug'); + $xmlWriter->writeAttribute('version', phpversion('xdebug')); } if ($runtime->hasPCOV()) { - $driverNode->setAttribute('name', 'pcov'); - $driverNode->setAttribute('version', phpversion('pcov')); + $xmlWriter->writeAttribute('name', 'pcov'); + $xmlWriter->writeAttribute('version', phpversion('pcov')); } + $xmlWriter->endElement(); - $this->contextNode->setAttribute('time', $buildDate->format('D M j G:i:s T Y')); - - $this->contextNode->setAttribute('phpunit', $phpUnitVersion); - $this->contextNode->setAttribute('coverage', $coverageVersion); - } - - private function nodeByName(string $name): DOMElement - { - $node = $this->contextNode->appendChild( - $this->contextNode->ownerDocument->createElementNS( - Facade::XML_NAMESPACE, - $name, - ), - ); - - assert($node instanceof DOMElement); - - return $node; + $xmlWriter->endElement(); } } diff --git a/src/Report/Xml/Coverage.php b/src/Report/Xml/Coverage.php index 9462780be..3038eb143 100644 --- a/src/Report/Xml/Coverage.php +++ b/src/Report/Xml/Coverage.php @@ -9,7 +9,6 @@ */ namespace SebastianBergmann\CodeCoverage\Report\Xml; -use DOMElement; use XMLWriter; /** @@ -17,20 +16,21 @@ */ final class Coverage { - private readonly DOMElement $contextNode; + private readonly XMLWriter $xmlWriter; private readonly string $line; - public function __construct(DOMElement $context, string $line) - { - $this->contextNode = $context; - $this->line = $line; + public function __construct( + XMLWriter $xmlWriter, + string $line + ) { + $this->xmlWriter = $xmlWriter; + $this->line = $line; } public function finalize(array $tests): void { - $writer = new XMLWriter; - $writer->openMemory(); - $writer->startElementNs(null, $this->contextNode->nodeName, Facade::XML_NAMESPACE); + $writer = $this->xmlWriter; + $writer->startElement('line'); $writer->writeAttribute('nr', $this->line); foreach ($tests as $test) { @@ -39,13 +39,5 @@ public function finalize(array $tests): void $writer->endElement(); } $writer->endElement(); - - $fragment = $this->contextNode->ownerDocument->createDocumentFragment(); - $fragment->appendXML($writer->outputMemory()); - - $this->contextNode->parentNode->replaceChild( - $fragment, - $this->contextNode, - ); } } diff --git a/src/Report/Xml/Facade.php b/src/Report/Xml/Facade.php index c3767088e..cba61f0c2 100644 --- a/src/Report/Xml/Facade.php +++ b/src/Report/Xml/Facade.php @@ -21,7 +21,6 @@ use function strlen; use function substr; use DateTimeImmutable; -use DOMDocument; use SebastianBergmann\CodeCoverage\CodeCoverage; use SebastianBergmann\CodeCoverage\Data\ProcessedClassType; use SebastianBergmann\CodeCoverage\Data\ProcessedFunctionType; @@ -31,11 +30,11 @@ use SebastianBergmann\CodeCoverage\Node\File as FileNode; use SebastianBergmann\CodeCoverage\PathExistsButIsNotDirectoryException; use SebastianBergmann\CodeCoverage\Util\Filesystem; -use SebastianBergmann\CodeCoverage\Util\Xml; use SebastianBergmann\CodeCoverage\Version; use SebastianBergmann\CodeCoverage\WriteOperationFailedException; use SebastianBergmann\CodeCoverage\XmlException; use SebastianBergmann\Environment\Runtime; +use XMLWriter; /** * @phpstan-import-type TestType from CodeCoverage @@ -66,15 +65,21 @@ public function process(CodeCoverage $coverage, string $target): void $report = $coverage->getReport(); + $writer = new XMLWriter; + $writer->openUri($this->targetFilePath('index')); + $writer->setIndent(true); + $writer->setIndentString(' '); $this->project = new Project( + $writer, $coverage->getReport()->name(), ); $this->setBuildInformation(); + + $this->project->startProject(); $this->processTests($coverage->getTests()); $this->processDirectory($report, $this->project); - - $this->saveDocument($this->project->asDom(), 'index'); + $this->project->finalize(); } private function setBuildInformation(): void @@ -119,7 +124,10 @@ private function processDirectory(DirectoryNode $directory, Node $context): void $directoryName = '/'; } - $directoryObject = $context->addDirectory($directoryName); + $writer = $this->project->getWriter(); + $writer->startElement('directory'); + $writer->writeAttribute('name', $directoryName); + $directoryObject = $context->addDirectory(); $this->setTotals($directory, $directoryObject->totals()); @@ -130,6 +138,7 @@ private function processDirectory(DirectoryNode $directory, Node $context): void foreach ($directory->files() as $node) { $this->processFile($node, $directoryObject); } + $writer->endElement(); } /** @@ -137,19 +146,26 @@ private function processDirectory(DirectoryNode $directory, Node $context): void */ private function processFile(FileNode $file, Directory $context): void { - $fileObject = $context->addFile( - $file->name(), - $file->id() . '.xml', - ); + $context->getWriter()->startElement('file'); + $context->getWriter()->writeAttribute('name', $file->name()); + $context->getWriter()->writeAttribute('href', $file->id() . '.xml'); + + $fileObject = $context->addFile(); $this->setTotals($file, $fileObject->totals()); + $context->getWriter()->endElement(); + $path = substr( $file->pathAsString(), strlen($this->project->projectSourceDirectory()), ); - $fileReport = new Report($path); + $writer = new XMLWriter; + $writer->openUri($this->targetFilePath($file->id())); + $writer->setIndent(true); + $writer->setIndentString(' '); + $fileReport = new Report($writer, $path); $this->setTotals($file, $fileReport->totals()); @@ -161,6 +177,8 @@ private function processFile(FileNode $file, Directory $context): void $this->processFunction($function, $fileReport); } + $fileReport->getWriter()->startElement('coverage'); + foreach ($file->lineCoverageData() as $line => $tests) { if (!is_array($tests) || count($tests) === 0) { continue; @@ -169,17 +187,20 @@ private function processFile(FileNode $file, Directory $context): void $coverage = $fileReport->lineCoverage((string) $line); $coverage->finalize($tests); } + $fileReport->getWriter()->endElement(); $fileReport->source()->setSourceCode( file_get_contents($file->pathAsString()), ); - $this->saveDocument($fileReport->asDom(), $file->id()); + $fileReport->finalize(); } private function processUnit(ProcessedClassType|ProcessedTraitType $unit, Report $report): void { if ($unit instanceof ProcessedClassType) { + $report->getWriter()->startElement('class'); + $unitObject = $report->classObject( $unit->className, $unit->namespace, @@ -189,6 +210,8 @@ private function processUnit(ProcessedClassType|ProcessedTraitType $unit, Report (float) $unit->crap, ); } else { + $report->getWriter()->startElement('trait'); + $unitObject = $report->traitObject( $unit->traitName, $unit->namespace, @@ -200,6 +223,8 @@ private function processUnit(ProcessedClassType|ProcessedTraitType $unit, Report } foreach ($unit->methods as $method) { + $report->getWriter()->startElement('method'); + $unitObject->addMethod( $method->methodName, $method->signature, @@ -210,11 +235,17 @@ private function processUnit(ProcessedClassType|ProcessedTraitType $unit, Report (string) $method->coverage, $method->crap, ); + + $report->getWriter()->endElement(); } + + $report->getWriter()->endElement(); } private function processFunction(ProcessedFunctionType $function, Report $report): void { + $report->getWriter()->startElement('function'); + $report->functionObject( $function->functionName, $function->signature, @@ -225,6 +256,8 @@ private function processFunction(ProcessedFunctionType $function, Report $report (string) $function->coverage, $function->crap, ); + + $report->getWriter()->endElement(); } /** @@ -232,15 +265,21 @@ private function processFunction(ProcessedFunctionType $function, Report $report */ private function processTests(array $tests): void { + $this->project->getWriter()->startElement('tests'); + $testsObject = $this->project->tests(); foreach ($tests as $test => $result) { $testsObject->addTest($test, $result); } + + $this->project->getWriter()->endElement(); } private function setTotals(AbstractNode $node, Totals $totals): void { + $totals->getWriter()->startElement('totals'); + $loc = $node->linesOfCode(); $totals->setNumLines( @@ -251,6 +290,16 @@ private function setTotals(AbstractNode $node, Totals $totals): void $node->numberOfExecutedLines(), ); + $totals->setNumMethods( + $node->numberOfMethods(), + $node->numberOfTestedMethods(), + ); + + $totals->setNumFunctions( + $node->numberOfFunctions(), + $node->numberOfTestedFunctions(), + ); + $totals->setNumClasses( $node->numberOfClasses(), $node->numberOfTestedClasses(), @@ -261,15 +310,7 @@ private function setTotals(AbstractNode $node, Totals $totals): void $node->numberOfTestedTraits(), ); - $totals->setNumMethods( - $node->numberOfMethods(), - $node->numberOfTestedMethods(), - ); - - $totals->setNumFunctions( - $node->numberOfFunctions(), - $node->numberOfTestedFunctions(), - ); + $totals->getWriter()->endElement(); } private function targetDirectory(): string @@ -285,12 +326,4 @@ private function targetFilePath(string $name): string return $filename; } - - /** - * @throws XmlException - */ - private function saveDocument(DOMDocument $document, string $name): void - { - Filesystem::write($this->targetFilePath($name), Xml::asString($document)); - } } diff --git a/src/Report/Xml/File.php b/src/Report/Xml/File.php index e6dd5c4ba..2d35582a8 100644 --- a/src/Report/Xml/File.php +++ b/src/Report/Xml/File.php @@ -9,66 +9,32 @@ */ namespace SebastianBergmann\CodeCoverage\Report\Xml; -use function assert; -use DOMDocument; -use DOMElement; -use DOMNode; +use XMLWriter; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage */ class File { - protected readonly DOMDocument $dom; - private readonly DOMElement $contextNode; - private ?DOMNode $lineCoverage = null; + protected XMLWriter $xmlWriter; - public function __construct(DOMElement $context) + public function __construct(XMLWriter $xmlWriter) { - $this->dom = $context->ownerDocument; - $this->contextNode = $context; + $this->xmlWriter = $xmlWriter; } - public function totals(): Totals + public function getWriter(): XMLWriter { - $totalsContainer = $this->contextNode->appendChild( - $this->dom->createElementNS( - Facade::XML_NAMESPACE, - 'totals', - ), - ); - - assert($totalsContainer instanceof DOMElement); - - return new Totals($totalsContainer); + return $this->xmlWriter; } - public function lineCoverage(string $line): Coverage + public function totals(): Totals { - if ($this->lineCoverage === null) { - $this->lineCoverage = $this->contextNode->appendChild( - $this->dom->createElementNS( - Facade::XML_NAMESPACE, - 'coverage', - ), - ); - } - assert($this->lineCoverage instanceof DOMElement); - - $lineNode = $this->lineCoverage->appendChild( - $this->dom->createElementNS( - Facade::XML_NAMESPACE, - 'line', - ), - ); - - assert($lineNode instanceof DOMElement); - - return new Coverage($lineNode, $line); + return new Totals($this->xmlWriter); } - protected function contextNode(): DOMElement + public function lineCoverage(string $line): Coverage { - return $this->contextNode; + return new Coverage($this->xmlWriter, $line); } } diff --git a/src/Report/Xml/Method.php b/src/Report/Xml/Method.php index 1b5bdb28f..965ad5259 100644 --- a/src/Report/Xml/Method.php +++ b/src/Report/Xml/Method.php @@ -9,17 +9,17 @@ */ namespace SebastianBergmann\CodeCoverage\Report\Xml; -use DOMElement; +use XMLWriter; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage */ final readonly class Method { - private DOMElement $contextNode; + private XMLWriter $xmlWriter; public function __construct( - DOMElement $context, + XMLWriter $xmlWriter, string $name, string $signature, string $start, @@ -29,21 +29,21 @@ public function __construct( string $coverage, string $crap ) { - $this->contextNode = $context; + $this->xmlWriter = $xmlWriter; - $this->contextNode->setAttribute('name', $name); - $this->contextNode->setAttribute('signature', $signature); + $this->xmlWriter->writeAttribute('name', $name); + $this->xmlWriter->writeAttribute('signature', $signature); - $this->contextNode->setAttribute('start', $start); + $this->xmlWriter->writeAttribute('start', $start); if ($end !== null) { - $this->contextNode->setAttribute('end', $end); + $this->xmlWriter->writeAttribute('end', $end); } - $this->contextNode->setAttribute('crap', $crap); + $this->xmlWriter->writeAttribute('crap', $crap); - $this->contextNode->setAttribute('executable', $executable); - $this->contextNode->setAttribute('executed', $executed); - $this->contextNode->setAttribute('coverage', $coverage); + $this->xmlWriter->writeAttribute('executable', $executable); + $this->xmlWriter->writeAttribute('executed', $executed); + $this->xmlWriter->writeAttribute('coverage', $coverage); } } diff --git a/src/Report/Xml/Node.php b/src/Report/Xml/Node.php index 86fe70df4..36b75bcfe 100644 --- a/src/Report/Xml/Node.php +++ b/src/Report/Xml/Node.php @@ -9,71 +9,37 @@ */ namespace SebastianBergmann\CodeCoverage\Report\Xml; -use function assert; -use DOMDocument; -use DOMElement; +use XMLWriter; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage */ abstract class Node { - protected readonly DOMDocument $dom; - private readonly DOMElement $contextNode; + protected readonly XMLWriter $xmlWriter; - public function __construct(DOMElement $context) + public function __construct(XMLWriter $xmlWriter) { - $this->dom = $context->ownerDocument; - $this->contextNode = $context; + $this->xmlWriter = $xmlWriter; } public function totals(): Totals { - $totalsContainer = $this->contextNode()->firstChild; - - if ($totalsContainer === null) { - $totalsContainer = $this->contextNode()->appendChild( - $this->dom->createElementNS( - Facade::XML_NAMESPACE, - 'totals', - ), - ); - } - - assert($totalsContainer instanceof DOMElement); - - return new Totals($totalsContainer); + return new Totals($this->xmlWriter); } - public function addDirectory(string $name): Directory + public function addDirectory(): Directory { - $dirNode = $this->dom->createElementNS( - Facade::XML_NAMESPACE, - 'directory', - ); - - $dirNode->setAttribute('name', $name); - $this->contextNode()->appendChild($dirNode); - - return new Directory($dirNode); + return new Directory($this->xmlWriter); } - public function addFile(string $name, string $href): File + public function addFile(): File { - $fileNode = $this->dom->createElementNS( - Facade::XML_NAMESPACE, - 'file', - ); - - $fileNode->setAttribute('name', $name); - $fileNode->setAttribute('href', $href); - $this->contextNode()->appendChild($fileNode); - - return new File($fileNode); + return new File($this->xmlWriter); } - protected function contextNode(): DOMElement + public function getWriter(): XMLWriter { - return $this->contextNode; + return $this->xmlWriter; } } diff --git a/src/Report/Xml/Project.php b/src/Report/Xml/Project.php index c81a6a933..750908c75 100644 --- a/src/Report/Xml/Project.php +++ b/src/Report/Xml/Project.php @@ -9,11 +9,9 @@ */ namespace SebastianBergmann\CodeCoverage\Report\Xml; -use function assert; use DateTimeImmutable; -use DOMDocument; -use DOMElement; use SebastianBergmann\Environment\Runtime; +use XMLWriter; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage @@ -22,19 +20,16 @@ final class Project extends Node { private readonly string $directory; - public function __construct(string $directory) + public function __construct(XMLWriter $xmlWriter, string $directory) { - $dom = new DOMDocument; - $dom->loadXML(''); + $this->directory = $directory; - parent::__construct( - $dom->getElementsByTagNameNS( - Facade::XML_NAMESPACE, - 'project', - )->item(0), - ); + parent::__construct($xmlWriter); - $this->directory = $directory; + $this->xmlWriter->startDocument(); + + $this->xmlWriter->startElement('phpunit'); + $this->xmlWriter->writeAttribute('xmlns', Facade::XML_NAMESPACE); } public function projectSourceDirectory(): string @@ -48,15 +43,8 @@ public function buildInformation( string $phpUnitVersion, string $coverageVersion ): void { - $buildNode = $this->dom->getElementsByTagNameNS( - Facade::XML_NAMESPACE, - 'build', - )->item(0); - - assert($buildNode instanceof DOMElement); - new BuildInformation( - $buildNode, + $this->xmlWriter, $runtime, $buildDate, $phpUnitVersion, @@ -66,22 +54,24 @@ public function buildInformation( public function tests(): Tests { - $testsNode = $this->contextNode()->appendChild( - $this->dom->createElementNS( - Facade::XML_NAMESPACE, - 'tests', - ), - ); - - assert($testsNode instanceof DOMElement); + return new Tests($this->xmlWriter); + } - return new Tests($testsNode); + public function getWriter(): XMLWriter + { + return $this->xmlWriter; } - public function asDom(): DOMDocument + public function startProject(): void { - $this->contextNode()->setAttribute('source', $this->directory); + $this->xmlWriter->startElement('project'); + $this->xmlWriter->writeAttribute('source', $this->directory); + } - return $this->dom; + public function finalize(): void + { + $this->xmlWriter->endElement(); + $this->xmlWriter->endDocument(); + $this->xmlWriter->flush(); } } diff --git a/src/Report/Xml/Report.php b/src/Report/Xml/Report.php index ee9b401de..8cdc71e44 100644 --- a/src/Report/Xml/Report.php +++ b/src/Report/Xml/Report.php @@ -9,11 +9,10 @@ */ namespace SebastianBergmann\CodeCoverage\Report\Xml; -use function assert; use function basename; use function dirname; use DOMDocument; -use DOMElement; +use XMLWriter; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage @@ -22,8 +21,9 @@ final class Report extends File { private readonly string $name; - public function __construct(string $name) + public function __construct(XMLWriter $xmlWriter, string $name) { + /* $dom = new DOMDocument; $dom->loadXML(''); @@ -31,18 +31,26 @@ public function __construct(string $name) Facade::XML_NAMESPACE, 'file', )->item(0); - - parent::__construct($contextNode); +*/ + parent::__construct($xmlWriter); $this->name = $name; + + $xmlWriter->startDocument(); + $xmlWriter->startElement('phpunit'); + $xmlWriter->writeAttribute('xmlns', Facade::XML_NAMESPACE); + $xmlWriter->startElement('file'); + $xmlWriter->writeAttribute('name', basename($this->name)); + $xmlWriter->writeAttribute('path', dirname($this->name)); } - public function asDom(): DOMDocument + public function finalize(): void { - $this->contextNode()->setAttribute('name', basename($this->name)); - $this->contextNode()->setAttribute('path', dirname($this->name)); + $this->xmlWriter->endElement(); + $this->xmlWriter->endElement(); - return $this->dom; + $this->xmlWriter->endDocument(); + $this->xmlWriter->flush(); } public function functionObject( @@ -55,17 +63,8 @@ public function functionObject( string $coverage, string $crap ): void { - $node = $this->contextNode()->appendChild( - $this->dom->createElementNS( - Facade::XML_NAMESPACE, - 'function', - ), - ); - - assert($node instanceof DOMElement); - new Method( - $node, + $this->xmlWriter, $name, $signature, $start, @@ -85,16 +84,7 @@ public function classObject( int $executed, float $crap ): Unit { - $node = $this->contextNode()->appendChild( - $this->dom->createElementNS( - Facade::XML_NAMESPACE, - 'class', - ), - ); - - assert($node instanceof DOMElement); - - return new Unit($node, $name, $namespace, $start, $executable, $executed, $crap); + return new Unit($this->xmlWriter, $name, $namespace, $start, $executable, $executed, $crap); } public function traitObject( @@ -105,29 +95,11 @@ public function traitObject( int $executed, float $crap ): Unit { - $node = $this->contextNode()->appendChild( - $this->dom->createElementNS( - Facade::XML_NAMESPACE, - 'trait', - ), - ); - - assert($node instanceof DOMElement); - - return new Unit($node, $name, $namespace, $start, $executable, $executed, $crap); + return new Unit($this->xmlWriter, $name, $namespace, $start, $executable, $executed, $crap); } public function source(): Source { - $source = $this->contextNode()->appendChild( - $this->dom->createElementNS( - Facade::XML_NAMESPACE, - 'source', - ), - ); - - assert($source instanceof DOMElement); - - return new Source($source); + return new Source($this->xmlWriter); } } diff --git a/src/Report/Xml/Source.php b/src/Report/Xml/Source.php index 698a71b6d..e82b2c382 100644 --- a/src/Report/Xml/Source.php +++ b/src/Report/Xml/Source.php @@ -9,33 +9,26 @@ */ namespace SebastianBergmann\CodeCoverage\Report\Xml; -use DOMElement; use TheSeer\Tokenizer\NamespaceUri; use TheSeer\Tokenizer\Tokenizer; use TheSeer\Tokenizer\XMLSerializer; +use XMLWriter; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage */ final readonly class Source { - private DOMElement $context; + private XMLWriter $xmlWriter; - public function __construct(DOMElement $context) + public function __construct(XMLWriter $xmlWriter) { - $this->context = $context; + $this->xmlWriter = $xmlWriter; } public function setSourceCode(string $source): void { - $context = $this->context; - $tokens = (new Tokenizer)->parse($source); - $srcDom = (new XMLSerializer(new NamespaceUri(Facade::XML_NAMESPACE)))->toDom($tokens); - - $context->parentNode->replaceChild( - $context->ownerDocument->importNode($srcDom->documentElement, true), - $context, - ); + (new XMLSerializer(new NamespaceUri(Facade::XML_NAMESPACE)))->appendToWriter($this->xmlWriter, $tokens); } } diff --git a/src/Report/Xml/Tests.php b/src/Report/Xml/Tests.php index 1760fdfa5..3fea6f4d3 100644 --- a/src/Report/Xml/Tests.php +++ b/src/Report/Xml/Tests.php @@ -9,9 +9,8 @@ */ namespace SebastianBergmann\CodeCoverage\Report\Xml; -use function assert; -use DOMElement; use SebastianBergmann\CodeCoverage\CodeCoverage; +use XMLWriter; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage @@ -20,11 +19,11 @@ */ final readonly class Tests { - private DOMElement $contextNode; + private readonly XMLWriter $xmlWriter; - public function __construct(DOMElement $context) + public function __construct(XMLWriter $xmlWriter) { - $this->contextNode = $context; + $this->xmlWriter = $xmlWriter; } /** @@ -32,17 +31,12 @@ public function __construct(DOMElement $context) */ public function addTest(string $test, array $result): void { - $node = $this->contextNode->appendChild( - $this->contextNode->ownerDocument->createElementNS( - Facade::XML_NAMESPACE, - 'test', - ), - ); + $this->xmlWriter->startElement('test'); - assert($node instanceof DOMElement); + $this->xmlWriter->writeAttribute('name', $test); + $this->xmlWriter->writeAttribute('size', $result['size']); + $this->xmlWriter->writeAttribute('status', $result['status']); - $node->setAttribute('name', $test); - $node->setAttribute('size', $result['size']); - $node->setAttribute('status', $result['status']); + $this->xmlWriter->endElement(); } } diff --git a/src/Report/Xml/Totals.php b/src/Report/Xml/Totals.php index 28612f7aa..b0c57ec30 100644 --- a/src/Report/Xml/Totals.php +++ b/src/Report/Xml/Totals.php @@ -10,106 +10,86 @@ namespace SebastianBergmann\CodeCoverage\Report\Xml; use function sprintf; -use DOMElement; use SebastianBergmann\CodeCoverage\Util\Percentage; +use XMLWriter; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage */ final readonly class Totals { - private DOMElement $linesNode; - private DOMElement $methodsNode; - private DOMElement $functionsNode; - private DOMElement $classesNode; - private DOMElement $traitsNode; + private XMLWriter $xmlWriter; - public function __construct(DOMElement $container) + public function __construct(XMLWriter $xmlWriter) { - $dom = $container->ownerDocument; - - $this->linesNode = $dom->createElementNS( - Facade::XML_NAMESPACE, - 'lines', - ); - - $this->methodsNode = $dom->createElementNS( - Facade::XML_NAMESPACE, - 'methods', - ); - - $this->functionsNode = $dom->createElementNS( - Facade::XML_NAMESPACE, - 'functions', - ); - - $this->classesNode = $dom->createElementNS( - Facade::XML_NAMESPACE, - 'classes', - ); - - $this->traitsNode = $dom->createElementNS( - Facade::XML_NAMESPACE, - 'traits', - ); - - $container->appendChild($this->linesNode); - $container->appendChild($this->methodsNode); - $container->appendChild($this->functionsNode); - $container->appendChild($this->classesNode); - $container->appendChild($this->traitsNode); + $this->xmlWriter = $xmlWriter; } public function setNumLines(int $loc, int $cloc, int $ncloc, int $executable, int $executed): void { - $this->linesNode->setAttribute('total', (string) $loc); - $this->linesNode->setAttribute('comments', (string) $cloc); - $this->linesNode->setAttribute('code', (string) $ncloc); - $this->linesNode->setAttribute('executable', (string) $executable); - $this->linesNode->setAttribute('executed', (string) $executed); - $this->linesNode->setAttribute( + $this->xmlWriter->startElement('lines'); + $this->xmlWriter->writeAttribute('total', (string) $loc); + $this->xmlWriter->writeAttribute('comments', (string) $cloc); + $this->xmlWriter->writeAttribute('code', (string) $ncloc); + $this->xmlWriter->writeAttribute('executable', (string) $executable); + $this->xmlWriter->writeAttribute('executed', (string) $executed); + $this->xmlWriter->writeAttribute( 'percent', $executable === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($executed, $executable)->asFloat()), ); + $this->xmlWriter->endElement(); } public function setNumClasses(int $count, int $tested): void { - $this->classesNode->setAttribute('count', (string) $count); - $this->classesNode->setAttribute('tested', (string) $tested); - $this->classesNode->setAttribute( + $this->xmlWriter->startElement('classes'); + $this->xmlWriter->writeAttribute('count', (string) $count); + $this->xmlWriter->writeAttribute('tested', (string) $tested); + $this->xmlWriter->writeAttribute( 'percent', $count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat()), ); + $this->xmlWriter->endElement(); } public function setNumTraits(int $count, int $tested): void { - $this->traitsNode->setAttribute('count', (string) $count); - $this->traitsNode->setAttribute('tested', (string) $tested); - $this->traitsNode->setAttribute( + $this->xmlWriter->startElement('traits'); + $this->xmlWriter->writeAttribute('count', (string) $count); + $this->xmlWriter->writeAttribute('tested', (string) $tested); + $this->xmlWriter->writeAttribute( 'percent', $count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat()), ); + $this->xmlWriter->endElement(); } public function setNumMethods(int $count, int $tested): void { - $this->methodsNode->setAttribute('count', (string) $count); - $this->methodsNode->setAttribute('tested', (string) $tested); - $this->methodsNode->setAttribute( + $this->xmlWriter->startElement('methods'); + $this->xmlWriter->writeAttribute('count', (string) $count); + $this->xmlWriter->writeAttribute('tested', (string) $tested); + $this->xmlWriter->writeAttribute( 'percent', $count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat()), ); + $this->xmlWriter->endElement(); } public function setNumFunctions(int $count, int $tested): void { - $this->functionsNode->setAttribute('count', (string) $count); - $this->functionsNode->setAttribute('tested', (string) $tested); - $this->functionsNode->setAttribute( + $this->xmlWriter->startElement('functions'); + $this->xmlWriter->writeAttribute('count', (string) $count); + $this->xmlWriter->writeAttribute('tested', (string) $tested); + $this->xmlWriter->writeAttribute( 'percent', $count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat()), ); + $this->xmlWriter->endElement(); + } + + public function getWriter(): XMLWriter + { + return $this->xmlWriter; } } diff --git a/src/Report/Xml/Unit.php b/src/Report/Xml/Unit.php index fa97909c2..bfc5029c4 100644 --- a/src/Report/Xml/Unit.php +++ b/src/Report/Xml/Unit.php @@ -9,18 +9,17 @@ */ namespace SebastianBergmann\CodeCoverage\Report\Xml; -use function assert; -use DOMElement; +use XMLWriter; /** * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage */ final readonly class Unit { - private DOMElement $contextNode; + private XMLWriter $xmlWriter; public function __construct( - DOMElement $context, + XMLWriter $xmlWriter, string $name, string $namespace, int $start, @@ -28,23 +27,17 @@ public function __construct( int $executed, float $crap ) { - $this->contextNode = $context; + $this->xmlWriter = $xmlWriter; - $this->contextNode->setAttribute('name', $name); - $this->contextNode->setAttribute('start', (string) $start); - $this->contextNode->setAttribute('executable', (string) $executable); - $this->contextNode->setAttribute('executed', (string) $executed); - $this->contextNode->setAttribute('crap', (string) $crap); + $this->xmlWriter->writeAttribute('name', $name); + $this->xmlWriter->writeAttribute('start', (string) $start); + $this->xmlWriter->writeAttribute('executable', (string) $executable); + $this->xmlWriter->writeAttribute('executed', (string) $executed); + $this->xmlWriter->writeAttribute('crap', (string) $crap); - $node = $this->contextNode->appendChild( - $this->contextNode->ownerDocument->createElementNS( - Facade::XML_NAMESPACE, - 'namespace', - ), - ); - assert($node instanceof DOMElement); - - $node->setAttribute('name', $namespace); + $this->xmlWriter->startElement('namespace'); + $this->xmlWriter->writeAttribute('name', $namespace); + $this->xmlWriter->endElement(); } public function addMethod( @@ -57,17 +50,8 @@ public function addMethod( string $coverage, string $crap ): void { - $node = $this->contextNode->appendChild( - $this->contextNode->ownerDocument->createElementNS( - Facade::XML_NAMESPACE, - 'method', - ), - ); - - assert($node instanceof DOMElement); - new Method( - $node, + $this->xmlWriter, $name, $signature, $start, diff --git a/tests/_files/Report/XML/CoverageForBankAccount/BankAccount.php.xml b/tests/_files/Report/XML/CoverageForBankAccount/BankAccount.php.xml index b49cdf8ed..fa5cab570 100644 --- a/tests/_files/Report/XML/CoverageForBankAccount/BankAccount.php.xml +++ b/tests/_files/Report/XML/CoverageForBankAccount/BankAccount.php.xml @@ -35,7 +35,7 @@ - + <?php diff --git a/tests/_files/Report/XML/CoverageForClassWithAnonymousFunction/source_with_class_and_anonymous_function.php.xml b/tests/_files/Report/XML/CoverageForClassWithAnonymousFunction/source_with_class_and_anonymous_function.php.xml index 95d121dc8..5f488ee82 100644 --- a/tests/_files/Report/XML/CoverageForClassWithAnonymousFunction/source_with_class_and_anonymous_function.php.xml +++ b/tests/_files/Report/XML/CoverageForClassWithAnonymousFunction/source_with_class_and_anonymous_function.php.xml @@ -38,7 +38,7 @@ - + <?php diff --git a/tests/_files/Report/XML/CoverageForFileWithIgnoredLines/source_with_ignore.php.xml b/tests/_files/Report/XML/CoverageForFileWithIgnoredLines/source_with_ignore.php.xml index b862f3d1e..89c105012 100644 --- a/tests/_files/Report/XML/CoverageForFileWithIgnoredLines/source_with_ignore.php.xml +++ b/tests/_files/Report/XML/CoverageForFileWithIgnoredLines/source_with_ignore.php.xml @@ -22,7 +22,7 @@ - + <?php