Skip to content

Commit

Permalink
Update hook extraction script
Browse files Browse the repository at this point in the history
  • Loading branch information
akirk committed Apr 8, 2024
1 parent 175175a commit 8e06b80
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 86 deletions.
5 changes: 4 additions & 1 deletion bin/extract-hooks.ini
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
namespace = Friends
wiki_directory = ../../friends.wiki
base_dir = ..
wiki_directory = ../friends.wiki
github_url = https://github.com/akirk/friends/blob/main/
section = dir

[hooks]
ignore_regex = '/^activitypub_/'
ignore_filter[] = _get_template_part_
ignore_filter[] = _get_template_part
ignore_filter[] = _template_paths
Expand Down
216 changes: 131 additions & 85 deletions bin/extract-hooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,65 @@
// phpcs:disable WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_mkdir
// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped

function sample_ini() {
return <<<INI
namespace = App_Namespace
wiki_directory = ../../repo.wiki
base_dir = ..
wiki_directory = ../repo.wiki
github_blob_url = https://github.com/username/repo/blob/main/
[hooks]
ignore_filter[] = filter_name1
ignore_filter[] = filter_name2
INI;
}
$ini_file = __DIR__ . '/extract-hooks.ini';
$dirs = array( getcwd(), __DIR__ );
if ( isset( $_SERVER['argv'][1] ) && file_exists( $_SERVER['argv'][1] ) ) {
if ( is_dir( $_SERVER['argv'][1] ) ) {
array_unshift( $dirs, $_SERVER['argv'][1] );
} else {
array_unshift( $dirs, dirname( $_SERVER['argv'][1] ) );
}
}

foreach ( $dirs as $dir ) {
$ini_file = $dir . '/extract-hooks.ini';
if ( file_exists( $ini_file ) ) {
break;
}
}

if ( ! file_exists( $ini_file ) ) {
echo 'Please create an extract-hooks.ini file in the same directory as this script. Example: ', PHP_EOL, esc_attr( sample_ini() );
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;
exit( 1 );
}
echo 'Loading ', realpath( $ini_file ), PHP_EOL;
$ini = parse_ini_file( $ini_file );

