| * | Gina Peter Banyard | * +----------------------------------------------------------------------+ * | Description: This file parses the manual and checks that the | * | necessary sections are present, and that their order | * | is correct. | * +----------------------------------------------------------------------+ */ const SKIP_FOLDER = [ /* Directory class refers to usual dir/ functions */ 'directory', /* We don't care about a tutorial doc in this script */ 'tutorial', /* Skip predefined variables folder */ 'variables', ]; /* Files which should be skipped, needs a *good* reason */ const SKIP_FILE = [ /* Helper page to refer to unset() or unlink() */ 'reference/filesystem/functions/delete.xml', /* die() is equivalent to the language construct exit() */ 'reference/misc/functions/die.xml', /* This old alias was split into two functions and current * alias detection doesn't find it... so add it here */ 'reference/oci8/oldaliases/ocifetchinto.xml', /* This page uses tags to include the docs from the OO version */ 'reference/parallel/functions/parallel.run.xml', /* This page uses tags to include the docs from the OO version */ 'reference/pdo/pdo/connect.xml', /* These pages use tags to include the docs from the interface version */ 'reference/dom/domcharacterdata/after.xml', 'reference/dom/domcharacterdata/remove.xml', 'reference/dom/domcharacterdata/replacewith.xml', 'reference/dom/domcharacterdata/before.xml', 'reference/dom/domelement/prepend.xml', 'reference/dom/domelement/after.xml', 'reference/dom/domelement/replacechildren.xml', 'reference/dom/domelement/remove.xml', 'reference/dom/domelement/replacewith.xml', 'reference/dom/domelement/append.xml', 'reference/dom/domelement/before.xml', 'reference/dom/domnamednodemap/getiterator.xml', 'reference/dom/domdocumentfragment/prepend.xml', 'reference/dom/domdocumentfragment/replacechildren.xml', 'reference/dom/domdocumentfragment/append.xml', 'reference/dom/domdocument/prepend.xml', 'reference/dom/domdocument/replacechildren.xml', 'reference/dom/domdocument/append.xml', 'reference/dom/domnodelist/getiterator.xml', ]; const VALID_SECTION_ROLES = [ 'description', 'parameters', 'returnvalues', 'errors', 'changelog', 'examples', 'notes', 'seealso', ]; define('DOCROOT_EN', dirname(__DIR__, 3) . '/en/'); $fileCount = 0; /* Path to the root of EN extension reference tree */ $directoryToSearch = DOCROOT_EN . 'reference'; foreach (new DirectoryIterator($directoryToSearch) as $docs) { if ($docs->isDot() || !$docs->isDir() || !$docs->isReadable()) { continue; } $fileCount += checkExtension($docs->getPathname()); } /* Check section order in predefined classes */ $fileCount += checkExtension(DOCROOT_EN . 'language/predefined'); //echo "\n\e[0;32mFound {$fileCount} files with issues.\n"; echo "\nFound {$fileCount} files with issues.\n"; exit ($fileCount > 0 ? 1 : 0); function checkExtension($dirname) { $fileCount = 0; $docdir = new RecursiveDirectoryIterator($dirname, FilesystemIterator::SKIP_DOTS); foreach ($docdir as $base) { if (!$base->isDir() || !$base->isReadable()) { continue; } // Skip folder dirs which refer to another folder if (\in_array($base->getBasename(), SKIP_FOLDER)) { continue; } foreach (getXMLFiles($dirname . '/' . $base->getFilename()) as $path => $file) { if (!file_exists($path)) { continue; } if ($errors = checkSectionErrors($path)) { $fileCount++; foreach ($errors as $error) { //echo "\e[0;31mFile \e[0m{$path}\e[0;31m $error.\n"; echo "File {$path} $error.\n"; } } } } return $fileCount; } function getXMLFiles(string $dirname) { $directory = new RecursiveDirectoryIterator($dirname, FilesystemIterator::SKIP_DOTS); $files = []; foreach ($directory as $dir) { if ($dir->isDir() || !$dir->isReadable()) { continue; } if ($dir->getExtension() !== 'xml') { continue; } if (str_starts_with($dir->getFilename(), 'entities')) { continue; } /* Skip certain files */ $pathnameFromRoot = str_replace([DOCROOT_EN, '\\'], ['', '/'], $dir->getPathname()); if (in_array($pathnameFromRoot, SKIP_FILE)) { continue; } $files[$dir->getPathname()] = $dir->getFilename(); } return $files; } /** Section order * - description * - parameters * - returnvalues * - errors * - changelog * - examples * - notes * - seealso */ function checkSectionErrors(string $path): array { $pageHasNoReturnSection = false; $content = file_get_contents($path); /* Skip class or enum definitions */ if (preg_match('#') && !preg_match('//', $content) ) { // This generates a lot of errors leave for later //return ["Constructors should use instead of "]; } $pageHasNoReturnSection = true; /* Check if it has procedural constructor documented */ if (str_contains($content, '&style.procedural;')) { $pageHasNoReturnSection = false; } } /* Destructors are special */ if (str_ends_with($path, 'destruct.xml')) { if (!str_contains($content, '') && !preg_match('//', $content)) { // Early bail-out return ["Destructors should use instead of "]; } $pageHasNoReturnSection = true; } $dom = new DOMDocument(); /* Load as HTML as to not verify entities */ @$dom->loadHTML($content); return checkCommonSectionOrder($dom, $pageHasNoReturnSection); } function checkCommonSectionOrder(DOMDocument $document, bool $hasNotReturnValueSection): array { $errors = []; $elements = []; foreach ($document->getElementsByTagName('refsect1') as $node) { $role = $node->getAttribute('role'); if (!in_array($role, VALID_SECTION_ROLES)) { $errors[] = "Invalid section role: '$role'"; continue; } if (in_array($role, $elements)) { $errors[] = "Duplicate section: '$role'"; continue; } $elements[] = $role; } if ($elements === []) { $errors[] = "No sections"; return $errors; } if (!in_array('description', $elements)) { $errors[] = "No description sections"; } if (!in_array('parameters', $elements)) { $errors[] = "No parameters sections"; } if (!in_array('returnvalues', $elements)) { // Constructor&Destructor pages should not have a return value section if (!$hasNotReturnValueSection) { $errors[] = "No returnvalues sections"; } } else { /* Generates a lot of issues, * need to confirm constructors which share page with procedural aren't error-ing by mistake * bail out for now */ if ($hasNotReturnValueSection) { return $errors; $errors[] = "Return values sections should not be present for constructors/destructors"; } } /* There are bigger issues then section order so return early */ if ($errors) return $errors; if ($elements[0] !== 'description') { $errors[] = "Description sections is not first"; } if ($elements[1] !== 'parameters') { $errors[] = "Parameters sections is not second"; } // Check only for non constructor/destructor pages if (!$hasNotReturnValueSection && $elements[2] !== 'returnvalues') { $errors[] = "Return values sections is not third"; } /* if an error section is present it must be the 4th element * if the page is a constructor/destructor it must be the 3rd element */ if (in_array('errors', $elements) && $elements[3-$hasNotReturnValueSection] !== 'errors') { $errors[] = "Errors sections is not " . ($hasNotReturnValueSection ? 'third' : 'fourth'); } /* if a See Also section is present it must be the last element */ if (in_array('seealso', $elements) && $elements[array_key_last($elements)] !== 'seealso') { $errors[] = "See also sections is not last"; } $flipped = array_flip($elements); if (in_array('errors', $elements) && in_array('changelog', $elements)) { if ($flipped['errors'] > $flipped['changelog']) { $errors[] = "Changelog section before errors"; } } /* Check example section is in correct position */ if (in_array('changelog', $elements) && in_array('examples', $elements)) { if ($flipped['changelog'] > $flipped['examples']) { $errors[] = "Example section before changelog"; } } if (in_array('errors', $elements) && in_array('examples', $elements)) { if ($flipped['errors'] > $flipped['examples']) { $errors[] = "Examples section before errors"; } } /* Check notes section is in correct position */ if (in_array('changelog', $elements) && in_array('notes', $elements)) { if ($flipped['changelog'] > $flipped['notes']) { $errors[] = "Notes section before changelog"; } } if (in_array('errors', $elements) && in_array('notes', $elements)) { if ($flipped['errors'] > $flipped['notes']) { $errors[] = "Notes section before errors"; } } if (in_array('examples', $elements) && in_array('notes', $elements)) { if ($flipped['examples'] > $flipped['notes']) { $errors[] = "Notes section before examples"; } } return $errors; }