Skip to content

Commit c153984

Browse files
committed
yeast: Handle empty locations correctly
The location 0..0 was being treated as "no useable location", which at the beginning of a file could cause problems. We now represent this as None instead, making Some(0..0) a valid empty location.
1 parent e10de64 commit c153984

4 files changed

Lines changed: 99 additions & 73 deletions

File tree

‎shared/yeast/src/lib.rs‎

Lines changed: 27 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,7 @@ impl YeastDisplay for Id {
144144

145145
impl YeastSourceRange for Id {
146146
fn yeast_source_range(&self, ast: &Ast) -> Option<Range> {
147-
ast.get_node(*self).and_then(|n| match &n.content {
148-
NodeContent::Range(r) => Some(*r),
149-
_ => n.source_range,
150-
})
147+
ast.get_node(*self).and_then(Node::source_range)
151148
}
152149
}
153150

@@ -598,7 +595,7 @@ impl Ast {
598595
.union_source_range_of_children(&fields)
599596
.map_or(source_range, |child_range| {
600597
Some(match source_range {
601-
Some(source_range) => union_source_ranges(child_range, source_range),
598+
Some(source_range) => child_range.union(source_range),
602599
None => child_range,
603600
})
604601
}),
@@ -631,7 +628,7 @@ impl Ast {
631628
panic!("extend_source_range: cannot modify a parsed node");
632629
}
633630
node.source_range = Some(match node.source_range {
634-
Some(existing) => union_source_ranges(existing, source_range),
631+
Some(existing) => existing.union(source_range),
635632
None => source_range,
636633
});
637634
}
@@ -673,35 +670,30 @@ impl Ast {
673670
let Some(child) = self.get_node(child_id) else {
674671
continue;
675672
};
676-
677-
let child_start_byte = child.start_byte();
678-
let child_end_byte = child.end_byte();
679-
680-
// Skip children that carry no usable location.
681-
if child_start_byte == 0 && child_end_byte == 0 {
673+
let Some(child_range) = child.source_range() else {
682674
continue;
683-
}
675+
};
684676

685677
match start_byte {
686678
None => {
687-
start_byte = Some(child_start_byte);
688-
start_point = child.start_position();
679+
start_byte = Some(child_range.start_byte);
680+
start_point = child_range.start_point;
689681
}
690-
Some(current_start) if child_start_byte < current_start => {
691-
start_byte = Some(child_start_byte);
692-
start_point = child.start_position();
682+
Some(current_start) if child_range.start_byte < current_start => {
683+
start_byte = Some(child_range.start_byte);
684+
start_point = child_range.start_point;
693685
}
694686
_ => {}
695687
}
696688

697689
match end_byte {
698690
None => {
699-
end_byte = Some(child_end_byte);
700-
end_point = child.end_position();
691+
end_byte = Some(child_range.end_byte);
692+
end_point = child_range.end_point;
701693
}
702-
Some(current_end) if child_end_byte > current_end => {
703-
end_byte = Some(child_end_byte);
704-
end_point = child.end_position();
694+
Some(current_end) if child_range.end_byte > current_end => {
695+
end_byte = Some(child_range.end_byte);
696+
end_point = child_range.end_point;
705697
}
706698
_ => {}
707699
}
@@ -810,25 +802,6 @@ impl Ast {
810802
}
811803
}
812804

813-
fn union_source_ranges(first: Range, second: Range) -> Range {
814-
let (start_byte, start_point) = if first.start_byte <= second.start_byte {
815-
(first.start_byte, first.start_point)
816-
} else {
817-
(second.start_byte, second.start_point)
818-
};
819-
let (end_byte, end_point) = if first.end_byte >= second.end_byte {
820-
(first.end_byte, first.end_point)
821-
} else {
822-
(second.end_byte, second.end_point)
823-
};
824-
Range {
825-
start_byte,
826-
end_byte,
827-
start_point,
828-
end_point,
829-
}
830-
}
831-
832805
/// A node in our AST
833806
#[derive(PartialEq, Eq, Debug, Clone, Serialize)]
834807
pub struct Node {
@@ -871,36 +844,29 @@ impl Node {
871844
Point { row: 0, column: 0 }
872845
}
873846

874-
pub fn start_position(&self) -> Point {
847+
pub fn source_range(&self) -> Option<Range> {
875848
match self.content {
876-
NodeContent::Range(range) => range.start_point,
877-
_ => self
878-
.source_range
879-
.map_or_else(|| self.fake_point(), |r| r.start_point),
849+
NodeContent::Range(range) => Some(range),
850+
_ => self.source_range,
880851
}
881852
}
882853

854+
pub fn start_position(&self) -> Point {
855+
self.source_range()
856+
.map_or_else(|| self.fake_point(), |range| range.start_point)
857+
}
858+
883859
pub fn end_position(&self) -> Point {
884-
match self.content {
885-
NodeContent::Range(range) => range.end_point,
886-
_ => self
887-
.source_range
888-
.map_or_else(|| self.fake_point(), |r| r.end_point),
889-
}
860+
self.source_range()
861+
.map_or_else(|| self.fake_point(), |range| range.end_point)
890862
}
891863

892864
pub fn start_byte(&self) -> usize {
893-
match self.content {
894-
NodeContent::Range(range) => range.start_byte,
895-
_ => self.source_range.map_or(0, |r| r.start_byte),
896-
}
865+
self.source_range().map_or(0, |range| range.start_byte)
897866
}
898867

899868
pub fn end_byte(&self) -> usize {
900-
match self.content {
901-
NodeContent::Range(range) => range.end_byte,
902-
_ => self.source_range.map_or(0, |r| r.end_byte),
903-
}
869+
self.source_range().map_or(0, |range| range.end_byte)
904870
}
905871

906872
pub fn byte_range(&self) -> std::ops::Range<usize> {

‎shared/yeast/src/range.rs‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,43 @@ pub struct Range {
3030
pub start_point: Point,
3131
pub end_point: Point,
3232
}
33+
34+
impl Range {
35+
/// Return the smallest range containing both ranges.
36+
pub fn union(self, other: Self) -> Self {
37+
let (start_byte, start_point) = if self.start_byte <= other.start_byte {
38+
(self.start_byte, self.start_point)
39+
} else {
40+
(other.start_byte, other.start_point)
41+
};
42+
let (end_byte, end_point) = if self.end_byte >= other.end_byte {
43+
(self.end_byte, self.end_point)
44+
} else {
45+
(other.end_byte, other.end_point)
46+
};
47+
Self {
48+
start_byte,
49+
end_byte,
50+
start_point,
51+
end_point,
52+
}
53+
}
54+
55+
/// Return an empty range anchored at this range's start.
56+
pub fn empty_at_start(self) -> Self {
57+
Self {
58+
end_byte: self.start_byte,
59+
end_point: self.start_point,
60+
..self
61+
}
62+
}
63+
64+
/// Return an empty range anchored at this range's end.
65+
pub fn empty_at_end(self) -> Self {
66+
Self {
67+
start_byte: self.end_byte,
68+
start_point: self.end_point,
69+
..self
70+
}
71+
}
72+
}

‎shared/yeast/tests/test.rs‎

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

1712+
/// An explicit empty range at byte zero is a real location, not the sentinel
1713+
/// for an absent location, and therefore contributes to parent ranges.
1714+
#[test]
1715+
fn test_empty_range_at_file_start_contributes_to_parent() {
1716+
use std::collections::BTreeMap;
1717+
1718+
let lang: tree_sitter::Language = tree_sitter_ruby::LANGUAGE.into();
1719+
let schema =
1720+
yeast::node_types_yaml::schema_from_yaml_with_language(OUTPUT_SCHEMA_YAML, &lang).unwrap();
1721+
let mut ast = Ast::with_schema(schema);
1722+
let empty = Range {
1723+
start_byte: 0,
1724+
end_byte: 0,
1725+
start_point: Point::new(0, 0),
1726+
end_point: Point::new(0, 0),
1727+
};
1728+
let child =
1729+
ast.create_named_token_with_range("identifier", "synthetic".to_owned(), Some(empty));
1730+
let fields = BTreeMap::from([(ast.field_id_for_name("method").unwrap(), vec![child])]);
1731+
let parent = ast.create_node_with_range(
1732+
ast.id_for_node_kind("call").unwrap(),
1733+
NodeContent::DynamicString(String::new()),
1734+
fields,
1735+
true,
1736+
None,
1737+
);
1738+
1739+
assert_eq!(ast.get_node(parent).unwrap().source_range(), Some(empty));
1740+
}
1741+
17121742
/// A rule that only unwraps and returns a translated capture must not widen
17131743
/// that capture to the wrapper's source range.
17141744
#[test]

‎unified/extractor/tests/location_tests.rs‎

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ fn ranges(ast: &Ast, kind: &str, content: Option<&str>) -> Vec<std::ops::Range<u
3737
{
3838
return None;
3939
}
40-
Some(node.byte_range())
40+
node.source_range()
41+
.map(|range| range.start_byte..range.end_byte)
4142
})
4243
.collect()
4344
}
@@ -50,16 +51,6 @@ fn assert_has_span(ast: &Ast, source: &str, kind: &str, content: Option<&str>, e
5051
);
5152
}
5253

53-
fn assert_has_empty_span(ast: &Ast, kind: &str, content: Option<&str>, expected_offset: usize) {
54-
let ranges = ranges(ast, kind, content);
55-
assert!(
56-
ranges
57-
.iter()
58-
.any(|range| range.start == expected_offset && range.end == expected_offset),
59-
"expected {kind} {content:?} to have an empty span at {expected_offset}, got {ranges:?}"
60-
);
61-
}
62-
6354
#[test]
6455
fn generic_type_children_have_local_ranges() {
6556
let source = "let x = C<Foo>()";
@@ -96,5 +87,4 @@ fn import_member_chain_excludes_import_keyword() {
9687
None,
9788
"import Foundation.Networking.URLSession",
9889
);
99-
assert_has_empty_span(&ast, "bulk_importing_pattern", None, 0);
10090
}

0 commit comments

Comments
 (0)