use super::*;
pub(crate) struct LinterFactsBuilder<'a, 'analysis> {
file: &'a File,
source: &'a str,
semantic_artifacts: &'a LinterSemanticArtifacts<'a>,
semantic: &'a SemanticModel,
semantic_analysis: &'analysis SemanticAnalysis<'a>,
_indexer: &'a Indexer,
command_visits_by_id: &'a [Option<CommandVisit<'a>>],
shell: ShellDialect,
ambient_shell_options: AmbientShellOptions,
}
#[derive(Debug, Default)]
pub(crate) struct FactBuildCapacity {
commands: usize,
functions: usize,
word_nodes: usize,
word_occurrences: usize,
word_occurrences_per_command: usize,
compound_assignment_values: usize,
}
#[derive(Debug, Default)]
pub(crate) struct ArithmeticFactSummary {
pub(crate) array_index_arithmetic_spans: Vec<Span>,
pub(crate) arithmetic_score_line_spans: Vec<Span>,
pub(crate) dollar_in_arithmetic_spans: Vec<Span>,
pub(crate) arithmetic_expansion_spans: Vec<Span>,
pub(crate) arithmetic_index_subscript_spans: Vec<Span>,
pub(crate) arithmetic_command_substitution_spans: Vec<Span>,
pub(crate) arithmetic_only_suppressed_subscript_spans: Vec<Span>,
}
#[derive(Debug, Default)]
pub(crate) struct HeredocFactSummary {
pub(crate) unused_heredoc_spans: Vec<Span>,
pub(crate) heredoc_missing_end_spans: Vec<Span>,
pub(crate) heredoc_closer_not_alone_spans: Vec<Span>,
pub(crate) misquoted_heredoc_close_spans: Vec<Span>,
pub(crate) heredoc_end_space_spans: Vec<Span>,
pub(crate) echo_here_doc_spans: Vec<Span>,
pub(crate) spaced_tabstrip_close_spans: Vec<Span>,
}
#[cfg_attr(shuck_profiling, inline(never))]
pub(crate) fn estimate_fact_build_capacity(
semantic: &SemanticModel,
command_visits_by_id: &[Option<CommandVisit<'_>>],
) -> FactBuildCapacity {
let commands = semantic.command_count();
let mut direct_words = 0usize;
let mut maximum_direct_words = 0usize;
let mut compound_assignment_values = 0usize;
for id in semantic.commands_in_source_order().iter().copied() {
let Some(visit) = command_visits_by_id
.get(id.index())
.and_then(|visit| *visit)
else {
continue;
};
let (command_words, command_compound_assignment_values) =
estimate_command_word_capacity(visit.command, visit.redirects);
direct_words = direct_words.saturating_add(command_words);
maximum_direct_words = maximum_direct_words.max(command_words);
compound_assignment_values =
compound_assignment_values.saturating_add(command_compound_assignment_values);
}
let references = semantic.references().len();
let average_references = references.div_ceil(commands.max(1));
FactBuildCapacity {
commands,
functions: commands.saturating_div(8),
word_nodes: direct_words.saturating_add(references),
word_occurrences: direct_words.saturating_add(references.saturating_mul(2)),
word_occurrences_per_command: maximum_direct_words
.saturating_add(average_references.saturating_mul(2)),
compound_assignment_values,
}
}
fn estimate_command_word_capacity(command: &Command, redirects: &[Redirect]) -> (usize, usize) {
let mut words = redirects
.iter()
.filter(|redirect| redirect.word_target().is_some())
.count();
words = words.saturating_add(match command {
Command::Simple(command) => 1usize.saturating_add(command.args.len()),
Command::Builtin(command) => match command {
BuiltinCommand::Break(command) => {
usize::from(command.depth.is_some()).saturating_add(command.extra_args.len())
}
BuiltinCommand::Continue(command) => {
usize::from(command.depth.is_some()).saturating_add(command.extra_args.len())
}
BuiltinCommand::Return(command) => {
usize::from(command.code.is_some()).saturating_add(command.extra_args.len())
}
BuiltinCommand::Exit(command) => {
usize::from(command.code.is_some()).saturating_add(command.extra_args.len())
}
},
Command::Decl(command) => command
.operands
.iter()
.filter(|operand| matches!(operand, DeclOperand::Dynamic(_)))
.count(),
Command::Compound(command) => match command {
CompoundCommand::For(command) => command.words.as_ref().map_or(0, Vec::len),
CompoundCommand::Repeat(_) => 1,
CompoundCommand::Foreach(command) => command.words.len(),
CompoundCommand::Select(command) => command.words.len(),
CompoundCommand::Case(command) => 1usize.saturating_add(
command
.cases
.iter()
.map(|case| case.patterns.len())
.sum::<usize>(),
),
CompoundCommand::If(_)
| CompoundCommand::ArithmeticFor(_)
| CompoundCommand::While(_)
| CompoundCommand::Until(_)
| CompoundCommand::Subshell(_)
| CompoundCommand::BraceGroup(_)
| CompoundCommand::Arithmetic(_)
| CompoundCommand::Time(_)
| CompoundCommand::Conditional(_)
| CompoundCommand::Coproc(_)
| CompoundCommand::Always(_) => 0,
},
Command::Function(function) => function.header.entries.len(),
Command::AnonymousFunction(function) => function.args.len(),
Command::Binary(_) => 0,
});
let mut compound_assignment_values = 0usize;
for assignment in
command_assignments(command)
.iter()
.chain(
declaration_operands(command)
.iter()
.filter_map(|operand| match operand {
DeclOperand::Assignment(assignment) => Some(assignment),
DeclOperand::Flag(_) | DeclOperand::Name(_) | DeclOperand::Dynamic(_) => {
None
}
}),
)
{
match &assignment.value {
AssignmentValue::Scalar(_) => words = words.saturating_add(1),
AssignmentValue::Compound(array) => {
words = words.saturating_add(array.elements.len());
compound_assignment_values =
compound_assignment_values.saturating_add(array.elements.len());
}
}
}
(words, compound_assignment_values)
}
impl<'a, 'analysis> LinterFactsBuilder<'a, 'analysis> {
pub(crate) fn new(
file: &'a File,
source: &'a str,
semantic: &'a LinterSemanticArtifacts<'a>,
semantic_analysis: &'analysis SemanticAnalysis<'a>,
indexer: &'a Indexer,
shell: ShellDialect,
ambient_shell_options: AmbientShellOptions,
) -> Self {
Self {
file,
source,
semantic_artifacts: semantic,
semantic: semantic.semantic(),
semantic_analysis,
_indexer: indexer,
command_visits_by_id: semantic.command_visits_by_id(),
shell,
ambient_shell_options,
}
}
pub(crate) fn build(self) -> LinterFacts<'a> {
let source = self.source;
let line_index = self._indexer.line_index();
let locator = crate::Locator::new(source, line_index);
let semantic_analysis = self.semantic_analysis;
let capacity = estimate_fact_build_capacity(self.semantic, self.command_visits_by_id);
let mut commands: Vec<CommandFact<'_>> = Vec::with_capacity(self.semantic.command_count());
let mut substitution_occurrences_by_command_id: Vec<Vec<HostedSubstitutionOccurrence<'_>>> =
(0..self.semantic.command_count())
.map(|_| Vec::new())
.collect();
let mut redirect_fact_store = ListArena::new();
let mut declaration_assignment_probe_store = ListArena::new();
let mut command_ids_by_span =
CommandLookupIndex::with_capacity_and_hasher(capacity.commands, Default::default());
let mut command_ids_by_name_word_span = FxHashMap::with_capacity_and_hasher(
capacity.commands.saturating_div(2),
Default::default(),
);
let mut if_condition_command_ids =
DenseCommandIdSet::with_capacity(self.semantic.command_count());
let mut elif_condition_command_ids =
DenseCommandIdSet::with_capacity(self.semantic.command_count());
let mut binding_values =
FxHashMap::with_capacity_and_hasher(self.semantic.bindings().len(), Default::default());
let mut broken_assoc_key_spans = Vec::new();
let mut comma_array_assignment_spans = Vec::new();
let mut ifs_literal_backslash_assignment_value_spans = Vec::new();
let mut word_nodes = Vec::with_capacity(capacity.word_nodes);
let mut word_spans = ListArena::with_capacity(capacity.word_nodes.saturating_mul(2));
let mut word_span_scratch = Vec::new();
let mut word_node_ids_by_span =
FxHashMap::with_capacity_and_hasher(capacity.word_nodes, Default::default());
let mut word_occurrences = Vec::with_capacity(capacity.word_occurrences);
let mut pending_arithmetic_word_occurrences = Vec::new();
let mut pending_parameter_operand_word_occurrences = Vec::new();
let mut compound_assignment_value_word_spans = FxHashSet::with_capacity_and_hasher(
capacity.compound_assignment_values,
Default::default(),
);
let mut array_assignment_split_word_ids =
Vec::with_capacity(capacity.commands.saturating_div(8));
let mut seen_word_occurrences = FxHashSet::with_capacity_and_hasher(
capacity.word_occurrences_per_command,
Default::default(),
);
let mut seen_pending_arithmetic_word_occurrences = FxHashSet::with_capacity_and_hasher(
capacity.word_occurrences_per_command,
Default::default(),
);
let mut seen_pending_parameter_operand_word_occurrences =
FxHashSet::with_capacity_and_hasher(
capacity.word_occurrences_per_command,
Default::default(),
);
let mut assoc_binding_visibility_memo = FxHashMap::default();
let mut pattern_exactly_one_extglob_spans = Vec::new();
let mut case_pattern_expansions = Vec::new();
let mut pattern_literal_spans = Vec::new();
let mut pattern_charclass_spans = Vec::new();
let mut arithmetic_summary = ArithmeticFactSummary::default();
let mut surface_fragments = SurfaceFragmentSink::new(self.source);
let mut functions = Vec::with_capacity(capacity.functions);
let mut function_body_without_braces_spans = Vec::new();
let mut redundant_return_status_spans = Vec::new();
let mut getopts_cases = Vec::new();
let mut condition_status_capture_spans = Vec::new();
let mut precise_function_guard_suppressions = Vec::new();
let mut command_substitution_command_spans = Vec::new();
let mut arithmetic_update_operator_spans = Vec::new();
let mut arithmetic_literal_spans = Vec::new();
for id in self.semantic.commands_in_source_order().iter().copied() {
let Some(context) = self.semantic.command_context(id) else {
continue;
};
let Some(visit) = self
.command_visits_by_id
.get(id.index())
.and_then(|visit| *visit)
else {
continue;
};
let key = FactSpan::new(command_span(visit.command));
debug_assert_eq!(context.syntax_span(), command_span(visit.command));
debug_assert_eq!(
context.kind(),
shuck_semantic::CommandKind::from_command(visit.command)
);
let lookup_kind = command_lookup_kind(visit.command);
let entries = command_ids_by_span.entry(key).or_default();
if !entries.iter().any(|entry| entry.kind == lookup_kind) {
entries.push(CommandLookupEntry {
kind: lookup_kind,
id,
});
}
if context.is_in_if_condition() {
if_condition_command_ids.insert(id);
}
if context.is_in_elif_condition() {
elif_condition_command_ids.insert(id);
}
collect_binding_values(
visit.command,
self.semantic_artifacts,
self.semantic,
self.source,
&mut binding_values,
);
collect_broken_assoc_key_spans(visit.command, self.source, &mut broken_assoc_key_spans);
collect_command_substitution_command_span(
visit.command,
self.source,
&mut command_substitution_command_spans,
);
collect_comma_array_assignment_spans(
visit.command,
self.source,
self.shell,
self.semantic,
&mut comma_array_assignment_spans,
);
collect_ifs_literal_backslash_assignment_value_spans(
visit.command,
self.source,
&mut ifs_literal_backslash_assignment_value_spans,
);
let normalized = command::normalize_command(visit.command, self.source);
let command_start_offset = command_span(visit.command).start.offset();
let scope = context.scope();
let command_shell_behavior =
effective_command_shell_behavior(self.semantic, command_start_offset, &normalized);
if let Some(name_word) = normalized.body_name_word() {
command_ids_by_name_word_span
.entry(FactSpan::new(name_word.span))
.or_insert(id);
}
let nested_word_command = context.is_nested_word_command();
build_word_facts_for_command(
visit,
self.source,
locator,
self.semantic,
WordFactCommandContext {
command_id: id,
nested_word_command,
scope,
},
&normalized,
command_shell_behavior.clone(),
WordFactOutputs {
command_visits_by_id: self.command_visits_by_id,
word_nodes: &mut word_nodes,
word_spans: &mut word_spans,
word_span_scratch: &mut word_span_scratch,
word_node_ids_by_span: &mut word_node_ids_by_span,
word_occurrences: &mut word_occurrences,
pending_arithmetic_word_occurrences: &mut pending_arithmetic_word_occurrences,
pending_parameter_operand_word_occurrences:
&mut pending_parameter_operand_word_occurrences,
compound_assignment_value_word_spans: &mut compound_assignment_value_word_spans,
array_assignment_split_word_ids: &mut array_assignment_split_word_ids,
seen_word_occurrences: &mut seen_word_occurrences,
seen_pending_arithmetic_word_occurrences:
&mut seen_pending_arithmetic_word_occurrences,
seen_pending_parameter_operand_word_occurrences:
&mut seen_pending_parameter_operand_word_occurrences,
assoc_binding_visibility_memo: &mut assoc_binding_visibility_memo,
semantic_analysis: self.semantic_analysis,
case_pattern_expansions: &mut case_pattern_expansions,
pattern_literal_spans: &mut pattern_literal_spans,
arithmetic: &mut arithmetic_summary,
surface: &mut surface_fragments,
},
);
collect_zsh_option_map_arithmetic_suppressed_subscripts(
visit.command,
self.semantic,
scope,
self.source,
&mut arithmetic_summary.arithmetic_only_suppressed_subscript_spans,
);
collect_base_prefix_spans_in_command_parts(
visit.command,
self.source,
&mut arithmetic_literal_spans,
);
collect_arithmetic_update_operator_spans_in_command(
visit.command,
self.semantic,
self.semantic_artifacts,
scope,
self.source,
&mut arithmetic_update_operator_spans,
);
for redirect in visit.redirects {
if let Some(word) = redirect.word_target() {
collect_base_prefix_spans_in_word(
word,
self.source,
&mut arithmetic_literal_spans,
);
collect_arithmetic_update_operator_spans_in_word(
word,
self.semantic,
self.source,
&mut arithmetic_update_operator_spans,
);
} else if let Some(heredoc) = redirect.heredoc()
&& heredoc.delimiter.expands_body
{
collect_arithmetic_update_operator_spans_in_heredoc_body(
&heredoc.body.parts,
self.semantic,
self.semantic_artifacts,
self.source,
&mut arithmetic_update_operator_spans,
);
}
}
let redirect_facts = build_redirect_facts(
visit.redirects,
Some(self.semantic_artifacts),
locator,
&command_shell_behavior,
);
let redirect_fact_range = redirect_fact_store.push_many(redirect_facts);
let options = CommandOptionFacts::build(
visit.command,
&normalized,
self.semantic_artifacts,
self.source,
&command_shell_behavior,
)
.into_sparse();
let declaration_assignment_probes = build_declaration_assignment_probes(
visit.command,
&normalized,
self.semantic,
self.source,
&command_shell_behavior,
);
let declaration_assignment_probe_range =
declaration_assignment_probe_store.push_many(declaration_assignment_probes);
let glued_closing_bracket_operand_span =
build_glued_closing_bracket_operand_span(visit.command, self.source);
let glued_closing_bracket_insert_offset =
build_glued_closing_bracket_insert_offset(visit.command, self.source);
let simple_test = build_simple_test_fact(visit.command, self.source);
let conditional_expression_visits = self
.semantic_artifacts
.conditional_expression_visits(command_span(visit.command));
let conditional = build_conditional_fact(conditional_expression_visits, self.source);
let enclosing_function_scope = self.semantic.enclosing_function_scope(scope);
let command_fact = CommandFact {
id,
visit,
nested_word_command,
scope,
enclosing_function_scope,
normalized,
shell_behavior: command_shell_behavior,
redirect_facts: redirect_fact_range,
substitution_facts: IdRange::empty(),
options,
scope_read_source_words: IdRange::empty(),
scope_name_read_uses: IdRange::empty(),
scope_heredoc_name_read_uses: IdRange::empty(),
scope_name_write_uses: IdRange::empty(),
declaration_assignment_probes: declaration_assignment_probe_range,
glued_closing_bracket_operand_span,
glued_closing_bracket_insert_offset,
linebreak_in_test_anchor_span: None,
linebreak_in_test_insert_offset: None,
simple_test,
conditional,
};
collect_substitution_occurrences_for_command(
visit.command,
visit.redirects,
self.source,
&mut substitution_occurrences_by_command_id[id.index()],
);
commands.push(command_fact);
if let Command::Function(function) = visit.command {
functions.push(FunctionFactInput {
command_id: id,
function,
});
if let Some(span) = function_body_without_braces_span(function) {
function_body_without_braces_spans.push(span);
}
}
if !nested_word_command {
match visit.command {
Command::Function(function) => {
collect_redundant_return_status_spans_in_function_body(
&function.body,
self.source,
&mut redundant_return_status_spans,
);
}
Command::AnonymousFunction(function) => {
collect_redundant_return_status_spans_in_function_body(
&function.body,
self.source,
&mut redundant_return_status_spans,
);
}
Command::Simple(_)
| Command::Builtin(_)
| Command::Decl(_)
| Command::Binary(_)
| Command::Compound(_) => {}
}
collect_condition_status_capture_from_direct_body_sequences(
visit.command,
self.source,
&mut condition_status_capture_spans,
);
collect_precise_function_return_guard_suppressions_from_direct_body_sequences(
visit.command,
self.source,
&mut precise_function_guard_suppressions,
context.flow().in_function,
);
match visit.command {
Command::Compound(CompoundCommand::If(command)) => {
collect_condition_status_capture_from_body(
&command.condition,
&command.then_branch,
self.source,
&mut condition_status_capture_spans,
);
let mut previous_condition = &command.condition;
for (index, (condition, branch)) in command.elif_branches.iter().enumerate()
{
if index > 0
|| !stmt_seq_contains_nested_control_flow(
&command.then_branch,
self.semantic_artifacts.command_topology(),
)
{
collect_condition_status_capture_from_body(
previous_condition,
condition,
self.source,
&mut condition_status_capture_spans,
);
}
collect_condition_status_capture_from_body(
condition,
branch,
self.source,
&mut condition_status_capture_spans,
);
previous_condition = condition;
}
if let Some(else_branch) = &command.else_branch {
collect_condition_status_capture_from_body(
previous_condition,
else_branch,
self.source,
&mut condition_status_capture_spans,
);
}
}
Command::Compound(CompoundCommand::While(command)) => {
collect_condition_status_capture_from_body(
&command.condition,
&command.body,
self.source,
&mut condition_status_capture_spans,
);
if let Some(case) = build_getopts_case_fact_for_while(command, self.source)
{
getopts_cases.push(case);
}
}
Command::Compound(CompoundCommand::Until(command)) => {
collect_condition_status_capture_from_body(
&command.condition,
&command.body,
self.source,
&mut condition_status_capture_spans,
);
}
Command::Binary(command)
if matches!(command.op, BinaryOp::And | BinaryOp::Or) =>
{
if stmt_terminals_are_test_commands(&command.left, self.source) {
collect_status_parameter_spans_in_stmt(
&command.right,
self.source,
&mut condition_status_capture_spans,
);
}
}
Command::Simple(_)
| Command::Builtin(_)
| Command::Decl(_)
| Command::Binary(_)
| Command::Compound(_)
| Command::Function(_)
| Command::AnonymousFunction(_) => {}
}
}
}
arithmetic_update_operator_spans
.sort_unstable_by_key(|span| (span.start.offset(), span.end.offset()));
arithmetic_update_operator_spans.dedup();
redundant_return_status_spans
.sort_unstable_by_key(|span| (span.start.offset(), span.end.offset()));
redundant_return_status_spans.dedup_by_key(|span| FactSpan::new(*span));
sort_and_dedup_spans(&mut arithmetic_summary.arithmetic_expansion_spans);
sort_and_dedup_spans(&mut arithmetic_summary.arithmetic_index_subscript_spans);
let arithmetic_update_operator_fix_facts =
if matches!(self.shell, ShellDialect::Sh | ShellDialect::Dash) {
build_arithmetic_update_operator_fix_facts(
&arithmetic_update_operator_spans,
self.source,
)
} else {
Vec::new()
};
arithmetic_literal_spans
.sort_unstable_by_key(|(span, kind)| (span.start.offset(), span.end.offset(), *kind));
arithmetic_literal_spans.dedup();
let arithmetic_literal_facts = arithmetic_literal_spans
.iter()
.map(|(span, kind)| {
ArithmeticLiteralFact::new(
*span,
*kind,
self.semantic
.shell_behavior_at(span.start.offset())
.arithmetic_literals(),
)
})
.collect::<Vec<_>>();
let mut fact_store = FactStore::empty();
fact_store.redirect_facts = redirect_fact_store;
fact_store.declaration_assignment_probes = declaration_assignment_probe_store;
fact_store.word_spans = word_spans;
commands.sort_unstable_by(compare_command_facts_by_offset);
let command_fact_indices_by_id = build_command_fact_indices_by_id(&commands);
let structural_command_ids = self
.semantic
.structural_commands()
.iter()
.copied()
.filter(|id| {
command_fact_indices_by_id
.get(id.index())
.is_some_and(Option::is_some)
})
.collect::<Vec<_>>();
let command_child_index = CommandChildIndex::from_semantic_syntax_backed_children(
self.semantic,
&command_fact_indices_by_id,
);
populate_linebreak_in_test_facts(&mut commands, self.source);
populate_substitution_fact_ranges(
&mut commands,
&mut fact_store,
&command_fact_indices_by_id,
&command_ids_by_span,
&command_child_index,
self.semantic_artifacts,
locator,
&substitution_occurrences_by_command_id,
);
let presence_tested_names =
build_presence_tested_names(&commands, self.source, self.semantic);
let function_headers = build_function_header_facts(
self.semantic,
semantic_analysis,
&functions,
&commands,
&command_fact_indices_by_id,
self.source,
);
let case_cli_dispatches = semantic_analysis.case_cli_dispatches(self.file, self.source);
let function_cli_dispatch_facts = build_function_cli_dispatch_facts(&case_cli_dispatches);
let function_definition_command_ids_by_scope = function_headers
.iter()
.filter_map(|header| {
header
.function_scope()
.map(|scope| (scope, header.command_id()))
})
.collect::<FxHashMap<_, _>>();
let case_cli_reachable_function_scopes = semantic_analysis
.case_cli_reachable_function_scopes(self.file, &case_cli_dispatches)
.into_iter()
.collect();
collect_condition_status_capture_from_sequence(
&self.file.body,
self.source,
&mut condition_status_capture_spans,
);
collect_precise_function_return_guard_suppressions_in_seq(
&self.file.body,
self.source,
&mut precise_function_guard_suppressions,
false,
);
if !precise_function_guard_suppressions.is_empty() {
condition_status_capture_spans
.retain(|span| !precise_function_guard_suppressions.contains(span));
}
condition_status_capture_spans
.retain(|span| matches!(span.slice(self.source), "$?" | "${?}"));
sort_and_dedup_spans(&mut condition_status_capture_spans);
sort_and_dedup_spans(&mut command_substitution_command_spans);
sort_and_dedup_case_pattern_expansions(&mut case_pattern_expansions);
let function_in_alias_facts = build_function_in_alias_facts(&commands, self.source);
let function_parameter_fallback_spans = build_function_parameter_fallback_spans(
&commands,
&command_fact_indices_by_id,
&structural_command_ids,
self.source,
);
let for_headers = build_for_header_facts(
&commands,
&command_fact_indices_by_id,
&command_ids_by_span,
locator,
);
let select_headers = build_select_header_facts(
&commands,
&command_fact_indices_by_id,
&command_ids_by_span,
locator,
);
let case_items = build_case_item_facts(&commands, self.source);
let (case_pattern_shadows, case_pattern_impossible_spans) =
build_case_pattern_facts(&commands, self.source);
let pipelines = build_pipeline_facts(
&commands,
&command_fact_indices_by_id,
self.semantic,
&command_ids_by_span,
&command_child_index,
);
populate_scope_fact_ranges(
&mut commands,
&mut fact_store,
&command_fact_indices_by_id,
&pipelines,
&if_condition_command_ids,
source,
);
let lists = build_list_facts(
&commands,
&command_fact_indices_by_id,
&command_ids_by_span,
&command_child_index,
self.source,
);
let completion_registered_function_scopes = build_completion_registered_function_scopes(
self.semantic,
semantic_analysis,
&commands,
&command_fact_indices_by_id,
&lists,
self.source,
);
let completion_registered_function_command_flags =
build_completion_registered_function_command_flags(
&commands,
&completion_registered_function_scopes,
);
let external_entrypoint_function_scopes = build_external_entrypoint_function_scopes(
self.semantic,
&commands,
&command_fact_indices_by_id,
&lists,
self.source,
);
annotate_conditional_assignment_value_paths(self.semantic, &lists, &mut binding_values);
let statement_facts = build_statement_facts(&commands, self.semantic);
let background_semicolon_spans =
build_background_semicolon_spans(&commands, &case_items, locator);
let single_test_subshell_spans = build_single_test_subshell_spans(
&commands,
&command_fact_indices_by_id,
&command_ids_by_span,
&command_child_index,
locator,
);
let subshell_test_group_spans = build_subshell_test_group_spans(
&commands,
&command_fact_indices_by_id,
&command_ids_by_span,
&command_child_index,
locator,
);
let shebang_header_facts = build_shebang_header_facts(locator);
let errexit_enabled_anywhere = self.ambient_shell_options.errexit
|| shebang_header_facts.enables_errexit
|| commands
.iter()
.filter_map(|fact| fact.options().set())
.any(|set| set.errexit_change == Some(true));
let pipefail_enabled_anywhere = self.ambient_shell_options.pipefail
|| commands
.iter()
.filter_map(|fact| fact.options().set())
.any(|set| set.pipefail_change == Some(true));
let commented_continuation_comment_spans =
build_commented_continuation_comment_spans(self.source, self._indexer);
let comment_double_quote_nesting_spans =
build_comment_double_quote_nesting_spans(self.source, self._indexer);
let trailing_directive_comment_spans = build_trailing_directive_comment_spans(
self.semantic_artifacts.directive_attachment_facts(),
&case_items,
self.source,
self._indexer,
);
let backtick_command_name_spans = build_backtick_command_name_spans(&commands);
let dollar_question_after_command_spans =
build_dollar_question_after_command_spans(&self.file.body, self.source);
let SurfaceFragmentFacts {
single_quoted,
dollar_double_quoted,
open_double_quotes,
suspect_closing_quotes,
backticks,
legacy_arithmetic,
positional_parameters,
positional_parameter_operator_spans,
unicode_smart_quote_spans,
pattern_exactly_one_extglob_spans: surface_pattern_exactly_one_extglob_spans,
pattern_charclass_spans: surface_pattern_charclass_spans,
parameter_pattern_spans,
nested_pattern_charclass_spans,
nested_parameter_expansions,
indirect_expansions,
mut indexed_array_references,
plain_unindexed_references,
parameter_pattern_special_targets,
zsh_parameter_index_flags,
substring_expansions,
case_modifications,
replacement_expansions,
mut positional_parameter_trims,
suppressed_subscript_spans,
subscript_later_suppression_spans,
mut arithmetic_only_suppressed_subscript_spans,
} = surface_fragments.finish();
arithmetic_only_suppressed_subscript_spans.extend(
arithmetic_summary
.arithmetic_only_suppressed_subscript_spans
.iter()
.copied(),
);
for fragment in &mut indexed_array_references {
let span = match fragment {
IndexedArrayReferenceFragmentFact::OneBased(fragment)
| IndexedArrayReferenceFragmentFact::ZeroBased(fragment)
| IndexedArrayReferenceFragmentFact::OneBasedWithZeroAlias(fragment)
| IndexedArrayReferenceFragmentFact::Ambiguous(fragment) => fragment.span(),
};
let behavior = self
.semantic
.shell_behavior_at(span.start.offset())
.subscript_indexing();
*fragment = (*fragment).with_subscript_index_behavior(behavior);
}
let nonpersistent_assignment_spans = build_nonpersistent_assignment_spans(
self.semantic,
semantic_analysis,
&commands,
self.source,
matches!(self.shell, ShellDialect::Zsh),
matches!(self.shell, ShellDialect::Bash) && pipefail_enabled_anywhere,
&arithmetic_only_suppressed_subscript_spans,
);
let heredoc_summary =
build_heredoc_fact_summary(&commands, locator, self.file.span.end.offset());
let plus_equals_assignment_spans = build_plus_equals_assignment_spans(&commands);
let literal_brace_spans = build_literal_brace_spans(
&word_nodes,
&word_occurrences,
CommandFacts::new(&commands, &fact_store, &command_fact_indices_by_id),
&fact_store,
locator,
self._indexer.region_index(),
);
let function_positional_parameter_facts = build_function_positional_parameter_facts(
self.semantic,
&commands,
&positional_parameters,
);
let double_paren_grouping_spans = build_double_paren_grouping_spans(&commands, self.source);
let suppressed_subscript_reference_spans = build_suppressed_subscript_reference_spans(
self.semantic,
&suppressed_subscript_spans,
&arithmetic_only_suppressed_subscript_spans,
);
#[cfg(test)]
let subscript_later_suppression_reference_spans =
build_subscript_later_suppression_reference_spans(
self.semantic,
&subscript_later_suppression_spans,
);
pattern_exactly_one_extglob_spans.extend(surface_pattern_exactly_one_extglob_spans);
pattern_charclass_spans.extend(surface_pattern_charclass_spans);
let escape_scan_matches = build_escape_scan_matches(
&commands,
&command_fact_indices_by_id,
&word_nodes,
&word_occurrences,
EscapeScanInputs {
pattern_literal_spans: &pattern_literal_spans,
pattern_charclass_spans: &pattern_charclass_spans,
parameter_pattern_spans: ¶meter_pattern_spans,
single_quoted_fragments: &single_quoted,
backtick_fragments: &backticks,
},
EscapeScanContext {
source: self.source,
},
);
let echo_backslash_escape_word_spans =
build_echo_backslash_escape_word_spans(&commands, self.source);
let nested_pattern_charclass_spans = nested_pattern_charclass_spans
.into_iter()
.map(FactSpan::new)
.collect();
let conditional_portability = build_conditional_portability_facts(
&commands,
&elif_condition_command_ids,
ConditionalPortabilityInputs {
word_nodes: &word_nodes,
word_occurrences: &word_occurrences,
pattern_exactly_one_extglob_spans: &pattern_exactly_one_extglob_spans,
pattern_charclass_spans: &pattern_charclass_spans,
parameter_pattern_spans: ¶meter_pattern_spans,
nested_pattern_charclass_spans: &nested_pattern_charclass_spans,
},
source,
);
let EnvPrefixScopeSpans {
assignment_scope_spans: env_prefix_assignment_scope_spans,
expansion_scope_spans: env_prefix_expansion_scope_spans,
expansion_fix_facts: env_prefix_expansion_fix_facts,
} = build_env_prefix_scope_spans(self.source, self.semantic, &commands);
let unset_command_ids_by_target_name = build_unset_command_ids_by_target_name(
&commands,
&command_fact_indices_by_id,
&structural_command_ids,
source,
);
let function_unset_command_ids_by_target_name =
build_function_unset_command_ids_by_target_name(
&commands,
&command_fact_indices_by_id,
&structural_command_ids,
source,
);
let word_index = build_word_occurrence_index(
&commands,
&word_nodes,
&mut word_occurrences,
pending_arithmetic_word_occurrences,
pending_parameter_operand_word_occurrences,
&mut fact_store,
);
populate_array_assignment_split_scalar_expansion_spans(
self.shell,
&commands,
&word_nodes,
&mut word_occurrences,
&mut fact_store,
&array_assignment_split_word_ids,
);
let compound_assignment_value_word_flags: Box<[bool]> = word_occurrences
.iter()
.map(|occurrence| {
compound_assignment_value_word_spans
.contains(&word_nodes[occurrence.node_id.index()].key)
})
.collect();
let echo_to_sed_substitution_spans = build_echo_to_sed_substitution_spans(
CommandFacts::new(&commands, &fact_store, &command_fact_indices_by_id),
&pipelines,
&backticks,
WordFactLookup {
nodes: &word_nodes,
occurrences: &word_occurrences,
word_index: &word_index,
fact_store: &fact_store,
source,
line_index: self._indexer.line_index(),
},
);
let assignment_like_command_name_spans =
build_assignment_like_command_name_spans(&commands, self.source);
let bare_command_name_assignment_spans = build_bare_command_name_assignment_spans(
&commands,
&word_nodes,
&word_occurrences,
&word_index,
source,
);
let brace_variable_before_bracket_spans =
build_brace_variable_before_bracket_spans(&word_nodes, &word_occurrences, source);
let alias_definition_expansion_facts = build_alias_definition_expansion_facts(
&commands,
&fact_store,
&word_nodes,
&word_occurrences,
&word_index,
source,
);
let innermost_command_ids_by_offset = build_innermost_command_ids_by_offset(
&commands,
commands
.iter()
.map(|command| command.span().start.offset())
.collect(),
);
attach_positional_parameter_trim_fixes(
&mut positional_parameter_trims,
&commands,
&command_fact_indices_by_id,
source,
);
let innermost_command_ids_by_binding_offset = build_innermost_command_ids_by_offset(
&commands,
self.semantic
.bindings()
.iter()
.map(|binding| binding.span.start.offset())
.collect(),
);
let assignment_value_target_index = build_assignment_value_target_index(&commands);
let command_dominance_barrier_flags = build_command_dominance_barrier_flags(&commands);
let c006_suppressing_reference_offsets_by_name =
build_c006_suppressing_reference_offsets_by_name(
self.semantic,
&commands,
&command_fact_indices_by_id,
&innermost_command_ids_by_offset,
&subscript_later_suppression_spans,
);
let backtick_substitution_spans = text_ranges_to_spans(
self._indexer
.region_index()
.backtick_command_substitution_ranges(),
locator,
);
let backtick_escaped_parameters =
word_spans::backtick_escaped_parameters(locator, &backtick_substitution_spans);
let mut backtick_escaped_parameter_reference_spans =
word_spans::backtick_escaped_parameter_reference_spans(
locator,
&backtick_substitution_spans,
);
backtick_escaped_parameter_reference_spans.extend(
backtick_escaped_parameters
.iter()
.map(|escaped| escaped.reference_span),
);
backtick_escaped_parameter_reference_spans
.sort_by_key(|span| (span.start.offset(), span.end.offset()));
backtick_escaped_parameter_reference_spans.dedup();
let backtick_double_escaped_parameter_spans =
word_spans::backtick_double_escaped_parameter_spans(
locator,
&backtick_substitution_spans,
);
LinterFacts {
semantic: self.semantic,
semantic_artifacts: self.semantic_artifacts,
command: CommandFactStore {
commands,
command_fact_indices_by_id,
structural_command_ids,
#[cfg(test)]
command_ids_by_span,
command_ids_by_name_word_span,
innermost_command_ids_by_offset,
innermost_command_ids_by_binding_offset,
command_dominance_barrier_flags,
if_condition_command_ids,
elif_condition_command_ids,
fact_store,
redundant_echo_space_facts: OnceLock::new(),
completion_registered_function_command_flags,
completion_registered_function_scopes,
external_entrypoint_function_scopes,
function_headers,
function_doc_content: OnceLock::new(),
function_definition_command_ids_by_scope,
case_cli_reachable_function_scopes,
function_in_alias_facts,
alias_definition_expansion_facts,
function_body_without_braces_spans,
function_parameter_fallback_spans,
redundant_return_status_spans,
for_headers,
select_headers,
case_items,
case_pattern_shadows,
case_pattern_impossible_spans,
case_pattern_expansions,
getopts_cases,
pipelines,
lists,
tautology_chain_operator_spans: OnceLock::new(),
statement_facts,
background_semicolon_spans,
single_test_subshell_spans,
subshell_test_group_spans,
function_positional_parameter_facts,
function_cli_dispatch_facts,
condition_status_capture_spans,
command_substitution_command_spans,
backtick_command_name_spans,
assignment_spacing_spans: OnceLock::new(),
missing_space_before_bracket_close_facts: OnceLock::new(),
jammed_test_bracket_facts: OnceLock::new(),
assignment_like_command_name_spans,
assign_special_zero_spans: OnceLock::new(),
spacey_assignment_facts: OnceLock::new(),
bare_command_name_assignment_spans,
},
words: WordFactStore {
plain_unindexed_array_references: OnceLock::new(),
suppressed_subscript_reference_spans,
#[cfg(test)]
subscript_later_suppression_reference_spans,
compound_assignment_value_word_flags,
word_nodes,
word_occurrences,
word_index,
array_assignment_split_word_ids,
brace_variable_before_bracket_spans,
bare_done_word_spans: OnceLock::new(),
array_index_arithmetic_spans: arithmetic_summary.array_index_arithmetic_spans,
arithmetic_score_line_spans: arithmetic_summary.arithmetic_score_line_spans,
dollar_in_arithmetic_spans: arithmetic_summary.dollar_in_arithmetic_spans,
arithmetic_expansion_spans: arithmetic_summary.arithmetic_expansion_spans,
arithmetic_index_subscript_spans: arithmetic_summary
.arithmetic_index_subscript_spans,
arithmetic_command_substitution_spans: arithmetic_summary
.arithmetic_command_substitution_spans,
arithmetic_only_suppressed_subscript_spans,
single_quoted_fragments: single_quoted,
dollar_double_quoted_fragments: dollar_double_quoted,
open_double_quote_fragments: open_double_quotes,
suspect_closing_quote_fragments: suspect_closing_quotes,
literal_brace_spans,
backtick_fragments: backticks,
legacy_arithmetic_fragments: legacy_arithmetic,
positional_parameter_fragments: positional_parameters,
positional_parameter_operator_spans,
double_paren_grouping_spans,
arithmetic_update_operator_spans,
arithmetic_update_operator_fix_facts,
arithmetic_literal_facts,
escape_scan_matches,
echo_backslash_escape_word_spans,
echo_to_sed_substitution_spans,
unicode_smart_quote_spans,
#[cfg(test)]
pattern_literal_spans,
pattern_charclass_spans,
nested_parameter_expansion_fragments: nested_parameter_expansions,
indirect_expansion_fragments: indirect_expansions,
indexed_array_reference_fragments: indexed_array_references,
plain_unindexed_reference_spans: plain_unindexed_references,
parameter_pattern_special_target_fragments: parameter_pattern_special_targets,
zsh_parameter_index_flag_fragments: zsh_parameter_index_flags,
substring_expansion_fragments: substring_expansions,
case_modification_fragments: case_modifications,
replacement_expansion_fragments: replacement_expansions,
positional_parameter_trim_fragments: positional_parameter_trims,
},
assignments: AssignmentFactStore {
assignment_value_target_index,
binding_values,
broken_assoc_key_spans,
comma_array_assignment_spans,
ifs_literal_backslash_assignment_value_spans,
env_prefix_assignment_scope_spans,
env_prefix_expansion_scope_spans,
env_prefix_expansion_fix_facts,
unset_command_ids_by_target_name,
function_unset_command_ids_by_target_name,
presence_tested_names: presence_tested_names.global_names,
nested_presence_test_spans: presence_tested_names.nested_command_spans_by_name,
c006_presence_tested_names: presence_tested_names.c006_global_names,
c006_nested_presence_test_spans: presence_tested_names
.c006_nested_command_spans_by_name,
c006_suppressing_reference_offsets_by_name,
presence_test_references_by_name: presence_tested_names.references_by_name,
presence_test_names_by_name: presence_tested_names.names_by_name,
possible_variable_misspelling_use_scan: OnceLock::new(),
possible_variable_misspelling_index: OnceLock::new(),
subshell_assignment_sites: nonpersistent_assignment_spans.subshell_assignment_sites,
subshell_later_use_sites: nonpersistent_assignment_spans.subshell_later_use_sites,
plus_equals_assignment_spans,
},
source_facts: SourceFactStore {
source,
line_index: self._indexer.line_index(),
comment_index: self._indexer.comment_index(),
shell: self.shell,
script_line_count: OnceLock::new(),
indented_shebang_span: shebang_header_facts.indented_shebang_span,
indented_shebang_indent_span: shebang_header_facts.indented_shebang_indent_span,
space_after_hash_bang_span: shebang_header_facts.space_after_hash_bang_span,
space_after_hash_bang_whitespace_span: shebang_header_facts
.space_after_hash_bang_whitespace_span,
shebang_not_on_first_line_span: shebang_header_facts.shebang_not_on_first_line_span,
shebang_not_on_first_line_fix_span: shebang_header_facts
.shebang_not_on_first_line_fix_span,
shebang_not_on_first_line_preferred_newline: shebang_header_facts
.shebang_not_on_first_line_preferred_newline,
missing_shebang_line_span: shebang_header_facts.missing_shebang_line_span,
duplicate_shebang_flag_span: shebang_header_facts.duplicate_shebang_flag_span,
non_absolute_shebang_span: shebang_header_facts.non_absolute_shebang_span,
shebang_interpreter: OnceLock::new(),
shebang_invocation: OnceLock::new(),
missing_file_description_comment: OnceLock::new(),
errexit_enabled_anywhere,
region_index: self._indexer.region_index(),
commented_continuation_comment_spans,
comment_double_quote_nesting_spans,
escaped_dash_command_name_spans: OnceLock::new(),
trailing_directive_comment_spans,
todo_comment_facts: OnceLock::new(),
backtick_substitution_spans,
backtick_escaped_parameters,
backtick_escaped_parameter_reference_spans,
backtick_double_escaped_parameter_spans,
dollar_question_after_command_spans,
unused_heredoc_spans: heredoc_summary.unused_heredoc_spans,
heredoc_missing_end_spans: heredoc_summary.heredoc_missing_end_spans,
heredoc_closer_not_alone_spans: heredoc_summary.heredoc_closer_not_alone_spans,
misquoted_heredoc_close_spans: heredoc_summary.misquoted_heredoc_close_spans,
heredoc_end_space_spans: heredoc_summary.heredoc_end_space_spans,
indented_heredoc_close_facts: OnceLock::new(),
echo_here_doc_spans: heredoc_summary.echo_here_doc_spans,
spaced_tabstrip_close_spans: heredoc_summary.spaced_tabstrip_close_spans,
},
compat: CompatFactStore {
possible_variable_misspelling_scope_compat_name_uses: OnceLock::new(),
conditional_portability,
},
}
}
}
fn attach_positional_parameter_trim_fixes(
fragments: &mut [PositionalParameterTrimFragmentFact],
commands: &[CommandFact<'_>],
command_fact_indices_by_id: &[Option<usize>],
source: &str,
) {
if fragments.is_empty() {
return;
}
let command_ids_by_fragment_offset = build_innermost_command_ids_by_offset(
commands,
fragments
.iter()
.map(|fragment| fragment.span().start.offset())
.collect(),
);
let mut fragment_indices_by_command = FxHashMap::<CommandId, Vec<usize>>::default();
for (index, fragment) in fragments.iter().enumerate() {
let Some(command_id) = precomputed_command_id_for_offset(
&command_ids_by_fragment_offset,
fragment.span().start.offset(),
) else {
continue;
};
fragment_indices_by_command
.entry(command_id)
.or_default()
.push(index);
}
for (command_id, mut indices) in fragment_indices_by_command {
let Some(command) = command_fact_indices_by_id
.get(command_id.index())
.copied()
.flatten()
.and_then(|index| commands.get(index))
else {
continue;
};
if command.is_nested_word_command() {
continue;
}
indices.sort_unstable_by_key(|index| fragments[*index].span().start.offset());
let first_span = fragments[indices[0]].span();
if command.span().start.line() != first_span.start.line() {
continue;
}
let line_start = source[..command.span().start.offset()]
.rfind('\n')
.map_or(0, |offset| offset + 1);
let indent = &source[line_start..command.span().start.offset()];
if !indent.bytes().all(|byte| matches!(byte, b' ' | b'\t'))
|| previous_line_ends_with_control_operator(source, line_start)
{
continue;
}
let mut target = None;
let mut replacements = Vec::with_capacity(indices.len());
let mut compatible = true;
for index in &indices {
let span = fragments[*index].span();
let text = span.slice(source);
let Some((current_target, rest)) = text
.strip_prefix("${*")
.map(|rest| ('*', rest))
.or_else(|| text.strip_prefix("${@").map(|rest| ('@', rest)))
else {
compatible = false;
break;
};
if target.is_some_and(|target| target != current_target) {
compatible = false;
break;
}
target = Some(current_target);
replacements.push(PositionalParameterTrimReplacement::new(
span,
format!("${{_shuck_positional_params{rest}").into_boxed_str(),
));
}
let Some(target) = target.filter(|_| compatible) else {
continue;
};
fragments[indices[0]].set_fix(PositionalParameterTrimFix::new(
line_start,
format!("{indent}_shuck_positional_params=${target}\n").into_boxed_str(),
replacements.into_boxed_slice(),
));
}
}
fn previous_line_ends_with_control_operator(source: &str, line_start: usize) -> bool {
let previous = source[..line_start]
.strip_suffix('\n')
.unwrap_or(&source[..line_start]);
let line = previous
.rsplit_once('\n')
.map_or(previous, |(_, line)| line);
let line = line_without_trailing_comment(line).trim_end_matches([' ', '\t']);
let line = line
.strip_suffix('\\')
.map_or(line, |continued| continued.trim_end_matches([' ', '\t']));
line.ends_with('|') || line.ends_with("|&") || line.ends_with("&&")
}
fn line_without_trailing_comment(line: &str) -> &str {
let mut escaped = false;
let mut in_single_quotes = false;
let mut in_double_quotes = false;
let mut comment_can_start = true;
for (offset, ch) in line.char_indices() {
if escaped {
escaped = false;
continue;
}
if in_single_quotes {
if ch == '\'' {
in_single_quotes = false;
}
continue;
}
if in_double_quotes {
match ch {
'\\' => escaped = true,
'"' => in_double_quotes = false,
_ => {}
}
continue;
}
match ch {
'#' if comment_can_start => return &line[..offset],
'\\' => {
escaped = true;
comment_can_start = false;
}
'\'' => {
in_single_quotes = true;
comment_can_start = false;
}
'"' => {
in_double_quotes = true;
comment_can_start = false;
}
' ' | '\t' => comment_can_start = true,
'|' | '&' | ';' | '(' | ')' | '<' | '>' => comment_can_start = true,
_ => comment_can_start = false,
}
}
line
}
#[cfg_attr(shuck_profiling, inline(never))]
pub(crate) fn build_word_occurrence_index(
commands: &[CommandFact<'_>],
word_nodes: &[WordNode<'_>],
word_occurrences: &mut Vec<WordOccurrence>,
pending_arithmetic_word_occurrences: Vec<PendingArithmeticWordOccurrence>,
pending_parameter_operand_word_occurrences: Vec<PendingParameterOperandWordOccurrence>,
fact_store: &mut FactStore<'_>,
) -> FxHashMap<FactSpan, SmallVec<[WordOccurrenceId; 2]>> {
word_occurrences.extend(
pending_arithmetic_word_occurrences
.into_iter()
.map(|pending| WordOccurrence {
node_id: pending.node_id,
command_id: pending.command_id,
nested_word_command: pending.nested_word_command,
context: WordFactContext::ArithmeticCommand,
host_kind: pending.host_kind,
runtime_literal: RuntimeLiteralAnalysis::default(),
operand_class: None,
enclosing_expansion_context: Some(pending.enclosing_expansion_context),
split_sensitive_unquoted_command_substitution_spans: IdRange::empty(),
array_assignment_split_scalar_expansion_spans: IdRange::empty(),
}),
);
word_occurrences.extend(pending_parameter_operand_word_occurrences.into_iter().map(
|pending| WordOccurrence {
node_id: pending.node_id,
command_id: pending.command_id,
nested_word_command: pending.nested_word_command,
context: WordFactContext::ParameterOperand,
host_kind: pending.host_kind,
runtime_literal: RuntimeLiteralAnalysis::default(),
operand_class: None,
enclosing_expansion_context: Some(pending.enclosing_expansion_context),
split_sensitive_unquoted_command_substitution_spans: IdRange::empty(),
array_assignment_split_scalar_expansion_spans: IdRange::empty(),
},
));
let mut word_index = FxHashMap::<FactSpan, SmallVec<[WordOccurrenceId; 2]>>::default();
word_index.reserve(word_occurrences.len());
let command_slot_count = commands
.iter()
.map(|command| command.id().index())
.max()
.map_or(0, |index| index + 1);
let mut word_occurrence_offsets_by_command = vec![0usize; command_slot_count];
for fact in word_occurrences.iter() {
word_occurrence_offsets_by_command[fact.command_id.index()] += 1;
}
let mut next_word_occurrence_offset = 0usize;
let word_occurrence_ids_by_command = word_occurrence_offsets_by_command
.iter_mut()
.map(|count| {
let range = IdRange::from_start_len(next_word_occurrence_offset, *count);
*count = next_word_occurrence_offset;
next_word_occurrence_offset = range.end_index();
range
})
.collect::<Vec<_>>();
let mut word_occurrence_ids = vec![WordOccurrenceId::new(0); next_word_occurrence_offset];
for (index, fact) in word_occurrences.iter().enumerate() {
let id = WordOccurrenceId::new(index);
word_index
.entry(occurrence_key(word_nodes, fact))
.or_default()
.push(id);
let command_index = fact.command_id.index();
let offset = word_occurrence_offsets_by_command[command_index];
word_occurrence_ids[offset] = id;
word_occurrence_offsets_by_command[command_index] += 1;
}
let mut word_occurrence_id_store = ListArena::with_capacity(word_occurrence_ids.len());
let all_word_occurrence_ids = word_occurrence_id_store.push_many(word_occurrence_ids);
debug_assert_eq!(all_word_occurrence_ids.start_index(), 0);
debug_assert_eq!(
all_word_occurrence_ids.end_index(),
next_word_occurrence_offset
);
fact_store.word_occurrence_ids = word_occurrence_id_store;
fact_store.word_occurrence_ids_by_command = word_occurrence_ids_by_command;
word_index
}
pub(crate) fn build_c006_suppressing_reference_offsets_by_name(
semantic: &SemanticModel,
commands: &[CommandFact<'_>],
command_fact_indices_by_id: &[Option<usize>],
innermost_command_ids_by_offset: &CommandOffsetLookup,
subscript_later_suppression_spans: &[Span],
) -> FxHashMap<Name, Vec<usize>> {
let mut offsets_by_name = semantic
.guarded_or_defaulting_reference_offsets_by_name()
.iter()
.map(|(name, offsets)| (name.clone(), offsets.to_vec()))
.collect::<FxHashMap<_, _>>();
for span in subscript_later_suppression_spans {
for reference in semantic.references_in_span(*span) {
if c006_subscript_reference_suppresses_later_references(
commands,
command_fact_indices_by_id,
innermost_command_ids_by_offset,
reference,
) {
offsets_by_name
.entry(reference.name.clone())
.or_default()
.push(reference.span.start.offset());
}
}
}
for offsets in offsets_by_name.values_mut() {
offsets.sort_unstable();
offsets.dedup();
}
offsets_by_name
}
pub(crate) fn c006_subscript_reference_suppresses_later_references(
commands: &[CommandFact<'_>],
command_fact_indices_by_id: &[Option<usize>],
innermost_command_ids_by_offset: &CommandOffsetLookup,
reference: &Reference,
) -> bool {
precomputed_command_id_for_offset(
innermost_command_ids_by_offset,
reference.span.start.offset(),
)
.and_then(|id| {
command_fact_indices_by_id
.get(id.index())
.copied()
.flatten()
.and_then(|index| commands.get(index))
})
.and_then(CommandFact::static_utility_name)
.is_none_or(|name| !matches!(name, "unset" | "[" | "[[" | "test"))
}
pub(crate) fn stmt_seq_contains_nested_control_flow(
body: &StmtSeq,
command_topology: CommandTopology<'_, '_>,
) -> bool {
let mut contains = false;
command_topology
.body(body)
.for_each_command_visit(false, |_, visit| match visit.command {
Command::Compound(
CompoundCommand::If(_)
| CompoundCommand::While(_)
| CompoundCommand::Until(_)
| CompoundCommand::For(_)
| CompoundCommand::Select(_)
| CompoundCommand::Case(_)
| CompoundCommand::Always(_),
) => {
contains = true;
CommandTopologyTraversal::Break
}
Command::Binary(_)
| Command::Compound(
CompoundCommand::BraceGroup(_)
| CompoundCommand::Subshell(_)
| CompoundCommand::Time(_),
) => CommandTopologyTraversal::Descend,
Command::Simple(_)
| Command::Builtin(_)
| Command::Decl(_)
| Command::Compound(_)
| Command::Function(_)
| Command::AnonymousFunction(_) => CommandTopologyTraversal::SkipChildren,
});
contains
}
pub(crate) fn populate_linebreak_in_test_facts(commands: &mut [CommandFact<'_>], source: &str) {
for index in 0..commands.len().saturating_sub(1) {
let (current_slice, next_slice) = commands.split_at_mut(index + 1);
let current = &mut current_slice[index];
let next = &next_slice[0];
let Some((anchor_span, insert_offset)) =
build_linebreak_in_test_site(current, next, source)
else {
continue;
};
current.linebreak_in_test_anchor_span = Some(anchor_span);
current.linebreak_in_test_insert_offset = Some(insert_offset);
}
}
pub(crate) fn build_linebreak_in_test_site(
current: &CommandFact<'_>,
next: &CommandFact<'_>,
source: &str,
) -> Option<(Span, usize)> {
if !current.static_utility_name_is("[")
|| !next.static_utility_name_is("]")
|| !next.body_args().is_empty()
{
return None;
}
let last_arg_is_closing_bracket = current
.body_args()
.last()
.and_then(|word| static_word_text(word, source))
.as_deref()
== Some("]");
let current_span = current.span();
if last_arg_is_closing_bracket {
return None;
}
let insert_offset = linebreak_in_test_insert_offset(current_span, source)?;
let between = source.get(current_span.end.offset()..next.span().start.offset())?;
if !between.chars().all(|char| matches!(char, ' ' | '\t')) {
return None;
}
let anchor_span = current
.body_args()
.last()
.map(|word| word.span)
.or_else(|| current.body_name_word().map(|word| word.span))
.map(|span| Span::from_positions(span.end, span.end))
.unwrap_or_else(|| Span::from_positions(current_span.end, current_span.end));
Some((anchor_span, insert_offset))
}
pub(crate) fn linebreak_in_test_insert_offset(span: Span, source: &str) -> Option<usize> {
let text = span.slice(source);
if text.ends_with("\r\n") {
Some(span.end.offset() - 2)
} else if text.ends_with('\n') {
Some(span.end.offset() - 1)
} else {
None
}
}
pub(crate) fn text_ranges_to_spans(ranges: &[TextRange], locator: Locator<'_>) -> Vec<Span> {
ranges
.iter()
.filter_map(|range| text_range_to_span(*range, locator))
.collect()
}
pub(crate) fn text_range_to_span(range: TextRange, locator: Locator<'_>) -> Option<Span> {
Some(Span::from_positions(
locator.position_at_offset(usize::from(range.start()))?,
locator.position_at_offset(usize::from(range.end()))?,
))
}
pub(crate) fn build_unset_command_ids_by_target_name(
commands: &[CommandFact<'_>],
command_fact_indices_by_id: &[Option<usize>],
structural_command_ids: &[CommandId],
source: &str,
) -> FxHashMap<Name, Vec<CommandId>> {
let mut command_ids_by_name = FxHashMap::<Name, Vec<CommandId>>::default();
for command_id in structural_command_ids.iter().copied() {
let Some(command) = command_fact_indices_by_id
.get(command_id.index())
.copied()
.flatten()
.and_then(|index| commands.get(index))
else {
continue;
};
let Some(unset) = command.options().unset() else {
continue;
};
if unset.function_mode || unset.nameref_mode() || !unset.options_parseable() {
continue;
}
for operand in unset.operand_facts() {
if operand.array_subscript().is_some() {
continue;
}
if let Some(text) = static_word_text(operand.word(), source) {
command_ids_by_name
.entry(Name::from(text.as_ref()))
.or_default()
.push(command_id);
}
}
}
command_ids_by_name
}
pub(crate) fn build_function_unset_command_ids_by_target_name(
commands: &[CommandFact<'_>],
command_fact_indices_by_id: &[Option<usize>],
structural_command_ids: &[CommandId],
source: &str,
) -> FxHashMap<Name, Vec<CommandId>> {
let mut command_ids_by_name = FxHashMap::<Name, Vec<CommandId>>::default();
for command_id in structural_command_ids.iter().copied() {
let Some(command) = command_fact_indices_by_id
.get(command_id.index())
.copied()
.flatten()
.and_then(|index| commands.get(index))
else {
continue;
};
let Some(unset) = command.options().unset() else {
continue;
};
if !unset.function_mode || !unset.options_parseable() {
continue;
}
let mut targets = Vec::new();
for word in unset.operand_words() {
let Some(text) = static_word_text(word, source) else {
break;
};
let target = Name::from(text.as_ref());
if !targets.contains(&target) {
targets.push(target);
}
}
for target in targets {
command_ids_by_name
.entry(target)
.or_default()
.push(command_id);
}
}
command_ids_by_name
}
pub(crate) fn sort_and_dedup_case_pattern_expansions(
expansions: &mut Vec<CasePatternExpansionFact>,
) {
let mut seen = FxHashSet::default();
expansions.retain(|fact| seen.insert(FactSpan::new(fact.span())));
expansions.sort_by_key(|fact| (fact.span().start.offset(), fact.span().end.offset()));
}
#[cfg(test)]
mod builder_tests {
use shuck_ast::{Position, Span};
use super::linebreak_in_test_insert_offset;
#[test]
fn linebreak_in_test_insert_offset_targets_lf_newlines() {
let source = "if [ \"$x\" = y\n";
let span = Span::from_positions(Position::new(), Position::new().advanced_by(source));
let insert_offset =
linebreak_in_test_insert_offset(span, source).expect("expected LF insert offset");
assert_eq!(&source[insert_offset..], "\n");
}
#[test]
fn linebreak_in_test_insert_offset_targets_crlf_newlines() {
let source = "if [ \"$x\" = y\r\n";
let span = Span::from_positions(Position::new(), Position::new().advanced_by(source));
let insert_offset =
linebreak_in_test_insert_offset(span, source).expect("expected CRLF insert offset");
assert_eq!(&source[insert_offset..], "\r\n");
}
}