Skip to content

Commit 09d8d9a

Browse files
authored
Merge pull request #22624 from github/tausbn/unified-corpus-source-skeletons
unified: Output source code skeletons in AST dump
2 parents cdcde47 + a0f9f35 commit 09d8d9a

141 files changed

Lines changed: 2210 additions & 1943 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

shared/yeast/doc/yeast.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,17 @@ yeast::trees!(ctx,
235235
(identifier #{name}) // an identifier from a Rust variable
236236
```
237237

238+
For reviewing locations, `DumpOptions::show_abridged_source` prints each node's
239+
source range with every direct child replaced by its field name in Unicode
240+
angle brackets. This keeps delimiters and other parent-owned syntax visible
241+
without repeating entire subtrees:
242+
243+
```text
244+
return_expr source="return ⟨value⟩"
245+
value:
246+
call_expr source="⟨callee⟩(⟨argument⟩)"
247+
```
248+
238249
### Optional fields (`?`)
239250

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

shared/yeast/src/dump.rs

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,17 @@ pub struct DumpOptions {
1515
pub show_locations: bool,
1616
/// Whether to include source text for leaf nodes.
1717
pub show_content: bool,
18+
/// Whether to include each node's source range with direct-child ranges
19+
/// replaced by their field names in `⟨angle brackets⟩`.
20+
pub show_abridged_source: bool,
1821
}
1922

2023
impl Default for DumpOptions {
2124
fn default() -> Self {
2225
Self {
2326
show_locations: false,
2427
show_content: true,
28+
show_abridged_source: false,
2529
}
2630
}
2731
}
@@ -226,6 +230,10 @@ fn dump_node(
226230
}
227231
}
228232

233+
if options.show_abridged_source {
234+
write_source_skeleton(ast, node, source, out);
235+
}
236+
229237
if let Some(context) = type_check {
230238
if let Some(err) =
231239
type_error_for_node(context.schema, node, context.expected, context.parent_field)
@@ -409,6 +417,10 @@ fn dump_node_inline(
409417
}
410418
}
411419

420+
if options.show_abridged_source {
421+
write_source_skeleton(ast, node, source, out);
422+
}
423+
412424
if let Some(context) = type_check {
413425
if let Some(err) =
414426
type_error_for_node(context.schema, node, context.expected, context.parent_field)
@@ -424,6 +436,246 @@ fn is_leaf(node: &Node) -> bool {
424436
node.fields.is_empty()
425437
}
426438

439+
enum SourceSkeleton {
440+
Missing,
441+
Text(String),
442+
Invalid(String),
443+
}
444+
445+
fn write_source_skeleton(ast: &Ast, node: &Node, source: &str, out: &mut String) {
446+
match source_skeleton(ast, node, source) {
447+
SourceSkeleton::Missing => write!(out, " source=<no location>").unwrap(),
448+
SourceSkeleton::Text(text) => write!(out, " source={text:?}").unwrap(),
449+
SourceSkeleton::Invalid(error) => write!(out, " source=<invalid: {error}>").unwrap(),
450+
}
451+
}
452+
453+
fn node_source_range(node: &Node) -> Option<crate::Range> {
454+
match node.content {
455+
NodeContent::Range(range) => Some(range),
456+
_ => node.source_range,
457+
}
458+
}
459+
460+
fn source_skeleton(ast: &Ast, node: &Node, source: &str) -> SourceSkeleton {
461+
let Some(parent) = node_source_range(node) else {
462+
return SourceSkeleton::Missing;
463+
};
464+
let parent = parent.start_byte..parent.end_byte;
465+
if parent.start > parent.end || source.get(parent.clone()).is_none() {
466+
return SourceSkeleton::Invalid(format!(
467+
"node range {}..{} is outside the source or not on UTF-8 boundaries",
468+
parent.start, parent.end
469+
));
470+
}
471+
472+
let mut children = Vec::new();
473+
for (field_id, child_ids) in &node.fields {
474+
let field_name = if *field_id == CHILD_FIELD {
475+
"child"
476+
} else {
477+
ast.field_name_for_id(*field_id).unwrap_or("?")
478+
};
479+
for child_id in child_ids {
480+
let Some(child) = ast.get_node(*child_id) else {
481+
continue;
482+
};
483+
if !child.is_named() {
484+
continue;
485+
}
486+
let Some(child) = node_source_range(child) else {
487+
continue;
488+
};
489+
let child = child.start_byte..child.end_byte;
490+
if child.start > child.end || source.get(child.clone()).is_none() {
491+
return SourceSkeleton::Invalid(format!(
492+
"child range {}..{} is outside the source or not on UTF-8 boundaries",
493+
child.start, child.end
494+
));
495+
}
496+
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+
));
501+
}
502+
if child.start == child.end {
503+
continue;
504+
}
505+
children.push((child, field_name));
506+
}
507+
}
508+
children.sort_by_key(|(range, field_name)| (range.start, range.end, *field_name));
509+
510+
let mut result = String::new();
511+
let mut cursor = parent.start;
512+
for (range, field_name) in children {
513+
if cursor < range.start {
514+
result.push_str(source.get(cursor..range.start).unwrap());
515+
}
516+
result.push('⟨');
517+
result.push_str(field_name);
518+
result.push('⟩');
519+
cursor = cursor.max(range.end);
520+
}
521+
result.push_str(source.get(cursor..parent.end).unwrap());
522+
SourceSkeleton::Text(result)
523+
}
524+
525+
#[cfg(test)]
526+
mod tests {
527+
use super::*;
528+
use crate::{NodeContent, Point, Range};
529+
use std::collections::BTreeMap;
530+
531+
fn range(start: usize, end: usize) -> Range {
532+
Range {
533+
start_byte: start,
534+
end_byte: end,
535+
start_point: Point::new(0, start),
536+
end_point: Point::new(0, end),
537+
}
538+
}
539+
540+
fn dump_with_children(source: &str, parent_range: Range, children: &[(&str, Range)]) -> String {
541+
let mut ast = Ast::with_schema(crate::schema::Schema::new());
542+
let parent_kind = ast.register_kind("parent");
543+
let child_kind = ast.register_kind("child");
544+
let mut fields = BTreeMap::new();
545+
for (field_name, range) in children {
546+
let field = ast.register_field(field_name);
547+
let child = ast.create_node_with_range(
548+
child_kind,
549+
NodeContent::Range(*range),
550+
BTreeMap::new(),
551+
true,
552+
None,
553+
);
554+
fields.entry(field).or_insert_with(Vec::new).push(child);
555+
}
556+
let parent = ast.create_node_with_range(
557+
parent_kind,
558+
NodeContent::Range(parent_range),
559+
fields,
560+
true,
561+
None,
562+
);
563+
ast.set_root(parent);
564+
565+
dump_ast_with_options(
566+
&ast,
567+
parent,
568+
source,
569+
&DumpOptions {
570+
show_locations: false,
571+
show_content: false,
572+
show_abridged_source: true,
573+
},
574+
)
575+
}
576+
577+
#[test]
578+
fn source_skeleton_elides_direct_children_and_ignores_empty_ranges() {
579+
let source = "αbefore(child)afterω";
580+
let child_start = source.find("child").unwrap();
581+
let child_end = child_start + "child".len();
582+
let dump = dump_with_children(
583+
source,
584+
range(0, source.len()),
585+
&[
586+
("value", range(child_start, child_end)),
587+
("marker", range(child_start, child_start)),
588+
],
589+
);
590+
591+
assert!(dump.starts_with("parent source=\"αbefore(⟨value⟩)afterω\"\n"));
592+
}
593+
594+
#[test]
595+
fn source_skeleton_preserves_unnamed_tokens() {
596+
let source = "x = 1";
597+
let runner: crate::Runner = crate::Runner::new(tree_sitter_ruby::LANGUAGE.into(), &[]);
598+
let ast = runner.run(source).unwrap();
599+
let dump = dump_ast_with_options(
600+
&ast,
601+
ast.get_root(),
602+
source,
603+
&DumpOptions {
604+
show_locations: false,
605+
show_content: false,
606+
show_abridged_source: true,
607+
},
608+
);
609+
610+
assert!(dump.contains("assignment source=\"⟨left⟩ = ⟨right⟩\""));
611+
}
612+
613+
#[test]
614+
fn source_skeleton_validates_empty_child_ranges() {
615+
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+
),
622+
(
623+
"abcdef",
624+
range(0, 6),
625+
range(7, 7),
626+
"child range 7..7 is outside the source or not on UTF-8 boundaries",
627+
),
628+
(
629+
"αbc",
630+
range(0, 4),
631+
range(1, 1),
632+
"child range 1..1 is outside the source or not on UTF-8 boundaries",
633+
),
634+
];
635+
636+
for (source, parent, child, error) in cases {
637+
let dump = dump_with_children(source, parent, &[("marker", child)]);
638+
assert!(
639+
dump.starts_with(&format!("parent source=<invalid: {error}>\n")),
640+
"unexpected dump: {dump}"
641+
);
642+
}
643+
}
644+
645+
#[test]
646+
fn source_skeleton_keeps_adjacent_child_fields_separate() {
647+
let source = "abcdef";
648+
let dump = dump_with_children(
649+
source,
650+
range(0, source.len()),
651+
&[("left", range(1, 3)), ("right", range(3, 5))],
652+
);
653+
654+
assert!(dump.starts_with("parent source=\"a⟨left⟩⟨right⟩f\"\n"));
655+
}
656+
657+
#[test]
658+
fn source_skeleton_keeps_overlapping_child_fields_separate() {
659+
let source = "abcdef";
660+
let dump = dump_with_children(
661+
source,
662+
range(0, source.len()),
663+
&[("left", range(1, 4)), ("right", range(3, 5))],
664+
);
665+
666+
assert!(dump.starts_with("parent source=\"a⟨left⟩⟨right⟩f\"\n"));
667+
}
668+
669+
#[test]
670+
fn source_skeleton_reports_children_outside_the_parent() {
671+
let source = "abcdefghi";
672+
let dump = dump_with_children(source, range(0, 6), &[("child", range(7, 9))]);
673+
674+
assert!(dump
675+
.starts_with("parent source=<invalid: child range 7..9 is outside node range 0..6>\n"));
676+
}
677+
}
678+
427679
fn node_content(node: &Node, source: &str) -> String {
428680
match &node.content {
429681
NodeContent::DynamicString(s) if !s.is_empty() => s.clone(),

unified/extractor/tests/corpus/swift/closures/closure-with-capture-list.output

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,24 +60,24 @@ sourceFile
6060

6161
---
6262

63-
top_level
63+
top_level source="⟨body⟩"
6464
body:
65-
block
65+
block source="⟨stmt⟩"
6666
stmt:
67-
variable_declaration
68-
modifier: modifier "let"
69-
pattern: identifier "f"
67+
variable_declaration source="⟨modifier⟩ ⟨pattern⟩ = ⟨value⟩"
68+
modifier: modifier "let" source="let"
69+
pattern: identifier "f" source="f"
7070
value:
71-
function_expr
71+
function_expr source="⟨body⟩⟨capture_declaration⟩"
7272
capture_declaration:
73-
variable_declaration
74-
modifier: modifier "weak"
75-
pattern: identifier "self"
73+
variable_declaration source="⟨modifier⟩ ⟨pattern⟩"
74+
modifier: modifier "weak" source="weak"
75+
pattern: identifier "self" source="self"
7676
body:
77-
block
77+
block source="{ [weak self] in ⟨stmt⟩ }"
7878
stmt:
79-
call_expr
79+
call_expr source="⟨callee⟩()"
8080
callee:
81-
member_access_expr
82-
base: identifier "self"
83-
member_name_node: identifier "doThing"
81+
member_access_expr source="⟨base⟩?.⟨member_name_node⟩"
82+
base: identifier "self" source="self"
83+
member_name_node: identifier "doThing" source="doThing"

unified/extractor/tests/corpus/swift/closures/closure-with-explicit-parameters.output

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -62,24 +62,24 @@ sourceFile
6262

6363
---
6464

65-
top_level
65+
top_level source="⟨body⟩"
6666
body:
67-
block
67+
block source="⟨stmt⟩"
6868
stmt:
69-
variable_declaration
70-
modifier: modifier "let"
71-
pattern: identifier "f"
69+
variable_declaration source="⟨modifier⟩ ⟨pattern⟩ = ⟨value⟩"
70+
modifier: modifier "let" source="let"
71+
pattern: identifier "f" source="f"
7272
value:
73-
function_expr
73+
function_expr source="⟨body⟩⟨parameter⟩⟨return_type⟩"
7474
parameter:
75-
parameter
76-
type: identifier "Int"
77-
pattern: identifier "x"
78-
return_type: identifier "Int"
75+
parameter source="⟨pattern⟩: ⟨type⟩"
76+
type: identifier "Int" source="Int"
77+
pattern: identifier "x" source="x"
78+
return_type: identifier "Int" source="Int"
7979
body:
80-
block
80+
block source="{ (x: Int) -> Int in ⟨stmt⟩ }"
8181
stmt:
82-
binary_expr
83-
left: identifier "x"
84-
operator: infix_operator "*"
85-
right: int_literal "2"
82+
binary_expr source="⟨left⟩ ⟨operator⟩ ⟨right⟩"
83+
left: identifier "x" source="x"
84+
operator: infix_operator "*" source="*"
85+
right: int_literal "2" source="2"

0 commit comments

Comments
 (0)