Skip to content

Add support for replacing class synopses based on stubs #7340

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 13, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 139 additions & 28 deletions build/gen_stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -1494,7 +1494,7 @@ public function getClassSynopsisElement(DOMDocument $doc, array $classMap): ?DOM
$parentClassName = self::getClassSynopsisFilename($parent);
$includeElement = $this->createIncludeElement(
$doc,
"xmlns(db=http://docbook.org/ns/docbook) xpointer(id('class.$parentClassName')/db:partintro/db:section/db:classsynopsis/db:fieldsynopsis[preceding-sibling::db:classsynopsisinfo[1][@role='comment' and text()='&Properties;']]))"
"xmlns(db=http://docbook.org/ns/docbook) xpointer(id('class.$parentClassName')/db:partintro/db:section/db:classsynopsis/db:fieldsynopsis[preceding-sibling::db:classsynopsisinfo[1][@role='comment' and text()='Properties']]))"
);
$classSynopsis->appendChild($includeElement);
}
Expand All @@ -1512,7 +1512,7 @@ public function getClassSynopsisElement(DOMDocument $doc, array $classMap): ?DOM
$classSynopsis->appendChild(new DOMText("\n "));
$includeElement = $this->createIncludeElement(
$doc,
"xmlns(db=http://docbook.org/ns/docbook) xpointer(id('class.$className')/db:refentry/db:refsect1[@role='description']/descendant::db:constructorsynopsis[not(@role='procedural')]"
"xmlns(db=http://docbook.org/ns/docbook) xpointer(id('class.$className')/db:refentry/db:refsect1[@role='description']/descendant::db:constructorsynopsis[not(@role='procedural')])"
);
$classSynopsis->appendChild($includeElement);
}
Expand All @@ -1530,7 +1530,7 @@ public function getClassSynopsisElement(DOMDocument $doc, array $classMap): ?DOM
$classSynopsis->appendChild(new DOMText("\n "));
$includeElement = $this->createIncludeElement(
$doc,
"xmlns(db=http://docbook.org/ns/docbook) xpointer(id('class.$className')/db:refentry/db:refsect1[@role='description']/descendant::db:destructorsynopsis[not(@role='procedural')]"
"xmlns(db=http://docbook.org/ns/docbook) xpointer(id('class.$className')/db:refentry/db:refsect1[@role='description']/descendant::db:destructorsynopsis[not(@role='procedural')])"
);
$classSynopsis->appendChild($includeElement);
}
Expand Down Expand Up @@ -2440,7 +2440,113 @@ function generateClassSynopses(array $classMap): array {
*/
function replaceClassSynopses(string $targetDirectory, array $classMap): array
{
throw new Exception("Not yet implemented!");
$classSynopses = [];

$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($targetDirectory),
RecursiveIteratorIterator::LEAVES_ONLY
);

foreach ($it as $file) {
$pathName = $file->getPathName();
if (!preg_match('/\.xml$/i', $pathName)) {
continue;
}

$xml = file_get_contents($pathName);
if ($xml === false) {
continue;
}

if (stripos($xml, "<classsynopsis") === false) {
continue;
}

$replacedXml = getReplacedSynopsisXml($xml);

$doc = new DOMDocument();
$doc->formatOutput = false;
$doc->preserveWhiteSpace = true;
$doc->validateOnParse = true;
$success = $doc->loadXML($replacedXml);
if (!$success) {
echo "Failed opening $pathName\n";
continue;
}

$classSynopsisElements = [];
foreach ($doc->getElementsByTagName("classsynopsis") as $element) {
$classSynopsisElements[] = $element;
}

foreach ($classSynopsisElements as $classSynopsis) {
if (!$classSynopsis instanceof DOMElement) {
continue;
}

$firstChild = $classSynopsis->firstElementChild;
if ($firstChild === null) {
continue;
}
$firstChild = $firstChild->firstElementChild;
if ($firstChild === null) {
continue;
}
$className = $firstChild->textContent;
if (!isset($classMap[$className])) {
continue;
}
$classInfo = $classMap[$className];

$newClassSynopsis = $classInfo->getClassSynopsisElement($doc, $classMap);
if ($newClassSynopsis === null) {
continue;
}

// Check if there is any change - short circuit if there is not any.

if (replaceAndCompareXmls($doc, $classSynopsis, $newClassSynopsis)) {
continue;
}

// Return the updated XML

$replacedXml = $doc->saveXML();

$replacedXml = preg_replace(
[
"/REPLACED-ENTITY-([A-Za-z0-9._{}%-]+?;)/",
"/<phpdoc:classref\s+xmlns:phpdoc=\"([a-z0-9.:\/]+)\"\s+xmlns=\"([a-z0-9.:\/]+)\"\s+xmlns:xi=\"([a-z0-9.:\/]+)\"\s+xml:id=\"([a-z0-9._-]+)\"\s*>/i",
"/<phpdoc:classref\s+xmlns:phpdoc=\"([a-z0-9.:\/]+)\"\s+xmlns=\"([a-z0-9.:\/]+)\"\s+xmlns:xlink=\"([a-z0-9.:\/]+)\"\s+xmlns:xi=\"([a-z0-9.:\/]+)\"\s+xml:id=\"([a-z0-9._-]+)\"\s*>/i",
],
[
"&$1",
"<phpdoc:classref xml:id=\"$4\" xmlns:phpdoc=\"$1\" xmlns=\"$2\" xmlns:xi=\"$4\">",
"<phpdoc:classref xml:id=\"$5\" xmlns:phpdoc=\"$1\" xmlns=\"$2\" xmlns:xlink=\"$3\" xmlns:xi=\"$4\">",
],
$replacedXml
);

$classSynopses[$pathName] = $replacedXml;
}
}

return $classSynopses;
}

