Skip to content

Commit c8d3608

Browse files
committed
yeast: Annotate children outside parent source ranges
In some cases (I'm looking at you, access_declaration) we link up things like the modifier of an accessor to a node that lives _outside_ of the declaration itself. Previously this was treated as an error (and the "fix" was to extend the source ranges), but this lead to some weird source skeletons in the output. Instead, we now allow the source of a node to be anywhere (within reason -- it has to be inside the file, and properly UTF-8 aligned), and simply add a trailing ` (external)` to the source annotation. Thus, the fields containing these values do not appear in the source skeleton for the node in question.
1 parent 3edeab0 commit c8d3608

2 files changed

Lines changed: 59 additions & 15 deletions

File tree

‎shared/yeast/doc/yeast.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,17 @@ return_expr source="return ⟨value⟩"
292292
call_expr source="⟨callee⟩(⟨argument⟩)"
293293
```
294294

295+
Children outside the node's source range retain their own locations and are
296+
annotated where they are printed rather than being treated as errors:
297+
298+
```text
299+
accessor_declaration source="⟨accessor_kind⟩"
300+
name_node: identifier "value" source="value" (external)
301+
```
302+
303+
Node and child ranges are still validated against the source text and UTF-8
304+
boundaries.
305+
295306
### Optional fields (`?`)
296307

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

‎shared/yeast/src/dump.rs‎

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ pub fn dump_ast(ast: &Ast, root: Id, source: &str) -> String {
5151

5252
pub fn dump_ast_with_options(ast: &Ast, root: Id, source: &str, options: &DumpOptions) -> String {
5353
let mut out = String::new();
54-
dump_node(ast, root, source, options, 0, None, &mut out);
54+
dump_node(ast, root, source, options, 0, None, false, &mut out);
5555
out
5656
}
5757

@@ -86,6 +86,7 @@ pub fn dump_ast_with_type_errors_and_options(
8686
expected: None,
8787
parent_field: None,
8888
}),
89+
false,
8990
&mut out,
9091
);
9192
out
@@ -195,6 +196,7 @@ fn dump_node(
195196
options: &DumpOptions,
196197
indent: usize,
197198
type_check: Option<TypeCheckContext<'_>>,
199+
external_to_parent: bool,
198200
out: &mut String,
199201
) {
200202
let node = match ast.get_node(id) {
@@ -232,6 +234,9 @@ fn dump_node(
232234

233235
if options.show_abridged_source {
234236
write_source_skeleton(ast, node, source, out);
237+
if external_to_parent {
238+
write!(out, " (external)").unwrap();
239+
}
235240
}
236241

237242
if let Some(context) = type_check {
@@ -300,9 +305,18 @@ fn dump_node(
300305
write!(out, "{prefix} {field_name}:").unwrap();
301306
// Inline single child
302307
let child = ast.get_node(children[0]);
308+
let external = child.is_some_and(|child| is_external_child(node, child));
303309
if child.is_some_and(is_leaf) {
304310
write!(out, " ").unwrap();
305-
dump_node_inline(ast, children[0], source, options, child_type_check, out);
311+
dump_node_inline(
312+
ast,
313+
children[0],
314+
source,
315+
options,
316+
child_type_check,
317+
external,
318+
out,
319+
);
306320
} else {
307321
writeln!(out).unwrap();
308322
dump_node(
@@ -312,19 +326,24 @@ fn dump_node(
312326
options,
313327
indent + 2,
314328
child_type_check,
329+
external,
315330
out,
316331
);
317332
}
318333
} else {
319334
writeln!(out, "{prefix} {field_name}:").unwrap();
320335
for &child_id in children {
336+
let external = ast
337+
.get_node(child_id)
338+
.is_some_and(|child| is_external_child(node, child));
321339
dump_node(
322340
ast,
323341
child_id,
324342
source,
325343
options,
326344
indent + 2,
327345
child_type_check,
346+
external,
328347
out,
329348
);
330349
}
@@ -365,13 +384,15 @@ fn dump_node(
365384
for &child_id in children {
366385
if let Some(child) = ast.get_node(child_id) {
367386
if child.is_named() {
387+
let external = is_external_child(node, child);
368388
dump_node(
369389
ast,
370390
child_id,
371391
source,
372392
options,
373393
indent + 1,
374394
child_type_check,
395+
external,
375396
out,
376397
);
377398
}
@@ -387,6 +408,7 @@ fn dump_node_inline(
387408
source: &str,
388409
options: &DumpOptions,
389410
type_check: Option<TypeCheckContext<'_>>,
411+
external_to_parent: bool,
390412
out: &mut String,
391413
) {
392414
let node = match ast.get_node(id) {
@@ -419,6 +441,9 @@ fn dump_node_inline(
419441

420442
if options.show_abridged_source {
421443
write_source_skeleton(ast, node, source, out);
444+
if external_to_parent {
445+
write!(out, " (external)").unwrap();
446+
}
422447
}
423448

424449
if let Some(context) = type_check {
@@ -457,6 +482,13 @@ fn node_source_range(node: &Node) -> Option<crate::Range> {
457482
}
458483
}
459484

485+
fn is_external_child(parent: &Node, child: &Node) -> bool {
486+
let (Some(parent), Some(child)) = (node_source_range(parent), node_source_range(child)) else {
487+
return false;
488+
};
489+
child.start_byte < parent.start_byte || child.end_byte > parent.end_byte
490+
}
491+
460492
fn source_skeleton(ast: &Ast, node: &Node, source: &str) -> SourceSkeleton {
461493
let Some(parent) = node_source_range(node) else {
462494
return SourceSkeleton::Missing;
@@ -494,10 +526,7 @@ fn source_skeleton(ast: &Ast, node: &Node, source: &str) -> SourceSkeleton {
494526
));
495527
}
496528
if child.start < parent.start || child.end > parent.end {
497-
return SourceSkeleton::Invalid(format!(
498-
"child range {}..{} is outside node range {}..{}",
499-
child.start, child.end, parent.start, parent.end
500-
));
529+
continue;
501530
}
502531
if child.start == child.end {
503532
continue;
@@ -613,12 +642,6 @@ mod tests {
613642
#[test]
614643
fn source_skeleton_validates_empty_child_ranges() {
615644
let cases = [
616-
(
617-
"abcdef",
618-
range(0, 3),
619-
range(4, 4),
620-
"child range 4..4 is outside node range 0..3",
621-
),
622645
(
623646
"abcdef",
624647
range(0, 6),
@@ -640,6 +663,13 @@ mod tests {
640663
"unexpected dump: {dump}"
641664
);
642665
}
666+
667+
let dump = dump_with_children("abcdef", range(0, 3), &[("marker", range(4, 4))]);
668+
assert!(dump.starts_with("parent source=\"abc\"\n"));
669+
assert!(
670+
dump.contains("marker: child source=\"\" (external)\n"),
671+
"unexpected dump: {dump}"
672+
);
643673
}
644674

645675
#[test]
@@ -667,12 +697,15 @@ mod tests {
667697
}
668698

669699
#[test]
670-
fn source_skeleton_reports_children_outside_the_parent() {
700+
fn source_skeleton_marks_children_outside_the_parent() {
671701
let source = "abcdefghi";
672702
let dump = dump_with_children(source, range(0, 6), &[("child", range(7, 9))]);
673703

674-
assert!(dump
675-
.starts_with("parent source=<invalid: child range 7..9 is outside node range 0..6>\n"));
704+
assert!(dump.starts_with("parent source=\"abcdef\"\n"));
705+
assert!(
706+
dump.contains(" child source=\"hi\" (external)\n"),
707+
"unexpected dump: {dump}"
708+
);
676709
}
677710
}
678711

0 commit comments

Comments
 (0)