Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions interface/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ export interface Hook {
* The number of arguments passed to the hook
*/
args: number;
/**
* The version the hook was deprecated in, for deprecated hooks
*/
deprecated_version?: string;
/**
* The name of the hook that should be used instead, for deprecated hooks
*/
deprecated_replacement?: string;
/**
* The message to accompany the deprecation notice, for deprecated hooks
*/
deprecated_message?: string;
}
/**
* The docblock information for the hook
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@johnbillion/wp-hooks-generator",
"version": "1.0.2",
"version": "1.1.0",
"description": "Generates a JSON representation of the WordPress actions and filters in your code",
"private": true,
"repository": {
Expand Down
32 changes: 32 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,38 @@ You can ignore hooks in two ways:
}
```

## Including Deprecated Hooks

Hooks fired via `do_action_deprecated()` and `apply_filters_deprecated()` are omitted by default. You can include them in two ways:

### On the Command Line

./vendor/bin/wp-hooks-generator --input=src --output=hooks --include-deprecated

### In composer.json

```json
"extra": {
"wp-hooks": {
"include-deprecated": true
}
}
```

Deprecated hooks are written to `actions.json` and `filters.json` alongside the other hooks, with a type of `action_deprecated` or `filter_deprecated` so they can be identified:

```php
$deprecated = array_filter( $filters, function( array $hook ) : bool {
return ( 'filter_deprecated' === $hook['type'] );
} );
```

They also carry the deprecation information passed to the function call:

* `deprecated_version`: The version the hook was deprecated in.
* `deprecated_replacement`: The name of the hook that should be used instead, if one was given.
* `deprecated_message`: The message that accompanies the deprecation notice, if one was given.

## TypeScript Interfaces for the Hook Files

The TypeScript interfaces for the hook files can be found in [`interface/index.d.ts`](interface/index.d.ts). Usage:
Expand Down
27 changes: 25 additions & 2 deletions schema.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://github.com/wp-hooks/generator/blob/1.0.2/schema.json",
"$id": "https://github.com/wp-hooks/generator/blob/1.1.0/schema.json",
"title": "HooksContainer",
"description": "The container for the list of hooks",
"type": "object",
Expand Down Expand Up @@ -62,7 +62,9 @@
"action",
"filter",
"action_reference",
"filter_reference"
"filter_reference",
"action_deprecated",
"filter_deprecated"
]
},
"doc": {
Expand Down Expand Up @@ -169,6 +171,27 @@
"args": {
"description": "The number of arguments passed to the hook",
"type": "integer"
},
"deprecated_version": {
"description": "The version the hook was deprecated in, for deprecated hooks",
"type": "string",
"examples": [
"4.6.0"
]
},
"deprecated_replacement": {
"description": "The name of the hook that should be used instead, for deprecated hooks",
"type": "string",
"examples": [
"document_title"
]
},
"deprecated_message": {
"description": "The message to accompany the deprecation notice, for deprecated hooks",
"type": "string",
"examples": [
"Use the document_title filter instead."
]
}
}
}
Expand Down
157 changes: 148 additions & 9 deletions src/generate.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@
"output:",
"ignore-files::",
"ignore-hooks::",
"include-deprecated",
] );

if ( empty( $options['input' ] ) || empty( $options['output'] ) ) {
printf(
"Usage: %s --input=src --output=hooks [--ignore-files=ignore/this,ignore/that] [--ignore-hooks=this_hook,that_hook] \n",
"Usage: %s --input=src --output=hooks [--ignore-files=ignore/this,ignore/that] [--ignore-hooks=this_hook,that_hook] [--include-deprecated] \n",
$argv[0]
);
exit( 1 );
Expand All @@ -40,6 +41,9 @@
$options['ignore-hooks'] = explode( ',', $options['ignore-hooks'] );
}

// getopt() sets the value to false for valueless flags, so check for array key existence and the false value.
$options['include-deprecated'] = array_key_exists( 'include-deprecated', $options ) && false === $options['include-deprecated'];

$config = ( file_exists( 'composer.json' ) ? json_decode( file_get_contents( 'composer.json' ) ) : false );

if ( ! empty( $config ) && ! empty( $config->extra ) && ! empty( $config->extra->{"wp-hooks"} ) ) {
Expand All @@ -52,6 +56,11 @@
if ( empty( $options['ignore-hooks'] ) && ! empty( $config->extra->{"wp-hooks"}->{"ignore-hooks"} ) ) {
$options['ignore-hooks'] = array_values( $config->extra->{"wp-hooks"}->{"ignore-hooks"} );
}

// Read include-deprecated from Composer config:
if ( empty( $options['include-deprecated'] ) && ! empty( $config->extra->{"wp-hooks"}->{"include-deprecated"} ) ) {
$options['include-deprecated'] = true;
}
}

if ( empty( $options['ignore-files'] ) ) {
Expand All @@ -66,6 +75,7 @@
$target_dir = $options['output'];
$ignore_files = $options['ignore-files'];
$ignore_hooks = $options['ignore-hooks'];
$include_deprecated = $options['include-deprecated'];

if ( ! file_exists( $source_dir ) ) {
printf(
Expand Down Expand Up @@ -192,9 +202,10 @@ public function enterNode(Node $node) {
* @param array<int,string> $files
* @param string $root
* @param array<int,string> $ignore_hooks
* @param bool $include_deprecated
* @return array
*/
function hooks_parse_files( array $files, string $root, array $ignore_hooks ) : array {
function hooks_parse_files( array $files, string $root, array $ignore_hooks, bool $include_deprecated = false ) : array {
$output = array();

// Create a new parser instance
Expand All @@ -207,6 +218,15 @@ function hooks_parse_files( array $files, string $root, array $ignore_hooks ) :
'apply_filters_ref_array',
];

$deprecated_funcs = [
'do_action_deprecated',
'apply_filters_deprecated',
];

if ( $include_deprecated ) {
$funcs = array_merge( $funcs, $deprecated_funcs );
}

foreach ( $files as $filename ) {
// Parse the PHP file
$contents = file_get_contents($filename);
Expand Down Expand Up @@ -248,6 +268,8 @@ function hooks_parse_files( array $files, string $root, array $ignore_hooks ) :
continue;
}

$is_deprecated_hook = in_array( $funcNameStr, $deprecated_funcs, true );

$docblock = $expr->getDocComment();

if ( $docblock && str_starts_with($docblock->getText(), '/** This action is documented in') ) {
Expand Down Expand Up @@ -281,7 +303,13 @@ function hooks_parse_files( array $files, string $root, array $ignore_hooks ) :
$filename,
);

continue;
// Deprecated hooks may use the deprecated function arguments to convey the
// deprecation details, so we want to keep those docs in the output.
if ( ! $is_deprecated_hook ) {
continue;
}

$docblock = new Doc( '/** This hook is deprecated. */' );
}

