Skip to main content

fluidattacks_blends/
ast.rs

1//! Walk a tree-sitter parse tree into a `domain::ast::AstGraph`.
2
3use std::collections::HashMap;
4
5use blends_domain::ast::{AstGraph, AstNode};
6use blends_domain::NodeId;
7use tree_sitter::Node;
8
9use crate::content::Content;
10use crate::language::Language;
11use crate::parse::parse;
12
13const fn needs_field_resolution(language: Language) -> bool {
14    matches!(language, Language::Swift)
15}
16
17/// Whether a node with children must NOT be descended into (its subtree is
18/// collapsed to a single node)
19fn is_node_terminal(language: Language, kind: &str) -> bool {
20    matches!(
21        (language, kind),
22        (Language::Swift, "bang")
23            | (
24                Language::Yaml,
25                "single_quote_scalar" | "double_quote_scalar"
26            )
27    )
28}
29
30/// Whether a non-leaf node should still capture its source text, ported from
31/// the python `_has_content_node` table (dart arrives with that language).
32fn has_content_node(language: Language, kind: &str) -> bool {
33    matches!(
34        (language, kind),
35        (
36            Language::CSharp,
37            "character_literal" | "predefined_type" | "string_literal" | "verbatim_string_literal"
38        ) | (Language::Go, "interpreted_string_literal")
39            | (
40                Language::JavaScript | Language::TypeScript,
41                "template_string" | "regex"
42            )
43            | (Language::Kotlin, "string_literal")
44            | (Language::Swift, "bang")
45            | (
46                Language::Yaml,
47                "single_quote_scalar" | "double_quote_scalar"
48            )
49    )
50}
51
52fn to_u32(value: usize) -> u32 {
53    u32::try_from(value).unwrap_or(u32::MAX)
54}
55
56fn decode_latin1(bytes: &[u8]) -> String {
57    bytes.iter().map(|&byte| char::from(byte)).collect()
58}
59
60fn build_node(
61    content: &Content,
62    graph: &mut AstGraph,
63    node: Node<'_>,
64    counter: &mut u64,
65) -> NodeId {
66    *counter = counter.saturating_add(1);
67    let id = NodeId(*counter);
68
69    let start = node.start_position();
70    let mut ast_node = AstNode::new(
71        to_u32(start.row.saturating_add(1)),
72        to_u32(start.column.saturating_add(1)),
73        node.kind().to_owned(),
74    );
75
76    let child_count = node.child_count();
77    if child_count == 0 || has_content_node(content.language, node.kind()) {
78        ast_node.text = Some(
79            content
80                .bytes
81                .get(node.start_byte()..node.end_byte())
82                .map(decode_latin1)
83                .unwrap_or_default(),
84        );
85    }
86    graph.add_node(id, ast_node);
87
88    if child_count > 0 && !is_node_terminal(content.language, node.kind()) {
89        build_children(content, graph, node, id, counter);
90    }
91
92    id
93}
94
95type NodeKey = (usize, usize, &'static str);
96
97fn node_key(node: &Node<'_>) -> NodeKey {
98    (node.start_byte(), node.end_byte(), node.kind())
99}
100
101fn field_by_child(parent: Node<'_>) -> HashMap<NodeKey, &'static str> {
102    let language = parent.language();
103    let mut field_ids: Vec<u16> = (1..=language.field_count())
104        .filter_map(|id| u16::try_from(id).ok())
105        .collect();
106    field_ids.sort_by_key(|&id| language.field_name_for_id(id));
107
108    let mut map = HashMap::new();
109    for id in field_ids {
110        if let (Some(name), Some(child)) =
111            (language.field_name_for_id(id), parent.child_by_field_id(id))
112        {
113            map.insert(node_key(&child), name);
114        }
115    }
116    map
117}
118
119fn build_children(
120    content: &Content,
121    graph: &mut AstGraph,
122    parent: Node<'_>,
123    parent_id: NodeId,
124    counter: &mut u64,
125) {
126    let overrides = needs_field_resolution(content.language).then(|| field_by_child(parent));
127
128    let mut cursor = parent.walk();
129    if !cursor.goto_first_child() {
130        return;
131    }
132
133    let mut index: u32 = 0;
134    loop {
135        let child = cursor.node();
136        let field = overrides.as_ref().map_or_else(
137            || cursor.field_name(),
138            |fields| fields.get(&node_key(&child)).copied(),
139        );
140        let child_id = build_node(content, graph, child, counter);
141        graph.add_edge(parent_id, child_id, index);
142
143        if let Some(name) = field {
144            graph.set_field(parent_id, format!("{name}_id"), child_id);
145        }
146
147        index = index.saturating_add(1);
148        if !cursor.goto_next_sibling() {
149            break;
150        }
151    }
152}
153
154pub fn get_ast_graph(content: &Content) -> Option<AstGraph> {
155    let Ok(tree) = parse(content) else {
156        tracing::warn!(path = %content.path.display(), "Unable to parse possibly malformed file");
157        return None;
158    };
159
160    let mut graph = AstGraph::new();
161    let mut counter: u64 = 0;
162    build_node(content, &mut graph, tree.root_node(), &mut counter);
163
164    Some(graph)
165}
166
167#[cfg(test)]
168mod tests {
169    use super::get_ast_graph;
170    use crate::content::Content;
171    use blends_domain::ast::AstGraph;
172    use blends_domain::NodeId;
173    use std::fs;
174
175    fn build_graph(source: &[u8]) -> AstGraph {
176        let dir = tempfile::tempdir().unwrap();
177        let path = dir.path().join("snippet.java");
178        fs::write(&path, source).unwrap();
179        let content = Content::from_path(&path, None).unwrap();
180        get_ast_graph(&content).unwrap()
181    }
182
183    #[test]
184    fn root_node_is_program_at_one_one() {
185        let graph = build_graph(b"class A {}");
186        let root = graph.nodes.get(&NodeId(1)).unwrap();
187
188        assert_eq!(root.kind, "program");
189        assert_eq!(root.line, 1);
190        assert_eq!(root.col, 1);
191    }
192
193    #[test]
194    fn leaf_nodes_carry_their_text() {
195        let graph = build_graph(b"class A {}");
196
197        assert!(graph
198            .nodes
199            .values()
200            .any(|node| node.text.as_deref() == Some("class")));
201    }
202
203    #[test]
204    fn non_leaf_nodes_have_no_text() {
205        let graph = build_graph(b"class A {}");
206        let root = graph.nodes.get(&NodeId(1)).unwrap();
207
208        assert!(root.text.is_none());
209    }
210
211    #[test]
212    fn first_child_edge_is_indexed_zero() {
213        let graph = build_graph(b"class A {}");
214        let from_root = graph.edges.get(&NodeId(1)).unwrap();
215
216        assert!(from_root.values().any(|edge| edge.index == 0));
217    }
218
219    #[test]
220    fn empty_file_still_builds_a_root() {
221        let graph = build_graph(b"");
222
223        assert!(graph.nodes.contains_key(&NodeId(1)));
224    }
225}