use std::collections::BTreeSet;
use std::fmt;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
pub type NodeId = SymbolId;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)]
pub struct CallSiteId(pub String);
impl CallSiteId {
pub fn source(syntax: &CallSyntax, span: &SourceSpan) -> Self {
Self(format!(
"{}@{}:{}-{}:{}",
syntax.key_fragment(),
span.start_line,
span.start_column,
span.end_line,
span.end_column
))
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)]
pub struct LanguageId(pub String);
impl LanguageId {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
}
impl fmt::Display for LanguageId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)]
pub struct SymbolId {
pub language: LanguageId,
pub module: Vec<String>,
pub container: Option<String>,
pub name: String,
}
impl SymbolId {
pub fn short_name(&self) -> String {
match &self.container {
Some(container) => format!("{container}::{}", self.name),
None => self.name.clone(),
}
}
pub fn qualified_parts(&self) -> Vec<&str> {
self.module
.iter()
.map(String::as_str)
.chain(self.container.iter().map(String::as_str))
.chain(std::iter::once(self.name.as_str()))
.collect()
}
}
impl fmt::Display for SymbolId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}://", self.language)?;
let parts = self.qualified_parts();
write!(f, "{}", parts.join("::"))
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct SourceSpan {
pub file: PathBuf,
pub start_line: usize,
pub start_column: usize,
pub start_byte: Option<usize>,
pub end_line: usize,
pub end_column: usize,
pub end_byte: Option<usize>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum CallSyntax {
Path(Vec<String>),
SelfMethod(String),
Method { receiver: String, method: String },
}
impl CallSyntax {
pub fn key_fragment(&self) -> String {
match self {
Self::Path(parts) => parts.join("::"),
Self::SelfMethod(method) => format!("self.{method}"),
Self::Method { receiver, method } => format!("{receiver}.{method}"),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct CallSite {
pub id: CallSiteId,
pub syntax: CallSyntax,
pub target: CallTarget,
pub label: CallLabel,
pub span: SourceSpan,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub enum CallTarget {
#[default]
Unresolved,
Indirect {
signature: Option<String>,
reason: UnresolvedReason,
},
Direct(SymbolId),
Dynamic {
dispatch: SymbolId,
candidates: Vec<DispatchCandidate>,
resolution: DispatchResolution,
evidence: DispatchEvidence,
unresolved_reasons: BTreeSet<UnresolvedReason>,
},
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub enum DispatchEvidence {
#[default]
ExactFlow,
ClosedSet,
}
impl fmt::Display for DispatchEvidence {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::ExactFlow => "exact-flow",
Self::ClosedSet => "closed-set",
})
}
}
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub enum UnresolvedReason {
OpaqueInput,
ExternalCode,
ExternalMemory,
FunctionPointer,
CrossCrateBoundary,
AnalysisLimit,
UnsupportedConstruct,
}
impl fmt::Display for UnresolvedReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let text = match self {
Self::OpaqueInput => "opaque input",
Self::ExternalCode => "external code",
Self::ExternalMemory => "external memory",
Self::FunctionPointer => "function pointer",
Self::CrossCrateBoundary => "cross-crate boundary",
Self::AnalysisLimit => "analysis limit",
Self::UnsupportedConstruct => "unsupported construct",
};
f.write_str(text)
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub enum DispatchResolution {
#[default]
Complete,
Partial,
Unresolved,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct DispatchCandidate {
pub target: SymbolId,
pub label: CallLabel,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct CallLabel {
pub default: String,
pub typed: Option<String>,
}
impl CallLabel {
pub fn new(default: impl Into<String>) -> Self {
Self {
default: default.into(),
typed: None,
}
}
pub fn with_types(default: impl Into<String>, typed: impl Into<String>) -> Self {
Self {
default: default.into(),
typed: Some(typed.into()),
}
}
pub fn text(&self, show_types: bool) -> &str {
if show_types {
self.typed.as_deref().unwrap_or(&self.default)
} else {
&self.default
}
}
pub fn with_suffix(&self, suffix: &str) -> Self {
Self {
default: format!("{}{suffix}", self.default),
typed: self.typed.as_ref().map(|typed| format!("{typed}{suffix}")),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct FunctionInfo {
pub id: SymbolId,
pub label: CallLabel,
pub public: bool,
pub calls: Vec<CallSite>,
pub span: SourceSpan,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct LanguageFact {
pub subject: SymbolId,
pub namespace: LanguageId,
pub kind: String,
pub key: String,
pub value: String,
pub span: SourceSpan,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct SemanticCallGraph {
pub functions: Vec<FunctionInfo>,
pub facts: Vec<LanguageFact>,
pub source_files: BTreeSet<PathBuf>,
pub roots: BTreeSet<SymbolId>,
}
pub type FileAnalysis = SemanticCallGraph;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CallNode {
pub key: String,
pub callsite: Option<CallSiteId>,
pub label: CallLabel,
pub relation: CallRelation,
pub children: Vec<CallNode>,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum CallRelation {
#[default]
Call,
DispatchCandidate,
BackEdge,
}