use alloc::borrow::ToOwned;
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use crate::syntax::node::{FileInstanceData, FileStructData, FileStructValue, SyntaxNode};
use crate::syntax::SyntaxGraphArgs;
use crate::syntax::SyntaxGraphError;
use crate::{Language, NodeId};
#[allow(dead_code, reason = "wired when the java class reader lands")]
pub fn add_class_to_metadata(
args: &mut SyntaxGraphArgs<'_>,
n_id: NodeId,
name: &str,
) -> Result<(), SyntaxGraphError> {
let Some(SyntaxNode::Metadata { structure, .. }) = args.syntax_graph.nodes.get_mut(&NodeId(0))
else {
return Err(SyntaxGraphError::UnexpectedAstShape);
};
let mut parent_class = structure;
for elem in &args.metadata.class_path {
let data = &mut parent_class
.get_mut(elem)
.ok_or(SyntaxGraphError::UnexpectedAstShape)?
.data;
parent_class = match data {
FileStructValue::MethodName(_) => return Ok(()),
FileStructValue::Children(children) => children,
};
}
parent_class.insert(
name.to_owned(),
FileStructData {
node: n_id,
kind: "class".to_owned(),
data: FileStructValue::Children(BTreeMap::new()),
node_range: None,
},
);
args.metadata.class_path.push(name.to_owned());
Ok(())
}
#[allow(dead_code, reason = "wired when the java method reader lands")]
pub fn add_method_to_metadata(
args: &mut SyntaxGraphArgs<'_>,
n_id: NodeId,
name: &str,
) -> Result<(), SyntaxGraphError> {
let Some(SyntaxNode::Metadata { structure, .. }) = args.syntax_graph.nodes.get_mut(&NodeId(0))
else {
return Err(SyntaxGraphError::UnexpectedAstShape);
};
let mut parent_class = structure;
for elem in &args.metadata.class_path {
let data = &mut parent_class
.get_mut(elem)
.ok_or(SyntaxGraphError::UnexpectedAstShape)?
.data;
parent_class = match data {
FileStructValue::MethodName(_) => return Ok(()),
FileStructValue::Children(children) => children,
};
}
parent_class.insert(
name.to_owned(),
FileStructData {
node: n_id,
kind: "method".to_owned(),
data: FileStructValue::MethodName(name.to_owned()),
node_range: None,
},
);
Ok(())
}
pub fn add_node_range_to_method(
args: &mut SyntaxGraphArgs<'_>,
n_id: NodeId,
name: &str,
) -> Result<(), SyntaxGraphError> {
let node_range: Vec<NodeId> = args
.syntax_graph
.nodes
.keys()
.copied()
.skip_while(|node_id| *node_id != n_id)
.collect();
let Some(SyntaxNode::Metadata { structure, .. }) = args.syntax_graph.nodes.get_mut(&NodeId(0))
else {
return Err(SyntaxGraphError::UnexpectedAstShape);
};
let mut parent_class = structure;
for elem in &args.metadata.class_path {
let data = &mut parent_class
.get_mut(elem)
.ok_or(SyntaxGraphError::UnexpectedAstShape)?
.data;
parent_class = match data {
FileStructValue::MethodName(_) => return Ok(()),
FileStructValue::Children(children) => children,
};
}
if let Some(method_data) = parent_class.get_mut(name) {
method_data.node_range = Some(node_range);
}
Ok(())
}
#[allow(
dead_code,
reason = "wired when the java variable-declaration reader lands"
)]
pub fn add_instance_to_metadata(
args: &mut SyntaxGraphArgs<'_>,
var_type: &str,
var_name: &str,
multi_paths: &[&str],
) -> Result<(), SyntaxGraphError> {
let Some(current_class) = args.metadata.class_path.last().cloned() else {
return Ok(());
};
let extension = primary_extension(args.language);
let imports: Vec<String> = match args.syntax_graph.nodes.get(&NodeId(0)) {
Some(SyntaxNode::Metadata { imports, .. }) => imports.clone(),
_ => return Err(SyntaxGraphError::UnexpectedAstShape),
};
let Some(SyntaxNode::Metadata { instances, .. }) = args.syntax_graph.nodes.get_mut(&NodeId(0))
else {
return Err(SyntaxGraphError::UnexpectedAstShape);
};
let class_instances = instances.entry(current_class).or_default();
let mut possible_path = var_type.replace('.', "/");
possible_path.push_str(extension);
if let Some(path) = get_file_from_path(&possible_path, multi_paths) {
class_instances.insert(
var_name.to_owned(),
FileInstanceData {
object: split_on_last_dot(var_type).1.to_owned(),
source: path.to_owned(),
source_type: "file_path".to_owned(),
},
);
}
for imported_package in &imports {
let (import_prefix, import_leaf) = split_on_last_dot(imported_package);
let (var_prefix, _) = split_on_last_dot(var_type);
let is_match = if import_leaf == "*" {
imported_package == var_type || import_prefix == var_prefix
} else {
imported_package == var_type || import_leaf == var_type
};
if is_match {
class_instances.insert(
var_name.to_owned(),
FileInstanceData {
object: var_type.to_owned(),
source: import_prefix.to_owned(),
source_type: "package".to_owned(),
},
);
}
}
Ok(())
}
#[allow(dead_code, reason = "wired when build_assignment_node lands")]
pub fn del_metadata_instance(
args: &mut SyntaxGraphArgs<'_>,
variable_id: NodeId,
value_id: NodeId,
) -> Result<(), SyntaxGraphError> {
let Some(SyntaxNode::SymbolLookup { symbol, .. }) = args.syntax_graph.nodes.get(&variable_id)
else {
return Ok(());
};
if symbol.is_empty() {
return Ok(());
}
let var = symbol.clone();
let Some(current_class) = args.metadata.class_path.last().cloned() else {
return Ok(());
};
let new_object = match args.syntax_graph.nodes.get(&value_id) {
Some(SyntaxNode::ObjectCreation { name, .. }) => Some(name.clone()),
_ => None,
};
let Some(SyntaxNode::Metadata { instances, .. }) = args.syntax_graph.nodes.get_mut(&NodeId(0))
else {
return Err(SyntaxGraphError::UnexpectedAstShape);
};
let Some(class_instances) = instances.get_mut(¤t_class) else {
return Ok(());
};
let Some(tracked_object) = class_instances.get(&var).map(|data| data.object.clone()) else {
return Ok(());
};
if new_object.as_deref() != Some(tracked_object.as_str()) {
class_instances.remove(&var);
}
Ok(())
}
const fn primary_extension(language: Language) -> &'static str {
match language {
Language::CSharp => ".cs",
Language::Elixir => ".ex",
Language::Go => ".go",
Language::Hcl => ".hcl",
Language::Java => ".java",
Language::JavaScript => ".js",
Language::Json => ".json",
Language::Kotlin => ".kt",
Language::Php => ".php",
Language::Python => ".py",
Language::Ruby => ".rb",
Language::Rust => ".rs",
Language::Scala => ".scala",
Language::Swift => ".swift",
Language::TypeScript => ".ts",
Language::Yaml => ".yaml",
}
}
fn split_on_last_dot(value: &str) -> (&str, &str) {
value.rsplit_once('.').map_or((value, ""), |split| split)
}
fn get_file_from_path<'a>(file_name: &str, multi_paths: &'a [&str]) -> Option<&'a str> {
const MIN_PATH_DEPTH: usize = 4;
if file_name.matches('/').count() >= MIN_PATH_DEPTH {
return multi_paths
.iter()
.copied()
.find(|path| path.contains(file_name));
}
None
}
#[cfg(test)]
mod tests {
use super::{
add_class_to_metadata, add_instance_to_metadata, add_method_to_metadata,
add_node_range_to_method, del_metadata_instance, split_on_last_dot,
};
use crate::ast::AstGraph;
use crate::syntax::node::{FileInstanceData, FileStructData, FileStructValue, SyntaxNode};
use crate::syntax::{
SyntaxGraph, SyntaxGraphArgs, SyntaxGraphError, SyntaxMetadata, SyntaxReader,
};
use crate::{Language, NodeId};
use alloc::borrow::ToOwned;
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
fn no_dispatch(_: &str) -> Option<SyntaxReader> {
None
}
fn metadata_graph() -> SyntaxGraph {
let mut graph = SyntaxGraph::new();
graph.add_node(
NodeId(0),
SyntaxNode::Metadata {
path: "F.java".to_owned(),
structure: BTreeMap::new(),
instances: BTreeMap::new(),
imports: Vec::new(),
package: None,
},
);
graph
}
fn structure(graph: &SyntaxGraph) -> &BTreeMap<String, FileStructData> {
match graph.nodes.get(&NodeId(0)) {
Some(SyntaxNode::Metadata { structure, .. }) => structure,
_ => panic!("node 0 must be Metadata"),
}
}
fn graph_with_method_leaf(class_name: &str) -> SyntaxGraph {
let mut graph = metadata_graph();
match graph.nodes.get_mut(&NodeId(0)) {
Some(SyntaxNode::Metadata { structure, .. }) => {
structure.insert(
class_name.to_owned(),
FileStructData {
node: NodeId(2),
kind: "method".to_owned(),
data: FileStructValue::MethodName(class_name.to_owned()),
node_range: None,
},
);
}
_ => panic!("node 0 must be Metadata"),
}
graph
}
fn graph_tracking_widget_as_obj() -> SyntaxGraph {
let mut graph = metadata_graph();
match graph.nodes.get_mut(&NodeId(0)) {
Some(SyntaxNode::Metadata { instances, .. }) => {
let mut class_map = BTreeMap::new();
class_map.insert(
"obj".to_owned(),
FileInstanceData {
object: "Widget".to_owned(),
source: "s".to_owned(),
source_type: "package".to_owned(),
},
);
instances.insert("Foo".to_owned(), class_map);
}
_ => panic!("node 0 must be Metadata"),
}
graph
}
fn instances(graph: &SyntaxGraph) -> &BTreeMap<String, BTreeMap<String, FileInstanceData>> {
match graph.nodes.get(&NodeId(0)) {
Some(SyntaxNode::Metadata { instances, .. }) => instances,
_ => panic!("node 0 must be Metadata"),
}
}
#[test]
fn split_on_last_dot_splits_at_the_final_dot() {
assert_eq!(split_on_last_dot("a.b.c"), ("a.b", "c"));
assert_eq!(split_on_last_dot("nodot"), ("nodot", ""));
assert_eq!(split_on_last_dot(""), ("", ""));
}
#[test]
fn class_and_method_nest_under_the_class_path() {
let ast = AstGraph::new();
let mut graph = metadata_graph();
let mut meta = SyntaxMetadata::seeded(NodeId(1));
{
let mut args =
SyntaxGraphArgs::new(Language::Java, &ast, &mut graph, &mut meta, no_dispatch);
add_class_to_metadata(&mut args, NodeId(5), "Foo").unwrap();
add_method_to_metadata(&mut args, NodeId(6), "bar").unwrap();
}
assert_eq!(meta.class_path, vec!["Foo".to_owned()]);
let Some(SyntaxNode::Metadata { structure, .. }) = graph.nodes.get(&NodeId(0)) else {
panic!("node 0 must be Metadata");
};
let FileStructValue::Children(children) = &structure.get("Foo").unwrap().data else {
panic!("Foo must be a class with children");
};
assert!(matches!(
&children.get("bar").unwrap().data,
FileStructValue::MethodName(name) if name == "bar"
));
}
#[test]
fn add_instance_matches_an_imported_package() {
let ast = AstGraph::new();
let mut graph = metadata_graph();
if let Some(SyntaxNode::Metadata { imports, .. }) = graph.nodes.get_mut(&NodeId(0)) {
imports.push("com.example.Widget".to_owned());
}
let mut meta = SyntaxMetadata::seeded(NodeId(1));
meta.class_path.push("Foo".to_owned());
{
let mut args =
SyntaxGraphArgs::new(Language::Java, &ast, &mut graph, &mut meta, no_dispatch);
add_instance_to_metadata(&mut args, "com.example.Widget", "w", &[]).unwrap();
}
let data = instances(&graph).get("Foo").unwrap().get("w").unwrap();
assert_eq!(data.object, "com.example.Widget");
assert_eq!(data.source, "com.example");
assert_eq!(data.source_type, "package");
}
#[test]
fn add_instance_matches_a_resolved_file_path() {
let ast = AstGraph::new();
let mut graph = metadata_graph();
let mut meta = SyntaxMetadata::seeded(NodeId(1));
meta.class_path.push("Foo".to_owned());
{
let mut args =
SyntaxGraphArgs::new(Language::Java, &ast, &mut graph, &mut meta, no_dispatch);
add_instance_to_metadata(
&mut args,
"a.b.c.d.Widget",
"w",
&["/root/a/b/c/d/Widget.java"],
)
.unwrap();
}
let data = instances(&graph).get("Foo").unwrap().get("w").unwrap();
assert_eq!(data.object, "Widget");
assert_eq!(data.source, "/root/a/b/c/d/Widget.java");
assert_eq!(data.source_type, "file_path");
}
#[test]
fn del_metadata_instance_removes_on_a_non_matching_reassignment() {
let ast = AstGraph::new();
let mut graph = metadata_graph();
if let Some(SyntaxNode::Metadata { instances, .. }) = graph.nodes.get_mut(&NodeId(0)) {
let mut class_map = BTreeMap::new();
class_map.insert(
"obj".to_owned(),
FileInstanceData {
object: "Widget".to_owned(),
source: "s".to_owned(),
source_type: "package".to_owned(),
},
);
instances.insert("Foo".to_owned(), class_map);
}
graph.add_node(
NodeId(10),
SyntaxNode::SymbolLookup {
symbol: "obj".to_owned(),
symbol_scope: None,
value: None,
},
);
graph.add_node(
NodeId(11),
SyntaxNode::ObjectCreation {
name: "Other".to_owned(),
arguments_id: None,
initializer_id: None,
},
);
let mut meta = SyntaxMetadata::seeded(NodeId(1));
meta.class_path.push("Foo".to_owned());
{
let mut args =
SyntaxGraphArgs::new(Language::Java, &ast, &mut graph, &mut meta, no_dispatch);
del_metadata_instance(&mut args, NodeId(10), NodeId(11)).unwrap();
}
assert!(instances(&graph).get("Foo").unwrap().get("obj").is_none());
}
#[test]
fn del_metadata_instance_keeps_a_matching_object_creation() {
let ast = AstGraph::new();
let mut graph = metadata_graph();
if let Some(SyntaxNode::Metadata { instances, .. }) = graph.nodes.get_mut(&NodeId(0)) {
let mut class_map = BTreeMap::new();
class_map.insert(
"obj".to_owned(),
FileInstanceData {
object: "Widget".to_owned(),
source: "s".to_owned(),
source_type: "package".to_owned(),
},
);
instances.insert("Foo".to_owned(), class_map);
}
graph.add_node(
NodeId(10),
SyntaxNode::SymbolLookup {
symbol: "obj".to_owned(),
symbol_scope: None,
value: None,
},
);
graph.add_node(
NodeId(11),
SyntaxNode::ObjectCreation {
name: "Widget".to_owned(),
arguments_id: None,
initializer_id: None,
},
);
let mut meta = SyntaxMetadata::seeded(NodeId(1));
meta.class_path.push("Foo".to_owned());
{
let mut args =
SyntaxGraphArgs::new(Language::Java, &ast, &mut graph, &mut meta, no_dispatch);
del_metadata_instance(&mut args, NodeId(10), NodeId(11)).unwrap();
}
assert!(instances(&graph).get("Foo").unwrap().get("obj").is_some());
}
#[test]
#[allow(clippy::too_many_lines, reason = "one arm per metadata entry point")]
fn metadata_mutators_reject_a_graph_without_a_root_metadata_node() {
let ast = AstGraph::new();
for (name, outcome) in [
("add_class_to_metadata", {
let mut graph = SyntaxGraph::new();
let mut meta = SyntaxMetadata::seeded(NodeId(1));
let mut args =
SyntaxGraphArgs::new(Language::Java, &ast, &mut graph, &mut meta, no_dispatch);
add_class_to_metadata(&mut args, NodeId(5), "Foo")
}),
("add_method_to_metadata", {
let mut graph = SyntaxGraph::new();
let mut meta = SyntaxMetadata::seeded(NodeId(1));
let mut args =
SyntaxGraphArgs::new(Language::Java, &ast, &mut graph, &mut meta, no_dispatch);
add_method_to_metadata(&mut args, NodeId(6), "bar")
}),
("add_node_range_to_method", {
let mut graph = SyntaxGraph::new();
let mut meta = SyntaxMetadata::seeded(NodeId(1));
let mut args =
SyntaxGraphArgs::new(Language::Java, &ast, &mut graph, &mut meta, no_dispatch);
add_node_range_to_method(&mut args, NodeId(6), "bar")
}),
("add_instance_to_metadata", {
let mut graph = SyntaxGraph::new();
let mut meta = SyntaxMetadata::seeded(NodeId(1));
meta.class_path.push("Foo".to_owned());
let mut args =
SyntaxGraphArgs::new(Language::Java, &ast, &mut graph, &mut meta, no_dispatch);
add_instance_to_metadata(&mut args, "com.example.Widget", "w", &[])
}),
("del_metadata_instance", {
let mut graph = SyntaxGraph::new();
graph.add_node(
NodeId(10),
SyntaxNode::SymbolLookup {
symbol: "w".to_owned(),
symbol_scope: None,
value: None,
},
);
let mut meta = SyntaxMetadata::seeded(NodeId(1));
meta.class_path.push("Foo".to_owned());
let mut args =
SyntaxGraphArgs::new(Language::Java, &ast, &mut graph, &mut meta, no_dispatch);
del_metadata_instance(&mut args, NodeId(10), NodeId(11))
}),
] {
assert!(
matches!(outcome, Err(SyntaxGraphError::UnexpectedAstShape)),
"{name} must reject a graph with no metadata root, got {outcome:?}"
);
}
}
#[test]
fn add_class_to_metadata_stops_when_the_path_reaches_a_method_leaf() {
let ast = AstGraph::new();
let mut graph = graph_with_method_leaf("Foo");
let mut meta = SyntaxMetadata::seeded(NodeId(1));
meta.class_path.push("Foo".to_owned());
{
let mut args =
SyntaxGraphArgs::new(Language::Java, &ast, &mut graph, &mut meta, no_dispatch);
add_class_to_metadata(&mut args, NodeId(5), "Bar").expect("the walk must not error");
}
assert!(
structure(&graph).get("Bar").is_none(),
"Bar must not be nested under a method leaf"
);
assert_eq!(meta.class_path, vec!["Foo".to_owned()]);
}
#[test]
fn add_method_to_metadata_stops_when_the_path_reaches_a_method_leaf() {
let ast = AstGraph::new();
let mut graph = graph_with_method_leaf("Foo");
let mut meta = SyntaxMetadata::seeded(NodeId(1));
meta.class_path.push("Foo".to_owned());
{
let mut args =
SyntaxGraphArgs::new(Language::Java, &ast, &mut graph, &mut meta, no_dispatch);
add_method_to_metadata(&mut args, NodeId(6), "bar").expect("the walk must not error");
}
assert!(
structure(&graph).get("bar").is_none(),
"bar must not be nested under a method leaf"
);
}
#[test]
fn add_instance_to_metadata_records_nothing_while_no_class_is_open() {
let ast = AstGraph::new();
let mut graph = metadata_graph();
let mut meta = SyntaxMetadata::seeded(NodeId(1));
let mut args =
SyntaxGraphArgs::new(Language::Java, &ast, &mut graph, &mut meta, no_dispatch);
add_instance_to_metadata(&mut args, "com.example.Widget", "w", &[])
.expect("a missing class is not an error");
assert!(instances(args.syntax_graph).is_empty());
}
#[test]
fn add_instance_matches_a_wildcard_import_by_package_prefix() {
let ast = AstGraph::new();
let mut graph = metadata_graph();
if let Some(SyntaxNode::Metadata { imports, .. }) = graph.nodes.get_mut(&NodeId(0)) {
imports.push("com.example.*".to_owned());
}
let mut meta = SyntaxMetadata::seeded(NodeId(1));
meta.class_path.push("Foo".to_owned());
{
let mut args =
SyntaxGraphArgs::new(Language::Java, &ast, &mut graph, &mut meta, no_dispatch);
let result = add_instance_to_metadata(&mut args, "com.example.Widget", "w", &[]);
assert!(result.is_ok());
}
let data = instances(&graph).get("Foo").unwrap().get("w").unwrap();
assert_eq!(data.object, "com.example.Widget");
assert_eq!(data.source, "com.example");
assert_eq!(data.source_type, "package");
}
#[test]
fn del_metadata_instance_keeps_the_tracked_instance_when_it_cannot_act() {
let ast = AstGraph::new();
for (scenario, variable, open_class) in [
("variable is not a symbol lookup", None, true),
("no class is open", Some("obj"), false),
] {
let mut graph = graph_tracking_widget_as_obj();
match variable {
Some(symbol) => graph.add_node(
NodeId(10),
SyntaxNode::SymbolLookup {
symbol: symbol.to_owned(),
symbol_scope: None,
value: None,
},
),
None => graph.add_node(
NodeId(10),
SyntaxNode::ObjectCreation {
name: "NotASymbol".to_owned(),
arguments_id: None,
initializer_id: None,
},
),
}
graph.add_node(
NodeId(11),
SyntaxNode::ObjectCreation {
name: "Other".to_owned(),
arguments_id: None,
initializer_id: None,
},
);
let mut meta = SyntaxMetadata::seeded(NodeId(1));
if open_class {
meta.class_path.push("Foo".to_owned());
}
{
let mut args =
SyntaxGraphArgs::new(Language::Java, &ast, &mut graph, &mut meta, no_dispatch);
del_metadata_instance(&mut args, NodeId(10), NodeId(11))
.expect("a no-op is not an error");
}
assert!(
instances(&graph)
.get("Foo")
.and_then(|class| class.get("obj"))
.is_some(),
"the tracked instance must survive when {scenario}"
);
}
}
}