foreach ( array( 'namespace', 'wiki_directory', 'github_url' ) as $key ) {
foreach ( array( 'namespace', 'base_dir', 'wiki_directory', 'github_url' ) as $key ) {
if ( ! isset( $ini[ $key ] ) ) {
echo 'Missing ini entry ', esc_attr( $key ), '. Example: ', PHP_EOL, esc_attr( sample_ini() );
echo 'Missing ini entry ', $key, '. Example: ', PHP_EOL, sample_ini(), PHP_EOL;
exit( 1 );
}
}

if ( empty( $ini['ignore_filter'] ) ) {
$ini['ignore_filter'] = array();
}

$base = dirname( __DIR__ );
if ( empty( $ini['ignore_regex'] ) ) {
$ini['ignore_regex'] = false;
}
if ( empty( $ini['section'] ) ) {
$ini['section'] = 'file';
}
// if the base dir is not absolute (also on windows), prepend it with $dir.
if ( '/' === substr( $ini['base_dir'], 0, 1 ) ) {
$base = $ini['base_dir'];
} else {
$base = realpath( $dir . '/' . $ini['base_dir'] );
}
echo 'Scanning ', $base, PHP_EOL;
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator( $base ),
RecursiveIteratorIterator::LEAVES_ONLY
Expand Down Expand Up @@ -101,27 +130,24 @@ function sample_ini() {
}

if (
$hook
&& ! in_array( $hook, $ini['ignore_filter'] )
&& ! preg_match( '/^(activitypub_)/', $hook )

$hook
&& ! in_array( $hook, $ini['ignore_filter'] )
&& ( ! $ini['ignore_regex'] || ! preg_match( $ini['ignore_regex'], $hook ) )
) {
if ( ! isset( $filters[ $hook ] ) ) {
$filters[ $hook ] = array(
'files' => array(),
'section' => $main_dir,
'type' => $token[1],
'params' => array(),
'signatures' => array(),
'files' => array(),
'section' => 'dir' === $ini['section'] ? $main_dir : basename( $file->getFilename() ),
'type' => $token[1],
'params' => array(),
'comment' => '',
);
}

$ret = extract_vars( $filters[ $hook ]['params'], $tokens, $i );
$filters[ $hook ]['files'][ $dir . '/' . $file->getFilename() . ':' . $token[2] ] = $ret[1];
$filters[ $hook ]['base_dirs'][ $main_dir ] = true;
$filters[ $hook ]['params'] = $ret[0];

$filters[ $hook ] = array_merge( parse_docblock( $comment, $filters[ $hook ]['params'] ), $filters[ $hook ] );
$filters[ $hook ] = array_merge( $filters[ $hook ], parse_docblock( $comment, $filters[ $hook ]['params'] ) );
}
}
}
Expand All @@ -142,7 +168,8 @@ function extract_vars( $params, $tokens, $i ) {
$vars = array( '' );
$signature = $tokens[ $i ][1];
$line = $tokens[ $i ][2];
for ( $j = $i + 1; $j < $i + 100; $j++ ) {
$search_window = 50;
for ( $j = $i + 1; $j < $i + $search_window; $j++ ) {
if ( ! isset( $tokens[ $j ] ) ) {
break;
}
Expand Down Expand Up @@ -200,23 +227,29 @@ function extract_vars( $params, $tokens, $i ) {
}
}
}
if ( $j === $i + $search_window ) {
$signature = rtrim( $signature ) . PHP_EOL . '// ...';
}
array_shift( $vars );
foreach ( $vars as $k => $var ) {
if ( isset( $params[ $k ] ) ) {
if ( $params[ $k ] !== $var ) {
$params[ $k ] .= '|' . $var;
if ( ! in_array( $var, $params[ $k ] ) ) {
$params[ $k ][] = $var;
}
} else {
$params[ $k ] = $var;
$params[ $k ] = array( $var );
}
}
return array( $params, $signature );
}

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

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

case 0:
case 2:
break;
return array();

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

foreach ( $lines as $line ) {
$line = preg_replace( '#^[ \t]*\* ?#', '', $line );
if ( preg_match( '#^Documented (in|at) #', $line ) ) {
return array();
}

if ( preg_match( '#@(param)(.*)#', $line, $matches ) ) {
$tag_value = \trim( $matches[2] );

// If this tag was already parsed, make its value an array.
if ( isset( $tags['params'] ) ) {
$tags['params'][] = array( $tag_value );
} else {
$tags['params'] = array( array( $tag_value ) );
}
continue;
}
if ( preg_match( '#@([^ ]+)(.*)#', $line, $matches ) ) {
$tag_name = $matches[1] . 's';
$tag_value = \trim( $matches[2] );

// If this tag was already parsed, make its value an array.
if ( isset( $tags[ $tag_name ] ) ) {
if ( ! \is_array( $tags[ $tag_name ] ) ) {
$tags[ $tag_name ] = array( $tags[ $tag_name ] );
}

$tags[ $tag_name ][] = $tag_value;
$tags[ $tag_name ][] = array( $tag_value );
} else {
$tags[ $tag_name ] = $tag_value;
}
Expand All @@ -270,6 +313,8 @@ function parse_docblock( $raw_comment, $params ) {
foreach ( $params as $k => $param ) {
if ( ! isset( $tags['params'][ $k ] ) ) {
$tags['params'][ $k ] = $param;
} elseif ( ! in_array( $tags['params'][ $k ], $param ) ) {
$tags['params'][ $k ] = array_merge( $tags['params'][ $k ], $param );
}
}

Expand Down Expand Up @@ -302,10 +347,9 @@ function parse_docblock( $raw_comment, $params ) {
}
$doc = '';
$index .= "- [`$hook`]($hook)";

if ( ! empty( $data['comment'] ) ) {
$index .= ' ' . strtok( $data['comment'], PHP_EOL );
$doc .= PHP_EOL . $data['comment'] . PHP_EOL . PHP_EOL;
$doc .= PHP_EOL . preg_replace( '/^Example:?$/m', '### Example' . PHP_EOL, $data['comment'] ) . PHP_EOL . PHP_EOL;
}

$index .= PHP_EOL;
Expand All @@ -322,57 +366,59 @@ function parse_docblock( $raw_comment, $params ) {
$params = "## Parameters\n";
$first = false;
$count = 0;
foreach ( $data['params'] as $param ) {
if ( false === strpos( $param, ' ' ) || false !== strpos( $param, '\'' ) ) {
// This was an extracted variable, so let's create a parameter definition.
$vars = explode( '|', $param );
foreach ( array_keys( $vars ) as $k ) {
if ( preg_match( '#array\(#', $vars[ $k ], $matches ) ) {
$vars[ $k ] = '$array';
}
if ( preg_match( '#\$(?:[a-zA-Z0-9_]+)\[[\'"]([^\'"]+)[\'"]#', $vars[ $k ], $matches ) ) {
$vars[ $k ] = '$' . $matches[1];
}
if ( preg_match( '#_[_xn]\(\s*([\'"][^\'"]+[\'"])#', $vars[ $k ], $matches ) ) {
$vars[ $k ] = $matches[1];
}
foreach ( $data['params'] as $i => $vars ) {
$param = false;
foreach ( $vars as $k => $var ) {
if ( false !== strpos( $var, ' ' ) && false === strpos( $var, '\'' ) ) {
$param = $var;
$p = preg_split( '/ +/', $param, 3 );
$vars[ $k ] = $p[1];
}

foreach ( $vars as $k => $var ) {
if ( $var && trim( $var, '})]' ) === $var ) {
unset( $vars[ $k ] );
$type = 'unknown';
if ( strlen( $var ) - strlen( trim( $var, '"\'' ) ) === 2 ) {
$type = 'string';
$var = '$string';
} elseif ( is_numeric( $var ) ) {
$type = 'int';
$var = '$int';
} elseif ( 'true' === $var || 'false' === $var ) {
$type = 'bool';
$var = '$' . $var;
} elseif ( 'null' === $var ) {
$type = 'null';
$var = '$null';
} elseif ( in_array( $var, array( '$url' ), true ) ) {
$type = 'string';
} elseif ( '$array' === $var ) {
$type = 'array';
$var = '$array';
}
$param = $type . ' ' . $var;
if ( $vars ) {
$param .= ' Other examples: `' . implode( '`, `', $vars ) . '`';
}
break;
}
}
$type = 'unknown';

// This was an extracted variable, so let's create a parameter definition.
foreach ( $vars as $k => $var ) {
if ( preg_match( '#array\(#', $var, $matches ) ) {
$type = 'array';
$vars[ $k ] = '$array';
} elseif ( preg_match( '#\$(?:[a-zA-Z0-9_]+)\[[\'"]([^\'"]+)[\'"]#', $var, $matches ) ) {
$vars[ $k ] = '$' . $matches[1];
} elseif ( preg_match( '#([a-zA-Z0-9_]+)\(\)#', $var, $matches ) ) {
$vars[ $k ] = '$' . str_replace( 'wp_get_', '', $matches[1] );
} elseif ( preg_match( '#\$(?:[a-zA-Z0-9_]+)->(.+)$#', $var, $matches ) ) {
$vars[ $k ] = '$' . $matches[1];
} elseif ( preg_match( '#_[_xn]\(\s*([\'"][^\'"]+[\'"])#', $var, $matches ) ) {
$type = 'string';
$vars[ $k ] = '$' . preg_replace( '/[^a-z0-9]/', '_', strtolower( trim( $matches[1], '"\'' ) ) );
} elseif ( strlen( $var ) - strlen( trim( $var, '"\'' ) ) === 2 ) {
$type = 'string';
$vars[ $k ] = '$' . preg_replace( '/[^a-z0-9]/', '_', strtolower( trim( $var, '"\'' ) ) );
} elseif ( is_numeric( $var ) ) {
$type = 'int';
$vars[ $k ] = '$int';
} elseif ( 'true' === $var || 'false' === $var ) {
$type = 'bool';
$vars[ $k ] = '$' . $var;
} elseif ( 'null' === $var ) {
$vars[ $k ] = '$ret';
} elseif ( in_array( $var, array( '$url' ), true ) ) {
$type = 'string';
} elseif ( '$array' === $var ) {
$type = 'array';
$vars[ $k ] = '$array';
}
if ( false === strpos( $param, ' ' ) ) {
// If we didn't manage to find a type, let's use unknown.
$param = 'unknown ' . $param;
}
if ( ! $param ) {
$var = reset( $vars );
$param = $type . ' ' . $var;
$other = array_unique( array_diff( $vars, array( $param, $var, 'null' ) ) );
if ( $other ) {
$param .= ' Other variable names: `' . implode( '`, `', $other ) . '`';
}
}


$count += 1;
$p = preg_split( '/ +/', $param, 3 );
if ( '\\' === substr( $p[0], 0, 1 ) ) {
Expand Down Expand Up @@ -419,13 +465,13 @@ function parse_docblock( $raw_comment, $params ) {
}
$signature .= PHP_EOL . ');';

$doc .= '### Generic Example' . PHP_EOL . PHP_EOL . '```php' . PHP_EOL . $signature . PHP_EOL . '```' . PHP_EOL . PHP_EOL;
$doc .= '### Auto-generated Example' . PHP_EOL . PHP_EOL . '```php' . PHP_EOL . $signature . PHP_EOL . '```' . PHP_EOL . PHP_EOL;
$doc .= $params . PHP_EOL . PHP_EOL;
}

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

}
file_put_contents(
$docs . '/Hooks.md',
$index
);
file_put_contents(
$docs . '/Hooks.md',
$index
);

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

0 comments on commit 8e06b80

Please sign in to comment.