#![allow(clippy::enum_glob_use, clippy::if_not_else, clippy::wildcard_imports)]
use serde::{Deserialize, Serialize};
use crate::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[non_exhaustive]
pub struct Span {
pub start_line: usize,
pub start_col: usize,
pub end_line: usize,
pub end_col: usize,
#[serde(default)]
pub start_byte: usize,
#[serde(default)]
pub end_byte: usize,
}
impl Span {
#[must_use]
pub fn new(
start_line: usize,
start_col: usize,
end_line: usize,
end_col: usize,
start_byte: usize,
end_byte: usize,
) -> Self {
Self {
start_line,
start_col,
end_line,
end_col,
start_byte,
end_byte,
}
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AstPayload {
#[serde(default)]
pub id: String,
pub file_name: String,
pub code: String,
#[serde(default)]
pub comment: bool,
#[serde(default)]
pub span: bool,
}
#[derive(Debug, Serialize)]
pub struct AstResponse {
pub id: String,
pub language: String,
pub root: Option<AstNode>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct AstNode {
pub r#type: &'static str,
pub value: String,
pub span: Option<Span>,
pub field_name: Option<&'static str>,
pub children: Vec<AstNode>,
}
impl AstNode {
#[must_use]
pub fn new(
r#type: &'static str,
value: String,
span: Option<Span>,
children: Vec<AstNode>,
) -> Self {
Self::with_field_name(r#type, value, span, None, children)
}
#[must_use]
pub fn with_field_name(
r#type: &'static str,
value: String,
span: Option<Span>,
field_name: Option<&'static str>,
children: Vec<AstNode>,
) -> Self {
Self {
r#type,
value,
span,
field_name,
children,
}
}
}
fn build<T: ParserTrait>(parser: &T, span: bool, comment: bool) -> Option<AstNode> {
struct Frame<'a> {
node: crate::Node<'a>,
field: Option<&'static str>,
children: Vec<AstNode>,
next_child_index: usize,
}
let code = parser.code();
let root = parser.root();
let mut stack: Vec<Frame<'_>> = vec![Frame {
node: root,
field: None,
children: Vec::with_capacity(root.child_count()),
next_child_index: 0,
}];
loop {
let frame = stack
.last_mut()
.expect("stack invariant: loop only runs while stack is non-empty");
let child_count = frame.node.child_count();
if frame.next_child_index < child_count {
let idx = frame.next_child_index;
frame.next_child_index += 1;
let child = frame
.node
.child(idx)
.expect("stack invariant: idx < child_count so the child exists");
let field = frame.node.field_name_for_child(
u32::try_from(idx).expect("invariant: tree-sitter caps child indices at u32::MAX"),
);
stack.push(Frame {
node: child,
field,
children: Vec::with_capacity(child.child_count()),
next_child_index: 0,
});
} else {
let frame = stack
.pop()
.expect("stack invariant: just observed non-empty via last_mut()");
let node = T::Checker::get_ast_node(
&frame.node,
code,
span,
comment,
frame.field,
frame.children,
);
match (node, stack.last_mut()) {
(Some(ast), Some(parent)) => parent.children.push(ast),
(Some(ast), None) => return Some(ast),
(None, None) => return None,
(None, Some(_)) => {}
}
}
}
}
#[derive(Debug)]
pub struct AstCfg {
pub id: String,
pub language: String,
pub comment: bool,
pub span: bool,
}
pub(crate) fn dump_inner<T: ParserTrait>(parser: &T, cfg: AstCfg) -> AstResponse {
AstResponse {
id: cfg.id,
language: cfg.language,
root: build(parser, cfg.span, cfg.comment),
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
fn build_ast<P: ParserTrait>(code: &[u8], filename: &str) -> AstNode {
let path = PathBuf::from(filename);
let parser = P::new(code.to_vec(), &path, None);
let cfg = AstCfg {
id: String::new(),
language: String::new(),
comment: false,
span: false,
};
dump_inner(&parser, cfg)
.root
.expect("parser should produce a root AST node")
}
fn build_ast_with_span<P: ParserTrait>(code: &[u8], filename: &str) -> AstNode {
let path = PathBuf::from(filename);
let parser = P::new(code.to_vec(), &path, None);
let cfg = AstCfg {
id: String::new(),
language: String::new(),
comment: false,
span: true,
};
dump_inner(&parser, cfg)
.root
.expect("parser should produce a root AST node")
}
fn find_first<'a>(node: &'a AstNode, kind: &str) -> Option<&'a AstNode> {
if node.r#type == kind {
return Some(node);
}
node.children.iter().find_map(|c| find_first(c, kind))
}
fn find_child<'a>(parent: &'a AstNode, field: &str) -> Option<&'a AstNode> {
parent.children.iter().find(|c| c.field_name == Some(field))
}
#[test]
fn root_has_no_field_name() {
let root = build_ast::<crate::RustParser>(b"fn main() {}", "test.rs");
assert_eq!(root.field_name, None);
}
#[test]
fn rust_assignment_carries_left_and_right_field_names() {
let root =
build_ast::<crate::RustParser>(b"fn f() { let mut a = 0; a = a + 1; }", "test.rs");
let assign = find_first(&root, "assignment_expression")
.expect("expected an assignment_expression node");
let left = find_child(assign, "left").expect("expected a `left` child");
let right = find_child(assign, "right").expect("expected a `right` child");
assert_eq!(left.field_name, Some("left"));
assert_eq!(right.field_name, Some("right"));
assert!(
assign
.children
.iter()
.any(|c| c.r#type == "=" && c.field_name.is_none()),
"expected the `=` token child to carry no field name; got {:?}",
assign
.children
.iter()
.map(|c| (c.r#type, c.field_name))
.collect::<Vec<_>>(),
);
}
#[test]
fn rust_function_carries_name_and_body_field_names() {
let root =
build_ast::<crate::RustParser>(b"fn greet(name: &str) -> &str { name }", "test.rs");
let func = find_first(&root, "function_item").expect("expected a function_item node");
let name_child = find_child(func, "name").expect("function_item should have a name child");
assert_eq!(name_child.field_name, Some("name"));
assert_eq!(name_child.r#type, "identifier");
let params_child =
find_child(func, "parameters").expect("function_item should have a parameters child");
assert_eq!(params_child.field_name, Some("parameters"));
assert_eq!(params_child.r#type, "parameters");
let body_child = find_child(func, "body").expect("function_item should have a body child");
assert_eq!(body_child.field_name, Some("body"));
assert_eq!(body_child.r#type, "block");
}
#[test]
fn cpp_assignment_carries_left_and_right_field_names() {
let root =
build_ast::<crate::CppParser>(b"int main(){ int x = 0; x = x + 1; }", "test.cpp");
let assign = find_first(&root, "assignment_expression")
.expect("expected an assignment_expression node");
assert_eq!(
find_child(assign, "left").map(|n| n.r#type),
Some("identifier")
);
assert_eq!(
find_child(assign, "right").map(|n| n.r#type),
Some("binary_expression")
);
}
#[test]
fn serialized_json_includes_field_name_key() {
let root = build_ast::<crate::RustParser>(b"fn f(){ let a = 1; }", "test.rs");
let json = serde_json::to_string(&root).expect("serialize");
assert!(
json.contains("\"field_name\""),
"field_name missing from JSON: {json}"
);
assert!(
json.contains("\"field_name\":\"pattern\""),
"expected pattern field name; got {json}"
);
assert!(
json.contains("\"field_name\":\"value\""),
"expected value field name; got {json}"
);
}
#[test]
fn serialized_json_uses_snake_case_keys() {
let root = build_ast_with_span::<crate::RustParser>(b"fn f(){}", "test.rs");
let json = serde_json::to_string(&root).expect("serialize");
for key in [
"\"type\":",
"\"value\":",
"\"span\":",
"\"field_name\":",
"\"children\":",
] {
assert!(json.contains(key), "expected key {key}; got {json}");
}
for legacy in ["\"Type\"", "\"TextValue\"", "\"Span\"", "\"Children\""] {
assert!(
!json.contains(legacy),
"unexpected PascalCase key {legacy}; got {json}"
);
}
}
#[test]
fn span_serializes_as_named_object() {
let root = build_ast_with_span::<crate::RustParser>(b"fn f(){}", "test.rs");
let span = root
.span
.expect("root span present when span tracking is on");
assert_eq!(span, Span::new(1, 1, 1, 9, 0, 8));
let json = serde_json::to_string(&root.span).expect("serialize span");
assert!(
json.contains("\"start_line\":1")
&& json.contains("\"start_col\":1")
&& json.contains("\"end_line\":1")
&& json.contains("\"end_col\":9")
&& json.contains("\"start_byte\":0")
&& json.contains("\"end_byte\":8"),
"expected named span object with byte offsets; got {json}"
);
assert!(
!json.contains("start_row") && !json.contains("end_row"),
"unexpected pre-2.0 *_row span keys; got {json}"
);
}
#[test]
fn span_round_trips_through_serde() {
let span = Span::new(2, 3, 4, 5, 11, 42);
let json = serde_json::to_string(&span).expect("serialize");
let back: Span = serde_json::from_str(&json).expect("deserialize");
assert_eq!(span, back);
}
#[test]
fn span_deserializes_pre_byte_offsets_with_default() {
let legacy = r#"{"start_line":2,"start_col":3,"end_line":4,"end_col":5}"#;
let span: Span = serde_json::from_str(legacy).expect("deserialize legacy span");
assert_eq!(span, Span::new(2, 3, 4, 5, 0, 0));
}
}