Skip to content

Commit 4d9e295

Browse files
tausbnCopilot
andcommitted
Unified: Exclude trailing commas from locations
Build arguments, tuple-pattern elements, and tuple-type elements from their meaningful child ranges so separators remain owned by their parent nodes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ba9279d commit 4d9e295

29 files changed

Lines changed: 227 additions & 91 deletions

‎shared/yeast/doc/yeast.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,12 @@ let synthetic = tree_at!(ctx, source, (synthetic_node child: (nested value: {chi
272272
let synthetic = tree_spanning!(ctx, nodes, (synthetic_node child: {child}));
273273
```
274274

275+
For input fields whose leading or trailing syntax should never belong to rule
276+
results, configure them once with
277+
`DesugaringConfig::with_ignored_location_fields(...)`. For example, ignoring
278+
`trailingComma` retains the rest of each matched list element without requiring
279+
every rule to capture or handle the comma.
280+
275281
For literals, `ctx.literal_at_start_of(...)` creates an empty range at another
276282
node's start.
277283

‎shared/yeast/src/lib.rs‎

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,25 @@ impl Ast {
597597
self.nodes.get(id.0)
598598
}
599599

600+
fn source_range_ignoring_fields(
601+
&self,
602+
id: Id,
603+
ignored_fields: &[&str],
604+
) -> Option<Range> {
605+
let node = self.get_node(id)?;
606+
let source_range = node.source_range()?;
607+
let ignored_ranges = node
608+
.fields
609+
.iter()
610+
.filter(|(field_id, _)| {
611+
self.field_name_for_id(**field_id)
612+
.is_some_and(|name| ignored_fields.contains(&name))
613+
})
614+
.flat_map(|(_, children)| children)
615+
.filter_map(|child| self.get_node(*child).and_then(Node::source_range));
616+
Some(source_range.ignoring_boundary_ranges(ignored_ranges))
617+
}
618+
600619
pub fn print(&self, source: &str, root_id: Id) -> Value {
601620
let root = &self.nodes()[root_id.0];
602621
self.print_node(root, source)
@@ -1100,6 +1119,7 @@ pub struct Rule<C = ()> {
11001119
query: QueryNode,
11011120
guard: Option<Guard<C>>,
11021121
transform: Transform<C>,
1122+
ignored_location_fields: Vec<&'static str>,
11031123
/// If true, after this rule fires on a node the engine will try to
11041124
/// re-apply this same rule on the result root. Defaults to false:
11051125
/// each rule fires at most once on a given node, which prevents
@@ -1114,6 +1134,7 @@ impl<C> Rule<C> {
11141134
query,
11151135
guard: None,
11161136
transform,
1137+
ignored_location_fields: Vec::new(),
11171138
repeated: false,
11181139
}
11191140
}
@@ -1125,6 +1146,7 @@ impl<C> Rule<C> {
11251146
query,
11261147
guard: Some(guard),
11271148
transform,
1149+
ignored_location_fields: Vec::new(),
11281150
repeated: false,
11291151
}
11301152
}
@@ -1138,6 +1160,10 @@ impl<C> Rule<C> {
11381160
self
11391161
}
11401162

1163+
fn set_ignored_location_fields(&mut self, fields: &[&'static str]) {
1164+
self.ignored_location_fields = fields.to_vec();
1165+
}
1166+
11411167
/// Attempt to match this rule's query against `node`, returning the raw
11421168
/// captures on success. Does not evaluate the guard or invoke the
11431169
/// transform.
@@ -1176,10 +1202,8 @@ impl<C> Rule<C> {
11761202
translator: TranslatorHandle<'_, C>,
11771203
) -> Result<Vec<Id>, String> {
11781204
fresh.next_scope();
1179-
let source_range = ast.get_node(node).and_then(|n| match n.content {
1180-
NodeContent::Range(r) => Some(r),
1181-
_ => n.source_range,
1182-
});
1205+
let source_range =
1206+
ast.source_range_ignoring_fields(node, &self.ignored_location_fields);
11831207
(self.transform)(ast, captures, fresh, source_range, user_ctx, translator)
11841208
}
11851209
}
@@ -1465,6 +1489,9 @@ pub struct DesugaringConfig<C = ()> {
14651489
/// node types are used (i.e. the desugared AST has the same node types
14661490
/// as the tree-sitter grammar).
14671491
pub output_node_types_yaml: Option<&'static str>,
1492+
/// Input field names whose boundary ranges are excluded from rule-result
1493+
/// locations.
1494+
pub ignored_location_fields: Vec<&'static str>,
14681495
}
14691496

14701497
// Manual `Default` impl so users with a custom `C` that doesn't implement
@@ -1474,6 +1501,7 @@ impl<C> Default for DesugaringConfig<C> {
14741501
Self {
14751502
phases: Vec::new(),
14761503
output_node_types_yaml: None,
1504+
ignored_location_fields: Vec::new(),
14771505
}
14781506
}
14791507
}
@@ -1490,12 +1518,30 @@ impl<C> DesugaringConfig<C> {
14901518
mut self,
14911519
name: impl Into<String>,
14921520
kind: PhaseKind,
1493-
rules: Vec<Rule<C>>,
1521+
mut rules: Vec<Rule<C>>,
14941522
) -> Self {
1523+
for rule in &mut rules {
1524+
rule.set_ignored_location_fields(&self.ignored_location_fields);
1525+
}
14951526
self.phases.push(Phase::new(name, kind, rules));
14961527
self
14971528
}
14981529

1530+
/// Ignore boundary syntax stored under any of these input field names when
1531+
/// calculating matched locations for rule results.
1532+
pub fn with_ignored_location_fields(
1533+
mut self,
1534+
fields: impl IntoIterator<Item = &'static str>,
1535+
) -> Self {
1536+
self.ignored_location_fields = fields.into_iter().collect();
1537+
for phase in &mut self.phases {
1538+
for rule in &mut phase.rules {
1539+
rule.set_ignored_location_fields(&self.ignored_location_fields);
1540+
}
1541+
}
1542+
self
1543+
}
1544+
14991545
pub fn with_output_node_types_yaml(mut self, yaml: &'static str) -> Self {
15001546
self.output_node_types_yaml = Some(yaml);
15011547
self

‎shared/yeast/src/range.rs‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,31 @@ impl Range {
6969
..self
7070
}
7171
}
72+
73+
pub(crate) fn ignoring_boundary_ranges(
74+
mut self,
75+
ignored: impl IntoIterator<Item = Self>,
76+
) -> Self {
77+
let ignored: Vec<_> = ignored.into_iter().collect();
78+
loop {
79+
let previous = self;
80+
for range in &ignored {
81+
if *range == self {
82+
self = self.empty_at_start();
83+
continue;
84+
}
85+
if range.start_byte == self.start_byte && range.end_byte > range.start_byte {
86+
self.start_byte = range.end_byte;
87+
self.start_point = range.end_point;
88+
}
89+
if range.end_byte == self.end_byte && range.end_byte > range.start_byte {
90+
self.end_byte = range.start_byte;
91+
self.end_point = range.start_point;
92+
}
93+
}
94+
if self == previous {
95+
return self;
96+
}
97+
}
98+
}
7299
}

‎shared/yeast/tests/test.rs‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1787,6 +1787,30 @@ fn test_returned_capture_keeps_its_location() {
17871787
assert_eq!(identifier.byte_range(), 4..7);
17881788
}
17891789

1790+
#[test]
1791+
fn test_ignored_location_field_is_excluded_from_rule_result_location() {
1792+
let rule: Rule = rule!(
1793+
(assignment
1794+
left: (identifier) @left)
1795+
=>
1796+
(call method: {left})
1797+
);
1798+
1799+
let language: tree_sitter::Language = tree_sitter_ruby::LANGUAGE.into();
1800+
let config = DesugaringConfig::new()
1801+
.with_ignored_location_fields(["right"])
1802+
.add_phase("test", PhaseKind::Repeating, vec![rule]);
1803+
let runner: Runner = Runner::from_config(language, &config).unwrap();
1804+
let ast = runner.run("x = 1").unwrap();
1805+
let call = ast
1806+
.reachable_node_ids()
1807+
.into_iter()
1808+
.filter_map(|id| ast.get_node(id))
1809+
.find(|node| node.kind_name() == "call")
1810+
.expect("call exists");
1811+
assert_eq!(call.byte_range(), 0..4);
1812+
}
1813+
17901814
/// Nodes allocated by an explicit recursive translation belong to that nested
17911815
/// rule invocation, even when the outer rule returns one directly.
17921816
#[test]

‎unified/extractor/src/languages/swift/swift.rs‎

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -480,13 +480,13 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
480480
rule!(
481481
(enumCaseElement
482482
name: @name
483-
parameterClause: (enumCaseParameterClause parameters: _* @params)) @@element
483+
parameterClause: (enumCaseParameterClause parameters: _* @params) @@clause)
484484
=>
485485
class_like_declaration {
486486
let body = tree!((block));
487-
let constructor = tree_at!(
487+
let constructor = tree_spanning!(
488488
ctx,
489-
element,
489+
[name, clause],
490490
(constructor_declaration parameter: {params} body: {body})
491491
);
492492
tree!((class_like_declaration
@@ -643,27 +643,41 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
643643
// `Array<T>` generic type constructor instead.
644644
rule!(
645645
(functionCallExpr
646-
calledExpression: (arrayExpr elements: (arrayElement expression: (genericSpecializationExpr) @element))
646+
calledExpression: (arrayExpr
647+
elements: (arrayElement expression: (genericSpecializationExpr) @element)) @@array
647648
arguments: _* @args
648649
trailingClosure: @tc)
649650
=>
650-
(call_expr
651-
callee: (generic_type_expr
651+
call_expr {
652+
let callee = tree_at!(
653+
ctx,
654+
array,
655+
(generic_type_expr
652656
base: (identifier "Array")
653657
type_argument: {element})
654-
argument: {args}
655-
argument: (argument value: {tc}))
658+
);
659+
tree!((call_expr
660+
callee: {callee}
661+
argument: {args}
662+
argument: (argument value: {tc})))
663+
}
656664
),
657665
rule!(
658666
(functionCallExpr
659-
calledExpression: (arrayExpr elements: (arrayElement expression: (genericSpecializationExpr) @element))
667+
calledExpression: (arrayExpr
668+
elements: (arrayElement expression: (genericSpecializationExpr) @element)) @@array
660669
arguments: _* @args)
661670
=>
662-
(call_expr
663-
callee: (generic_type_expr
671+
call_expr {
672+
let callee = tree_at!(
673+
ctx,
674+
array,
675+
(generic_type_expr
664676
base: (identifier "Array")
665677
type_argument: {element})
666-
argument: {args})
678+
);
679+
tree!((call_expr callee: {callee} argument: {args}))
680+
}
667681
),
668682
// A function/method call (`foo(1, 2)`). `calledExpression` is the callee
669683
// and `arguments` is an (elided) list of `labeledExpr`, each translated
@@ -734,14 +748,22 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
734748
// meaning as `Array<T>` rather than an array literal.
735749
rule!(
736750
(memberAccessExpr
737-
base: (arrayExpr elements: (arrayElement expression: (genericSpecializationExpr) @element))
751+
base: (arrayExpr
752+
elements: (arrayElement expression: (genericSpecializationExpr) @element)) @@array
738753
declName: (declReferenceExpr baseName: @member))
739754
=>
740-
(member_access_expr
741-
base: (generic_type_expr
742-
base: (identifier "Array")
743-
type_argument: {element})
744-
member_name_node: (identifier #{member}))
755+
member_access_expr {
756+
let base = tree_at!(
757+
ctx,
758+
array,
759+
(generic_type_expr
760+
base: (identifier "Array")
761+
type_argument: {element})
762+
);
763+
tree!((member_access_expr
764+
base: {base}
765+
member_name_node: (identifier #{member})))
766+
}
745767
),
746768
rule!(
747769
(memberAccessExpr base: @base declName: (declReferenceExpr baseName: @member))
@@ -863,7 +885,13 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
863885
(switch_case body: (block stmt: {body}))
864886
),
865887
// A single case item unwraps to its pattern, possibly boxed in conditional_pattern
866-
rule!((switchCaseItem pattern: @p whereClause: (whereClause condition: @cond)) => (conditional_pattern pattern: { p } condition: {cond})),
888+
rule!(
889+
(switchCaseItem
890+
pattern: @p
891+
whereClause: (whereClause condition: @cond))
892+
=>
893+
(conditional_pattern pattern: {p} condition: {cond})
894+
),
867895
rule!((switchCaseItem pattern: @p) => pattern { p }),
868896
// A pattern-matching condition (`if case let x = e`, `if case .foo(let x)
869897
// = e`) becomes a `pattern_guard_expr`: the matched pattern and the
@@ -1008,7 +1036,9 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
10081036
catch_clause: {catches})
10091037
),
10101038
rule!(
1011-
(catchItem pattern: @pattern whereClause: (whereClause condition: @guard))
1039+
(catchItem
1040+
pattern: @pattern
1041+
whereClause: (whereClause condition: @guard))
10121042
=>
10131043
(conditional_pattern pattern: {pattern} condition: {guard})
10141044
),
@@ -1170,7 +1200,9 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
11701200
}
11711201
),
11721202
rule!(
1173-
(tupleTypeElement firstName: _? @@name type: @ty)
1203+
(tupleTypeElement
1204+
firstName: _? @@name
1205+
type: @ty)
11741206
=>
11751207
argument {
11761208
if ctx.in_function_type {
@@ -1399,6 +1431,7 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
13991431

14001432
pub fn language_spec(desugared_ast_schema: &'static str) -> desugaring::LanguageSpec {
14011433
let config = DesugaringConfig::<SwiftContext>::new()
1434+
.with_ignored_location_fields(["trailingComma"])
14021435
.add_phase("translate", PhaseKind::OneShot, translation_rules())
14031436
.with_output_node_types_yaml(desugared_ast_schema);
14041437
let desugarer =

‎unified/extractor/tests/corpus/swift/collections/tuple-literal.output‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ top_level source="⟨body⟩"
5353
modifier: modifier "let" source="let"
5454
pattern: identifier "t" source="t"
5555
value:
56-
tuple_expr source="(⟨element⟩ ⟨element⟩ ⟨element⟩)"
56+
tuple_expr source="(⟨element⟩, ⟨element⟩, ⟨element⟩)"
5757
element:
58-
argument source="⟨value⟩,"
58+
argument source="⟨value⟩"
5959
value: int_literal "1" source="1"
60-
argument source="⟨value⟩,"
60+
argument source="⟨value⟩"
6161
value: string_literal "\"two\"" source="\"two\""
6262
argument source="⟨value⟩"
6363
value: float_literal "3.0" source="3.0"

‎unified/extractor/tests/corpus/swift/control-flow/nested-enum-case-pattern.output‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,13 +167,13 @@ top_level source="⟨body⟩"
167167
expr_pattern source="⟨modifier⟩ ⟨expr⟩"
168168
modifier: modifier "let" source="let"
169169
expr:
170-
call_expr source="⟨callee⟩(⟨argument⟩ ⟨argument⟩)"
170+
call_expr source="⟨callee⟩(⟨argument⟩, ⟨argument⟩)"
171171
callee:
172172
member_access_expr source="⟨base⟩⟨member_name_node⟩"
173173
base: inferred_type_expr "." source="."
174174
member_name_node: identifier "received" source="received"
175175
argument:
176-
argument source="⟨value⟩,"
176+
argument source="⟨value⟩"
177177
value:
178178
call_expr source="⟨callee⟩(⟨argument⟩)"
179179
callee:
@@ -188,10 +188,10 @@ top_level source="⟨body⟩"
188188
body:
189189
block source="⟨stmt⟩"
190190
stmt:
191-
call_expr source="⟨callee⟩(⟨argument⟩ ⟨argument⟩)"
191+
call_expr source="⟨callee⟩(⟨argument⟩, ⟨argument⟩)"
192192
callee: identifier "print" source="print"
193193
argument:
194-
argument source="⟨value⟩,"
194+
argument source="⟨value⟩"
195195
value: identifier "value" source="value"
196196
argument source="⟨value⟩"
197197
value: identifier "timestamp" source="timestamp"

‎unified/extractor/tests/corpus/swift/control-flow/switch-case-item-where-clauses.output‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,9 @@ top_level source="⟨body⟩"
179179
value: string_literal "\"positive\"" source="\"positive\""
180180
switch_case source="case ⟨pattern⟩:\n ⟨body⟩"
181181
pattern:
182-
or_pattern source="⟨pattern⟩ ⟨pattern⟩"
182+
or_pattern source="⟨pattern⟩, ⟨pattern⟩"
183183
pattern:
184-
conditional_pattern source="⟨pattern⟩ where ⟨condition⟩,"
184+
conditional_pattern source="⟨pattern⟩ where ⟨condition⟩"
185185
condition:
186186
binary_expr source="⟨left⟩ ⟨operator⟩ ⟨right⟩"
187187
left: identifier "y" source="y"

0 commit comments

Comments
 (0)