use crate::domain::edge::EdgeKind;
use crate::domain::graph::ContextGraph;
use crate::domain::node::{
FunctionNode, Mutability, Node, NodeCore, SourceSpan, TypeKind, TypeNode, VariableKind,
VariableNode, Visibility,
};
use crate::domain::policy::{DocumentationScorer, NodeInfo, NodeType, SizeFunction};
use crate::domain::ports::SourceReader;
use crate::domain::semantic::{ReferenceRole, SemanticData, SymbolKind, SymbolMetadata};
use anyhow::Result;
use std::collections::HashMap;
use std::path::Path;
pub struct GraphBuilder {
size_function: Box<dyn SizeFunction>,
doc_scorer: Box<dyn DocumentationScorer>,
}
impl GraphBuilder {
pub fn new(
size_function: Box<dyn SizeFunction>,
doc_scorer: Box<dyn DocumentationScorer>,
) -> Self {
Self {
size_function,
doc_scorer,
}
}
pub fn build(
&self,
semantic_data: SemanticData,
source_reader: &dyn SourceReader,
) -> Result<ContextGraph> {
let mut graph = ContextGraph::new();
let mut symbol_to_kind: HashMap<String, SymbolKind> = HashMap::new();
let mut symbol_to_parent: HashMap<String, String> = HashMap::new();
for document in &semantic_data.documents {
for definition in &document.definitions {
symbol_to_kind.insert(definition.symbol.clone(), definition.metadata.kind.clone());
if let Some(parent) = &definition.metadata.enclosing_symbol {
symbol_to_parent.insert(definition.symbol.clone(), parent.clone());
}
}
}
for document in &semantic_data.documents {
let source_path = Path::new(&semantic_data.project_root).join(&document.relative_path);
let source_code = source_reader.read(&source_path)?;
for definition in &document.definitions {
let kind = &definition.metadata.kind;
let should_be_node = match kind {
SymbolKind::Function
| SymbolKind::Method
| SymbolKind::Constructor
| SymbolKind::StaticMethod
| SymbolKind::AbstractMethod
| SymbolKind::Class
| SymbolKind::Interface
| SymbolKind::Struct
| SymbolKind::Enum
| SymbolKind::TypeAlias
| SymbolKind::Trait
| SymbolKind::Protocol => true,
SymbolKind::Variable | SymbolKind::Field | SymbolKind::Constant => definition
.metadata
.enclosing_symbol
.as_ref()
.and_then(|parent_sym| symbol_to_kind.get(parent_sym))
.is_none_or(|parent_kind| {
!matches!(
parent_kind,
SymbolKind::Function
| SymbolKind::Method
| SymbolKind::Constructor
| SymbolKind::StaticMethod
| SymbolKind::AbstractMethod
)
}),
_ => false, };
if !should_be_node {
continue;
}
let node_id = graph.graph.node_count() as u32;
let doc_texts: Vec<String> = definition.metadata.documentation.clone();
let span = SourceSpan {
start_line: definition.enclosing_range.start_line,
start_column: definition.enclosing_range.start_column,
end_line: definition.enclosing_range.end_line,
end_column: definition.enclosing_range.end_column,
};
let context_size = self.size_function.compute(&source_code, &span, &doc_texts);
let doc_text = doc_texts.first().map(|s| s.as_str());
let language = document
.relative_path
.split('.')
.next_back()
.map(|ext| ext.to_lowercase());
let node_info = NodeInfo {
node_type: infer_node_type_from_kind(kind),
name: definition.metadata.display_name.clone(),
signature: definition.metadata.signature.clone(),
language,
};
let doc_score = self.doc_scorer.score(&node_info, doc_text);
let core = NodeCore::new(
node_id,
definition.metadata.display_name.clone(),
definition.metadata.enclosing_symbol.clone(),
context_size,
span,
doc_score,
definition.metadata.is_external,
document.relative_path.clone(),
);
let node = create_node_from_definition(core, &definition.metadata)?;
graph.add_node(definition.symbol.clone(), node);
}
}
let resolve_to_node_symbol = |mut sym: String,
graph: &ContextGraph,
symbol_to_parent: &HashMap<String, String>|
-> Option<String> {
while !graph.symbol_to_node.contains_key(&sym) {
if let Some(parent) = symbol_to_parent.get(&sym) {
sym = parent.clone();
} else {
return None;
}
}
Some(sym)
};
let mut state_writers: HashMap<String, Vec<petgraph::graph::NodeIndex>> = HashMap::new();
let mut callers: HashMap<String, Vec<petgraph::graph::NodeIndex>> = HashMap::new();
let mut readers: Vec<(petgraph::graph::NodeIndex, String)> = Vec::new();
for document in &semantic_data.documents {
for reference in &document.references {
let resolved_source_sym = resolve_to_node_symbol(
reference.enclosing_symbol.clone(),
&graph,
&symbol_to_parent,
);
let resolved_target_sym =
resolve_to_node_symbol(reference.symbol.clone(), &graph, &symbol_to_parent);
if let (Some(source_sym), Some(target_sym)) =
(resolved_source_sym, resolved_target_sym)
{
let source_idx = *graph.symbol_to_node.get(&source_sym).unwrap();
let target_idx = *graph.symbol_to_node.get(&target_sym).unwrap();
if source_idx == target_idx {
continue;
}
let edge_kind = infer_edge_kind(&reference.role, source_idx, target_idx);
if matches!(edge_kind, EdgeKind::Write) {
state_writers
.entry(target_sym.clone())
.or_default()
.push(source_idx);
}
if matches!(edge_kind, EdgeKind::Read) {
readers.push((source_idx, target_sym.clone()));
}
if matches!(edge_kind, EdgeKind::Call) {
callers
.entry(target_sym.clone())
.or_default()
.push(source_idx);
}
graph.add_edge(source_idx, target_idx, edge_kind);
}
}
}
for document in &semantic_data.documents {
for definition in &document.definitions {
if let Some(&source_idx) = graph.symbol_to_node.get(&definition.symbol) {
for relationship in &definition.metadata.relationships {
if let Some(target_idx) = resolve_to_node_symbol(
relationship.target_symbol.clone(),
&graph,
&symbol_to_parent,
)
.and_then(|resolved_target| {
graph.symbol_to_node.get(&resolved_target).copied()
}) {
if source_idx == target_idx {
continue;
}
let edge_kind = match relationship.kind {
crate::domain::semantic::RelationshipKind::TypeDefinition => {
match graph.node(source_idx) {
crate::domain::node::Node::Function(_) => {
EdgeKind::ReturnType
}
crate::domain::node::Node::Variable(_) => {
EdgeKind::VariableType
}
crate::domain::node::Node::Type(_) => EdgeKind::FieldType,
}
}
crate::domain::semantic::RelationshipKind::Implements => {
EdgeKind::Implements
}
crate::domain::semantic::RelationshipKind::Inherits => {
EdgeKind::Inherits
}
crate::domain::semantic::RelationshipKind::References => {
continue;
}
};
graph.add_edge(source_idx, target_idx, edge_kind);
}
}
}
}
}
for (reader_idx, state_symbol) in readers {
if let Some(writers) = state_writers.get(&state_symbol) {
for &writer_idx in writers {
if reader_idx != writer_idx {
graph.add_edge(reader_idx, writer_idx, EdgeKind::SharedStateWrite);
}
}
}
}
let symbols: Vec<String> = graph.symbol_to_node.keys().cloned().collect();
for callee_symbol in symbols {
if let (Some(callee_idx), Some(caller_indices)) = (
graph.get_node_by_symbol(&callee_symbol),
callers.get(&callee_symbol),
) {
for &caller_idx in caller_indices {
if callee_idx != caller_idx {
graph.add_edge(callee_idx, caller_idx, EdgeKind::CallIn);
}
}
}
}
Ok(graph)
}
}
fn infer_node_type_from_kind(kind: &SymbolKind) -> NodeType {
match kind {
SymbolKind::Function
| SymbolKind::Method
| SymbolKind::Constructor
| SymbolKind::StaticMethod
| SymbolKind::AbstractMethod => NodeType::Function,
SymbolKind::Variable
| SymbolKind::Field
| SymbolKind::Constant
| SymbolKind::Parameter
| SymbolKind::Module | SymbolKind::Namespace | SymbolKind::Package
| SymbolKind::Macro => NodeType::Variable, SymbolKind::Class
| SymbolKind::Interface
| SymbolKind::Struct
| SymbolKind::Enum
| SymbolKind::TypeAlias
| SymbolKind::Trait
| SymbolKind::Protocol => NodeType::Type,
_ => NodeType::Variable, }
}
fn create_node_from_definition(core: NodeCore, metadata: &SymbolMetadata) -> Result<Node> {
match infer_node_type_from_kind(&metadata.kind) {
NodeType::Function => {
Ok(Node::Function(FunctionNode {
core,
param_count: 0, typed_param_count: 0, has_return_type: metadata.signature.is_some(), is_async: false, is_generator: false, visibility: Visibility::Public, }))
}
NodeType::Variable => Ok(Node::Variable(VariableNode {
core,
has_type_annotation: metadata.signature.is_some(),
mutability: Mutability::Mutable, variable_kind: VariableKind::Global, })),
NodeType::Type => {
let mut is_abstract = matches!(
metadata.kind,
SymbolKind::Interface | SymbolKind::Trait | SymbolKind::Protocol
);
if matches!(metadata.kind, SymbolKind::Class) {
is_abstract = metadata.relationships.iter().any(|r| {
matches!(
r.kind,
crate::domain::semantic::RelationshipKind::Implements
) && r.target_symbol.contains("typing/Protocol#")
});
}
Ok(Node::Type(TypeNode {
core,
type_kind: match metadata.kind {
SymbolKind::Class if is_abstract => TypeKind::Protocol, SymbolKind::Class => TypeKind::Class,
SymbolKind::Interface => TypeKind::Interface,
SymbolKind::Struct => TypeKind::Struct,
SymbolKind::Enum => TypeKind::Enum,
SymbolKind::TypeAlias => TypeKind::TypeAlias,
SymbolKind::Trait => TypeKind::Protocol, SymbolKind::Protocol => TypeKind::Protocol,
_ => TypeKind::Class, },
is_abstract,
type_param_count: 0, }))
}
}
}
fn infer_edge_kind(
role: &ReferenceRole,
_source: petgraph::graph::NodeIndex,
_target: petgraph::graph::NodeIndex,
) -> EdgeKind {
match role {
ReferenceRole::Read => EdgeKind::Read,
ReferenceRole::Write => EdgeKind::Write,
ReferenceRole::Call => EdgeKind::Call,
ReferenceRole::TypeUsage => EdgeKind::ParamType, ReferenceRole::Import => EdgeKind::Call, ReferenceRole::Unknown => EdgeKind::Call, }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::semantic::SymbolMetadata;
#[test]
fn test_infer_node_type_from_kind() {
assert_eq!(
infer_node_type_from_kind(&SymbolKind::Function),
NodeType::Function
);
assert_eq!(
infer_node_type_from_kind(&SymbolKind::Class),
NodeType::Type
);
assert_eq!(
infer_node_type_from_kind(&SymbolKind::Variable),
NodeType::Variable
);
assert_eq!(
infer_node_type_from_kind(&SymbolKind::Unknown),
NodeType::Variable
);
}
#[test]
fn test_create_node_from_definition_class_vs_protocol() {
let core = NodeCore::new(
0,
"MyClass".into(),
None,
10,
SourceSpan {
start_line: 0,
start_column: 0,
end_line: 1,
end_column: 0,
},
1.0,
false,
"file.py".into(),
);
let mut metadata = SymbolMetadata {
symbol: "MyClass#".into(),
kind: SymbolKind::Class,
display_name: "MyClass".into(),
documentation: vec![],
signature: None,
relationships: vec![],
enclosing_symbol: None,
is_external: false,
};
let node = create_node_from_definition(core.clone(), &metadata).unwrap();
if let Node::Type(t) = node {
assert_eq!(t.type_kind, TypeKind::Class);
} else {
panic!("Expected Type node");
}
metadata
.relationships
.push(crate::domain::semantic::Relationship {
target_symbol: "typing/Protocol#".into(),
kind: crate::domain::semantic::RelationshipKind::Implements,
});
let node = create_node_from_definition(core.clone(), &metadata).unwrap();
if let Node::Type(t) = node {
assert_eq!(t.type_kind, TypeKind::Protocol);
} else {
panic!("Expected Protocol node");
}
}
}