1use blends_domain::ast::AstGraph;
5use blends_domain::syntax;
6use blends_domain::syntax::cfg;
7use blends_domain::syntax::cfg::SyntaxCfgError;
8use blends_domain::syntax::SyntaxGraph;
9use blends_domain::syntax::SyntaxNode;
10
11use crate::content::Content;
12
13fn trace_missing_readers(syntax_graph: &SyntaxGraph, content: &Content) {
14 for (n_id, node) in &syntax_graph.nodes {
15 if let SyntaxNode::MissingNode { node_type } = node {
16 tracing::debug!(
17 node_type,
18 language = ?content.language,
19 node_id = n_id.0,
20 path = %content.path.display(),
21 "missing syntax reader"
22 );
23 }
24 }
25}
26
27fn apply_cfg_overlay(
28 syntax_graph: &mut SyntaxGraph,
29 content: &Content,
30 is_multifile: bool,
31) -> bool {
32 match cfg::add_syntax_cfg(syntax_graph, is_multifile) {
33 Ok(()) => true,
34 Err(error @ SyntaxCfgError::MissingCfgBuilder { .. }) => {
35 tracing::error!(
36 %error,
37 path = %content.path.display(),
38 "missing cfg builder"
39 );
40 true
41 }
42 Err(
43 error @ (SyntaxCfgError::MissingSyntaxNode { .. }
44 | SyntaxCfgError::UnexpectedSyntaxShape { .. }),
45 ) => {
46 tracing::error!(
47 %error,
48 path = %content.path.display(),
49 "unable to build the syntax_graph"
50 );
51 false
52 }
53 }
54}
55
56#[must_use]
57pub fn get_syntax_graph(
58 ast_graph: &AstGraph,
59 content: &Content,
60 with_cfg: Option<bool>,
61 with_metadata: Option<bool>,
62) -> Option<SyntaxGraph> {
63 let path = content.path.display().to_string();
64 let with_metadata = with_metadata.unwrap_or(false);
65 let mut syntax_graph =
66 match syntax::build_syntax_graph(ast_graph, &path, content.language, with_metadata) {
67 Ok(syntax_graph) => syntax_graph,
68 Err(syntax::SyntaxGraphError::UnsupportedLanguage) => {
69 tracing::debug!(
70 language = ?content.language,
71 path = %content.path.display(),
72 "syntax dispatcher not implemented yet"
73 );
74 return None;
75 }
76 Err(error) => {
77 tracing::error!(
78 %error,
79 path = %content.path.display(),
80 "unable to build the syntax_graph"
81 );
82 return None;
83 }
84 };
85
86 trace_missing_readers(&syntax_graph, content);
87
88 if with_cfg.unwrap_or(true) && !apply_cfg_overlay(&mut syntax_graph, content, with_metadata) {
89 return None;
90 }
91
92 syntax_graph.finalize_symbol_index();
93 Some(syntax_graph)
94}
95
96#[cfg(test)]
97mod tests {
98 use super::get_syntax_graph;
99 use crate::ast::get_ast_graph;
100 use crate::content::Content;
101 use crate::language::Language;
102 use blends_domain::ast::{AstGraph, AstNode};
103 use blends_domain::path_search::get_backward_paths;
104 use blends_domain::path_search::search::definition_search;
105 use blends_domain::query::get_all_scope_definitions;
106 use blends_domain::syntax::SyntaxNode;
107 use blends_domain::NodeId;
108 use std::path::PathBuf;
109 use test_case::test_case;
110
111 fn content_for(language: Language, extension: &str, text: &str) -> Content {
112 Content {
113 bytes: text.as_bytes().to_vec(),
114 text: text.to_owned(),
115 language,
116 path: PathBuf::from(format!("test.{extension}")),
117 }
118 }
119
120 fn single_node_ast(node_type: &str) -> AstGraph {
121 let mut ast = AstGraph::new();
122 ast.add_node(NodeId(1), AstNode::new(1, 1, node_type.to_owned()));
123 ast
124 }
125
126 #[test]
127 fn builds_the_syntax_graph_for_a_supported_language() {
128 let content = content_for(Language::Yaml, "yaml", "key: value");
129 let ast = single_node_ast("stream");
130
131 let result = get_syntax_graph(&ast, &content, None, None);
132
133 let graph = result.expect("syntax graph should be built");
134 assert!(!graph.nodes.is_empty());
135 }
136
137 #[test]
138 fn definition_search_resolves_the_second_local_declarator() {
139 let content = content_for(
140 Language::Java,
141 "java",
142 "class T { void run() { String car = \"a\", plane = \"b\", boat = \"c\"; System.out.println(plane); } }",
143 );
144 let ast = get_ast_graph(&content).expect("java source should parse");
145
146 let graph = get_syntax_graph(&ast, &content, None, None).expect("graph should be built");
147
148 let lookup_id = graph
149 .nodes
150 .iter()
151 .find_map(|(n_id, node)| match node {
152 SyntaxNode::SymbolLookup { symbol, .. } if symbol == "plane" => Some(*n_id),
153 _ => None,
154 })
155 .expect("plane lookup should exist");
156 let def_id = get_backward_paths(&graph, lookup_id, None)
157 .iter()
158 .find_map(|path| definition_search(&graph, path, "plane"))
159 .expect("plane definition should resolve");
160
161 assert!(matches!(
162 graph.nodes.get(&def_id),
163 Some(SyntaxNode::VariableDeclaration { variable, .. }) if variable == "plane"
164 ));
165 }
166
167 fn lookup_for(graph: &blends_domain::syntax::SyntaxGraph, wanted: &str) -> NodeId {
168 graph
169 .nodes
170 .iter()
171 .find_map(|(n_id, node)| match node {
172 SyntaxNode::SymbolLookup { symbol, .. } if symbol == wanted => Some(*n_id),
173 _ => None,
174 })
175 .expect("lookup should exist")
176 }
177
178 #[test]
179 fn scope_definitions_resolve_the_second_local_declarator() {
180 let content = content_for(
181 Language::Java,
182 "java",
183 "class T { void run() { String car = \"a\", plane = \"b\", boat = \"c\"; System.out.println(plane); } }",
184 );
185 let ast = get_ast_graph(&content).expect("java source should parse");
186
187 let graph = get_syntax_graph(&ast, &content, None, None).expect("graph should be built");
188
189 let definitions = get_all_scope_definitions(&graph, lookup_for(&graph, "plane"));
190
191 let &[def_id] = definitions.as_slice() else {
192 panic!("expected one scope definition for plane");
193 };
194 assert!(matches!(
195 graph.nodes.get(&def_id),
196 Some(SyntaxNode::VariableDeclaration { variable, variable_type: Some(variable_type), .. })
197 if variable == "plane" && variable_type == "String"
198 ));
199 }
200
201 #[test]
202 fn scope_definitions_resolve_the_second_interface_constant() {
203 let content = content_for(
204 Language::Java,
205 "java",
206 "interface Limits { int MIN = 1, MAX = 10; default int clamp(int value) { return value > MAX ? MAX : value; } }",
207 );
208 let ast = get_ast_graph(&content).expect("java source should parse");
209
210 let graph = get_syntax_graph(&ast, &content, None, None).expect("graph should be built");
211
212 let definitions = get_all_scope_definitions(&graph, lookup_for(&graph, "MAX"));
213
214 let &[def_id] = definitions.as_slice() else {
215 panic!("expected one scope definition for MAX");
216 };
217 assert!(matches!(
218 graph.nodes.get(&def_id),
219 Some(SyntaxNode::VariableDeclaration { variable, variable_type: Some(variable_type), .. })
220 if variable == "MAX" && variable_type == "int"
221 ));
222 }
223
224 #[test]
225 fn metadata_node_added_at_zero_when_requested() {
226 let content = content_for(Language::Yaml, "yaml", "key: value");
227 let ast = single_node_ast("stream");
228
229 let result = get_syntax_graph(&ast, &content, None, Some(true));
230
231 let graph = result.expect("syntax graph should be built");
232 assert_eq!(
233 graph.nodes.get(&NodeId(0)),
234 Some(&SyntaxNode::Metadata {
235 path: "test.yaml".to_owned(),
236 structure: std::collections::BTreeMap::new(),
237 instances: std::collections::BTreeMap::new(),
238 imports: std::vec::Vec::new(),
239 package: None,
240 })
241 );
242 }
243
244 #[test_case(None ; "by default")]
245 #[test_case(Some(false) ; "when explicitly disabled")]
246 fn no_metadata_node(with_metadata: Option<bool>) {
247 let content = content_for(Language::Yaml, "yaml", "key: value");
248 let ast = single_node_ast("stream");
249
250 let result = get_syntax_graph(&ast, &content, None, with_metadata);
251
252 let graph = result.expect("syntax graph should be built");
253 assert!(!graph.nodes.contains_key(&NodeId(0)));
254 }
255
256 #[test]
257 fn language_without_dispatcher_has_no_syntax_graph() {
258 let content = content_for(Language::Kotlin, "kt", "class A {}");
259 let ast = single_node_ast("program");
260
261 assert!(get_syntax_graph(&ast, &content, None, None).is_none());
262 }
263
264 #[test]
265 fn unhandled_root_node_type_degrades_to_missing_node_and_recurses() {
266 let content = content_for(Language::Yaml, "yaml", "key: value");
267 let mut ast = single_node_ast("unknown_node_type_xyz");
268 ast.add_node(NodeId(2), AstNode::new(1, 5, "another_unknown".to_owned()));
269 ast.add_edge(NodeId(1), NodeId(2), 0);
270
271 let result = get_syntax_graph(&ast, &content, None, None);
272
273 let graph = result.expect("syntax graph should be built");
274 assert_eq!(
275 graph.nodes.get(&NodeId(1)),
276 Some(&SyntaxNode::MissingNode {
277 node_type: "unknown_node_type_xyz".to_owned()
278 })
279 );
280 assert_eq!(
281 graph.nodes.get(&NodeId(2)),
282 Some(&SyntaxNode::MissingNode {
283 node_type: "another_unknown".to_owned()
284 })
285 );
286 assert!(graph
287 .edges
288 .get(&NodeId(1))
289 .is_some_and(|adjacent| adjacent.contains_key(&NodeId(2))));
290 }
291
292 #[test]
293 fn fatal_reader_error_returns_none() {
294 let content = content_for(Language::Yaml, "yaml", "[]");
295 let ast = single_node_ast("flow_node");
296
297 assert!(get_syntax_graph(&ast, &content, None, None).is_none());
298 }
299
300 #[test]
301 fn empty_ast_graph_returns_none() {
302 let content = content_for(Language::Yaml, "yaml", "");
303
304 assert!(get_syntax_graph(&AstGraph::new(), &content, None, None).is_none());
305 }
306
307 #[test]
308 fn ast_graph_without_root_node_returns_none() {
309 let content = content_for(Language::Yaml, "yaml", "key: value");
310 let mut ast = AstGraph::new();
311 ast.add_node(NodeId(7), AstNode::new(1, 1, "stream".to_owned()));
312
313 assert!(get_syntax_graph(&ast, &content, None, None).is_none());
314 }
315
316 #[test]
317 fn cfg_marks_overlay_the_ast_edges_by_default() {
318 let content = content_for(Language::Yaml, "yaml", "key: value");
319 let ast = get_ast_graph(&content).expect("yaml should parse");
320
321 let result = get_syntax_graph(&ast, &content, None, None);
322
323 let graph = result.expect("syntax graph should be built");
324 assert!(graph
325 .edges
326 .get(&NodeId(1))
327 .is_some_and(|adjacent| adjacent.values().any(|edge| edge.cfg.is_some())));
328 }
329
330 #[test]
331 fn no_cfg_marks_when_disabled() {
332 let content = content_for(Language::Yaml, "yaml", "key: value");
333 let ast = get_ast_graph(&content).expect("yaml should parse");
334
335 let result = get_syntax_graph(&ast, &content, Some(false), None);
336
337 let graph = result.expect("syntax graph should be built");
338 assert!(graph
339 .edges
340 .values()
341 .flat_map(|adjacent| adjacent.values())
342 .all(|edge| edge.cfg.is_none()));
343 }
344
345 #[test]
346 fn empty_yaml_builds_a_lone_file_node() {
347 let content = content_for(Language::Yaml, "yaml", "");
348 let ast = get_ast_graph(&content).expect("empty yaml should parse");
349
350 let result = get_syntax_graph(&ast, &content, None, None);
351
352 let graph = result.expect("syntax graph should be built");
353 assert_eq!(graph.nodes.len(), 1);
354 assert_eq!(graph.nodes.get(&NodeId(1)), Some(&SyntaxNode::File));
355 }
356}