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>,
}
impl SourceSpan {
pub fn has_same_coordinates(&self, other: &Self) -> bool {
self.start_line == other.start_line
&& self.start_column == other.start_column
&& self.end_line == other.end_line
&& self.end_column == other.end_column
}
pub fn contains(&self, other: &Self) -> bool {
(self.start_line, self.start_column) <= (other.start_line, other.start_column)
&& (other.end_line, other.end_column) <= (self.end_line, self.end_column)
}
pub fn overlaps(&self, other: &Self) -> bool {
(self.start_line, self.start_column) <= (other.end_line, other.end_column)
&& (other.start_line, other.start_column) <= (self.end_line, self.end_column)
}
pub fn extent(&self) -> usize {
self.end_line.saturating_sub(self.start_line) * 10_000
+ self.end_column.saturating_sub(self.start_column)
}
pub fn start_distance(&self, other: &Self) -> usize {
self.start_line.abs_diff(other.start_line) * 10_000
+ self.start_column.abs_diff(other.start_column)
}
pub fn boundary_distance(&self, other: &Self) -> usize {
self.start_distance(other)
+ self.end_line.abs_diff(other.end_line) * 10_000
+ self.end_column.abs_diff(other.end_column)
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum CallSyntax {
Path(Vec<String>),
SelfMethod(String),
Method {
receiver: String,
method: String,
},
Expression(String),
CompilerConfirmed(Box<CallSyntax>),
}
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}"),
Self::Expression(expression) => expression.clone(),
Self::CompilerConfirmed(syntax) => {
format!("compiler-confirmed:{}", syntax.key_fragment())
}
}
}
pub fn visible(&self) -> &Self {
match self {
Self::CompilerConfirmed(syntax) => syntax.visible(),
syntax => syntax,
}
}
pub fn requires_compiler_confirmation(&self) -> bool {
matches!(self, Self::CompilerConfirmed(_))
}
}
#[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>,
},
}
impl CallTarget {
pub fn dynamic(
dispatch: SymbolId,
candidates: Vec<DispatchCandidate>,
evidence: DispatchEvidence,
unresolved_reasons: BTreeSet<UnresolvedReason>,
) -> Self {
let resolution = DispatchResolution::from_candidates(
!candidates.is_empty(),
!unresolved_reasons.is_empty(),
);
Self::Dynamic {
dispatch,
candidates,
resolution,
evidence,
unresolved_reasons,
}
}
}
#[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,
}
impl DispatchResolution {
pub fn from_candidates(has_candidates: bool, has_unresolved_sources: bool) -> Self {
match (has_candidates, has_unresolved_sources) {
(false, _) => Self::Unresolved,
(true, true) => Self::Partial,
(true, false) => Self::Complete,
}
}
}
#[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>,
}
impl SemanticCallGraph {
pub fn append(&mut self, mut fragment: Self) {
self.functions.append(&mut fragment.functions);
self.facts.append(&mut fragment.facts);
self.source_files.append(&mut fragment.source_files);
self.roots.append(&mut fragment.roots);
}
}
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,
}