mirror of
https://github.com/github/codeql.git
synced 2026-05-14 19:29:28 +02:00
Yeast: Add AST dumper for human-readable tree output
Add yeast::dump::dump_ast() which produces indented text output:
program
assignment
left:
left_assignment_list
identifier "x"
identifier "y"
right:
call
method: identifier "foo"
Named fields are shown with "field:" labels, unnamed children are
indented under their parent. Leaf nodes show their text content.
Locations are optional via DumpOptions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
155
shared/yeast/src/dump.rs
Normal file
155
shared/yeast/src/dump.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
use std::fmt::Write;
|
||||
|
||||
use crate::{Ast, Node, NodeContent, CHILD_FIELD};
|
||||
|
||||
/// Options for controlling AST dump output.
|
||||
pub struct DumpOptions {
|
||||
/// Whether to include source locations in the output.
|
||||
pub show_locations: bool,
|
||||
/// Whether to include source text for leaf nodes.
|
||||
pub show_content: bool,
|
||||
}
|
||||
|
||||
impl Default for DumpOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
show_locations: false,
|
||||
show_content: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Dump a yeast AST as a human-readable indented text format.
|
||||
///
|
||||
/// Output format:
|
||||
/// ```text
|
||||
/// program
|
||||
/// assignment
|
||||
/// left:
|
||||
/// left_assignment_list
|
||||
/// identifier "x"
|
||||
/// identifier "y"
|
||||
/// right:
|
||||
/// call
|
||||
/// method:
|
||||
/// identifier "foo"
|
||||
/// ```
|
||||
pub fn dump_ast(ast: &Ast, root: usize, source: &str) -> String {
|
||||
dump_ast_with_options(ast, root, source, &DumpOptions::default())
|
||||
}
|
||||
|
||||
pub fn dump_ast_with_options(ast: &Ast, root: usize, source: &str, options: &DumpOptions) -> String {
|
||||
let mut out = String::new();
|
||||
dump_node(ast, root, source, options, 0, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
fn dump_node(ast: &Ast, id: usize, source: &str, options: &DumpOptions, indent: usize, out: &mut String) {
|
||||
let node = match ast.get_node(id) {
|
||||
Some(n) => n,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let prefix = " ".repeat(indent);
|
||||
|
||||
// Node kind
|
||||
write!(out, "{}{}", prefix, node.kind_name()).unwrap();
|
||||
|
||||
// Location
|
||||
if options.show_locations {
|
||||
let start = node.start_position();
|
||||
let end = node.end_position();
|
||||
write!(out, " [{},{}]-[{},{}]",
|
||||
start.row + 1, start.column + 1,
|
||||
end.row + 1, end.column + 1
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
// Content for leaf nodes
|
||||
if options.show_content && node.is_named() && is_leaf(node) {
|
||||
let content = node_content(node, source);
|
||||
if !content.is_empty() {
|
||||
write!(out, " {:?}", content).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(out).unwrap();
|
||||
|
||||
// Named fields first
|
||||
for (&field_id, children) in &node.fields {
|
||||
if field_id == CHILD_FIELD {
|
||||
continue; // Handle unnamed children last
|
||||
}
|
||||
let field_name = ast.field_name_for_id(field_id).unwrap_or("?");
|
||||
if children.len() == 1 {
|
||||
write!(out, "{} {}:", prefix, field_name).unwrap();
|
||||
// Inline single child
|
||||
let child = ast.get_node(children[0]);
|
||||
if child.map_or(false, |c| is_leaf(c)) {
|
||||
write!(out, " ").unwrap();
|
||||
dump_node_inline(ast, children[0], source, options, out);
|
||||
} else {
|
||||
writeln!(out).unwrap();
|
||||
dump_node(ast, children[0], source, options, indent + 2, out);
|
||||
}
|
||||
} else {
|
||||
writeln!(out, "{} {}:", prefix, field_name).unwrap();
|
||||
for &child_id in children {
|
||||
dump_node(ast, child_id, source, options, indent + 2, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unnamed children
|
||||
if let Some(children) = node.fields.get(&CHILD_FIELD) {
|
||||
for &child_id in children {
|
||||
dump_node(ast, child_id, source, options, indent + 1, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Dump a leaf node inline (no newline prefix, caller provides context).
|
||||
fn dump_node_inline(ast: &Ast, id: usize, source: &str, options: &DumpOptions, out: &mut String) {
|
||||
let node = match ast.get_node(id) {
|
||||
Some(n) => n,
|
||||
None => return,
|
||||
};
|
||||
|
||||
write!(out, "{}", node.kind_name()).unwrap();
|
||||
|
||||
if options.show_locations {
|
||||
let start = node.start_position();
|
||||
let end = node.end_position();
|
||||
write!(out, " [{},{}]-[{},{}]",
|
||||
start.row + 1, start.column + 1,
|
||||
end.row + 1, end.column + 1
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
if options.show_content && node.is_named() {
|
||||
let content = node_content(node, source);
|
||||
if !content.is_empty() {
|
||||
write!(out, " {:?}", content).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(out).unwrap();
|
||||
}
|
||||
|
||||
fn is_leaf(node: &Node) -> bool {
|
||||
node.fields.is_empty()
|
||||
}
|
||||
|
||||
fn node_content(node: &Node, source: &str) -> String {
|
||||
match &node.content {
|
||||
NodeContent::DynamicString(s) if !s.is_empty() => s.clone(),
|
||||
_ => {
|
||||
let range = node.byte_range();
|
||||
if range.start < source.len() && range.end <= source.len() {
|
||||
source[range.start..range.end].to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use serde_json::{json, Value};
|
||||
pub mod build;
|
||||
pub mod captures;
|
||||
pub mod cursor;
|
||||
pub mod dump;
|
||||
pub mod node_types_yaml;
|
||||
pub mod print;
|
||||
pub mod query;
|
||||
@@ -238,7 +239,7 @@ impl Ast {
|
||||
id
|
||||
}
|
||||
|
||||
fn field_name_for_id(&self, id: FieldId) -> Option<&'static str> {
|
||||
pub fn field_name_for_id(&self, id: FieldId) -> Option<&'static str> {
|
||||
if id == CHILD_FIELD {
|
||||
Some(CHILD_FIELD_NAME)
|
||||
} else {
|
||||
@@ -386,8 +387,8 @@ pub struct Node {
|
||||
id: Id,
|
||||
kind: KindId,
|
||||
kind_name: &'static str,
|
||||
fields: BTreeMap<FieldId, Vec<Id>>,
|
||||
content: NodeContent,
|
||||
pub(crate) fields: BTreeMap<FieldId, Vec<Id>>,
|
||||
pub(crate) content: NodeContent,
|
||||
is_named: bool,
|
||||
is_missing: bool,
|
||||
is_extra: bool,
|
||||
@@ -403,6 +404,10 @@ impl Node {
|
||||
self.kind_name
|
||||
}
|
||||
|
||||
pub fn kind_name(&self) -> &'static str {
|
||||
self.kind_name
|
||||
}
|
||||
|
||||
pub fn is_named(&self) -> bool {
|
||||
self.is_named
|
||||
}
|
||||
|
||||
@@ -182,3 +182,15 @@ fn test_shorthand_rule() {
|
||||
assert!(output.contains("\"method\""));
|
||||
assert!(output.contains("\"receiver\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dump_ast() {
|
||||
let input = "x, y = foo()";
|
||||
let runner = Runner::new(tree_sitter_ruby::LANGUAGE.into(), vec![]);
|
||||
let ast = runner.run(input);
|
||||
let output = yeast::dump::dump_ast(&ast, ast.get_root(), input);
|
||||
println!("{}", output);
|
||||
assert!(output.contains("program"));
|
||||
assert!(output.contains("assignment"));
|
||||
assert!(output.contains("identifier"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user