Skip to content

Commit

Permalink
Generate hook names using parsed Node instead of pretty-printed name.
Browse files Browse the repository at this point in the history
Previously, the hook-name printer was reyling on the parser's pretty
printer to generate names, and then post-processing the name to
extract string literal values and concatenations.

Unfortunately this left some cases unhandled, specifically the case
where concatenations are joining function calls with string literals.
Any number of other unexpected situations might arise, and there can
be defects in the redundant code attempting to parse PHP syntax.

In this patch the pretty-printed version of the expression is used
only as a fallback in unrecognized situations. Primarily, a direct
encoding from the parsed syntax tree to string is used to rely on
the parser's own handling of syntax, and making it clearer how to
add additional support for other syntaxes.
  • Loading branch information
dmsnell committed Mar 20, 2024
1 parent 7fc2227 commit 881b2bc
Showing 1 changed file with 48 additions and 19 deletions.
67 changes: 48 additions & 19 deletions lib/class-hook-reflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,71 @@
* Custom reflector for WordPress hooks.
*/
class Hook_Reflector extends BaseReflector {
private $report = null;

/**
* @return string
*/
public function getName() {
$printer = new PHPParser_PrettyPrinter_Default;
return $this->cleanupName( $printer->prettyPrintExpr( $this->node->args[0]->value ) );
if ( null === $this->report ) {
$this->report = fopen( '/Users/dmsnell/code/wp/dev-docs/hook-names.txt', 'a' );
}

$name = $this->cleanupName( $this->node->args[0]->value );
fwrite( $this->report, $name . "\n" );
return $name;
}

/**
* Hook names are the first argument to actions and filters.
*
* These are expected to be string values or concatenations of strings and variables.
*
* Example:
*
* do_action( 'wp_insert_post', $post_ID, $post, true );
* do_action( "{$old_status}_to_{$new_status}", $post );
* do_action( $filter_name, $args );
*
* @param string $name
*
* @return string
*/
private function cleanupName( $name ) {
$matches = array();
private function cleanupName( \PhpParser\Node $node ) {
$printer = new PHPParser_PrettyPrinter_Default;
$name = $printer->prettyPrintExpr( $node );

// quotes on both ends of a string
if ( preg_match( '/^[\'"]([^\'"]*)[\'"]$/', $name, $matches ) ) {
return $matches[1];
}
if ( $node instanceof \PhpParser\Node\Scalar\String_ ) {
return $node->value;
} elseif ( $node instanceof \PhpParser\Node\Scalar\Encapsed ) {
$name = '';

// two concatenated things, last one of them a variable
if ( preg_match(
'/(?:[\'"]([^\'"]*)[\'"]\s*\.\s*)?' . // First filter name string (optional)
'(\$[^\s]*)' . // Dynamic variable
'(?:\s*\.\s*[\'"]([^\'"]*)[\'"])?/', // Second filter name string (optional)
$name, $matches ) ) {

if ( isset( $matches[3] ) ) {
return $matches[1] . '{' . $matches[2] . '}' . $matches[3];
} else {
return $matches[1] . '{' . $matches[2] . '}';
foreach ( $node->parts as $part ) {
if ( is_string( $part ) ) {
$name .= $part;
} else {
$name .= $this->cleanupName( $part );
}
}

return $name;
} elseif ( $node instanceof \PhpParser\Node\Expr\BinaryOp\Concat ) {
return $this->cleanupName( $node->left ) . $this->cleanupName( $node->right );
} elseif ( $node instanceof \PhpParser\Node\Expr\PropertyFetch ) {
return "{{$name}}";
} elseif ( $node instanceof \PhpParser\Node\Expr\Variable ) {
return "{\${$node->name}}";
}

/*
* If none of these known constructions match, then
* fallback to the pretty-printed version of the node.
*
* For improving the quality of the hook-name generation,
* replace this return statement by throwing an exception
* to determine which cases aren't handled, and then add
* them above.
*/
return $name;
}

Expand Down

0 comments on commit 881b2bc

Please sign in to comment.