Skip to content

Commit 8e06b80

Browse files
committed
Update hook extraction script
1 parent 175175a commit 8e06b80

File tree

2 files changed

+135
-86
lines changed

2 files changed

+135
-86
lines changed

bin/extract-hooks.ini

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
namespace = Friends
2-
wiki_directory = ../../friends.wiki
2+
base_dir = ..
3+
wiki_directory = ../friends.wiki
34
github_url = https://github.com/akirk/friends/blob/main/
5+
section = dir
46

57
[hooks]
8+
ignore_regex = '/^activitypub_/'
69
ignore_filter[] = _get_template_part_
710
ignore_filter[] = _get_template_part
811
ignore_filter[] = _template_paths

bin/extract-hooks.php

Lines changed: 131 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -8,36 +8,65 @@
88
// phpcs:disable WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
99
// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_mkdir
1010
// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
11+
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
1112

1213
function sample_ini() {
1314
return <<<INI
1415
namespace = App_Namespace
15-
wiki_directory = ../../repo.wiki
16+
base_dir = ..
17+
wiki_directory = ../repo.wiki
1618
github_blob_url = https://github.com/username/repo/blob/main/
1719
[hooks]
1820
ignore_filter[] = filter_name1
1921
ignore_filter[] = filter_name2
2022
INI;
2123
}
22-
$ini_file = __DIR__ . '/extract-hooks.ini';
24+
$dirs = array( getcwd(), __DIR__ );
25+
if ( isset( $_SERVER['argv'][1] ) && file_exists( $_SERVER['argv'][1] ) ) {
26+
if ( is_dir( $_SERVER['argv'][1] ) ) {
27+
array_unshift( $dirs, $_SERVER['argv'][1] );
28+
} else {
29+
array_unshift( $dirs, dirname( $_SERVER['argv'][1] ) );
30+
}
31+
}
32+
33+
foreach ( $dirs as $dir ) {
34+
$ini_file = $dir . '/extract-hooks.ini';
35+
if ( file_exists( $ini_file ) ) {
36+
break;
37+
}
38+
}
39+
2340
if ( ! file_exists( $ini_file ) ) {
24-
echo 'Please create an extract-hooks.ini file in the same directory as this script. Example: ', PHP_EOL, esc_attr( sample_ini() );
41+
echo 'Please provide an extract-hooks.ini file in the current directory or the same directory as this script. Example: ', PHP_EOL, sample_ini(), PHP_EOL;
2542
exit( 1 );
2643
}
44+
echo 'Loading ', realpath( $ini_file ), PHP_EOL;
2745
$ini = parse_ini_file( $ini_file );
2846

