use rustc_hash::{FxHashMap, FxHashSet};
use shuck_ast::Span;
use smallvec::SmallVec;
use crate::cfg::{
CommandConditionRole, CommandId, CommandKind, FlowContext, RecordedCommandKind,
RecordedListOperator, RecordedPipelineOperatorKind, RecordedProgram,
};
use crate::{ScopeId, SemanticModel, SpanKey};
#[derive(Debug)]
pub(crate) struct CommandTopology {
pub(crate) syntax_backed_ids: Vec<CommandId>,
pub(crate) structural_ids: Vec<CommandId>,
pub(crate) contexts: Vec<Option<SemanticCommandContext>>,
pub(crate) ids_by_syntax_span: FxHashMap<SpanKey, SmallVec<[CommandId; 1]>>,
pub(crate) parent_ids: Vec<Option<CommandId>>,
pub(crate) child_ids: Vec<Vec<CommandId>>,
pub(crate) syntax_backed_parent_ids: Vec<Option<CommandId>>,
pub(crate) syntax_backed_child_ids: Vec<Vec<CommandId>>,
pub(crate) syntax_containing_offset_entries: Vec<CommandContainingOffsetEntry>,
pub(crate) containing_offset_entries: Vec<CommandContainingOffsetEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SemanticCommandContext {
id: CommandId,
span: Span,
syntax_span: Span,
kind: CommandKind,
scope: ScopeId,
flow: FlowContext,
structural: bool,
nested_word_command: bool,
nested_word_command_depth: usize,
in_if_condition: bool,
in_elif_condition: bool,
condition_role: Option<CommandConditionRole>,
}
impl SemanticCommandContext {
pub fn id(&self) -> CommandId {
self.id
}
pub fn span(&self) -> Span {
self.span
}
pub fn syntax_span(&self) -> Span {
self.syntax_span
}
pub fn kind(&self) -> CommandKind {
self.kind
}
pub fn scope(&self) -> ScopeId {
self.scope
}
pub fn flow(&self) -> FlowContext {
self.flow
}
pub fn is_structural(&self) -> bool {
self.structural
}
pub fn is_nested_word_command(&self) -> bool {
self.nested_word_command
}
pub fn nested_word_command_depth(&self) -> usize {
self.nested_word_command_depth
}
pub fn is_in_if_condition(&self) -> bool {
self.in_if_condition
}
pub fn is_in_elif_condition(&self) -> bool {
self.in_elif_condition
}
pub fn condition_role(&self) -> Option<CommandConditionRole> {
self.condition_role
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct CommandContainingOffsetEntry {
start_offset: usize,
end_offset: usize,
id: CommandId,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum CommandContainingOffsetEventKind {
End,
Start,
}
#[derive(Debug, Clone, Copy)]
enum CommandContainingOffsetSpan {
Syntax,
Statement,
}
#[derive(Debug, Clone, Copy)]
struct CommandContainingOffsetEvent {
offset: usize,
end_offset: usize,
id: CommandId,
kind: CommandContainingOffsetEventKind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SemanticListCommand {
pub span: Span,
pub segments: Box<[SemanticListSegment]>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SemanticListSegment {
pub command_span: Span,
pub operator_before: Option<SemanticListOperator>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SemanticListOperator {
pub kind: SemanticListOperatorKind,
pub span: Span,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SemanticListOperatorKind {
And,
Or,
}
#[derive(Debug, Clone, Copy)]
struct RecordedListOperatorWithSpan {
operator: RecordedListOperator,
span: Span,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SemanticPipelineCommand {
pub span: Span,
pub segments: Box<[SemanticPipelineSegment]>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SemanticPipelineSegment {
pub command_span: Span,
pub operator_before: Option<SemanticPipelineOperator>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SemanticPipelineOperator {
pub kind: SemanticPipelineOperatorKind,
pub span: Span,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SemanticPipelineOperatorKind {
Pipe,
PipeAll,
}
impl SemanticModel {
pub fn commands(&self) -> &[CommandId] {
&self.command_topology().syntax_backed_ids
}
pub fn structural_commands(&self) -> &[CommandId] {
&self.command_topology().structural_ids
}
pub fn command_context(&self, id: CommandId) -> Option<&SemanticCommandContext> {
self.command_topology()
.contexts
.get(id.index())
.and_then(Option::as_ref)
}
pub fn command_contexts(&self) -> impl Iterator<Item = &SemanticCommandContext> {
self.command_topology()
.contexts
.iter()
.filter_map(Option::as_ref)
}
pub fn structural_command_contexts(&self) -> impl Iterator<Item = &SemanticCommandContext> {
self.command_contexts()
.filter(|context| context.is_structural())
}
pub fn command_condition_role(&self, id: CommandId) -> Option<CommandConditionRole> {
self.command_context(id)
.and_then(SemanticCommandContext::condition_role)
}
pub fn command_is_nested_word_command(&self, id: CommandId) -> bool {
self.command_context(id)
.is_some_and(SemanticCommandContext::is_nested_word_command)
}
pub fn command_span(&self, id: CommandId) -> Span {
self.recorded_program.command(id).span
}
pub fn command_syntax_span(&self, id: CommandId) -> Span {
self.recorded_program.command(id).syntax_span
}
pub fn command_kind(&self, id: CommandId) -> CommandKind {
self.command_syntax_kind(id)
.expect("semantic command syntax kind is recorded")
}
pub fn command_by_span(&self, span: Span) -> Option<CommandId> {
self.command_topology()
.ids_by_syntax_span
.get(&SpanKey::new(span))
.and_then(|ids| {
ids.iter()
.copied()
.find(|id| self.command_syntax_kind(*id).is_some())
})
}
pub fn command_by_span_and_kind(&self, span: Span, kind: CommandKind) -> Option<CommandId> {
self.command_topology()
.ids_by_syntax_span
.get(&SpanKey::new(span))
.and_then(|ids| {
ids.iter()
.copied()
.find(|id| self.command_syntax_kind(*id) == Some(kind))
})
}
pub fn command_parent_id(&self, id: CommandId) -> Option<CommandId> {
self.command_topology().parent_ids[id.index()]
}
pub fn command_children(&self, id: CommandId) -> &[CommandId] {
&self.command_topology().child_ids[id.index()]
}
pub fn syntax_backed_command_parent_id(&self, id: CommandId) -> Option<CommandId> {
self.command_topology().syntax_backed_parent_ids[id.index()]
}
pub fn syntax_backed_command_children(&self, id: CommandId) -> &[CommandId] {
&self.command_topology().syntax_backed_child_ids[id.index()]
}
pub fn innermost_command_id_at(&self, offset: usize) -> Option<CommandId> {
let topology = self.command_topology();
innermost_command_id_in_containing_offset_entries(
&topology.syntax_containing_offset_entries,
offset,
)
}
pub fn innermost_command_id_containing_offset(&self, offset: usize) -> Option<CommandId> {
let topology = self.command_topology();
innermost_command_id_in_containing_offset_entries(
&topology.containing_offset_entries,
offset,
)
}
pub fn list_commands(&self) -> Vec<SemanticListCommand> {
self.recorded_program
.commands()
.iter()
.enumerate()
.filter_map(|(index, command)| {
let RecordedCommandKind::List { first, rest } = command.kind else {
return None;
};
let command_id = CommandId(index as u32);
if self.command_parent_id(command_id).is_some_and(|parent| {
matches!(
self.recorded_program.command(parent).kind,
RecordedCommandKind::List { .. }
)
}) {
return None;
}
let mut segments = Vec::new();
self.flatten_list_segment(first, None, &mut segments);
for item in self.recorded_program.list_items(rest) {
self.flatten_list_segment(
item.command,
Some(RecordedListOperatorWithSpan {
operator: item.operator,
span: item.operator_span,
}),
&mut segments,
);
}
Some(SemanticListCommand {
span: command.span,
segments: segments.into_boxed_slice(),
})
})
.collect()
}
fn flatten_list_segment(
&self,
command: CommandId,
operator_before: Option<RecordedListOperatorWithSpan>,
out: &mut Vec<SemanticListSegment>,
) {
if let RecordedCommandKind::List { first, rest } =
self.recorded_program.command(command).kind
{
self.flatten_list_segment(first, operator_before, out);
for item in self.recorded_program.list_items(rest) {
self.flatten_list_segment(
item.command,
Some(RecordedListOperatorWithSpan {
operator: item.operator,
span: item.operator_span,
}),
out,
);
}
return;
}
out.push(SemanticListSegment {
command_span: self.recorded_program.command(command).span,
operator_before: operator_before.map(|operator| SemanticListOperator {
kind: match operator.operator {
RecordedListOperator::And => SemanticListOperatorKind::And,
RecordedListOperator::Or => SemanticListOperatorKind::Or,
},
span: operator.span,
}),
});
}
pub fn pipeline_commands(&self) -> Vec<SemanticPipelineCommand> {
self.recorded_program
.commands()
.iter()
.enumerate()
.filter_map(|(index, command)| {
let RecordedCommandKind::Pipeline { segments } = command.kind else {
return None;
};
let command_id = CommandId(index as u32);
if self.command_parent_id(command_id).is_some_and(|parent| {
matches!(
self.recorded_program.command(parent).kind,
RecordedCommandKind::Pipeline { .. }
)
}) {
return None;
}
let mut flattened = Vec::new();
for segment in self.recorded_program.pipeline_segments(segments) {
self.flatten_pipeline_segment(
segment.command,
segment.operator_before,
&mut flattened,
);
}
Some(SemanticPipelineCommand {
span: command.span,
segments: flattened.into_boxed_slice(),
})
})
.collect()
}
fn flatten_pipeline_segment(
&self,
command: CommandId,
operator_before: Option<crate::cfg::RecordedPipelineOperator>,
out: &mut Vec<SemanticPipelineSegment>,
) {
if let RecordedCommandKind::Pipeline { segments } =
self.recorded_program.command(command).kind
{
for (index, segment) in self
.recorded_program
.pipeline_segments(segments)
.iter()
.enumerate()
{
let operator = if index == 0 {
operator_before
} else {
segment.operator_before
};
self.flatten_pipeline_segment(segment.command, operator, out);
}
return;
}
out.push(SemanticPipelineSegment {
command_span: self.recorded_program.command(command).span,
operator_before: operator_before.map(|operator| SemanticPipelineOperator {
kind: match operator.operator {
RecordedPipelineOperatorKind::Pipe => SemanticPipelineOperatorKind::Pipe,
RecordedPipelineOperatorKind::PipeAll => SemanticPipelineOperatorKind::PipeAll,
},
span: operator.span,
}),
});
}
fn command_topology(&self) -> &CommandTopology {
self.command_topology
.get_or_init(|| build_command_topology(self))
}
pub(crate) fn command_syntax_kind(&self, id: CommandId) -> Option<CommandKind> {
self.recorded_program.command(id).syntax_kind
}
}
fn build_command_topology(model: &SemanticModel) -> CommandTopology {
let program = model.recorded_program();
let command_count = program.commands().len();
let ids = (0..command_count)
.map(|index| CommandId(index as u32))
.collect::<Vec<_>>();
let mut ids_by_syntax_span = FxHashMap::<SpanKey, SmallVec<[CommandId; 1]>>::default();
let mut parent_ids = vec![None; command_count];
let mut child_ids = vec![Vec::new(); command_count];
let mut nested_region_command_ids = FxHashSet::default();
let mut nested_region_root_command_ids = FxHashSet::default();
for id in ids.iter().copied() {
let command = program.command(id);
ids_by_syntax_span
.entry(SpanKey::new(command.syntax_span))
.or_default()
.push(id);
record_command_children(
program,
id,
&mut parent_ids,
&mut child_ids,
&mut nested_region_command_ids,
&mut nested_region_root_command_ids,
);
}
let structural_parent_ids = parent_ids.clone();
let structural_child_ids = child_ids.clone();
attach_function_body_commands(model, &ids, &mut parent_ids, &mut child_ids);
attach_containing_command_parents(model, &ids, &mut parent_ids, &mut child_ids);
let nested_region_depths = build_nested_region_depths(
command_count,
&parent_ids,
&child_ids,
&nested_region_root_command_ids,
);
let mut structural_ids = ids
.iter()
.copied()
.filter(|id| !nested_region_command_ids.contains(id))
.collect::<Vec<_>>();
structural_ids
.sort_unstable_by(|left, right| compare_command_ids_by_syntax_span(model, *left, *right));
let syntax_backed_ids = ids
.into_iter()
.filter(|id| program.command(*id).syntax_kind.is_some())
.collect::<Vec<_>>();
let syntax_backed_parent_ids =
build_syntax_backed_command_parent_ids(model, &syntax_backed_ids, &parent_ids);
let syntax_backed_child_ids =
build_command_child_ids(command_count, &syntax_backed_ids, &syntax_backed_parent_ids);
let syntax_containing_offset_entries = build_command_containing_offset_entries(
model,
&syntax_backed_ids,
CommandContainingOffsetSpan::Syntax,
);
let containing_offset_entries = build_command_containing_offset_entries(
model,
&syntax_backed_ids,
CommandContainingOffsetSpan::Statement,
);
let condition_contexts = build_command_condition_contexts(
program,
command_count,
&structural_parent_ids,
&structural_child_ids,
);
let contexts = build_command_contexts(
model,
&syntax_backed_ids,
&structural_ids,
&nested_region_command_ids,
&nested_region_depths,
&condition_contexts,
);
CommandTopology {
syntax_backed_ids,
structural_ids,
contexts,
ids_by_syntax_span,
parent_ids,
child_ids,
syntax_backed_parent_ids,
syntax_backed_child_ids,
syntax_containing_offset_entries,
containing_offset_entries,
}
}
fn build_command_contexts(
model: &SemanticModel,
syntax_backed_ids: &[CommandId],
structural_ids: &[CommandId],
nested_region_command_ids: &FxHashSet<CommandId>,
nested_region_depths: &[usize],
condition_contexts: &[CommandConditionContext],
) -> Vec<Option<SemanticCommandContext>> {
let program = model.recorded_program();
let structural_ids = structural_ids.iter().copied().collect::<FxHashSet<_>>();
let mut contexts = vec![None; program.commands().len()];
for id in syntax_backed_ids.iter().copied() {
let command = program.command(id);
let Some(kind) = command.syntax_kind else {
continue;
};
let Some(scope) = command.scope else {
continue;
};
let Some(flow) = command.flow_context else {
continue;
};
contexts[id.index()] = Some(SemanticCommandContext {
id,
span: command.span,
syntax_span: command.syntax_span,
kind,
scope,
flow,
structural: structural_ids.contains(&id),
nested_word_command: nested_region_command_ids.contains(&id),
nested_word_command_depth: nested_region_depths[id.index()],
in_if_condition: condition_contexts
.get(id.index())
.is_some_and(|context| context.in_if_condition),
in_elif_condition: condition_contexts
.get(id.index())
.is_some_and(|context| context.in_elif_condition),
condition_role: condition_contexts
.get(id.index())
.and_then(|context| context.role),
});
}
contexts
}
#[derive(Debug, Clone, Copy, Default)]
struct CommandConditionContext {
role: Option<CommandConditionRole>,
in_if_condition: bool,
in_elif_condition: bool,
}
fn build_command_condition_contexts(
program: &RecordedProgram,
command_count: usize,
parent_ids: &[Option<CommandId>],
child_ids: &[Vec<CommandId>],
) -> Vec<CommandConditionContext> {
let mut starts = vec![SmallVec::<[ConditionAssignment; 1]>::new(); command_count];
let mut contexts = vec![CommandConditionContext::default(); command_count];
for id in (0..command_count).map(|index| CommandId(index as u32)) {
record_command_condition_starts(program, id, &mut starts);
}
let mut visited = vec![false; command_count];
for id in (0..command_count).map(|index| CommandId(index as u32)) {
if parent_ids[id.index()].is_none() {
propagate_command_condition_contexts(
id,
CommandConditionContext::default(),
&starts,
child_ids,
&mut contexts,
&mut visited,
);
}
}
for id in (0..command_count).map(|index| CommandId(index as u32)) {
if !visited[id.index()] {
propagate_command_condition_contexts(
id,
CommandConditionContext::default(),
&starts,
child_ids,
&mut contexts,
&mut visited,
);
}
}
contexts
}
fn record_command_condition_starts(
program: &RecordedProgram,
id: CommandId,
starts: &mut [SmallVec<[ConditionAssignment; 1]>],
) {
match program.command(id).kind {
RecordedCommandKind::If {
condition,
elif_branches,
..
} => {
record_condition_range_starts(
program,
condition,
ConditionAssignment {
role: CommandConditionRole::If,
in_if_condition: true,
in_elif_condition: false,
},
starts,
);
for branch in program.elif_branches(elif_branches) {
record_condition_range_starts(
program,
branch.condition,
ConditionAssignment {
role: CommandConditionRole::Elif,
in_if_condition: true,
in_elif_condition: true,
},
starts,
);
}
}
RecordedCommandKind::While { condition, .. } => {
record_condition_range_starts(
program,
condition,
ConditionAssignment {
role: CommandConditionRole::While,
in_if_condition: false,
in_elif_condition: false,
},
starts,
);
}
RecordedCommandKind::Until { condition, .. } => {
record_condition_range_starts(
program,
condition,
ConditionAssignment {
role: CommandConditionRole::Until,
in_if_condition: false,
in_elif_condition: false,
},
starts,
);
}
RecordedCommandKind::Linear
| RecordedCommandKind::Break { .. }
| RecordedCommandKind::Continue { .. }
| RecordedCommandKind::Return
| RecordedCommandKind::Exit
| RecordedCommandKind::List { .. }
| RecordedCommandKind::For { .. }
| RecordedCommandKind::Select { .. }
| RecordedCommandKind::ArithmeticFor { .. }
| RecordedCommandKind::Case { .. }
| RecordedCommandKind::BraceGroup { .. }
| RecordedCommandKind::Always { .. }
| RecordedCommandKind::Subshell { .. }
| RecordedCommandKind::Pipeline { .. } => {}
}
}
#[derive(Debug, Clone, Copy)]
struct ConditionAssignment {
role: CommandConditionRole,
in_if_condition: bool,
in_elif_condition: bool,
}
fn record_condition_range_starts(
program: &RecordedProgram,
range: crate::cfg::RecordedCommandRange,
assignment: ConditionAssignment,
starts: &mut [SmallVec<[ConditionAssignment; 1]>],
) {
for command in program.commands_in(range).iter().copied() {
starts[command.index()].push(assignment);
}
}
fn propagate_command_condition_contexts(
root: CommandId,
inherited: CommandConditionContext,
starts: &[SmallVec<[ConditionAssignment; 1]>],
child_ids: &[Vec<CommandId>],
contexts: &mut [CommandConditionContext],
visited: &mut [bool],
) {
let mut stack = vec![(root, inherited)];
while let Some((id, mut context)) = stack.pop() {
if visited[id.index()] {
continue;
}
visited[id.index()] = true;
for assignment in &starts[id.index()] {
context.role = Some(assignment.role);
context.in_if_condition |= assignment.in_if_condition;
context.in_elif_condition |= assignment.in_elif_condition;
}
contexts[id.index()] = context;
for child in child_ids[id.index()].iter().rev().copied() {
stack.push((child, context));
}
}
}
fn build_syntax_backed_command_parent_ids(
model: &SemanticModel,
syntax_backed_ids: &[CommandId],
parent_ids: &[Option<CommandId>],
) -> Vec<Option<CommandId>> {
let mut syntax_backed_parent_ids = vec![None; parent_ids.len()];
for id in syntax_backed_ids.iter().copied() {
let mut current = parent_ids[id.index()];
while let Some(parent) = current {
if model.command_syntax_kind(parent).is_some() {
syntax_backed_parent_ids[id.index()] = Some(parent);
break;
}
current = parent_ids[parent.index()];
}
}
syntax_backed_parent_ids
}
fn build_command_child_ids(
command_count: usize,
command_ids: &[CommandId],
parent_ids: &[Option<CommandId>],
) -> Vec<Vec<CommandId>> {
let mut child_ids = vec![Vec::new(); command_count];
for child in command_ids.iter().copied() {
if let Some(parent) = parent_ids[child.index()] {
child_ids[parent.index()].push(child);
}
}
child_ids
}
fn build_command_containing_offset_entries(
model: &SemanticModel,
syntax_backed_ids: &[CommandId],
span_kind: CommandContainingOffsetSpan,
) -> Vec<CommandContainingOffsetEntry> {
let mut events = syntax_backed_ids
.iter()
.copied()
.flat_map(|id| {
let span = match span_kind {
CommandContainingOffsetSpan::Syntax => model.command_syntax_span(id),
CommandContainingOffsetSpan::Statement => model.command_span(id),
};
[
CommandContainingOffsetEvent {
offset: span.start.offset,
end_offset: span.end.offset,
id,
kind: CommandContainingOffsetEventKind::Start,
},
CommandContainingOffsetEvent {
offset: span.end.offset.saturating_add(1),
end_offset: span.end.offset,
id,
kind: CommandContainingOffsetEventKind::End,
},
]
})
.collect::<Vec<_>>();
events.sort_unstable_by(|left, right| {
left.offset
.cmp(&right.offset)
.then_with(|| left.kind.cmp(&right.kind))
.then_with(|| right.end_offset.cmp(&left.end_offset))
.then_with(|| left.id.index().cmp(&right.id.index()))
});
let mut entries = Vec::new();
let mut active = Vec::<CommandId>::new();
let mut index = 0;
while let Some(event) = events.get(index).copied() {
let offset = event.offset;
while events.get(index).is_some_and(|event| {
event.offset == offset && event.kind == CommandContainingOffsetEventKind::End
}) {
let id = events[index].id;
active.retain(|active_id| *active_id != id);
index += 1;
}
while events.get(index).is_some_and(|event| {
event.offset == offset && event.kind == CommandContainingOffsetEventKind::Start
}) {
active.push(events[index].id);
index += 1;
}
let Some(next_offset) = events.get(index).map(|event| event.offset) else {
break;
};
if offset < next_offset
&& let Some(id) = active.last().copied()
{
push_command_containing_offset_entry(&mut entries, offset, next_offset - 1, id);
}
}
entries
}
pub(crate) fn innermost_command_id_in_containing_offset_entries(
entries: &[CommandContainingOffsetEntry],
offset: usize,
) -> Option<CommandId> {
let upper_bound = entries.partition_point(|entry| entry.start_offset <= offset);
let entry = entries.get(upper_bound.checked_sub(1)?)?;
(offset <= entry.end_offset).then_some(entry.id)
}
fn push_command_containing_offset_entry(
entries: &mut Vec<CommandContainingOffsetEntry>,
start_offset: usize,
end_offset: usize,
id: CommandId,
) {
if let Some(last) = entries.last_mut()
&& last.id == id
&& last.end_offset.saturating_add(1) == start_offset
{
last.end_offset = end_offset;
return;
}
entries.push(CommandContainingOffsetEntry {
start_offset,
end_offset,
id,
});
}
fn attach_containing_command_parents(
model: &SemanticModel,
command_ids: &[CommandId],
parent_ids: &mut [Option<CommandId>],
child_ids: &mut [Vec<CommandId>],
) {
let mut sorted = command_ids.to_vec();
sorted.sort_unstable_by(|left, right| compare_command_ids_by_syntax_span(model, *left, *right));
let mut stack = Vec::<CommandId>::new();
for child in sorted {
let child_span = model.command_syntax_span(child);
while stack.last().is_some_and(|candidate| {
!contains_command_span(model.command_syntax_span(*candidate), child_span)
}) {
stack.pop();
}
if parent_ids[child.index()].is_none()
&& let Some(parent) = stack.iter().rev().copied().find(|candidate| {
*candidate != child
&& contains_command_span(model.command_syntax_span(*candidate), child_span)
&& !would_create_command_parent_cycle(*candidate, child, parent_ids)
})
{
assign_command_parent(parent, child, parent_ids, child_ids);
}
stack.push(child);
}
}
fn record_command_children(
program: &RecordedProgram,
parent: CommandId,
parent_ids: &mut [Option<CommandId>],
child_ids: &mut [Vec<CommandId>],
nested_region_command_ids: &mut FxHashSet<CommandId>,
nested_region_root_command_ids: &mut FxHashSet<CommandId>,
) {
let command = program.command(parent);
for region in program.nested_regions(command.nested_regions) {
for child in commands_in_range_recursive(program, region.commands) {
nested_region_command_ids.insert(child);
}
for child in program.commands_in(region.commands).iter().copied() {
nested_region_root_command_ids.insert(child);
assign_command_parent(parent, child, parent_ids, child_ids);
}
}
match command.kind {
RecordedCommandKind::Linear
| RecordedCommandKind::Break { .. }
| RecordedCommandKind::Continue { .. }
| RecordedCommandKind::Return
| RecordedCommandKind::Exit => {}
RecordedCommandKind::List { first, rest } => {
assign_command_parent(parent, first, parent_ids, child_ids);
for item in program.list_items(rest) {
assign_command_parent(parent, item.command, parent_ids, child_ids);
}
}
RecordedCommandKind::If {
condition,
then_branch,
elif_branches,
else_branch,
} => {
assign_range_parent(program, parent, condition, parent_ids, child_ids);
assign_range_parent(program, parent, then_branch, parent_ids, child_ids);
for branch in program.elif_branches(elif_branches) {
assign_range_parent(program, parent, branch.condition, parent_ids, child_ids);
assign_range_parent(program, parent, branch.body, parent_ids, child_ids);
}
assign_range_parent(program, parent, else_branch, parent_ids, child_ids);
}
RecordedCommandKind::While { condition, body }
| RecordedCommandKind::Until { condition, body } => {
assign_range_parent(program, parent, condition, parent_ids, child_ids);
assign_range_parent(program, parent, body, parent_ids, child_ids);
}
RecordedCommandKind::For { body }
| RecordedCommandKind::Select { body }
| RecordedCommandKind::ArithmeticFor { body }
| RecordedCommandKind::BraceGroup { body }
| RecordedCommandKind::Subshell { body } => {
assign_range_parent(program, parent, body, parent_ids, child_ids);
}
RecordedCommandKind::Always { body, always_body } => {
assign_range_parent(program, parent, body, parent_ids, child_ids);
assign_range_parent(program, parent, always_body, parent_ids, child_ids);
}
RecordedCommandKind::Case { arms } => {
for arm in program.case_arms(arms) {
assign_range_parent(program, parent, arm.commands, parent_ids, child_ids);
}
}
RecordedCommandKind::Pipeline { segments } => {
for segment in program.pipeline_segments(segments) {
assign_command_parent(parent, segment.command, parent_ids, child_ids);
}
}
}
}
fn assign_range_parent(
program: &RecordedProgram,
parent: CommandId,
range: crate::cfg::RecordedCommandRange,
parent_ids: &mut [Option<CommandId>],
child_ids: &mut [Vec<CommandId>],
) {
for child in program.commands_in(range).iter().copied() {
assign_command_parent(parent, child, parent_ids, child_ids);
}
}
fn assign_command_parent(
parent: CommandId,
child: CommandId,
parent_ids: &mut [Option<CommandId>],
child_ids: &mut [Vec<CommandId>],
) {
if parent != child
&& parent_ids[child.index()].is_none()
&& !would_create_command_parent_cycle(parent, child, parent_ids)
{
parent_ids[child.index()] = Some(parent);
child_ids[parent.index()].push(child);
}
}
fn would_create_command_parent_cycle(
parent: CommandId,
child: CommandId,
parent_ids: &[Option<CommandId>],
) -> bool {
let mut current = Some(parent);
while let Some(id) = current {
if id == child {
return true;
}
current = parent_ids[id.index()];
}
false
}
fn build_nested_region_depths(
command_count: usize,
parent_ids: &[Option<CommandId>],
child_ids: &[Vec<CommandId>],
nested_region_root_command_ids: &FxHashSet<CommandId>,
) -> Vec<usize> {
let mut depths = vec![0; command_count];
let mut stack = Vec::new();
for index in (0..command_count).rev() {
let id = CommandId(index as u32);
if parent_ids[id.index()].is_none() {
stack.push((id, 0));
}
}
while let Some((id, depth)) = stack.pop() {
depths[id.index()] = depth;
for child in child_ids[id.index()].iter().rev().copied() {
let child_depth = if nested_region_root_command_ids.contains(&child) {
depth + 1
} else {
depth
};
stack.push((child, child_depth));
}
}
depths
}
fn commands_in_range_recursive(
program: &RecordedProgram,
range: crate::cfg::RecordedCommandRange,
) -> Vec<CommandId> {
let mut commands = Vec::new();
for command in program.commands_in(range).iter().copied() {
commands.push(command);
commands.extend(command_descendants(program, command));
}
commands
}
fn command_descendants(program: &RecordedProgram, command: CommandId) -> Vec<CommandId> {
let mut descendants = Vec::new();
let command = program.command(command);
for region in program.nested_regions(command.nested_regions) {
descendants.extend(commands_in_range_recursive(program, region.commands));
}
match command.kind {
RecordedCommandKind::Linear
| RecordedCommandKind::Break { .. }
| RecordedCommandKind::Continue { .. }
| RecordedCommandKind::Return
| RecordedCommandKind::Exit => {}
RecordedCommandKind::List { first, rest } => {
descendants.push(first);
descendants.extend(command_descendants(program, first));
for item in program.list_items(rest) {
descendants.push(item.command);
descendants.extend(command_descendants(program, item.command));
}
}
RecordedCommandKind::If {
condition,
then_branch,
elif_branches,
else_branch,
} => {
descendants.extend(commands_in_range_recursive(program, condition));
descendants.extend(commands_in_range_recursive(program, then_branch));
for branch in program.elif_branches(elif_branches) {
descendants.extend(commands_in_range_recursive(program, branch.condition));
descendants.extend(commands_in_range_recursive(program, branch.body));
}
descendants.extend(commands_in_range_recursive(program, else_branch));
}
RecordedCommandKind::While { condition, body }
| RecordedCommandKind::Until { condition, body } => {
descendants.extend(commands_in_range_recursive(program, condition));
descendants.extend(commands_in_range_recursive(program, body));
}
RecordedCommandKind::For { body }
| RecordedCommandKind::Select { body }
| RecordedCommandKind::ArithmeticFor { body }
| RecordedCommandKind::BraceGroup { body }
| RecordedCommandKind::Subshell { body } => {
descendants.extend(commands_in_range_recursive(program, body));
}
RecordedCommandKind::Always { body, always_body } => {
descendants.extend(commands_in_range_recursive(program, body));
descendants.extend(commands_in_range_recursive(program, always_body));
}
RecordedCommandKind::Case { arms } => {
for arm in program.case_arms(arms) {
descendants.extend(commands_in_range_recursive(program, arm.commands));
}
}
RecordedCommandKind::Pipeline { segments } => {
for segment in program.pipeline_segments(segments) {
descendants.push(segment.command);
descendants.extend(command_descendants(program, segment.command));
}
}
}
descendants
}
fn attach_function_body_commands(
model: &SemanticModel,
command_ids: &[CommandId],
parent_ids: &mut [Option<CommandId>],
child_ids: &mut [Vec<CommandId>],
) {
let mut function_ids = command_ids
.iter()
.copied()
.filter(|id| model.command_syntax_kind(*id) == Some(CommandKind::Function))
.collect::<Vec<_>>();
if function_ids.is_empty() {
return;
}
function_ids
.sort_unstable_by(|left, right| compare_command_ids_by_syntax_span(model, *left, *right));
let mut body_children = Vec::new();
for body in model.recorded_program.function_bodies().values().copied() {
for child in model.recorded_program.commands_in(body).iter().copied() {
body_children.push(child);
}
}
body_children
.sort_unstable_by(|left, right| compare_command_ids_by_syntax_span(model, *left, *right));
body_children.dedup();
let mut active_functions = Vec::new();
let mut next_function = 0usize;
for child in body_children {
if parent_ids[child.index()].is_some() {
continue;
}
let child_span = model.command_syntax_span(child);
while let Some(function_id) = function_ids.get(next_function).copied() {
let function_span = model.command_syntax_span(function_id);
if function_span.start.offset > child_span.start.offset {
break;
}
while active_functions.last().is_some_and(|active| {
!contains_command_span(model.command_syntax_span(*active), function_span)
}) {
active_functions.pop();
}
active_functions.push(function_id);
next_function += 1;
}
while active_functions.last().is_some_and(|active| {
!contains_command_span(model.command_syntax_span(*active), child_span)
}) {
active_functions.pop();
}
if let Some(parent) = active_functions.iter().rev().copied().find(|candidate| {
*candidate != child
&& contains_command_span(model.command_syntax_span(*candidate), child_span)
}) {
assign_command_parent(parent, child, parent_ids, child_ids);
}
}
}
fn contains_command_span(outer: Span, inner: Span) -> bool {
outer.start.offset <= inner.start.offset && inner.end.offset <= outer.end.offset
}
fn compare_command_ids_by_syntax_span(
model: &SemanticModel,
left: CommandId,
right: CommandId,
) -> std::cmp::Ordering {
let left_span = model.command_syntax_span(left);
let right_span = model.command_syntax_span(right);
left_span
.start
.offset
.cmp(&right_span.start.offset)
.then_with(|| right_span.end.offset.cmp(&left_span.end.offset))
.then_with(|| right.index().cmp(&left.index()))
}