use ahash::AHashMap;
use bonsai_common::{FileId, Precision, Span, SymbolId};
use serde::{Deserialize, Serialize};
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
pub struct LanguageId(pub &'static str);
impl LanguageId {
#[must_use]
pub const fn new(s: &'static str) -> Self {
Self(s)
}
#[must_use]
pub fn as_str(self) -> &'static str {
self.0
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkspaceRoot {
pub name: String,
pub files: Vec<FileId>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Visibility {
Public,
Private,
Crate,
Module,
Protected,
Internal,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DeclKind {
Module,
Namespace,
Function,
Method,
Constructor,
Class,
Struct,
Trait,
Interface,
Enum,
EnumVariant,
TypeAlias,
Global,
Const,
Static,
Import,
Field,
Other,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ModulePath {
pub segments: Vec<String>,
}
impl ModulePath {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn from_segments<I, S>(segments: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self {
segments: segments.into_iter().map(Into::into).collect(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.segments.is_empty()
}
#[must_use]
pub fn matches(&self, other: &ModulePath) -> bool {
!self.segments.is_empty() && self.segments == other.segments
}
#[must_use]
pub fn shares_top_segment(&self, other: &ModulePath) -> bool {
match (self.segments.first(), other.segments.first()) {
(Some(a), Some(b)) => a == b,
_ => false,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Decl {
pub symbol: SymbolId,
pub kind: DeclKind,
pub name: String,
pub qualified_name: Option<String>,
#[serde(default, skip_serializing_if = "ModulePath::is_empty")]
pub module_path: ModulePath,
pub span: Span,
pub name_span: Span,
pub visibility: Visibility,
pub parent: Option<SymbolId>,
pub body_span: Option<Span>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub flow_events: Vec<FlowEvent>,
#[serde(default)]
pub has_implicit_returns: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub params: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub param_annotations: Vec<Vec<String>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub param_default_calls: Vec<Vec<String>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub type_aliases: Vec<TypeAliasBinding>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub bases: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub receiver_param_index: Option<usize>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub receiver_field_writes: Vec<FieldWrite>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub receiver_field_initializers: Vec<ReceiverFieldInitializer>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub implicit_receiver_names: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub receiver_state_sources: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub return_type: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub is_variadic: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TypeAliasBinding {
pub name: String,
pub type_name: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FieldWrite {
pub span: Span,
pub target: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub source_param_indices: Vec<usize>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReceiverFieldInitializer {
pub span: Span,
pub target: String,
pub call_name: String,
pub call_kind: CallKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub call_receiver: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub call_receiver_types: Vec<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExpressionFlow {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub place: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub projection: Option<ExpressionProjection>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub source_names: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub call_sites: Vec<Span>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub aggregate_fields: Vec<ExpressionField>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tuple_items: Vec<ExpressionFlow>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub spreads: Vec<ExpressionFlow>,
}
impl ExpressionFlow {
#[must_use]
pub fn from_place(place: impl Into<String>) -> Self {
let place = place.into();
let place = place.trim().to_string();
if place.is_empty() {
return Self::default();
}
Self {
projection: ExpressionProjection::from_adapter_place(&place),
source_names: vec![place.clone()],
place: Some(place),
..Self::default()
}
}
#[must_use]
pub fn from_source_names(source_names: Vec<String>) -> Self {
Self {
source_names,
..Self::default()
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.place.is_none()
&& self.projection.is_none()
&& self.source_names.is_empty()
&& self.call_sites.is_empty()
&& self.aggregate_fields.is_empty()
&& self.tuple_items.is_empty()
&& self.spreads.is_empty()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExpressionProjection {
pub base: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub path: Vec<String>,
}
impl ExpressionProjection {
#[must_use]
pub fn from_adapter_place(place: &str) -> Option<Self> {
let mut parts = place.split('.').map(str::trim).filter(|part| !part.is_empty());
let base = parts.next()?.to_string();
let path: Vec<String> = parts.map(ToString::to_string).collect();
(!path.is_empty()).then_some(Self { base, path })
}
#[must_use]
pub fn canonical_place(&self) -> String {
std::iter::once(self.base.as_str())
.chain(self.path.iter().map(String::as_str))
.collect::<Vec<_>>()
.join(".")
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExpressionField {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub value_span: Option<Span>,
pub value: ExpressionFlow,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AggregateLayout {
pub type_name: String,
pub fields: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum FlowEvent {
Call {
span: Span,
name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
receiver: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
receiver_types: Vec<String>,
call_kind: CallKind,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
args: Vec<CallArg>,
},
Branch {
span: Span,
#[serde(default, skip_serializing_if = "Option::is_none")]
condition: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
then_events: Vec<FlowEvent>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
else_events: Vec<FlowEvent>,
},
Loop {
span: Span,
loop_kind: LoopKind,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
body: Vec<FlowEvent>,
},
Assign {
span: Span,
target: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
source_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
source_call: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
source_call_args: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
source_names: Vec<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
declares_new_binding: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
value_kind: Option<AssignValueKind>,
},
AggregateAssign {
span: Span,
target: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
type_name: Option<String>,
#[serde(default, skip_serializing_if = "ExpressionFlow::is_empty")]
value_flow: ExpressionFlow,
},
Return {
span: Span,
#[serde(default, skip_serializing_if = "Option::is_none")]
value_kind: Option<AssignValueKind>,
#[serde(default, skip_serializing_if = "Option::is_none")]
value_text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
value_name: Option<String>,
#[serde(default, skip_serializing_if = "ExpressionFlow::is_empty")]
value_flow: ExpressionFlow,
},
Throw {
span: Span,
#[serde(default, skip_serializing_if = "Option::is_none")]
value_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
thrown_type: Option<String>,
},
Try {
span: Span,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
body: Vec<FlowEvent>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
catch_events: Vec<FlowEvent>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
finally_events: Vec<FlowEvent>,
#[serde(default, skip_serializing_if = "Option::is_none")]
catch_param: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
catch_types: Vec<String>,
},
Break {
span: Span,
#[serde(default, skip_serializing_if = "Option::is_none")]
label: Option<String>,
},
Continue {
span: Span,
#[serde(default, skip_serializing_if = "Option::is_none")]
label: Option<String>,
},
Yield {
span: Span,
#[serde(default, skip_serializing_if = "Option::is_none")]
value_text: Option<String>,
#[serde(default, skip_serializing_if = "ExpressionFlow::is_empty")]
value_flow: ExpressionFlow,
},
Await {
span: Span,
#[serde(default, skip_serializing_if = "Option::is_none")]
value_name: Option<String>,
},
Defer {
span: Span,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
body: Vec<FlowEvent>,
},
Using {
span: Span,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
body: Vec<FlowEvent>,
},
Lifecycle {
span: Span,
name: String,
transition: String,
},
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompilerSyntaxHeader {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub calls: Vec<CompilerCallHeader>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub returns: Vec<CompilerReturnHeader>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub assignment_aliases: Vec<CompilerAssignmentAlias>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub factory_assignments: Vec<CompilerFactoryCallAssignment>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub callback_arguments: Vec<CompilerCallbackArgumentHeader>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub typed_callables: Vec<CompilerTypedCallableHeader>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub type_aliases: Vec<TypeAliasBinding>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompilerBrowseHeader {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub groups: Vec<CompilerBrowseTermGroup>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompilerBrowseTermGroup {
pub kind: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub terms: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompilerCallHeader {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub receiver: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub receiver_types: Vec<String>,
pub call_kind: CallKind,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompilerReturnHeader {
pub span: Span,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub value_text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub value_name: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompilerAssignmentAlias {
pub target: String,
pub source: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompilerFactoryCallAssignment {
pub target: String,
pub call_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub call_receiver: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompilerCallbackArgumentHeader {
pub call_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub call_receiver: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub call_receiver_types: Vec<String>,
pub argument_index: usize,
pub params: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompilerTypedCallableHeader {
pub name: String,
pub params: Vec<String>,
pub type_names: Vec<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompilerAttribution {
pub file: FileId,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub functions: Vec<CompilerFunctionAttribution>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompilerFunctionAttribution {
pub declaration_span: Span,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub calls: Vec<CompilerCallAttribution>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub return_spans: Vec<Span>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub writes: Vec<CompilerWriteAttribution>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompilerCallAttribution {
pub span: Span,
pub name: String,
pub call_kind: CallKind,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub args: Vec<CompilerCallArgumentAttribution>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub receiver: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub receiver_source_names: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub receiver_types: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompilerCallArgumentAttribution {
pub span: Span,
pub value_text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub place: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub source_names: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompilerWriteAttribution {
pub span: Span,
pub target: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub source_names: Vec<String>,
}
impl CompilerAttribution {
#[must_use]
pub fn from_decl_index(index: &DeclIndex) -> Self {
fn collect_expression_carriers(flow: &ExpressionFlow, out: &mut ahash::AHashSet<String>) {
if let Some(place) = flow.place.as_deref() {
let place = place.trim();
if !place.is_empty() {
out.insert(place.to_string());
}
}
for source in &flow.source_names {
let source = source.trim();
if !source.is_empty() {
out.insert(source.to_string());
}
}
for field in &flow.aggregate_fields {
collect_expression_carriers(&field.value, out);
}
for item in &flow.tuple_items {
collect_expression_carriers(item, out);
}
for spread in &flow.spreads {
collect_expression_carriers(spread, out);
}
}
fn walk(
events: &[FlowEvent],
receiver_facts: &[CallReceiverFact],
calls: &mut ahash::AHashMap<Span, CompilerCallAttribution>,
return_spans: &mut Vec<Span>,
writes: &mut Vec<CompilerWriteAttribution>,
) {
for event in events {
match event {
FlowEvent::Call {
span,
name,
receiver,
receiver_types,
call_kind,
args,
} => {
let mut receiver_source_names = ahash::AHashSet::default();
if let Some(fact) = call_receiver_fact_for_span(receiver_facts, *span) {
collect_expression_carriers(&fact.value_flow, &mut receiver_source_names);
}
let mut receiver_source_names = receiver_source_names.into_iter().collect::<Vec<_>>();
receiver_source_names.sort();
calls.insert(
*span,
CompilerCallAttribution {
span: *span,
name: name.clone(),
call_kind: *call_kind,
args: args
.iter()
.map(|arg| CompilerCallArgumentAttribution {
span: arg.span,
value_text: arg.value_text.clone(),
place: arg.place.clone(),
source_names: arg.source_names.clone(),
})
.collect(),
receiver: receiver.clone(),
receiver_source_names,
receiver_types: receiver_types.clone(),
},
);
}
FlowEvent::Assign {
span,
target,
source_name,
source_names,
source_call,
source_call_args,
..
} => {
let mut write_sources = source_name.iter().cloned().collect::<Vec<_>>();
write_sources.extend(source_names.iter().cloned());
writes.push(CompilerWriteAttribution {
span: *span,
target: target.clone(),
source_names: write_sources,
});
if let Some(name) = source_call {
calls.entry(*span).or_insert_with(|| CompilerCallAttribution {
span: *span,
name: name.clone(),
call_kind: CallKind::Function,
args: source_call_args
.iter()
.map(|argument| {
let argument = argument.trim();
CompilerCallArgumentAttribution {
span: *span,
value_text: argument.to_string(),
place: None,
source_names: Vec::new(),
}
})
.collect(),
receiver: None,
receiver_source_names: Vec::new(),
receiver_types: Vec::new(),
});
}
}
FlowEvent::Return { span, .. } => return_spans.push(*span),
FlowEvent::Branch {
then_events,
else_events,
..
} => {
walk(then_events, receiver_facts, calls, return_spans, writes);
walk(else_events, receiver_facts, calls, return_spans, writes);
}
FlowEvent::Loop { body, .. }
| FlowEvent::Defer { body, .. }
| FlowEvent::Using { body, .. } => {
walk(body, receiver_facts, calls, return_spans, writes);
}
FlowEvent::Try {
body,
catch_events,
finally_events,
..
} => {
walk(body, receiver_facts, calls, return_spans, writes);
walk(catch_events, receiver_facts, calls, return_spans, writes);
walk(finally_events, receiver_facts, calls, return_spans, writes);
}
_ => {}
}
}
}
let mut functions = Vec::new();
for decl in &index.defs {
if !matches!(
decl.kind,
DeclKind::Function | DeclKind::Method | DeclKind::Constructor
) {
continue;
}
let mut calls = ahash::AHashMap::default();
let mut return_spans = Vec::new();
let mut writes = Vec::new();
walk(
&decl.flow_events,
&index.call_receivers,
&mut calls,
&mut return_spans,
&mut writes,
);
let mut calls = calls.into_values().collect::<Vec<_>>();
calls.sort_by_key(|call| {
(
call.span.file.raw(),
call.span.start,
std::cmp::Reverse(call.span.end),
)
});
return_spans.sort_by_key(|span| (span.file.raw(), span.start, span.end));
return_spans.dedup();
functions.push(CompilerFunctionAttribution {
declaration_span: decl.span,
calls,
return_spans,
writes,
});
}
functions.sort_by_key(|function| {
(
function.declaration_span.file.raw(),
function.declaration_span.start,
function.declaration_span.end,
)
});
Self {
file: index.file,
functions,
}
}
#[must_use]
pub fn function_at_span(&self, span: Span) -> Option<&CompilerFunctionAttribution> {
let key = |value: Span| (value.file.raw(), value.start, value.end);
let wanted = key(span);
let index = self
.functions
.binary_search_by_key(&wanted, |function| key(function.declaration_span))
.ok()?;
self.functions.get(index)
}
}
impl CompilerSyntaxHeader {
#[must_use]
pub fn from_decl_index(index: &DeclIndex) -> Self {
fn push_unique(values: &mut Vec<String>, value: &str) {
let value = value.trim();
if !value.is_empty() && !values.iter().any(|existing| existing == value) {
values.push(value.to_string());
}
}
struct Projection<'a> {
assignment_values: &'a ahash::AHashMap<Span, &'a AssignmentValueFact>,
call_argument_values: &'a ahash::AHashMap<Span, Vec<&'a CallArgumentValueFact>>,
out: &'a mut CompilerSyntaxHeader,
}
fn walk(events: &[FlowEvent], projection: &mut Projection<'_>) {
for event in events {
match event {
FlowEvent::Call {
span,
name,
receiver,
receiver_types,
call_kind,
..
} => {
projection.out.calls.push(CompilerCallHeader {
name: name.clone(),
receiver: receiver.clone(),
receiver_types: receiver_types.clone(),
call_kind: *call_kind,
});
if let Some(arguments) = projection.call_argument_values.get(span) {
for argument in arguments
.iter()
.filter(|argument| !argument.inline_callback_params.is_empty())
{
projection
.out
.callback_arguments
.push(CompilerCallbackArgumentHeader {
call_name: name.clone(),
call_receiver: receiver.clone(),
call_receiver_types: receiver_types.clone(),
argument_index: argument.argument_index,
params: argument.inline_callback_params.clone(),
});
}
}
}
FlowEvent::Assign {
span,
target,
source_name,
source_call,
..
} => {
let indexed = projection.assignment_values.get(span).copied();
if let Some(source) = source_name.as_deref() {
let target = target.trim();
let source = source.trim();
if !target.is_empty() && !source.is_empty() {
projection.out.assignment_aliases.push(CompilerAssignmentAlias {
target: target.to_string(),
source: source.to_string(),
});
}
}
if let Some(name) = source_call
.as_deref()
.map(str::trim)
.filter(|name| !name.is_empty())
{
projection.out.calls.push(CompilerCallHeader {
name: name.to_string(),
receiver: indexed
.and_then(|fact| fact.direct_call_receiver.as_deref())
.map(str::to_string),
receiver_types: Vec::new(),
call_kind: CallKind::Function,
});
}
let call_name = indexed
.and_then(|fact| fact.direct_call_name.as_deref())
.or(source_call.as_deref())
.map(str::trim)
.filter(|name| !name.is_empty());
if let Some(call_name) = call_name {
let target = target.trim();
if !target.is_empty() {
projection
.out
.factory_assignments
.push(CompilerFactoryCallAssignment {
target: target.to_string(),
call_name: call_name.to_string(),
call_receiver: indexed
.and_then(|fact| fact.direct_call_receiver.as_deref())
.map(str::to_string),
});
}
}
}
FlowEvent::Return {
span,
value_text,
value_name,
..
} => projection.out.returns.push(CompilerReturnHeader {
span: *span,
value_text: value_text.clone(),
value_name: value_name.clone(),
}),
FlowEvent::Branch {
then_events,
else_events,
..
} => {
walk(then_events, projection);
walk(else_events, projection);
}
FlowEvent::Loop { body, .. }
| FlowEvent::Defer { body, .. }
| FlowEvent::Using { body, .. } => {
walk(body, projection);
}
FlowEvent::Try {
body,
catch_events,
finally_events,
..
} => {
walk(body, projection);
walk(catch_events, projection);
walk(finally_events, projection);
}
_ => {}
}
}
}
let mut out = Self::default();
let assignment_values = index
.assignment_values
.iter()
.map(|fact| (fact.assignment_span, fact))
.collect::<ahash::AHashMap<_, _>>();
let mut call_argument_values = ahash::AHashMap::<Span, Vec<&CallArgumentValueFact>>::new();
for fact in &index.call_argument_values {
call_argument_values.entry(fact.call_span).or_default().push(fact);
}
for decl in &index.defs {
walk(
&decl.flow_events,
&mut Projection {
assignment_values: &assignment_values,
call_argument_values: &call_argument_values,
out: &mut out,
},
);
for alias in &decl.type_aliases {
if !alias.name.trim().is_empty()
&& !alias.type_name.trim().is_empty()
&& !out.type_aliases.iter().any(|existing| existing == alias)
{
out.type_aliases.push(alias.clone());
}
}
if !decl.params.is_empty() {
let mut type_names = decl
.type_aliases
.iter()
.filter(|alias| alias.name == decl.name)
.map(|alias| alias.type_name.clone())
.collect::<Vec<_>>();
type_names.sort();
type_names.dedup();
if !type_names.is_empty() {
out.typed_callables.push(CompilerTypedCallableHeader {
name: decl.name.clone(),
params: decl.params.clone(),
type_names,
});
}
}
}
for reference in &index.refs {
if reference.kind == RefKind::Call && !reference.name.trim().is_empty() {
out.calls.push(CompilerCallHeader {
name: reference.name.clone(),
receiver: None,
receiver_types: Vec::new(),
call_kind: CallKind::Function,
});
}
}
for call in &mut out.calls {
call.receiver_types.sort();
call.receiver_types.dedup();
}
out.calls.sort_by(|left, right| {
left.name
.cmp(&right.name)
.then_with(|| left.call_kind.as_str().cmp(right.call_kind.as_str()))
.then_with(|| left.receiver.cmp(&right.receiver))
.then_with(|| left.receiver_types.cmp(&right.receiver_types))
});
out.calls.dedup_by(|left, right| left == right);
out.returns.sort_by(|left, right| {
left.span
.start
.cmp(&right.span.start)
.then_with(|| left.span.end.cmp(&right.span.end))
.then_with(|| {
left.value_text
.cmp(&right.value_text)
.then_with(|| left.value_name.cmp(&right.value_name))
})
});
out.returns.dedup();
out.assignment_aliases.sort_by(|left, right| {
left.target
.cmp(&right.target)
.then_with(|| left.source.cmp(&right.source))
});
out.assignment_aliases.dedup_by(|left, right| left == right);
out.factory_assignments.sort_by(|left, right| {
left.target
.cmp(&right.target)
.then_with(|| left.call_name.cmp(&right.call_name))
.then_with(|| left.call_receiver.cmp(&right.call_receiver))
});
out.factory_assignments.dedup();
out.callback_arguments.sort_by(|left, right| {
left.call_name
.cmp(&right.call_name)
.then_with(|| left.argument_index.cmp(&right.argument_index))
.then_with(|| left.call_receiver.cmp(&right.call_receiver))
.then_with(|| left.call_receiver_types.cmp(&right.call_receiver_types))
.then_with(|| left.params.cmp(&right.params))
});
out.callback_arguments.dedup();
out.typed_callables.sort_by(|left, right| {
left.name
.cmp(&right.name)
.then_with(|| left.type_names.cmp(&right.type_names))
.then_with(|| left.params.cmp(&right.params))
});
out.typed_callables.dedup();
out.type_aliases.sort_by(|left, right| {
left.name
.cmp(&right.name)
.then_with(|| left.type_name.cmp(&right.type_name))
});
out.type_aliases.dedup();
for call in &mut out.calls {
let mut normalized = Vec::with_capacity(call.receiver_types.len());
for receiver_type in &call.receiver_types {
push_unique(&mut normalized, receiver_type);
}
call.receiver_types = normalized;
}
out
}
}
impl FlowEvent {
#[must_use]
pub fn span(&self) -> Span {
match self {
FlowEvent::Call { span, .. }
| FlowEvent::Branch { span, .. }
| FlowEvent::Loop { span, .. }
| FlowEvent::Assign { span, .. }
| FlowEvent::AggregateAssign { span, .. }
| FlowEvent::Return { span, .. }
| FlowEvent::Throw { span, .. }
| FlowEvent::Try { span, .. }
| FlowEvent::Break { span, .. }
| FlowEvent::Continue { span, .. }
| FlowEvent::Yield { span, .. }
| FlowEvent::Await { span, .. }
| FlowEvent::Defer { span, .. }
| FlowEvent::Using { span, .. }
| FlowEvent::Lifecycle { span, .. } => *span,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CallKind {
Function,
Method,
Constructor,
Macro,
Operator,
Indirect,
ChannelSend,
IndexWrite,
}
impl CallKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
CallKind::Function => "function",
CallKind::Method => "method",
CallKind::Constructor => "constructor",
CallKind::Macro => "macro",
CallKind::Operator => "operator",
CallKind::Indirect => "indirect",
CallKind::ChannelSend => "channel_send",
CallKind::IndexWrite => "index_write",
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AssignValueKind {
Literal,
CallResult,
YieldResult,
Destructure,
CallableReference,
Compound,
Unknown,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LoopKind {
For,
While,
DoWhile,
ForEach,
Loop,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CallArg {
pub span: Span,
#[serde(default)]
pub passing_mode: ArgumentPassingMode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub value_text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub place: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub source_names: Vec<String>,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ArgumentPassingMode {
#[default]
Value,
WriteBack,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Operation {
pub span: Span,
pub kind: OperationKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub operands: Vec<OperationOperand>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OperationKind {
Read,
Write,
Call,
Index,
Deref,
FieldAccess,
Cast,
ResourceUse,
Allocate,
Release,
Lifecycle,
Return,
Throw,
Await,
Yield,
BranchCondition,
CatchBinding,
ExternalBoundary,
}
impl OperationKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
OperationKind::Read => "read",
OperationKind::Write => "write",
OperationKind::Call => "call",
OperationKind::Index => "index",
OperationKind::Deref => "deref",
OperationKind::FieldAccess => "field_access",
OperationKind::Cast => "cast",
OperationKind::ResourceUse => "resource_use",
OperationKind::Allocate => "allocate",
OperationKind::Release => "release",
OperationKind::Lifecycle => "lifecycle",
OperationKind::Return => "return",
OperationKind::Throw => "throw",
OperationKind::Await => "await",
OperationKind::Yield => "yield",
OperationKind::BranchCondition => "branch_condition",
OperationKind::CatchBinding => "catch_binding",
OperationKind::ExternalBoundary => "external_boundary",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct OperationOperand {
pub name: String,
pub role: OperationOperandRole,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OperationOperandRole {
Read,
Write,
Receiver,
Argument,
Callee,
Condition,
Returned,
Thrown,
Resource,
Transition,
}
impl OperationOperandRole {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
OperationOperandRole::Read => "read",
OperationOperandRole::Write => "write",
OperationOperandRole::Receiver => "receiver",
OperationOperandRole::Argument => "argument",
OperationOperandRole::Callee => "callee",
OperationOperandRole::Condition => "condition",
OperationOperandRole::Returned => "returned",
OperationOperandRole::Thrown => "thrown",
OperationOperandRole::Resource => "resource",
OperationOperandRole::Transition => "transition",
}
}
}
#[must_use]
pub fn operations_from_flow_events(events: &[FlowEvent]) -> Vec<Operation> {
let mut out = Vec::new();
collect_operations(events, &mut out);
for op in &mut out {
dedup_operands(&mut op.operands);
}
out
}
fn collect_operations(events: &[FlowEvent], out: &mut Vec<Operation>) {
for event in events {
match event {
FlowEvent::Call {
span,
name,
receiver,
call_kind,
args,
..
} => {
let mut operands = vec![OperationOperand {
name: name.clone(),
role: OperationOperandRole::Callee,
}];
if let Some(receiver) = non_empty(receiver.as_deref()) {
operands.push(OperationOperand {
name: receiver.to_string(),
role: OperationOperandRole::Receiver,
});
}
for arg in args {
push_call_arg_operands(&mut operands, arg);
}
out.push(Operation {
span: *span,
kind: OperationKind::Call,
target: Some(name.clone()),
operands,
detail: Some(call_kind.as_str().to_string()),
});
if matches!(call_kind, CallKind::Constructor) {
out.push(Operation {
span: *span,
kind: OperationKind::Allocate,
target: Some(name.clone()),
operands: vec![OperationOperand {
name: name.clone(),
role: OperationOperandRole::Callee,
}],
detail: Some("constructor".to_string()),
});
}
if let Some(receiver) = non_empty(receiver.as_deref()) {
out.push(Operation {
span: *span,
kind: OperationKind::Read,
target: Some(receiver.to_string()),
operands: vec![OperationOperand {
name: receiver.to_string(),
role: OperationOperandRole::Receiver,
}],
detail: Some("call_receiver".to_string()),
});
push_place_shape_operations(*span, receiver, OperationOperandRole::Receiver, out);
}
for arg in args {
push_call_arg_read_operations(*span, arg, out);
}
}
FlowEvent::Branch {
span,
condition,
then_events,
else_events,
} => {
if let Some(condition) = non_empty(condition.as_deref()) {
out.push(Operation {
span: *span,
kind: OperationKind::BranchCondition,
target: Some(condition.to_string()),
operands: vec![OperationOperand {
name: condition.to_string(),
role: OperationOperandRole::Condition,
}],
detail: None,
});
}
collect_operations(then_events, out);
collect_operations(else_events, out);
}
FlowEvent::Loop { body, .. } => collect_operations(body, out),
FlowEvent::Assign {
span,
target,
source_name,
source_call,
source_call_args,
source_names,
..
} => {
let mut operands = vec![OperationOperand {
name: target.clone(),
role: OperationOperandRole::Write,
}];
push_optional_operand(&mut operands, source_name.as_deref(), OperationOperandRole::Read);
push_optional_operand(
&mut operands,
source_call.as_deref(),
OperationOperandRole::Callee,
);
for source in source_names {
push_optional_operand(&mut operands, Some(source.as_str()), OperationOperandRole::Read);
}
for arg in source_call_args {
push_optional_operand(&mut operands, Some(arg.as_str()), OperationOperandRole::Argument);
}
out.push(Operation {
span: *span,
kind: OperationKind::Write,
target: Some(target.clone()),
operands,
detail: None,
});
push_place_shape_operations(*span, target, OperationOperandRole::Write, out);
if let Some(source) = non_empty(source_name.as_deref()) {
out.push(Operation {
span: *span,
kind: OperationKind::Read,
target: Some(source.to_string()),
operands: vec![OperationOperand {
name: source.to_string(),
role: OperationOperandRole::Read,
}],
detail: Some("assign_source".to_string()),
});
push_place_shape_operations(*span, source, OperationOperandRole::Read, out);
}
for source in source_names {
if let Some(source) = non_empty(Some(source.as_str())) {
out.push(Operation {
span: *span,
kind: OperationKind::Read,
target: Some(source.to_string()),
operands: vec![OperationOperand {
name: source.to_string(),
role: OperationOperandRole::Read,
}],
detail: Some("assign_source".to_string()),
});
push_place_shape_operations(*span, source, OperationOperandRole::Read, out);
}
}
if let Some(call) = non_empty(source_call.as_deref()) {
out.push(Operation {
span: *span,
kind: OperationKind::Call,
target: Some(call.to_string()),
operands: source_call_args
.iter()
.filter_map(|arg| non_empty(Some(arg.as_str())))
.map(|arg| OperationOperand {
name: arg.to_string(),
role: OperationOperandRole::Argument,
})
.collect(),
detail: Some("assignment_source".to_string()),
});
}
}
FlowEvent::AggregateAssign {
span,
target,
value_flow,
..
} => {
let sources = expression_flow_source_names(value_flow);
let mut operands = vec![OperationOperand {
name: target.clone(),
role: OperationOperandRole::Write,
}];
operands.extend(sources.iter().cloned().map(|name| OperationOperand {
name,
role: OperationOperandRole::Read,
}));
out.push(Operation {
span: *span,
kind: OperationKind::Write,
target: Some(target.clone()),
operands,
detail: Some("aggregate_initializer".to_string()),
});
push_place_shape_operations(*span, target, OperationOperandRole::Write, out);
for source in sources {
out.push(Operation {
span: *span,
kind: OperationKind::Read,
target: Some(source.clone()),
operands: vec![OperationOperand {
name: source.clone(),
role: OperationOperandRole::Read,
}],
detail: Some("aggregate_initializer".to_string()),
});
push_place_shape_operations(*span, &source, OperationOperandRole::Read, out);
}
}
FlowEvent::Return {
span,
value_name,
value_flow,
..
} => {
let target = value_name
.as_ref()
.filter(|s| !s.trim().is_empty())
.or_else(|| value_flow.place.as_ref().filter(|s| !s.trim().is_empty()))
.cloned();
let sources = expression_flow_source_names(value_flow);
let operands = sources
.iter()
.map(|source| OperationOperand {
name: source.clone(),
role: OperationOperandRole::Returned,
})
.collect();
out.push(Operation {
span: *span,
kind: OperationKind::Return,
target,
operands,
detail: None,
});
for value in sources {
out.push(Operation {
span: *span,
kind: OperationKind::Read,
target: Some(value.clone()),
operands: vec![OperationOperand {
name: value.clone(),
role: OperationOperandRole::Returned,
}],
detail: Some("return_value".to_string()),
});
push_place_shape_operations(*span, &value, OperationOperandRole::Returned, out);
}
}
FlowEvent::Throw {
span,
value_name,
thrown_type,
} => {
let mut operands = Vec::new();
push_optional_operand(&mut operands, value_name.as_deref(), OperationOperandRole::Thrown);
out.push(Operation {
span: *span,
kind: OperationKind::Throw,
target: value_name.clone().or_else(|| thrown_type.clone()),
operands,
detail: thrown_type.clone(),
});
if let Some(value) = non_empty(value_name.as_deref()) {
out.push(Operation {
span: *span,
kind: OperationKind::Read,
target: Some(value.to_string()),
operands: vec![OperationOperand {
name: value.to_string(),
role: OperationOperandRole::Thrown,
}],
detail: Some("throw_value".to_string()),
});
}
}
FlowEvent::Try {
body,
catch_events,
finally_events,
catch_param,
..
} => {
collect_operations(body, out);
if let Some(catch_param) = non_empty(catch_param.as_deref()) {
out.push(Operation {
span: event.span(),
kind: OperationKind::CatchBinding,
target: Some(catch_param.to_string()),
operands: vec![OperationOperand {
name: catch_param.to_string(),
role: OperationOperandRole::Write,
}],
detail: None,
});
}
collect_operations(catch_events, out);
collect_operations(finally_events, out);
}
FlowEvent::Yield {
span,
value_text,
value_flow,
} => {
let sources = expression_flow_source_names(value_flow);
let source_syntax = value_text
.as_deref()
.map(str::trim)
.filter(|text| !text.is_empty() && value_flow.place.is_some() && sources.len() == 1);
out.push(Operation {
span: *span,
kind: OperationKind::Yield,
target: value_text.clone().filter(|s| !s.trim().is_empty()),
operands: sources
.iter()
.map(|source| OperationOperand {
name: source.clone(),
role: OperationOperandRole::Returned,
})
.collect(),
detail: None,
});
for value in sources {
let rendered = source_syntax.unwrap_or(&value);
out.push(Operation {
span: *span,
kind: OperationKind::Read,
target: Some(rendered.to_string()),
operands: vec![OperationOperand {
name: rendered.to_string(),
role: OperationOperandRole::Returned,
}],
detail: Some("yield_value".to_string()),
});
push_place_shape_operations(*span, rendered, OperationOperandRole::Returned, out);
}
}
FlowEvent::Await { span, value_name } => {
let mut operands = Vec::new();
push_optional_operand(&mut operands, value_name.as_deref(), OperationOperandRole::Read);
out.push(Operation {
span: *span,
kind: OperationKind::Await,
target: value_name.clone(),
operands,
detail: None,
});
if let Some(value) = non_empty(value_name.as_deref()) {
out.push(Operation {
span: *span,
kind: OperationKind::Read,
target: Some(value.to_string()),
operands: vec![OperationOperand {
name: value.to_string(),
role: OperationOperandRole::Read,
}],
detail: Some("await_value".to_string()),
});
}
}
FlowEvent::Defer { body, .. } => collect_operations(body, out),
FlowEvent::Using { span, body } => {
out.push(Operation {
span: *span,
kind: OperationKind::ResourceUse,
target: None,
operands: Vec::new(),
detail: Some("using_scope".to_string()),
});
collect_operations(body, out);
}
FlowEvent::Lifecycle {
span,
name,
transition,
} => {
let kind = if is_release_transition(transition) {
OperationKind::Release
} else {
OperationKind::Lifecycle
};
out.push(Operation {
span: *span,
kind,
target: Some(name.clone()),
operands: vec![
OperationOperand {
name: name.clone(),
role: OperationOperandRole::Resource,
},
OperationOperand {
name: transition.clone(),
role: OperationOperandRole::Transition,
},
],
detail: Some(transition.clone()),
});
}
FlowEvent::Break { .. } | FlowEvent::Continue { .. } => {}
}
}
}
fn push_call_arg_operands(out: &mut Vec<OperationOperand>, arg: &CallArg) {
if let Some(place) = non_empty(arg.place.as_deref()) {
out.push(OperationOperand {
name: place.to_string(),
role: OperationOperandRole::Argument,
});
}
for source in &arg.source_names {
push_optional_operand(out, Some(source.as_str()), OperationOperandRole::Read);
}
}
fn push_call_arg_read_operations(span: Span, arg: &CallArg, out: &mut Vec<Operation>) {
if let Some(place) = non_empty(arg.place.as_deref()) {
out.push(Operation {
span,
kind: OperationKind::Read,
target: Some(place.to_string()),
operands: vec![OperationOperand {
name: place.to_string(),
role: OperationOperandRole::Argument,
}],
detail: Some("call_argument".to_string()),
});
push_place_shape_operations(span, place, OperationOperandRole::Argument, out);
}
for source in &arg.source_names {
if let Some(source) = non_empty(Some(source.as_str())) {
out.push(Operation {
span,
kind: OperationKind::Read,
target: Some(source.to_string()),
operands: vec![OperationOperand {
name: source.to_string(),
role: OperationOperandRole::Argument,
}],
detail: Some("call_argument".to_string()),
});
push_place_shape_operations(span, source, OperationOperandRole::Argument, out);
}
}
}
fn push_place_shape_operations(
span: Span,
place: &str,
role: OperationOperandRole,
out: &mut Vec<Operation>,
) {
if place_has_index_shape(place) {
out.push(Operation {
span,
kind: OperationKind::Index,
target: Some(place.to_string()),
operands: vec![OperationOperand {
name: place.to_string(),
role,
}],
detail: Some("normalized_place".to_string()),
});
}
if place_has_deref_shape(place) {
out.push(Operation {
span,
kind: OperationKind::Deref,
target: Some(place.to_string()),
operands: vec![OperationOperand {
name: place.to_string(),
role,
}],
detail: Some("normalized_place".to_string()),
});
}
if place_has_field_shape(place) {
out.push(Operation {
span,
kind: OperationKind::FieldAccess,
target: Some(place.to_string()),
operands: vec![OperationOperand {
name: place.to_string(),
role,
}],
detail: Some("normalized_place".to_string()),
});
}
}
fn place_has_index_shape(place: &str) -> bool {
place.contains('[') && place.contains(']')
}
fn place_has_deref_shape(place: &str) -> bool {
let trimmed = place.trim_start();
trimmed.starts_with('*') || trimmed.starts_with('&')
}
fn place_has_field_shape(place: &str) -> bool {
bonsai_common::qualified_name_owner(place).is_some()
}
fn push_optional_operand(out: &mut Vec<OperationOperand>, name: Option<&str>, role: OperationOperandRole) {
if let Some(name) = non_empty(name) {
out.push(OperationOperand {
name: name.to_string(),
role,
});
}
}
fn non_empty(value: Option<&str>) -> Option<&str> {
value.and_then(|s| {
let trimmed = s.trim();
(!trimmed.is_empty()).then_some(trimmed)
})
}
fn expression_flow_source_names(flow: &ExpressionFlow) -> Vec<String> {
fn collect(flow: &ExpressionFlow, out: &mut Vec<String>) {
if let Some(place) = non_empty(flow.place.as_deref()) {
out.push(place.to_string());
}
out.extend(
flow.source_names
.iter()
.filter(|name| !name.trim().is_empty())
.cloned(),
);
for field in &flow.aggregate_fields {
collect(&field.value, out);
}
for item in &flow.tuple_items {
collect(item, out);
}
for spread in &flow.spreads {
collect(spread, out);
}
}
let mut out = Vec::new();
collect(flow, &mut out);
out.sort();
out.dedup();
out
}
fn is_release_transition(transition: &str) -> bool {
matches!(
transition,
"freed" | "closed" | "unlocked" | "cancelled" | "canceled" | "moved"
)
}
fn dedup_operands(operands: &mut Vec<OperationOperand>) {
let mut seen = std::collections::HashSet::new();
operands.retain(|operand| seen.insert((operand.role, operand.name.clone())));
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RefKind {
Read,
Write,
Call,
Type,
Macro,
Import,
Decorator,
Other,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Ref {
pub span: Span,
pub name: String,
pub kind: RefKind,
pub scope: Option<SymbolId>,
pub resolved: Option<SymbolId>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AssignmentValueFact {
pub assignment_span: Span,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
#[serde(default)]
pub target_is_immutable: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_owner: Option<SymbolId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_span: Option<Span>,
pub value_span: Span,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub call_sites: Vec<Span>,
#[serde(default, skip_serializing_if = "ExpressionFlow::is_empty")]
pub value_flow: ExpressionFlow,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exact_callable_return: Option<ExpressionFlow>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exact_static_call_args: Option<Vec<StaticScalarValue>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub direct_call_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub direct_call_receiver: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct StaticStringMapEntry {
pub key: String,
pub value: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct StaticStringMapFact {
pub assignment_span: Span,
pub target: String,
pub entries: Vec<StaticStringMapEntry>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum StringCompositionPart {
Literal {
value: String,
},
Place {
place: String,
},
PlaceOrLiteral {
place: String,
fallback: String,
},
Call {
span: Span,
},
CallOrLiteral {
span: Span,
fallback: String,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct StringCompositionFact {
pub container_span: Span,
pub value_span: Span,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
pub parts: Vec<StringCompositionPart>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FiniteLiteralSelectionFact {
pub selection_span: Span,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub assignment_span: Option<Span>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub call_span: Option<Span>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub argument_index: Option<usize>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum CharacterSubstitutionDomain {
TableKeysWithIdentityFallback,
ExactCharacters { characters: Vec<String> },
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CharacterSubstitutionFact {
pub function_span: Span,
pub transform_span: Span,
pub input_param_index: usize,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub exact_mappings: Vec<StaticStringMapEntry>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub table: String,
pub domain: CharacterSubstitutionDomain,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CharacterClass {
Alphabetic,
Alphanumeric,
Digit,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum CharacterConstraintDomain {
AllowOnly {
classes: Vec<CharacterClass>,
exact_characters: Vec<String>,
},
ExcludesExact { characters: Vec<String> },
SubstitutesExact { mappings: Vec<StaticStringMapEntry> },
ProviderBound {
factory_call: String,
operation_call: String,
domain: Box<CharacterConstraintDomain>,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum CharacterConstraintOutput {
Assignment {
target: String,
},
Return,
Expression {
span: Span,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CharacterConstraintFact {
pub function_span: Span,
pub transform_span: Span,
pub input_place: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_param_index: Option<usize>,
pub output: CharacterConstraintOutput,
pub domain: CharacterConstraintDomain,
}
#[must_use]
pub fn character_constraints_from_substitutions(
defs: &[Decl],
substitutions: &[CharacterSubstitutionFact],
) -> Vec<CharacterConstraintFact> {
substitutions
.iter()
.filter_map(|fact| {
let decl = defs.iter().find(|decl| decl.span == fact.function_span)?;
let input_place = decl.params.get(fact.input_param_index)?.clone();
let mut characters = fact
.exact_mappings
.iter()
.filter(|mapping| !mapping.value.contains(&mapping.key))
.map(|mapping| mapping.key.clone())
.collect::<Vec<_>>();
characters.sort();
characters.dedup();
(!characters.is_empty()).then_some(CharacterConstraintFact {
function_span: fact.function_span,
transform_span: fact.transform_span,
input_place,
input_param_index: Some(fact.input_param_index),
output: CharacterConstraintOutput::Return,
domain: CharacterConstraintDomain::ExcludesExact { characters },
})
})
.collect()
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct GuardedValueFilterFact {
pub function_span: Span,
pub predicate_call_span: Span,
pub write_span: Span,
pub input_place: String,
pub output_place: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SameOriginPathConstraintFact {
pub function_span: Span,
pub guard_span: Span,
pub input_place: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_param_index: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_call: Option<String>,
pub rejects_scheme: bool,
pub rejects_authority: bool,
pub requires_absolute_path: bool,
pub rejects_scheme_relative_path: bool,
}
pub const COMPILER_GUARD_RELATIVE_PATH_BOUNDARY_REJECTION: &str = "path.relative-boundary-rejection";
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompilerGuardFact {
pub function_span: Span,
pub guarded_call_span: Span,
pub proof_span: Span,
pub capability: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub evidence: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DynamicKeyFilterFact {
pub function_span: Span,
pub guard_span: Span,
pub input_param_index: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_place: Option<String>,
pub collection_constructor: String,
pub membership_check: String,
pub rejected_exact_values: Vec<String>,
pub recursive: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeTypeNarrowingFact {
pub branch_span: Span,
pub guarded_span: Span,
pub subject: String,
pub type_name: String,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BranchConditionPolarity {
Positive,
Negated,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct MembershipConditionFact {
pub subject: String,
pub collection: String,
pub then_contains: bool,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConditionEquality {
Equal,
NotEqual,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConditionOperandFact {
pub span: Span,
#[serde(default, skip_serializing_if = "ExpressionFlow::is_empty")]
pub value_flow: ExpressionFlow,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub static_string: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub static_value: Option<StaticScalarValue>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ConditionExpressionFact {
Atom {
span: Span,
},
Truthy {
span: Span,
operand: ConditionOperandFact,
},
Not {
span: Span,
operand: Box<ConditionExpressionFact>,
},
All {
span: Span,
operands: Vec<ConditionExpressionFact>,
},
Any {
span: Span,
operands: Vec<ConditionExpressionFact>,
},
Equality {
span: Span,
relation: ConditionEquality,
left: ConditionOperandFact,
right: ConditionOperandFact,
},
TypeTest {
span: Span,
subject: ConditionOperandFact,
type_name: String,
},
Membership {
span: Span,
subject: ConditionOperandFact,
collection: ConditionOperandFact,
then_contains: bool,
},
}
impl ConditionExpressionFact {
#[must_use]
pub const fn span(&self) -> Span {
match self {
Self::Atom { span }
| Self::Truthy { span, .. }
| Self::Not { span, .. }
| Self::All { span, .. }
| Self::Any { span, .. }
| Self::Equality { span, .. }
| Self::TypeTest { span, .. }
| Self::Membership { span, .. } => *span,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BranchConditionFact {
pub branch_span: Span,
pub condition_span: Span,
pub polarity: BranchConditionPolarity,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub membership: Option<MembershipConditionFact>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expression: Option<ConditionExpressionFact>,
}
#[must_use]
pub fn branch_condition_fact_for_span(
facts: &[BranchConditionFact],
branch_span: Span,
) -> Option<&BranchConditionFact> {
let key = |span: Span| (span.file.raw(), span.start, span.end);
let wanted = key(branch_span);
let index = facts.partition_point(|fact| key(fact.branch_span) < wanted);
facts.get(index).filter(|fact| fact.branch_span == branch_span)
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CallReceiverFact {
pub call_span: Span,
pub receiver_span: Span,
pub value_flow: ExpressionFlow,
#[serde(default, skip_serializing_if = "CallReceiverRole::is_value")]
pub role: CallReceiverRole,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub static_value: Option<StaticScalarValue>,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CallReceiverRole {
#[default]
Value,
Namespace,
}
impl CallReceiverRole {
#[must_use]
pub const fn is_value(&self) -> bool {
matches!(self, Self::Value)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CallArgumentValueFact {
pub call_span: Span,
pub argument_index: usize,
pub argument_span: Span,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub direct_call_span: Option<Span>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub value_kind: Option<AssignValueKind>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub inline_callback_params: Vec<String>,
pub value_flow: ExpressionFlow,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub static_value: Option<StaticScalarValue>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub exact_static_aggregate_fields: Vec<StaticAggregateFieldValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exact_static_sequence_values: Option<Vec<Option<StaticScalarValue>>>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct StaticAggregateFieldValue {
pub path: Vec<String>,
pub value: StaticScalarValue,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum StaticScalarValue {
String(String),
Boolean(bool),
Null,
}
#[must_use]
pub fn call_receiver_fact_for_span(facts: &[CallReceiverFact], call_span: Span) -> Option<&CallReceiverFact> {
let key = |span: Span| (span.file.raw(), span.start, span.end);
let wanted = key(call_span);
let index = facts.partition_point(|fact| key(fact.call_span) < wanted);
facts.get(index).filter(|fact| fact.call_span == call_span)
}
#[must_use]
pub fn call_argument_value_fact(
facts: &[CallArgumentValueFact],
call_span: Span,
argument_index: usize,
) -> Option<&CallArgumentValueFact> {
let key = |span: Span, index: usize| (span.file.raw(), span.start, span.end, index);
let wanted = key(call_span, argument_index);
let index = facts.partition_point(|fact| key(fact.call_span, fact.argument_index) < wanted);
facts
.get(index)
.filter(|fact| fact.call_span == call_span && fact.argument_index == argument_index)
}
#[must_use]
pub fn assignment_value_fact_for_span(
facts: &[AssignmentValueFact],
assignment_span: Span,
) -> Option<&AssignmentValueFact> {
let key = |span: Span| (span.file.raw(), span.start, span.end);
let wanted = key(assignment_span);
let index = facts.partition_point(|fact| key(fact.assignment_span) < wanted);
facts
.get(index)
.filter(|fact| fact.assignment_span == assignment_span)
}
#[must_use]
pub fn finite_literal_selection_for_assignment(
facts: &[FiniteLiteralSelectionFact],
assignment_span: Span,
) -> Option<&FiniteLiteralSelectionFact> {
let key = |span: Span| (span.file.raw(), span.start, span.end);
let wanted = key(assignment_span);
let index = facts.partition_point(|fact| {
key(fact
.assignment_span
.or(fact.call_span)
.unwrap_or(fact.selection_span))
< wanted
});
facts
.get(index)
.filter(|fact| fact.assignment_span == Some(assignment_span))
}
#[must_use]
pub fn assignment_value_rendering<'a>(
facts: &[AssignmentValueFact],
assignment_span: Span,
source_text: &'a str,
) -> Option<&'a str> {
let fact = assignment_value_fact_for_span(facts, assignment_span)?;
render_assignment_syntax_span(fact.value_span, assignment_span, source_text)
}
#[derive(Clone, Debug, Default)]
pub struct AssignmentValueIndex {
spans: AHashMap<Span, AssignmentSyntaxSpans>,
}
#[derive(Copy, Clone, Debug)]
struct AssignmentSyntaxSpans {
target: Option<Span>,
value: Span,
}
impl AssignmentValueIndex {
#[must_use]
pub fn new(facts: &[AssignmentValueFact]) -> Self {
let mut spans = AHashMap::with_capacity(facts.len());
for fact in facts {
spans
.entry(fact.assignment_span)
.or_insert(AssignmentSyntaxSpans {
target: fact.target_span,
value: fact.value_span,
});
}
Self { spans }
}
#[must_use]
pub fn value_span(&self, assignment_span: Span) -> Option<Span> {
self.spans.get(&assignment_span).map(|spans| spans.value)
}
#[must_use]
pub fn target_span(&self, assignment_span: Span) -> Option<Span> {
self.spans.get(&assignment_span)?.target
}
#[must_use]
pub fn rendering<'a>(&self, assignment_span: Span, source_text: &'a str) -> Option<&'a str> {
let value_span = self.value_span(assignment_span)?;
render_assignment_syntax_span(value_span, assignment_span, source_text)
}
#[must_use]
pub fn target_rendering<'a>(&self, assignment_span: Span, source_text: &'a str) -> Option<&'a str> {
let target_span = self.target_span(assignment_span)?;
render_assignment_syntax_span(target_span, assignment_span, source_text)
}
}
fn render_assignment_syntax_span<'a>(
syntax_span: Span,
assignment_span: Span,
source_text: &'a str,
) -> Option<&'a str> {
if syntax_span.file != assignment_span.file
|| syntax_span.start < assignment_span.start
|| syntax_span.end > assignment_span.end
{
return None;
}
let start = usize::try_from(syntax_span.start).ok()?;
let end = usize::try_from(syntax_span.end).ok()?;
source_text
.get(start..end)
.map(str::trim)
.filter(|value| !value.is_empty())
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeclIndex {
pub file: FileId,
pub defs: Vec<Decl>,
pub refs: Vec<Ref>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub assignment_values: Vec<AssignmentValueFact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub call_receivers: Vec<CallReceiverFact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub call_argument_values: Vec<CallArgumentValueFact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub static_string_maps: Vec<StaticStringMapFact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub string_compositions: Vec<StringCompositionFact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub finite_literal_selections: Vec<FiniteLiteralSelectionFact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub character_substitutions: Vec<CharacterSubstitutionFact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub character_constraints: Vec<CharacterConstraintFact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub guarded_value_filters: Vec<GuardedValueFilterFact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub same_origin_path_constraints: Vec<SameOriginPathConstraintFact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub compiler_guards: Vec<CompilerGuardFact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dynamic_key_filters: Vec<DynamicKeyFilterFact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub runtime_type_narrowings: Vec<RuntimeTypeNarrowingFact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub branch_conditions: Vec<BranchConditionFact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub aggregate_layouts: Vec<AggregateLayout>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub strings: Vec<StringLiteral>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub comments: Vec<Comment>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Comment {
pub span: Span,
pub text: String,
pub kind: CommentKind,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CommentKind {
Todo,
Fixme,
Security,
Doc,
DisabledCode,
Generic,
}
impl CommentKind {
#[must_use]
pub fn classify(body: &str, is_doc: bool) -> Self {
let upper = body.trim_start().to_ascii_uppercase();
if upper.contains("SECURITY:")
|| upper.contains("CVE-")
|| upper.contains("XXX SECURITY")
|| upper.contains("VULN:")
|| upper.contains("VULNERAB")
|| upper.contains(" INJECTION")
|| upper.contains("TAINT")
|| upper.starts_with("SOURCE:")
|| upper.contains(" SOURCE:")
|| upper.starts_with("SINK:")
|| upper.contains(" SINK:")
|| upper.starts_with("SANITIZER:")
|| upper.contains(" SANITIZER:")
|| upper.starts_with("UNSANITIZED")
|| upper.contains(" UNSANITIZED")
{
return Self::Security;
}
if upper.starts_with("TODO") || upper.contains(" TODO:") || upper.contains(" TODO ") {
return Self::Todo;
}
if upper.starts_with("FIXME")
|| upper.starts_with("XXX")
|| upper.starts_with("HACK")
|| upper.starts_with("BUG")
|| upper.contains(" FIXME")
|| upper.contains(" XXX")
|| upper.contains(" HACK")
|| upper.contains(" BUG")
{
return Self::Fixme;
}
if is_doc {
return Self::Doc;
}
let trimmed = body.trim();
let has_terminator = trimmed.ends_with(';') || trimmed.ends_with('{') || trimmed.ends_with('}');
let has_assign_or_call = trimmed.contains('=') || (trimmed.contains('(') && trimmed.contains(')'));
if has_terminator && has_assign_or_call {
return Self::DisabledCode;
}
Self::Generic
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct StringLiteral {
pub span: Span,
pub text: String,
pub category: StringCategory,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub static_value: Option<String>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StringCategory {
Sql,
Url,
Shell,
Path,
Regex,
Format,
Generic,
}
impl StringCategory {
#[must_use]
pub fn classify(text: &str) -> Self {
let trimmed = text.trim_matches(|c: char| matches!(c, '"' | '\'' | '`')).trim();
let lower = trimmed.to_ascii_lowercase();
if lower.starts_with("select ")
|| lower.starts_with("insert ")
|| lower.starts_with("update ")
|| lower.starts_with("delete ")
|| lower.starts_with("create table")
|| lower.contains(" from ") && lower.contains("select ")
{
return Self::Sql;
}
if trimmed.starts_with("http://") || trimmed.starts_with("https://") || trimmed.starts_with("ws://") {
return Self::Url;
}
if trimmed.starts_with('/') || trimmed.starts_with("./") || trimmed.starts_with("../") {
return Self::Path;
}
if trimmed.starts_with('^') && trimmed.ends_with('$') {
return Self::Regex;
}
if trimmed.contains("{}") || trimmed.contains("%s") || trimmed.contains("%d") {
return Self::Format;
}
if trimmed.contains(" | ") || trimmed.starts_with("cmd ") || trimmed.starts_with("sh -") {
return Self::Shell;
}
Self::Generic
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ImportScope {
#[default]
Module,
Local,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ImportSpec {
pub span: Span,
pub module: String,
pub alias: Option<String>,
pub is_wildcard: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub original_name: Option<String>,
#[serde(default, skip_serializing_if = "ImportScope::is_module")]
pub scope: ImportScope,
}
impl ImportScope {
#[must_use]
pub fn is_module(&self) -> bool {
matches!(self, ImportScope::Module)
}
#[must_use]
pub fn is_local(&self) -> bool {
matches!(self, ImportScope::Local)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ImportIndex {
pub file: FileId,
pub imports: Vec<ImportSpec>,
}
impl CompilerBrowseHeader {
#[must_use]
pub fn from_indexes(declarations: Option<&DeclIndex>, imports: Option<&ImportIndex>) -> Self {
fn add(groups: &mut ahash::AHashMap<String, ahash::AHashSet<String>>, kind: &str, value: &str) {
let group = groups.entry(kind.to_string()).or_default();
let raw = value.trim().to_lowercase();
if !raw.is_empty() {
group.insert(raw);
}
group.extend(
value
.split(|character: char| {
!(character.is_ascii_alphanumeric()
|| matches!(character, '_' | '$' | '@' | ':' | '.'))
})
.filter(|part| !part.is_empty())
.map(str::to_lowercase),
);
}
fn collect_flow(
groups: &mut ahash::AHashMap<String, ahash::AHashSet<String>>,
events: &[FlowEvent],
enclosing: &str,
) {
for event in events {
match event {
FlowEvent::Call { name, args, .. } => {
add(groups, "call", name);
add(groups, "call", enclosing);
for argument in args {
add(groups, "arg", &argument.value_text);
add(groups, "arg", enclosing);
if let Some(name) = argument.name.as_deref() {
add(groups, "arg", name);
add(groups, "arg", &format!("{name}={}", argument.value_text));
}
if let Some(place) = argument.place.as_deref() {
add(groups, "arg", place);
}
for source in &argument.source_names {
add(groups, "arg", source);
}
}
}
FlowEvent::Assign {
target,
source_name,
source_call,
source_call_args,
source_names,
..
} => {
add(groups, "var", target);
add(groups, "var", enclosing);
let display_source = source_name
.as_deref()
.or(source_call.as_deref())
.or_else(|| source_names.first().map(String::as_str));
if let Some(source) = display_source {
add(groups, "var", source);
add(groups, "var", &format!("{target} = {source}"));
}
if let Some(call) = source_call {
add(groups, "call", call);
add(groups, "call", enclosing);
for argument in source_call_args {
add(groups, "arg", argument);
}
}
for source in source_names {
add(groups, "ref-read", source);
add(groups, "ref-read", enclosing);
}
}
FlowEvent::Branch {
then_events,
else_events,
..
} => {
collect_flow(groups, then_events, enclosing);
collect_flow(groups, else_events, enclosing);
}
FlowEvent::Loop { body, .. }
| FlowEvent::Defer { body, .. }
| FlowEvent::Using { body, .. } => collect_flow(groups, body, enclosing),
FlowEvent::Try {
body,
catch_events,
finally_events,
..
} => {
collect_flow(groups, body, enclosing);
collect_flow(groups, catch_events, enclosing);
collect_flow(groups, finally_events, enclosing);
}
_ => {}
}
}
}
fn enclosing_function(index: &DeclIndex, span: Span) -> Option<&str> {
index
.defs
.iter()
.filter(|declaration| {
matches!(
declaration.kind,
DeclKind::Function | DeclKind::Method | DeclKind::Constructor
) && declaration.span.start <= span.start
&& span.end <= declaration.span.end
})
.min_by_key(|declaration| declaration.span.end.saturating_sub(declaration.span.start))
.map(|declaration| declaration.name.as_str())
}
let mut groups = ahash::AHashMap::<String, ahash::AHashSet<String>>::default();
if let Some(index) = declarations {
for declaration in &index.defs {
let kind = format!("{:?}", declaration.kind).to_lowercase();
add(&mut groups, &kind, &declaration.name);
if let Some(qualified) = declaration.qualified_name.as_deref() {
add(&mut groups, &kind, qualified);
}
for parameter in &declaration.params {
add(&mut groups, &kind, parameter);
}
for operation in operations_from_flow_events(&declaration.flow_events) {
add(&mut groups, "operation", operation.kind.as_str());
add(&mut groups, "operation", &declaration.name);
if let Some(target) = operation.target.as_deref() {
add(&mut groups, "operation", target);
}
if let Some(detail) = operation.detail.as_deref() {
add(&mut groups, "operation", detail);
}
for operand in operation.operands {
add(&mut groups, "operation", &operand.name);
add(&mut groups, "operation", operand.role.as_str());
add(
&mut groups,
"operation",
&format!("{}:{}", operand.role.as_str(), operand.name),
);
}
}
collect_flow(&mut groups, &declaration.flow_events, &declaration.name);
}
for reference in &index.refs {
let kind = match reference.kind {
RefKind::Read => "ref-read",
RefKind::Write => "ref-write",
RefKind::Call => "ref-call",
RefKind::Decorator => "ref-decorator",
_ => "ref",
};
add(&mut groups, kind, &reference.name);
}
for string in &index.strings {
add(&mut groups, "string", &string.text);
add(
&mut groups,
"string",
&format!("{:?}", string.category).to_lowercase(),
);
if let Some(function) = enclosing_function(index, string.span) {
add(&mut groups, "string", function);
}
}
for comment in &index.comments {
add(&mut groups, "comment", &comment.text);
add(
&mut groups,
"comment",
&format!("{:?}", comment.kind).to_lowercase(),
);
if let Some(function) = enclosing_function(index, comment.span) {
add(&mut groups, "comment", function);
}
}
}
if let Some(index) = imports {
for import in &index.imports {
let kind = if import.alias.is_some() {
"import-alias"
} else {
"import"
};
add(&mut groups, kind, &import.module);
if let Some(alias) = import.alias.as_deref() {
add(&mut groups, kind, alias);
}
if let Some(original) = import.original_name.as_deref() {
add(&mut groups, kind, original);
}
}
}
let mut groups = groups
.into_iter()
.map(|(kind, terms)| {
let mut terms = terms.into_iter().collect::<Vec<_>>();
terms.sort_unstable();
CompilerBrowseTermGroup { kind, terms }
})
.collect::<Vec<_>>();
groups.sort_unstable_by(|left, right| left.kind.cmp(&right.kind));
Self { groups }
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnsupportedConstruct {
pub span: Span,
pub note: String,
pub precision: Precision,
}
#[cfg(test)]
mod compiler_attribution_tests {
use super::*;
fn span(start: u64, end: u64) -> Span {
Span::new(FileId::new(4), start, end)
}
fn decl(kind: DeclKind, declaration_span: Span, flow_events: Vec<FlowEvent>) -> Decl {
Decl {
symbol: SymbolId::new(u32::try_from(declaration_span.start).expect("test symbol")),
kind,
name: "fixture".to_string(),
qualified_name: None,
module_path: ModulePath::default(),
span: declaration_span,
name_span: declaration_span,
visibility: Visibility::Private,
parent: None,
body_span: Some(declaration_span),
flow_events,
has_implicit_returns: false,
params: Vec::new(),
param_annotations: Vec::new(),
param_default_calls: Vec::new(),
type_aliases: Vec::new(),
bases: Vec::new(),
receiver_param_index: None,
receiver_field_writes: Vec::new(),
receiver_field_initializers: Vec::new(),
implicit_receiver_names: Vec::new(),
receiver_state_sources: Vec::new(),
return_type: None,
is_variadic: false,
}
}
#[test]
fn compiler_attribution_projects_nested_adapter_ir_without_text_tokenization() {
let direct_call = span(10, 18);
let assignment_call = span(30, 42);
let function_span = span(1, 80);
let index = DeclIndex {
file: FileId::new(4),
defs: vec![
decl(
DeclKind::Function,
function_span,
vec![
FlowEvent::Call {
span: direct_call,
name: "send".to_string(),
receiver: Some("repo.client".to_string()),
receiver_types: vec!["Repository".to_string()],
call_kind: CallKind::Method,
args: vec![CallArg {
span: span(15, 17),
passing_mode: ArgumentPassingMode::Value,
name: None,
value_text: "payload".to_string(),
place: Some("payload".to_string()),
source_names: vec!["payload".to_string()],
}],
},
FlowEvent::Branch {
span: span(19, 29),
condition: None,
then_events: vec![FlowEvent::Return {
span: span(21, 27),
value_kind: Some(AssignValueKind::Compound),
value_text: Some("payload".to_string()),
value_name: Some("payload".to_string()),
value_flow: ExpressionFlow::from_place("payload"),
}],
else_events: Vec::new(),
},
FlowEvent::Assign {
span: assignment_call,
target: "saved".to_string(),
source_name: Some("payload".to_string()),
source_call: Some("transform".to_string()),
source_call_args: vec!["payload".to_string(), "x + y".to_string()],
source_names: vec!["fallback".to_string()],
declares_new_binding: true,
value_kind: Some(AssignValueKind::CallResult),
},
],
),
decl(
DeclKind::Class,
span(90, 120),
vec![FlowEvent::Call {
span: span(100, 110),
name: "ignored".to_string(),
receiver: None,
receiver_types: Vec::new(),
call_kind: CallKind::Function,
args: Vec::new(),
}],
),
],
call_receivers: vec![CallReceiverFact {
call_span: direct_call,
receiver_span: span(10, 14),
value_flow: ExpressionFlow {
place: Some("repo.client".to_string()),
source_names: vec!["repo".to_string()],
aggregate_fields: vec![ExpressionField {
name: "nested".to_string(),
value_span: None,
value: ExpressionFlow::from_place("nested.value"),
}],
..ExpressionFlow::default()
},
role: CallReceiverRole::Value,
static_value: None,
}],
..DeclIndex::default()
};
let projected = CompilerAttribution::from_decl_index(&index);
assert_eq!(projected.file, FileId::new(4));
assert_eq!(projected.functions.len(), 1);
let function = projected
.function_at_span(function_span)
.expect("function projection");
assert_eq!(function.calls.len(), 2);
assert_eq!(function.calls[0].name, "send");
assert_eq!(
function.calls[0].receiver_source_names,
["nested.value", "repo", "repo.client"]
);
assert_eq!(function.return_spans, [span(21, 27)]);
assert_eq!(function.writes.len(), 1);
assert_eq!(function.writes[0].source_names, ["payload", "fallback"]);
assert_eq!(function.calls[1].name, "transform");
assert_eq!(function.calls[1].args[0].place, None);
assert!(
function.calls[1].args[1].source_names.is_empty(),
"rendered compound text must not be split into invented carriers"
);
assert!(projected.function_at_span(span(90, 120)).is_none());
}
}
#[cfg(test)]
mod operation_tests {
use super::*;
fn span(start: u64) -> Span {
Span::new(FileId::new(0), start, start + 1)
}
fn kinds(ops: &[Operation]) -> Vec<OperationKind> {
ops.iter().map(|op| op.kind).collect()
}
#[test]
fn comment_security_classification_covers_review_markers() {
for text in [
"source: user input",
"sink: SQL injection",
"flows to command injection",
"VULN: insecure deserialization",
"unsanitized request parameter",
] {
assert_eq!(
CommentKind::classify(text, false),
CommentKind::Security,
"{text}"
);
}
}
#[test]
fn comment_security_classification_does_not_match_generic_source_word() {
assert_eq!(
CommentKind::classify("source file generated by build", false),
CommentKind::Generic
);
}
#[test]
fn operations_capture_assignment_reads_writes_and_place_shapes() {
let ops = operations_from_flow_events(&[FlowEvent::Assign {
span: span(10),
target: "user.name".to_string(),
source_name: Some("payload[0]".to_string()),
source_call: None,
source_call_args: Vec::new(),
source_names: vec!["request.body".to_string()],
declares_new_binding: false,
value_kind: Some(AssignValueKind::Compound),
}]);
assert!(ops.iter().any(|op| {
op.kind == OperationKind::Write
&& op.target.as_deref() == Some("user.name")
&& op
.operands
.iter()
.any(|operand| operand.name == "payload[0]" && operand.role == OperationOperandRole::Read)
}));
assert!(ops
.iter()
.any(|op| op.kind == OperationKind::Read && op.target.as_deref() == Some("request.body")));
assert!(ops
.iter()
.any(|op| op.kind == OperationKind::FieldAccess && op.target.as_deref() == Some("user.name")));
assert!(ops
.iter()
.any(|op| op.kind == OperationKind::Index && op.target.as_deref() == Some("payload[0]")));
}
#[test]
fn operations_capture_calls_arguments_and_allocations() {
let ops = operations_from_flow_events(&[FlowEvent::Call {
span: span(20),
name: "Widget".to_string(),
receiver: Some("factory".to_string()),
receiver_types: Vec::new(),
call_kind: CallKind::Constructor,
args: vec![CallArg {
passing_mode: Default::default(),
span: span(21),
name: None,
value_text: "config".to_string(),
place: Some("opts.value".to_string()),
source_names: vec!["config".to_string()],
}],
}]);
assert!(ops.iter().any(|op| {
op.kind == OperationKind::Call
&& op.target.as_deref() == Some("Widget")
&& op.detail.as_deref() == Some("constructor")
}));
assert!(ops
.iter()
.any(|op| op.kind == OperationKind::Allocate && op.target.as_deref() == Some("Widget")));
assert!(ops
.iter()
.any(|op| op.kind == OperationKind::Read && op.target.as_deref() == Some("config")));
assert!(ops
.iter()
.any(|op| op.kind == OperationKind::FieldAccess && op.target.as_deref() == Some("opts.value")));
}
#[test]
fn operations_recurse_through_structured_flow_and_lifecycle() {
let ops = operations_from_flow_events(&[FlowEvent::Branch {
span: span(30),
condition: Some("allowed".to_string()),
then_events: vec![FlowEvent::Try {
span: span(31),
body: vec![FlowEvent::Lifecycle {
span: span(32),
name: "fd".to_string(),
transition: "closed".to_string(),
}],
catch_events: vec![FlowEvent::Throw {
span: span(33),
value_name: Some("err".to_string()),
thrown_type: Some("Error".to_string()),
}],
finally_events: vec![FlowEvent::Return {
span: span(34),
value_kind: Some(AssignValueKind::Compound),
value_text: None,
value_name: Some("result".to_string()),
value_flow: ExpressionFlow::from_place("result"),
}],
catch_param: Some("err".to_string()),
catch_types: Vec::new(),
}],
else_events: Vec::new(),
}]);
let observed = kinds(&ops);
for expected in [
OperationKind::BranchCondition,
OperationKind::Release,
OperationKind::CatchBinding,
OperationKind::Throw,
OperationKind::Return,
] {
assert!(
observed.contains(&expected),
"missing {expected:?} in {observed:?}"
);
}
}
#[test]
fn operations_capture_yield_value_reads_conservatively() {
let ops = operations_from_flow_events(&[
FlowEvent::Yield {
span: span(40),
value_text: Some("payload[0]".to_string()),
value_flow: ExpressionFlow::from_place("payload[0]"),
},
FlowEvent::Yield {
span: span(50),
value_text: Some("left + right".to_string()),
value_flow: ExpressionFlow::from_source_names(vec!["left".to_string(), "right".to_string()]),
},
]);
assert!(ops
.iter()
.any(|op| { op.kind == OperationKind::Yield && op.target.as_deref() == Some("payload[0]") }));
assert!(ops.iter().any(|op| {
op.kind == OperationKind::Read
&& op.target.as_deref() == Some("payload[0]")
&& op.detail.as_deref() == Some("yield_value")
}));
assert!(ops
.iter()
.any(|op| { op.kind == OperationKind::Index && op.target.as_deref() == Some("payload[0]") }));
for operand in ["left", "right"] {
assert!(ops.iter().any(|op| {
op.kind == OperationKind::Read
&& op.target.as_deref() == Some(operand)
&& op.detail.as_deref() == Some("yield_value")
}));
}
}
}