Skip to content

Commit f2ed3a6

Browse files
committed
fix(api): stop documenting imports, keep docblocks on guarded declarations
Two token-scanner bugs made the generated PHP API reference wrong. 'use function foo;' imports a function, it does not declare one, but the scanner took the name after any 'function' keyword. SearchWidget.php's import block alone produced ten phantom functions, namespaced to the importing file. Three reached the published site, two of them duplicate pages for gravityview_get_form_fields sitting beside the real one, each with no signature, description, or parameters. Separately, a docblock was dropped whenever a conditional-declaration guard sat between it and its symbol. WordPress code routinely writes 'if ( ! function_exists( 'x' ) ) { function x() {} }', and GravityView 3.0 moved connector-functions.php to src/Utils and adopted that idiom, so all 31 functions in it lost their prose, parameter types, returns, and — for gravityview_get_template_id — its 3.0.0 deprecation notice. Only *_exists() predicates bridge, so a file guard such as 'if ( ! defined( 'ABSPATH' ) )' still stops a docblock as before. Verified against GravityView develop: phantoms 10 -> 0 in that file, docblocks 0 -> 31 in connector-functions.php. Across all 37 products the published symbol set loses exactly the 3 phantoms and gains nothing else. Unit tests and a full build pass.
1 parent 29b3cc9 commit f2ed3a6

1 file changed

Lines changed: 84 additions & 0 deletions

File tree

scripts/extract-php-api.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,62 @@ function nextNonIgnorableIndex(array $tokens, int $from): int {
106106
return -1;
107107
}
108108

109+
/**
110+
* Highest line number in a token range. Single-character tokens carry no line,
111+
* so scanning to the last numbered token is the only way to date a range that
112+
* ends on one (`{`, `)`).
113+
*/
114+
function maxTokenLine(array $tokens, int $from, int $to): int {
115+
$line = 0;
116+
for ($i = $from; $i <= $to && $i < count($tokens); $i++) {
117+
$line = max($line, tokenLine($tokens[$i]));
118+
}
119+
return $line;
120+
}
121+
122+
/**
123+
* Index of the `{` opening a conditional-declaration guard, or -1.
124+
*
125+
* WordPress code routinely wraps a declaration in `if ( ! function_exists( 'x' ) ) { ... }`,
126+
* which puts the guard between a docblock and the symbol it documents. Only guards testing
127+
* a *_exists() predicate qualify; a file guard such as `if ( ! defined( 'ABSPATH' ) )`
128+
* deliberately does not, so its preceding docblock still stops there.
129+
*/
130+
function conditionalDeclarationGuardBrace(array $tokens, int $ifIndex): int {
131+
$count = count($tokens);
132+
$openIdx = nextNonIgnorableIndex($tokens, $ifIndex + 1);
133+
if ($openIdx < 0 || tokenText($tokens[$openIdx]) !== '(') {
134+
return -1;
135+
}
136+
137+
$depth = 0;
138+
$sawExistsPredicate = false;
139+
for ($i = $openIdx; $i < $count; $i++) {
140+
$text = tokenText($tokens[$i]);
141+
if ($text === '(') {
142+
$depth++;
143+
continue;
144+
}
145+
if ($text === ')') {
146+
$depth--;
147+
if ($depth === 0) {
148+
if (!$sawExistsPredicate) {
149+
return -1;
150+
}
151+
$braceIdx = nextNonIgnorableIndex($tokens, $i + 1);
152+
return ($braceIdx >= 0 && tokenText($tokens[$braceIdx]) === '{') ? $braceIdx : -1;
153+
}
154+
continue;
155+
}
156+
if (tokenId($tokens[$i]) === T_STRING
157+
&& in_array($text, ['function_exists', 'class_exists', 'interface_exists', 'trait_exists', 'enum_exists'], true)) {
158+
$sawExistsPredicate = true;
159+
}
160+
}
161+
162+
return -1;
163+
}
164+
109165
function normalizeSignature(string $sig): string {
110166
$sig = preg_replace('/\s+/', ' ', $sig ?? '') ?? '';
111167
$sig = trim($sig);
@@ -228,6 +284,7 @@ function extractSymbolsFromFile(string $filePath, string $root): array {
228284

229285
$pendingClassIndex = null;
230286
$classStack = []; // each: ['index' => int, 'braceLevel' => int]
287+
$inImportStatement = false;
231288

232289
$count = count($tokens);
233290
for ($i = 0; $i < $count; $i++) {
@@ -242,6 +299,29 @@ function extractSymbolsFromFile(string $filePath, string $root): array {
242299
continue;
243300
}
244301

302+
// `use function foo;` imports a function, it does not declare one. A closure's
303+
// `use (...)` clause is not an import, so it must not raise the flag. Only `;`
304+
// lowers it, which keeps a group import `use Foo\{function bar};` covered.
305+
if ($id === T_USE) {
306+
$afterUseIdx = nextNonIgnorableIndex($tokens, $i + 1);
307+
$inImportStatement = !($afterUseIdx >= 0 && tokenText($tokens[$afterUseIdx]) === '(');
308+
} elseif ($text === ';') {
309+
$inImportStatement = false;
310+
}
311+
312+
// A conditional-declaration guard sits between a docblock and the symbol it
313+
// documents; step over it so the docblock survives, and re-anchor the
314+
// proximity check to the guard's opening brace.
315+
if ($id === T_IF && $lastDoc !== null) {
316+
$guardBraceIdx = conditionalDeclarationGuardBrace($tokens, $i);
317+
if ($guardBraceIdx >= 0) {
318+
$lastDoc['endLine'] = max($lastDoc['endLine'], maxTokenLine($tokens, $i, $guardBraceIdx));
319+
$braceLevel++;
320+
$i = $guardBraceIdx;
321+
continue;
322+
}
323+
}
324+
245325
// If a docblock is followed by unrelated code (like file-guard `if (...)`),
246326
// prevent it from incorrectly attaching to the next symbol.
247327
if ($lastDoc && $id !== null && !isIgnorableToken($tok) && !in_array($id, $bridgingDocTokenIds, true) && $text !== '{' && $text !== '}' && $text !== ';') {
@@ -518,6 +598,10 @@ function extractSymbolsFromFile(string $filePath, string $root): array {
518598
}
519599

520600
if ($id === T_FUNCTION) {
601+
if ($inImportStatement) {
602+
continue;
603+
}
604+
521605
$nameIdx = nextNonIgnorableIndex($tokens, $i + 1);
522606
if ($nameIdx < 0) continue;
523607

0 commit comments

Comments
 (0)