1#![warn(missing_docs)]
2#![cfg_attr(not(test), warn(clippy::unwrap_used))]
3
4mod analysis;
9mod array_use;
10mod binding;
11mod builder;
12mod call_graph;
13mod cfg;
14mod command_topology;
15mod contract;
16mod dataflow;
17mod declaration;
18mod dense_bit_set;
19mod editor;
20mod function_call_reachability;
21mod function_resolution;
22mod glob;
23mod nonpersistent;
24mod reachability;
25mod reference;
26mod runtime;
27mod scope;
28mod source_closure;
29mod source_ref;
30mod uninitialized;
31mod unused;
32mod value_flow;
33mod zsh_options;
34mod zsh_plugin_framework;
35
36pub use binding::{
38 AssignmentValueOrigin, Binding, BindingAttributes, BindingId, BindingKind, BindingOrigin,
39 BuiltinBindingTargetKind, LoopValueOrigin,
40};
41pub use call_graph::{
43 CallGraph, CallSite, OverwrittenFunction, UnreachedFunction, UnreachedFunctionReason,
44};
45pub use cfg::{
47 BasicBlock, BlockId, BuiltinCommandKind, CommandConditionRole, CommandId, CommandKind,
48 CompoundCommandKind, ControlFlowGraph, EdgeKind, FlowContext, StatementSequenceCommand,
49 UnreachableCauseKind,
50};
51pub use command_topology::{
53 SemanticCommandContext, SemanticListCommand, SemanticListOperator, SemanticListOperatorKind,
54 SemanticListSegment, SemanticPipelineCommand, SemanticPipelineOperator,
55 SemanticPipelineOperatorKind, SemanticPipelineSegment,
56};
57pub use contract::{
59 ContractCertainty, FileContract, FileEntryBindingInitialization, FileEntryContractCollector,
60 FileEntryContractCollectorFactory, FunctionContract, ProvidedBinding, ProvidedBindingKind,
61 SemanticBuildOptions,
62};
63pub use dataflow::{
65 DeadCode, ReachingDefinitions, UninitializedCertainty, UninitializedReference,
66 UnusedAssignment, UnusedReason,
67};
68pub use declaration::{Declaration, DeclarationBuiltin, DeclarationOperand};
70pub use editor::{
72 EditorCompletion, EditorCompletionKind, EditorCompletionOptions, EditorCompletions,
73 EditorDocumentSymbol, EditorFunctionCallTarget, EditorHover, EditorOccurrence,
74 EditorOccurrenceKind, EditorQuery, EditorRuntimeNameTarget, EditorSymbol, EditorSymbolKind,
75 EditorSymbolTarget, RenameSet, RenameUnavailable,
76};
77pub use function_call_reachability::{
79 DirectFunctionCallReachability, DirectFunctionCallWindow, FunctionCallCandidate,
80 FunctionCallPersistence,
81};
82pub use glob::{
84 BraceCharacterClassBehavior, FieldSplittingBehavior, FileExpansionOrderBehavior,
85 GlobDotBehavior, GlobFailureBehavior, GlobPatternBehavior, PathnameExpansionBehavior,
86 PatternOperatorBehavior,
87};
88pub use nonpersistent::{
90 NonpersistentAssignmentAnalysis, NonpersistentAssignmentAnalysisContext,
91 NonpersistentAssignmentAnalysisOptions, NonpersistentAssignmentCommandContext,
92 NonpersistentAssignmentEffect, NonpersistentAssignmentExtraRead, NonpersistentLaterUseKind,
93};
94pub use reference::{Reference, ReferenceId, ReferenceKind};
96pub use scope::{FunctionScopeKind, Scope, ScopeId, ScopeKind};
98pub use shuck_parser::{OptionValue, ShellDialect, ShellProfile, ZshEmulationMode, ZshOptionState};
100pub use source_closure::{layout_for_plugin_framework, zsh_plugin_frameworks};
102pub use source_ref::{SourceRef, SourceRefDiagnosticClass, SourceRefKind, SourceRefResolution};
104pub use value_flow::SemanticValueFlow;
106pub use zsh_plugin_framework::{
108 ZshPluginFramework, resolve_zsh_plugin_entrypoint, resolve_zsh_plugin_source_paths,
109 zsh_plugin_framework_from_name, zsh_plugin_root_keys,
110};
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum ArrayReferencePolicy {
115 RequiresExplicitSelector,
117 NativeZshScalar,
119 Ambiguous,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum SubscriptIndexBehavior {
126 OneBased,
128 ZeroBased,
130 OneBasedWithZeroAlias,
132 Ambiguous,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum ArithmeticLiteralBehavior {
139 DecimalUnlessExplicitBase,
141 LeadingZeroOctal,
143 CStyleAndLeadingZeroOctal,
145 Ambiguous,
147}
148
149#[derive(Debug, Clone)]
151pub struct ShellBehaviorAt<'model> {
152 shell: ShellDialect,
153 zsh_options: Option<&'model ZshOptionState>,
154 runtime_options: Option<ZshOptionState>,
155 zsh_sh_emulation: Option<bool>,
156}
157
158impl ShellBehaviorAt<'_> {
159 fn effective_zsh_options(&self) -> Option<&ZshOptionState> {
160 self.runtime_options.as_ref().or(self.zsh_options)
161 }
162
163 pub fn zsh_options(&self) -> Option<&ZshOptionState> {
165 self.effective_zsh_options()
166 }
167
168 pub fn shell_dialect(&self) -> ShellDialect {
170 self.shell
171 }
172
173 pub fn zsh_sh_emulation(&self) -> bool {
175 self.shell == ShellDialect::Zsh && self.zsh_sh_emulation.unwrap_or(false)
176 }
177
178 pub fn for_dialect(shell: ShellDialect) -> Self {
180 Self {
181 shell,
182 zsh_options: None,
183 runtime_options: None,
184 zsh_sh_emulation: None,
185 }
186 }
187
188 pub fn with_zsh_option_overlay(mut self, overlay: impl FnOnce(&mut ZshOptionState)) -> Self {
194 if self.shell == ShellDialect::Zsh {
195 let mut options = self
196 .effective_zsh_options()
197 .cloned()
198 .unwrap_or_else(ZshOptionState::zsh_default);
199 overlay(&mut options);
200 self.runtime_options = Some(options);
201 }
202 self
203 }
204
205 pub fn array_reference_policy(&self) -> ArrayReferencePolicy {
207 if self.shell != ShellDialect::Zsh {
208 return ArrayReferencePolicy::RequiresExplicitSelector;
209 }
210
211 match self
212 .effective_zsh_options()
213 .map(|options| options.ksh_arrays)
214 {
215 Some(OptionValue::Off) => ArrayReferencePolicy::NativeZshScalar,
216 Some(OptionValue::Unknown) => ArrayReferencePolicy::Ambiguous,
217 Some(OptionValue::On) | None => ArrayReferencePolicy::RequiresExplicitSelector,
218 }
219 }
220
221 pub fn subscript_indexing(&self) -> SubscriptIndexBehavior {
223 if self.shell != ShellDialect::Zsh {
224 return SubscriptIndexBehavior::ZeroBased;
225 }
226
227 let Some(options) = self.effective_zsh_options() else {
228 return SubscriptIndexBehavior::OneBased;
229 };
230
231 match (options.ksh_arrays, options.ksh_zero_subscript) {
232 (OptionValue::On, _) => SubscriptIndexBehavior::ZeroBased,
233 (OptionValue::Unknown, _) | (OptionValue::Off, OptionValue::Unknown) => {
234 SubscriptIndexBehavior::Ambiguous
235 }
236 (OptionValue::Off, OptionValue::On) => SubscriptIndexBehavior::OneBasedWithZeroAlias,
237 (OptionValue::Off, OptionValue::Off) => SubscriptIndexBehavior::OneBased,
238 }
239 }
240
241 pub fn arithmetic_literals(&self) -> ArithmeticLiteralBehavior {
243 if self.shell != ShellDialect::Zsh {
244 return ArithmeticLiteralBehavior::CStyleAndLeadingZeroOctal;
245 }
246
247 let Some(options) = self.effective_zsh_options() else {
248 return ArithmeticLiteralBehavior::DecimalUnlessExplicitBase;
249 };
250
251 match options.octal_zeroes {
252 OptionValue::Off => ArithmeticLiteralBehavior::DecimalUnlessExplicitBase,
253 OptionValue::On => ArithmeticLiteralBehavior::LeadingZeroOctal,
254 OptionValue::Unknown => ArithmeticLiteralBehavior::Ambiguous,
255 }
256 }
257}
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq)]
261pub struct CaseCliDispatch {
262 function_scope: ScopeId,
263 dispatcher_span: Span,
264}
265
266impl CaseCliDispatch {
267 fn new(function_scope: ScopeId, dispatcher_span: Span) -> Self {
268 Self {
269 function_scope,
270 dispatcher_span,
271 }
272 }
273
274 pub fn function_scope(self) -> ScopeId {
276 self.function_scope
277 }
278
279 pub fn dispatcher_span(self) -> Span {
281 self.dispatcher_span
282 }
283}
284
285use rustc_hash::{FxHashMap, FxHashSet};
286use shuck_ast::{Command, ConditionalExpr, File, Name, Span, Stmt};
287use shuck_indexer::Indexer;
288use smallvec::{Array, SmallVec};
289use std::path::{Path, PathBuf};
290use std::sync::OnceLock;
291
292use crate::builder::SemanticModelBuilder;
293use crate::call_graph::build_call_graph;
294use crate::cfg::RecordedProgram;
295use crate::command_topology::CommandTopology;
296#[cfg(test)]
297use crate::dataflow::DataflowResult;
298use crate::dataflow::{DataflowContext, ExactVariableDataflow};
299use crate::function_resolution::FunctionBindingLookup;
300use crate::runtime::RuntimePrelude;
301use crate::scope::{ancestor_scopes, enclosing_function_scope};
302use crate::zsh_options::ZshOptionAnalysis;
303
304const MAX_FUNCTIONS_FOR_TERMINATION_REACHABILITY: usize = 200;
305const MAX_TERMINATION_REACHABILITY_WORK: usize = 20_000;
306
307struct AssocCallerSeenNames {
308 inline: SmallVec<[Name; 8]>,
309 hashed: Option<FxHashSet<Name>>,
310}
311
312impl AssocCallerSeenNames {
313 const HASH_THRESHOLD: usize = 32;
314
315 fn new() -> Self {
316 Self {
317 inline: SmallVec::new(),
318 hashed: None,
319 }
320 }
321
322 fn insert(&mut self, name: &Name) -> bool {
323 if let Some(hashed) = &mut self.hashed {
324 return hashed.insert(name.clone());
325 }
326
327 if self.inline.iter().any(|seen_name| seen_name == name) {
328 return false;
329 }
330
331 if self.inline.len() < Self::HASH_THRESHOLD {
332 self.inline.push(name.clone());
333 return true;
334 }
335
336 let mut hashed =
337 FxHashSet::with_capacity_and_hasher(Self::HASH_THRESHOLD * 2, Default::default());
338 hashed.extend(self.inline.drain(..));
339 let inserted = hashed.insert(name.clone());
340 self.hashed = Some(hashed);
341 inserted
342 }
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
346pub(crate) struct SpanKey {
347 start: usize,
348 end: usize,
349}
350
351impl SpanKey {
352 pub(crate) fn new(span: Span) -> Self {
353 Self {
354 start: span.start.offset,
355 end: span.end.offset,
356 }
357 }
358}
359
360#[derive(Debug, Clone, PartialEq, Eq)]
361pub(crate) struct SourceDirectiveOverride {
362 pub(crate) kind: SourceRefKind,
363 pub(crate) own_line: bool,
364}
365
366#[derive(Debug, Clone, PartialEq, Eq)]
367pub(crate) enum IndirectTargetHint {
368 Exact {
369 name: Name,
370 array_like: bool,
371 },
372 Pattern {
373 prefix: compact_str::CompactString,
374 suffix: compact_str::CompactString,
375 array_like: bool,
376 },
377}
378
379#[derive(Debug, Clone, PartialEq, Eq)]
381pub struct SyntheticRead {
382 pub(crate) scope: ScopeId,
383 pub(crate) span: Span,
384 pub(crate) name: Name,
385}
386
387#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
389pub struct FunctionPositionalReferenceSummary {
390 required_arg_count: usize,
391 uses_unprotected_positional_parameters: bool,
392}
393
394impl FunctionPositionalReferenceSummary {
395 pub fn required_arg_count(self) -> usize {
397 self.required_arg_count
398 }
399
400 pub fn uses_unprotected_positional_parameters(self) -> bool {
402 self.uses_unprotected_positional_parameters
403 }
404
405 fn record_required_arg_count(&mut self, index: usize) {
406 self.required_arg_count = self.required_arg_count.max(index);
407 self.uses_unprotected_positional_parameters = true;
408 }
409
410 fn record_use(&mut self) {
411 self.uses_unprotected_positional_parameters = true;
412 }
413}
414
415#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
417pub struct UnusedAssignmentAnalysisOptions {
418 pub treat_indirect_expansion_targets_as_used: bool,
422 pub report_unreachable_assignments: bool,
425}
426
427#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
429pub struct UnreachedFunctionAnalysisOptions {
430 pub report_unreached_nested_definitions: bool,
433}
434
435impl SyntheticRead {
436 pub fn scope(&self) -> ScopeId {
438 self.scope
439 }
440
441 pub fn span(&self) -> Span {
443 self.span
444 }
445
446 pub fn name(&self) -> &Name {
448 &self.name
449 }
450}
451
452#[doc(hidden)]
453pub trait TraversalObserver<'a> {
454 fn enter_command(&mut self, _command: &Command, _scope: ScopeId, _flow: FlowContext) {}
455
456 fn exit_command(&mut self, _command: &Command, _scope: ScopeId) {}
457
458 fn conditional_expression(
459 &mut self,
460 _command_span: Span,
461 _expression: &'a ConditionalExpr,
462 _parent_in_same_logical_group: bool,
463 ) {
464 }
465
466 fn recorded_command(
467 &mut self,
468 _id: CommandId,
469 _stmt: &'a Stmt,
470 _scope: ScopeId,
471 _flow: FlowContext,
472 ) {
473 }
474
475 fn recorded_statement_sequence_command(
476 &mut self,
477 _body_span: Span,
478 _stmt_span: Span,
479 _id: CommandId,
480 ) {
481 }
482
483 fn record_binding(&mut self, _binding: &Binding) {}
484
485 fn record_reference(&mut self, _reference: &Reference, _resolved: Option<&Binding>) {}
486}
487
488#[doc(hidden)]
489pub struct NoopTraversalObserver;
490
491impl<'a> TraversalObserver<'a> for NoopTraversalObserver {}
492
493#[doc(hidden)]
494pub trait SourcePathResolver {
495 fn resolve_candidate_paths(&self, source_path: &Path, candidate: &str) -> Vec<PathBuf>;
496}
497
498impl<F> SourcePathResolver for F
499where
500 F: Fn(&Path, &str) -> Vec<PathBuf> + Send + Sync,
501{
502 fn resolve_candidate_paths(&self, source_path: &Path, candidate: &str) -> Vec<PathBuf> {
503 self(source_path, candidate)
504 }
505}
506
507#[derive(Debug, Clone, PartialEq, Eq, Hash)]
509pub enum PluginFramework {
510 OhMyZsh,
512 Prezto,
514 Zdot,
516 Zinit,
518 ExplicitFilesystem,
520 Other(String),
522}
523
524#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
526pub enum PluginRequestKind {
527 Plugin,
529 Theme,
531 Entrypoint,
533}
534
535#[derive(Debug, Clone, PartialEq, Eq)]
537pub struct PluginRequest {
538 pub framework: PluginFramework,
540 pub kind: PluginRequestKind,
542 pub name: String,
544 pub span: Span,
546 pub explicit: bool,
548 pub root_hint: Option<PathBuf>,
550}
551
552#[derive(Debug, Clone, Default, PartialEq, Eq)]
554pub struct PluginResolution {
555 pub entrypoints: Vec<PathBuf>,
557 pub file_entry_contracts: Vec<FileContract>,
559 pub requesting_file_contract: FileContract,
561}
562
563pub trait PluginResolver {
565 fn additional_plugin_requests(&self, _source_path: &Path) -> Vec<PluginRequest> {
567 Vec::new()
568 }
569
570 fn resolve_source_path(&self, _source_path: &Path, _candidate: &str) -> Vec<PathBuf> {
572 Vec::new()
573 }
574
575 fn resolve_plugin_request(
577 &self,
578 source_path: &Path,
579 request: &PluginRequest,
580 ) -> PluginResolution;
581}
582
583fn dedup_synthetic_reads(reads: Vec<SyntheticRead>) -> Vec<SyntheticRead> {
584 let mut seen = FxHashSet::default();
585 let mut deduped = Vec::new();
586 for read in reads {
587 if seen.insert((read.scope, read.span.start.offset, read.name.clone())) {
588 deduped.push(read);
589 }
590 }
591 deduped
592}
593
594fn assignment_like_binding(kind: BindingKind) -> bool {
595 matches!(
596 kind,
597 BindingKind::Assignment
598 | BindingKind::AppendAssignment
599 | BindingKind::ArrayAssignment
600 | BindingKind::ArithmeticAssignment
601 )
602}
603
604fn binding_blocks_same_scope_assoc_lookup(binding: &Binding) -> bool {
605 binding.attributes.contains(BindingAttributes::LOCAL) || !assignment_like_binding(binding.kind)
606}
607
608fn previous_visible_binding_id_from_slice(
609 all_bindings: &[Binding],
610 bindings: &[BindingId],
611 offset: usize,
612 ignored_binding_span: Option<Span>,
613) -> Option<BindingId> {
614 let candidate_count = bindings
615 .partition_point(|binding_id| all_bindings[binding_id.index()].span.start.offset <= offset);
616
617 bindings[..candidate_count]
618 .iter()
619 .rev()
620 .copied()
621 .find(|binding_id| ignored_binding_span != Some(all_bindings[binding_id.index()].span))
622}
623
624trait BindingIdCollection {
625 fn as_slice(&self) -> &[BindingId];
626 fn insert_binding_id(&mut self, index: usize, id: BindingId);
627}
628
629impl BindingIdCollection for Vec<BindingId> {
630 fn as_slice(&self) -> &[BindingId] {
631 self
632 }
633
634 fn insert_binding_id(&mut self, index: usize, id: BindingId) {
635 self.insert(index, id);
636 }
637}
638
639impl<A> BindingIdCollection for SmallVec<A>
640where
641 A: Array<Item = BindingId>,
642{
643 fn as_slice(&self) -> &[BindingId] {
644 self
645 }
646
647 fn insert_binding_id(&mut self, index: usize, id: BindingId) {
648 self.insert(index, id);
649 }
650}
651
652fn insert_binding_id_sorted(
653 bindings: &mut impl BindingIdCollection,
654 all_bindings: &[Binding],
655 id: BindingId,
656) {
657 let target = &all_bindings[id.index()];
658 let insertion = bindings.as_slice().partition_point(|candidate_id| {
659 let candidate = &all_bindings[candidate_id.index()];
660 candidate.span.start.offset < target.span.start.offset
661 || (candidate.span.start.offset == target.span.start.offset
662 && candidate.span.end.offset < target.span.end.offset)
663 || (candidate.span.start.offset == target.span.start.offset
664 && candidate.span.end.offset == target.span.end.offset
665 && candidate.id.index() < target.id.index())
666 });
667 bindings.insert_binding_id(insertion, id);
668}
669
670#[derive(Debug)]
671struct AssocLookupBindingIndex {
672 blocking_bindings_by_scope: Vec<FxHashMap<Name, Box<[BindingId]>>>,
673}
674
675#[derive(Debug)]
676struct ScopeProvidedBindingIndex {
677 provided_bindings_by_scope: Vec<Box<[ProvidedBinding]>>,
678 definite_provider_scopes_by_name: FxHashMap<Name, Box<[ScopeId]>>,
679}
680
681#[derive(Debug)]
682struct ScopeLookup {
683 children: Vec<Box<[ScopeId]>>,
684}
685
686impl ScopeLookup {
687 fn new(scopes: &[Scope]) -> Self {
688 let mut children = vec![Vec::new(); scopes.len()];
689
690 for scope in scopes {
691 if let Some(parent) = scope.parent {
692 children[parent.index()].push(scope.id);
693 }
694 }
695
696 for scope_ids in &mut children {
697 scope_ids.sort_by_key(|scope_id| {
698 let span = scopes[scope_id.index()].span;
699 (span.start.offset, span.end.offset)
700 });
701 }
702
703 Self {
704 children: children.into_iter().map(Vec::into_boxed_slice).collect(),
705 }
706 }
707
708 fn scope_at(&self, scopes: &[Scope], offset: usize) -> Option<ScopeId> {
709 let root = scopes.first()?;
710 if !contains_offset(root.span, offset) {
711 return None;
712 }
713
714 let mut scope = root.id;
715 while let Some(child) = self.child_scope_at(scopes, scope, offset) {
716 scope = child;
717 }
718
719 Some(scope)
720 }
721
722 fn child_scope_at(&self, scopes: &[Scope], parent: ScopeId, offset: usize) -> Option<ScopeId> {
723 let children = self.children.get(parent.index())?;
724 let cutoff = children
725 .partition_point(|scope_id| scopes[scope_id.index()].span.start.offset <= offset);
726 let mut best: Option<ScopeId> = None;
727 let mut index = cutoff;
728
729 while index > 0 {
730 index -= 1;
731 let scope_id = children[index];
732 let span = scopes[scope_id.index()].span;
733 if span.end.offset < offset {
734 break;
735 }
736 if contains_offset(span, offset) {
737 match best {
738 Some(current)
739 if scope_span_width(scopes[current.index()].span)
740 <= scope_span_width(span) => {}
741 _ => best = Some(scope_id),
742 }
743 }
744 }
745
746 best
747 }
748}
749
750#[derive(Debug)]
752pub struct SemanticModel {
753 shell_profile: ShellProfile,
754 scopes: Vec<Scope>,
755 scope_lookup: ScopeLookup,
756 bindings: Vec<Binding>,
757 references: Vec<Reference>,
758 reference_index: FxHashMap<Name, SmallVec<[ReferenceId; 2]>>,
759 predefined_runtime_refs: FxHashSet<ReferenceId>,
760 guarded_parameter_refs: FxHashSet<ReferenceId>,
761 parameter_guard_flow_refs: FxHashSet<ReferenceId>,
762 defaulting_parameter_operand_refs: FxHashSet<ReferenceId>,
763 self_referential_assignment_refs: FxHashSet<ReferenceId>,
764 binding_index: FxHashMap<Name, SmallVec<[BindingId; 2]>>,
765 resolved: FxHashMap<ReferenceId, BindingId>,
766 unresolved: Vec<ReferenceId>,
767 functions: FxHashMap<Name, SmallVec<[BindingId; 2]>>,
768 call_sites: FxHashMap<Name, SmallVec<[CallSite; 2]>>,
769 call_graph: OnceLock<CallGraph>,
770 source_refs: Vec<SourceRef>,
771 source_path_templates_by_binding: FxHashMap<BindingId, source_closure::SourcePathTemplate>,
772 runtime: RuntimePrelude,
773 declarations: Vec<Declaration>,
774 indirect_target_hints: FxHashMap<BindingId, IndirectTargetHint>,
775 indirect_targets_by_binding: FxHashMap<BindingId, Vec<BindingId>>,
776 indirect_targets_by_reference: FxHashMap<ReferenceId, Vec<BindingId>>,
777 array_like_indirect_expansion_refs: FxHashSet<ReferenceId>,
778 synthetic_reads: Vec<SyntheticRead>,
779 entry_bindings: Vec<BindingId>,
780 flow_contexts: Vec<(Span, FlowContext)>,
781 recorded_program: RecordedProgram,
782 command_bindings: FxHashMap<SpanKey, SmallVec<[BindingId; 2]>>,
783 command_references: FxHashMap<SpanKey, SmallVec<[ReferenceId; 4]>>,
784 cleared_variables: FxHashMap<(ScopeId, Name), SmallVec<[usize; 2]>>,
785 import_origins_by_binding: FxHashMap<BindingId, Vec<PathBuf>>,
786 imported_dependency_paths: Vec<PathBuf>,
787 heuristic_unused_assignments: Vec<BindingId>,
788 zsh_option_analysis: OnceLock<Option<ZshOptionAnalysis>>,
789 zsh_runtime_ambiguous_entry_mask: OnceLock<zsh_options::ZshOptionMask>,
790 zsh_runtime_by_function: OnceLock<FxHashMap<ScopeId, OnceLock<Option<ZshOptionAnalysis>>>>,
791 zsh_runtime_function_summaries: OnceLock<zsh_options::SharedFunctionSummaryCache>,
792 assoc_lookup_binding_index: OnceLock<AssocLookupBindingIndex>,
793 command_topology: OnceLock<CommandTopology>,
794 references_sorted_by_start: OnceLock<Vec<ReferenceId>>,
795 bindings_sorted_by_start: OnceLock<Vec<BindingId>>,
796 bindings_by_definition_span: OnceLock<FxHashMap<SpanKey, BindingId>>,
797 guarded_or_defaulting_reference_offsets_by_name: OnceLock<FxHashMap<Name, Box<[usize]>>>,
798 declarations_by_command_span: OnceLock<FxHashMap<SpanKey, usize>>,
799 editor_document_symbol_ranges_by_binding: OnceLock<Vec<Option<Span>>>,
800 editor_hover_targets: OnceLock<editor::EditorHoverTargets>,
801 unconditional_function_bindings: OnceLock<FxHashSet<BindingId>>,
802 function_bindings_by_scope: OnceLock<FxHashMap<ScopeId, SmallVec<[BindingId; 2]>>>,
803 visible_function_call_bindings: OnceLock<FxHashMap<SpanKey, BindingId>>,
804 function_definition_binding_ids: OnceLock<Vec<BindingId>>,
805 array_use_index: OnceLock<array_use::ArrayUseIndex>,
806}
807
808#[derive(Debug)]
810pub struct SemanticAnalysis<'model> {
811 model: &'model SemanticModel,
812 cfg: OnceLock<ControlFlowGraph>,
813 exact_variable_dataflow: OnceLock<ExactVariableDataflow>,
814 #[cfg(test)]
815 dataflow: OnceLock<DataflowResult>,
816 unused_assignments: OnceLock<Vec<BindingId>>,
817 unused_assignments_shellcheck_compat: OnceLock<Vec<BindingId>>,
818 uninitialized_references: OnceLock<Vec<UninitializedReference>>,
819 uninitialized_reference_certainties: OnceLock<FxHashMap<SpanKey, UninitializedCertainty>>,
820 dead_code: OnceLock<Vec<DeadCode>>,
821 unreachable_blocks: OnceLock<FxHashSet<BlockId>>,
822 binding_block_index: OnceLock<Vec<Vec<BlockId>>>,
823 overwritten_functions: OnceLock<Vec<OverwrittenFunction>>,
824 unreached_functions: OnceLock<Vec<UnreachedFunction>>,
825 unreached_functions_shellcheck_compat: OnceLock<Vec<UnreachedFunction>>,
826 scope_provided_binding_index: OnceLock<ScopeProvidedBindingIndex>,
827}
828
829impl SemanticModel {
830 pub fn build(file: &File, source: &str, indexer: &Indexer) -> Self {
832 Self::build_with_options(file, source, indexer, SemanticBuildOptions::default())
833 }
834
835 pub fn build_with_options(
837 file: &File,
838 source: &str,
839 indexer: &Indexer,
840 options: SemanticBuildOptions<'_>,
841 ) -> Self {
842 let mut observer = NoopTraversalObserver;
843 build_with_observer_with_options(file, source, indexer, &mut observer, options)
844 }
845
846 fn from_build_output(built: builder::BuildOutput) -> Self {
847 let mut reference_index = built.reference_index;
848 for reference_ids in reference_index.values_mut() {
849 reference_ids.sort_by_key(|reference_id| {
850 built.references[reference_id.index()].span.start.offset
851 });
852 }
853 let indirect_targets_by_binding =
854 build_indirect_targets_by_binding(&built.bindings, &built.indirect_target_hints);
855 let indirect_targets_by_reference = build_indirect_targets_by_reference(
856 &built.references,
857 &built.resolved,
858 &built.indirect_expansion_refs,
859 &indirect_targets_by_binding,
860 );
861 let array_like_indirect_expansion_refs = build_array_like_indirect_expansion_refs(
862 &built.references,
863 &built.resolved,
864 &built.indirect_expansion_refs,
865 &built.indirect_target_hints,
866 );
867 let scope_lookup = ScopeLookup::new(&built.scopes);
868 Self {
869 shell_profile: built.shell_profile,
870 scopes: built.scopes,
871 scope_lookup,
872 bindings: built.bindings,
873 references: built.references,
874 reference_index,
875 predefined_runtime_refs: built.predefined_runtime_refs,
876 guarded_parameter_refs: built.guarded_parameter_refs,
877 parameter_guard_flow_refs: built.parameter_guard_flow_refs,
878 defaulting_parameter_operand_refs: built.defaulting_parameter_operand_refs,
879 self_referential_assignment_refs: built.self_referential_assignment_refs,
880 binding_index: built.binding_index,
881 resolved: built.resolved,
882 unresolved: built.unresolved,
883 functions: built.functions,
884 call_sites: built.call_sites,
885 call_graph: OnceLock::new(),
886 source_refs: built.source_refs,
887 source_path_templates_by_binding: built.source_path_templates_by_binding,
888 runtime: built.runtime,
889 declarations: built.declarations,
890 indirect_target_hints: built.indirect_target_hints,
891 indirect_targets_by_binding,
892 indirect_targets_by_reference,
893 array_like_indirect_expansion_refs,
894 synthetic_reads: Vec::new(),
895 entry_bindings: Vec::new(),
896 flow_contexts: built.flow_contexts,
897 recorded_program: built.recorded_program,
898 command_bindings: built.command_bindings,
899 command_references: built.command_references,
900 cleared_variables: built.cleared_variables,
901 import_origins_by_binding: FxHashMap::default(),
902 imported_dependency_paths: Vec::new(),
903 heuristic_unused_assignments: built.heuristic_unused_assignments,
904 zsh_option_analysis: OnceLock::new(),
905 zsh_runtime_ambiguous_entry_mask: OnceLock::new(),
906 zsh_runtime_by_function: OnceLock::new(),
907 zsh_runtime_function_summaries: OnceLock::new(),
908 assoc_lookup_binding_index: OnceLock::new(),
909 command_topology: OnceLock::new(),
910 references_sorted_by_start: OnceLock::new(),
911 bindings_sorted_by_start: OnceLock::new(),
912 bindings_by_definition_span: OnceLock::new(),
913 guarded_or_defaulting_reference_offsets_by_name: OnceLock::new(),
914 declarations_by_command_span: OnceLock::new(),
915 editor_document_symbol_ranges_by_binding: OnceLock::new(),
916 editor_hover_targets: OnceLock::new(),
917 unconditional_function_bindings: OnceLock::new(),
918 function_bindings_by_scope: OnceLock::new(),
919 visible_function_call_bindings: OnceLock::new(),
920 function_definition_binding_ids: OnceLock::new(),
921 array_use_index: OnceLock::new(),
922 }
923 }
924
925 pub fn analysis(&self) -> SemanticAnalysis<'_> {
927 SemanticAnalysis::new(self)
928 }
929
930 pub fn shell_profile(&self) -> &ShellProfile {
932 &self.shell_profile
933 }
934
935 pub fn zsh_options_at(&self, offset: usize) -> Option<&ZshOptionState> {
937 self.zsh_option_analysis()
938 .and_then(|analysis| analysis.options_at(&self.scopes, offset))
939 }
940
941 pub fn zsh_sh_emulation_at(&self, offset: usize) -> Option<bool> {
943 self.zsh_option_analysis()
944 .and_then(|analysis| analysis.sh_emulation_at(&self.scopes, offset))
945 }
946
947 pub fn shell_behavior_at(&self, offset: usize) -> ShellBehaviorAt<'_> {
949 ShellBehaviorAt {
950 shell: self.shell_profile.dialect,
951 zsh_options: self.zsh_options_at(offset),
952 runtime_options: self.zsh_runtime_options_at(offset),
953 zsh_sh_emulation: self.zsh_sh_emulation_at(offset),
954 }
955 }
956
957 fn zsh_runtime_ambiguous_entry_mask(&self) -> zsh_options::ZshOptionMask {
958 if self.shell_profile.zsh_options().is_none() {
959 return zsh_options::ZshOptionMask::default();
960 }
961
962 *self.zsh_runtime_ambiguous_entry_mask.get_or_init(|| {
963 crate::zsh_options::runtime_ambiguous_entry_mask(&self.recorded_program)
964 })
965 }
966
967 fn zsh_runtime_options_at(&self, offset: usize) -> Option<ZshOptionState> {
968 self.shell_profile.zsh_options()?;
969 let ordinary = *self.zsh_options_at(offset)?;
970 let ambiguous_entry = self.zsh_runtime_ambiguous_entry_mask();
971 if ambiguous_entry.is_empty() {
972 return None;
973 }
974
975 let scope = self.scope_at(offset);
976 let function_scope = self.enclosing_function_scope(scope)?;
977 let ambient = self
978 .zsh_runtime_analysis_for_function(function_scope)
979 .and_then(|analysis| analysis.options_at(&self.scopes, offset));
980
981 Some(ambient.map_or(ordinary, |options| ordinary.merge(options)))
982 }
983
984 fn zsh_option_analysis(&self) -> Option<&ZshOptionAnalysis> {
985 self.zsh_option_analysis
986 .get_or_init(|| self.build_zsh_option_analysis())
987 .as_ref()
988 }
989
990 fn build_zsh_option_analysis(&self) -> Option<ZshOptionAnalysis> {
991 let zsh_dynamic_calls = zsh_options::DynamicCallAnalysisContext {
992 references: &self.references,
993 resolved: &self.resolved,
994 indirect_target_hints: &self.indirect_target_hints,
995 indirect_targets_by_binding: &self.indirect_targets_by_binding,
996 command_references: &self.command_references,
997 };
998 zsh_options::analyze(
999 &self.shell_profile,
1000 &self.scopes,
1001 &self.bindings,
1002 &self.recorded_program,
1003 zsh_dynamic_calls,
1004 )
1005 }
1006
1007 fn zsh_runtime_by_function(&self) -> &FxHashMap<ScopeId, OnceLock<Option<ZshOptionAnalysis>>> {
1008 self.zsh_runtime_by_function.get_or_init(|| {
1009 self.recorded_program
1010 .function_bodies()
1011 .keys()
1012 .map(|&function_scope| (function_scope, OnceLock::new()))
1013 .collect()
1014 })
1015 }
1016
1017 fn zsh_runtime_analysis_for_function(
1018 &self,
1019 function_scope: ScopeId,
1020 ) -> Option<&ZshOptionAnalysis> {
1021 self.zsh_runtime_by_function()
1022 .get(&function_scope)?
1023 .get_or_init(|| self.build_zsh_runtime_analysis_for_function(function_scope))
1024 .as_ref()
1025 }
1026
1027 fn build_zsh_runtime_analysis_for_function(
1028 &self,
1029 function_scope: ScopeId,
1030 ) -> Option<ZshOptionAnalysis> {
1031 let function_entry_offset = self.scope(function_scope).span.start.offset;
1032 let mut function_entry = *self.zsh_options_at(function_entry_offset)?;
1033 for field in self.zsh_runtime_ambiguous_entry_mask().iter() {
1034 crate::zsh_options::set_public_option_field(
1035 &mut function_entry,
1036 field,
1037 OptionValue::Unknown,
1038 );
1039 }
1040 crate::zsh_options::function_runtime_analysis_with_entry(
1041 &self.scopes,
1042 &self.bindings,
1043 &self.recorded_program,
1044 crate::zsh_options::DynamicCallAnalysisContext {
1045 references: &self.references,
1046 resolved: &self.resolved,
1047 indirect_target_hints: &self.indirect_target_hints,
1048 indirect_targets_by_binding: &self.indirect_targets_by_binding,
1049 command_references: &self.command_references,
1050 },
1051 Some(
1052 self.zsh_runtime_function_summaries
1053 .get_or_init(Default::default),
1054 ),
1055 function_scope,
1056 function_entry,
1057 )
1058 }
1059
1060 pub fn scopes(&self) -> &[Scope] {
1062 &self.scopes
1063 }
1064
1065 pub fn scope(&self, id: ScopeId) -> &Scope {
1067 &self.scopes[id.index()]
1068 }
1069
1070 pub fn bindings(&self) -> &[Binding] {
1072 &self.bindings
1073 }
1074
1075 pub fn function_definition_bindings(&self) -> impl ExactSizeIterator<Item = &Binding> + '_ {
1080 let ids = self.function_definition_binding_ids.get_or_init(|| {
1081 self.bindings
1082 .iter()
1083 .filter(|binding| matches!(binding.kind, BindingKind::FunctionDefinition))
1084 .map(|binding| binding.id)
1085 .collect()
1086 });
1087 ids.iter().map(|id| &self.bindings[id.index()])
1088 }
1089
1090 pub fn references(&self) -> &[Reference] {
1092 &self.references
1093 }
1094
1095 pub fn guarded_or_defaulting_reference_offsets_by_name(
1100 &self,
1101 ) -> &FxHashMap<Name, Box<[usize]>> {
1102 self.guarded_or_defaulting_reference_offsets_by_name
1103 .get_or_init(|| {
1104 build_guarded_or_defaulting_reference_offsets_by_name(
1105 &self.references,
1106 &self.guarded_parameter_refs,
1107 &self.defaulting_parameter_operand_refs,
1108 )
1109 })
1110 }
1111
1112 pub fn function_positional_reference_summary(
1117 &self,
1118 local_reset_offsets_by_scope: &FxHashMap<ScopeId, Vec<usize>>,
1119 ) -> FxHashMap<ScopeId, FunctionPositionalReferenceSummary> {
1120 let mut summaries = FxHashMap::<ScopeId, FunctionPositionalReferenceSummary>::default();
1121
1122 for (name, reference_ids) in &self.reference_index {
1123 let Some(kind) = positional_parameter_reference_kind(name.as_str()) else {
1124 continue;
1125 };
1126
1127 for reference_id in reference_ids {
1128 let reference = &self.references[reference_id.index()];
1129 if self.is_guarded_parameter_reference(reference.id)
1130 || reference_has_local_positional_reset(
1131 self,
1132 reference.scope,
1133 reference.span.start.offset,
1134 local_reset_offsets_by_scope,
1135 )
1136 {
1137 continue;
1138 }
1139
1140 let Some(function_scope) = self.enclosing_function_scope(reference.scope) else {
1141 continue;
1142 };
1143
1144 let entry = summaries.entry(function_scope).or_default();
1145 match kind {
1146 PositionalParameterReferenceKind::Indexed(index) => {
1147 entry.record_required_arg_count(index);
1148 }
1149 PositionalParameterReferenceKind::Special => entry.record_use(),
1150 }
1151 }
1152 }
1153
1154 summaries
1155 }
1156
1157 pub fn references_in_span(&self, outer: Span) -> ReferencesInSpan<'_> {
1163 let sorted = self
1164 .references_sorted_by_start
1165 .get_or_init(|| build_references_sorted_by_start(&self.references));
1166 let lower = sorted.partition_point(|id| {
1167 self.references[id.index()].span.start.offset < outer.start.offset
1168 });
1169 ReferencesInSpan {
1170 references: &self.references,
1171 ids: sorted[lower..].iter(),
1172 end: outer.end.offset,
1173 }
1174 }
1175
1176 pub fn references_in_command_span(
1182 &self,
1183 command_span: Span,
1184 outer: Span,
1185 ) -> CommandReferencesInSpan<'_> {
1186 let command_span = self
1187 .command_references
1188 .contains_key(&SpanKey::new(command_span))
1189 .then_some(command_span)
1190 .or_else(|| {
1191 self.command_by_span(command_span)
1192 .map(|id| self.command_span(id))
1193 });
1194 let ids = command_span
1195 .filter(|span| contains_span(*span, outer))
1196 .and_then(|span| self.command_references.get(&SpanKey::new(span)))
1197 .map(SmallVec::as_slice)
1198 .unwrap_or(&[]);
1199 CommandReferencesInSpan {
1200 references: &self.references,
1201 ids: ids.iter(),
1202 outer,
1203 }
1204 }
1205
1206 pub fn bindings_in_span(&self, outer: Span) -> BindingsInSpan<'_> {
1212 let sorted = self
1213 .bindings_sorted_by_start
1214 .get_or_init(|| build_bindings_sorted_by_start(&self.bindings));
1215 let lower = sorted
1216 .partition_point(|id| self.bindings[id.index()].span.start.offset < outer.start.offset);
1217 BindingsInSpan {
1218 bindings: &self.bindings,
1219 ids: sorted[lower..].iter(),
1220 end: outer.end.offset,
1221 }
1222 }
1223
1224 pub fn binding(&self, id: BindingId) -> &Binding {
1226 &self.bindings[id.index()]
1227 }
1228
1229 pub fn binding_for_definition_span(&self, span: Span) -> Option<BindingId> {
1231 let index = self
1232 .bindings_by_definition_span
1233 .get_or_init(|| build_bindings_by_definition_span(&self.bindings));
1234 index.get(&SpanKey::new(span)).copied()
1235 }
1236
1237 pub fn reference(&self, id: ReferenceId) -> &Reference {
1239 &self.references[id.index()]
1240 }
1241
1242 pub fn resolved_binding(&self, id: ReferenceId) -> Option<&Binding> {
1244 self.resolved
1245 .get(&id)
1246 .map(|binding| &self.bindings[binding.index()])
1247 }
1248
1249 pub fn reference_is_predefined_runtime_array(&self, id: ReferenceId) -> bool {
1251 self.predefined_runtime_refs.contains(&id)
1252 && self
1253 .references
1254 .get(id.index())
1255 .is_some_and(|reference| self.runtime.is_preinitialized_array(&reference.name))
1256 }
1257
1258 pub fn name_is_predefined_runtime(&self, name: &str) -> bool {
1260 self.runtime.is_preinitialized(&Name::from(name))
1261 }
1262
1263 pub fn name_is_known_runtime(&self, name: &str) -> bool {
1266 self.runtime.is_known_runtime_name(&Name::from(name))
1267 }
1268
1269 pub fn is_guarded_parameter_reference(&self, id: ReferenceId) -> bool {
1272 self.guarded_parameter_refs.contains(&id)
1273 }
1274
1275 pub fn is_defaulting_parameter_operand_reference(&self, id: ReferenceId) -> bool {
1277 self.defaulting_parameter_operand_refs.contains(&id)
1278 }
1279
1280 pub fn indirect_targets_for_binding(&self, id: BindingId) -> &[BindingId] {
1282 self.indirect_targets_by_binding
1283 .get(&id)
1284 .map(Vec::as_slice)
1285 .unwrap_or(&[])
1286 }
1287
1288 pub fn indirect_targets_for_reference(&self, id: ReferenceId) -> &[BindingId] {
1290 self.indirect_targets_by_reference
1291 .get(&id)
1292 .map(Vec::as_slice)
1293 .unwrap_or(&[])
1294 }
1295
1296 pub fn bindings_for(&self, name: &Name) -> &[BindingId] {
1298 self.binding_index
1299 .get(name)
1300 .map(SmallVec::as_slice)
1301 .unwrap_or(&[])
1302 }
1303
1304 pub fn visible_binding(&self, name: &Name, at: Span) -> Option<&Binding> {
1306 self.previous_visible_binding(name, at, None)
1307 }
1308
1309 pub fn visible_candidate_bindings_for_reference(
1312 &self,
1313 reference: &Reference,
1314 ) -> Vec<BindingId> {
1315 let all_bindings = self.bindings_for(&reference.name);
1316 let binding_ids = self
1317 .ancestor_scopes(reference.scope)
1318 .filter_map(|scope| {
1319 all_bindings.iter().copied().rev().find(|binding_id| {
1320 let binding = self.binding(*binding_id);
1321 binding.scope == scope && self.binding_visible_at(*binding_id, reference.span)
1322 })
1323 })
1324 .collect::<Vec<_>>();
1325 if !binding_ids.is_empty() {
1326 return binding_ids;
1327 }
1328
1329 self.ancestor_scopes(reference.scope)
1330 .skip(1)
1331 .filter_map(|scope| {
1332 all_bindings.iter().copied().rev().find(|binding_id| {
1333 let binding = self.binding(*binding_id);
1334 binding.scope == scope && self.binding_visible_at(*binding_id, reference.span)
1335 })
1336 })
1337 .chain(all_bindings.iter().copied().filter(|binding_id| {
1338 let binding = self.binding(*binding_id);
1339 binding.scope != reference.scope
1340 && binding.span.start.offset < reference.span.start.offset
1341 }))
1342 .collect::<FxHashSet<_>>()
1343 .into_iter()
1344 .collect::<Vec<_>>()
1345 }
1346
1347 #[doc(hidden)]
1348 #[doc(hidden)]
1350 pub fn binding_visible_at(&self, binding_id: BindingId, at: Span) -> bool {
1351 let binding = self.binding(binding_id);
1352 binding.span.start.offset <= at.start.offset
1353 && self
1354 .ancestor_scopes(self.scope_at(at.start.offset))
1355 .any(|scope| scope == binding.scope)
1356 }
1357
1358 #[doc(hidden)]
1360 pub fn binding_cleared_before(&self, binding_id: BindingId, at: Span) -> bool {
1361 let binding = self.binding(binding_id);
1362 self.cleared_variables
1363 .get(&(binding.scope, binding.name.clone()))
1364 .is_some_and(|cleared_offsets| {
1365 cleared_offsets.iter().any(|cleared_offset| {
1366 *cleared_offset > binding.span.start.offset && *cleared_offset < at.start.offset
1367 })
1368 })
1369 }
1370
1371 #[doc(hidden)]
1373 pub fn binding_and_reference_share_command(
1374 &self,
1375 binding_id: BindingId,
1376 reference_id: ReferenceId,
1377 ) -> bool {
1378 self.command_bindings.iter().any(|(command, bindings)| {
1379 bindings.contains(&binding_id)
1380 && self
1381 .command_references
1382 .get(command)
1383 .is_some_and(|references| references.contains(&reference_id))
1384 })
1385 }
1386
1387 #[doc(hidden)]
1390 pub fn previous_visible_binding(
1391 &self,
1392 name: &Name,
1393 at: Span,
1394 ignored_binding_span: Option<Span>,
1395 ) -> Option<&Binding> {
1396 let scope = self.scope_at(at.start.offset);
1397 self.previous_visible_binding_id_in_scope_chain(
1398 name,
1399 scope,
1400 at.start.offset,
1401 ignored_binding_span,
1402 )
1403 .map(|binding_id| self.binding(binding_id))
1404 }
1405
1406 #[doc(hidden)]
1408 pub fn visible_binding_for_assoc_lookup(
1409 &self,
1410 name: &Name,
1411 current_scope: ScopeId,
1412 at: Span,
1413 ) -> Option<&Binding> {
1414 if let Some(binding_id) =
1415 self.previous_assoc_lookup_binding_id_in_scope(current_scope, name, at.start.offset)
1416 {
1417 return Some(self.binding(binding_id));
1418 }
1419
1420 self.ancestor_scopes(current_scope)
1421 .skip(1)
1422 .find_map(|scope| {
1423 self.previous_visible_binding_id_in_scope(scope, name, at.start.offset, None)
1424 })
1425 .map(|binding_id| self.binding(binding_id))
1426 }
1427
1428 #[doc(hidden)]
1430 pub fn visible_binding_for_lookup(
1431 &self,
1432 name: &Name,
1433 current_scope: ScopeId,
1434 at: Span,
1435 ) -> Option<&Binding> {
1436 if let Some(binding_id) = self.previous_visible_binding_id_in_scope_chain(
1437 name,
1438 current_scope,
1439 at.start.offset,
1440 None,
1441 ) {
1442 return Some(self.binding(binding_id));
1443 }
1444
1445 self.visible_binding_from_named_callers(name, current_scope)
1446 }
1447
1448 #[doc(hidden)]
1450 pub fn visible_assoc_lookup_binding_for_lookup(
1451 &self,
1452 name: &Name,
1453 current_scope: ScopeId,
1454 at: Span,
1455 ) -> Option<&Binding> {
1456 if let Some(binding) = self.visible_binding_for_assoc_lookup(name, current_scope, at) {
1457 return Some(binding);
1458 }
1459
1460 self.visible_assoc_lookup_binding_from_named_callers(name, current_scope)
1461 }
1462
1463 pub fn assoc_binding_visible_for_lookup(
1465 &self,
1466 name: &Name,
1467 current_scope: ScopeId,
1468 at: Span,
1469 ) -> bool {
1470 if let Some(visible) = self.assoc_binding_visible_in_scope(name, current_scope, at) {
1471 return visible;
1472 }
1473
1474 self.assoc_binding_visible_from_named_callers(name, current_scope)
1475 }
1476
1477 fn assoc_binding_visible_in_scope(
1478 &self,
1479 name: &Name,
1480 current_scope: ScopeId,
1481 at: Span,
1482 ) -> Option<bool> {
1483 self.visible_binding_for_assoc_lookup(name, current_scope, at)
1484 .map(|binding| binding.attributes.contains(BindingAttributes::ASSOC))
1485 }
1486
1487 fn assoc_binding_visible_from_named_callers(
1488 &self,
1489 name: &Name,
1490 current_scope: ScopeId,
1491 ) -> bool {
1492 let Some(function_names) = self.named_function_scope_names(current_scope) else {
1493 return false;
1494 };
1495
1496 let mut seen = AssocCallerSeenNames::new();
1497 let mut worklist = SmallVec::<[Name; 4]>::new();
1498 worklist.extend(function_names.iter().cloned());
1499
1500 while let Some(function_name) = worklist.pop() {
1501 if !seen.insert(&function_name) {
1502 continue;
1503 }
1504
1505 for call_site in self.call_sites_for(&function_name) {
1506 if let Some(binding) = self.visible_binding_for_assoc_lookup(
1507 name,
1508 call_site.scope,
1509 call_site.name_span,
1510 ) {
1511 if binding.attributes.contains(BindingAttributes::ASSOC) {
1512 return true;
1513 }
1514 continue;
1515 }
1516
1517 if let Some(caller_names) = self.named_function_scope_names(call_site.scope) {
1518 worklist.extend(caller_names.iter().cloned());
1519 }
1520 }
1521 }
1522
1523 false
1524 }
1525
1526 fn visible_assoc_lookup_binding_from_named_callers(
1527 &self,
1528 name: &Name,
1529 current_scope: ScopeId,
1530 ) -> Option<&Binding> {
1531 let function_names = self.named_function_scope_names(current_scope)?;
1532
1533 let mut seen = AssocCallerSeenNames::new();
1534 let mut worklist = SmallVec::<[Name; 4]>::new();
1535 worklist.extend(function_names.iter().cloned());
1536
1537 while let Some(function_name) = worklist.pop() {
1538 if !seen.insert(&function_name) {
1539 continue;
1540 }
1541
1542 for call_site in self.call_sites_for(&function_name) {
1543 if let Some(binding) = self.visible_binding_for_assoc_lookup(
1544 name,
1545 call_site.scope,
1546 call_site.name_span,
1547 ) {
1548 return Some(binding);
1549 }
1550
1551 if let Some(caller_names) = self.named_function_scope_names(call_site.scope) {
1552 worklist.extend(caller_names.iter().cloned());
1553 }
1554 }
1555 }
1556
1557 None
1558 }
1559
1560 fn visible_binding_from_named_callers(
1561 &self,
1562 name: &Name,
1563 current_scope: ScopeId,
1564 ) -> Option<&Binding> {
1565 let function_names = self.named_function_scope_names(current_scope)?;
1566
1567 let mut seen = AssocCallerSeenNames::new();
1568 let mut worklist = SmallVec::<[Name; 4]>::new();
1569 worklist.extend(function_names.iter().cloned());
1570
1571 while let Some(function_name) = worklist.pop() {
1572 if !seen.insert(&function_name) {
1573 continue;
1574 }
1575
1576 for call_site in self.call_sites_for(&function_name) {
1577 if let Some(binding_id) = self.previous_visible_binding_id_in_scope_chain(
1578 name,
1579 call_site.scope,
1580 call_site.name_span.start.offset,
1581 None,
1582 ) {
1583 return Some(self.binding(binding_id));
1584 }
1585
1586 if let Some(caller_names) = self.named_function_scope_names(call_site.scope) {
1587 worklist.extend(caller_names.iter().cloned());
1588 }
1589 }
1590 }
1591
1592 None
1593 }
1594
1595 fn named_function_scope_names(&self, scope: ScopeId) -> Option<&[Name]> {
1596 self.ancestor_scopes(scope)
1597 .find_map(|scope_id| match &self.scope(scope_id).kind {
1598 ScopeKind::Function(FunctionScopeKind::Named(names)) => Some(names.as_slice()),
1599 _ => None,
1600 })
1601 }
1602
1603 pub fn defined_anywhere(&self, name: &Name) -> bool {
1605 self.binding_index.contains_key(name)
1606 }
1607
1608 pub fn defined_in_any_function(&self, name: &Name) -> bool {
1610 self.binding_index.get(name).is_some_and(|bindings| {
1611 bindings.iter().any(|binding| {
1612 matches!(
1613 self.scopes[self.bindings[binding.index()].scope.index()].kind,
1614 ScopeKind::Function(_)
1615 )
1616 })
1617 })
1618 }
1619
1620 pub fn is_runtime_consumed_binding(&self, binding_id: BindingId) -> bool {
1623 self.bindings
1624 .get(binding_id.index())
1625 .is_some_and(|binding| self.runtime.is_always_used_binding(&binding.name))
1626 }
1627
1628 pub fn required_before(&self, name: &Name, scope: ScopeId, offset: usize) -> bool {
1630 self.references.iter().any(|reference| {
1631 reference.scope == scope
1632 && &reference.name == name
1633 && matches!(reference.kind, ReferenceKind::RequiredRead)
1634 && reference.span.start.offset < offset
1635 })
1636 }
1637
1638 pub fn maybe_defined_outside(&self, name: &Name, scope: ScopeId) -> bool {
1640 self.ancestor_scopes(scope)
1641 .skip(1)
1642 .any(|scope| self.scopes[scope.index()].bindings.contains_key(name))
1643 }
1644
1645 pub fn unresolved_references(&self) -> &[ReferenceId] {
1647 &self.unresolved
1648 }
1649
1650 pub fn scope_at(&self, offset: usize) -> ScopeId {
1652 self.scope_lookup
1653 .scope_at(&self.scopes, offset)
1654 .unwrap_or(ScopeId(0))
1655 }
1656
1657 pub fn scope_kind(&self, scope: ScopeId) -> &ScopeKind {
1659 &self.scopes[scope.index()].kind
1660 }
1661
1662 fn scope_is_transient(&self, scope: ScopeId) -> bool {
1663 matches!(
1664 self.scope_kind(scope),
1665 ScopeKind::Subshell | ScopeKind::CommandSubstitution | ScopeKind::Pipeline
1666 )
1667 }
1668
1669 pub fn ancestor_scopes(&self, scope: ScopeId) -> impl Iterator<Item = ScopeId> + '_ {
1671 ancestor_scopes(&self.scopes, scope)
1672 }
1673
1674 pub fn scope_is_in_scope_or_descendant(&self, scope: ScopeId, ancestor_scope: ScopeId) -> bool {
1676 self.ancestor_scopes(scope)
1677 .any(|scope| scope == ancestor_scope)
1678 }
1679
1680 pub fn scope_is_descendant_of(&self, scope: ScopeId, ancestor_scope: ScopeId) -> bool {
1682 scope != ancestor_scope && self.scope_is_in_scope_or_descendant(scope, ancestor_scope)
1683 }
1684
1685 pub fn enclosing_function_scope(&self, scope: ScopeId) -> Option<ScopeId> {
1687 enclosing_function_scope(&self.scopes, scope)
1688 }
1689
1690 #[doc(hidden)]
1692 pub fn transient_ancestor_scopes_within_function(
1693 &self,
1694 scope: ScopeId,
1695 ) -> impl Iterator<Item = ScopeId> + '_ {
1696 self.ancestor_scopes(scope)
1697 .take_while(|scope_id| !matches!(self.scope_kind(*scope_id), ScopeKind::Function(_)))
1698 .filter(|scope_id| self.scope_is_transient(*scope_id))
1699 }
1700
1701 #[doc(hidden)]
1703 pub fn innermost_transient_scope_within_function(&self, scope: ScopeId) -> Option<ScopeId> {
1704 self.transient_ancestor_scopes_within_function(scope).next()
1705 }
1706
1707 #[doc(hidden)]
1709 pub fn enclosing_function_scope_without_transient_boundary(
1710 &self,
1711 scope: ScopeId,
1712 ) -> Option<ScopeId> {
1713 if self
1714 .transient_ancestor_scopes_within_function(scope)
1715 .next()
1716 .is_some()
1717 {
1718 None
1719 } else {
1720 self.enclosing_function_scope(scope)
1721 }
1722 }
1723
1724 fn previous_visible_binding_id_in_scope_chain(
1725 &self,
1726 name: &Name,
1727 scope: ScopeId,
1728 offset: usize,
1729 ignored_binding_span: Option<Span>,
1730 ) -> Option<BindingId> {
1731 self.ancestor_scopes(scope).find_map(|scope_id| {
1732 self.previous_visible_binding_id_in_scope(scope_id, name, offset, ignored_binding_span)
1733 })
1734 }
1735
1736 fn previous_visible_binding_id_in_scope(
1737 &self,
1738 scope: ScopeId,
1739 name: &Name,
1740 offset: usize,
1741 ignored_binding_span: Option<Span>,
1742 ) -> Option<BindingId> {
1743 let bindings = self.scopes[scope.index()].bindings.get(name)?;
1744 previous_visible_binding_id_from_slice(
1745 &self.bindings,
1746 bindings,
1747 offset,
1748 ignored_binding_span,
1749 )
1750 }
1751
1752 fn previous_assoc_lookup_binding_id_in_scope(
1753 &self,
1754 scope: ScopeId,
1755 name: &Name,
1756 offset: usize,
1757 ) -> Option<BindingId> {
1758 let bindings = self
1759 .assoc_lookup_binding_index()
1760 .blocking_bindings_by_scope
1761 .get(scope.index())
1762 .and_then(|bindings_by_name| bindings_by_name.get(name))?;
1763 previous_visible_binding_id_from_slice(&self.bindings, bindings, offset, None)
1764 }
1765
1766 fn assoc_lookup_binding_index(&self) -> &AssocLookupBindingIndex {
1767 self.assoc_lookup_binding_index.get_or_init(|| {
1768 let blocking_bindings_by_scope = self
1769 .scopes
1770 .iter()
1771 .map(|scope| {
1772 let mut bindings_by_name = FxHashMap::default();
1773 for (name, bindings) in &scope.bindings {
1774 let filtered = bindings
1775 .iter()
1776 .copied()
1777 .filter(|binding_id| {
1778 binding_blocks_same_scope_assoc_lookup(
1779 &self.bindings[binding_id.index()],
1780 )
1781 })
1782 .collect::<Vec<_>>();
1783 if !filtered.is_empty() {
1784 bindings_by_name.insert(name.clone(), filtered.into_boxed_slice());
1785 }
1786 }
1787 bindings_by_name
1788 })
1789 .collect();
1790
1791 AssocLookupBindingIndex {
1792 blocking_bindings_by_scope,
1793 }
1794 })
1795 }
1796
1797 pub fn flow_context_at(&self, span: &Span) -> Option<&FlowContext> {
1799 self.flow_contexts
1800 .iter()
1801 .rfind(|(candidate, _)| candidate == span)
1802 .map(|(_, context)| context)
1803 .or_else(|| {
1804 self.flow_contexts
1805 .iter()
1806 .enumerate()
1807 .filter(|(_, (candidate, _))| {
1808 contains_span(*candidate, *span) || contains_span(*span, *candidate)
1809 })
1810 .min_by_key(|(index, (candidate, _))| {
1811 (
1812 candidate.end.offset.saturating_sub(candidate.start.offset),
1813 std::cmp::Reverse(*index),
1814 )
1815 })
1816 .map(|(_, (_, context))| context)
1817 })
1818 }
1819
1820 fn add_imported_binding(
1821 &mut self,
1822 provided: &ProvidedBinding,
1823 scope: ScopeId,
1824 span: Span,
1825 command_span: Option<Span>,
1826 origin_paths: Vec<PathBuf>,
1827 file_entry_contract: bool,
1828 ) -> BindingId {
1829 let mut attributes = BindingAttributes::empty();
1830 if provided.certainty == ContractCertainty::Possible {
1831 attributes |= BindingAttributes::IMPORTED_POSSIBLE;
1832 }
1833 if provided.kind == ProvidedBindingKind::Function {
1834 attributes |= BindingAttributes::IMPORTED_FUNCTION;
1835 }
1836 if file_entry_contract {
1837 attributes |= BindingAttributes::IMPORTED_FILE_ENTRY;
1838 if provided.file_entry_initialization == FileEntryBindingInitialization::Initialized {
1839 attributes |= BindingAttributes::IMPORTED_FILE_ENTRY_INITIALIZED;
1840 }
1841 }
1842
1843 let id = BindingId(self.bindings.len() as u32);
1844 self.bindings.push(Binding {
1845 id,
1846 name: provided.name.clone(),
1847 kind: BindingKind::Imported,
1848 origin: BindingOrigin::Imported {
1849 definition_span: span,
1850 },
1851 scope,
1852 span,
1853 references: Vec::new(),
1854 attributes,
1855 });
1856 insert_binding_id_sorted(
1857 self.binding_index.entry(provided.name.clone()).or_default(),
1858 &self.bindings,
1859 id,
1860 );
1861 insert_binding_id_sorted(
1862 self.scopes[scope.index()]
1863 .bindings
1864 .entry(provided.name.clone())
1865 .or_default(),
1866 &self.bindings,
1867 id,
1868 );
1869 if provided.kind == ProvidedBindingKind::Function {
1870 insert_binding_id_sorted(
1871 self.functions.entry(provided.name.clone()).or_default(),
1872 &self.bindings,
1873 id,
1874 );
1875 }
1876 if let Some(command_span) = command_span {
1877 self.command_bindings
1878 .entry(SpanKey::new(command_span))
1879 .or_default()
1880 .push(id);
1881 }
1882 if !origin_paths.is_empty() {
1883 self.import_origins_by_binding.insert(id, origin_paths);
1884 }
1885 self.bindings_by_definition_span.take();
1886 id
1887 }
1888
1889 pub(crate) fn apply_file_entry_contract(&mut self, contract: FileContract, file: &File) {
1890 if contract.required_reads.is_empty()
1891 && contract.provided_bindings.is_empty()
1892 && contract.provided_functions.is_empty()
1893 && !contract.externally_consumed_bindings
1894 && contract.externally_consumed_binding_names.is_empty()
1895 && contract.externally_consumed_binding_prefixes.is_empty()
1896 {
1897 return;
1898 }
1899
1900 if contract.externally_consumed_bindings {
1901 self.mark_file_entry_consumed_bindings();
1902 }
1903 if !contract.externally_consumed_binding_names.is_empty() {
1904 self.mark_file_entry_consumed_binding_names(
1905 &contract.externally_consumed_binding_names,
1906 );
1907 }
1908 if !contract.externally_consumed_binding_prefixes.is_empty() {
1909 self.mark_file_entry_consumed_binding_prefixes(
1910 &contract.externally_consumed_binding_prefixes,
1911 );
1912 }
1913
1914 let mut synthetic_reads = self.synthetic_reads.clone();
1915 for name in contract.required_reads {
1916 synthetic_reads.push(SyntheticRead {
1917 scope: ScopeId(0),
1918 span: file.span,
1919 name,
1920 });
1921 }
1922
1923 let entry_span = Span::from_positions(file.span.start, file.span.start);
1924 let mut entry_bindings = self.entry_bindings.clone();
1925 let function_origin_paths = contract
1926 .provided_functions
1927 .iter()
1928 .map(|function| (function.name.clone(), function.origin_paths.clone()))
1929 .collect::<FxHashMap<_, _>>();
1930 let mut provided_bindings = contract.provided_bindings;
1931 for function in contract.provided_functions {
1932 if !provided_bindings.iter().any(|binding| {
1933 binding.kind == ProvidedBindingKind::Function && binding.name == function.name
1934 }) {
1935 provided_bindings.push(ProvidedBinding::new(
1936 function.name,
1937 ProvidedBindingKind::Function,
1938 ContractCertainty::Definite,
1939 ));
1940 }
1941 }
1942 for binding in &provided_bindings {
1943 let origin_paths = function_origin_paths
1944 .get(&binding.name)
1945 .cloned()
1946 .unwrap_or_default();
1947 let id = self.add_imported_binding(
1948 binding,
1949 ScopeId(0),
1950 entry_span,
1951 None,
1952 origin_paths,
1953 true,
1954 );
1955 entry_bindings.push(id);
1956 }
1957
1958 self.set_synthetic_reads(dedup_synthetic_reads(synthetic_reads));
1959 self.set_entry_bindings(entry_bindings);
1960 self.invalidate_function_binding_lookup();
1961 self.resolve_unresolved_references();
1962 self.invalidate_semantic_caches();
1963 }
1964
1965 fn mark_file_entry_consumed_bindings(&mut self) {
1966 for binding in &mut self.bindings {
1967 if file_entry_contract_can_consume_binding(binding) {
1968 binding.attributes |= BindingAttributes::EXTERNALLY_CONSUMED;
1969 }
1970 }
1971 self.heuristic_unused_assignments.retain(|binding_id| {
1972 !self.bindings[binding_id.index()]
1973 .attributes
1974 .contains(BindingAttributes::EXTERNALLY_CONSUMED)
1975 });
1976 }
1977
1978 fn mark_file_entry_consumed_binding_names(&mut self, names: &[Name]) {
1979 for binding in &mut self.bindings {
1980 if file_entry_contract_can_consume_binding(binding) && names.contains(&binding.name) {
1981 binding.attributes |= BindingAttributes::EXTERNALLY_CONSUMED;
1982 }
1983 }
1984 self.heuristic_unused_assignments.retain(|binding_id| {
1985 !self.bindings[binding_id.index()]
1986 .attributes
1987 .contains(BindingAttributes::EXTERNALLY_CONSUMED)
1988 });
1989 }
1990
1991 fn mark_file_entry_consumed_binding_prefixes(&mut self, prefixes: &[Name]) {
1992 for binding in &mut self.bindings {
1993 if file_entry_contract_can_consume_binding(binding)
1994 && prefixes
1995 .iter()
1996 .any(|prefix| binding.name.as_str().starts_with(prefix.as_str()))
1997 {
1998 binding.attributes |= BindingAttributes::EXTERNALLY_CONSUMED;
1999 }
2000 }
2001 self.heuristic_unused_assignments.retain(|binding_id| {
2002 !self.bindings[binding_id.index()]
2003 .attributes
2004 .contains(BindingAttributes::EXTERNALLY_CONSUMED)
2005 });
2006 }
2007
2008 pub(crate) fn apply_source_contracts(
2009 &mut self,
2010 contracts: source_closure::SourceClosureContracts,
2011 ) {
2012 if contracts
2013 .requesting_file_contract
2014 .externally_consumed_bindings
2015 {
2016 self.mark_file_entry_consumed_bindings();
2017 }
2018 if !contracts
2019 .requesting_file_contract
2020 .externally_consumed_binding_names
2021 .is_empty()
2022 {
2023 self.mark_file_entry_consumed_binding_names(
2024 &contracts
2025 .requesting_file_contract
2026 .externally_consumed_binding_names,
2027 );
2028 }
2029 if !contracts
2030 .requesting_file_contract
2031 .externally_consumed_binding_prefixes
2032 .is_empty()
2033 {
2034 self.mark_file_entry_consumed_binding_prefixes(
2035 &contracts
2036 .requesting_file_contract
2037 .externally_consumed_binding_prefixes,
2038 );
2039 }
2040
2041 if contracts.synthetic_reads.is_empty()
2042 && contracts.imported_bindings.is_empty()
2043 && !contracts
2044 .requesting_file_contract
2045 .externally_consumed_bindings
2046 && contracts
2047 .requesting_file_contract
2048 .externally_consumed_binding_names
2049 .is_empty()
2050 && contracts
2051 .requesting_file_contract
2052 .externally_consumed_binding_prefixes
2053 .is_empty()
2054 && contracts.dependency_paths.is_empty()
2055 && contracts.source_ref_resolutions.is_empty()
2056 && contracts.source_ref_explicitness.is_empty()
2057 && contracts.source_ref_diagnostic_classes.is_empty()
2058 {
2059 return;
2060 }
2061
2062 let mut merged_reads = self.synthetic_reads.clone();
2063 merged_reads.extend(contracts.synthetic_reads);
2064 self.set_synthetic_reads(dedup_synthetic_reads(merged_reads));
2065 if !contracts.dependency_paths.is_empty() {
2066 for path in contracts.dependency_paths {
2067 if !self.imported_dependency_paths.contains(&path) {
2068 self.imported_dependency_paths.push(path);
2069 }
2070 }
2071 self.imported_dependency_paths.sort();
2072 self.imported_dependency_paths.dedup();
2073 }
2074
2075 if !contracts.source_ref_resolutions.is_empty() {
2076 debug_assert_eq!(
2077 contracts.source_ref_resolutions.len(),
2078 self.source_refs.len()
2079 );
2080 for (source_ref, resolution) in self
2081 .source_refs
2082 .iter_mut()
2083 .zip(contracts.source_ref_resolutions)
2084 {
2085 source_ref.resolution = resolution;
2086 }
2087 }
2088 if !contracts.source_ref_explicitness.is_empty() {
2089 debug_assert_eq!(
2090 contracts.source_ref_explicitness.len(),
2091 self.source_refs.len()
2092 );
2093 for (source_ref, explicitly_provided) in self
2094 .source_refs
2095 .iter_mut()
2096 .zip(contracts.source_ref_explicitness)
2097 {
2098 source_ref.explicitly_provided = explicitly_provided;
2099 }
2100 }
2101 if !contracts.source_ref_diagnostic_classes.is_empty() {
2102 debug_assert_eq!(
2103 contracts.source_ref_diagnostic_classes.len(),
2104 self.source_refs.len()
2105 );
2106 for (source_ref, diagnostic_class) in self
2107 .source_refs
2108 .iter_mut()
2109 .zip(contracts.source_ref_diagnostic_classes)
2110 {
2111 source_ref.diagnostic_class = diagnostic_class;
2112 }
2113 }
2114
2115 for site in contracts.imported_bindings {
2116 self.add_imported_binding(
2117 &site.binding,
2118 site.scope,
2119 site.span,
2120 Some(site.span),
2121 site.origin_paths,
2122 false,
2123 );
2124 }
2125 self.invalidate_function_binding_lookup();
2126 self.resolve_unresolved_references();
2127 self.invalidate_semantic_caches();
2128 }
2129
2130 fn invalidate_function_binding_lookup(&mut self) {
2131 self.unconditional_function_bindings.take();
2132 self.function_bindings_by_scope.take();
2133 self.visible_function_call_bindings.take();
2134 self.function_definition_binding_ids.take();
2135 }
2136
2137 fn invalidate_semantic_caches(&mut self) {
2138 self.call_graph.take();
2139 self.zsh_option_analysis.take();
2140 self.zsh_runtime_by_function.take();
2141 self.zsh_runtime_function_summaries.take();
2142 }
2143
2144 fn resolve_unresolved_references(&mut self) {
2145 let unresolved = std::mem::take(&mut self.unresolved);
2146 for reference_id in unresolved {
2147 let reference = &self.references[reference_id.index()];
2148 let resolved =
2149 self.resolve_binding_at(&reference.name, reference.scope, reference.span);
2150 if let Some(binding_id) = resolved {
2151 self.resolved.insert(reference_id, binding_id);
2152 self.bindings[binding_id.index()]
2153 .references
2154 .push(reference_id);
2155 } else {
2156 self.unresolved.push(reference_id);
2157 }
2158 }
2159 }
2160
2161 fn resolve_binding_at(&self, name: &Name, scope: ScopeId, span: Span) -> Option<BindingId> {
2162 for scope in self.ancestor_scopes(scope) {
2163 let Some(bindings) = self.scopes[scope.index()].bindings.get(name) else {
2164 continue;
2165 };
2166
2167 for binding in bindings.iter().rev().copied() {
2168 if self.bindings[binding.index()].span.start.offset <= span.start.offset {
2169 return Some(binding);
2170 }
2171 }
2172 }
2173 None
2174 }
2175
2176 pub fn function_definitions(&self, name: &Name) -> &[BindingId] {
2178 self.functions
2179 .get(name)
2180 .map(SmallVec::as_slice)
2181 .unwrap_or(&[])
2182 }
2183
2184 pub fn call_sites_for(&self, name: &Name) -> &[CallSite] {
2186 self.call_sites
2187 .get(name)
2188 .map(SmallVec::as_slice)
2189 .unwrap_or(&[])
2190 }
2191
2192 pub fn call_graph(&self) -> &CallGraph {
2194 self.call_graph.get_or_init(|| {
2195 build_call_graph(
2196 &self.scopes,
2197 &self.bindings,
2198 &self.functions,
2199 &self.call_sites,
2200 )
2201 })
2202 }
2203
2204 pub fn declarations(&self) -> &[Declaration] {
2206 &self.declarations
2207 }
2208
2209 pub fn declaration_for_command_span(&self, span: Span) -> Option<&Declaration> {
2211 let index = self
2212 .declarations_by_command_span
2213 .get_or_init(|| build_declarations_by_command_span(&self.declarations));
2214 index
2215 .get(&SpanKey::new(span))
2216 .map(|declaration_index| &self.declarations[*declaration_index])
2217 }
2218
2219 pub fn function_definition_binding_for_command_span(&self, span: Span) -> Option<BindingId> {
2221 self.command_bindings
2222 .get(&SpanKey::new(span))
2223 .and_then(|bindings| {
2224 bindings.iter().copied().find(|binding_id| {
2225 matches!(
2226 self.bindings[binding_id.index()].kind,
2227 BindingKind::FunctionDefinition
2228 )
2229 })
2230 })
2231 }
2232
2233 pub fn source_refs(&self) -> &[SourceRef] {
2235 &self.source_refs
2236 }
2237
2238 pub fn synthetic_reads(&self) -> &[SyntheticRead] {
2240 &self.synthetic_reads
2241 }
2242
2243 pub fn import_origins_for_binding(&self, id: BindingId) -> &[PathBuf] {
2245 self.import_origins_by_binding
2246 .get(&id)
2247 .map(Vec::as_slice)
2248 .unwrap_or(&[])
2249 }
2250
2251 pub fn imported_dependency_paths(&self) -> &[PathBuf] {
2253 &self.imported_dependency_paths
2254 }
2255
2256 pub fn statement_sequence_commands(&self) -> &[StatementSequenceCommand] {
2258 self.recorded_program.statement_sequence_commands()
2259 }
2260
2261 pub fn command_count(&self) -> usize {
2263 self.recorded_program.commands().len()
2264 }
2265
2266 pub(crate) fn recorded_program(&self) -> &RecordedProgram {
2267 &self.recorded_program
2268 }
2269
2270 pub(crate) fn set_synthetic_reads(&mut self, synthetic_reads: Vec<SyntheticRead>) {
2271 self.synthetic_reads = synthetic_reads;
2272 }
2273
2274 fn set_entry_bindings(&mut self, entry_bindings: Vec<BindingId>) {
2275 self.entry_bindings = entry_bindings;
2276 }
2277
2278 fn function_binding_lookup(&self) -> FunctionBindingLookup<'_> {
2279 FunctionBindingLookup {
2280 program: &self.recorded_program,
2281 scopes: &self.scopes,
2282 bindings: &self.bindings,
2283 call_sites: &self.call_sites,
2284 unconditional_function_bindings: self.unconditional_function_bindings(),
2285 function_bindings_by_scope: self.function_binding_scope_index(),
2286 }
2287 }
2288
2289 fn unconditional_function_bindings(&self) -> &FxHashSet<BindingId> {
2290 self.unconditional_function_bindings.get_or_init(|| {
2291 function_resolution::collect_unconditional_function_bindings(
2292 &self.recorded_program,
2293 &self.command_bindings,
2294 &self.bindings,
2295 )
2296 })
2297 }
2298
2299 pub(crate) fn function_binding_scope_index(
2300 &self,
2301 ) -> &FxHashMap<ScopeId, SmallVec<[BindingId; 2]>> {
2302 self.function_bindings_by_scope
2303 .get_or_init(|| function_resolution::function_bindings_by_scope(&self.recorded_program))
2304 }
2305
2306 pub(crate) fn visible_function_call_bindings(&self) -> &FxHashMap<SpanKey, BindingId> {
2307 self.visible_function_call_bindings.get_or_init(|| {
2308 self.function_binding_lookup()
2309 .visible_function_call_bindings()
2310 })
2311 }
2312
2313 fn dataflow_context<'a>(&'a self, cfg: &'a ControlFlowGraph) -> DataflowContext<'a> {
2314 DataflowContext {
2315 cfg,
2316 runtime: &self.runtime,
2317 scopes: &self.scopes,
2318 bindings: &self.bindings,
2319 references: &self.references,
2320 predefined_runtime_refs: &self.predefined_runtime_refs,
2321 guarded_parameter_refs: &self.guarded_parameter_refs,
2322 parameter_guard_flow_refs: &self.parameter_guard_flow_refs,
2323 self_referential_assignment_refs: &self.self_referential_assignment_refs,
2324 resolved: &self.resolved,
2325 call_sites: &self.call_sites,
2326 visible_function_call_bindings: self.visible_function_call_bindings(),
2327 function_body_scopes: &self.recorded_program.function_body_scopes,
2328 indirect_targets_by_reference: &self.indirect_targets_by_reference,
2329 array_like_indirect_expansion_refs: &self.array_like_indirect_expansion_refs,
2330 synthetic_reads: &self.synthetic_reads,
2331 entry_bindings: &self.entry_bindings,
2332 }
2333 }
2334}
2335
2336fn file_entry_contract_can_consume_binding(binding: &Binding) -> bool {
2337 if binding.attributes.contains(BindingAttributes::LOCAL) {
2338 return false;
2339 }
2340
2341 file_entry_contract_can_consume_exact_binding(binding)
2342}
2343
2344fn file_entry_contract_can_consume_exact_binding(binding: &Binding) -> bool {
2345 matches!(
2346 binding.kind,
2347 BindingKind::Assignment
2348 | BindingKind::ArrayAssignment
2349 | BindingKind::AppendAssignment
2350 | BindingKind::ParameterDefaultAssignment
2351 | BindingKind::LoopVariable
2352 | BindingKind::ReadTarget
2353 | BindingKind::MapfileTarget
2354 | BindingKind::PrintfTarget
2355 | BindingKind::GetoptsTarget
2356 | BindingKind::ArithmeticAssignment
2357 | BindingKind::Declaration(_)
2358 )
2359}
2360
2361#[doc(hidden)]
2362pub fn build_with_observer<'a>(
2363 file: &'a File,
2364 source: &'a str,
2365 indexer: &Indexer,
2366 observer: &mut dyn TraversalObserver<'a>,
2367) -> SemanticModel {
2368 build_with_observer_with_options(
2369 file,
2370 source,
2371 indexer,
2372 observer,
2373 SemanticBuildOptions::default(),
2374 )
2375}
2376
2377#[doc(hidden)]
2378pub fn build_with_observer_with_options<'a>(
2379 file: &'a File,
2380 source: &'a str,
2381 indexer: &Indexer,
2382 observer: &mut dyn TraversalObserver<'a>,
2383 options: SemanticBuildOptions<'_>,
2384) -> SemanticModel {
2385 build_semantic_model(file, source, indexer, observer, options)
2386}
2387
2388#[doc(hidden)]
2389pub fn build_with_observer_at_path<'a>(
2390 file: &'a File,
2391 source: &'a str,
2392 indexer: &Indexer,
2393 observer: &mut dyn TraversalObserver<'a>,
2394 source_path: Option<&Path>,
2395) -> SemanticModel {
2396 build_with_observer_at_path_with_resolver(file, source, indexer, observer, source_path, None)
2397}
2398
2399#[doc(hidden)]
2400pub fn build_with_observer_at_path_with_resolver<'a>(
2401 file: &'a File,
2402 source: &'a str,
2403 indexer: &Indexer,
2404 observer: &mut dyn TraversalObserver<'a>,
2405 source_path: Option<&Path>,
2406 source_path_resolver: Option<&(dyn SourcePathResolver + Send + Sync)>,
2407) -> SemanticModel {
2408 build_semantic_model(
2409 file,
2410 source,
2411 indexer,
2412 observer,
2413 SemanticBuildOptions {
2414 source_path,
2415 source_path_resolver,
2416 plugin_resolver: None,
2417 file_entry_contract: None,
2418 file_entry_contract_collector: None,
2419 file_entry_contract_collector_factory: None,
2420 analyzed_paths: None,
2421 shell_profile: None,
2422 resolve_source_closure: true,
2423 },
2424 )
2425}
2426
2427fn build_semantic_model<'a>(
2428 file: &'a File,
2429 source: &'a str,
2430 indexer: &Indexer,
2431 observer: &mut dyn TraversalObserver<'a>,
2432 options: SemanticBuildOptions<'_>,
2433) -> SemanticModel {
2434 let SemanticBuildOptions {
2435 source_path,
2436 source_path_resolver,
2437 plugin_resolver,
2438 file_entry_contract,
2439 mut file_entry_contract_collector,
2440 file_entry_contract_collector_factory,
2441 analyzed_paths,
2442 shell_profile,
2443 resolve_source_closure,
2444 } = options;
2445 let mut model = build_semantic_model_base(
2446 file,
2447 source,
2448 indexer,
2449 observer,
2450 source_path,
2451 shell_profile.clone(),
2452 file_entry_contract_collector
2453 .as_mut()
2454 .map(|collector| &mut **collector as &mut dyn FileEntryContractCollector),
2455 );
2456 if let Some(contract) = file_entry_contract {
2457 model.apply_file_entry_contract(contract, file);
2458 }
2459 if let Some(contract) = file_entry_contract_collector
2460 .as_ref()
2461 .and_then(|collector| collector.finish())
2462 {
2463 model.apply_file_entry_contract(contract, file);
2464 }
2465 if let Some(source_path) = source_path {
2466 let contracts = if resolve_source_closure {
2467 source_closure::collect_source_closure_contracts(
2468 &model,
2469 file,
2470 source,
2471 source_path,
2472 source_closure::SourceClosureResolverConfig {
2473 source_path_resolver,
2474 plugin_resolver,
2475 file_entry_contract_collector_factory,
2476 analyzed_paths,
2477 },
2478 )
2479 } else {
2480 let (source_ref_resolutions, source_ref_explicitness, source_ref_diagnostic_classes) =
2481 source_closure::collect_source_ref_metadata(
2482 &model,
2483 source_path,
2484 source_path_resolver,
2485 analyzed_paths,
2486 );
2487 source_closure::SourceClosureContracts::from_source_ref_metadata(
2488 source_ref_resolutions,
2489 source_ref_explicitness,
2490 source_ref_diagnostic_classes,
2491 )
2492 };
2493 model.apply_source_contracts(contracts);
2494 }
2495 model
2496}
2497
2498pub(crate) fn build_semantic_model_base<'a, 'observer>(
2499 file: &'a File,
2500 source: &'a str,
2501 indexer: &Indexer,
2502 observer: &'observer mut dyn TraversalObserver<'a>,
2503 source_path: Option<&Path>,
2504 shell_profile: Option<ShellProfile>,
2505 file_entry_contract_collector: Option<&'observer mut dyn FileEntryContractCollector>,
2506) -> SemanticModel {
2507 let shell_profile = shell_profile.unwrap_or_else(|| infer_shell_profile(source, source_path));
2508 let built = SemanticModelBuilder::build(
2509 file,
2510 source,
2511 indexer,
2512 observer,
2513 file_entry_contract_collector,
2514 bash_runtime_vars_enabled(source, source_path),
2515 shell_profile,
2516 );
2517 SemanticModel::from_build_output(built)
2518}
2519
2520fn infer_shell_profile(source: &str, path: Option<&Path>) -> ShellProfile {
2521 let dialect = infer_parse_dialect_from_source(source, path);
2522 ShellProfile::native(dialect)
2523}
2524
2525fn infer_parse_dialect_from_source(
2526 source: &str,
2527 path: Option<&Path>,
2528) -> shuck_parser::ShellDialect {
2529 if let Some(interpreter) = shebang_interpreter(source) {
2530 return parse_dialect_from_name(interpreter).unwrap_or(shuck_parser::ShellDialect::Bash);
2531 }
2532
2533 infer_parse_dialect_from_path(path).unwrap_or(shuck_parser::ShellDialect::Bash)
2534}
2535
2536pub(crate) fn infer_explicit_parse_dialect_from_source(
2537 source: &str,
2538 path: Option<&Path>,
2539) -> Option<shuck_parser::ShellDialect> {
2540 if let Some(interpreter) = shebang_interpreter(source)
2541 && let Some(dialect) = parse_dialect_from_name(interpreter)
2542 {
2543 return Some(dialect);
2544 }
2545
2546 infer_parse_dialect_from_path(path)
2547}
2548
2549fn shebang_interpreter(source: &str) -> Option<&str> {
2550 let first_line_end = source.find('\n').unwrap_or(source.len());
2551 let first_line = source.get(..first_line_end)?;
2552 shuck_parser::shebang::interpreter_name(first_line.strip_suffix('\r').unwrap_or(first_line))
2553}
2554
2555fn infer_parse_dialect_from_path(path: Option<&Path>) -> Option<shuck_parser::ShellDialect> {
2556 match path
2557 .and_then(|path| path.extension().and_then(|ext| ext.to_str()))
2558 .map(|ext| ext.to_ascii_lowercase())
2559 .as_deref()
2560 {
2561 Some("sh" | "dash" | "ksh") => Some(shuck_parser::ShellDialect::Posix),
2562 Some("mksh") => Some(shuck_parser::ShellDialect::Mksh),
2563 Some("bash") => Some(shuck_parser::ShellDialect::Bash),
2564 Some("zsh") => Some(shuck_parser::ShellDialect::Zsh),
2565 _ => None,
2566 }
2567}
2568
2569fn parse_dialect_from_name(name: &str) -> Option<shuck_parser::ShellDialect> {
2570 match name.to_ascii_lowercase().as_str() {
2571 "sh" | "dash" | "ksh" | "posix" => Some(shuck_parser::ShellDialect::Posix),
2572 "mksh" => Some(shuck_parser::ShellDialect::Mksh),
2573 "bash" => Some(shuck_parser::ShellDialect::Bash),
2574 "zsh" => Some(shuck_parser::ShellDialect::Zsh),
2575 _ => None,
2576 }
2577}
2578
2579fn bash_runtime_vars_enabled(source: &str, path: Option<&Path>) -> bool {
2580 infer_bash_from_shebang(source).unwrap_or_else(|| {
2581 path.and_then(|path| path.extension().and_then(|ext| ext.to_str()))
2582 .is_some_and(|ext| ext.eq_ignore_ascii_case("bash"))
2583 })
2584}
2585
2586fn infer_bash_from_shebang(source: &str) -> Option<bool> {
2587 shebang_interpreter(source).map(|interpreter| interpreter.eq_ignore_ascii_case("bash"))
2588}
2589
2590fn contains_offset(span: Span, offset: usize) -> bool {
2591 span.start.offset <= offset && offset <= span.end.offset
2592}
2593
2594fn build_references_sorted_by_start(references: &[Reference]) -> Vec<ReferenceId> {
2595 let mut ids: Vec<ReferenceId> = (0..references.len() as u32).map(ReferenceId).collect();
2596 ids.sort_by_key(|id| references[id.index()].span.start.offset);
2597 ids
2598}
2599
2600#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2601enum PositionalParameterReferenceKind {
2602 Indexed(usize),
2603 Special,
2604}
2605
2606fn positional_parameter_reference_kind(name: &str) -> Option<PositionalParameterReferenceKind> {
2607 match name {
2608 "@" | "*" | "#" => Some(PositionalParameterReferenceKind::Special),
2609 "0" => None,
2610 _ if name.chars().all(|ch| ch.is_ascii_digit()) => name
2611 .parse::<usize>()
2612 .ok()
2613 .map(PositionalParameterReferenceKind::Indexed),
2614 _ => None,
2615 }
2616}
2617
2618fn reference_has_local_positional_reset(
2619 semantic: &SemanticModel,
2620 scope: ScopeId,
2621 offset: usize,
2622 local_reset_offsets_by_scope: &FxHashMap<ScopeId, Vec<usize>>,
2623) -> bool {
2624 semantic
2625 .transient_ancestor_scopes_within_function(scope)
2626 .any(|transient_scope| {
2627 local_reset_offsets_by_scope
2628 .get(&transient_scope)
2629 .is_some_and(|offsets| offsets.iter().any(|reset_offset| *reset_offset < offset))
2630 })
2631}
2632
2633fn build_bindings_sorted_by_start(bindings: &[Binding]) -> Vec<BindingId> {
2634 let mut ids: Vec<BindingId> = (0..bindings.len() as u32).map(BindingId).collect();
2635 ids.sort_by_key(|id| bindings[id.index()].span.start.offset);
2636 ids
2637}
2638
2639fn build_guarded_or_defaulting_reference_offsets_by_name(
2640 references: &[Reference],
2641 guarded_parameter_refs: &FxHashSet<ReferenceId>,
2642 defaulting_parameter_operand_refs: &FxHashSet<ReferenceId>,
2643) -> FxHashMap<Name, Box<[usize]>> {
2644 let mut offsets_by_name = FxHashMap::<Name, Vec<usize>>::default();
2645
2646 for reference in references {
2647 if guarded_parameter_refs.contains(&reference.id)
2648 || defaulting_parameter_operand_refs.contains(&reference.id)
2649 {
2650 offsets_by_name
2651 .entry(reference.name.clone())
2652 .or_default()
2653 .push(reference.span.start.offset);
2654 }
2655 }
2656
2657 offsets_by_name
2658 .into_iter()
2659 .map(|(name, mut offsets)| {
2660 offsets.sort_unstable();
2661 offsets.dedup();
2662 (name, offsets.into_boxed_slice())
2663 })
2664 .collect()
2665}
2666
2667fn build_declarations_by_command_span(declarations: &[Declaration]) -> FxHashMap<SpanKey, usize> {
2668 let mut index = FxHashMap::with_capacity_and_hasher(declarations.len(), Default::default());
2669 for (declaration_index, declaration) in declarations.iter().enumerate() {
2670 index.insert(SpanKey::new(declaration.span), declaration_index);
2671 }
2672 index
2673}
2674
2675fn build_bindings_by_definition_span(bindings: &[Binding]) -> FxHashMap<SpanKey, BindingId> {
2676 let mut index = FxHashMap::with_capacity_and_hasher(bindings.len(), Default::default());
2677 for binding in bindings {
2678 index.insert(SpanKey::new(binding.span), binding.id);
2679 }
2680 index
2681}
2682
2683#[derive(Debug, Clone)]
2688pub struct ReferencesInSpan<'a> {
2689 references: &'a [Reference],
2690 ids: std::slice::Iter<'a, ReferenceId>,
2691 end: usize,
2692}
2693
2694impl<'a> Iterator for ReferencesInSpan<'a> {
2695 type Item = &'a Reference;
2696
2697 fn next(&mut self) -> Option<&'a Reference> {
2698 loop {
2699 let id = self.ids.next()?;
2700 let reference = &self.references[id.index()];
2701 if reference.span.start.offset > self.end {
2702 return None;
2703 }
2704 if reference.span.end.offset <= self.end {
2705 return Some(reference);
2706 }
2707 }
2708 }
2709}
2710
2711#[derive(Debug, Clone)]
2716pub struct CommandReferencesInSpan<'a> {
2717 references: &'a [Reference],
2718 ids: std::slice::Iter<'a, ReferenceId>,
2719 outer: Span,
2720}
2721
2722impl<'a> Iterator for CommandReferencesInSpan<'a> {
2723 type Item = &'a Reference;
2724
2725 fn next(&mut self) -> Option<&'a Reference> {
2726 loop {
2727 let id = self.ids.next()?;
2728 let reference = &self.references[id.index()];
2729 if contains_span(self.outer, reference.span) {
2730 return Some(reference);
2731 }
2732 }
2733 }
2734}
2735
2736#[derive(Debug, Clone)]
2741pub struct BindingsInSpan<'a> {
2742 bindings: &'a [Binding],
2743 ids: std::slice::Iter<'a, BindingId>,
2744 end: usize,
2745}
2746
2747impl<'a> Iterator for BindingsInSpan<'a> {
2748 type Item = &'a Binding;
2749
2750 fn next(&mut self) -> Option<&'a Binding> {
2751 loop {
2752 let id = self.ids.next()?;
2753 let binding = &self.bindings[id.index()];
2754 if binding.span.start.offset > self.end {
2755 return None;
2756 }
2757 if binding.span.end.offset <= self.end {
2758 return Some(binding);
2759 }
2760 }
2761 }
2762}
2763
2764fn scope_span_width(span: Span) -> usize {
2765 span.end.offset.saturating_sub(span.start.offset)
2766}
2767
2768fn contains_span(outer: Span, inner: Span) -> bool {
2769 outer.start.offset <= inner.start.offset && outer.end.offset >= inner.end.offset
2770}
2771
2772#[cfg(test)]
2773fn linear_scope_at(scopes: &[Scope], offset: usize) -> ScopeId {
2774 scopes
2775 .iter()
2776 .filter(|scope| contains_offset(scope.span, offset))
2777 .min_by_key(|scope| scope_span_width(scope.span))
2778 .map(|scope| scope.id)
2779 .unwrap_or(ScopeId(0))
2780}
2781
2782fn build_indirect_targets_by_binding(
2783 bindings: &[Binding],
2784 indirect_target_hints: &FxHashMap<BindingId, IndirectTargetHint>,
2785) -> FxHashMap<BindingId, Vec<BindingId>> {
2786 let mut targets_by_binding = FxHashMap::default();
2787 for (binding_id, hint) in indirect_target_hints {
2788 let targets: Vec<_> = bindings
2789 .iter()
2790 .filter(|binding| indirect_target_matches(hint, binding))
2791 .map(|binding| binding.id)
2792 .collect();
2793 if !targets.is_empty() {
2794 targets_by_binding.insert(*binding_id, targets);
2795 }
2796 }
2797 targets_by_binding
2798}
2799
2800fn build_indirect_targets_by_reference(
2801 references: &[Reference],
2802 resolved: &FxHashMap<ReferenceId, BindingId>,
2803 indirect_expansion_refs: &FxHashSet<ReferenceId>,
2804 indirect_targets_by_binding: &FxHashMap<BindingId, Vec<BindingId>>,
2805) -> FxHashMap<ReferenceId, Vec<BindingId>> {
2806 let mut targets_by_reference = FxHashMap::default();
2807 for reference in references {
2808 if !indirect_expansion_refs.contains(&reference.id) {
2809 continue;
2810 }
2811 let Some(binding_id) = resolved.get(&reference.id).copied() else {
2812 continue;
2813 };
2814 if let Some(targets) = indirect_targets_by_binding.get(&binding_id) {
2815 targets_by_reference.insert(reference.id, targets.clone());
2816 }
2817 }
2818 targets_by_reference
2819}
2820
2821fn build_array_like_indirect_expansion_refs(
2822 references: &[Reference],
2823 resolved: &FxHashMap<ReferenceId, BindingId>,
2824 indirect_expansion_refs: &FxHashSet<ReferenceId>,
2825 indirect_target_hints: &FxHashMap<BindingId, IndirectTargetHint>,
2826) -> FxHashSet<ReferenceId> {
2827 let mut array_like_refs = FxHashSet::default();
2828 for reference in references {
2829 if !indirect_expansion_refs.contains(&reference.id) {
2830 continue;
2831 }
2832 let Some(binding_id) = resolved.get(&reference.id).copied() else {
2833 continue;
2834 };
2835 let Some(hint) = indirect_target_hints.get(&binding_id) else {
2836 continue;
2837 };
2838 let array_like = match hint {
2839 IndirectTargetHint::Exact { array_like, .. }
2840 | IndirectTargetHint::Pattern { array_like, .. } => *array_like,
2841 };
2842 if array_like {
2843 array_like_refs.insert(reference.id);
2844 }
2845 }
2846 array_like_refs
2847}
2848
2849fn indirect_target_matches(hint: &IndirectTargetHint, binding: &Binding) -> bool {
2850 match hint {
2851 IndirectTargetHint::Exact { name, array_like } => {
2852 binding.name == *name && (!array_like || binding::is_array_like_binding(binding))
2853 }
2854 IndirectTargetHint::Pattern {
2855 prefix,
2856 suffix,
2857 array_like,
2858 } => {
2859 let name = binding.name.as_str();
2860 name.starts_with(prefix.as_str())
2861 && name.ends_with(suffix.as_str())
2862 && (!array_like || binding::is_array_like_binding(binding))
2863 }
2864 }
2865}
2866
2867#[cfg(test)]
2868mod plugins;
2869#[cfg(test)]
2870mod tests;