rule_id = $rule_id; $this->rule_name = $rule_name; } public function append_child( $node ) { $this->children[] = $node; } /** * Flatten the matched rule fragments as if their children were direct * descendants of the current rule. * * What are rule fragments? * * When we initially parse the grammar file, it has compound rules such * as this one: * * query ::= EOF | ((simpleStatement | beginWork) ((SEMICOLON_SYMBOL EOF?) | EOF)) * * Building a parser that can understand such rules is way more complex than building * a parser that only follows simple rules, so we flatten those compound rules into * simpler ones. The above rule would be flattened to: * * query ::= EOF | %query0 * %query0 ::= %%query01 %%query02 * %%query01 ::= simpleStatement | beginWork * %%query02 ::= SEMICOLON_SYMBOL EOF_zero_or_one | EOF * EOF_zero_or_one ::= EOF | ε * * This factorization happens in "convert-grammar.php". * * "Fragments" are intermediate artifacts whose names are not in the original grammar. * They are extremely useful for the parser, but the API consumer should never have to * worry about them. Fragment names start with a percent sign ("%"). * * The code below inlines every fragment back in its parent rule. * * We could optimize this. The current $match may be discarded later on so any inlining * effort here would be wasted. However, inlining seems cheap and doing it bottom-up here * is **much** easier than reprocessing the parse tree top-down later on. * * The following parse tree: * * [ * 'query' => [ * [ * '%query01' => [ * [ * 'simpleStatement' => [ * MySQLToken(MySQLLexer::WITH_SYMBOL, 'WITH') * ], * '%query02' => [ * [ * 'simpleStatement' => [ * MySQLToken(MySQLLexer::WITH_SYMBOL, 'WITH') * ] * ], * ] * ] * ] * ] * ] * * Would be inlined as: * * [ * 'query' => [ * [ * 'simpleStatement' => [ * MySQLToken(MySQLLexer::WITH_SYMBOL, 'WITH') * ] * ], * [ * 'simpleStatement' => [ * MySQLToken(MySQLLexer::WITH_SYMBOL, 'WITH') * ] * ] * ] * ] */ public function merge_fragment( $node ) { $this->children = array_merge( $this->children, $node->children ); } /** * Check if this node has any child nodes or tokens. * * @return bool True if this node has any child nodes or tokens, false otherwise. */ public function has_child(): bool { return count( $this->children ) > 0; } /** * Check if this node has any child nodes. * * @param string|null $rule_name Optional. A node rule name to check for. * @return bool True if any child nodes are found, false otherwise. */ public function has_child_node( ?string $rule_name = null ): bool { foreach ( $this->children as $child ) { if ( $child instanceof WP_Parser_Node && ( null === $rule_name || $child->rule_name === $rule_name ) ) { return true; } } return false; } /** * Check if this node has any child tokens. * * @param int|null $token_id Optional. A token ID to check for. * @return bool True if any child tokens are found, false otherwise. */ public function has_child_token( ?int $token_id = null ): bool { foreach ( $this->children as $child ) { if ( $child instanceof WP_Parser_Token && ( null === $token_id || $child->id === $token_id ) ) { return true; } } return false; } /** * Get the first child node or token of this node. * * @return WP_Parser_Node|WP_Parser_Token|null The first child node or token; * null when no children are found. */ public function get_first_child() { return $this->children[0] ?? null; } /** * Get the first child node of this node. * * @param string|null $rule_name Optional. A node rule name to check for. * @return WP_Parser_Node|null The first matching child node; null when no children are found. */ public function get_first_child_node( ?string $rule_name = null ): ?WP_Parser_Node { foreach ( $this->children as $child ) { if ( $child instanceof WP_Parser_Node && ( null === $rule_name || $child->rule_name === $rule_name ) ) { return $child; } } return null; } /** * Get the first child token of this node. * * @param int|null $token_id Optional. A token ID to check for. * @return WP_Parser_Token|null The first matching child token; null when no children are found. */ public function get_first_child_token( ?int $token_id = null ): ?WP_Parser_Token { foreach ( $this->children as $child ) { if ( $child instanceof WP_Parser_Token && ( null === $token_id || $child->id === $token_id ) ) { return $child; } } return null; } /** * Get the first descendant node of this node. * * The node children are traversed recursively in a depth-first order until * a matching descendant node is found, or the entire subtree is searched. * * @param string|null $rule_name Optional. A node rule name to check for. * @return WP_Parser_Node|null The first matching descendant node; null when no descendants are found. */ public function get_first_descendant_node( ?string $rule_name = null ): ?WP_Parser_Node { for ( $i = 0; $i < count( $this->children ); $i++ ) { $child = $this->children[ $i ]; if ( ! $child instanceof WP_Parser_Node ) { continue; } if ( null === $rule_name || $child->rule_name === $rule_name ) { return $child; } $node = $child->get_first_descendant_node( $rule_name ); if ( $node ) { return $node; } } return null; } /** * Get the first descendant token of this node. * * The node children are traversed recursively in a depth-first order until * a matching descendant token is found, or the entire subtree is searched. * * @param int|null $token_id Optional. A token ID to check for. * @return WP_Parser_Token|null The first matching descendant token; null when no descendants are found. */ public function get_first_descendant_token( ?int $token_id = null ): ?WP_Parser_Token { for ( $i = 0; $i < count( $this->children ); $i++ ) { $child = $this->children[ $i ]; if ( $child instanceof WP_Parser_Token ) { if ( null === $token_id || $child->id === $token_id ) { return $child; } } else { $token = $child->get_first_descendant_token( $token_id ); if ( $token ) { return $token; } } } return null; } /** * Get all children of this node. * * @return array An array of all child nodes and tokens of this node. */ public function get_children(): array { return $this->children; } /** * Get all child nodes of this node. * * @param string|null $rule_name Optional. A node rule name to check for. * @return WP_Parser_Node[] An array of all matching child nodes. */ public function get_child_nodes( ?string $rule_name = null ): array { $nodes = array(); foreach ( $this->children as $child ) { if ( $child instanceof WP_Parser_Node && ( null === $rule_name || $child->rule_name === $rule_name ) ) { $nodes[] = $child; } } return $nodes; } /** * Get all child tokens of this node. * * @param int|null $token_id Optional. A token ID to check for. * @return WP_Parser_Token[] An array of all matching child tokens. */ public function get_child_tokens( ?int $token_id = null ): array { $tokens = array(); foreach ( $this->children as $child ) { if ( $child instanceof WP_Parser_Token && ( null === $token_id || $child->id === $token_id ) ) { $tokens[] = $child; } } return $tokens; } /** * Get all descendants of this node. * * The descendants are collected using a depth-first pre-order NLR traversal. * This produces a natural ordering that corresponds to the original input. * * @return array An array of all descendant nodes and tokens of this node. */ public function get_descendants(): array { $descendants = array(); foreach ( $this->children as $child ) { if ( $child instanceof WP_Parser_Node ) { $descendants[] = $child; $descendants = array_merge( $descendants, $child->get_descendants() ); } else { $descendants[] = $child; } } return $descendants; } /** * Get all descendant nodes of this node. * * The descendants are collected using a depth-first pre-order NLR traversal. * This produces a natural ordering that corresponds to the original input. * All matching nodes are collected during the traversal. * * @param string|null $rule_name Optional. A node rule name to check for. * @return WP_Parser_Node[] An array of all matching descendant nodes. */ public function get_descendant_nodes( ?string $rule_name = null ): array { $nodes = array(); foreach ( $this->children as $child ) { if ( ! $child instanceof WP_Parser_Node ) { continue; } if ( null === $rule_name || $child->rule_name === $rule_name ) { $nodes[] = $child; } $nodes = array_merge( $nodes, $child->get_descendant_nodes( $rule_name ) ); } return $nodes; } /** * Get all descendant tokens of this node. * * The descendants are collected using a depth-first pre-order NLR traversal. * This produces a natural ordering that corresponds to the original input. * All matching tokens are collected during the traversal. * * @param int|null $token_id Optional. A token ID to check for. * @return WP_Parser_Token[] An array of all matching descendant tokens. */ public function get_descendant_tokens( ?int $token_id = null ): array { $tokens = array(); foreach ( $this->children as $child ) { if ( $child instanceof WP_Parser_Token ) { if ( null === $token_id || $child->id === $token_id ) { $tokens[] = $child; } } else { $tokens = array_merge( $tokens, $child->get_descendant_tokens( $token_id ) ); } } return $tokens; } /** * Get the byte offset in the input string where this node begins. * * @return int The byte offset in the input string where this node begins. */ public function get_start(): int { return $this->get_first_descendant_token()->start; } /** * Get the byte length of this node in the input string. * * @return int The byte length of this node in the input string. */ public function get_length(): int { $tokens = $this->get_descendant_tokens(); $first_token = $tokens[0]; $last_token = $tokens[ count( $tokens ) - 1 ]; return $last_token->start + $last_token->length - $first_token->start; } }