use blends_domain::ast::AstGraph;
use blends_domain::syntax;
use blends_domain::syntax::cfg;
use blends_domain::syntax::cfg::SyntaxCfgError;
use blends_domain::syntax::SyntaxGraph;
use blends_domain::syntax::SyntaxNode;
use crate::content::Content;
fn trace_missing_readers(syntax_graph: &SyntaxGraph, content: &Content) {
for (n_id, node) in &syntax_graph.nodes {
if let SyntaxNode::MissingNode { node_type } = node {
tracing::debug!(
node_type,
language = ?content.language,
node_id = n_id.0,
path = %content.path.display(),
"missing syntax reader"
);
}
}
}
fn apply_cfg_overlay(
syntax_graph: &mut SyntaxGraph,
content: &Content,
is_multifile: bool,
) -> bool {
match cfg::add_syntax_cfg(syntax_graph, is_multifile) {
Ok(()) => true,
Err(error @ SyntaxCfgError::MissingCfgBuilder { .. }) => {
tracing::error!(
%error,
path = %content.path.display(),
"missing cfg builder"
);
true
}
Err(
error @ (SyntaxCfgError::MissingSyntaxNode { .. }
| SyntaxCfgError::UnexpectedSyntaxShape { .. }),
) => {
tracing::error!(
%error,
path = %content.path.display(),
"unable to build the syntax_graph"
);
false
}
}
}
#[must_use]
pub fn get_syntax_graph(
ast_graph: &AstGraph,
content: &Content,
with_cfg: Option<bool>,
with_metadata: Option<bool>,
) -> Option<SyntaxGraph> {
let path = content.path.display().to_string();
let with_metadata = with_metadata.unwrap_or(false);
let mut syntax_graph =
match syntax::build_syntax_graph(ast_graph, &path, content.language, with_metadata) {
Ok(syntax_graph) => syntax_graph,
Err(syntax::SyntaxGraphError::UnsupportedLanguage) => {
tracing::debug!(
language = ?content.language,
path = %content.path.display(),
"syntax dispatcher not implemented yet"
);
return None;
}
Err(error) => {
tracing::error!(
%error,
path = %content.path.display(),
"unable to build the syntax_graph"
);
return None;
}
};
trace_missing_readers(&syntax_graph, content);
if with_cfg.unwrap_or(true) && !apply_cfg_overlay(&mut syntax_graph, content, with_metadata) {
return None;
}
syntax_graph.finalize_symbol_index();
Some(syntax_graph)
}
#[cfg(test)]
mod tests {
use super::get_syntax_graph;
use crate::ast::get_ast_graph;
use crate::content::Content;
use crate::language::Language;
use blends_domain::ast::{AstGraph, AstNode};
use blends_domain::path_search::get_backward_paths;
use blends_domain::path_search::search::definition_search;
use blends_domain::query::get_all_scope_definitions;
use blends_domain::syntax::SyntaxNode;
use blends_domain::NodeId;
use std::path::PathBuf;
use test_case::test_case;
fn content_for(language: Language, extension: &str, text: &str) -> Content {
Content {
bytes: text.as_bytes().to_vec(),
text: text.to_owned(),
language,
path: PathBuf::from(format!("test.{extension}")),
}
}
fn single_node_ast(node_type: &str) -> AstGraph {
let mut ast = AstGraph::new();
ast.add_node(NodeId(1), AstNode::new(1, 1, node_type.to_owned()));
ast
}
#[test]
fn builds_the_syntax_graph_for_a_supported_language() {
let content = content_for(Language::Yaml, "yaml", "key: value");
let ast = single_node_ast("stream");
let result = get_syntax_graph(&ast, &content, None, None);
let graph = result.expect("syntax graph should be built");
assert!(!graph.nodes.is_empty());
}
#[test]
fn definition_search_resolves_the_second_local_declarator() {
let content = content_for(
Language::Java,
"java",
"class T { void run() { String car = \"a\", plane = \"b\", boat = \"c\"; System.out.println(plane); } }",
);
let ast = get_ast_graph(&content).expect("java source should parse");
let graph = get_syntax_graph(&ast, &content, None, None).expect("graph should be built");
let lookup_id = graph
.nodes
.iter()
.find_map(|(n_id, node)| match node {
SyntaxNode::SymbolLookup { symbol, .. } if symbol == "plane" => Some(*n_id),
_ => None,
})
.expect("plane lookup should exist");
let def_id = get_backward_paths(&graph, lookup_id, None)
.iter()
.find_map(|path| definition_search(&graph, path, "plane"))
.expect("plane definition should resolve");
assert!(matches!(
graph.nodes.get(&def_id),
Some(SyntaxNode::VariableDeclaration { variable, .. }) if variable == "plane"
));
}
fn lookup_for(graph: &blends_domain::syntax::SyntaxGraph, wanted: &str) -> NodeId {
graph
.nodes
.iter()
.find_map(|(n_id, node)| match node {
SyntaxNode::SymbolLookup { symbol, .. } if symbol == wanted => Some(*n_id),
_ => None,
})
.expect("lookup should exist")
}
#[test]
fn scope_definitions_resolve_the_second_local_declarator() {
let content = content_for(
Language::Java,
"java",
"class T { void run() { String car = \"a\", plane = \"b\", boat = \"c\"; System.out.println(plane); } }",
);
let ast = get_ast_graph(&content).expect("java source should parse");
let graph = get_syntax_graph(&ast, &content, None, None).expect("graph should be built");
let definitions = get_all_scope_definitions(&graph, lookup_for(&graph, "plane"));
let &[def_id] = definitions.as_slice() else {
panic!("expected one scope definition for plane");
};
assert!(matches!(
graph.nodes.get(&def_id),
Some(SyntaxNode::VariableDeclaration { variable, variable_type: Some(variable_type), .. })
if variable == "plane" && variable_type == "String"
));
}
#[test]
fn scope_definitions_resolve_the_second_interface_constant() {
let content = content_for(
Language::Java,
"java",
"interface Limits { int MIN = 1, MAX = 10; default int clamp(int value) { return value > MAX ? MAX : value; } }",
);
let ast = get_ast_graph(&content).expect("java source should parse");
let graph = get_syntax_graph(&ast, &content, None, None).expect("graph should be built");
let definitions = get_all_scope_definitions(&graph, lookup_for(&graph, "MAX"));
let &[def_id] = definitions.as_slice() else {
panic!("expected one scope definition for MAX");
};
assert!(matches!(
graph.nodes.get(&def_id),
Some(SyntaxNode::VariableDeclaration { variable, variable_type: Some(variable_type), .. })
if variable == "MAX" && variable_type == "int"
));
}
#[test]
fn metadata_node_added_at_zero_when_requested() {
let content = content_for(Language::Yaml, "yaml", "key: value");
let ast = single_node_ast("stream");
let result = get_syntax_graph(&ast, &content, None, Some(true));
let graph = result.expect("syntax graph should be built");
assert_eq!(
graph.nodes.get(&NodeId(0)),
Some(&SyntaxNode::Metadata {
path: "test.yaml".to_owned(),
structure: std::collections::BTreeMap::new(),
instances: std::collections::BTreeMap::new(),
imports: std::vec::Vec::new(),
package: None,
})
);
}
#[test_case(None ; "by default")]
#[test_case(Some(false) ; "when explicitly disabled")]
fn no_metadata_node(with_metadata: Option<bool>) {
let content = content_for(Language::Yaml, "yaml", "key: value");
let ast = single_node_ast("stream");
let result = get_syntax_graph(&ast, &content, None, with_metadata);
let graph = result.expect("syntax graph should be built");
assert!(!graph.nodes.contains_key(&NodeId(0)));
}
#[test]
fn language_without_dispatcher_has_no_syntax_graph() {
let content = content_for(Language::Kotlin, "kt", "class A {}");
let ast = single_node_ast("program");
assert!(get_syntax_graph(&ast, &content, None, None).is_none());
}
#[test]
fn unhandled_root_node_type_degrades_to_missing_node_and_recurses() {
let content = content_for(Language::Yaml, "yaml", "key: value");
let mut ast = single_node_ast("unknown_node_type_xyz");
ast.add_node(NodeId(2), AstNode::new(1, 5, "another_unknown".to_owned()));
ast.add_edge(NodeId(1), NodeId(2), 0);
let result = get_syntax_graph(&ast, &content, None, None);
let graph = result.expect("syntax graph should be built");
assert_eq!(
graph.nodes.get(&NodeId(1)),
Some(&SyntaxNode::MissingNode {
node_type: "unknown_node_type_xyz".to_owned()
})
);
assert_eq!(
graph.nodes.get(&NodeId(2)),
Some(&SyntaxNode::MissingNode {
node_type: "another_unknown".to_owned()
})
);
assert!(graph
.edges
.get(&NodeId(1))
.is_some_and(|adjacent| adjacent.contains_key(&NodeId(2))));
}
#[test]
fn fatal_reader_error_returns_none() {
let content = content_for(Language::Yaml, "yaml", "[]");
let ast = single_node_ast("flow_node");
assert!(get_syntax_graph(&ast, &content, None, None).is_none());
}
#[test]
fn empty_ast_graph_returns_none() {
let content = content_for(Language::Yaml, "yaml", "");
assert!(get_syntax_graph(&AstGraph::new(), &content, None, None).is_none());
}
#[test]
fn ast_graph_without_root_node_returns_none() {
let content = content_for(Language::Yaml, "yaml", "key: value");
let mut ast = AstGraph::new();
ast.add_node(NodeId(7), AstNode::new(1, 1, "stream".to_owned()));
assert!(get_syntax_graph(&ast, &content, None, None).is_none());
}
#[test]
fn cfg_marks_overlay_the_ast_edges_by_default() {
let content = content_for(Language::Yaml, "yaml", "key: value");
let ast = get_ast_graph(&content).expect("yaml should parse");
let result = get_syntax_graph(&ast, &content, None, None);
let graph = result.expect("syntax graph should be built");
assert!(graph
.edges
.get(&NodeId(1))
.is_some_and(|adjacent| adjacent.values().any(|edge| edge.cfg.is_some())));
}
#[test]
fn no_cfg_marks_when_disabled() {
let content = content_for(Language::Yaml, "yaml", "key: value");
let ast = get_ast_graph(&content).expect("yaml should parse");
let result = get_syntax_graph(&ast, &content, Some(false), None);
let graph = result.expect("syntax graph should be built");
assert!(graph
.edges
.values()
.flat_map(|adjacent| adjacent.values())
.all(|edge| edge.cfg.is_none()));
}
#[test]
fn empty_yaml_builds_a_lone_file_node() {
let content = content_for(Language::Yaml, "yaml", "");
let ast = get_ast_graph(&content).expect("empty yaml should parse");
let result = get_syntax_graph(&ast, &content, None, None);
let graph = result.expect("syntax graph should be built");
assert_eq!(graph.nodes.len(), 1);
assert_eq!(graph.nodes.get(&NodeId(1)), Some(&SyntaxNode::File));
}
}