Skip to content

Commit df51ea4

Browse files
authored
Merge pull request #22608 from github/tausbn/unified-improve-node-locations
unified: improve node locations
2 parents 4fa22b9 + 152bf4b commit df51ea4

85 files changed

Lines changed: 1625 additions & 640 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎shared/yeast-macros/src/parse.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -987,6 +987,7 @@ pub fn parse_rule_top(input: TokenStream) -> Result<TokenStream> {
987987
#(#translated_bindings)*
988988
let mut #ctx_ident = yeast::build::BuildCtx::with_translator(__ast, &__captures, __fresh, __source_range, __user_ctx, __translator);
989989
let __result: Vec<yeast::Id> = { #transform_body };
990+
let __result = #ctx_ident.finish_rule(__result);
990991
Ok(__result)
991992
}),
992993
)

‎shared/yeast/doc/yeast.md‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,81 @@ yeast::trees!(ctx,
235235
(identifier #{name}) // an identifier from a Rust variable
236236
```
237237

238+
### Source locations
239+
240+
Captured nodes keep the locations assigned by their own translations. New
241+
nodes in an output template derive their locations from their children. A
242+
source-less nested node receives an empty location at the start of the matched
243+
input node. After the transform completes, the full matched range is added only
244+
to locally-created nodes returned as rule results:
245+
246+
```rust
247+
rule!(
248+
(wrapper child: (_) @child)
249+
=>
250+
(outer nested: (inner value: {child}))
251+
)
252+
```
253+
254+
Here `inner` derives its range from `child`, while the returned `outer` node
255+
also includes the full `wrapper` range. A nested node with no located children
256+
would instead receive an empty range at the start of `wrapper`. This lets
257+
replacement roots include elided keywords or delimiters without assigning the
258+
same broad range to every synthetic descendant. A transform that simply
259+
returns a translated capture does not widen that capture to the wrapper's
260+
range.
261+
262+
The following macros can be used to explicitly set the location associated
263+
with a newly-created node. They assign a location only to the root of their
264+
template; nested nodes still derive their locations normally.
265+
266+
`tree_at!` assigns the range of one captured input node to the template root:
267+
268+
```rust
269+
rule!(
270+
(wrapper
271+
source: (_) @source_node
272+
child: (_) @child)
273+
=>
274+
synthetic_node {
275+
tree_at!(
276+
ctx,
277+
source_node,
278+
(synthetic_node child: (nested value: {child}))
279+
)
280+
}
281+
)
282+
```
283+
284+
`tree_spanning!` assigns the smallest range containing several captured input
285+
nodes:
286+
287+
```rust
288+
rule!(
289+
(wrapper
290+
first: (_) @first
291+
second: (_) @second
292+
child: (_) @child)
293+
=>
294+
synthetic_node {
295+
tree_spanning!(
296+
ctx,
297+
[first, second],
298+
(synthetic_node child: {child})
299+
)
300+
}
301+
)
302+
```
303+
304+
For input fields whose leading or trailing syntax should never belong to rule
305+
results, configure them once with
306+
`DesugaringConfig::with_ignored_location_fields(...)`. For example, ignoring
307+
`trailingComma` retains the rest of each matched list element without requiring
308+
every rule to capture or handle the comma.
309+
310+
For literals, `ctx.literal_at_start_of(...)` creates an empty range at another
311+
node's start.
312+
238313
For reviewing locations, `DumpOptions::show_abridged_source` prints each node's
239314
source range with every direct child replaced by its field name in Unicode
240315
angle brackets. This keeps delimiters and other parent-owned syntax visible
@@ -246,6 +321,17 @@ return_expr source="return ⟨value⟩"
246321
call_expr source="⟨callee⟩(⟨argument⟩)"
247322
```
248323

324+
Children outside the node's source range retain their own locations and are
325+
annotated where they are printed rather than being treated as errors:
326+
327+
```text
328+
accessor_declaration source="⟨accessor_kind⟩"
329+
name_node: identifier "value" source="value" (external)
330+
```
331+
332+
Node and child ranges are still validated against the source text and UTF-8
333+
boundaries.
334+
249335
### Optional fields (`?`)
250336

251337
A `?` on a field's value makes that field fallible. If a `#{expr}` anywhere

‎shared/yeast/src/build.rs‎

Lines changed: 126 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
use std::collections::BTreeMap;
1+
use std::collections::{BTreeMap, BTreeSet};
22

33
use crate::captures::Captures;
44
use crate::tree_builder::FreshScope;
5-
use crate::{Ast, FieldId, Id, NodeContent, Range, TranslatorHandle};
5+
use crate::{Ast, FieldId, Id, KindId, NodeContent, Range, TranslatorHandle};
66

77
/// Context for building new AST nodes during a transformation.
88
///
@@ -33,13 +33,24 @@ pub struct BuildCtx<'a, C: 'a = ()> {
3333
pub ast: &'a mut Ast,
3434
pub captures: &'a Captures,
3535
pub fresh: &'a FreshScope,
36-
/// Source range of the matched node, inherited by synthetic nodes.
36+
/// Source range of the node matched by the current rule.
37+
///
38+
/// The `rule!` macro applies this range to locally-created result roots
39+
/// after the transform completes. Nested synthetic nodes derive their
40+
/// ranges from their children, falling back to an empty range at this
41+
/// range's start.
3742
pub source_range: Option<Range>,
3843
/// User-supplied context, accessible directly via `ctx.field` (via Deref).
3944
pub user_ctx: &'a mut C,
4045
/// Optional translator handle, populated when the context is built by
4146
/// the framework's rule driver. None when the context is built by hand.
4247
pub(crate) translator: Option<TranslatorHandle<'a, C>>,
48+
/// Nodes built directly through this context without an explicit source
49+
/// range in either their content or constructor argument. Recursive
50+
/// translations use their own context and therefore do not contribute to
51+
/// this set. Membership checks identify result roots to widen, while
52+
/// removals exclude nodes that are later assigned an explicit range.
53+
created_nodes_without_source_range: BTreeSet<Id>,
4354
}
4455

4556
impl<'a, C> BuildCtx<'a, C> {
@@ -56,9 +67,11 @@ impl<'a, C> BuildCtx<'a, C> {
5667
source_range: None,
5768
user_ctx,
5869
translator: None,
70+
created_nodes_without_source_range: BTreeSet::new(),
5971
}
6072
}
6173

74+
/// Construct a context carrying a matched input source range.
6275
pub fn with_source_range(
6376
ast: &'a mut Ast,
6477
captures: &'a Captures,
@@ -73,6 +86,7 @@ impl<'a, C> BuildCtx<'a, C> {
7386
source_range,
7487
user_ctx,
7588
translator: None,
89+
created_nodes_without_source_range: BTreeSet::new(),
7690
}
7791
}
7892

@@ -93,7 +107,81 @@ impl<'a, C> BuildCtx<'a, C> {
93107
source_range,
94108
user_ctx,
95109
translator: Some(translator),
110+
created_nodes_without_source_range: BTreeSet::new(),
111+
}
112+
}
113+
114+
/// Create a node and record it as constructed by this rule invocation.
115+
pub fn create_node_with_range(
116+
&mut self,
117+
kind: KindId,
118+
content: NodeContent,
119+
fields: BTreeMap<FieldId, Vec<Id>>,
120+
is_named: bool,
121+
source_range: Option<Range>,
122+
) -> Id {
123+
let has_explicit_source_range =
124+
source_range.is_some() || matches!(&content, NodeContent::Range(_));
125+
let id = self
126+
.ast
127+
.create_node_with_range(kind, content, fields, is_named, source_range);
128+
if self
129+
.ast
130+
.get_node(id)
131+
.is_some_and(|node| node.source_range().is_none())
132+
{
133+
if let Some(source_range) = self.source_range {
134+
self.ast
135+
.extend_source_range(id, source_range.empty_at_start());
136+
}
137+
}
138+
if !has_explicit_source_range {
139+
self.created_nodes_without_source_range.insert(id);
96140
}
141+
id
142+
}
143+
144+
/// Create a named token and record it as constructed by this rule invocation.
145+
pub fn create_named_token_with_range(
146+
&mut self,
147+
kind: &'static str,
148+
content: String,
149+
source_range: Option<Range>,
150+
) -> Id {
151+
let has_explicit_source_range = source_range.is_some();
152+
let source_range =
153+
source_range.or_else(|| self.source_range.map(|range| range.empty_at_start()));
154+
let id = self
155+
.ast
156+
.create_named_token_with_range(kind, content, source_range);
157+
if !has_explicit_source_range {
158+
self.created_nodes_without_source_range.insert(id);
159+
}
160+
id
161+
}
162+
163+
/// Finish the current rule invocation by applying the matched source range
164+
/// to locally-created result roots.
165+
#[doc(hidden)]
166+
pub fn finish_rule(self, results: Vec<Id>) -> Vec<Id> {
167+
if let Some(source_range) = self.source_range {
168+
for &id in &results {
169+
if self.created_nodes_without_source_range.contains(&id) {
170+
self.ast.extend_source_range(id, source_range);
171+
}
172+
}
173+
}
174+
results
175+
}
176+
177+
/// Assign an explicit source range to a newly-built result root.
178+
#[doc(hidden)]
179+
pub fn set_node_source_range(&mut self, node: Id, source_range: Option<Range>) -> Id {
180+
if let Some(source_range) = source_range {
181+
self.ast.set_source_range(node, source_range);
182+
self.created_nodes_without_source_range.remove(&node);
183+
}
184+
node
97185
}
98186

99187
/// Look up a capture variable, returning its node Id.
@@ -119,6 +207,18 @@ impl<'a, C> BuildCtx<'a, C> {
119207
self.ast.source_text(id)
120208
}
121209

210+
/// Return the source range of a parsed or synthetic node.
211+
fn source_range_of(&self, id: Id) -> Option<Range> {
212+
self.ast.get_node(id).and_then(|node| node.source_range())
213+
}
214+
215+
/// Return an empty range between two non-overlapping nodes.
216+
pub fn empty_source_range_between(&self, left: Id, right: Id) -> Option<Range> {
217+
let left = self.source_range_of(left)?;
218+
let right = self.source_range_of(right)?;
219+
(left.end_byte <= right.start_byte).then(|| left.empty_at_end())
220+
}
221+
122222
/// Create a named AST node with the given kind and fields.
123223
pub fn node(&mut self, kind: &str, fields: Vec<(&str, Vec<Id>)>) -> Id {
124224
let kind_id = self
@@ -133,41 +233,40 @@ impl<'a, C> BuildCtx<'a, C> {
133233
.unwrap_or_else(|| panic!("build: field '{name}' not found"));
134234
field_map.entry(field_id).or_default().extend(ids);
135235
}
136-
self.ast.create_node_with_range(
236+
self.create_node_with_range(
137237
kind_id,
138238
NodeContent::DynamicString(String::new()),
139239
field_map,
140240
true,
141-
self.source_range,
241+
None,
142242
)
143243
}
144244

145245
/// Create a leaf node with a fixed string content.
146246
pub fn literal(&mut self, kind: &'static str, value: &str) -> Id {
147-
self.ast
148-
.create_named_token_with_range(kind, value.to_string(), self.source_range)
247+
self.create_named_token_with_range(kind, value.to_string(), None)
149248
}
150249

151-
/// Create a leaf node with fixed content and an optional preferred source range.
152-
/// If `source_range` is `None`, falls back to this context's inherited range.
250+
/// Create a leaf node with fixed content and an optional source range.
153251
pub fn literal_with_source_range(
154252
&mut self,
155253
kind: &'static str,
156254
value: &str,
157255
source_range: Option<Range>,
158256
) -> Id {
159-
self.ast.create_named_token_with_range(
160-
kind,
161-
value.to_string(),
162-
source_range.or(self.source_range),
163-
)
257+
self.create_named_token_with_range(kind, value.to_string(), source_range)
258+
}
259+
260+
/// Create a literal with an empty range at another node's start.
261+
pub fn literal_at_start_of(&mut self, kind: &'static str, value: &str, source: Id) -> Id {
262+
let source_range = self.source_range_of(source).map(Range::empty_at_start);
263+
self.literal_with_source_range(kind, value, source_range)
164264
}
165265

166266
/// Create a leaf node with an auto-generated unique name.
167267
pub fn fresh(&mut self, kind: &'static str, name: &str) -> Id {
168268
let generated = self.fresh.resolve(name);
169-
self.ast
170-
.create_named_token_with_range(kind, generated, self.source_range)
269+
self.create_named_token_with_range(kind, generated, None)
171270
}
172271
}
173272

@@ -203,8 +302,9 @@ impl<C: Clone> BuildCtx<'_, C> {
203302

204303
/// Run `f` with a temporary child [`BuildCtx`] whose `user_ctx` is
205304
/// a fresh clone of the current one, sharing everything else
206-
/// (`ast`, `captures`, `fresh`, `source_range`, `translator`) by
207-
/// re-borrow. Any mutations `f` makes to the child's `user_ctx`
305+
/// (`ast`, `captures`, `fresh`, source ranges, `translator`) by re-borrow.
306+
/// Nodes constructed through the child remain part of the current rule
307+
/// invocation. Any mutations `f` makes to the child's `user_ctx`
208308
/// are discarded when it returns — no restore needed, because the
209309
/// mutations only ever happened on a local clone.
210310
///
@@ -238,8 +338,15 @@ impl<C: Clone> BuildCtx<'_, C> {
238338
source_range: self.source_range,
239339
user_ctx: &mut child_user_ctx,
240340
translator: self.translator,
341+
created_nodes_without_source_range: BTreeSet::new(),
241342
};
242-
f(&mut child)
343+
let result = f(&mut child);
344+
let created_nodes_without_source_range =
345+
std::mem::take(&mut child.created_nodes_without_source_range);
346+
drop(child);
347+
self.created_nodes_without_source_range
348+
.extend(created_nodes_without_source_range);
349+
result
243350
// child_user_ctx dropped; the outer `self` is unaffected.
244351
}
245352
}

0 commit comments

Comments
 (0)