diff --git a/interface/index.d.ts b/interface/index.d.ts index 6a8b962..4d6acd9 100644 --- a/interface/index.d.ts +++ b/interface/index.d.ts @@ -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 diff --git a/package-lock.json b/package-lock.json index 7b48a9f..d4a19ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "wp-hooks/generator", - "version": "1.0.2", + "version": "1.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 23ff6e3..09ef487 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/readme.md b/readme.md index 9b319a5..41fe301 100644 --- a/readme.md +++ b/readme.md @@ -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: diff --git a/schema.json b/schema.json index 0d4e354..9c6598d 100644 --- a/schema.json +++ b/schema.json @@ -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", @@ -62,7 +62,9 @@ "action", "filter", "action_reference", - "filter_reference" + "filter_reference", + "action_deprecated", + "filter_deprecated" ] }, "doc": { @@ -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." + ] } } } diff --git a/src/generate.php b/src/generate.php index 49a2017..4a07440 100755 --- a/src/generate.php +++ b/src/generate.php @@ -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 ); @@ -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"} ) ) { @@ -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'] ) ) { @@ -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( @@ -192,9 +202,10 @@ public function enterNode(Node $node) { * @param array $files * @param string $root * @param array $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 @@ -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); @@ -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') ) { @@ -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() : ''; @@ -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; } @@ -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 */ @@ -486,15 +625,15 @@ 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, ]; @@ -502,11 +641,11 @@ function parse_aliases( string $html ) : array { // 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, ]; diff --git a/src/validate.php b/src/validate.php index f21ce01..f5b8de7 100755 --- a/src/validate.php +++ b/src/validate.php @@ -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',