mirror of
https://github.com/php/doc-base.git
synced 2026-03-24 23:32:15 +01:00
338 lines
12 KiB
PHP
338 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* +----------------------------------------------------------------------+
|
|
* | Copyright (c) 1997-2023 The PHP Group |
|
|
* +----------------------------------------------------------------------+
|
|
* | This source file is subject to version 3.01 of the PHP license, |
|
|
* | that is bundled with this package in the file LICENSE, and is |
|
|
* | available through the world-wide-web at the following url: |
|
|
* | https://www.php.net/license/3_01.txt. |
|
|
* | If you did not receive a copy of the PHP license and are unable to |
|
|
* | obtain it through the world-wide-web, please send a note to |
|
|
* | license@php.net, so we can mail you a copy immediately. |
|
|
* +----------------------------------------------------------------------+
|
|
* | Authors: Kim Hallberg <work@hallberg.kim> |
|
|
* | Gina Peter Banyard <girgias@php.net> |
|
|
* +----------------------------------------------------------------------+
|
|
* | 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 <xi:include> tags to include the docs from the OO version */
|
|
'reference/parallel/functions/parallel.run.xml',
|
|
/* This page uses <xi:include> tags to include the docs from the OO version */
|
|
'reference/pdo/pdo/connect.xml',
|
|
/* These pages use <xi:include> 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('#<reference xml:id="(.+)" role="(class|enum)"#', $content)) {
|
|
return [];
|
|
}
|
|
/* Skip Exception classes definitions */
|
|
if (str_contains($content, '<phpdoc:exceptionref')) {
|
|
return [];
|
|
}
|
|
|
|
/* Skip aliases */
|
|
if (str_contains($content, '&info.function.alias;')
|
|
|| str_contains($content, '&Alias;')
|
|
|| str_contains($content, "&info.method.alias;")
|
|
) {
|
|
return [];
|
|
}
|
|
/* Skip undocumented functions (for now) */
|
|
if (str_contains($content, '&warn.undocumented.func;')) {
|
|
return [];
|
|
}
|
|
|
|
/* Constructors are special */
|
|
if (str_ends_with($path, 'construct.xml')) {
|
|
if (!str_contains($content, '<constructorsynopsis>') &&
|
|
!preg_match('/<constructorsynopsis role="[^"]*">/', $content)
|
|
) {
|
|
// This generates a lot of errors leave for later
|
|
//return ["Constructors should use <constructorsynopsis> instead of <methodsynopsis>"];
|
|
}
|
|
$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, '<destructorsynopsis>') &&
|
|
!preg_match('/<destructorsynopsis role="[^"]*">/', $content)) {
|
|
// Early bail-out
|
|
return ["Destructors should use <destructorsynopsis> instead of <methodsynopsis>"];
|
|
}
|
|
$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;
|
|
}
|