$dbt = $docblock ? $docblock->getText() : '';
Expand Down Expand Up @@ -445,10 +473,60 @@ static function( array $matches ) : string {
case 'apply_filters_ref_array':
$out['type'] = 'filter_reference';
break;
case 'do_action_deprecated':
$out['type'] = 'action_deprecated';
break;
case 'apply_filters_deprecated':
$out['type'] = 'filter_deprecated';
break;
}

if ( $is_deprecated_hook ) {
// Deprecated hooks are fired via either `do_action_deprecated()` or `apply_filters_deprecated()`,
// which specify deprecation details in the arguments.
$version = get_literal_arg( $expr, 2 );

if ( null === $version ) {
echo sprintf(
"Deprecated hook '%s' in file '%s' is missing a version.\n",
$hook_name,
$filename,
);
}

$out['deprecated_version'] = $version ?? '';

$replacement = get_literal_arg( $expr, 3 );

if ( null !== $replacement ) {
$out['deprecated_replacement'] = $replacement;
}

$message = get_literal_arg( $expr, 4 );

if ( null !== $message ) {
$out['deprecated_message'] = $message;
}
}

$out['doc'] = $doc;
$out['args'] = count( $expr->args ) - 1;

if ( $is_deprecated_hook ) {
// The hook arguments are passed as an array in the second argument, so the
// argument count comes from that array rather than from the call itself.
$out['args'] = count_array_arg( $expr, 1 );
if ( null === $out['args'] ) {
$param_tags = array_filter(
$tags,
function( array $tag ) : bool {
return 'param' === $tag['name'];
}
);
$out['args'] = count( $param_tags );
}
} else {
$out['args'] = count( $expr->args ) - 1;
}

