Skip to content

Commit 3edeab0

Browse files
committed
Unified: Exclude trailing commas from locations
Trailing commas appear in a lot of places in the swift-syntax AST, and are all attached to the individual values separated by said commas, not the parent node. This means that the default heuristics thinks a call like `foo(1, 2)` has arguments `1,` and `2`, which is undesirable. Since this applies uniformly for a lot of different comma-separated lists, I decided that adding a generic mechanism for ignoring certain fields was the least invasive change. Thus, we now add `trailingComma` to that list, and this means it's automatically excluded from the location calculations, without need a bunch of specific changes to the existing rules.
1 parent 04a6768 commit 3edeab0

28 files changed

Lines changed: 171 additions & 67 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: 4 additions & 3 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
@@ -1439,6 +1439,7 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
14391439

14401440
pub fn language_spec(desugared_ast_schema: &'static str) -> desugaring::LanguageSpec {
14411441
let config = DesugaringConfig::<SwiftContext>::new()
1442+
.with_ignored_location_fields(["trailingComma"])
14421443
.add_phase("translate", PhaseKind::OneShot, translation_rules())
14431444
.with_output_node_types_yaml(desugared_ast_schema);
14441445
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"

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,9 +333,9 @@ top_level source="⟨body⟩"
333333
member_access_expr source="⟨base⟩⟨member_name_node⟩"
334334
base: inferred_type_expr "." source="."
335335
member_name_node: identifier "inferred" source="inferred"
336-
tuple_expr source="(⟨element⟩ ⟨element⟩)"
336+
tuple_expr source="(⟨element⟩, ⟨element⟩)"
337337
element:
338-
argument source="⟨value⟩,"
338+
argument source="⟨value⟩"
339339
value: identifier "value" source="value"
340340
argument source="⟨value⟩"
341341
value: identifier "offset" source="offset"

‎unified/extractor/tests/corpus/swift/control-flow/switch-with-labeled-case-pattern-arguments.output‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,13 @@ top_level source="⟨body⟩"
151151
value: string_literal "\"yes\"" source="\"yes\""
152152
switch_case source="case ⟨pattern⟩:\n ⟨body⟩"
153153
pattern:
154-
call_expr source="⟨callee⟩(⟨argument⟩ ⟨argument⟩)"
154+
call_expr source="⟨callee⟩(⟨argument⟩, ⟨argument⟩)"
155155
callee:
156156
member_access_expr source="⟨base⟩⟨member_name_node⟩"
157157
base: inferred_type_expr "." source="."
158158
member_name_node: identifier "thread" source="thread"
159159
argument:
160-
argument source="⟨name_node⟩: ⟨value⟩,"
160+
argument source="⟨name_node⟩: ⟨value⟩"
161161
name_node: identifier "threadRowId" source="threadRowId"
162162
value: identifier "_" source="_"
163163
argument source="⟨value⟩"

0 commit comments

Comments
 (0)