29-
foreach ( array( 'namespace', 'wiki_directory', 'github_url' ) as $key ) {
47+
foreach ( array( 'namespace', 'base_dir', 'wiki_directory', 'github_url' ) as $key ) {
3048
if ( ! isset( $ini[ $key ] ) ) {
31-
echo 'Missing ini entry ', esc_attr( $key ), '. Example: ', PHP_EOL, esc_attr( sample_ini() );
49+
echo 'Missing ini entry ', $key, '. Example: ', PHP_EOL, sample_ini(), PHP_EOL;
3250
exit( 1 );
3351
}
3452
}
3553

3654
if ( empty( $ini['ignore_filter'] ) ) {
3755
$ini['ignore_filter'] = array();
3856
}
39-
40-
$base = dirname( __DIR__ );
57+
if ( empty( $ini['ignore_regex'] ) ) {
58+
$ini['ignore_regex'] = false;
59+
}
60+
if ( empty( $ini['section'] ) ) {
61+
$ini['section'] = 'file';
62+
}
63+
// if the base dir is not absolute (also on windows), prepend it with $dir.
64+
if ( '/' === substr( $ini['base_dir'], 0, 1 ) ) {
65+
$base = $ini['base_dir'];
66+
} else {
67+
$base = realpath( $dir . '/' . $ini['base_dir'] );
68+
}
69+
echo 'Scanning ', $base, PHP_EOL;
4170
$files = new RecursiveIteratorIterator(
4271
new RecursiveDirectoryIterator( $base ),
4372
RecursiveIteratorIterator::LEAVES_ONLY
@@ -101,27 +130,24 @@ function sample_ini() {
101130
}
102131

103132
if (
104-
$hook
105-
&& ! in_array( $hook, $ini['ignore_filter'] )
106-
&& ! preg_match( '/^(activitypub_)/', $hook )
107-
133+
$hook
134+
&& ! in_array( $hook, $ini['ignore_filter'] )
135+
&& ( ! $ini['ignore_regex'] || ! preg_match( $ini['ignore_regex'], $hook ) )
108136
) {
109137
if ( ! isset( $filters[ $hook ] ) ) {
110138
$filters[ $hook ] = array(
111-
'files' => array(),
112-
'section' => $main_dir,
113-
'type' => $token[1],
114-
'params' => array(),
115-
'signatures' => array(),
139+
'files' => array(),
140+
'section' => 'dir' === $ini['section'] ? $main_dir : basename( $file->getFilename() ),
141+
'type' => $token[1],
142+
'params' => array(),
143+
'comment' => '',
116144
);
117145
}
118146

119147
$ret = extract_vars( $filters[ $hook ]['params'], $tokens, $i );
120148
$filters[ $hook ]['files'][ $dir . '/' . $file->getFilename() . ':' . $token[2] ] = $ret[1];
121-
$filters[ $hook ]['base_dirs'][ $main_dir ] = true;
122149
$filters[ $hook ]['params'] = $ret[0];
123-
124-
$filters[ $hook ] = array_merge( parse_docblock( $comment, $filters[ $hook ]['params'] ), $filters[ $hook ] );
150+
$filters[ $hook ] = array_merge( $filters[ $hook ], parse_docblock( $comment, $filters[ $hook ]['params'] ) );
125151
}
126152
}
127153
}
@@ -142,7 +168,8 @@ function extract_vars( $params, $tokens, $i ) {
142168
$vars = array( '' );
143169
$signature = $tokens[ $i ][1];
144170
$line = $tokens[ $i ][2];
145-
for ( $j = $i + 1; $j < $i + 100; $j++ ) {
171+
$search_window = 50;
172+
for ( $j = $i + 1; $j < $i + $search_window; $j++ ) {
146173
if ( ! isset( $tokens[ $j ] ) ) {
147174
break;
148175
}
@@ -200,23 +227,29 @@ function extract_vars( $params, $tokens, $i ) {
200227
}
201228
}
202229
}
230+
if ( $j === $i + $search_window ) {
231+
$signature = rtrim( $signature ) . PHP_EOL . '// ...';
232+
}
203233
array_shift( $vars );
204234
foreach ( $vars as $k => $var ) {
205235
if ( isset( $params[ $k ] ) ) {
206-
if ( $params[ $k ] !== $var ) {
207-
$params[ $k ] .= '|' . $var;
236+
if ( ! in_array( $var, $params[ $k ] ) ) {
237+
$params[ $k ][] = $var;
208238
}
209239
} else {
210-
$params[ $k ] = $var;
240+
$params[ $k ] = array( $var );
211241
}
212242
}
213243
return array( $params, $signature );
214244
}
215245

216246
function parse_docblock( $raw_comment, $params ) {
247+
if ( preg_match( '#^([ \t]*\*\s*|//\s*)?Documented (in|at) #m', $raw_comment ) ) {
248+
return array();
249+
}
217250
// Adapted from https://github.com/kamermans/docblock-reflection.
218251
$tags = array();
219-
$lines = explode( PHP_EOL, trim( $raw_comment ) );
252+
$lines = array_filter( explode( PHP_EOL, trim( $raw_comment ) ) );
220253
$matches = null;
221254
$comment = '';
222255

@@ -233,7 +266,7 @@ function parse_docblock( $raw_comment, $params ) {
233266

234267
case 0:
235268
case 2:
236-
break;
269+
return array();
237270

238271
default:
239272
// Handle multi-line docblock.
@@ -244,18 +277,28 @@ function parse_docblock( $raw_comment, $params ) {
244277

245278
foreach ( $lines as $line ) {
246279
$line = preg_replace( '#^[ \t]*\* ?#', '', $line );
280+
if ( preg_match( '#^Documented (in|at) #', $line ) ) {
281+
return array();
282+
}
283+
284+
if ( preg_match( '#@(param)(.*)#', $line, $matches ) ) {
285+
$tag_value = \trim( $matches[2] );
247286

287+
// If this tag was already parsed, make its value an array.
288+
if ( isset( $tags['params'] ) ) {
289+
$tags['params'][] = array( $tag_value );
290+
} else {
291+
$tags['params'] = array( array( $tag_value ) );
292+
}
293+
continue;
294+
}
248295
if ( preg_match( '#@([^ ]+)(.*)#', $line, $matches ) ) {
249296
$tag_name = $matches[1] . 's';
250297
$tag_value = \trim( $matches[2] );
251298

252299
// If this tag was already parsed, make its value an array.
253300
if ( isset( $tags[ $tag_name ] ) ) {
254-
if ( ! \is_array( $tags[ $tag_name ] ) ) {
255-
$tags[ $tag_name ] = array( $tags[ $tag_name ] );
256-
}
257-
258-
$tags[ $tag_name ][] = $tag_value;
301+
$tags[ $tag_name ][] = array( $tag_value );
259302
} else {
260303
$tags[ $tag_name ] = $tag_value;
261304
}
@@ -270,6 +313,8 @@ function parse_docblock( $raw_comment, $params ) {
270313
foreach ( $params as $k => $param ) {
271314
if ( ! isset( $tags['params'][ $k ] ) ) {
272315
$tags['params'][ $k ] = $param;
316+
} elseif ( ! in_array( $tags['params'][ $k ], $param ) ) {
317+
$tags['params'][ $k ] = array_merge( $tags['params'][ $k ], $param );
273318
}
274319
}
275320

@@ -302,10 +347,9 @@ function parse_docblock( $raw_comment, $params ) {
302347
}
303348
$doc = '';
304349
$index .= "- [`$hook`]($hook)";
305-
306350
if ( ! empty( $data['comment'] ) ) {
307351
$index .= ' ' . strtok( $data['comment'], PHP_EOL );
308-
$doc .= PHP_EOL . $data['comment'] . PHP_EOL . PHP_EOL;
352+
$doc .= PHP_EOL . preg_replace( '/^Example:?$/m', '### Example' . PHP_EOL, $data['comment'] ) . PHP_EOL . PHP_EOL;
309353
}
310354

311355
$index .= PHP_EOL;
@@ -322,57 +366,59 @@ function parse_docblock( $raw_comment, $params ) {
322366
$params = "## Parameters\n";
323367
$first = false;
324368
$count = 0;
325-
foreach ( $data['params'] as $param ) {
326-
if ( false === strpos( $param, ' ' ) || false !== strpos( $param, '\'' ) ) {
327-
// This was an extracted variable, so let's create a parameter definition.
328-
$vars = explode( '|', $param );
329-
foreach ( array_keys( $vars ) as $k ) {
330-
if ( preg_match( '#array\(#', $vars[ $k ], $matches ) ) {
331-
$vars[ $k ] = '$array';
332-
}
333-
if ( preg_match( '#\$(?:[a-zA-Z0-9_]+)\[[\'"]([^\'"]+)[\'"]#', $vars[ $k ], $matches ) ) {
334-
$vars[ $k ] = '$' . $matches[1];
335-
}
336-
if ( preg_match( '#_[_xn]\(\s*([\'"][^\'"]+[\'"])#', $vars[ $k ], $matches ) ) {
337-
$vars[ $k ] = $matches[1];
338-
}
369+
foreach ( $data['params'] as $i => $vars ) {
370+
$param = false;
371+
foreach ( $vars as $k => $var ) {
372+
if ( false !== strpos( $var, ' ' ) && false === strpos( $var, '\'' ) ) {
373+
$param = $var;
374+
$p = preg_split( '/ +/', $param, 3 );
375+
$vars[ $k ] = $p[1];
339376
}
340-
341-
foreach ( $vars as $k => $var ) {
342-
if ( $var && trim( $var, '})]' ) === $var ) {
343-
unset( $vars[ $k ] );
344-
$type = 'unknown';
345-
if ( strlen( $var ) - strlen( trim( $var, '"\'' ) ) === 2 ) {
346-
$type = 'string';
347-
$var = '$string';
348-
} elseif ( is_numeric( $var ) ) {
349-
$type = 'int';
350-
$var = '$int';
351-
} elseif ( 'true' === $var || 'false' === $var ) {
352-
$type = 'bool';
353-
$var = '$' . $var;
354-
} elseif ( 'null' === $var ) {
355-
$type = 'null';
356-
$var = '$null';
357-
} elseif ( in_array( $var, array( '$url' ), true ) ) {
358-
$type = 'string';
359-
} elseif ( '$array' === $var ) {
360-
$type = 'array';
361-
$var = '$array';
362-
}
363-
$param = $type . ' ' . $var;
364-
if ( $vars ) {
365-
$param .= ' Other examples: `' . implode( '`, `', $vars ) . '`';
366-
}
367-
break;
368-
}
377+
}
378+
$type = 'unknown';
379+
380+
// This was an extracted variable, so let's create a parameter definition.
381+
foreach ( $vars as $k => $var ) {
382+
if ( preg_match( '#array\(#', $var, $matches ) ) {
383+
$type = 'array';
384+
$vars[ $k ] = '$array';
385+
} elseif ( preg_match( '#\$(?:[a-zA-Z0-9_]+)\[[\'"]([^\'"]+)[\'"]#', $var, $matches ) ) {
386+
$vars[ $k ] = '$' . $matches[1];
387+
} elseif ( preg_match( '#([a-zA-Z0-9_]+)\(\)#', $var, $matches ) ) {
388+
$vars[ $k ] = '$' . str_replace( 'wp_get_', '', $matches[1] );
389+
} elseif ( preg_match( '#\$(?:[a-zA-Z0-9_]+)->(.+)$#', $var, $matches ) ) {
390+
$vars[ $k ] = '$' . $matches[1];
391+
} elseif ( preg_match( '#_[_xn]\(\s*([\'"][^\'"]+[\'"])#', $var, $matches ) ) {
392+
$type = 'string';
393+
$vars[ $k ] = '$' . preg_replace( '/[^a-z0-9]/', '_', strtolower( trim( $matches[1], '"\'' ) ) );
394+
} elseif ( strlen( $var ) - strlen( trim( $var, '"\'' ) ) === 2 ) {
395+
$type = 'string';
396+
$vars[ $k ] = '$' . preg_replace( '/[^a-z0-9]/', '_', strtolower( trim( $var, '"\'' ) ) );
397+
} elseif ( is_numeric( $var ) ) {
398+
$type = 'int';
399+
$vars[ $k ] = '$int';
400+
} elseif ( 'true' === $var || 'false' === $var ) {
401+
$type = 'bool';
402+
$vars[ $k ] = '$' . $var;
403+
} elseif ( 'null' === $var ) {
404+
$vars[ $k ] = '$ret';
405+
} elseif ( in_array( $var, array( '$url' ), true ) ) {
406+
$type = 'string';
407+
} elseif ( '$array' === $var ) {
408+
$type = 'array';
409+
$vars[ $k ] = '$array';
369410
}
370-
if ( false === strpos( $param, ' ' ) ) {
371-
// If we didn't manage to find a type, let's use unknown.
372-
$param = 'unknown ' . $param;
411+
}
412+
if ( ! $param ) {
413+
$var = reset( $vars );
414+
$param = $type . ' ' . $var;
415+
$other = array_unique( array_diff( $vars, array( $param, $var, 'null' ) ) );
416+
if ( $other ) {
417+
$param .= ' Other variable names: `' . implode( '`, `', $other ) . '`';
373418
}
374419
}
375420

421+
376422
$count += 1;
377423
$p = preg_split( '/ +/', $param, 3 );
378424
if ( '\\' === substr( $p[0], 0, 1 ) ) {
@@ -419,13 +465,13 @@ function parse_docblock( $raw_comment, $params ) {
419465
}
420466
$signature .= PHP_EOL . ');';
421467

422-
$doc .= '### Generic Example' . PHP_EOL . PHP_EOL . '```php' . PHP_EOL . $signature . PHP_EOL . '```' . PHP_EOL . PHP_EOL;
468+
$doc .= '### Auto-generated Example' . PHP_EOL . PHP_EOL . '```php' . PHP_EOL . $signature . PHP_EOL . '```' . PHP_EOL . PHP_EOL;
423469
$doc .= $params . PHP_EOL . PHP_EOL;
424470
}
425471

426-
if ( ! empty( $data['return'] ) ) {
472+
if ( ! empty( $data['returns'] ) ) {
427473
$doc .= "## Returns\n";
428-
$p = preg_split( '/ +/', $data['return'], 2 );
474+
$p = preg_split( '/ +/', $data['returns'], 2 );
429475
if ( '\\' === substr( $p[0], 0, 1 ) ) {
430476
$p[0] = substr( $p[0], 1 );
431477
} elseif ( ! in_array( strtok( $p[0], '|' ), array( 'int', 'string', 'bool', 'array', 'unknown' ) ) && substr( $p[0], 0, 3 ) !== 'WP_' ) {
@@ -449,9 +495,9 @@ function parse_docblock( $raw_comment, $params ) {
449495
);
450496

451497
}
452-
file_put_contents(
453-
$docs . '/Hooks.md',
454-
$index
455-
);
498+
file_put_contents(
499+
$docs . '/Hooks.md',
500+
$index
501+
);
456502

457503
echo 'Genearated ' . count( $filters ) . ' hooks documentation files in ' . realpath( $docs ) . PHP_EOL; // phpcs:ignore

0 commit comments

Comments
 (0)