function getReplacedSynopsisXml(string $xml): string
{
return preg_replace(
[
"/&([A-Za-z0-9._{}%-]+?;)/",
"/<(\/)*xi:([A-Za-z]+?)/"
],
[
"REPLACED-ENTITY-$1",
"<$1XI$2",
],
$xml
);
}

/**
Expand Down Expand Up @@ -2489,7 +2595,7 @@ function replaceMethodSynopses(string $targetDirectory, array $funcMap, array $a
continue;
}

$replacedXml = preg_replace("/&([A-Za-z0-9._{}%-]+?;)/", "REPLACED-ENTITY-$1", $xml);
$replacedXml = getReplacedSynopsisXml($xml);

$doc = new DOMDocument();
$doc->formatOutput = false;
Expand All @@ -2501,10 +2607,6 @@ function replaceMethodSynopses(string $targetDirectory, array $funcMap, array $a
continue;
}

$docComparator = new DOMDocument();
$docComparator->preserveWhiteSpace = false;
$docComparator->formatOutput = true;

$methodSynopsisElements = [];
foreach ($doc->getElementsByTagName("constructorsynopsis") as $element) {
$methodSynopsisElements[] = $element;
Expand Down Expand Up @@ -2568,19 +2670,7 @@ function replaceMethodSynopses(string $targetDirectory, array $funcMap, array $a

// Check if there is any change - short circuit if there is not any.

$xml1 = $doc->saveXML($methodSynopsis);
$xml1 = preg_replace("/&([A-Za-z0-9._{}%-]+?;)/", "REPLACED-ENTITY-$1", $xml1);
$docComparator->loadXML($xml1);
$xml1 = $docComparator->saveXML();

$methodSynopsis->parentNode->replaceChild($newMethodSynopsis, $methodSynopsis);

$xml2 = $doc->saveXML($newMethodSynopsis);
$xml2 = preg_replace("/&([A-Za-z0-9._{}%-]+?;)/", "REPLACED-ENTITY-$1", $xml2);
$docComparator->loadXML($xml2);
$xml2 = $docComparator->saveXML();

if ($xml1 === $xml2) {
if (replaceAndCompareXmls($doc, $methodSynopsis, $newMethodSynopsis)) {
continue;
}

Expand Down Expand Up @@ -2631,6 +2721,28 @@ function replaceMethodSynopses(string $targetDirectory, array $funcMap, array $a
return $methodSynopses;
}

function replaceAndCompareXmls(DOMDocument $doc, DOMElement $originalSynopsis, DOMElement $newSynopsis): bool
{
$docComparator = new DOMDocument();
$docComparator->preserveWhiteSpace = false;
$docComparator->formatOutput = true;

$xml1 = $doc->saveXML($originalSynopsis);
$xml1 = getReplacedSynopsisXml($xml1);
$docComparator->loadXML($xml1);
$xml1 = $docComparator->saveXML();

$originalSynopsis->parentNode->replaceChild($newSynopsis, $originalSynopsis);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be in here? This looks unrelated to the comparison.

Copy link
Member Author

@kocsismate kocsismate Aug 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, even though it makes the function have a side-effect, it's still absolutely required (I tried to clone $originalSynopsis before in order to prevent this, but it didn't work) :/

The reason why we need this code is that it attaches the $newSynopsis element to $doc by replacing $originalSynopsis.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, would using DOMDocument::importNode work? Maybe it's possible to directly import it to $docComparator even?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just tried this:

$docComparator = new DOMDocument();
$docComparator->preserveWhiteSpace = false;
$docComparator->formatOutput = true;
$docComparator->loadXML('<root></root>');

$originalSynopsis = $docComparator->importNode($originalSynopsis, true);
$docComparator->documentElement->appendChild($originalSynopsis);
$xml1 = $docComparator->saveXML($originalSynopsis);

$newSynopsis = $docComparator->importNode($newSynopsis, true);
$docComparator->documentElement->appendChild($newSynopsis);
$newSynopsis->setAttribute("xmlns", "http://docbook.org/ns/docbook");
$xml2 = $docComparator->saveXML($newSynopsis);
$xml2 = getReplacedSynopsisXml($xml2);

return $xml1 === $xml2;

But it still doesn't disregard whitespace changes - I guess because $originalSynopsis keeps the whitespaces. As far as I see that's why we have to first convert the 2 DOMElements to strings, and then load them to the comparator.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, let's stick with what you have then. Maybe rename the function to make it clear that it also modifies, something like replaceAndCompareXml()? Can't think of a great name...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replaceAndCompareXml() is not that bad of a name - I don't have any better suggestion.


$xml2 = $doc->saveXML($newSynopsis);
$xml2 = getReplacedSynopsisXml($xml2);

$docComparator->loadXML($xml2);
$xml2 = $docComparator->saveXML();

return $xml1 === $xml2;
}

function installPhpParser(string $version, string $phpParserDir) {
$lockFile = __DIR__ . "/PHP-Parser-install-lock";
$lockFd = fopen($lockFile, 'w+');
Expand Down Expand Up @@ -2715,13 +2827,12 @@ function initPhpParser() {
$context->forceRegeneration = isset($options["f"]) || isset($options["force-regeneration"]);
$context->forceParse = $context->forceRegeneration || $printParameterStats || $verify || $generateClassSynopses || $replaceClassSynopses || $generateMethodSynopses || $replaceMethodSynopses;

$targetClassSynopses = $argv[$optind + 1] ?? null;
if ($replaceClassSynopses && $targetClassSynopses === null) {
$targetSynopses = $argv[$argc - 1] ?? null;
if ($replaceClassSynopses && $targetSynopses === null) {
die("A target class synopsis directory must be provided for.\n");
}

$targetMethodSynopses = $argv[$optind + 1 + ($targetClassSynopses !== null)] ?? null;
if ($replaceMethodSynopses && $targetMethodSynopses === null) {
if ($replaceMethodSynopses && $targetSynopses === null) {
die("A target method synopsis directory must be provided.\n");
}

Expand Down Expand Up @@ -2884,7 +2995,7 @@ function(?ArgInfo $aliasArg, ?ArgInfo $aliasedArg) use ($aliasFunc, $aliasedFunc
}

if ($replaceClassSynopses) {
$classSynopses = replaceClassSynopses($targetClassSynopses, $classMap);
$classSynopses = replaceClassSynopses($targetSynopses, $classMap);

foreach ($classSynopses as $filename => $content) {
if (file_put_contents($filename, $content)) {
Expand Down Expand Up @@ -2912,7 +3023,7 @@ function(?ArgInfo $aliasArg, ?ArgInfo $aliasedArg) use ($aliasFunc, $aliasedFunc
}

if ($replaceMethodSynopses) {
$methodSynopses = replaceMethodSynopses($targetMethodSynopses, $funcMap, $aliasMap);
$methodSynopses = replaceMethodSynopses($targetSynopses, $funcMap, $aliasMap);

foreach ($methodSynopses as $filename => $content) {
if (file_put_contents($filename, $content)) {
Expand Down