fluidattacks_blends/
ast.rs1use 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
17fn 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
30fn 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}