Skip to content

Commit fe6f904

Browse files
committed
yeast: Add location heuristic for synthetic nodes with no location set
For such nodes, we assign them the zero-length location at the beginning of the matched input node.
1 parent 1521355 commit fe6f904

36 files changed

Lines changed: 173 additions & 98 deletions

‎shared/yeast/doc/yeast.md‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,10 @@ yeast::trees!(ctx,
238238
### Source locations
239239

240240
Captured nodes keep the locations assigned by their own translations. New
241-
nodes in an output template derive their locations from their children. After
242-
the transform completes, the matched input node's range is added only to
243-
locally-created nodes returned as rule results:
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:
244245

245246
```rust
246247
rule!(
@@ -251,10 +252,12 @@ rule!(
251252
```
252253

253254
Here `inner` derives its range from `child`, while the returned `outer` node
254-
also includes the full `wrapper` range. This lets replacement roots include
255-
elided keywords or delimiters without assigning the same broad range to every
256-
synthetic descendant. A transform that simply returns a translated capture
257-
does not widen that capture to the wrapper's range.
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.
258261

259262
When the desired range belongs to another node, `tree_at!` assigns that range
260263
to the template's root. Nested nodes still derive their own locations normally:

‎shared/yeast/src/build.rs‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ pub struct BuildCtx<'a, C: 'a = ()> {
4040
///
4141
/// The `rule!` macro applies this range to locally-created result roots
4242
/// after the transform completes. Nested synthetic nodes derive their
43-
/// ranges from their children unless `source_range` is explicitly set.
43+
/// ranges from their children, falling back to an empty range at this
44+
/// range's start.
4445
pub matched_source_range: Option<Range>,
4546
/// User-supplied context, accessible directly via `ctx.field` (via Deref).
4647
pub user_ctx: &'a mut C,
@@ -128,6 +129,16 @@ impl<'a, C> BuildCtx<'a, C> {
128129
let id = self
129130
.ast
130131
.create_node_with_range(kind, content, fields, is_named, source_range);
132+
if self
133+
.ast
134+
.get_node(id)
135+
.is_some_and(|node| node.source_range().is_none())
136+
{
137+
if let Some(source_range) = self.matched_source_range {
138+
self.ast
139+
.extend_source_range(id, source_range.empty_at_start());
140+
}
141+
}
131142
if !has_explicit_source_range {
132143
self.created_nodes.insert(id);
133144
}
@@ -153,6 +164,10 @@ impl<'a, C> BuildCtx<'a, C> {
153164
source_range: Option<Range>,
154165
) -> Id {
155166
let has_explicit_source_range = source_range.is_some();
167+
let source_range = source_range.or_else(|| {
168+
self.matched_source_range
169+
.map(|range| range.empty_at_start())
170+
});
156171
let id = self
157172
.ast
158173
.create_named_token_with_range(kind, content, source_range);

‎shared/yeast/tests/test.rs‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1709,6 +1709,34 @@ fn test_nested_synthetic_node_uses_child_location() {
17091709
assert_eq!(arguments.byte_range(), 0..3);
17101710
}
17111711

1712+
/// A nested node with no explicit range or located children gets an empty
1713+
/// location at the start of the rule's matched node.
1714+
#[test]
1715+
fn test_source_less_nested_node_uses_empty_match_start() {
1716+
let rule: Rule = rule!(
1717+
(call
1718+
method: (identifier) @name
1719+
receiver: (identifier) @recv
1720+
)
1721+
=>
1722+
(call
1723+
method: {name}
1724+
receiver: {recv}
1725+
arguments: (argument_list)
1726+
)
1727+
);
1728+
1729+
let ast = run_and_ast("foo.bar()", vec![rule]);
1730+
let arguments = ast
1731+
.reachable_node_ids()
1732+
.into_iter()
1733+
.filter_map(|id| ast.get_node(id))
1734+
.find(|node| node.kind_name() == "argument_list")
1735+
.expect("argument list exists");
1736+
let range = arguments.source_range().unwrap();
1737+
assert_eq!(range.start_byte..range.end_byte, 0..0);
1738+
}
1739+
17121740
/// An explicit empty range at byte zero is a real location, not the sentinel
17131741
/// for an absent location, and therefore contributes to parent ranges.
17141742
#[test]

‎unified/extractor/tests/corpus/swift/collections/empty-array-literal-with-type.output‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,6 @@ top_level source="⟨body⟩"
4646
pattern: identifier "xs" source="xs"
4747
type:
4848
generic_type_expr source="[⟨type_argument⟩]"
49-
base: identifier "Array" source=<no location>
49+
base: identifier "Array" source=""
5050
type_argument: identifier "Int" source="Int"
5151
value: array_literal "[]" source="[]"

‎unified/extractor/tests/corpus/swift/control-flow/guard-let.output‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,18 @@ top_level source="⟨body⟩"
4242
stmt:
4343
guard_if_stmt source="guard ⟨condition⟩ else ⟨else⟩"
4444
condition:
45-
pattern_guard_expr source="let ⟨pattern⟩ = ⟨value⟩"
45+
pattern_guard_expr source="⟨pattern⟩ = ⟨value⟩"
4646
pattern:
4747
call_expr source="⟨argument⟩"
4848
callee:
49-
member_access_expr source=<no location>
50-
base: identifier "Optional" source=<no location>
51-
member_name_node: identifier "some" source=<no location>
49+
member_access_expr source=""
50+
base: identifier "Optional" source=""
51+
member_name_node: identifier "some" source=""
5252
argument:
5353
argument source="⟨value⟩"
5454
value:
55-
expr_pattern source="⟨expr⟩"
56-
modifier: modifier "let" source=<no location>
55+
expr_pattern source="let ⟨expr⟩"
56+
modifier: modifier "let" source=""
5757
expr: identifier "value" source="value"
5858
value: identifier "optional" source="optional"
5959
else:

‎unified/extractor/tests/corpus/swift/control-flow/if-let-optional-binding.output‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,18 @@ top_level source="⟨body⟩"
5555
stmt:
5656
if_expr source="if ⟨condition⟩ ⟨then⟩"
5757
condition:
58-
pattern_guard_expr source="let ⟨pattern⟩ = ⟨value⟩"
58+
pattern_guard_expr source="⟨pattern⟩ = ⟨value⟩"
5959
pattern:
6060
call_expr source="⟨argument⟩"
6161
callee:
62-
member_access_expr source=<no location>
63-
base: identifier "Optional" source=<no location>
64-
member_name_node: identifier "some" source=<no location>
62+
member_access_expr source=""
63+
base: identifier "Optional" source=""
64+
member_name_node: identifier "some" source=""
6565
argument:
6666
argument source="⟨value⟩"
6767
value:
68-
expr_pattern source="⟨expr⟩"
69-
modifier: modifier "let" source=<no location>
68+
expr_pattern source="let ⟨expr⟩"
69+
modifier: modifier "let" source=""
7070
expr: identifier "value" source="value"
7171
value: identifier "optional" source="optional"
7272
then:

‎unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -344,29 +344,29 @@ top_level source="⟨body⟩"
344344
map_literal "[key: value]" source="[key: value]"
345345
call_expr source="⟨argument⟩?"
346346
callee:
347-
member_access_expr source=<no location>
348-
base: identifier "Optional" source=<no location>
349-
member_name_node: identifier "some" source=<no location>
347+
member_access_expr source=""
348+
base: identifier "Optional" source=""
349+
member_name_node: identifier "some" source=""
350350
argument:
351351
argument source="⟨value⟩"
352352
value: identifier "optional" source="optional"
353353
unary_expr source="try ⟨operand⟩"
354354
operand: identifier "value" source="value"
355-
operator: prefix_operator "try" source=<no location>
355+
operator: prefix_operator "try" source=""
356356
unary_expr source="⟨operand⟩!"
357357
operand: identifier "value" source="value"
358-
operator: postfix_operator "!" source=<no location>
358+
operator: postfix_operator "!" source=""
359359
type_cast_expr source="⟨expr⟩ as ⟨type⟩"
360360
expr: identifier "value" source="value"
361-
operator: infix_operator "as" source=<no location>
361+
operator: infix_operator "as" source=""
362362
type: identifier "Target" source="Target"
363363
type_test_expr source="⟨expr⟩ is ⟨type⟩"
364364
expr: identifier "value" source="value"
365-
operator: infix_operator "is" source=<no location>
365+
operator: infix_operator "is" source=""
366366
type: identifier "Target" source="Target"
367367
unary_expr source="await ⟨operand⟩"
368368
operand: identifier "value" source="value"
369-
operator: prefix_operator "await" source=<no location>
369+
operator: prefix_operator "await" source=""
370370
body:
371371
block source="⟨stmt⟩"
372372
stmt:

‎unified/extractor/tests/corpus/swift/desugar/import-with-deeply-nested-path-three-parts.output‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ top_level source="⟨body⟩"
2727
body:
2828
block source="⟨stmt⟩"
2929
stmt:
30-
import_declaration source="import ⟨imported_expr⟩⟨pattern⟩"
30+
import_declaration source="⟨pattern⟩⟨imported_expr⟩"
3131
imported_expr:
3232
member_access_expr source="⟨base⟩.⟨member_name_node⟩"
3333
base:
@@ -36,6 +36,6 @@ top_level source="⟨body⟩"
3636
member_name_node: identifier "Networking" source="Networking"
3737
member_name_node: identifier "URLSession" source="URLSession"
3838
pattern:
39-
named_pattern source="⟨name_node⟩"
39+
named_pattern source="import Foundation.Networking.⟨name_node⟩"
4040
name_node: identifier "URLSession" source="URLSession"
41-
sub_pattern: bulk_importing_pattern source=<no location>
41+
sub_pattern: bulk_importing_pattern source=""

‎unified/extractor/tests/corpus/swift/desugar/import-with-dotted-path-two-parts.output‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ top_level source="⟨body⟩"
2424
body:
2525
block source="⟨stmt⟩"
2626
stmt:
27-
import_declaration source="import ⟨imported_expr⟩⟨pattern⟩"
27+
import_declaration source="⟨pattern⟩⟨imported_expr⟩"
2828
imported_expr:
2929
member_access_expr source="⟨base⟩.⟨member_name_node⟩"
3030
base: identifier "Foundation" source="Foundation"
3131
member_name_node: identifier "Networking" source="Networking"
3232
pattern:
33-
named_pattern source="⟨name_node⟩"
33+
named_pattern source="import Foundation.⟨name_node⟩"
3434
name_node: identifier "Networking" source="Networking"
35-
sub_pattern: bulk_importing_pattern source=<no location>
35+
sub_pattern: bulk_importing_pattern source=""

‎unified/extractor/tests/corpus/swift/desugar/simple-import-with-single-name.output‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ top_level source="⟨body⟩"
2121
body:
2222
block source="⟨stmt⟩"
2323
stmt:
24-
import_declaration source="import ⟨imported_expr⟩⟨pattern⟩"
24+
import_declaration source="⟨pattern⟩⟨imported_expr⟩"
2525
imported_expr: identifier "Foundation" source="Foundation"
2626
pattern:
27-
named_pattern source="⟨name_node⟩"
27+
named_pattern source="import ⟨name_node⟩"
2828
name_node: identifier "Foundation" source="Foundation"
29-
sub_pattern: bulk_importing_pattern source=<no location>
29+
sub_pattern: bulk_importing_pattern source=""

0 commit comments

Comments
 (0)