#[allow(clippy::wildcard_imports)]
use super::*;
struct ReceiverInitializerCall<'a> {
span: bonsai_common::Span,
name: &'a str,
kind: crate::CallKind,
receiver: Option<&'a str>,
receiver_types: &'a [String],
}
#[must_use]
pub fn collect_receiver_field_writes(
events: &[crate::FlowEvent],
params: &[String],
receiver_param_index: Option<usize>,
implicit_receiver_names: &[&str],
implicit_receiver_prefixes: &[&str],
) -> Vec<crate::FieldWrite> {
let param_keys: Vec<Vec<String>> = params.iter().map(|param| name_variants(param)).collect();
let mut out = Vec::new();
if let Some(receiver_idx) = receiver_param_index {
if let Some(receiver_param) = params.get(receiver_idx) {
collect_receiver_field_writes_inner(
events,
&[receiver_param.as_str()],
&[],
Some(receiver_idx),
¶m_keys,
&mut out,
);
}
}
if !implicit_receiver_names.is_empty() || !implicit_receiver_prefixes.is_empty() {
collect_receiver_field_writes_inner(
events,
implicit_receiver_names,
implicit_receiver_prefixes,
None,
¶m_keys,
&mut out,
);
}
out.sort_by_key(|write| (write.span.start, write.target.clone()));
out.dedup_by(|a, b| {
a.span == b.span && a.target == b.target && a.source_param_indices == b.source_param_indices
});
out
}
#[must_use]
pub fn collect_receiver_field_initializers(
events: &[crate::FlowEvent],
receiver_names: &[&str],
) -> Vec<crate::ReceiverFieldInitializer> {
fn collect_calls<'a>(events: &'a [crate::FlowEvent], out: &mut Vec<ReceiverInitializerCall<'a>>) {
for event in events {
match event {
crate::FlowEvent::Call {
span,
name,
call_kind,
receiver,
receiver_types,
..
} => out.push(ReceiverInitializerCall {
span: *span,
name,
kind: *call_kind,
receiver: receiver.as_deref(),
receiver_types,
}),
crate::FlowEvent::Branch {
then_events,
else_events,
..
} => {
collect_calls(then_events, out);
collect_calls(else_events, out);
}
crate::FlowEvent::Loop { body, .. }
| crate::FlowEvent::Defer { body, .. }
| crate::FlowEvent::Using { body, .. } => collect_calls(body, out),
crate::FlowEvent::Try {
body,
catch_events,
finally_events,
..
} => {
collect_calls(body, out);
collect_calls(catch_events, out);
collect_calls(finally_events, out);
}
_ => {}
}
}
}
fn collect_assignments(
events: &[crate::FlowEvent],
receiver_names: &[&str],
calls: &[ReceiverInitializerCall<'_>],
out: &mut Vec<crate::ReceiverFieldInitializer>,
) {
for event in events {
match event {
crate::FlowEvent::Assign {
span,
target,
source_call: Some(source_call),
..
} if receiver_names.iter().any(|receiver| {
target
.strip_prefix(receiver)
.is_some_and(|suffix| suffix.starts_with('.'))
}) =>
{
let call = calls
.iter()
.filter(|call| {
call.name == source_call
&& call.span.file == span.file
&& span.start <= call.span.start
&& call.span.end <= span.end
})
.max_by_key(|call| call.span.end.saturating_sub(call.span.start));
let (call_kind, call_receiver, call_receiver_types) = call.map_or_else(
|| (crate::CallKind::Function, None, Vec::new()),
|call| {
(
call.kind,
call.receiver.map(str::to_string),
call.receiver_types.to_vec(),
)
},
);
out.push(crate::ReceiverFieldInitializer {
span: *span,
target: target.clone(),
call_name: source_call.clone(),
call_kind,
call_receiver,
call_receiver_types,
});
}
crate::FlowEvent::Branch {
then_events,
else_events,
..
} => {
collect_assignments(then_events, receiver_names, calls, out);
collect_assignments(else_events, receiver_names, calls, out);
}
crate::FlowEvent::Loop { body, .. }
| crate::FlowEvent::Defer { body, .. }
| crate::FlowEvent::Using { body, .. } => {
collect_assignments(body, receiver_names, calls, out);
}
crate::FlowEvent::Try {
body,
catch_events,
finally_events,
..
} => {
collect_assignments(body, receiver_names, calls, out);
collect_assignments(catch_events, receiver_names, calls, out);
collect_assignments(finally_events, receiver_names, calls, out);
}
_ => {}
}
}
}
let mut calls = Vec::new();
collect_calls(events, &mut calls);
let mut out = Vec::new();
collect_assignments(events, receiver_names, &calls, &mut out);
out.sort_by(|left, right| {
(left.span, left.target.as_str(), left.call_name.as_str()).cmp(&(
right.span,
right.target.as_str(),
right.call_name.as_str(),
))
});
out.dedup();
out
}
fn collect_receiver_field_writes_inner(
events: &[crate::FlowEvent],
receiver_names: &[&str],
receiver_prefixes: &[&str],
receiver_idx: Option<usize>,
param_keys: &[Vec<String>],
out: &mut Vec<crate::FieldWrite>,
) {
for event in events {
match event {
crate::FlowEvent::Assign {
span,
target,
source_name,
source_call_args,
source_names,
..
} => {
if !place_matches_receiver(target, receiver_names, receiver_prefixes) {
continue;
}
let source_values =
assignment_source_values(source_name.as_ref(), source_call_args, source_names);
let mut source_param_indices = Vec::new();
for (idx, variants) in param_keys.iter().enumerate() {
if receiver_idx == Some(idx) {
continue;
}
if source_values
.iter()
.any(|source| variants.iter().any(|variant| source == variant))
{
source_param_indices.push(idx);
}
}
if !source_param_indices.is_empty() {
out.push(crate::FieldWrite {
span: *span,
target: target.clone(),
source_param_indices,
});
}
}
crate::FlowEvent::Branch {
then_events,
else_events,
..
} => {
collect_receiver_field_writes_inner(
then_events,
receiver_names,
receiver_prefixes,
receiver_idx,
param_keys,
out,
);
collect_receiver_field_writes_inner(
else_events,
receiver_names,
receiver_prefixes,
receiver_idx,
param_keys,
out,
);
}
crate::FlowEvent::Loop { body, .. }
| crate::FlowEvent::Defer { body, .. }
| crate::FlowEvent::Using { body, .. } => {
collect_receiver_field_writes_inner(
body,
receiver_names,
receiver_prefixes,
receiver_idx,
param_keys,
out,
);
}
crate::FlowEvent::Try {
body,
catch_events,
finally_events,
..
} => {
collect_receiver_field_writes_inner(
body,
receiver_names,
receiver_prefixes,
receiver_idx,
param_keys,
out,
);
collect_receiver_field_writes_inner(
catch_events,
receiver_names,
receiver_prefixes,
receiver_idx,
param_keys,
out,
);
collect_receiver_field_writes_inner(
finally_events,
receiver_names,
receiver_prefixes,
receiver_idx,
param_keys,
out,
);
}
_ => {}
}
}
}
fn place_matches_receiver(place: &str, receiver_names: &[&str], receiver_prefixes: &[&str]) -> bool {
let trimmed = place.trim_start();
receiver_names
.iter()
.any(|receiver| place_base_matches(place, receiver))
|| receiver_prefixes.iter().any(|prefix| trimmed.starts_with(prefix))
}
pub fn collect_receiver_state_sources(
events: &[crate::FlowEvent],
params: &[String],
implicit_receiver_names: &[&str],
) -> Vec<String> {
if implicit_receiver_names.is_empty() {
return Vec::new();
}
let mut locals: std::collections::HashSet<String> = params.iter().cloned().collect();
collect_assign_targets(events, &mut locals);
let mut out: std::collections::BTreeSet<String> = implicit_receiver_names
.iter()
.map(|name| (*name).to_string())
.collect();
collect_receiver_state_sources_inner(events, &locals, implicit_receiver_names, &mut out);
out.into_iter().collect()
}
pub fn collect_assign_targets<S: std::hash::BuildHasher>(
events: &[crate::FlowEvent],
out: &mut std::collections::HashSet<String, S>,
) {
for event in events {
match event {
crate::FlowEvent::Assign { target, .. } if !target.is_empty() => {
out.insert(target.trim().to_string());
}
crate::FlowEvent::Branch {
then_events,
else_events,
..
} => {
collect_assign_targets(then_events, out);
collect_assign_targets(else_events, out);
}
crate::FlowEvent::Loop { body, .. }
| crate::FlowEvent::Defer { body, .. }
| crate::FlowEvent::Using { body, .. } => collect_assign_targets(body, out),
crate::FlowEvent::Try {
body,
catch_events,
finally_events,
..
} => {
collect_assign_targets(body, out);
collect_assign_targets(catch_events, out);
collect_assign_targets(finally_events, out);
}
_ => {}
}
}
}
pub struct ImplicitMemberReadCall {
pub source_call: String,
pub call_name: String,
pub receiver: Option<String>,
pub call_kind: crate::CallKind,
}
#[derive(Clone, Debug)]
pub struct FlowFieldAssignInsertion {
pub assign_span: bonsai_common::Span,
pub target: String,
pub fields: Vec<crate::FlowEvent>,
}
pub fn insert_flow_field_assignments(
events: &mut Vec<crate::FlowEvent>,
insertions: &[FlowFieldAssignInsertion],
) {
let mut index = 0usize;
while index < events.len() {
match &mut events[index] {
crate::FlowEvent::Branch {
then_events,
else_events,
..
} => {
insert_flow_field_assignments(then_events, insertions);
insert_flow_field_assignments(else_events, insertions);
}
crate::FlowEvent::Loop { body, .. }
| crate::FlowEvent::Defer { body, .. }
| crate::FlowEvent::Using { body, .. } => insert_flow_field_assignments(body, insertions),
crate::FlowEvent::Try {
body,
catch_events,
finally_events,
..
} => {
insert_flow_field_assignments(body, insertions);
insert_flow_field_assignments(catch_events, insertions);
insert_flow_field_assignments(finally_events, insertions);
}
_ => {}
}
let fields = match &events[index] {
crate::FlowEvent::Assign { span, target, .. } => insertions
.iter()
.filter(|insertion| {
insertion.target == *target
&& span.file == insertion.assign_span.file
&& span.start <= insertion.assign_span.end
&& insertion.assign_span.start <= span.end
})
.flat_map(|insertion| insertion.fields.clone())
.collect::<Vec<_>>(),
_ => Vec::new(),
};
if fields.is_empty() {
index += 1;
continue;
}
let inserted = fields.len();
events.splice((index + 1)..=index, fields);
index += inserted + 1;
}
}
pub fn qualify_implicit_member_assign_targets<F, SM, SE>(
events: &mut [crate::FlowEvent],
members: &std::collections::HashSet<String, SM>,
excluded: &std::collections::HashSet<String, SE>,
qualify: F,
) where
F: Fn(&str) -> Option<String> + Copy,
SM: std::hash::BuildHasher,
SE: std::hash::BuildHasher,
{
for event in events {
match event {
crate::FlowEvent::Assign { target, .. } => {
let name = target.trim();
if members.contains(name) && !excluded.contains(name) {
if let Some(qualified) = qualify(name) {
*target = qualified;
}
}
}
crate::FlowEvent::Branch {
then_events,
else_events,
..
} => {
qualify_implicit_member_assign_targets(then_events, members, excluded, qualify);
qualify_implicit_member_assign_targets(else_events, members, excluded, qualify);
}
crate::FlowEvent::Loop { body, .. }
| crate::FlowEvent::Defer { body, .. }
| crate::FlowEvent::Using { body, .. } => {
qualify_implicit_member_assign_targets(body, members, excluded, qualify);
}
crate::FlowEvent::Try {
body,
catch_events,
finally_events,
..
} => {
qualify_implicit_member_assign_targets(body, members, excluded, qualify);
qualify_implicit_member_assign_targets(catch_events, members, excluded, qualify);
qualify_implicit_member_assign_targets(finally_events, members, excluded, qualify);
}
_ => {}
}
}
}
pub fn rewrite_implicit_member_reads<F, SG, SL>(
events: &mut Vec<crate::FlowEvent>,
getters: &std::collections::HashSet<String, SG>,
locals: &std::collections::HashSet<String, SL>,
call_for_name: F,
) where
F: Fn(&str) -> ImplicitMemberReadCall + Copy,
SG: std::hash::BuildHasher,
SL: std::hash::BuildHasher,
{
for event in events.iter_mut() {
match event {
crate::FlowEvent::Branch {
then_events,
else_events,
..
} => {
rewrite_implicit_member_reads(then_events, getters, locals, call_for_name);
rewrite_implicit_member_reads(else_events, getters, locals, call_for_name);
}
crate::FlowEvent::Loop { body, .. }
| crate::FlowEvent::Defer { body, .. }
| crate::FlowEvent::Using { body, .. } => {
rewrite_implicit_member_reads(body, getters, locals, call_for_name);
}
crate::FlowEvent::Try {
body,
catch_events,
finally_events,
..
} => {
rewrite_implicit_member_reads(body, getters, locals, call_for_name);
rewrite_implicit_member_reads(catch_events, getters, locals, call_for_name);
rewrite_implicit_member_reads(finally_events, getters, locals, call_for_name);
}
_ => {}
}
}
let mut idx = 0usize;
while idx < events.len() {
let (qualify_name, span) = match &events[idx] {
crate::FlowEvent::Assign {
target,
source_name,
source_call,
span,
..
} => {
if source_call.is_some() {
(None, *span)
} else if let Some(name) = source_name.as_deref().map(str::trim).map(str::to_string) {
if getters.contains(&name) && !locals.contains(&name) && name != target.trim() {
(Some(name), *span)
} else {
(None, *span)
}
} else {
(None, *span)
}
}
_ => {
idx += 1;
continue;
}
};
let Some(name) = qualify_name else {
idx += 1;
continue;
};
let call = call_for_name(&name);
if let crate::FlowEvent::Assign {
source_name,
source_call,
source_call_args,
source_names,
value_kind,
..
} = &mut events[idx]
{
*source_call = Some(call.source_call.clone());
*source_call_args = Vec::new();
*source_name = None;
source_names.retain(|s| {
let trimmed = s.trim();
trimmed != name && trimmed != call.source_call
});
*value_kind = Some(crate::AssignValueKind::CallResult);
}
events.insert(
idx,
crate::FlowEvent::Call {
span,
name: call.call_name,
receiver: call.receiver,
receiver_types: Vec::new(),
call_kind: call.call_kind,
args: Vec::new(),
},
);
idx += 2;
}
}
pub fn qualify_implicit_member_reads_in_index<F>(index: &mut crate::DeclIndex, call_for_name: F)
where
F: Fn(&str) -> ImplicitMemberReadCall + Copy,
{
let getter_names: std::collections::HashSet<String> = index
.defs
.iter()
.filter(|decl| {
matches!(decl.kind, crate::DeclKind::Method | crate::DeclKind::Function)
&& decl.params.is_empty()
&& !decl.name.is_empty()
})
.map(|decl| decl.name.clone())
.collect();
if getter_names.is_empty() {
return;
}
for decl in &mut index.defs {
if decl.flow_events.is_empty() {
continue;
}
let mut locals: std::collections::HashSet<String> = decl.params.iter().cloned().collect();
collect_assign_targets(&decl.flow_events, &mut locals);
rewrite_implicit_member_reads(&mut decl.flow_events, &getter_names, &locals, call_for_name);
}
}
pub fn qualify_receiver_field_expression_flows<S: std::hash::BuildHasher>(
events: &mut [crate::FlowEvent],
fields: &std::collections::HashSet<String, S>,
receiver: &str,
) {
for event in events {
match event {
crate::FlowEvent::Return { value_flow, .. } | crate::FlowEvent::Yield { value_flow, .. } => {
qualify_receiver_field_expression_flow(value_flow, fields, receiver);
}
crate::FlowEvent::Branch {
then_events,
else_events,
..
} => {
qualify_receiver_field_expression_flows(then_events, fields, receiver);
qualify_receiver_field_expression_flows(else_events, fields, receiver);
}
crate::FlowEvent::Loop { body, .. }
| crate::FlowEvent::Defer { body, .. }
| crate::FlowEvent::Using { body, .. } => {
qualify_receiver_field_expression_flows(body, fields, receiver);
}
crate::FlowEvent::Try {
body,
catch_events,
finally_events,
..
} => {
qualify_receiver_field_expression_flows(body, fields, receiver);
qualify_receiver_field_expression_flows(catch_events, fields, receiver);
qualify_receiver_field_expression_flows(finally_events, fields, receiver);
}
_ => {}
}
}
}
fn qualify_receiver_field_expression_flow<S: std::hash::BuildHasher>(
flow: &mut crate::ExpressionFlow,
fields: &std::collections::HashSet<String, S>,
receiver: &str,
) {
let old_place = flow.place.clone();
if let Some(projection) = &mut flow.projection {
if fields.contains(&projection.base) {
projection.path.insert(0, std::mem::take(&mut projection.base));
projection.base = receiver.to_string();
flow.place = Some(projection.canonical_place());
}
} else if let Some(place) = flow.place.as_deref() {
if fields.contains(place) {
flow.projection = Some(crate::ExpressionProjection {
base: receiver.to_string(),
path: vec![place.to_string()],
});
flow.place = Some(format!("{receiver}.{place}"));
}
}
if let (Some(old_place), Some(new_place)) = (old_place.as_deref(), flow.place.as_deref()) {
if old_place != new_place {
for source in &mut flow.source_names {
if source == old_place {
source.clone_from(&new_place.to_string());
}
}
}
}
for field in &mut flow.aggregate_fields {
qualify_receiver_field_expression_flow(&mut field.value, fields, receiver);
}
for item in &mut flow.tuple_items {
qualify_receiver_field_expression_flow(item, fields, receiver);
}
for spread in &mut flow.spreads {
qualify_receiver_field_expression_flow(spread, fields, receiver);
}
}
fn collect_receiver_state_sources_inner(
events: &[crate::FlowEvent],
locals: &std::collections::HashSet<String>,
implicit_receiver_names: &[&str],
out: &mut std::collections::BTreeSet<String>,
) {
for event in events {
match event {
crate::FlowEvent::Assign {
source_name,
source_names,
..
} => {
if let Some(source) = source_name {
collect_receiver_state_source_name(source, locals, implicit_receiver_names, out);
}
for source in source_names {
collect_receiver_state_source_name(source, locals, implicit_receiver_names, out);
}
}
crate::FlowEvent::Call { receiver, .. } => {
if let Some(receiver) = receiver {
if implicit_receiver_names
.iter()
.any(|name| place_base_matches(receiver, name))
{
out.insert(receiver.clone());
}
}
if let crate::FlowEvent::Call { args, .. } = event {
for arg in args {
if let Some(place) = &arg.place {
collect_receiver_state_source_name(place, locals, implicit_receiver_names, out);
}
for source in &arg.source_names {
collect_receiver_state_source_name(source, locals, implicit_receiver_names, out);
}
}
}
}
crate::FlowEvent::Return { value_flow, .. } => {
collect_receiver_state_expression_flow(value_flow, locals, implicit_receiver_names, out);
}
crate::FlowEvent::Branch {
then_events,
else_events,
..
} => {
collect_receiver_state_sources_inner(then_events, locals, implicit_receiver_names, out);
collect_receiver_state_sources_inner(else_events, locals, implicit_receiver_names, out);
}
crate::FlowEvent::Loop { body, .. }
| crate::FlowEvent::Defer { body, .. }
| crate::FlowEvent::Using { body, .. } => {
collect_receiver_state_sources_inner(body, locals, implicit_receiver_names, out);
}
crate::FlowEvent::Try {
body,
catch_events,
finally_events,
..
} => {
collect_receiver_state_sources_inner(body, locals, implicit_receiver_names, out);
collect_receiver_state_sources_inner(catch_events, locals, implicit_receiver_names, out);
collect_receiver_state_sources_inner(finally_events, locals, implicit_receiver_names, out);
}
_ => {}
}
}
}
fn collect_receiver_state_expression_flow(
flow: &crate::ExpressionFlow,
locals: &std::collections::HashSet<String>,
implicit_receiver_names: &[&str],
out: &mut std::collections::BTreeSet<String>,
) {
if let Some(place) = &flow.place {
collect_receiver_state_source_name(place, locals, implicit_receiver_names, out);
}
for source in &flow.source_names {
collect_receiver_state_source_name(source, locals, implicit_receiver_names, out);
}
for field in &flow.aggregate_fields {
collect_receiver_state_expression_flow(&field.value, locals, implicit_receiver_names, out);
}
for item in &flow.tuple_items {
collect_receiver_state_expression_flow(item, locals, implicit_receiver_names, out);
}
for spread in &flow.spreads {
collect_receiver_state_expression_flow(spread, locals, implicit_receiver_names, out);
}
}
fn collect_receiver_state_source_name(
source: &str,
locals: &std::collections::HashSet<String>,
implicit_receiver_names: &[&str],
out: &mut std::collections::BTreeSet<String>,
) {
let source = source.trim();
if source.is_empty() || locals.contains(source) {
return;
}
let base_is_local = normalised_place_base(source).is_some_and(|base| locals.contains(&base));
if implicit_receiver_names
.iter()
.any(|name| place_base_matches(source, name))
|| bonsai_common::qualified_name_owner(source).is_none()
|| !base_is_local
{
out.insert(source.to_string());
}
}
pub(crate) fn argument_place(node: &Node<'_>, src: &[u8], handler: &GrammarHandler) -> Option<String> {
structural_argument_place(*node, src, handler)
}
fn structural_argument_place(node: Node<'_>, src: &[u8], handler: &GrammarHandler) -> Option<String> {
let kind = node.kind();
if let Some(operand) = handler
.indirect_place_operand_extractor
.and_then(|extract| extract(node))
.filter(|operand| operand.id() != node.id())
{
return structural_argument_place(operand, src, handler);
}
if let Some(extract) = handler.expression_place_extractor {
let extracted = extract(node, src);
if extracted.places.len() == 1 {
let all_consumed = extracted.consumed_node_ids.contains(&node.id()) || {
let mut cursor = node.walk();
let all = node
.named_children(&mut cursor)
.all(|child| extracted.consumed_node_ids.contains(&child.id()));
all
};
if all_consumed {
return extracted.places.into_iter().next();
}
}
}
let text = normalize_call_name_whitespace(node_text(&node, src));
if node.named_child_count() == 0
&& handler
.implicit_receiver_names
.iter()
.any(|receiver| *receiver == text)
{
return Some(text);
}
if handler.identifier_kinds.contains(&kind)
|| handler.sigil_variable_kinds.contains(&kind)
|| handler.global_variable_kinds.contains(&kind)
{
let text = handler
.reference_name_extractor
.and_then(|extract| extract(node, src))
.unwrap_or(text);
return (!text.is_empty()).then_some(text);
}
if let Some((base, key)) = handler
.computed_subscript_extractor
.and_then(|extract| extract(node))
{
return structural_subscript_place(base, key, src, handler);
}
if handler.member_expression_kinds.contains(&kind) {
let base = first_declared_field(node, handler.member_base_field_names)
.and_then(|base| structural_argument_place(base, src, handler))?;
let member = first_declared_field(node, handler.member_name_field_names)?;
let member = handler
.reference_name_extractor
.and_then(|extract| extract(member, src))
.unwrap_or_else(|| normalize_call_name_whitespace(node_text(&member, src)));
if member.is_empty() {
return None;
}
return Some(format!("{base}.{member}"));
}
if handler.subscript_expression_kinds.contains(&kind) {
let base = first_declared_field(node, handler.subscript_base_field_names)?;
let key = first_declared_field(node, handler.subscript_index_field_names)?;
return structural_subscript_place(base, key, src, handler);
}
if handler.transparent_call_wrapper_kinds.contains(&kind) {
let mut cursor = node.walk();
let children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
if let [child] = children.as_slice() {
return structural_argument_place(*child, src, handler);
}
}
node.child_by_field_name("value")
.filter(|value| value.id() != node.id())
.and_then(|value| structural_argument_place(value, src, handler))
}
fn structural_subscript_place(
base: Node<'_>,
key: Node<'_>,
src: &[u8],
handler: &GrammarHandler,
) -> Option<String> {
let base = structural_argument_place(base, src, handler)?;
let static_key = handler
.static_subscript_key_extractor
.and_then(|extract| extract(key, src))
.or_else(|| {
let text = normalize_call_name_whitespace(node_text(&key, src));
handler.is_literal_value(key.kind(), &text).then_some(text)
});
Some(static_key.map_or_else(|| format!("{base}.*"), |key| format!("{base}.{key}")))
}
fn first_declared_field<'tree>(node: Node<'tree>, field_names: &[&str]) -> Option<Node<'tree>> {
field_names
.iter()
.find_map(|field| node.child_by_field_name(field))
}
fn assignment_source_values(
source_name: Option<&String>,
source_call_args: &[String],
source_names: &[String],
) -> Vec<String> {
let mut out = Vec::new();
if let Some(source_name) = source_name {
out.extend(name_variants(source_name));
}
for source in source_call_args.iter().chain(source_names.iter()) {
out.extend(name_variants(source));
}
out.sort();
out.dedup();
out
}
fn name_variants(name: &str) -> Vec<String> {
let trimmed = name.trim();
if trimmed.is_empty() {
return Vec::new();
}
let stripped = bonsai_common::trim_leading_name_punctuation(trimmed);
if stripped == trimmed || stripped.is_empty() {
vec![trimmed.to_string()]
} else {
vec![trimmed.to_string(), stripped.to_string()]
}
}
fn place_base_matches(place: &str, expected_base: &str) -> bool {
let Some(base) = normalised_place_base(place) else {
return false;
};
name_variants(expected_base)
.iter()
.any(|candidate| candidate == &base)
}
fn normalised_place_base(place: &str) -> Option<String> {
let place = place.trim();
if place.is_empty() {
return None;
}
let base = place
.split('.')
.next()
.map(str::trim)
.filter(|base| !base.is_empty())?;
Some(base.to_string())
}