$output[] = $out;
}
Expand All @@ -461,6 +539,67 @@ static function( array $matches ) : string {
return $output;
}

/**
* Get the value of a positional argument within a function call.
*
* Quotes around string literals are stripped. Anything that isn't a literal, such as a constant or a
* variable, is returned as its printed source code. Returns null when the argument isn't present, is
* empty, or is passed by name.
*
* @param Node\Expr\FuncCall $expr
* @param int $index
*
* @return string|null
*/
function get_literal_arg( Node\Expr\FuncCall $expr, int $index ) : ?string {
$arg = $expr->args[ $index ] ?? null;

// Named arguments and first class callable syntax are not supported.
if ( ! ( $arg instanceof Node\Arg ) || null !== $arg->name ) {
return null;
}

$printer = new Standard();
$value = $printer->prettyPrintExpr( $arg->value );
// Check for an edge case where we have "'text'". We want to keep the inner quote characters.
$value = preg_replace( '/^"(\'.+\')"$/', '$1', $value, -1, $wrapped_quote_count );
if ( 0 === $wrapped_quote_count ) {
$value = preg_replace( '/^"(.*)"$/', '$1', $value );
$value = preg_replace( "/^'(.*)'$/", '$1', $value );
}

if ( '' === $value ) {
return null;
}

return $value;
}

/**
* Count the number of array elements for a positional argument within a function call.
*
* Returns null when the argument isn't present or isn't an array literal, for example
* when a variable is passed instead.
*
* @param Node\Expr\FuncCall $expr
* @param int $index
*
* @return int|null
*/
function count_array_arg( Node\Expr\FuncCall $expr, int $index ) : ?int {
$arg = $expr->args[ $index ] ?? null;

if ( ! ( $arg instanceof Node\Arg ) || null !== $arg->name ) {
return null;
}

if ( ! ( $arg->value instanceof Node\Expr\Array_ ) ) {
return null;
}

return count( $arg->value->items );
}

/**
* @return array<int, string>
*/
Expand All @@ -486,27 +625,27 @@ function parse_aliases( string $html ) : array {
return $aliases;
}

$output = hooks_parse_files( $files, $source_dir, $ignore_hooks );
$output = hooks_parse_files( $files, $source_dir, $ignore_hooks, $include_deprecated );

// Actions
$actions = array_values( array_filter( $output, function( array $hook ) : bool {
return in_array( $hook['type'], [ 'action', 'action_reference' ], true );
return in_array( $hook['type'], [ 'action', 'action_reference', 'action_deprecated' ], true );
} ) );

$actions = [
'$schema' => 'https://raw.githubusercontent.com/wp-hooks/generator/1.0.2/schema.json',
'$schema' => 'https://raw.githubusercontent.com/wp-hooks/generator/1.1.0/schema.json',
'hooks' => $actions,
];

$result = file_put_contents( $target_dir . '/actions.json', json_encode( $actions, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) );

// Filters
$filters = array_values( array_filter( $output, function( array $hook ) : bool {
return in_array( $hook['type'], [ 'filter', 'filter_reference' ], true );
return in_array( $hook['type'], [ 'filter', 'filter_reference', 'filter_deprecated' ], true );
} ) );

$filters = [
'$schema' => 'https://raw.githubusercontent.com/wp-hooks/generator/1.0.2/schema.json',
'$schema' => 'https://raw.githubusercontent.com/wp-hooks/generator/1.1.0/schema.json',
'hooks' => $filters,
];

Expand Down
2 changes: 1 addition & 1 deletion src/validate.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

$data = json_decode(file_get_contents( $argv[1] ));
$validator = new Validator();
$id = 'https://github.com/wp-hooks/generator/blob/1.0.2/schema.json';
$id = 'https://github.com/wp-hooks/generator/blob/1.1.0/schema.json';
$validator->resolver()->registerFile(
$id,
'schema.json',
Expand Down