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 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
2286pub(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}