Yeast: Propagate source locations to synthetic nodes

Synthetic nodes created by desugaring rules now inherit the source
range of the original matched node. This fixes invalid TRAP locations
(previously (0,0)-(0,0)) for desugared nodes.

- Node gains a source_range field used as fallback for position/byte
  methods when content is not a Range
- BuildCtx stores the matched node's range and passes it to all
  created nodes
- Rule::try_rule extracts the source range from the matched node
  and passes it through the transform closure

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Taus
2026-05-01 15:20:20 +00:00
parent afcedea877
commit 23011671ff
4 changed files with 64 additions and 13 deletions

View File

@@ -623,9 +623,9 @@ pub fn parse_rule_top(input: TokenStream) -> Result<TokenStream> {
Ok(quote! {
{
let __query = #query_code;
yeast::Rule::new(__query, Box::new(|__ast: &mut yeast::Ast, __captures: yeast::captures::Captures, __fresh: &yeast::tree_builder::FreshScope| {
yeast::Rule::new(__query, Box::new(|__ast: &mut yeast::Ast, __captures: yeast::captures::Captures, __fresh: &yeast::tree_builder::FreshScope, __source_range: Option<tree_sitter::Range>| {
#(#bindings)*
let mut #ctx_ident = yeast::build::BuildCtx::new(__ast, &__captures, __fresh);
let mut #ctx_ident = yeast::build::BuildCtx::with_source_range(__ast, &__captures, __fresh, __source_range);
#transform_body
}))
}

View File

@@ -13,6 +13,8 @@ pub struct BuildCtx<'a> {
pub ast: &'a mut Ast,
pub captures: &'a Captures,
pub fresh: &'a FreshScope,
/// Source range of the matched node, inherited by synthetic nodes.
pub source_range: Option<tree_sitter::Range>,
}
impl<'a> BuildCtx<'a> {
@@ -21,6 +23,16 @@ impl<'a> BuildCtx<'a> {
ast,
captures,
fresh,
source_range: None,
}
}
pub fn with_source_range(ast: &'a mut Ast, captures: &'a Captures, fresh: &'a FreshScope, source_range: Option<tree_sitter::Range>) -> Self {
Self {
ast,
captures,
fresh,
source_range,
}
}
@@ -53,18 +65,18 @@ impl<'a> BuildCtx<'a> {
})
.collect();
self.ast
.create_node(kind_id, NodeContent::DynamicString(String::new()), field_map, true)
.create_node_with_range(kind_id, NodeContent::DynamicString(String::new()), field_map, true, self.source_range)
}
/// Create a leaf node with a fixed string content.
pub fn literal(&mut self, kind: &'static str, value: &str) -> Id {
self.ast.create_named_token(kind, value.to_string())
self.ast.create_named_token_with_range(kind, value.to_string(), self.source_range)
}
/// Create a leaf node with an auto-generated unique name.
pub fn fresh(&mut self, kind: &'static str, name: &str) -> Id {
let generated = self.fresh.resolve(name);
self.ast.create_named_token(kind, generated)
self.ast.create_named_token_with_range(kind, generated, self.source_range)
}
/// Create a node for unnamed children (the synthetic "child" field).
@@ -87,6 +99,6 @@ impl<'a> BuildCtx<'a> {
field_map.insert(CHILD_FIELD, children);
}
self.ast
.create_node(kind_id, NodeContent::DynamicString(String::new()), field_map, true)
.create_node_with_range(kind_id, NodeContent::DynamicString(String::new()), field_map, true, self.source_range)
}
}

View File

@@ -205,6 +205,17 @@ impl Ast {
content: NodeContent,
fields: BTreeMap<FieldId, Vec<Id>>,
is_named: bool,
) -> Id {
self.create_node_with_range(kind, content, children, is_named, None)
}
pub fn create_node_with_range(
&mut self,
kind: KindId,
content: NodeContent,
children: Vec<(FieldId, Id)>,
is_named: bool,
source_range: Option<tree_sitter::Range>,
) -> Id {
let id = self.nodes.len();
self.nodes.push(Node {
@@ -217,11 +228,16 @@ impl Ast {
is_error: false,
is_extra: false,
is_named,
source_range,
});
id
}
pub fn create_named_token(&mut self, kind: &'static str, content: String) -> Id {
self.create_named_token_with_range(kind, content, None)
}
pub fn create_named_token_with_range(&mut self, kind: &'static str, content: String, source_range: Option<tree_sitter::Range>) -> Id {
let kind_id = self.language.id_for_node_kind(kind, true);
let id = self.nodes.len();
self.nodes.push(Node {
@@ -231,6 +247,7 @@ impl Ast {
is_named: true,
is_missing: false,
is_error: false,
source_range,
is_extra: false,
fields: BTreeMap::new(),
content: NodeContent::DynamicString(content),
@@ -318,6 +335,7 @@ impl Ast {
content: NodeContent::String("x = 1"),
is_missing: false,
is_error: false,
source_range: None,
is_extra: false,
is_named: true,
},
@@ -330,6 +348,7 @@ impl Ast {
content: NodeContent::String("x"),
is_missing: false,
is_error: false,
source_range: None,
is_extra: false,
is_named: true,
},
@@ -342,6 +361,7 @@ impl Ast {
content: NodeContent::String("="),
is_missing: false,
is_error: false,
source_range: None,
is_extra: false,
is_named: false,
},
@@ -354,6 +374,7 @@ impl Ast {
content: NodeContent::String("1"),
is_missing: false,
is_error: false,
source_range: None,
is_extra: false,
is_named: true,
},
@@ -388,6 +409,10 @@ pub struct Node {
kind_name: &'static str,
pub(crate) fields: BTreeMap<FieldId, Vec<Id>>,
pub(crate) content: NodeContent,
/// For synthetic nodes, the source range of the original node they
/// were desugared from. Used for location information in TRAP output.
#[serde(skip)]
source_range: Option<tree_sitter::Range>,
is_named: bool,
is_missing: bool,
is_extra: bool,
@@ -439,28 +464,34 @@ impl Node {
pub fn start_position(&self) -> tree_sitter::Point {
match self.content {
NodeContent::Range(range) => range.start_point,
_ => self.fake_point(),
_ => self.source_range.map_or_else(
|| self.fake_point(),
|r| r.start_point,
),
}
}
pub fn end_position(&self) -> tree_sitter::Point {
match self.content {
NodeContent::Range(range) => range.end_point,
_ => self.fake_point(),
_ => self.source_range.map_or_else(
|| self.fake_point(),
|r| r.end_point,
),
}
}
pub fn start_byte(&self) -> usize {
match self.content {
NodeContent::Range(range) => range.start_byte,
_ => 0,
_ => self.source_range.map_or(0, |r| r.start_byte),
}
}
pub fn end_byte(&self) -> usize {
match self.content {
NodeContent::Range(range) => range.end_byte,
_ => 0,
_ => self.source_range.map_or(0, |r| r.end_byte),
}
}
@@ -500,11 +531,11 @@ impl From<tree_sitter::Range> for NodeContent {
pub struct Rule {
query: QueryNode,
transform: Box<dyn Fn(&mut Ast, Captures, &tree_builder::FreshScope) -> Vec<Id>>,
transform: Box<dyn Fn(&mut Ast, Captures, &tree_builder::FreshScope, Option<tree_sitter::Range>) -> Vec<Id>>,
}
impl Rule {
pub fn new(query: QueryNode, transform: Box<dyn Fn(&mut Ast, Captures, &tree_builder::FreshScope) -> Vec<Id>>) -> Self {
pub fn new(query: QueryNode, transform: Box<dyn Fn(&mut Ast, Captures, &tree_builder::FreshScope, Option<tree_sitter::Range>) -> Vec<Id>>) -> Self {
Self { query, transform }
}
@@ -512,7 +543,14 @@ impl Rule {
let mut captures = Captures::new();
if self.query.do_match(ast, node, &mut captures).unwrap() {
fresh.next_scope();
Some((self.transform)(ast, captures, fresh))
// Get the source range of the matched node for location info
let source_range = ast.get_node(node).and_then(|n| {
match n.content {
NodeContent::Range(r) => Some(r),
_ => n.source_range,
}
});
Some((self.transform)(ast, captures, fresh, source_range))
} else {
None
}

View File

@@ -68,6 +68,7 @@ impl Visitor {
is_named: n.is_named(),
is_extra: n.is_extra(),
is_error: n.is_error(),
source_range: None,
},
parent: self.current,
});