use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum DependencyKind {
Data,
Control,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DfgNode {
pub id: String,
pub name: String,
pub file: String,
pub line: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DfgEdge {
pub from: String,
pub to: String,
pub kind: DependencyKind,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DataFlowGraph {
pub nodes: Vec<DfgNode>,
pub edges: Vec<DfgEdge>,
}
pub trait DfgBuilder {
fn build(path: &str, source: &str) -> DataFlowGraph;
}
pub struct RustDfgBuilder;
fn parse_ruby_param_name(raw: &str) -> Option<String> {
let candidate = raw
.trim()
.split('=')
.next()
.unwrap_or("")
.trim()
.trim_start_matches(['*', '&'])
.split(':')
.next()
.unwrap_or("")
.trim();
let mut chars = candidate.chars();
let first = chars.next()?;
if !(first.is_ascii_alphabetic() || first == '_') {
return None;
}
if !chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_') {
return None;
}
Some(candidate.to_string())
}
fn strip_ruby_inline_comment(raw: &str) -> String {
let mut out = String::with_capacity(raw.len());
let mut in_single = false;
let mut in_double = false;
let mut escaped = false;
for ch in raw.chars() {
if escaped {
out.push(ch);
escaped = false;
continue;
}
if (in_single || in_double) && ch == '\\' {
out.push(ch);
escaped = true;
continue;
}
if !in_double && ch == '\'' {
in_single = !in_single;
out.push(ch);
continue;
}
if !in_single && ch == '"' {
in_double = !in_double;
out.push(ch);
continue;
}
if !in_single && !in_double && ch == '#' {
break;
}
out.push(ch);
}
out.trim().to_string()
}
impl DfgBuilder for RustDfgBuilder {
fn build(path: &str, source: &str) -> DataFlowGraph {
use std::collections::{HashMap, HashSet};
let mut nodes: Vec<DfgNode> = Vec::new();
let mut edges: Vec<DfgEdge> = Vec::new();
let mut def_records_by_name: HashMap<String, Vec<(u32, String)>> = HashMap::new();
let mut param_def_records_by_name: HashMap<String, Vec<(u32, String)>> = HashMap::new();
let mut def_lines_by_name: HashMap<String, HashSet<u32>> = HashMap::new();
let mut seen_node_ids: HashSet<String> = HashSet::new();
let line_offsets = crate::languages::util::line_offsets(source);
let mut ignored_use_byte_ranges: Vec<(usize, usize)> = Vec::new();
{
let mut parser = tree_sitter::Parser::new();
let lang: tree_sitter::Language = tree_sitter_rust::LANGUAGE.into();
parser.set_language(&lang).expect("set language");
if let Some(tree) = parser.parse(source, None) {
let mut cursor = tree.root_node().walk();
let mut stack = vec![tree.root_node()];
while let Some(node) = stack.pop() {
for child in node.named_children(&mut cursor) {
stack.push(child);
}
if node.kind() == "function_item" {
if let Some(body) = node.child_by_field_name("body") {
ignored_use_byte_ranges.push((node.start_byte(), body.start_byte()));
}
if let Some(params_node) = node.child_by_field_name("parameters") {
for param in params_node.named_children(&mut cursor) {
if param.kind() == "parameter"
&& let Some(pat) = param.child_by_field_name("pattern")
{
let name = pat.utf8_text(source.as_bytes()).unwrap_or("");
if !name.is_empty() {
let sl = crate::languages::util::byte_to_line(
&line_offsets,
pat.start_byte(),
);
let node_id = format!("{}:def:{}:{}", path, name, sl);
if seen_node_ids.insert(node_id.clone()) {
nodes.push(DfgNode {
id: node_id.clone(),
name: name.to_string(),
file: path.to_string(),
line: sl,
});
def_records_by_name
.entry(name.to_string())
.or_default()
.push((sl, node_id.clone()));
param_def_records_by_name
.entry(name.to_string())
.or_default()
.push((sl, node_id.clone()));
}
}
}
}
}
}
if node.kind() == "assignment_expression"
&& let Some(lhs) = node.child_by_field_name("left")
&& lhs.kind() == "identifier"
{
let name = lhs.utf8_text(source.as_bytes()).unwrap_or("");
if !name.is_empty() {
let sl = crate::languages::util::byte_to_line(
&line_offsets,
lhs.start_byte(),
);
let node_id = format!("{}:def:{}:{}", path, name, sl);
if seen_node_ids.insert(node_id.clone()) {
nodes.push(DfgNode {
id: node_id.clone(),
name: name.to_string(),
file: path.to_string(),
line: sl,
});
def_records_by_name
.entry(name.to_string())
.or_default()
.push((sl, node_id.clone()));
}
def_lines_by_name
.entry(name.to_string())
.or_default()
.insert(sl);
}
}
}
}
}
let reserved = [
"let", "mut", "fn", "pub", "self", "super", "crate", "if", "else", "match", "for",
"while", "loop", "return", "use", "struct", "enum", "trait", "impl", "mod", "as", "in",
"true", "false",
];
let re_let_def =
regex::Regex::new(r"^\s*let(?:\s+mut)?\s+([A-Za-z_][A-Za-z0-9_]*)\b").unwrap();
for (idx, line) in source.lines().enumerate() {
let line_no = (idx + 1) as u32;
let Some(cap) = re_let_def.captures(line) else {
continue;
};
let Some(name) = cap.get(1).map(|m| m.as_str()) else {
continue;
};
let node_id = format!("{}:def:{}:{}", path, name, line_no);
if seen_node_ids.insert(node_id.clone()) {
nodes.push(DfgNode {
id: node_id.clone(),
name: name.to_string(),
file: path.to_string(),
line: line_no,
});
def_records_by_name
.entry(name.to_string())
.or_default()
.push((line_no, node_id.clone()));
}
def_lines_by_name
.entry(name.to_string())
.or_default()
.insert(line_no);
}
for records in def_records_by_name.values_mut() {
records.sort_by_key(|(line, _)| *line);
}
let control_ranges = collect_control_ranges_rust(source);
let re_alias_let = regex::Regex::new(
r"^\s*let(?:\s+mut)?\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s*:[^=]+)?\s*=\s*([A-Za-z_][A-Za-z0-9_]*)\b",
)
.unwrap();
let re_alias_assign =
regex::Regex::new(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([A-Za-z_][A-Za-z0-9_]*)\b")
.unwrap();
for (idx, line) in source.lines().enumerate() {
let line_no = (idx + 1) as u32;
let pair = if let Some(cap) = re_alias_let.captures(line) {
Some((
cap.get(1).unwrap().as_str().to_string(),
cap.get(2).unwrap().as_str().to_string(),
))
} else {
re_alias_assign.captures(line).map(|cap| {
(
cap.get(1).unwrap().as_str().to_string(),
cap.get(2).unwrap().as_str().to_string(),
)
})
};
let Some((lhs, rhs)) = pair else {
continue;
};
if lhs == rhs {
continue;
}
let Some(lhs_def_id) = def_records_by_name.get(&lhs).and_then(|recs| {
recs.iter()
.rev()
.find(|(ln, _)| *ln == line_no)
.map(|(_, id)| id.clone())
}) else {
continue;
};
let rhs_defs =
reaching_def_ids_ssa_like(&rhs, line_no, &def_records_by_name, &control_ranges);
for rhs_def_id in rhs_defs {
edges.push(DfgEdge {
from: rhs_def_id,
to: lhs_def_id.clone(),
kind: DependencyKind::Data,
});
}
}
let token_re = regex::Regex::new(r"[A-Za-z_][A-Za-z0-9_]*").unwrap();
for m in token_re.find_iter(source) {
let token = m.as_str();
if reserved.contains(&token) {
continue;
}
if ignored_use_byte_ranges
.iter()
.any(|(start, end)| m.start() >= *start && m.start() < *end)
{
continue;
}
let line_no = crate::languages::util::byte_to_line(&line_offsets, m.start());
if def_lines_by_name
.get(token)
.is_some_and(|lines| lines.contains(&line_no))
{
continue;
}
let reaching = reaching_def_ids_ssa_like_with_same_line_params(
token,
line_no,
&def_records_by_name,
¶m_def_records_by_name,
&control_ranges,
);
if !reaching.is_empty() {
let node_id = format!("{}:use:{}:{}", path, token, line_no);
if seen_node_ids.insert(node_id.clone()) {
nodes.push(DfgNode {
id: node_id.clone(),
name: token.to_string(),
file: path.to_string(),
line: line_no,
});
}
for def_id in reaching {
edges.push(DfgEdge {
from: def_id,
to: node_id.clone(),
kind: DependencyKind::Data,
});
}
}
}
let data_node_count = nodes.len();
for (start_ln, end_ln) in &control_ranges {
let ctrl_id = format!("{}:ctrl:{}:{}", path, start_ln, end_ln);
if seen_node_ids.insert(ctrl_id.clone()) {
nodes.push(DfgNode {
id: ctrl_id.clone(),
name: "control".to_string(),
file: path.to_string(),
line: *start_ln,
});
}
for nd in &nodes[..data_node_count] {
if nd.line >= *start_ln && nd.line <= *end_ln {
edges.push(DfgEdge {
from: ctrl_id.clone(),
to: nd.id.clone(),
kind: DependencyKind::Control,
});
}
}
}
DataFlowGraph { nodes, edges }
}
}
pub struct RubyDfgBuilder;
impl DfgBuilder for RubyDfgBuilder {
fn build(path: &str, source: &str) -> DataFlowGraph {
use regex::Regex;
use std::collections::{HashMap, HashSet};
let mut nodes: Vec<DfgNode> = Vec::new();
let mut edges: Vec<DfgEdge> = Vec::new();
let mut def_records_by_name: HashMap<String, Vec<(u32, String)>> = HashMap::new();
let mut def_lines_by_name: HashMap<String, HashSet<u32>> = HashMap::new();
let mut seen_node_ids: HashSet<String> = HashSet::new();
let reserved = [
"if", "else", "elsif", "end", "return", "def", "class", "module",
];
let fn_re =
Regex::new(r"^\s*def\s+[^\s(]+\s*(?:\(([^)]*)\)|\s+([*&A-Za-z_][^#\n;]*))?$").unwrap();
for (idx, line) in source.lines().enumerate() {
let line_no = (idx + 1) as u32;
if let Some(cap) = fn_re.captures(line) {
let params = cap
.get(1)
.or_else(|| cap.get(2))
.map(|m| strip_ruby_inline_comment(m.as_str()))
.unwrap_or_default();
for p in params.split(',') {
let Some(name) = parse_ruby_param_name(p) else {
continue;
};
if !name.is_empty() {
let node_id = format!("{}:def:{}:{}", path, name, line_no);
if seen_node_ids.insert(node_id.clone()) {
nodes.push(DfgNode {
id: node_id.clone(),
name: name.clone(),
file: path.to_string(),
line: line_no,
});
def_records_by_name
.entry(name.clone())
.or_default()
.push((line_no, node_id));
}
def_lines_by_name.entry(name).or_default().insert(line_no);
}
}
}
}
for records in def_records_by_name.values_mut() {
records.sort_by_key(|(line, _)| *line);
}
let control_ranges = collect_control_ranges_ruby(source);
let assign_re =
Regex::new(r"^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*([a-zA-Z_][a-zA-Z0-9_]*)").unwrap();
for (idx, line) in source.lines().enumerate() {
let line_no = (idx + 1) as u32;
if let Some(cap) = assign_re.captures(line) {
let lhs = cap.get(1).unwrap().as_str();
let rhs = cap.get(2).unwrap().as_str();
let reaching_rhs =
reaching_def_ids_ssa_like(rhs, line_no, &def_records_by_name, &control_ranges);
if !reaching_rhs.is_empty() {
let use_id = format!("{}:use:{}:{}", path, rhs, line_no);
if seen_node_ids.insert(use_id.clone()) {
nodes.push(DfgNode {
id: use_id.clone(),
name: rhs.to_string(),
file: path.to_string(),
line: line_no,
});
}
for def_id in &reaching_rhs {
edges.push(DfgEdge {
from: def_id.clone(),
to: use_id.clone(),
kind: DependencyKind::Data,
});
}
}
let def_id = format!("{}:def:{}:{}", path, lhs, line_no);
if seen_node_ids.insert(def_id.clone()) {
nodes.push(DfgNode {
id: def_id.clone(),
name: lhs.to_string(),
file: path.to_string(),
line: line_no,
});
def_records_by_name
.entry(lhs.to_string())
.or_default()
.push((line_no, def_id.clone()));
}
def_lines_by_name
.entry(lhs.to_string())
.or_default()
.insert(line_no);
if lhs != rhs {
for rhs_def_id in &reaching_rhs {
edges.push(DfgEdge {
from: rhs_def_id.clone(),
to: def_id.clone(),
kind: DependencyKind::Data,
});
}
}
}
}
for records in def_records_by_name.values_mut() {
records.sort_by_key(|(line, _)| *line);
}
let return_re = Regex::new(r"return\s+([a-zA-Z_][a-zA-Z0-9_]*)").unwrap();
for (idx, line) in source.lines().enumerate() {
let line_no = (idx + 1) as u32;
if let Some(cap) = return_re.captures(line) {
let name = cap.get(1).unwrap().as_str();
if def_lines_by_name
.get(name)
.is_some_and(|s| s.contains(&line_no))
{
continue;
}
let node_id = format!("{}:use:{}:{}", path, name, line_no);
if seen_node_ids.insert(node_id.clone()) {
nodes.push(DfgNode {
id: node_id.clone(),
name: name.to_string(),
file: path.to_string(),
line: line_no,
});
}
let reaching =
reaching_def_ids_ssa_like(name, line_no, &def_records_by_name, &control_ranges);
for def_id in reaching {
edges.push(DfgEdge {
from: def_id,
to: node_id.clone(),
kind: DependencyKind::Data,
});
}
}
}
for (idx, line) in source.lines().enumerate() {
let line_no = (idx + 1) as u32;
for token in line.split(|c: char| !c.is_alphanumeric() && c != '_') {
if token.is_empty() || reserved.contains(&token) {
continue;
}
if def_lines_by_name
.get(token)
.is_some_and(|s| s.contains(&line_no))
{
continue;
}
let reaching = reaching_def_ids_ssa_like(
token,
line_no,
&def_records_by_name,
&control_ranges,
);
if reaching.is_empty() {
continue;
}
let use_id = format!("{}:use:{}:{}", path, token, line_no);
if seen_node_ids.insert(use_id.clone()) {
nodes.push(DfgNode {
id: use_id.clone(),
name: token.to_string(),
file: path.to_string(),
line: line_no,
});
}
for def_id in reaching {
edges.push(DfgEdge {
from: def_id,
to: use_id.clone(),
kind: DependencyKind::Data,
});
}
}
}
let data_count = nodes.len();
for (start_ln, end_ln) in &control_ranges {
let ctrl_id = format!("{}:ctrl:{}:{}", path, start_ln, end_ln);
if seen_node_ids.insert(ctrl_id.clone()) {
nodes.push(DfgNode {
id: ctrl_id.clone(),
name: "control".to_string(),
file: path.to_string(),
line: *start_ln,
});
}
for nd in &nodes[..data_count] {
if nd.line >= *start_ln && nd.line <= *end_ln {
edges.push(DfgEdge {
from: ctrl_id.clone(),
to: nd.id.clone(),
kind: DependencyKind::Control,
});
}
}
}
DataFlowGraph { nodes, edges }
}
}
use crate::ir::reference::Reference;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FunctionInputImpact {
pub input_node_id: String,
pub impacted_node_ids: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FunctionSummary {
pub function_id: String,
pub file: String,
pub start_line: u32,
pub end_line: u32,
pub inputs: Vec<String>,
pub flows: Vec<FunctionInputImpact>,
}
pub struct PdgBuilder;
fn push_unique_edge(
pdg: &mut DataFlowGraph,
seen_edges: &mut std::collections::HashSet<(String, String, DependencyKind)>,
from: String,
to: String,
kind: DependencyKind,
) {
if seen_edges.insert((from.clone(), to.clone(), kind.clone())) {
pdg.edges.push(DfgEdge { from, to, kind });
}
}
fn collect_nested_call_refs<'a>(
outer_callee_id: &str,
impacted_node_ids: &[String],
refs: &'a [Reference],
node_loc_by_id: &std::collections::HashMap<String, (String, u32)>,
) -> Vec<&'a Reference> {
let impacted_locs: std::collections::HashSet<(String, u32)> = impacted_node_ids
.iter()
.filter_map(|id| node_loc_by_id.get(id).cloned())
.collect();
if impacted_locs.is_empty() {
return Vec::new();
}
refs.iter()
.filter(|nested| {
nested.kind == crate::ir::reference::RefKind::Call
&& nested.provenance == crate::ir::reference::EdgeProvenance::CallGraph
&& nested.from.0 == outer_callee_id
&& impacted_locs.contains(&(nested.file.clone(), nested.line))
})
.collect()
}
fn has_nested_completion_candidate(
outer_callee_id: &str,
impacted_node_ids: &[String],
refs: &[Reference],
node_loc_by_id: &std::collections::HashMap<String, (String, u32)>,
) -> bool {
!collect_nested_call_refs(outer_callee_id, impacted_node_ids, refs, node_loc_by_id).is_empty()
}
#[allow(clippy::too_many_arguments)]
fn push_nested_completion_bridges(
pdg: &mut DataFlowGraph,
seen_edges: &mut std::collections::HashSet<(String, String, DependencyKind)>,
outer_callee_id: &str,
impacted_node_ids: &[String],
callsite_defs: &[String],
refs: &[Reference],
summary_by_fn: &std::collections::HashMap<String, FunctionSummary>,
node_loc_by_id: &std::collections::HashMap<String, (String, u32)>,
uses_by_loc: &std::collections::HashMap<(String, u32), Vec<String>>,
remaining_hops: usize,
) -> bool {
if callsite_defs.is_empty() || remaining_hops == 0 {
return false;
}
let impacted_set: std::collections::HashSet<&str> =
impacted_node_ids.iter().map(String::as_str).collect();
let mut propagated_any = false;
for nested_ref in
collect_nested_call_refs(outer_callee_id, impacted_node_ids, refs, node_loc_by_id)
{
let Some(nested_summary) = summary_by_fn.get(&nested_ref.to.0) else {
continue;
};
let nested_callsite_uses = collect_callsite_uses(
uses_by_loc,
nested_ref.file.as_str(),
nested_ref.line,
nested_summary.inputs.len(),
);
let mut relevant_bindings: Vec<(String, String)> = if nested_callsite_uses.len()
== nested_summary.inputs.len()
&& !nested_callsite_uses.is_empty()
{
nested_summary
.inputs
.iter()
.zip(nested_callsite_uses.iter())
.filter(|(_, use_id)| impacted_set.contains(use_id.as_str()))
.map(|(input, use_id)| (use_id.clone(), input.clone()))
.collect()
} else {
Vec::new()
};
if relevant_bindings.is_empty() && nested_summary.inputs.len() == 1 {
let binding_sources: Vec<String> = if nested_callsite_uses.len() == 1 {
vec![nested_callsite_uses[0].clone()]
} else {
impacted_node_ids
.iter()
.filter(|impacted| {
node_loc_by_id
.get(impacted.as_str())
.is_some_and(|(file, line)| {
*file == nested_ref.file && *line == nested_ref.line
})
})
.cloned()
.collect()
};
let nested_input = nested_summary.inputs[0].clone();
relevant_bindings = binding_sources
.into_iter()
.map(|binding_source| (binding_source, nested_input.clone()))
.collect();
}
for (binding_source, nested_input) in relevant_bindings {
push_unique_edge(
pdg,
seen_edges,
binding_source,
nested_input.clone(),
DependencyKind::Data,
);
for nested_flow in nested_summary.flows.iter().filter(|flow| {
flow.input_node_id == nested_input && !flow.impacted_node_ids.is_empty()
}) {
propagated_any = true;
for nested_impacted in &nested_flow.impacted_node_ids {
for d in callsite_defs {
push_unique_edge(
pdg,
seen_edges,
nested_impacted.clone(),
d.clone(),
DependencyKind::Data,
);
}
}
propagated_any |= push_nested_completion_bridges(
pdg,
seen_edges,
nested_ref.to.0.as_str(),
&nested_flow.impacted_node_ids,
callsite_defs,
refs,
summary_by_fn,
node_loc_by_id,
uses_by_loc,
remaining_hops.saturating_sub(1),
);
}
}
}
propagated_any
}
fn collect_callsite_nodes(
nodes_by_loc: &std::collections::HashMap<(String, u32), Vec<String>>,
file: &str,
line: u32,
expected_len: usize,
preferred_offsets: &[i32],
) -> Vec<String> {
let mut collected = Vec::new();
for offset in preferred_offsets {
let candidate_line = if *offset < 0 {
let delta = offset.unsigned_abs();
if delta > line {
continue;
}
line - delta
} else {
line.saturating_add(*offset as u32)
};
let key = (file.to_string(), candidate_line);
let Some(node_ids) = nodes_by_loc.get(&key) else {
continue;
};
collected.extend(node_ids.iter().cloned());
if expected_len > 0 && collected.len() >= expected_len {
break;
}
}
collected
}
fn collect_exact_callsite_uses(
uses_by_loc: &std::collections::HashMap<(String, u32), Vec<String>>,
file: &str,
line: u32,
expected_len: usize,
) -> Vec<String> {
collect_callsite_nodes(uses_by_loc, file, line, expected_len, &[0])
}
fn collect_callsite_uses(
uses_by_loc: &std::collections::HashMap<(String, u32), Vec<String>>,
file: &str,
line: u32,
expected_len: usize,
) -> Vec<String> {
let exact = collect_exact_callsite_uses(uses_by_loc, file, line, expected_len);
if expected_len == 0 || exact.len() >= expected_len {
return exact;
}
collect_callsite_nodes(
uses_by_loc,
file,
line,
expected_len,
&[0, 1, -1, 2, -2, 3, -3],
)
}
fn collect_exact_callsite_defs(
defs_by_loc: &std::collections::HashMap<(String, u32), Vec<String>>,
file: &str,
line: u32,
) -> Vec<String> {
collect_callsite_nodes(defs_by_loc, file, line, 0, &[0])
}
fn collect_callsite_defs(
defs_by_loc: &std::collections::HashMap<(String, u32), Vec<String>>,
file: &str,
line: u32,
) -> Vec<String> {
let exact = collect_exact_callsite_defs(defs_by_loc, file, line);
if !exact.is_empty() {
return exact;
}
collect_callsite_nodes(defs_by_loc, file, line, 0, &[0, -1, 1, -2, 2])
}
fn filter_symbolic_propagation_refs<'a>(
refs: &'a [Reference],
defs_by_loc: &std::collections::HashMap<(String, u32), Vec<String>>,
uses_by_loc: &std::collections::HashMap<(String, u32), Vec<String>>,
summary_by_fn: &std::collections::HashMap<String, FunctionSummary>,
) -> Vec<&'a Reference> {
let mut grouped: std::collections::BTreeMap<(String, String, String), Vec<&Reference>> =
std::collections::BTreeMap::new();
for reference in refs.iter().filter(|r| {
r.kind == crate::ir::reference::RefKind::Call
&& r.provenance == crate::ir::reference::EdgeProvenance::CallGraph
}) {
grouped
.entry((
reference.from.0.clone(),
reference.to.0.clone(),
reference.file.clone(),
))
.or_default()
.push(reference);
}
let mut selected = Vec::new();
for group in grouped.into_values() {
let summary_input_count = group
.first()
.and_then(|reference| summary_by_fn.get(&reference.to.0))
.map(|summary| summary.inputs.len())
.unwrap_or_default()
.max(1);
let with_exact_defs_and_uses: Vec<&Reference> = group
.iter()
.copied()
.filter(|reference| {
let exact_defs = collect_exact_callsite_defs(
defs_by_loc,
reference.file.as_str(),
reference.line,
);
let exact_uses = collect_exact_callsite_uses(
uses_by_loc,
reference.file.as_str(),
reference.line,
summary_input_count,
);
!exact_defs.is_empty()
&& !exact_uses.is_empty()
&& exact_uses.len() >= summary_input_count
})
.collect();
if !with_exact_defs_and_uses.is_empty() {
selected.extend(with_exact_defs_and_uses);
continue;
}
let with_exact_defs: Vec<&Reference> = group
.iter()
.copied()
.filter(|reference| {
!collect_exact_callsite_defs(defs_by_loc, reference.file.as_str(), reference.line)
.is_empty()
})
.collect();
if !with_exact_defs.is_empty() {
selected.extend(with_exact_defs);
continue;
}
let with_exact_uses: Vec<&Reference> = group
.iter()
.copied()
.filter(|reference| {
let exact_uses = collect_exact_callsite_uses(
uses_by_loc,
reference.file.as_str(),
reference.line,
summary_input_count,
);
!exact_uses.is_empty() && exact_uses.len() >= summary_input_count
})
.collect();
if !with_exact_uses.is_empty() {
selected.extend(with_exact_uses);
continue;
}
selected.extend(group);
}
selected
}
impl PdgBuilder {
pub fn build(dfg: &DataFlowGraph, refs: &[Reference]) -> DataFlowGraph {
let mut pdg = dfg.clone();
for r in refs {
pdg.edges.push(DfgEdge {
from: r.from.0.clone(),
to: r.to.0.clone(),
kind: DependencyKind::Data,
});
}
pdg
}
pub fn augment_symbolic_propagation(
pdg: &mut DataFlowGraph,
refs: &[Reference],
index: &crate::ir::reference::SymbolIndex,
) {
use crate::ir::SymbolKind;
let mut uses_by_loc: std::collections::HashMap<(String, u32), Vec<String>> =
std::collections::HashMap::new();
let mut defs_by_loc: std::collections::HashMap<(String, u32), Vec<String>> =
std::collections::HashMap::new();
for n in &pdg.nodes {
let key = (n.file.clone(), n.line);
if n.id.contains(":use:") {
uses_by_loc
.entry(key.clone())
.or_default()
.push(n.id.clone());
}
if n.id.contains(":def:") {
defs_by_loc.entry(key).or_default().push(n.id.clone());
}
}
let mut seen_edges: std::collections::HashSet<(String, String, DependencyKind)> = pdg
.edges
.iter()
.map(|e| (e.from.clone(), e.to.clone(), e.kind.clone()))
.collect();
let node_loc_by_id: std::collections::HashMap<String, (String, u32)> = pdg
.nodes
.iter()
.map(|node| (node.id.clone(), (node.file.clone(), node.line)))
.collect();
let summaries = Self::build_function_summaries(pdg, index);
let mut summary_by_fn: std::collections::HashMap<String, FunctionSummary> =
std::collections::HashMap::new();
for s in summaries {
summary_by_fn.insert(s.function_id.clone(), s);
}
let symbolic_refs =
filter_symbolic_propagation_refs(refs, &defs_by_loc, &uses_by_loc, &summary_by_fn);
for r in symbolic_refs {
let summary_input_count = summary_by_fn
.get(&r.to.0)
.map(|summary| summary.inputs.len())
.unwrap_or_default();
let callsite_uses = collect_callsite_uses(
&uses_by_loc,
r.file.as_str(),
r.line,
summary_input_count.max(1),
);
let callsite_defs = collect_callsite_defs(&defs_by_loc, r.file.as_str(), r.line);
for u in &callsite_uses {
push_unique_edge(
pdg,
&mut seen_edges,
u.clone(),
r.to.0.clone(),
DependencyKind::Data,
);
}
for d in &callsite_defs {
push_unique_edge(
pdg,
&mut seen_edges,
r.to.0.clone(),
d.clone(),
DependencyKind::Data,
);
}
if let Some(summary) = summary_by_fn.get(&r.to.0) {
let active_flows: Vec<&FunctionInputImpact> = summary
.flows
.iter()
.filter(|flow| !flow.impacted_node_ids.is_empty())
.collect();
if active_flows.is_empty() {
continue;
}
if callsite_uses.len() == summary.inputs.len() && !callsite_uses.is_empty() {
let use_by_input: std::collections::HashMap<&str, &String> = summary
.inputs
.iter()
.zip(callsite_uses.iter())
.map(|(input, use_id)| (input.as_str(), use_id))
.collect();
for flow in &active_flows {
let Some(mapped_use) = use_by_input.get(flow.input_node_id.as_str()) else {
continue;
};
push_unique_edge(
pdg,
&mut seen_edges,
(*mapped_use).clone(),
flow.input_node_id.clone(),
DependencyKind::Data,
);
let requires_nested_validation = has_nested_completion_candidate(
r.to.0.as_str(),
&flow.impacted_node_ids,
refs,
&node_loc_by_id,
);
let nested_completion_found = push_nested_completion_bridges(
pdg,
&mut seen_edges,
r.to.0.as_str(),
&flow.impacted_node_ids,
&callsite_defs,
refs,
&summary_by_fn,
&node_loc_by_id,
&uses_by_loc,
2,
);
let can_bridge_to_callsite_defs =
!requires_nested_validation || nested_completion_found;
if can_bridge_to_callsite_defs {
for impacted in &flow.impacted_node_ids {
for d in &callsite_defs {
push_unique_edge(
pdg,
&mut seen_edges,
impacted.clone(),
d.clone(),
DependencyKind::Data,
);
}
}
}
if callsite_defs.len() == 1 && can_bridge_to_callsite_defs {
push_unique_edge(
pdg,
&mut seen_edges,
(*mapped_use).clone(),
callsite_defs[0].clone(),
DependencyKind::Data,
);
}
}
} else if summary.inputs.len() == 1 && callsite_uses.len() == 1 {
let only_use = callsite_uses[0].clone();
for flow in &active_flows {
push_unique_edge(
pdg,
&mut seen_edges,
only_use.clone(),
flow.input_node_id.clone(),
DependencyKind::Data,
);
let requires_nested_validation = has_nested_completion_candidate(
r.to.0.as_str(),
&flow.impacted_node_ids,
refs,
&node_loc_by_id,
);
let nested_completion_found = push_nested_completion_bridges(
pdg,
&mut seen_edges,
r.to.0.as_str(),
&flow.impacted_node_ids,
&callsite_defs,
refs,
&summary_by_fn,
&node_loc_by_id,
&uses_by_loc,
2,
);
let can_bridge_to_callsite_defs =
!requires_nested_validation || nested_completion_found;
if can_bridge_to_callsite_defs {
for impacted in &flow.impacted_node_ids {
for d in &callsite_defs {
push_unique_edge(
pdg,
&mut seen_edges,
impacted.clone(),
d.clone(),
DependencyKind::Data,
);
}
}
if callsite_defs.len() == 1 {
push_unique_edge(
pdg,
&mut seen_edges,
only_use.clone(),
callsite_defs[0].clone(),
DependencyKind::Data,
);
}
}
}
} else if summary.inputs.len() == 1
&& callsite_uses.is_empty()
&& callsite_defs.len() == 1
{
for flow in &active_flows {
let requires_nested_validation = has_nested_completion_candidate(
r.to.0.as_str(),
&flow.impacted_node_ids,
refs,
&node_loc_by_id,
);
let nested_completion_found = push_nested_completion_bridges(
pdg,
&mut seen_edges,
r.to.0.as_str(),
&flow.impacted_node_ids,
&callsite_defs,
refs,
&summary_by_fn,
&node_loc_by_id,
&uses_by_loc,
2,
);
if !requires_nested_validation || nested_completion_found {
for impacted in &flow.impacted_node_ids {
push_unique_edge(
pdg,
&mut seen_edges,
impacted.clone(),
callsite_defs[0].clone(),
DependencyKind::Data,
);
}
}
}
}
}
}
for s in &index.symbols {
if !matches!(s.kind, SymbolKind::Function | SymbolKind::Method) {
continue;
}
let in_span_node_ids: Vec<String> = pdg
.nodes
.iter()
.filter(|n| {
n.file == s.file && n.line >= s.range.start_line && n.line <= s.range.end_line
})
.map(|n| n.id.clone())
.collect();
for node_id in in_span_node_ids {
push_unique_edge(
pdg,
&mut seen_edges,
s.id.0.clone(),
node_id,
DependencyKind::Data,
);
}
}
}
pub fn build_function_summaries(
pdg: &DataFlowGraph,
index: &crate::ir::reference::SymbolIndex,
) -> Vec<FunctionSummary> {
use crate::ir::SymbolKind;
use std::collections::{BTreeSet, HashMap, HashSet, VecDeque};
let mut nodes_by_id: HashMap<&str, &DfgNode> = HashMap::new();
for n in &pdg.nodes {
nodes_by_id.insert(n.id.as_str(), n);
}
let mut out_adj: HashMap<&str, Vec<&str>> = HashMap::new();
let mut in_adj: HashMap<&str, Vec<&str>> = HashMap::new();
for e in &pdg.edges {
if e.kind != DependencyKind::Data {
continue;
}
if nodes_by_id.contains_key(e.from.as_str()) && nodes_by_id.contains_key(e.to.as_str())
{
out_adj
.entry(e.from.as_str())
.or_default()
.push(e.to.as_str());
in_adj
.entry(e.to.as_str())
.or_default()
.push(e.from.as_str());
}
}
let mut summaries = Vec::new();
for s in &index.symbols {
if !matches!(s.kind, SymbolKind::Function | SymbolKind::Method) {
continue;
}
let in_range_ids: Vec<String> = pdg
.nodes
.iter()
.filter(|n| {
n.file == s.file
&& n.line >= s.range.start_line
&& n.line <= s.range.end_line
&& (n.id.contains(":def:") || n.id.contains(":use:"))
})
.map(|n| n.id.clone())
.collect();
if in_range_ids.is_empty() {
continue;
}
let in_range_set: HashSet<&str> = in_range_ids.iter().map(|id| id.as_str()).collect();
let mut param_like_inputs: Vec<String> = Vec::new();
for id in &in_range_ids {
if !id.contains(":def:") {
continue;
}
if nodes_by_id
.get(id.as_str())
.is_none_or(|n| n.line != s.range.start_line)
{
continue;
}
if !param_like_inputs.iter().any(|seen| seen == id) {
param_like_inputs.push(id.clone());
}
}
let inputs: Vec<String> = if !param_like_inputs.is_empty() {
param_like_inputs
} else {
let mut roots = Vec::new();
for id in &in_range_ids {
if !id.contains(":def:") {
continue;
}
let is_root = in_adj
.get(id.as_str())
.map(|froms| froms.iter().all(|f| !in_range_set.contains(*f)))
.unwrap_or(true);
if is_root && !roots.iter().any(|seen| seen == id) {
roots.push(id.clone());
}
}
roots
};
if inputs.is_empty() {
continue;
}
let mut flows = Vec::new();
for input in &inputs {
let mut q: VecDeque<&str> = VecDeque::new();
let mut seen: HashSet<&str> = HashSet::new();
q.push_back(input.as_str());
seen.insert(input.as_str());
let mut impacted: BTreeSet<String> = BTreeSet::new();
while let Some(cur) = q.pop_front() {
for next in out_adj.get(cur).cloned().unwrap_or_default() {
if !in_range_set.contains(next) || seen.contains(next) {
continue;
}
seen.insert(next);
q.push_back(next);
if next != input.as_str() {
impacted.insert(next.to_string());
}
}
}
flows.push(FunctionInputImpact {
input_node_id: input.clone(),
impacted_node_ids: impacted.into_iter().collect(),
});
}
summaries.push(FunctionSummary {
function_id: s.id.0.clone(),
file: s.file.clone(),
start_line: s.range.start_line,
end_line: s.range.end_line,
inputs,
flows,
});
}
summaries
}
}
fn collect_control_ranges_rust(source: &str) -> Vec<(u32, u32)> {
let spec = crate::ts_core::load_rust_spec();
let compiled = match crate::ts_core::compile_queries_rust(&spec) {
Ok(c) => c,
Err(_) => return Vec::new(),
};
let Some(ctrl_q) = &compiled.control else {
return Vec::new();
};
let runner = crate::ts_core::QueryRunner::new_rust();
let offs = crate::languages::util::line_offsets(source);
let mut out = Vec::new();
for caps in runner.run_captures(source, ctrl_q) {
if let Some(c0) = caps.first() {
let start_ln = crate::languages::util::byte_to_line(&offs, c0.start);
let end_ln = crate::languages::util::byte_to_line(&offs, c0.end.saturating_sub(1));
out.push((start_ln, end_ln));
}
}
out.sort_unstable();
out.dedup();
out
}
fn collect_control_ranges_ruby(source: &str) -> Vec<(u32, u32)> {
let spec = crate::ts_core::load_ruby_spec();
let compiled = match crate::ts_core::compile_queries_ruby(&spec) {
Ok(c) => c,
Err(_) => return Vec::new(),
};
let Some(ctrl_q) = &compiled.control else {
return Vec::new();
};
let runner = crate::ts_core::QueryRunner::new_ruby();
let offs = crate::languages::util::line_offsets(source);
let mut out = Vec::new();
for caps in runner.run_captures(source, ctrl_q) {
if let Some(c0) = caps.first() {
let start_ln = crate::languages::util::byte_to_line(&offs, c0.start);
let end_ln = crate::languages::util::byte_to_line(&offs, c0.end.saturating_sub(1));
out.push((start_ln, end_ln));
}
}
let mut if_stack: Vec<u32> = Vec::new();
for (idx, raw) in source.lines().enumerate() {
let line_no = (idx + 1) as u32;
let t = raw.trim_start();
if t.starts_with("if ") || t == "if" || t.starts_with("unless ") || t == "unless" {
if_stack.push(line_no);
continue;
}
if t.starts_with("end")
&& let Some(start_ln) = if_stack.pop()
{
out.push((start_ln, line_no));
}
}
out.sort_unstable();
out.dedup();
out
}
fn reaching_def_ids_ssa_like(
name: &str,
use_line: u32,
def_records_by_name: &std::collections::HashMap<String, Vec<(u32, String)>>,
control_ranges: &[(u32, u32)],
) -> Vec<String> {
let Some(records) = def_records_by_name.get(name) else {
return Vec::new();
};
let mut prior: Vec<(u32, String)> = records
.iter()
.filter(|(line, _)| *line < use_line)
.cloned()
.collect();
if prior.is_empty() {
return Vec::new();
}
prior.sort_by_key(|(line, _)| *line);
let latest_line = prior.last().map(|(line, _)| *line).unwrap_or(0);
let mut selected: std::collections::BTreeSet<String> = prior
.iter()
.filter(|(line, _)| *line == latest_line)
.map(|(_, id)| id.clone())
.collect();
for (start, end) in control_ranges {
if *end >= use_line || latest_line < *start || latest_line > *end {
continue;
}
let mut in_range_defs: Vec<&String> = Vec::new();
for (line, id) in &prior {
if *line >= *start && *line <= *end {
selected.insert(id.clone());
in_range_defs.push(id);
}
}
if in_range_defs.len() <= 1
&& let Some((_, pre_id)) = prior.iter().rev().find(|(line, _)| *line < *start)
{
selected.insert(pre_id.clone());
}
}
selected.into_iter().collect()
}
fn reaching_def_ids_ssa_like_with_same_line_params(
name: &str,
use_line: u32,
def_records_by_name: &std::collections::HashMap<String, Vec<(u32, String)>>,
param_def_records_by_name: &std::collections::HashMap<String, Vec<(u32, String)>>,
control_ranges: &[(u32, u32)],
) -> Vec<String> {
let reaching = reaching_def_ids_ssa_like(name, use_line, def_records_by_name, control_ranges);
if !reaching.is_empty() {
return reaching;
}
let Some(param_records) = param_def_records_by_name.get(name) else {
return Vec::new();
};
let mut same_line_params: Vec<String> = param_records
.iter()
.filter(|(line, _)| *line == use_line)
.map(|(_, id)| id.clone())
.collect();
same_line_params.sort();
same_line_params.dedup();
same_line_params
}
#[cfg(test)]
mod pdg_tests {
use super::*;
use crate::ir::SymbolId;
use crate::ir::reference::Reference;
#[test]
fn build_empty_pdg() {
let dfg = DataFlowGraph {
nodes: Vec::new(),
edges: Vec::new(),
};
let refs: Vec<Reference> = Vec::new();
let pdg = PdgBuilder::build(&dfg, &refs);
assert!(pdg.nodes.is_empty());
assert!(pdg.edges.is_empty());
}
#[test]
fn build_pdg_with_refs() {
let from_id = SymbolId::new("rust", "f.rs", &crate::ir::SymbolKind::Function, "foo", 1);
let to_id = SymbolId::new("rust", "f.rs", &crate::ir::SymbolKind::Function, "bar", 5);
let r = Reference {
from: from_id.clone(),
to: to_id.clone(),
kind: crate::ir::reference::RefKind::Call,
file: "f.rs".to_string(),
line: 10,
certainty: crate::ir::reference::EdgeCertainty::Confirmed,
provenance: crate::ir::reference::EdgeProvenance::CallGraph,
};
let dfg = DataFlowGraph {
nodes: Vec::new(),
edges: Vec::new(),
};
let pdg = PdgBuilder::build(&dfg, &[r]);
assert_eq!(pdg.edges.len(), 1);
let e = &pdg.edges[0];
assert_eq!(&e.from, &from_id.0);
assert_eq!(&e.to, &to_id.0);
assert_eq!(e.kind, DependencyKind::Data);
}
#[test]
fn pdg_includes_control_and_call_edges() {
let mut dfg = DataFlowGraph {
nodes: Vec::new(),
edges: Vec::new(),
};
dfg.nodes.push(DfgNode {
id: "f.rs:ctrl:2:4".to_string(),
name: "control".to_string(),
file: "f.rs".to_string(),
line: 2,
});
dfg.nodes.push(DfgNode {
id: "f.rs:def: x:2".to_string(),
name: "x".to_string(),
file: "f.rs".to_string(),
line: 2,
});
let ref_sym = Reference {
from: crate::ir::SymbolId("rust:f.rs:fn:foo:1".to_string()),
to: crate::ir::SymbolId("rust:f.rs:fn:bar:1".to_string()),
kind: crate::ir::reference::RefKind::Call,
file: "f.rs".to_string(),
line: 10,
certainty: crate::ir::reference::EdgeCertainty::Confirmed,
provenance: crate::ir::reference::EdgeProvenance::CallGraph,
};
let pdg = PdgBuilder::build(&dfg, std::slice::from_ref(&ref_sym));
assert!(pdg.edges.iter().any(|e| e.from == ref_sym.from.0
&& e.to == ref_sym.to.0
&& e.kind == DependencyKind::Data));
assert!(pdg.nodes.iter().any(|n| n.id == "f.rs:ctrl:2:4"));
}
#[test]
fn propagation_connects_summary_interprocedurally() {
use crate::ir::{Symbol, SymbolKind, TextRange};
let mut pdg = DataFlowGraph {
nodes: vec![
DfgNode {
id: "f.rs:use:x:10".to_string(),
name: "x".to_string(),
file: "f.rs".to_string(),
line: 10,
},
DfgNode {
id: "f.rs:def:y:10".to_string(),
name: "y".to_string(),
file: "f.rs".to_string(),
line: 10,
},
DfgNode {
id: "f.rs:def:a:1".to_string(),
name: "a".to_string(),
file: "f.rs".to_string(),
line: 1,
},
DfgNode {
id: "f.rs:use:a:2".to_string(),
name: "a".to_string(),
file: "f.rs".to_string(),
line: 2,
},
],
edges: vec![DfgEdge {
from: "f.rs:def:a:1".to_string(),
to: "f.rs:use:a:2".to_string(),
kind: DependencyKind::Data,
}],
};
let refs = vec![Reference {
from: SymbolId("rust:f.rs:fn:caller:9".to_string()),
to: SymbolId("rust:f.rs:fn:callee:1".to_string()),
kind: crate::ir::reference::RefKind::Call,
file: "f.rs".to_string(),
line: 10,
certainty: crate::ir::reference::EdgeCertainty::Confirmed,
provenance: crate::ir::reference::EdgeProvenance::CallGraph,
}];
let index = crate::ir::reference::SymbolIndex::build(vec![Symbol {
id: SymbolId("rust:f.rs:fn:callee:1".to_string()),
name: "callee".to_string(),
kind: SymbolKind::Function,
file: "f.rs".to_string(),
range: TextRange {
start_line: 1,
end_line: 3,
},
language: "rust".to_string(),
}]);
PdgBuilder::augment_symbolic_propagation(&mut pdg, &refs, &index);
assert!(pdg.edges.iter().any(|e| {
e.kind == DependencyKind::Data && e.from == "f.rs:use:x:10" && e.to == "f.rs:def:a:1"
}));
assert!(pdg.edges.iter().any(|e| {
e.kind == DependencyKind::Data && e.from == "f.rs:use:a:2" && e.to == "f.rs:def:y:10"
}));
assert!(pdg.edges.iter().any(|e| {
e.kind == DependencyKind::Data && e.from == "f.rs:use:x:10" && e.to == "f.rs:def:y:10"
}));
}
#[test]
fn propagation_summary_bridge_maps_later_arg_without_leaking_earlier_arg() {
use crate::ir::{Symbol, SymbolKind, TextRange};
let mut pdg = DataFlowGraph {
nodes: vec![
DfgNode {
id: "f.rs:use:x:10".to_string(),
name: "x".to_string(),
file: "f.rs".to_string(),
line: 10,
},
DfgNode {
id: "f.rs:use:y:10".to_string(),
name: "y".to_string(),
file: "f.rs".to_string(),
line: 10,
},
DfgNode {
id: "f.rs:def:out:10".to_string(),
name: "out".to_string(),
file: "f.rs".to_string(),
line: 10,
},
DfgNode {
id: "f.rs:def:a:1".to_string(),
name: "a".to_string(),
file: "f.rs".to_string(),
line: 1,
},
DfgNode {
id: "f.rs:def:b:1".to_string(),
name: "b".to_string(),
file: "f.rs".to_string(),
line: 1,
},
DfgNode {
id: "f.rs:use:b:2".to_string(),
name: "b".to_string(),
file: "f.rs".to_string(),
line: 2,
},
],
edges: vec![DfgEdge {
from: "f.rs:def:b:1".to_string(),
to: "f.rs:use:b:2".to_string(),
kind: DependencyKind::Data,
}],
};
let refs = vec![Reference {
from: SymbolId("rust:f.rs:fn:caller:9".to_string()),
to: SymbolId("rust:f.rs:fn:callee:1".to_string()),
kind: crate::ir::reference::RefKind::Call,
file: "f.rs".to_string(),
line: 10,
certainty: crate::ir::reference::EdgeCertainty::Confirmed,
provenance: crate::ir::reference::EdgeProvenance::CallGraph,
}];
let index = crate::ir::reference::SymbolIndex::build(vec![Symbol {
id: SymbolId("rust:f.rs:fn:callee:1".to_string()),
name: "callee".to_string(),
kind: SymbolKind::Function,
file: "f.rs".to_string(),
range: TextRange {
start_line: 1,
end_line: 2,
},
language: "rust".to_string(),
}]);
PdgBuilder::augment_symbolic_propagation(&mut pdg, &refs, &index);
assert!(pdg.edges.iter().any(|e| {
e.kind == DependencyKind::Data && e.from == "f.rs:use:y:10" && e.to == "f.rs:def:b:1"
}));
assert!(pdg.edges.iter().any(|e| {
e.kind == DependencyKind::Data && e.from == "f.rs:use:y:10" && e.to == "f.rs:def:out:10"
}));
assert!(!pdg.edges.iter().any(|e| {
e.kind == DependencyKind::Data && e.from == "f.rs:use:x:10" && e.to == "f.rs:def:b:1"
}));
assert!(!pdg.edges.iter().any(|e| {
e.kind == DependencyKind::Data && e.from == "f.rs:use:x:10" && e.to == "f.rs:def:out:10"
}));
}
#[test]
fn function_summary_minimal_input_to_impact() {
use crate::ir::{Symbol, SymbolKind, TextRange};
let dfg = RustDfgBuilder::build(
"f.rs",
"fn foo(a: i32) -> i32 {\n let b = a;\n return b;\n}\n",
);
let pdg = PdgBuilder::build(&dfg, &[]);
let foo = Symbol {
id: SymbolId::new("rust", "f.rs", &SymbolKind::Function, "foo", 1),
name: "foo".to_string(),
kind: SymbolKind::Function,
file: "f.rs".to_string(),
range: TextRange {
start_line: 1,
end_line: 4,
},
language: "rust".to_string(),
};
let index = crate::ir::reference::SymbolIndex::build(vec![foo]);
let summaries = PdgBuilder::build_function_summaries(&pdg, &index);
assert_eq!(summaries.len(), 1);
let s = &summaries[0];
assert_eq!(s.function_id, "rust:f.rs:fn:foo:1");
assert!(s.inputs.iter().any(|id| id.ends_with(":def:a:1")));
let flow = s
.flows
.iter()
.find(|f| f.input_node_id.ends_with(":def:a:1"))
.expect("flow for input a");
assert!(
flow.impacted_node_ids
.iter()
.any(|id| id.ends_with(":def:b:2"))
);
assert!(
flow.impacted_node_ids
.iter()
.any(|id| id.ends_with(":use:b:3"))
);
}
#[test]
fn function_summary_tracks_same_line_parameter_use_in_single_line_function() {
use crate::ir::{Symbol, SymbolKind, TextRange};
let dfg = RustDfgBuilder::build("f.rs", "fn foo(a: i32) -> i32 { a + 1 }\n");
assert!(
dfg.nodes.iter().any(|n| n.id == "f.rs:use:a:1"),
"single-line parameter use should be tracked"
);
assert!(
dfg.edges.iter().any(|e| {
e.kind == DependencyKind::Data && e.from == "f.rs:def:a:1" && e.to == "f.rs:use:a:1"
}),
"single-line parameter use should connect back to its parameter def"
);
let foo = Symbol {
id: SymbolId::new("rust", "f.rs", &SymbolKind::Function, "foo", 1),
name: "foo".to_string(),
kind: SymbolKind::Function,
file: "f.rs".to_string(),
range: TextRange {
start_line: 1,
end_line: 1,
},
language: "rust".to_string(),
};
let index = crate::ir::reference::SymbolIndex::build(vec![foo]);
let pdg = PdgBuilder::build(&dfg, &[]);
let summaries = PdgBuilder::build_function_summaries(&pdg, &index);
let s = summaries.first().expect("summary for foo");
assert!(s.inputs.iter().any(|id| id.ends_with(":def:a:1")));
let flow = s
.flows
.iter()
.find(|f| f.input_node_id.ends_with(":def:a:1"))
.expect("flow for param a");
assert!(
flow.impacted_node_ids
.iter()
.any(|id| id.ends_with(":use:a:1")),
"single-line return expression should remain visible in summary impact"
);
}
#[test]
fn function_summary_prefers_parameter_inputs_over_local_roots() {
use crate::ir::{Symbol, SymbolKind, TextRange};
let dfg = RustDfgBuilder::build(
"f.rs",
"fn foo(a: i32) -> i32 {\n let seed = 1;\n let b = a + seed;\n return b;\n}\n",
);
let pdg = PdgBuilder::build(&dfg, &[]);
let foo = Symbol {
id: SymbolId::new("rust", "f.rs", &SymbolKind::Function, "foo", 1),
name: "foo".to_string(),
kind: SymbolKind::Function,
file: "f.rs".to_string(),
range: TextRange {
start_line: 1,
end_line: 5,
},
language: "rust".to_string(),
};
let index = crate::ir::reference::SymbolIndex::build(vec![foo]);
let summaries = PdgBuilder::build_function_summaries(&pdg, &index);
let s = summaries.first().expect("summary for foo");
assert!(s.inputs.iter().any(|id| id.ends_with(":def:a:1")));
assert!(
!s.inputs.iter().any(|id| id.ends_with(":def:seed:2")),
"parameter-style input selection should suppress local root defs"
);
}
#[test]
fn function_summary_falls_back_to_root_defs_without_params() {
use crate::ir::{Symbol, SymbolKind, TextRange};
let dfg = RustDfgBuilder::build(
"f.rs",
"fn bar() -> i32 {\n let seed = 1;\n let out = seed;\n return out;\n}\n",
);
let pdg = PdgBuilder::build(&dfg, &[]);
let bar = Symbol {
id: SymbolId::new("rust", "f.rs", &SymbolKind::Function, "bar", 1),
name: "bar".to_string(),
kind: SymbolKind::Function,
file: "f.rs".to_string(),
range: TextRange {
start_line: 1,
end_line: 5,
},
language: "rust".to_string(),
};
let index = crate::ir::reference::SymbolIndex::build(vec![bar]);
let summaries = PdgBuilder::build_function_summaries(&pdg, &index);
let s = summaries.first().expect("summary for bar");
assert!(
s.inputs.iter().any(|id| id.ends_with(":def:seed:2")),
"fallback root-def input should remain for parameterless function"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dfg::RubyDfgBuilder;
#[test]
fn build_empty_graph() {
let dfg = RustDfgBuilder::build("foo.rs", "");
assert!(dfg.nodes.is_empty(), "expected no nodes in empty source");
assert!(dfg.edges.is_empty(), "expected no edges in empty source");
}
#[test]
fn build_assignment_definitions() {
let src = r#"
let x = 1;
x = 42;
let y = x;
"#;
let dfg = RustDfgBuilder::build("f.rs", src);
let defs: Vec<_> = dfg
.nodes
.iter()
.filter(|n| n.name == "x" && n.id.contains(":def:"))
.collect();
assert!(
defs.len() >= 2,
"expected >=2 definitions of x, got {}",
defs.len()
);
let use_id = "f.rs:use:x:4";
let incoming: Vec<_> = dfg
.edges
.iter()
.filter(|e| e.kind == DependencyKind::Data && e.to == use_id)
.map(|e| e.from.clone())
.collect();
assert_eq!(incoming.len(), 1, "expected one reaching def for x@line4");
assert!(
incoming[0].ends_with(":def:x:3"),
"expected latest def at line 3, got {}",
incoming[0]
);
assert!(dfg.edges.iter().any(|e| {
e.kind == DependencyKind::Data
&& e.from.ends_with(":def:x:3")
&& e.to.ends_with(":def:y:4")
}));
}
#[test]
fn rust_alias_chain_and_reassignment_prefers_latest_def() {
let src = "let mut a = seed;\nlet b = a;\na = other;\nlet c = a;\nlet d = b;\n";
let dfg = RustDfgBuilder::build("f.rs", src);
assert!(dfg.nodes.iter().any(|n| n.id.ends_with(":def:a:1")));
assert!(!dfg.nodes.iter().any(|n| n.name == "mut"));
assert!(dfg.edges.iter().any(|e| {
e.kind == DependencyKind::Data
&& e.from.ends_with(":def:a:1")
&& e.to.ends_with(":def:b:2")
}));
assert!(dfg.edges.iter().any(|e| {
e.kind == DependencyKind::Data
&& e.from.ends_with(":def:b:2")
&& e.to.ends_with(":def:d:5")
}));
assert!(dfg.edges.iter().any(|e| {
e.kind == DependencyKind::Data
&& e.from.ends_with(":def:a:3")
&& e.to.ends_with(":def:c:4")
}));
assert!(!dfg.edges.iter().any(|e| {
e.kind == DependencyKind::Data
&& e.from.ends_with(":def:a:1")
&& e.to.ends_with(":def:c:4")
}));
}
#[test]
fn build_return_dependency() {
let src = r#"
fn foo() {
let a = compute();
return a;
}
"#;
let dfg = RustDfgBuilder::build("f.rs", src);
let def_ids: Vec<&String> = dfg
.nodes
.iter()
.filter(|n| n.name == "a" && n.id.contains(":def:"))
.map(|n| &n.id)
.collect();
assert_eq!(
def_ids.len(),
1,
"expected one definition of a, got {}",
def_ids.len()
);
let use_ids: Vec<&String> = dfg
.nodes
.iter()
.filter(|n| n.name == "a" && n.id.contains(":use:"))
.map(|n| &n.id)
.collect();
assert_eq!(
use_ids.len(),
1,
"expected one use of a, got {}",
use_ids.len()
);
let edge_exists = dfg.edges.iter().any(|e| {
e.kind == DependencyKind::Data && e.from == *def_ids[0] && e.to == *use_ids[0]
});
assert!(
edge_exists,
"expected data edge from '{}' to '{}'",
def_ids[0], use_ids[0]
);
}
#[test]
fn build_control_dependencies() {
let src = r#"
let x = 1;
if x > 0 {
let y = x;
let z = y;
}
"#;
let dfg = RustDfgBuilder::build("f.rs", src);
let ctrl_nodes: Vec<_> = dfg
.nodes
.iter()
.filter(|n| n.id.contains(":ctrl:"))
.collect();
assert!(!ctrl_nodes.is_empty(), "expected control nodes");
let ctrl_edges: Vec<_> = dfg
.edges
.iter()
.filter(|e| e.kind == DependencyKind::Control)
.collect();
assert!(!ctrl_edges.is_empty(), "expected control edges");
for edge in ctrl_edges {
assert!(
edge.from.contains(":ctrl:"),
"control edge should originate from control node"
);
}
}
#[test]
fn rust_reaching_defs_branch_join_includes_both_branch_defs() {
let src = "let x = 0;\nif cond {\n x = 1;\n} else {\n x = 2;\n}\nlet y = x;\n";
let dfg = RustDfgBuilder::build("f.rs", src);
let use_id = "f.rs:use:x:7";
let incoming: std::collections::BTreeSet<_> = dfg
.edges
.iter()
.filter(|e| e.kind == DependencyKind::Data && e.to == use_id)
.map(|e| e.from.clone())
.collect();
assert!(
incoming.iter().any(|id| id.ends_with(":def:x:3")),
"expected then-branch def to reach join"
);
assert!(
incoming.iter().any(|id| id.ends_with(":def:x:5")),
"expected else-branch def to reach join"
);
assert!(
!incoming.iter().any(|id| id.ends_with(":def:x:1")),
"initial def should be shadowed by branch defs"
);
}
#[test]
fn rust_reaching_defs_partial_branch_keeps_pre_branch_def() {
let src = "let x = 0;\nif cond {\n x = 1;\n}\nlet y = x;\n";
let dfg = RustDfgBuilder::build("f.rs", src);
let use_id = "f.rs:use:x:5";
let incoming: std::collections::BTreeSet<_> = dfg
.edges
.iter()
.filter(|e| e.kind == DependencyKind::Data && e.to == use_id)
.map(|e| e.from.clone())
.collect();
assert!(
incoming.iter().any(|id| id.ends_with(":def:x:3")),
"branch def should reach join"
);
assert!(
incoming.iter().any(|id| id.ends_with(":def:x:1")),
"pre-branch def should remain for non-taken path"
);
}
#[test]
fn ruby_alias_assignment_adds_def_to_def_edge() {
let src = "a = seed\nb = a\nreturn b\n";
let dfg = RubyDfgBuilder::build("test.rb", src);
assert!(dfg.edges.iter().any(|e| {
e.kind == DependencyKind::Data
&& e.from.ends_with(":def:a:1")
&& e.to.ends_with(":def:b:2")
}));
}
#[test]
fn ruby_alias_reassignment_prefers_latest_def() {
let src = "a = seed\nb = a\na = other\nc = a\n";
let dfg = RubyDfgBuilder::build("test.rb", src);
assert!(dfg.edges.iter().any(|e| {
e.kind == DependencyKind::Data
&& e.from.ends_with(":def:a:1")
&& e.to.ends_with(":def:b:2")
}));
assert!(dfg.edges.iter().any(|e| {
e.kind == DependencyKind::Data
&& e.from.ends_with(":def:a:3")
&& e.to.ends_with(":def:c:4")
}));
assert!(!dfg.edges.iter().any(|e| {
e.kind == DependencyKind::Data
&& e.from.ends_with(":def:a:1")
&& e.to.ends_with(":def:c:4")
}));
}
#[test]
fn ruby_reaching_defs_partial_branch_keeps_pre_branch_def() {
let src = "a = seed\nif cond\n a = other\nend\nb = a\n";
let dfg = RubyDfgBuilder::build("test.rb", src);
let use_id = "test.rb:use:a:5";
let incoming: std::collections::BTreeSet<_> = dfg
.edges
.iter()
.filter(|e| e.kind == DependencyKind::Data && e.to == use_id)
.map(|e| e.from.clone())
.collect();
assert!(
incoming.iter().any(|id| id.ends_with(":def:a:3")),
"branch def should reach join"
);
assert!(
incoming.iter().any(|id| id.ends_with(":def:a:1")),
"pre-branch def should remain for non-taken path"
);
}
#[test]
fn ruby_no_paren_method_params_feed_alias_and_return_flow() {
let src = r#"def bounce value, fallback: nil
alias_value = value
return alias_value
end
"#;
let dfg = RubyDfgBuilder::build("test.rb", src);
assert!(dfg.nodes.iter().any(|n| n.id == "test.rb:def:value:1"));
assert!(dfg.edges.iter().any(|e| {
e.kind == DependencyKind::Data
&& e.from == "test.rb:def:value:1"
&& e.to == "test.rb:def:alias_value:2"
}));
assert!(dfg.edges.iter().any(|e| {
e.kind == DependencyKind::Data
&& e.from == "test.rb:def:alias_value:2"
&& e.to == "test.rb:use:alias_value:3"
}));
}
#[test]
fn build_ruby_dfg_simple() {
let src = r#"
def foo(a)
b = a
if b > 0
c = b
end
return c
end
"#;
let path = "test.rb";
let dfg = RubyDfgBuilder::build(path, src);
assert!(
dfg.nodes
.iter()
.any(|n| n.name == "a" && n.id.starts_with("test.rb:def:a:")),
"expected definition node for 'a'"
);
assert!(
dfg.nodes.iter().any(|n| n.id.contains(":def:b:")),
"expected def node for 'b'"
);
assert!(
dfg.nodes.iter().any(|n| n.id.contains(":def:c:")),
"expected def node for 'c'"
);
assert!(
dfg.edges
.iter()
.any(|e| e.from.contains(":def:a:") && e.to.contains(":use:a:")),
"expected data edge from def:a to use:a"
);
assert!(
dfg.edges
.iter()
.any(|e| e.from.contains(":def:b:") && e.to.contains(":use:b:")),
"expected data edge from def:b to use:b"
);
assert!(
dfg.nodes.iter().any(|n| n.id.contains(":ctrl:")),
"expected control node"
);
assert!(
dfg.edges.iter().any(|e| e.kind == DependencyKind::Control),
"expected control edges"
);
}
}