Files
codeql/shared/yeast-macros/src/lib.rs
Taus a4df96aad6 yeast: Support capturing unnamed nodes in queries
Three improvements to the query parser, all aimed at allowing query
patterns to refer to unnamed tokens:

1. Bare-literal capture: `"=" @op` now captures the unnamed `=` token,
   matching the parenthesized form `("=") @op`. Previously the literal
   branch in parse_query_list skipped the maybe_wrap_capture call, so
   the `@op` was a leftover token and would error.

2. Bare `_` matches any node, named or unnamed. Previously bare `_` and
   `(_)` both produced QueryNode::Any with the same matches_named_only
   behaviour, so bare `_` would skip unnamed children. Now Any carries a
   match_unnamed flag: false for `(_)` (named-only, tree-sitter default)
   and true for bare `_` (any node).

3. Named fields and bare child patterns may be intermixed in any order.
   Previously, once parse_query_fields saw a bare pattern it would stop
   accepting named fields. The fix accumulates bare patterns into the
   implicit `child` field and keeps parsing.

Each named field independently selects its target field for matching, so
the source-order of fields in the query is purely cosmetic and intermixing
is safe.

Add tests covering parenthesized capture, bare-literal capture, and the
named-vs-any distinction between `(_)` and bare `_`. Update query-syntax
docs to reflect all three.
2026-05-07 15:08:21 +00:00

113 lines
3.9 KiB
Rust

use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
mod parse;
/// Proc macro for constructing a `QueryNode` from a tree-sitter-inspired pattern.
///
/// # Syntax
///
/// ```text
/// (_) - match any named node (skips unnamed tokens)
/// _ - match any node, named or unnamed
/// (kind) - match a named node of the given kind
/// ("literal") - match an unnamed token by its text
/// "literal" - shorthand for `("literal")`
/// (kind field: (pattern)) - match with named field
/// (kind field: _) - bare `_` and bare literals work in field position too
/// (kind (pat) (pat)...) - match unnamed children
/// (pattern) @capture - capture the matched node
/// "literal" @capture - capture an unnamed token
/// _ @capture - capture any node
/// (pattern)* @capture - capture each repeated match
/// (pattern)? - zero or one
/// ```
///
/// Named fields and bare child patterns may be intermixed in any order.
#[proc_macro]
pub fn query(input: TokenStream) -> TokenStream {
let input2: TokenStream2 = input.into();
match parse::parse_query_top(input2) {
Ok(output) => output.into(),
Err(err) => err.to_compile_error().into(),
}
}
/// Build a single AST node from a template, returning its `Id`.
///
/// # Template syntax
///
/// ```text
/// (kind "literal") - leaf with static content
/// (kind #{expr}) - leaf with computed content (expr.to_string())
/// (kind $fresh) - leaf with auto-generated unique name
/// {expr} - embed a Rust expression returning Id
/// {..expr} - splice an iterable of Id (in child/field position)
/// field: {..expr} - splice into a named field
/// ```
///
/// Can be called with an explicit context or using the implicit context
/// from an enclosing `rule!`:
///
/// ```text
/// tree!(ctx, (kind ...)) // explicit BuildCtx
/// tree!((kind ...)) // implicit context from rule!
/// ```
#[proc_macro]
pub fn tree(input: TokenStream) -> TokenStream {
let input2: TokenStream2 = input.into();
match parse::parse_tree_top(input2) {
Ok(output) => output.into(),
Err(err) => err.to_compile_error().into(),
}
}
/// Build a list of AST nodes from a template, returning `Vec<Id>`.
///
/// Like `tree!` but returns `Vec<Id>` and supports multiple top-level
/// elements. All syntax from `tree!` is available.
///
/// Can be called with an explicit context or using the implicit context
/// from an enclosing `rule!`:
///
/// ```text
/// trees!(ctx, (node1 ...) (node2 ...)) // explicit BuildCtx
/// trees!((node1 ...) (node2 ...)) // implicit context from rule!
/// ```
#[proc_macro]
pub fn trees(input: TokenStream) -> TokenStream {
let input2: TokenStream2 = input.into();
match parse::parse_trees_top(input2) {
Ok(output) => output.into(),
Err(err) => err.to_compile_error().into(),
}
}
/// Define a desugaring rule with query and transform in one declaration.
///
/// ```text
/// rule!(
/// (query_pattern field: (_) @name (kind)* @repeated (_)? @optional)
/// =>
/// (output_template field: {name} {..repeated})
/// )
///
/// // Shorthand: captures become fields on the output node
/// rule!((query ...) => output_kind)
/// ```
///
/// Captures become Rust variables automatically:
/// - `@name` (no quantifier) → `name: Id`
/// - `@name` (after `*`/`+`) → `name: Vec<Id>`
/// - `@name` (after `?`) → `name: Option<Id>`
///
/// `tree!` and `trees!` can be used without explicit context inside `{...}`.
#[proc_macro]
pub fn rule(input: TokenStream) -> TokenStream {
let input2: TokenStream2 = input.into();
match parse::parse_rule_top(input2) {
Ok(output) => output.into(),
Err(err) => err.to_compile_error().into(),
}
}