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