Skip to main content

shuck_linter/facts/
model.rs

1use super::*;
2
3pub(crate) struct CommandFactStore<'a> {
4    pub(in crate::facts) commands: Vec<CommandFact<'a>>,
5    pub(in crate::facts) command_fact_indices_by_id: Vec<Option<usize>>,
6    pub(in crate::facts) structural_command_ids: Vec<CommandId>,
7    #[cfg(test)]
8    pub(in crate::facts) command_ids_by_span: CommandLookupIndex,
9    pub(in crate::facts) command_ids_by_name_word_span: FxHashMap<FactSpan, CommandId>,
10    pub(in crate::facts) innermost_command_ids_by_offset: CommandOffsetLookup,
11    pub(in crate::facts) innermost_command_ids_by_binding_offset: CommandOffsetLookup,
12    pub(in crate::facts) command_dominance_barrier_flags: Vec<bool>,
13    pub(in crate::facts) if_condition_command_ids: DenseCommandIdSet,
14    pub(in crate::facts) elif_condition_command_ids: DenseCommandIdSet,
15    pub(in crate::facts) fact_store: FactStore<'a>,
16    pub(in crate::facts) redundant_echo_space_facts: OnceLock<Vec<RedundantEchoSpaceFact>>,
17    pub(in crate::facts) completion_registered_function_command_flags: Vec<bool>,
18    pub(in crate::facts) completion_registered_function_scopes: FxHashSet<ScopeId>,
19    pub(in crate::facts) external_entrypoint_function_scopes: FxHashSet<ScopeId>,
20    pub(in crate::facts) function_headers: Vec<FunctionHeaderFact<'a>>,
21    pub(in crate::facts) function_doc_content: OnceLock<Vec<FunctionDocContentFact>>,
22    pub(in crate::facts) function_definition_command_ids_by_scope: FxHashMap<ScopeId, CommandId>,
23    pub(in crate::facts) case_cli_reachable_function_scopes: FxHashSet<ScopeId>,
24    pub(in crate::facts) function_in_alias_spans: Vec<Span>,
25    pub(in crate::facts) alias_definition_expansion_spans: Vec<Span>,
26    pub(in crate::facts) function_body_without_braces_spans: Vec<Span>,
27    pub(in crate::facts) function_parameter_fallback_spans: Vec<Span>,
28    pub(in crate::facts) redundant_return_status_spans: Vec<Span>,
29    pub(in crate::facts) for_headers: Vec<ForHeaderFact<'a>>,
30    pub(in crate::facts) select_headers: Vec<SelectHeaderFact<'a>>,
31    pub(in crate::facts) case_items: Vec<CaseItemFact<'a>>,
32    pub(in crate::facts) case_pattern_shadows: Vec<CasePatternShadowFact>,
33    pub(in crate::facts) case_pattern_impossible_spans: Vec<Span>,
34    pub(in crate::facts) case_pattern_expansions: Vec<CasePatternExpansionFact>,
35    pub(in crate::facts) getopts_cases: Vec<GetoptsCaseFact>,
36    pub(in crate::facts) pipelines: Vec<PipelineFact<'a>>,
37    pub(in crate::facts) lists: Vec<ListFact<'a>>,
38    pub(in crate::facts) tautology_chain_operator_spans: OnceLock<Vec<Span>>,
39    pub(in crate::facts) statement_facts: Vec<StatementFact>,
40    pub(in crate::facts) background_semicolon_spans: Vec<Span>,
41    pub(in crate::facts) single_test_subshell_spans: Vec<Span>,
42    pub(in crate::facts) subshell_test_group_spans: Vec<Span>,
43    pub(in crate::facts) function_positional_parameter_facts:
44        FxHashMap<ScopeId, FunctionPositionalParameterFacts>,
45    pub(in crate::facts) function_cli_dispatch_facts: FxHashMap<ScopeId, FunctionCliDispatchFacts>,
46    pub(in crate::facts) condition_status_capture_spans: Vec<Span>,
47    pub(in crate::facts) command_substitution_command_spans: Vec<Span>,
48    pub(in crate::facts) backtick_command_name_spans: Vec<Span>,
49    pub(in crate::facts) assignment_spacing_spans: OnceLock<Vec<Span>>,
50    pub(in crate::facts) missing_space_before_bracket_close_facts: OnceLock<Vec<(Span, usize)>>,
51    pub(in crate::facts) jammed_test_bracket_facts: OnceLock<Vec<(Span, usize)>>,
52    pub(in crate::facts) assignment_like_command_name_spans: Vec<Span>,
53    pub(in crate::facts) assign_special_zero_spans: OnceLock<Vec<Span>>,
54    pub(in crate::facts) spacey_assignment_spans: OnceLock<Vec<Span>>,
55    pub(in crate::facts) bare_command_name_assignment_spans: Vec<Span>,
56}
57
58pub(crate) struct WordFactStore<'a> {
59    pub(in crate::facts) plain_unindexed_array_references:
60        OnceLock<Vec<PlainUnindexedArrayReferenceFact>>,
61    pub(in crate::facts) suppressed_subscript_reference_spans: FxHashSet<FactSpan>,
62    #[cfg(test)]
63    pub(in crate::facts) subscript_later_suppression_reference_spans: FxHashSet<FactSpan>,
64    pub(in crate::facts) compound_assignment_value_word_flags: Box<[bool]>,
65    pub(in crate::facts) word_nodes: Vec<WordNode<'a>>,
66    pub(in crate::facts) word_occurrences: Vec<WordOccurrence>,
67    pub(in crate::facts) word_index: FxHashMap<FactSpan, SmallVec<[WordOccurrenceId; 2]>>,
68    pub(in crate::facts) array_assignment_split_word_ids: Vec<WordOccurrenceId>,
69    pub(in crate::facts) brace_variable_before_bracket_spans: Vec<Span>,
70    pub(in crate::facts) bare_done_word_spans: OnceLock<Vec<Span>>,
71    pub(in crate::facts) array_index_arithmetic_spans: Vec<Span>,
72    pub(in crate::facts) arithmetic_score_line_spans: Vec<Span>,
73    pub(in crate::facts) dollar_in_arithmetic_spans: Vec<Span>,
74    pub(in crate::facts) arithmetic_expansion_spans: Vec<Span>,
75    pub(in crate::facts) arithmetic_index_subscript_spans: Vec<Span>,
76    pub(in crate::facts) arithmetic_command_substitution_spans: Vec<Span>,
77    pub(in crate::facts) arithmetic_only_suppressed_subscript_spans: Vec<Span>,
78    pub(in crate::facts) single_quoted_fragments: Vec<SingleQuotedFragmentFact>,
79    pub(in crate::facts) dollar_double_quoted_fragments: Vec<DollarDoubleQuotedFragmentFact>,
80    pub(in crate::facts) open_double_quote_fragments: Vec<OpenDoubleQuoteFragmentFact>,
81    pub(in crate::facts) suspect_closing_quote_fragments: Vec<SuspectClosingQuoteFragmentFact>,
82    pub(in crate::facts) literal_brace_spans: Vec<Span>,
83    pub(in crate::facts) backtick_fragments: Vec<BacktickFragmentFact>,
84    pub(in crate::facts) legacy_arithmetic_fragments: Vec<LegacyArithmeticFragmentFact>,
85    pub(in crate::facts) positional_parameter_fragments: Vec<PositionalParameterFragmentFact>,
86    pub(in crate::facts) positional_parameter_operator_spans: Vec<Span>,
87    pub(in crate::facts) double_paren_grouping_spans: Vec<Span>,
88    pub(in crate::facts) arithmetic_update_operator_spans: Vec<Span>,
89    pub(in crate::facts) arithmetic_update_operator_fix_facts: Vec<ArithmeticUpdateOperatorFixFact>,
90    pub(in crate::facts) arithmetic_literal_facts: Vec<ArithmeticLiteralFact>,
91    pub(in crate::facts) escape_scan_matches: Vec<EscapeScanMatch>,
92    pub(in crate::facts) echo_backslash_escape_word_spans: Vec<Span>,
93    pub(in crate::facts) echo_to_sed_substitution_spans: Vec<Span>,
94    pub(in crate::facts) unicode_smart_quote_spans: Vec<Span>,
95    #[cfg(test)]
96    pub(in crate::facts) pattern_literal_spans: Vec<Span>,
97    pub(in crate::facts) pattern_charclass_spans: Vec<Span>,
98    pub(in crate::facts) nested_parameter_expansion_fragments:
99        Vec<NestedParameterExpansionFragmentFact>,
100    pub(in crate::facts) indirect_expansion_fragments: Vec<IndirectExpansionFragmentFact>,
101    pub(in crate::facts) indexed_array_reference_fragments: Vec<IndexedArrayReferenceFragmentFact>,
102    pub(in crate::facts) plain_unindexed_reference_spans: Vec<Span>,
103    pub(in crate::facts) parameter_pattern_special_target_fragments:
104        Vec<ParameterPatternSpecialTargetFragmentFact>,
105    pub(in crate::facts) zsh_parameter_index_flag_fragments: Vec<ZshParameterIndexFlagFragmentFact>,
106    pub(in crate::facts) substring_expansion_fragments: Vec<SubstringExpansionFragmentFact>,
107    pub(in crate::facts) case_modification_fragments: Vec<CaseModificationFragmentFact>,
108    pub(in crate::facts) replacement_expansion_fragments: Vec<ReplacementExpansionFragmentFact>,
109    pub(in crate::facts) positional_parameter_trim_fragments:
110        Vec<PositionalParameterTrimFragmentFact>,
111}
112
113pub(crate) struct AssignmentFactStore<'a> {
114    pub(in crate::facts) assignment_value_target_index: AssignmentValueTargetIndex,
115    pub(in crate::facts) binding_values: FxHashMap<BindingId, BindingValueFact<'a>>,
116    pub(in crate::facts) broken_assoc_key_spans: Vec<Span>,
117    pub(in crate::facts) comma_array_assignment_spans: Vec<Span>,
118    pub(in crate::facts) ifs_literal_backslash_assignment_value_spans: Vec<Span>,
119    pub(in crate::facts) env_prefix_assignment_scope_spans: Vec<Span>,
120    pub(in crate::facts) env_prefix_expansion_scope_spans: Vec<Span>,
121    pub(in crate::facts) env_prefix_expansion_fix_facts: Vec<EnvPrefixExpansionFixFact>,
122    pub(in crate::facts) unset_command_ids_by_target_name: FxHashMap<Name, Vec<CommandId>>,
123    pub(in crate::facts) function_unset_command_ids_by_target_name: FxHashMap<Name, Vec<CommandId>>,
124    pub(in crate::facts) presence_tested_names: FxHashSet<Name>,
125    pub(in crate::facts) nested_presence_test_spans: FxHashMap<Name, Vec<Span>>,
126    pub(in crate::facts) c006_presence_tested_names: FxHashSet<Name>,
127    pub(in crate::facts) c006_nested_presence_test_spans: FxHashMap<Name, Vec<Span>>,
128    pub(in crate::facts) c006_suppressing_reference_offsets_by_name: FxHashMap<Name, Vec<usize>>,
129    pub(in crate::facts) presence_test_references_by_name:
130        FxHashMap<Name, Vec<PresenceTestReferenceFact>>,
131    pub(in crate::facts) presence_test_names_by_name: FxHashMap<Name, Vec<PresenceTestNameFact>>,
132    pub(in crate::facts) possible_variable_misspelling_use_scan: OnceLock<bool>,
133    pub(in crate::facts) possible_variable_misspelling_index:
134        OnceLock<PossibleVariableMisspellingIndex>,
135    pub(in crate::facts) subshell_assignment_sites: Vec<NamedSpan>,
136    pub(in crate::facts) subshell_later_use_sites: Vec<NamedSpan>,
137    pub(in crate::facts) plus_equals_assignment_spans: Vec<Span>,
138}
139
140pub(crate) struct SourceFactStore<'a> {
141    pub(in crate::facts) source: &'a str,
142    pub(in crate::facts) line_index: &'a LineIndex,
143    pub(in crate::facts) comment_index: &'a CommentIndex,
144    pub(in crate::facts) shell: ShellDialect,
145    pub(in crate::facts) script_line_count: OnceLock<ScriptLineCountFact>,
146    pub(in crate::facts) indented_shebang_span: Option<Span>,
147    pub(in crate::facts) indented_shebang_indent_span: Option<Span>,
148    pub(in crate::facts) space_after_hash_bang_span: Option<Span>,
149    pub(in crate::facts) space_after_hash_bang_whitespace_span: Option<Span>,
150    pub(in crate::facts) shebang_not_on_first_line_span: Option<Span>,
151    pub(in crate::facts) shebang_not_on_first_line_fix_span: Option<Span>,
152    pub(in crate::facts) shebang_not_on_first_line_preferred_newline: Option<&'static str>,
153    pub(in crate::facts) missing_shebang_line_span: Option<Span>,
154    pub(in crate::facts) duplicate_shebang_flag_span: Option<Span>,
155    pub(in crate::facts) non_absolute_shebang_span: Option<Span>,
156    pub(in crate::facts) shebang_interpreter: OnceLock<Option<ShebangInterpreterFact>>,
157    pub(in crate::facts) shebang_invocation: OnceLock<Option<ShebangInvocationFact>>,
158    pub(in crate::facts) missing_file_description_comment:
159        OnceLock<Option<FileDescriptionCommentFact>>,
160    pub(in crate::facts) errexit_enabled_anywhere: bool,
161    pub(in crate::facts) region_index: &'a RegionIndex,
162    pub(in crate::facts) commented_continuation_comment_spans: Vec<Span>,
163    pub(in crate::facts) comment_double_quote_nesting_spans: Vec<Span>,
164    pub(in crate::facts) escaped_dash_command_name_spans: OnceLock<Vec<Span>>,
165    pub(in crate::facts) trailing_directive_comment_spans: Vec<Span>,
166    pub(in crate::facts) todo_comment_facts: OnceLock<Vec<TodoCommentFact>>,
167    pub(in crate::facts) backtick_substitution_spans: Vec<Span>,
168    pub(in crate::facts) backtick_escaped_parameters: Vec<BacktickEscapedParameter>,
169    pub(in crate::facts) backtick_escaped_parameter_reference_spans: Vec<Span>,
170    pub(in crate::facts) backtick_double_escaped_parameter_spans: Vec<Span>,
171    pub(in crate::facts) dollar_question_after_command_spans: Vec<Span>,
172    pub(in crate::facts) unused_heredoc_spans: Vec<Span>,
173    pub(in crate::facts) heredoc_missing_end_spans: Vec<Span>,
174    pub(in crate::facts) heredoc_closer_not_alone_spans: Vec<Span>,
175    pub(in crate::facts) misquoted_heredoc_close_spans: Vec<Span>,
176    pub(in crate::facts) heredoc_end_space_spans: Vec<Span>,
177    pub(in crate::facts) indented_heredoc_close_facts: OnceLock<Vec<(Span, Span)>>,
178    pub(in crate::facts) echo_here_doc_spans: Vec<Span>,
179    pub(in crate::facts) spaced_tabstrip_close_spans: Vec<Span>,
180}
181
182pub(crate) struct CompatFactStore {
183    pub(in crate::facts) possible_variable_misspelling_scope_compat_name_uses:
184        OnceLock<Vec<ComparableNameUse>>,
185    pub(in crate::facts) conditional_portability: ConditionalPortabilityFacts,
186}
187
188pub struct LinterFacts<'a> {
189    pub(in crate::facts) semantic: &'a SemanticModel,
190    pub(in crate::facts) semantic_artifacts: &'a LinterSemanticArtifacts<'a>,
191    pub(in crate::facts) command: CommandFactStore<'a>,
192    pub(in crate::facts) words: WordFactStore<'a>,
193    pub(in crate::facts) assignments: AssignmentFactStore<'a>,
194    pub(in crate::facts) source_facts: SourceFactStore<'a>,
195    pub(in crate::facts) compat: CompatFactStore,
196}
197
198#[derive(Clone, Copy)]
199pub struct CommandFactQueries<'facts, 'a> {
200    facts: &'facts LinterFacts<'a>,
201}
202
203#[derive(Clone, Copy)]
204pub struct WordFacts<'facts, 'a> {
205    facts: &'facts LinterFacts<'a>,
206}
207
208#[derive(Clone, Copy)]
209pub struct AssignmentFacts<'facts, 'a> {
210    facts: &'facts LinterFacts<'a>,
211}
212
213#[derive(Clone, Copy)]
214pub struct SourceFacts<'facts, 'a> {
215    facts: &'facts LinterFacts<'a>,
216}
217
218#[derive(Clone, Copy)]
219pub struct CompatFacts<'facts, 'a> {
220    facts: &'facts LinterFacts<'a>,
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq)]
224pub struct ScriptLineCountFact {
225    pub(in crate::facts) physical_lines: usize,
226    pub(in crate::facts) non_comment_non_blank_lines: usize,
227    pub(in crate::facts) report_span: Span,
228}
229
230impl ScriptLineCountFact {
231    pub fn physical_lines(self) -> usize {
232        self.physical_lines
233    }
234
235    pub fn non_comment_non_blank_lines(self) -> usize {
236        self.non_comment_non_blank_lines
237    }
238
239    pub fn report_span(self) -> Span {
240        self.report_span
241    }
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub struct FileDescriptionCommentFact {
246    pub(in crate::facts) span: Span,
247    pub(in crate::facts) shebang_only_file: bool,
248}
249
250impl FileDescriptionCommentFact {
251    pub fn span(self) -> Span {
252        self.span
253    }
254
255    pub fn is_shebang_only_file(self) -> bool {
256        self.shebang_only_file
257    }
258}
259
260impl<'a> LinterFacts<'a> {
261    pub fn build(
262        file: &'a File,
263        source: &'a str,
264        semantic: &'a LinterSemanticArtifacts<'a>,
265        indexer: &'a Indexer,
266    ) -> Self {
267        Self::build_with_ambient_shell_options(
268            file,
269            source,
270            semantic,
271            indexer,
272            AmbientShellOptions::default(),
273        )
274    }
275
276    pub fn build_with_ambient_shell_options(
277        file: &'a File,
278        source: &'a str,
279        semantic: &'a LinterSemanticArtifacts<'a>,
280        indexer: &'a Indexer,
281        ambient_shell_options: AmbientShellOptions,
282    ) -> Self {
283        Self::build_with_shell_and_ambient_shell_options(
284            file,
285            source,
286            semantic,
287            indexer,
288            ShellDialect::Unknown,
289            ambient_shell_options,
290        )
291    }
292
293    pub fn build_with_shell_and_ambient_shell_options(
294        file: &'a File,
295        source: &'a str,
296        semantic: &'a LinterSemanticArtifacts<'a>,
297        indexer: &'a Indexer,
298        shell: ShellDialect,
299        ambient_shell_options: AmbientShellOptions,
300    ) -> Self {
301        let semantic_analysis = semantic.semantic().analysis();
302        Self::build_with_semantic_analysis_shell_and_ambient_shell_options(
303            file,
304            source,
305            semantic,
306            &semantic_analysis,
307            indexer,
308            shell,
309            ambient_shell_options,
310        )
311    }
312
313    pub(crate) fn build_with_semantic_analysis_shell_and_ambient_shell_options(
314        file: &'a File,
315        source: &'a str,
316        semantic: &'a LinterSemanticArtifacts<'a>,
317        semantic_analysis: &SemanticAnalysis<'a>,
318        indexer: &'a Indexer,
319        shell: ShellDialect,
320        ambient_shell_options: AmbientShellOptions,
321    ) -> Self {
322        LinterFactsBuilder::new(
323            file,
324            source,
325            semantic,
326            semantic_analysis,
327            indexer,
328            shell,
329            ambient_shell_options,
330        )
331        .build()
332    }
333
334    pub fn commands(&self) -> CommandFacts<'_, 'a> {
335        CommandFacts::new(
336            &self.command.commands,
337            &self.command.fact_store,
338            &self.command.command_fact_indices_by_id,
339        )
340    }
341
342    pub fn command_facts(&self) -> CommandFactQueries<'_, 'a> {
343        CommandFactQueries { facts: self }
344    }
345
346    pub fn words(&self) -> WordFacts<'_, 'a> {
347        WordFacts { facts: self }
348    }
349
350    pub fn assignments(&self) -> AssignmentFacts<'_, 'a> {
351        AssignmentFacts { facts: self }
352    }
353
354    pub fn source_facts(&self) -> SourceFacts<'_, 'a> {
355        SourceFacts { facts: self }
356    }
357
358    pub fn compat(&self) -> CompatFacts<'_, 'a> {
359        CompatFacts { facts: self }
360    }
361}
362
363impl<'facts, 'a> CommandFactQueries<'facts, 'a> {
364    pub(crate) fn function_positional_parameter_facts(
365        self,
366        scope: ScopeId,
367    ) -> FunctionPositionalParameterFacts {
368        self.facts
369            .command
370            .function_positional_parameter_facts
371            .get(&scope)
372            .copied()
373            .unwrap_or_default()
374    }
375
376    pub(crate) fn function_cli_dispatch_facts(self, scope: ScopeId) -> FunctionCliDispatchFacts {
377        self.facts
378            .command
379            .function_cli_dispatch_facts
380            .get(&scope)
381            .copied()
382            .unwrap_or_default()
383    }
384
385    pub fn structural_commands(self) -> impl Iterator<Item = CommandFactRef<'facts, 'a>> + 'facts {
386        self.facts
387            .command
388            .structural_command_ids
389            .iter()
390            .copied()
391            .map(move |id| self.command(id))
392    }
393
394    pub(crate) fn command(self, id: CommandId) -> CommandFactRef<'facts, 'a> {
395        let index = self
396            .facts
397            .command
398            .command_fact_indices_by_id
399            .get(id.index())
400            .and_then(|index| *index)
401            .unwrap_or_else(|| panic!("command id {} must exist", id.index()));
402
403        CommandFactRef::new(
404            &self.facts.command.commands[index],
405            &self.facts.command.fact_store,
406        )
407    }
408
409    pub(crate) fn command_for_name_word_span(
410        self,
411        span: Span,
412    ) -> Option<CommandFactRef<'facts, 'a>> {
413        self.facts
414            .command
415            .command_ids_by_name_word_span
416            .get(&FactSpan::new(span))
417            .copied()
418            .or_else(|| self.command_id_for_exact_span(span))
419            .map(|id| self.command(id))
420    }
421
422    pub(crate) fn innermost_command_at(self, offset: usize) -> Option<CommandFactRef<'facts, 'a>> {
423        self.innermost_command_id_at(offset)
424            .map(|id| self.command(id))
425    }
426
427    pub(crate) fn expansion_behavior_at(self, offset: usize) -> ShellBehaviorAt<'a> {
428        self.innermost_command_at(offset).map_or_else(
429            || self.facts.semantic.shell_behavior_at(offset),
430            |command| command.shell_behavior().clone(),
431        )
432    }
433
434    pub(crate) fn innermost_command_id_at(self, offset: usize) -> Option<CommandId> {
435        precomputed_command_id_for_offset(
436            &self.facts.command.innermost_command_ids_by_offset,
437            offset,
438        )
439    }
440
441    pub(crate) fn innermost_command_id_containing_offset(self, offset: usize) -> Option<CommandId> {
442        self.facts
443            .semantic
444            .innermost_command_id_containing_offset(offset)
445            .filter(|id| {
446                self.facts
447                    .command
448                    .command_fact_indices_by_id
449                    .get(id.index())
450                    .is_some_and(Option::is_some)
451            })
452    }
453
454    pub(crate) fn innermost_command_at_binding_offset(
455        self,
456        offset: usize,
457    ) -> Option<CommandFactRef<'facts, 'a>> {
458        precomputed_command_id_for_offset(
459            &self.facts.command.innermost_command_ids_by_binding_offset,
460            offset,
461        )
462        .map(|id| self.command(id))
463    }
464
465    pub(crate) fn command_parent_id(self, id: CommandId) -> Option<CommandId> {
466        self.facts
467            .semantic
468            .syntax_backed_command_parent_id(id)
469            .filter(|parent_id| {
470                self.facts
471                    .command
472                    .command_fact_indices_by_id
473                    .get(parent_id.index())
474                    .is_some_and(Option::is_some)
475            })
476    }
477
478    #[cfg(test)]
479    pub(crate) fn command_parent(self, id: CommandId) -> Option<CommandFactRef<'facts, 'a>> {
480        self.command_parent_id(id)
481            .map(|parent_id| self.command(parent_id))
482    }
483
484    pub(crate) fn redundant_echo_space_facts(self) -> &'facts [RedundantEchoSpaceFact] {
485        self.facts
486            .command
487            .redundant_echo_space_facts
488            .get_or_init(|| build_redundant_echo_space_facts(self.facts))
489    }
490
491    pub(crate) fn function_definition_command(
492        self,
493        scope: ScopeId,
494    ) -> Option<CommandFactRef<'facts, 'a>> {
495        self.facts
496            .command
497            .function_definition_command_ids_by_scope
498            .get(&scope)
499            .copied()
500            .map(|id| self.command(id))
501    }
502
503    pub(crate) fn is_case_cli_reachable_function_scope(self, scope: ScopeId) -> bool {
504        self.facts
505            .command
506            .case_cli_reachable_function_scopes
507            .contains(&scope)
508    }
509
510    pub(crate) fn command_is_dominance_barrier(self, id: CommandId) -> bool {
511        self.facts
512            .command
513            .command_dominance_barrier_flags
514            .get(id.index())
515            .copied()
516            .unwrap_or(false)
517    }
518
519    #[cfg(test)]
520    pub(crate) fn command_id_for_stmt(self, stmt: &Stmt) -> Option<CommandId> {
521        self.command_id_for_command(&stmt.command)
522    }
523
524    #[cfg(test)]
525    pub(crate) fn command_id_for_command(self, command: &Command) -> Option<CommandId> {
526        command_id_for_command(command, &self.facts.command.command_ids_by_span)
527    }
528
529    fn command_id_for_exact_span(self, span: Span) -> Option<CommandId> {
530        let mut current = self.innermost_command_id_at(span.start.offset)?;
531        loop {
532            if self.command(current).span() == span {
533                return Some(current);
534            }
535            current = self.command_parent_id(current)?;
536        }
537    }
538
539    pub(crate) fn is_if_condition_command(self, id: CommandId) -> bool {
540        self.facts.command.if_condition_command_ids.contains(id)
541    }
542
543    pub(crate) fn is_elif_condition_command(self, id: CommandId) -> bool {
544        self.facts.command.elif_condition_command_ids.contains(id)
545    }
546
547    pub(crate) fn command_is_in_completion_registered_function(self, id: CommandId) -> bool {
548        self.facts
549            .command
550            .completion_registered_function_command_flags
551            .get(id.index())
552            .copied()
553            .unwrap_or(false)
554    }
555
556    pub(crate) fn function_is_completion_registered(self, scope: ScopeId) -> bool {
557        self.facts
558            .command
559            .completion_registered_function_scopes
560            .contains(&scope)
561    }
562
563    pub(crate) fn function_is_external_entrypoint(self, scope: ScopeId) -> bool {
564        self.facts
565            .command
566            .external_entrypoint_function_scopes
567            .contains(&scope)
568    }
569
570    pub(crate) fn function_headers(self) -> &'facts [FunctionHeaderFact<'a>] {
571        &self.facts.command.function_headers
572    }
573
574    pub(crate) fn function_doc_content(self) -> &'facts [FunctionDocContentFact] {
575        self.facts.command.function_doc_content.get_or_init(|| {
576            build_function_doc_content_facts(
577                self.facts.semantic,
578                &self.facts.command.function_headers,
579                &self.facts.command.commands,
580                &self.facts.command.function_positional_parameter_facts,
581                self.facts.source_facts.source,
582                self.facts.source_facts.line_index,
583                self.facts.source_facts.comment_index,
584            )
585        })
586    }
587
588    pub(crate) fn fork_bomb_pattern_spans(self) -> Vec<Span> {
589        self.facts
590            .command
591            .function_headers
592            .iter()
593            .filter_map(|function| {
594                let (name, _) = function.static_name_entry()?;
595                let scope = function.function_scope()?;
596                self.function_has_background_self_pipe(name.as_str(), scope)
597                    .then(|| function.span_in_source(self.facts.source_facts.source))
598            })
599            .collect()
600    }
601
602    fn function_has_background_self_pipe(self, name: &str, scope: ScopeId) -> bool {
603        self.facts.command.commands.iter().any(|command| {
604            command.enclosing_function_scope() == Some(scope)
605                && stmt_is_plain_background(command.stmt())
606                && match command.command() {
607                    Command::Binary(binary) => binary_is_two_segment_self_pipe(
608                        binary,
609                        name,
610                        self.facts.source_facts.source,
611                    ),
612                    _ => false,
613                }
614        })
615    }
616
617    pub(crate) fn function_in_alias_spans(self) -> &'facts [Span] {
618        &self.facts.command.function_in_alias_spans
619    }
620
621    pub(crate) fn alias_definition_expansion_spans(self) -> &'facts [Span] {
622        &self.facts.command.alias_definition_expansion_spans
623    }
624
625    pub(crate) fn function_body_without_braces_spans(self) -> &'facts [Span] {
626        &self.facts.command.function_body_without_braces_spans
627    }
628
629    pub(crate) fn function_parameter_fallback_spans(self) -> &'facts [Span] {
630        &self.facts.command.function_parameter_fallback_spans
631    }
632
633    pub(crate) fn redundant_return_status_spans(self) -> &'facts [Span] {
634        &self.facts.command.redundant_return_status_spans
635    }
636
637    pub(crate) fn for_headers(self) -> &'facts [ForHeaderFact<'a>] {
638        &self.facts.command.for_headers
639    }
640
641    pub(crate) fn select_headers(self) -> &'facts [SelectHeaderFact<'a>] {
642        &self.facts.command.select_headers
643    }
644
645    pub(crate) fn case_items(self) -> &'facts [CaseItemFact<'a>] {
646        &self.facts.command.case_items
647    }
648
649    pub(crate) fn case_pattern_shadows(self) -> &'facts [CasePatternShadowFact] {
650        &self.facts.command.case_pattern_shadows
651    }
652
653    pub(crate) fn case_pattern_impossible_spans(self) -> &'facts [Span] {
654        &self.facts.command.case_pattern_impossible_spans
655    }
656
657    pub(crate) fn case_pattern_expansions(self) -> &'facts [CasePatternExpansionFact] {
658        &self.facts.command.case_pattern_expansions
659    }
660
661    pub(crate) fn getopts_cases(self) -> &'facts [GetoptsCaseFact] {
662        &self.facts.command.getopts_cases
663    }
664
665    pub(crate) fn pipelines(self) -> &'facts [PipelineFact<'a>] {
666        &self.facts.command.pipelines
667    }
668
669    pub(crate) fn lists(self) -> &'facts [ListFact<'a>] {
670        &self.facts.command.lists
671    }
672
673    pub(crate) fn tautology_chain_operator_spans(self) -> &'facts [Span] {
674        self.facts
675            .command
676            .tautology_chain_operator_spans
677            .get_or_init(|| {
678                build_tautology_chain_operator_spans(
679                    &self.facts.command.commands,
680                    &self.facts.command.command_fact_indices_by_id,
681                    &self.facts.command.lists,
682                    self.facts.source_facts.source,
683                )
684            })
685    }
686
687    pub(crate) fn duplicate_redirect_spans(self) -> Vec<Span> {
688        let source = self.facts.source_facts.source;
689        self.facts
690            .commands()
691            .iter()
692            .flat_map(|command| duplicate_redirect_spans(command.redirect_facts(), source))
693            .collect()
694    }
695
696    pub(crate) fn statement_facts(self) -> &'facts [StatementFact] {
697        &self.facts.command.statement_facts
698    }
699
700    pub(crate) fn background_semicolon_spans(self) -> &'facts [Span] {
701        &self.facts.command.background_semicolon_spans
702    }
703
704    pub(crate) fn single_test_subshell_spans(self) -> &'facts [Span] {
705        &self.facts.command.single_test_subshell_spans
706    }
707
708    pub(crate) fn subshell_test_group_spans(self) -> &'facts [Span] {
709        &self.facts.command.subshell_test_group_spans
710    }
711
712    pub(crate) fn condition_status_capture_spans(self) -> &'facts [Span] {
713        &self.facts.command.condition_status_capture_spans
714    }
715
716    pub(crate) fn command_substitution_command_spans(self) -> &'facts [Span] {
717        &self.facts.command.command_substitution_command_spans
718    }
719
720    pub(crate) fn backtick_command_name_spans(self) -> &'facts [Span] {
721        &self.facts.command.backtick_command_name_spans
722    }
723
724    pub(crate) fn assignment_spacing_spans(self) -> &'facts [Span] {
725        self.facts.command.assignment_spacing_spans.get_or_init(|| {
726            let source = self.facts.source_facts.source;
727            if source_may_have_assignment_spacing_candidate(source)
728                && self.facts.command.commands.iter().any(|command| {
729                    command_may_have_assignment_spacing_candidate(command.command(), source)
730                })
731            {
732                build_assignment_spacing_spans(&self.facts.command.commands, source)
733            } else {
734                Vec::new()
735            }
736        })
737    }
738
739    pub(crate) fn missing_space_before_bracket_close_facts(self) -> &'facts [(Span, usize)] {
740        self.facts
741            .command
742            .missing_space_before_bracket_close_facts
743            .get_or_init(|| {
744                let locator = Locator::new(
745                    self.facts.source_facts.source,
746                    self.facts.source_facts.line_index,
747                );
748                self.facts
749                    .command
750                    .commands
751                    .iter()
752                    .filter_map(|command| {
753                        build_missing_space_before_bracket_close_fact(
754                            command.command(),
755                            self.facts.source_facts.source,
756                            locator,
757                        )
758                    })
759                    .collect()
760            })
761    }
762
763    pub(crate) fn jammed_test_bracket_facts(self) -> &'facts [(Span, usize)] {
764        self.facts
765            .command
766            .jammed_test_bracket_facts
767            .get_or_init(|| {
768                self.facts
769                    .command
770                    .commands
771                    .iter()
772                    .filter_map(|command| {
773                        build_jammed_test_bracket_fact(
774                            command.command(),
775                            self.facts.source_facts.source,
776                        )
777                    })
778                    .collect()
779            })
780    }
781
782    pub(crate) fn assignment_like_command_name_spans(self) -> &'facts [Span] {
783        &self.facts.command.assignment_like_command_name_spans
784    }
785
786    pub(crate) fn assign_special_zero_spans(self) -> &'facts [Span] {
787        self.facts
788            .command
789            .assign_special_zero_spans
790            .get_or_init(|| {
791                build_assign_special_zero_spans(
792                    &self.facts.command.commands,
793                    self.facts.source_facts.source,
794                )
795            })
796    }
797
798    pub(crate) fn spacey_assignment_spans(self) -> &'facts [Span] {
799        self.facts.command.spacey_assignment_spans.get_or_init(|| {
800            build_spacey_assignment_spans(
801                &self.facts.command.commands,
802                self.facts.source_facts.source,
803            )
804        })
805    }
806
807    pub(crate) fn bare_command_name_assignment_spans(self) -> &'facts [Span] {
808        &self.facts.command.bare_command_name_assignment_spans
809    }
810}
811
812impl<'facts, 'a> AssignmentFacts<'facts, 'a> {
813    pub(crate) fn binding_value(
814        self,
815        binding_id: BindingId,
816    ) -> Option<&'facts BindingValueFact<'a>> {
817        self.facts.assignments.binding_values.get(&binding_id)
818    }
819
820    pub(crate) fn broken_assoc_key_spans(self) -> &'facts [Span] {
821        &self.facts.assignments.broken_assoc_key_spans
822    }
823
824    pub(crate) fn comma_array_assignment_spans(self) -> &'facts [Span] {
825        &self.facts.assignments.comma_array_assignment_spans
826    }
827
828    pub(crate) fn ifs_literal_backslash_assignment_value_spans(self) -> &'facts [Span] {
829        &self
830            .facts
831            .assignments
832            .ifs_literal_backslash_assignment_value_spans
833    }
834
835    pub(crate) fn env_prefix_assignment_scope_spans(self) -> &'facts [Span] {
836        &self.facts.assignments.env_prefix_assignment_scope_spans
837    }
838
839    pub(crate) fn env_prefix_expansion_scope_spans(self) -> &'facts [Span] {
840        &self.facts.assignments.env_prefix_expansion_scope_spans
841    }
842
843    pub(crate) fn env_prefix_expansion_fix_facts(self) -> &'facts [EnvPrefixExpansionFixFact] {
844        &self.facts.assignments.env_prefix_expansion_fix_facts
845    }
846
847    pub(crate) fn unset_commands_for_name(
848        self,
849        name: &Name,
850    ) -> impl Iterator<Item = CommandFactRef<'facts, 'a>> + 'facts {
851        self.facts
852            .assignments
853            .unset_command_ids_by_target_name
854            .get(name)
855            .into_iter()
856            .flatten()
857            .copied()
858            .map(move |id| self.facts.command_facts().command(id))
859    }
860
861    pub(crate) fn function_unset_commands_for_name(
862        self,
863        name: &Name,
864    ) -> impl Iterator<Item = CommandFactRef<'facts, 'a>> + 'facts {
865        self.facts
866            .assignments
867            .function_unset_command_ids_by_target_name
868            .get(name)
869            .into_iter()
870            .flatten()
871            .copied()
872            .map(move |id| self.facts.command_facts().command(id))
873    }
874
875    pub(crate) fn possible_variable_misspelling_candidate(
876        self,
877        semantic: &SemanticModel,
878        target_name: &str,
879    ) -> Option<String> {
880        if *self
881            .facts
882            .assignments
883            .possible_variable_misspelling_use_scan
884            .get_or_init(|| {
885                should_scan_possible_variable_misspelling_candidates(
886                    semantic,
887                    &self.facts.assignments.presence_test_references_by_name,
888                    &self.facts.assignments.presence_test_names_by_name,
889                )
890            })
891        {
892            return scan_possible_variable_misspelling_candidate(
893                semantic,
894                &self.facts.assignments.presence_test_references_by_name,
895                &self.facts.assignments.presence_test_names_by_name,
896                target_name,
897            );
898        }
899
900        self.facts
901            .assignments
902            .possible_variable_misspelling_index
903            .get_or_init(|| {
904                build_possible_variable_misspelling_index(
905                    semantic,
906                    &self.facts.assignments.presence_test_references_by_name,
907                    &self.facts.assignments.presence_test_names_by_name,
908                )
909            })
910            .candidate_name(target_name)
911            .map(ToOwned::to_owned)
912    }
913
914    pub(crate) fn is_presence_tested_name(self, name: &Name, span: Span) -> bool {
915        self.facts.assignments.presence_tested_names.contains(name)
916            || self
917                .facts
918                .assignments
919                .nested_presence_test_spans
920                .get(name)
921                .is_some_and(|spans| {
922                    spans
923                        .iter()
924                        .copied()
925                        .any(|outer| contains_span(outer, span))
926                })
927    }
928
929    pub(crate) fn is_c006_presence_tested_name(self, name: &Name, _span: Span) -> bool {
930        self.facts
931            .assignments
932            .c006_presence_tested_names
933            .contains(name)
934            || self
935                .facts
936                .assignments
937                .c006_nested_presence_test_spans
938                .contains_key(name)
939    }
940
941    pub(crate) fn has_prior_c006_suppressing_reference(self, name: &Name, span: Span) -> bool {
942        self.facts
943            .assignments
944            .c006_suppressing_reference_offsets_by_name
945            .get(name)
946            .is_some_and(|offsets| {
947                offsets.partition_point(|offset| *offset < span.start.offset) > 0
948            })
949    }
950
951    pub(crate) fn assignment_value_target_name_for_span(self, span: Span) -> Option<&'facts Name> {
952        let query_start = span.start.offset;
953        let query_end = span.end.offset;
954        let upper = self
955            .facts
956            .assignments
957            .assignment_value_target_index
958            .partition_point(|entry| entry.value_start <= query_start);
959        self.facts.assignments.assignment_value_target_index[..upper]
960            .iter()
961            .rev()
962            .find(|entry| entry.value_end >= query_end)
963            .map(|entry| &entry.target_name)
964    }
965
966    pub(crate) fn presence_test_references(
967        self,
968        name: &Name,
969    ) -> &'facts [PresenceTestReferenceFact] {
970        self.facts
971            .assignments
972            .presence_test_references_by_name
973            .get(name)
974            .map(Vec::as_slice)
975            .unwrap_or(&[])
976    }
977
978    pub(crate) fn presence_test_names(self, name: &Name) -> &'facts [PresenceTestNameFact] {
979        self.facts
980            .assignments
981            .presence_test_names_by_name
982            .get(name)
983            .map(Vec::as_slice)
984            .unwrap_or(&[])
985    }
986
987    pub(crate) fn presence_test_candidate_spans(
988        self,
989        semantic: &SemanticModel,
990    ) -> Vec<(Name, Span)> {
991        let mut names = FxHashSet::<Name>::default();
992        names.extend(
993            self.facts
994                .assignments
995                .presence_test_references_by_name
996                .keys()
997                .cloned(),
998        );
999        names.extend(
1000            self.facts
1001                .assignments
1002                .presence_test_names_by_name
1003                .keys()
1004                .cloned(),
1005        );
1006
1007        names
1008            .into_iter()
1009            .filter_map(|name| {
1010                let span = self.first_presence_test_candidate_span(semantic, &name)?;
1011                Some((name, span))
1012            })
1013            .collect()
1014    }
1015
1016    fn first_presence_test_candidate_span(
1017        self,
1018        semantic: &SemanticModel,
1019        candidate_name: &Name,
1020    ) -> Option<Span> {
1021        self.facts
1022            .assignments
1023            .presence_test_references_by_name
1024            .get(candidate_name)
1025            .into_iter()
1026            .flatten()
1027            .map(|presence| semantic.reference(presence.reference_id()).span)
1028            .chain(
1029                self.facts
1030                    .assignments
1031                    .presence_test_names_by_name
1032                    .get(candidate_name)
1033                    .into_iter()
1034                    .flatten()
1035                    .map(|presence| presence.tested_span()),
1036            )
1037            .min_by_key(|span| (span.start.offset, span.end.offset))
1038    }
1039
1040    pub(crate) fn subshell_assignment_sites(self) -> &'facts [NamedSpan] {
1041        &self.facts.assignments.subshell_assignment_sites
1042    }
1043
1044    pub(crate) fn subshell_later_use_sites(self) -> &'facts [NamedSpan] {
1045        &self.facts.assignments.subshell_later_use_sites
1046    }
1047
1048    pub(crate) fn plus_equals_assignment_spans(self) -> &'facts [Span] {
1049        &self.facts.assignments.plus_equals_assignment_spans
1050    }
1051}
1052
1053impl<'facts, 'a> SourceFacts<'facts, 'a> {
1054    pub(crate) fn source(self) -> &'facts str {
1055        self.facts.source_facts.source
1056    }
1057
1058    pub(crate) fn line_index(self) -> &'facts LineIndex {
1059        self.facts.source_facts.line_index
1060    }
1061
1062    pub(crate) fn shell(self) -> ShellDialect {
1063        self.facts.source_facts.shell
1064    }
1065
1066    pub(crate) fn script_line_count(self) -> ScriptLineCountFact {
1067        *self.facts.source_facts.script_line_count.get_or_init(|| {
1068            build_script_line_count_fact(
1069                self.facts.source_facts.source,
1070                self.facts.source_facts.line_index,
1071                self.facts.source_facts.comment_index,
1072            )
1073        })
1074    }
1075
1076    pub(crate) fn indented_shebang_span(self) -> Option<Span> {
1077        self.facts.source_facts.indented_shebang_span
1078    }
1079
1080    pub(crate) fn indented_shebang_indent_span(self) -> Option<Span> {
1081        self.facts.source_facts.indented_shebang_indent_span
1082    }
1083
1084    pub(crate) fn space_after_hash_bang_span(self) -> Option<Span> {
1085        self.facts.source_facts.space_after_hash_bang_span
1086    }
1087
1088    pub(crate) fn space_after_hash_bang_whitespace_span(self) -> Option<Span> {
1089        self.facts
1090            .source_facts
1091            .space_after_hash_bang_whitespace_span
1092    }
1093
1094    pub(crate) fn shebang_not_on_first_line_span(self) -> Option<Span> {
1095        self.facts.source_facts.shebang_not_on_first_line_span
1096    }
1097
1098    pub(crate) fn shebang_not_on_first_line_fix_span(self) -> Option<Span> {
1099        self.facts.source_facts.shebang_not_on_first_line_fix_span
1100    }
1101
1102    pub(crate) fn shebang_not_on_first_line_preferred_newline(self) -> Option<&'static str> {
1103        self.facts
1104            .source_facts
1105            .shebang_not_on_first_line_preferred_newline
1106    }
1107
1108    pub(crate) fn missing_shebang_line_span(self) -> Option<Span> {
1109        self.facts.source_facts.missing_shebang_line_span
1110    }
1111
1112    pub(crate) fn duplicate_shebang_flag_span(self) -> Option<Span> {
1113        self.facts.source_facts.duplicate_shebang_flag_span
1114    }
1115
1116    pub(crate) fn non_absolute_shebang_span(self) -> Option<Span> {
1117        self.facts.source_facts.non_absolute_shebang_span
1118    }
1119
1120    pub(crate) fn shebang_interpreter(self) -> Option<&'facts ShebangInterpreterFact> {
1121        self.facts
1122            .source_facts
1123            .shebang_interpreter
1124            .get_or_init(|| {
1125                build_shebang_interpreter_fact(Locator::new(
1126                    self.facts.source_facts.source,
1127                    self.facts.source_facts.line_index,
1128                ))
1129            })
1130            .as_ref()
1131    }
1132
1133    pub(crate) fn shebang_invocation(self) -> Option<&'facts ShebangInvocationFact> {
1134        self.facts
1135            .source_facts
1136            .shebang_invocation
1137            .get_or_init(|| {
1138                build_shebang_invocation_fact(
1139                    self.facts.source_facts.source,
1140                    self.facts.source_facts.line_index,
1141                )
1142            })
1143            .as_ref()
1144    }
1145
1146    pub(crate) fn missing_file_description_comment(self) -> Option<FileDescriptionCommentFact> {
1147        *self
1148            .facts
1149            .source_facts
1150            .missing_file_description_comment
1151            .get_or_init(|| {
1152                build_missing_file_description_comment_fact(
1153                    self.facts.source_facts.source,
1154                    self.facts.source_facts.line_index,
1155                )
1156            })
1157    }
1158
1159    pub(crate) fn errexit_enabled_anywhere(self) -> bool {
1160        self.facts.source_facts.errexit_enabled_anywhere
1161    }
1162
1163    pub(crate) fn commented_continuation_comment_spans(self) -> &'facts [Span] {
1164        &self.facts.source_facts.commented_continuation_comment_spans
1165    }
1166
1167    pub(crate) fn comment_double_quote_nesting_spans(self) -> &'facts [Span] {
1168        &self.facts.source_facts.comment_double_quote_nesting_spans
1169    }
1170
1171    pub(crate) fn escaped_dash_command_name_spans(self) -> &'facts [Span] {
1172        self.facts
1173            .source_facts
1174            .escaped_dash_command_name_spans
1175            .get_or_init(|| {
1176                let mut command_name_offsets = self
1177                    .facts
1178                    .command_facts()
1179                    .structural_commands()
1180                    .filter_map(|command| command.command_name_word())
1181                    .map(|word| word.span.start.offset)
1182                    .collect::<Vec<_>>();
1183                command_name_offsets.sort_unstable();
1184                command_name_offsets.dedup();
1185
1186                build_escaped_dash_command_name_spans(
1187                    self.facts.source_facts.source,
1188                    self.facts.source_facts.line_index,
1189                    self.facts.source_facts.region_index,
1190                    &command_name_offsets,
1191                )
1192            })
1193    }
1194
1195    pub(crate) fn trailing_directive_comment_spans(self) -> &'facts [Span] {
1196        &self.facts.source_facts.trailing_directive_comment_spans
1197    }
1198
1199    pub(crate) fn todo_comment_facts(self) -> &'facts [TodoCommentFact] {
1200        self.facts.source_facts.todo_comment_facts.get_or_init(|| {
1201            build_todo_comment_facts(
1202                self.facts.source_facts.source,
1203                self.facts.source_facts.line_index,
1204                self.facts.source_facts.comment_index,
1205            )
1206        })
1207    }
1208
1209    pub(crate) fn backtick_substitution_spans(self) -> &'facts [Span] {
1210        &self.facts.source_facts.backtick_substitution_spans
1211    }
1212
1213    pub(crate) fn backtick_escaped_parameters(self) -> &'facts [BacktickEscapedParameter] {
1214        &self.facts.source_facts.backtick_escaped_parameters
1215    }
1216
1217    pub(crate) fn backtick_escaped_parameter_reference_spans(self) -> &'facts [Span] {
1218        &self
1219            .facts
1220            .source_facts
1221            .backtick_escaped_parameter_reference_spans
1222    }
1223
1224    pub(crate) fn is_backtick_double_escaped_parameter_reference(self, span: Span) -> bool {
1225        self.facts
1226            .source_facts
1227            .backtick_double_escaped_parameter_spans
1228            .contains(&span)
1229    }
1230
1231    pub(crate) fn dollar_question_after_command_spans(self) -> &'facts [Span] {
1232        &self.facts.source_facts.dollar_question_after_command_spans
1233    }
1234
1235    pub(crate) fn unused_heredoc_spans(self) -> &'facts [Span] {
1236        &self.facts.source_facts.unused_heredoc_spans
1237    }
1238
1239    pub(crate) fn heredoc_missing_end_spans(self) -> &'facts [Span] {
1240        &self.facts.source_facts.heredoc_missing_end_spans
1241    }
1242
1243    pub(crate) fn heredoc_closer_not_alone_spans(self) -> &'facts [Span] {
1244        &self.facts.source_facts.heredoc_closer_not_alone_spans
1245    }
1246
1247    pub(crate) fn misquoted_heredoc_close_spans(self) -> &'facts [Span] {
1248        &self.facts.source_facts.misquoted_heredoc_close_spans
1249    }
1250
1251    pub(crate) fn heredoc_end_space_spans(self) -> &'facts [Span] {
1252        &self.facts.source_facts.heredoc_end_space_spans
1253    }
1254
1255    pub(crate) fn indented_heredoc_close_facts(self) -> &'facts [(Span, Span)] {
1256        self.facts
1257            .source_facts
1258            .indented_heredoc_close_facts
1259            .get_or_init(|| {
1260                let locator = Locator::new(
1261                    self.facts.source_facts.source,
1262                    self.facts.source_facts.line_index,
1263                );
1264                build_indented_heredoc_close_facts(&self.facts.command.commands, locator)
1265            })
1266    }
1267
1268    pub(crate) fn echo_here_doc_spans(self) -> &'facts [Span] {
1269        &self.facts.source_facts.echo_here_doc_spans
1270    }
1271
1272    pub(crate) fn spaced_tabstrip_close_spans(self) -> &'facts [Span] {
1273        &self.facts.source_facts.spaced_tabstrip_close_spans
1274    }
1275}
1276
1277impl<'facts, 'a> WordFacts<'facts, 'a> {
1278    pub(crate) fn is_suppressed_subscript_reference(self, span: Span) -> bool {
1279        self.facts
1280            .words
1281            .suppressed_subscript_reference_spans
1282            .contains(&FactSpan::new(span))
1283    }
1284
1285    #[cfg(test)]
1286    pub(crate) fn is_subscript_later_suppression_reference(self, span: Span) -> bool {
1287        self.facts
1288            .words
1289            .subscript_later_suppression_reference_spans
1290            .contains(&FactSpan::new(span))
1291    }
1292
1293    pub fn word_facts(self) -> WordOccurrenceIter<'facts, 'a> {
1294        WordOccurrenceIter::all(self.facts, WordOccurrenceFilter::NonArithmetic)
1295    }
1296
1297    pub(crate) fn arithmetic_command_word_facts(self) -> WordOccurrenceIter<'facts, 'a> {
1298        WordOccurrenceIter::all(self.facts, WordOccurrenceFilter::ArithmeticCommand)
1299    }
1300
1301    #[cfg(test)]
1302    pub(crate) fn parameter_operand_word_facts(self) -> WordOccurrenceIter<'facts, 'a> {
1303        WordOccurrenceIter::all(self.facts, WordOccurrenceFilter::ParameterOperand)
1304    }
1305
1306    pub(crate) fn is_compound_assignment_value_word(self, fact: WordOccurrenceRef<'_, '_>) -> bool {
1307        self.facts
1308            .words
1309            .compound_assignment_value_word_flags
1310            .get(fact.occurrence_id().index())
1311            .copied()
1312            .unwrap_or(false)
1313    }
1314
1315    pub(crate) fn expansion_word_facts(
1316        self,
1317        context: ExpansionContext,
1318    ) -> WordOccurrenceIter<'facts, 'a> {
1319        WordOccurrenceIter::all(self.facts, WordOccurrenceFilter::Expansion(context))
1320    }
1321
1322    pub(crate) fn case_subject_facts(self) -> WordOccurrenceIter<'facts, 'a> {
1323        WordOccurrenceIter::all(self.facts, WordOccurrenceFilter::CaseSubject)
1324    }
1325
1326    pub fn word_fact(
1327        self,
1328        span: Span,
1329        context: WordFactContext,
1330    ) -> Option<WordOccurrenceRef<'facts, 'a>> {
1331        self.facts
1332            .words
1333            .word_index
1334            .get(&FactSpan::new(span))
1335            .into_iter()
1336            .flat_map(|indices| indices.iter())
1337            .copied()
1338            .map(|id| self.word_occurrence_ref(id))
1339            .find(|fact| fact.context() == context)
1340    }
1341
1342    pub(crate) fn any_word_fact(self, span: Span) -> Option<WordOccurrenceRef<'facts, 'a>> {
1343        self.facts
1344            .words
1345            .word_index
1346            .get(&FactSpan::new(span))
1347            .and_then(|indices| indices.first().copied())
1348            .map(|id| self.word_occurrence_ref(id))
1349    }
1350
1351    pub(crate) fn array_assignment_split_word_facts(self) -> WordOccurrenceIter<'facts, 'a> {
1352        WordOccurrenceIter::ids(
1353            self.facts,
1354            &self.facts.words.array_assignment_split_word_ids,
1355            WordOccurrenceFilter::Any,
1356        )
1357    }
1358
1359    pub(crate) fn word_occurrence_ref(self, id: WordOccurrenceId) -> WordOccurrenceRef<'facts, 'a> {
1360        WordOccurrenceRef {
1361            facts: self.facts,
1362            id,
1363        }
1364    }
1365
1366    pub(crate) fn word_occurrence(self, id: WordOccurrenceId) -> &'facts WordOccurrence {
1367        &self.facts.words.word_occurrences[id.index()]
1368    }
1369
1370    pub(crate) fn word_node(self, id: WordNodeId) -> &'facts WordNode<'a> {
1371        &self.facts.words.word_nodes[id.index()]
1372    }
1373
1374    pub(crate) fn word_node_derived(self, id: WordNodeId) -> &'facts WordNodeDerived<'a> {
1375        word_node_derived(self.word_node(id))
1376    }
1377
1378    pub(crate) fn brace_variable_before_bracket_spans(self) -> &'facts [Span] {
1379        &self.facts.words.brace_variable_before_bracket_spans
1380    }
1381
1382    pub(crate) fn bare_done_word_spans(self) -> &'facts [Span] {
1383        self.facts.words.bare_done_word_spans.get_or_init(|| {
1384            build_bare_done_word_spans(
1385                &self.facts.command.commands,
1386                &self.facts.words.word_nodes,
1387                &self.facts.words.word_occurrences,
1388                self.facts.source_facts.source,
1389            )
1390        })
1391    }
1392
1393    pub(crate) fn array_index_arithmetic_spans(self) -> &'facts [Span] {
1394        &self.facts.words.array_index_arithmetic_spans
1395    }
1396
1397    pub(crate) fn arithmetic_score_line_spans(self) -> &'facts [Span] {
1398        &self.facts.words.arithmetic_score_line_spans
1399    }
1400
1401    pub(crate) fn dollar_in_arithmetic_spans(self) -> &'facts [Span] {
1402        &self.facts.words.dollar_in_arithmetic_spans
1403    }
1404
1405    pub(crate) fn arithmetic_expansion_spans(self) -> &'facts [Span] {
1406        &self.facts.words.arithmetic_expansion_spans
1407    }
1408
1409    pub(crate) fn arithmetic_index_subscript_spans(self) -> &'facts [Span] {
1410        &self.facts.words.arithmetic_index_subscript_spans
1411    }
1412
1413    pub fn single_quoted_fragments(self) -> &'facts [SingleQuotedFragmentFact] {
1414        &self.facts.words.single_quoted_fragments
1415    }
1416
1417    pub(crate) fn dollar_double_quoted_fragments(self) -> &'facts [DollarDoubleQuotedFragmentFact] {
1418        &self.facts.words.dollar_double_quoted_fragments
1419    }
1420
1421    pub(crate) fn open_double_quote_fragments(self) -> &'facts [OpenDoubleQuoteFragmentFact] {
1422        &self.facts.words.open_double_quote_fragments
1423    }
1424
1425    pub(crate) fn suspect_closing_quote_fragments(
1426        self,
1427    ) -> &'facts [SuspectClosingQuoteFragmentFact] {
1428        &self.facts.words.suspect_closing_quote_fragments
1429    }
1430
1431    pub(crate) fn literal_brace_spans(self) -> &'facts [Span] {
1432        &self.facts.words.literal_brace_spans
1433    }
1434
1435    pub fn backtick_fragments(self) -> &'facts [BacktickFragmentFact] {
1436        &self.facts.words.backtick_fragments
1437    }
1438
1439    pub(crate) fn legacy_arithmetic_fragments(self) -> &'facts [LegacyArithmeticFragmentFact] {
1440        &self.facts.words.legacy_arithmetic_fragments
1441    }
1442
1443    pub(crate) fn positional_parameter_fragments(
1444        self,
1445    ) -> &'facts [PositionalParameterFragmentFact] {
1446        &self.facts.words.positional_parameter_fragments
1447    }
1448
1449    pub(crate) fn positional_parameter_operator_spans(self) -> &'facts [Span] {
1450        &self.facts.words.positional_parameter_operator_spans
1451    }
1452
1453    pub(crate) fn double_paren_grouping_spans(self) -> &'facts [Span] {
1454        &self.facts.words.double_paren_grouping_spans
1455    }
1456
1457    pub(crate) fn arithmetic_update_operator_spans(self) -> &'facts [Span] {
1458        &self.facts.words.arithmetic_update_operator_spans
1459    }
1460
1461    pub(crate) fn arithmetic_update_operator_fix_facts(
1462        self,
1463    ) -> &'facts [ArithmeticUpdateOperatorFixFact] {
1464        &self.facts.words.arithmetic_update_operator_fix_facts
1465    }
1466
1467    pub(crate) fn arithmetic_literal_facts(self) -> &'facts [ArithmeticLiteralFact] {
1468        &self.facts.words.arithmetic_literal_facts
1469    }
1470
1471    pub(crate) fn escape_scan_matches(self) -> &'facts [EscapeScanMatch] {
1472        &self.facts.words.escape_scan_matches
1473    }
1474
1475    pub(crate) fn echo_backslash_escape_word_spans(self) -> &'facts [Span] {
1476        &self.facts.words.echo_backslash_escape_word_spans
1477    }
1478
1479    pub(crate) fn echo_to_sed_substitution_spans(self) -> &'facts [Span] {
1480        &self.facts.words.echo_to_sed_substitution_spans
1481    }
1482
1483    pub(crate) fn arithmetic_command_substitution_spans(self) -> &'facts [Span] {
1484        &self.facts.words.arithmetic_command_substitution_spans
1485    }
1486
1487    pub(crate) fn arithmetic_only_suppressed_subscript_spans(self) -> &'facts [Span] {
1488        &self.facts.words.arithmetic_only_suppressed_subscript_spans
1489    }
1490
1491    pub(crate) fn unicode_smart_quote_spans(self) -> &'facts [Span] {
1492        &self.facts.words.unicode_smart_quote_spans
1493    }
1494
1495    #[cfg(test)]
1496    pub(crate) fn pattern_literal_spans(self) -> &'facts [Span] {
1497        &self.facts.words.pattern_literal_spans
1498    }
1499
1500    pub fn pattern_charclass_spans(self) -> &'facts [Span] {
1501        &self.facts.words.pattern_charclass_spans
1502    }
1503
1504    pub(crate) fn nested_parameter_expansion_fragments(
1505        self,
1506    ) -> &'facts [NestedParameterExpansionFragmentFact] {
1507        &self.facts.words.nested_parameter_expansion_fragments
1508    }
1509
1510    pub(crate) fn indirect_expansion_fragments(self) -> &'facts [IndirectExpansionFragmentFact] {
1511        &self.facts.words.indirect_expansion_fragments
1512    }
1513
1514    pub(crate) fn indexed_array_reference_fragments(
1515        self,
1516    ) -> &'facts [IndexedArrayReferenceFragmentFact] {
1517        &self.facts.words.indexed_array_reference_fragments
1518    }
1519
1520    pub(crate) fn plain_unindexed_array_references(
1521        self,
1522    ) -> impl Iterator<Item = PlainUnindexedArrayReferenceFact> + 'facts {
1523        self.facts
1524            .words
1525            .plain_unindexed_array_references
1526            .get_or_init(|| build_plain_unindexed_array_reference_facts(self.facts))
1527            .iter()
1528            .copied()
1529    }
1530
1531    pub(crate) fn parameter_pattern_special_target_fragments(
1532        self,
1533    ) -> &'facts [ParameterPatternSpecialTargetFragmentFact] {
1534        &self.facts.words.parameter_pattern_special_target_fragments
1535    }
1536
1537    pub(crate) fn zsh_parameter_index_flag_fragments(
1538        self,
1539    ) -> &'facts [ZshParameterIndexFlagFragmentFact] {
1540        &self.facts.words.zsh_parameter_index_flag_fragments
1541    }
1542
1543    pub fn substring_expansion_fragments(self) -> &'facts [SubstringExpansionFragmentFact] {
1544        &self.facts.words.substring_expansion_fragments
1545    }
1546
1547    pub fn case_modification_fragments(self) -> &'facts [CaseModificationFragmentFact] {
1548        &self.facts.words.case_modification_fragments
1549    }
1550
1551    pub fn replacement_expansion_fragments(self) -> &'facts [ReplacementExpansionFragmentFact] {
1552        &self.facts.words.replacement_expansion_fragments
1553    }
1554
1555    pub(crate) fn positional_parameter_trim_fragments(
1556        self,
1557    ) -> &'facts [PositionalParameterTrimFragmentFact] {
1558        &self.facts.words.positional_parameter_trim_fragments
1559    }
1560}
1561
1562impl<'facts, 'a> CompatFacts<'facts, 'a> {
1563    pub(crate) fn conditional_portability(self) -> &'facts ConditionalPortabilityFacts {
1564        &self.facts.compat.conditional_portability
1565    }
1566
1567    pub(crate) fn possible_variable_misspelling_scope_compat_name_uses(
1568        self,
1569    ) -> &'facts [ComparableNameUse] {
1570        self.facts
1571            .compat
1572            .possible_variable_misspelling_scope_compat_name_uses
1573            .get_or_init(|| build_possible_variable_misspelling_scope_compat_name_uses(self.facts))
1574    }
1575}
1576
1577#[derive(Debug, Clone, Copy, Default)]
1578pub(crate) struct FunctionDocSections {
1579    globals: bool,
1580    arguments: bool,
1581    outputs: bool,
1582    returns: bool,
1583}
1584
1585impl FunctionDocSections {
1586    pub(crate) fn record_comment_body(&mut self, body: &str) {
1587        let lower = body.trim().to_ascii_lowercase();
1588        if lower.starts_with("globals:") {
1589            self.globals = true;
1590        } else if lower.starts_with("arguments:") {
1591            self.arguments = true;
1592        } else if lower.starts_with("outputs:") {
1593            self.outputs = true;
1594        } else if lower.starts_with("returns:") {
1595            self.returns = true;
1596        }
1597    }
1598
1599    pub(crate) fn has_globals(self) -> bool {
1600        self.globals
1601    }
1602
1603    pub(crate) fn has_arguments(self) -> bool {
1604        self.arguments
1605    }
1606
1607    pub(crate) fn has_outputs(self) -> bool {
1608        self.outputs
1609    }
1610
1611    pub(crate) fn has_returns(self) -> bool {
1612        self.returns
1613    }
1614}
1615
1616#[derive(Debug, Clone)]
1617pub(crate) struct FunctionDocContentFact {
1618    name: Name,
1619    name_span: Span,
1620    body_line_count: usize,
1621    leading_comment_span: Option<Span>,
1622    documented_sections: FunctionDocSections,
1623    body_behavior: FunctionDocBodyBehavior,
1624}
1625
1626impl FunctionDocContentFact {
1627    pub(crate) fn new(
1628        name: &Name,
1629        name_span: Span,
1630        body_line_count: usize,
1631        leading_comment_span: Option<Span>,
1632        documented_sections: FunctionDocSections,
1633        body_behavior: FunctionDocBodyBehavior,
1634    ) -> Self {
1635        Self {
1636            name: name.clone(),
1637            name_span,
1638            body_line_count,
1639            leading_comment_span,
1640            documented_sections,
1641            body_behavior,
1642        }
1643    }
1644
1645    pub(crate) fn name(&self) -> &Name {
1646        &self.name
1647    }
1648
1649    pub(crate) fn name_span(&self) -> Span {
1650        self.name_span
1651    }
1652
1653    pub(crate) fn body_line_count(&self) -> usize {
1654        self.body_line_count
1655    }
1656
1657    pub(crate) fn has_leading_comment(&self) -> bool {
1658        self.leading_comment_span.is_some()
1659    }
1660
1661    pub(crate) fn documented_sections(&self) -> FunctionDocSections {
1662        self.documented_sections
1663    }
1664
1665    pub(crate) fn uses_global_variables(&self) -> bool {
1666        self.body_behavior.uses_global_variables()
1667    }
1668
1669    pub(crate) fn uses_positional_parameters(&self) -> bool {
1670        self.body_behavior.uses_positional_parameters()
1671    }
1672
1673    pub(crate) fn uses_any_positional_parameters(&self) -> bool {
1674        self.body_behavior.uses_any_positional_parameters()
1675    }
1676
1677    pub(crate) fn writes_stdout(&self) -> bool {
1678        self.body_behavior.writes_stdout()
1679    }
1680
1681    pub(crate) fn has_explicit_return(&self) -> bool {
1682        self.body_behavior.has_explicit_return()
1683    }
1684}
1685
1686#[derive(Debug, Clone, Copy, Default)]
1687pub(crate) struct FunctionDocBodyBehavior {
1688    uses_global_variables: bool,
1689    uses_positional_parameters: bool,
1690    uses_any_positional_parameters: bool,
1691    writes_stdout: bool,
1692    has_explicit_return: bool,
1693}
1694
1695impl FunctionDocBodyBehavior {
1696    pub(crate) fn new(
1697        uses_global_variables: bool,
1698        uses_positional_parameters: bool,
1699        uses_any_positional_parameters: bool,
1700        writes_stdout: bool,
1701        has_explicit_return: bool,
1702    ) -> Self {
1703        Self {
1704            uses_global_variables,
1705            uses_positional_parameters,
1706            uses_any_positional_parameters,
1707            writes_stdout,
1708            has_explicit_return,
1709        }
1710    }
1711
1712    pub(crate) fn uses_global_variables(self) -> bool {
1713        self.uses_global_variables
1714    }
1715
1716    pub(crate) fn uses_positional_parameters(self) -> bool {
1717        self.uses_positional_parameters
1718    }
1719
1720    pub(crate) fn uses_any_positional_parameters(self) -> bool {
1721        self.uses_any_positional_parameters
1722    }
1723
1724    pub(crate) fn writes_stdout(self) -> bool {
1725        self.writes_stdout
1726    }
1727
1728    pub(crate) fn has_explicit_return(self) -> bool {
1729        self.has_explicit_return
1730    }
1731}
1732
1733fn stmt_is_plain_background(stmt: &Stmt) -> bool {
1734    matches!(
1735        stmt.terminator,
1736        Some(StmtTerminator::Background(BackgroundOperator::Plain))
1737    )
1738}
1739
1740fn binary_is_two_segment_self_pipe(binary: &BinaryCommand, name: &str, source: &str) -> bool {
1741    let Some(chain) = BinaryCommandChain::pipeline(binary) else {
1742        return false;
1743    };
1744
1745    let mut segments = Vec::new();
1746    let mut operators = Vec::new();
1747    chain.visit_parts(
1748        |segment| segments.push(segment),
1749        |operator| operators.push(operator),
1750    );
1751
1752    matches!(operators.as_slice(), [operator] if operator.op == BinaryOp::Pipe)
1753        && matches!(segments.as_slice(), [left, right] if {
1754            stmt_is_zero_arg_call_to(left, name, source)
1755                && stmt_is_zero_arg_call_to(right, name, source)
1756        })
1757}
1758
1759fn stmt_is_zero_arg_call_to(stmt: &Stmt, name: &str, source: &str) -> bool {
1760    let Command::Simple(command) = &stmt.command else {
1761        return false;
1762    };
1763
1764    command.assignments.is_empty()
1765        && command.args.is_empty()
1766        && static_command_name_text(&command.name, source).as_deref() == Some(name)
1767}
1768
1769#[derive(Debug, Clone, PartialEq, Eq)]
1770pub(crate) struct TodoCommentFact {
1771    content_span: Span,
1772    content: String,
1773}
1774
1775impl TodoCommentFact {
1776    pub(crate) fn new(content_span: Span, content: String) -> Self {
1777        Self {
1778            content_span,
1779            content,
1780        }
1781    }
1782
1783    pub(crate) fn content(&self) -> &str {
1784        &self.content
1785    }
1786
1787    pub(crate) fn marker_span(&self, marker: &str) -> Span {
1788        Span::from_positions(
1789            self.content_span.start,
1790            self.content_span.start.advanced_by(marker),
1791        )
1792    }
1793}
1794
1795pub(crate) fn build_possible_variable_misspelling_scope_compat_name_uses(
1796    facts: &LinterFacts<'_>,
1797) -> Vec<ComparableNameUse> {
1798    let source_facts = facts.source_facts();
1799    let source = source_facts.source();
1800    if !source_may_have_scope_compat_misspelling(source) {
1801        return Vec::new();
1802    }
1803
1804    let mut uses = Vec::new();
1805    for word_fact in facts
1806        .words()
1807        .expansion_word_facts(ExpansionContext::DeclarationAssignmentValue)
1808        .chain(
1809            facts
1810                .words()
1811                .expansion_word_facts(ExpansionContext::AssignmentValue),
1812        )
1813        .chain(facts.words().case_subject_facts())
1814    {
1815        if let Some(name_use) = scope_compat_standalone_parameter_name_use(word_fact.word()) {
1816            uses.push(name_use);
1817        }
1818    }
1819    for command in facts.commands() {
1820        visit_command_words_for_substitutions(
1821            command.command(),
1822            command.redirects(),
1823            source,
1824            &mut |word| {
1825                collect_scope_compat_derived_name_uses(
1826                    word,
1827                    facts.semantic_artifacts,
1828                    source,
1829                    &mut uses,
1830                );
1831            },
1832        );
1833    }
1834    for word in facts
1835        .command_facts()
1836        .for_headers()
1837        .iter()
1838        .flat_map(|header| header.words())
1839        .chain(
1840            facts
1841                .command_facts()
1842                .select_headers()
1843                .iter()
1844                .flat_map(|header| header.words()),
1845        )
1846    {
1847        if let Some(mut name_use) = scope_compat_standalone_parameter_name_use(word.word()) {
1848            name_use.mark_derived();
1849            if is_interesting_scope_compat_name_use(
1850                source,
1851                name_use.key().as_str(),
1852                name_use.kind(),
1853                name_use.span(),
1854            ) {
1855                uses.push(name_use);
1856            }
1857        }
1858    }
1859    uses.extend(
1860        build_flag_for_loop_source_name_uses(Locator::new(source, source_facts.line_index()))
1861            .into_iter()
1862            .filter(|name_use| {
1863                is_interesting_scope_compat_name_use(
1864                    source,
1865                    name_use.key().as_str(),
1866                    name_use.kind(),
1867                    name_use.span(),
1868                )
1869            }),
1870    );
1871    dedup_comparable_name_uses(&mut uses);
1872    uses
1873}
1874
1875pub(crate) fn source_may_have_scope_compat_misspelling(source: &str) -> bool {
1876    source.contains("SHELLSPEC_EXECDIR")
1877        || source.contains("CFLAGS")
1878        || source.contains("CPPFLAGS")
1879        || source.contains("CXXFLAGS")
1880}
1881
1882pub(crate) fn scope_compat_standalone_parameter_name_use(word: &Word) -> Option<ComparableNameUse> {
1883    let name = standalone_comparable_parameter_name(&word.parts)?;
1884    Some(ComparableNameUse {
1885        span: word.span,
1886        key: ComparableNameKey(name.into()),
1887        kind: ComparableNameUseKind::Parameter,
1888    })
1889}
1890
1891pub(crate) fn collect_scope_compat_derived_name_uses(
1892    word: &Word,
1893    semantic: &LinterSemanticArtifacts<'_>,
1894    source: &str,
1895    uses: &mut Vec<ComparableNameUse>,
1896) {
1897    let allow_quoted_derived_words =
1898        analyze_word(word, source, None).quote == WordQuote::FullyQuoted;
1899    collect_scope_compat_command_substitution_name_uses_in_parts(
1900        &word.parts,
1901        semantic,
1902        source,
1903        allow_quoted_derived_words,
1904        uses,
1905    );
1906}
1907
1908pub(crate) fn collect_scope_compat_command_substitution_name_uses_in_parts(
1909    parts: &[WordPartNode],
1910    semantic: &LinterSemanticArtifacts<'_>,
1911    source: &str,
1912    allow_quoted_derived_words: bool,
1913    uses: &mut Vec<ComparableNameUse>,
1914) {
1915    for part in parts {
1916        match &part.kind {
1917            WordPart::DoubleQuoted { parts, .. } => {
1918                collect_scope_compat_command_substitution_name_uses_in_parts(
1919                    parts,
1920                    semantic,
1921                    source,
1922                    allow_quoted_derived_words,
1923                    uses,
1924                );
1925            }
1926            WordPart::CommandSubstitution { body, .. } => {
1927                collect_scope_compat_command_substitution_name_uses(
1928                    body,
1929                    semantic,
1930                    source,
1931                    allow_quoted_derived_words,
1932                    uses,
1933                );
1934            }
1935            WordPart::ParameterExpansion {
1936                operand_word_ast, ..
1937            }
1938            | WordPart::IndirectExpansion {
1939                operand_word_ast, ..
1940            } => {
1941                if let Some(word) = operand_word_ast {
1942                    collect_scope_compat_command_substitution_name_uses_in_parts(
1943                        &word.parts,
1944                        semantic,
1945                        source,
1946                        allow_quoted_derived_words,
1947                        uses,
1948                    );
1949                }
1950            }
1951            WordPart::Substring {
1952                offset_word_ast,
1953                length_word_ast,
1954                ..
1955            }
1956            | WordPart::ArraySlice {
1957                offset_word_ast,
1958                length_word_ast,
1959                ..
1960            } => {
1961                collect_scope_compat_arithmetic_name_use(offset_word_ast, source, uses);
1962                if let Some(word) = length_word_ast {
1963                    collect_scope_compat_arithmetic_name_use(word, source, uses);
1964                }
1965            }
1966            WordPart::ArithmeticExpansion {
1967                expression_word_ast,
1968                ..
1969            } => {
1970                collect_scope_compat_arithmetic_name_use(expression_word_ast, source, uses);
1971            }
1972            WordPart::Literal(_)
1973            | WordPart::ZshQualifiedGlob(_)
1974            | WordPart::SingleQuoted { .. }
1975            | WordPart::Variable(_)
1976            | WordPart::Parameter(_)
1977            | WordPart::Length(_)
1978            | WordPart::ArrayAccess(_)
1979            | WordPart::ArrayLength(_)
1980            | WordPart::ArrayIndices(_)
1981            | WordPart::PrefixMatch { .. }
1982            | WordPart::ProcessSubstitution { .. }
1983            | WordPart::Transformation { .. } => {}
1984        }
1985    }
1986}
1987
1988pub(crate) fn collect_scope_compat_command_substitution_name_uses(
1989    body: &StmtSeq,
1990    semantic: &LinterSemanticArtifacts<'_>,
1991    source: &str,
1992    allow_quoted_derived_words: bool,
1993    uses: &mut Vec<ComparableNameUse>,
1994) {
1995    visit_command_substitution_candidate_words(body, semantic, source, &mut |word| {
1996        push_scope_compat_command_substitution_word_use(
1997            word,
1998            source,
1999            allow_quoted_derived_words,
2000            uses,
2001        );
2002    });
2003}
2004
2005pub(crate) fn push_scope_compat_command_substitution_word_use(
2006    word: &Word,
2007    source: &str,
2008    allow_quoted_derived_words: bool,
2009    uses: &mut Vec<ComparableNameUse>,
2010) {
2011    if !allow_quoted_derived_words
2012        && analyze_word(word, source, None).quote == WordQuote::FullyQuoted
2013    {
2014        return;
2015    }
2016    if let Some(name_use) = scope_compat_standalone_derived_name_use(word, source) {
2017        uses.push(name_use);
2018    }
2019}
2020
2021pub(crate) fn collect_scope_compat_arithmetic_name_use(
2022    word: &Word,
2023    source: &str,
2024    uses: &mut Vec<ComparableNameUse>,
2025) {
2026    if let Some(name_use) = scope_compat_standalone_derived_name_use(word, source) {
2027        uses.push(name_use);
2028    }
2029}
2030
2031pub(crate) fn scope_compat_standalone_derived_name_use(
2032    word: &Word,
2033    source: &str,
2034) -> Option<ComparableNameUse> {
2035    let mut name_use = scope_compat_standalone_parameter_name_use(word)?;
2036    name_use.mark_derived();
2037    is_interesting_scope_compat_name_use(
2038        source,
2039        name_use.key().as_str(),
2040        name_use.kind(),
2041        name_use.span(),
2042    )
2043    .then_some(name_use)
2044}
2045
2046pub(crate) fn is_interesting_scope_compat_name_use(
2047    _source: &str,
2048    name: &str,
2049    kind: ComparableNameUseKind,
2050    _span: Span,
2051) -> bool {
2052    name == "SHELLSPEC_EXECDIR"
2053        || name == "SHELLSPEC_SPECDIR"
2054        || kind == ComparableNameUseKind::Derived && is_reportable_build_flag_family_name(name)
2055}
2056
2057pub(crate) fn is_reportable_build_flag_family_name(name: &str) -> bool {
2058    let Some((_, suffix)) = split_scope_compat_build_flag_family_name(name) else {
2059        return false;
2060    };
2061    matches!(suffix, "CFLAGS" | "CPPFLAGS" | "CXXFLAGS")
2062}
2063
2064pub(crate) fn split_scope_compat_build_flag_family_name(
2065    name: &str,
2066) -> Option<(&str, &'static str)> {
2067    ["CXXFLAGS", "CPPFLAGS", "CFLAGS"]
2068        .into_iter()
2069        .find_map(|suffix| {
2070            if name == suffix {
2071                Some(("", suffix))
2072            } else {
2073                name.strip_suffix(suffix)
2074                    .filter(|prefix| prefix.ends_with('_'))
2075                    .map(|prefix| (prefix, suffix))
2076            }
2077        })
2078}
2079
2080#[cfg_attr(shuck_profiling, inline(never))]
2081pub(crate) fn populate_array_assignment_split_scalar_expansion_spans(
2082    shell: ShellDialect,
2083    commands: &[CommandFact<'_>],
2084    word_nodes: &[WordNode<'_>],
2085    word_occurrences: &mut [WordOccurrence],
2086    fact_store: &mut FactStore<'_>,
2087    word_ids: &[WordOccurrenceId],
2088) {
2089    if word_ids.is_empty() {
2090        return;
2091    }
2092    let has_brace_expansion = shell_has_brace_expansion(shell);
2093    let mut split_sensitive_spans = Vec::new();
2094    let mut use_replacement_spans = Vec::new();
2095    let mut brace_expansion_spans = Vec::new();
2096    for id in word_ids.iter().copied() {
2097        collect_array_assignment_split_scalar_expansion_spans(
2098            id,
2099            commands,
2100            word_nodes,
2101            word_occurrences,
2102            fact_store,
2103            has_brace_expansion,
2104            &mut split_sensitive_spans,
2105            &mut use_replacement_spans,
2106            &mut brace_expansion_spans,
2107        );
2108        word_occurrences[id.index()].array_assignment_split_scalar_expansion_spans = fact_store
2109            .word_spans
2110            .push_many(split_sensitive_spans.drain(..));
2111    }
2112}
2113
2114#[allow(clippy::too_many_arguments)]
2115pub(crate) fn collect_array_assignment_split_scalar_expansion_spans(
2116    id: WordOccurrenceId,
2117    commands: &[CommandFact<'_>],
2118    word_nodes: &[WordNode<'_>],
2119    word_occurrences: &[WordOccurrence],
2120    fact_store: &FactStore<'_>,
2121    has_brace_expansion: bool,
2122    split_sensitive_spans: &mut Vec<Span>,
2123    use_replacement_spans: &mut Vec<Span>,
2124    brace_expansion_spans: &mut Vec<Span>,
2125) {
2126    split_sensitive_spans.clear();
2127    let fact = &word_occurrences[id.index()];
2128    let word = occurrence_word(word_nodes, fact);
2129    let derived = word_node_derived(&word_nodes[fact.node_id.index()]);
2130    if word_nodes[fact.node_id.index()]
2131        .analysis
2132        .can_expand_to_multiple_fields
2133    {
2134        split_sensitive_spans.extend(
2135            fact_store
2136                .word_spans(derived.unquoted_scalar_expansion_spans)
2137                .iter()
2138                .copied(),
2139        );
2140    }
2141
2142    let fact_span = occurrence_span(word_nodes, fact);
2143    let unquoted_command_substitution_spans =
2144        fact_store.word_spans(fact.split_sensitive_unquoted_command_substitution_spans);
2145
2146    if !unquoted_command_substitution_spans.is_empty() {
2147        // commands is sorted by start offset (compare_command_facts_by_offset),
2148        // so binary-search the subrange whose start lies inside fact_span.
2149        let start = commands.partition_point(|c| c.span().start.offset < fact_span.start.offset);
2150        for command in &commands[start..] {
2151            let command_span = command.span();
2152            if command_span.start.offset > fact_span.end.offset {
2153                break;
2154            }
2155            if !contains_span_strictly(fact_span, command_span) {
2156                continue;
2157            }
2158            if !unquoted_command_substitution_spans
2159                .iter()
2160                .any(|span| contains_span_strictly(*span, command_span))
2161            {
2162                continue;
2163            }
2164            for nested_id in fact_store.word_occurrence_ids_for_command(command.id()) {
2165                let nested = &word_occurrences[nested_id.index()];
2166                let nested_derived = word_node_derived(&word_nodes[nested.node_id.index()]);
2167                split_sensitive_spans.extend(
2168                    fact_store
2169                        .word_spans(nested_derived.scalar_expansion_spans)
2170                        .iter()
2171                        .copied(),
2172                );
2173            }
2174        }
2175    }
2176
2177    if split_sensitive_spans.is_empty() {
2178        return;
2179    }
2180
2181    use_replacement_spans.clear();
2182    collect_use_replacement_expansion_spans(&word.parts, use_replacement_spans);
2183
2184    brace_expansion_spans.clear();
2185    if has_brace_expansion {
2186        brace_expansion_spans.extend(
2187            word.brace_syntax()
2188                .iter()
2189                .filter(|brace| brace.expands())
2190                .map(|brace| brace.span),
2191        );
2192    }
2193
2194    split_sensitive_spans.retain(|span| {
2195        !use_replacement_spans
2196            .iter()
2197            .any(|replacement_span| contains_span(*replacement_span, *span))
2198            && !brace_expansion_spans
2199                .iter()
2200                .any(|brace_span| contains_span(*brace_span, *span))
2201    });
2202    sort_and_dedup_spans(split_sensitive_spans);
2203}
2204
2205pub(crate) fn shell_has_brace_expansion(shell: ShellDialect) -> bool {
2206    matches!(
2207        shell,
2208        ShellDialect::Bash | ShellDialect::Ksh | ShellDialect::Mksh | ShellDialect::Zsh
2209    )
2210}
2211
2212pub(crate) fn build_flag_for_loop_source_name_uses(locator: Locator<'_>) -> Vec<ComparableNameUse> {
2213    let source = locator.source();
2214    let mut uses = Vec::new();
2215    for line_number in 1..=locator.line_index().line_count() {
2216        let Some(line_range) = locator.line_range(line_number) else {
2217            continue;
2218        };
2219        let line_start = usize::from(line_range.start());
2220        let line = line_range.slice(source).trim_end_matches('\r');
2221        let trimmed = line.trim_start();
2222        let leading_whitespace = line.len() - trimmed.len();
2223        if let Some(after_for) = trimmed.strip_prefix("for ")
2224            && let Some(in_offset) = after_for.find(" in ")
2225        {
2226            let list = &after_for[in_offset + 4..];
2227            if let Some(name_start_in_list) = list.find("${") {
2228                let name_start = line_start
2229                    + leading_whitespace
2230                    + "for ".len()
2231                    + in_offset
2232                    + 4
2233                    + name_start_in_list
2234                    + 2;
2235                let name_text = &source[name_start..];
2236                let name_len = name_text
2237                    .bytes()
2238                    .take_while(|byte| byte.is_ascii_alphanumeric() || *byte == b'_')
2239                    .count();
2240                let name = &source[name_start..name_start + name_len];
2241                if is_build_flag_source_name(name)
2242                    && source
2243                        .as_bytes()
2244                        .get(name_start + name_len)
2245                        .is_some_and(|byte| *byte == b'}')
2246                    && let (Some(start), Some(end)) = (
2247                        locator.position_at_offset(name_start - 2),
2248                        locator.position_at_offset(name_start + name_len + 1),
2249                    )
2250                {
2251                    uses.push(ComparableNameUse {
2252                        span: Span::from_positions(start, end),
2253                        key: ComparableNameKey(name.into()),
2254                        kind: ComparableNameUseKind::Derived,
2255                    });
2256                }
2257            }
2258        }
2259    }
2260    uses
2261}
2262
2263pub(crate) fn is_build_flag_source_name(name: &str) -> bool {
2264    matches!(
2265        name,
2266        "CFLAGS" | "CXXFLAGS" | "CPPFLAGS" | "LDFLAGS" | "GOFLAGS"
2267    ) || name.ends_with("_CFLAGS")
2268        || name.ends_with("_CXXFLAGS")
2269        || name.ends_with("_CPPFLAGS")
2270        || name.ends_with("_LDFLAGS")
2271}
2272
2273pub(crate) fn assignment_value_span(value: &AssignmentValue) -> Option<Span> {
2274    match value {
2275        AssignmentValue::Scalar(word) => Some(word.span),
2276        AssignmentValue::Compound(_) => None,
2277    }
2278}
2279
2280pub(crate) struct AssignmentValueTargetEntry {
2281    value_start: usize,
2282    value_end: usize,
2283    target_name: Name,
2284}
2285
2286// Sorted ascending by `value_start`. Scalar assignment value spans are word-level
2287// and do not overlap, so a query span is contained by at most one entry.
2288pub(crate) type AssignmentValueTargetIndex = Vec<AssignmentValueTargetEntry>;
2289
2290pub(crate) fn build_assignment_value_target_index(
2291    commands: &[CommandFact<'_>],
2292) -> AssignmentValueTargetIndex {
2293    let mut entries = Vec::<AssignmentValueTargetEntry>::new();
2294    for command in commands {
2295        let cmd = command.command();
2296        for assignment in command_assignments(cmd) {
2297            push_assignment_value_target_entry(&mut entries, assignment);
2298        }
2299        for operand in declaration_operands(cmd) {
2300            if let DeclOperand::Assignment(assignment) = operand {
2301                push_assignment_value_target_entry(&mut entries, assignment);
2302            }
2303        }
2304    }
2305    entries.sort_by_key(|entry| (entry.value_start, entry.value_end));
2306    entries.dedup_by(|a, b| {
2307        a.value_start == b.value_start
2308            && a.value_end == b.value_end
2309            && a.target_name == b.target_name
2310    });
2311    entries
2312}
2313
2314pub(crate) fn push_assignment_value_target_entry(
2315    entries: &mut Vec<AssignmentValueTargetEntry>,
2316    assignment: &Assignment,
2317) {
2318    if let Some(value_span) = assignment_value_span(&assignment.value) {
2319        entries.push(AssignmentValueTargetEntry {
2320            value_start: value_span.start.offset,
2321            value_end: value_span.end.offset,
2322            target_name: assignment.target.name.clone(),
2323        });
2324    }
2325}