use std::cell::RefCell;

use rustc_hash::{FxHashMap, FxHashSet};
use shuck_ast::{
    BinaryOp, BourneParameterExpansion, BuiltinCommand, Command, CompoundCommand, FunctionDef,
    Name, ParameterExpansion, ParameterExpansionSyntax, ParameterOp, Position, RedirectKind, Span,
    Stmt, StmtSeq, StmtTerminator, VarRef, Word, WordPart, WordPartNode, static_word_text,
    word_is_standalone_status_capture, word_is_standalone_variable_like,
};
use shuck_semantic::{
    AssignmentValueOrigin, BindingAttributes, BindingKind, BindingOrigin, CallSite,
    LoopValueOrigin, ScopeId, ScopeKind, SemanticAnalysis, SemanticModel, SemanticValueFlow,
    UninitializedCertainty,
};
use shuck_semantic::{BindingId, BlockId, ReferenceId};

use crate::facts::words::analyze_literal_runtime;
use crate::{ExpansionContext, FactSpan, LinterFacts};

type S001FunctionEventKey = Vec<usize>;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SafeValueQuery {
    Argv,
    RedirectTarget,
    NumericTestOperand,
    Pattern,
    Regex,
    Quoted,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FieldSafeBindingClass {
    Empty,
    NonEmpty,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum S001QuoteExposure {
    Unsafe,
    Empty,
    QuoteInertNonEmpty,
}

impl SafeValueQuery {
    pub fn from_context(context: ExpansionContext) -> Option<Self> {
        match context {
            ExpansionContext::CommandName
            | ExpansionContext::CommandArgument
            | ExpansionContext::HereString
            | ExpansionContext::DeclarationAssignmentValue => Some(Self::Argv),
            ExpansionContext::RedirectTarget(_) | ExpansionContext::DescriptorDupTarget(_) => {
                Some(Self::RedirectTarget)
            }
            ExpansionContext::CasePattern
            | ExpansionContext::ConditionalPattern
            | ExpansionContext::ParameterPattern => Some(Self::Pattern),
            ExpansionContext::RegexOperand => Some(Self::Regex),
            ExpansionContext::AssignmentValue
            | ExpansionContext::ForList
            | ExpansionContext::SelectList
            | ExpansionContext::StringTestOperand
            | ExpansionContext::ConditionalVarRefSubscript
            | ExpansionContext::TrapAction => None,
        }
    }

    fn operand_context(self) -> Option<ExpansionContext> {
        match self {
            Self::Argv => Some(ExpansionContext::CommandArgument),
            Self::NumericTestOperand => Some(ExpansionContext::CommandArgument),
            Self::RedirectTarget => Some(ExpansionContext::RedirectTarget(RedirectKind::Output)),
            Self::Pattern => Some(ExpansionContext::CasePattern),
            Self::Regex => Some(ExpansionContext::RegexOperand),
            Self::Quoted => None,
        }
    }

    fn is_field_context(self) -> bool {
        matches!(
            self,
            Self::Argv | Self::RedirectTarget | Self::NumericTestOperand
        )
    }

    fn literal_is_safe(self, text: &str) -> bool {
        match self {
            Self::Argv | Self::RedirectTarget | Self::NumericTestOperand => {
                literal_is_field_safe(text)
            }
            Self::Pattern => literal_is_pattern_safe(text),
            Self::Regex => literal_is_regex_safe(text),
            Self::Quoted => true,
        }
    }
}

pub struct SafeValueIndex<'a> {
    semantic: &'a SemanticModel,
    analysis: &'a SemanticAnalysis<'a>,
    value_flow: RefCell<SemanticValueFlow<'a, 'a>>,
    facts: &'a LinterFacts<'a>,
    source: &'a str,
    command_cover_memo: RefCell<FxHashMap<(crate::facts::CommandId, Name, FactSpan), bool>>,
    terminal_inline_commands: Vec<(usize, crate::facts::CommandId)>,
    terminal_exit_function_commands: FxHashSet<crate::facts::CommandId>,
    memo: FxHashMap<(FactSpan, FactSpan, SafeValueQuery, Option<ScopeId>), bool>,
    visiting: FxHashSet<(FactSpan, FactSpan, SafeValueQuery, Option<ScopeId>)>,
    binding_value_stack: Vec<BindingId>,
    s001_unset_before_call_memo: FxHashMap<ScopeId, bool>,
    #[cfg(test)]
    uninitialized_reference_overrides: FxHashMap<FactSpan, UninitializedCertainty>,
}

impl<'a> SafeValueIndex<'a> {
    pub fn build(
        semantic: &'a SemanticModel,
        analysis: &'a SemanticAnalysis<'a>,
        facts: &'a LinterFacts<'a>,
        source: &'a str,
    ) -> Self {
        let mut terminal_inline_commands = facts
            .commands()
            .iter()
            .filter(|command| !command.is_nested_word_command())
            .filter(|command| {
                matches!(
                    stmt_terminal_flow_kind(command.stmt()),
                    TerminalFlowKind::Exit | TerminalFlowKind::Stop
                )
            })
            .map(|command| (command.span().end.offset(), command.id()))
            .collect::<Vec<_>>();
        terminal_inline_commands
            .sort_unstable_by_key(|(end_offset, command_id)| (*end_offset, command_id.index()));
        let terminal_exit_function_commands = facts
            .command_facts()
            .function_headers()
            .iter()
            .filter(|header| function_has_terminal_exit(header.function()))
            .map(|header| header.command_id())
            .collect::<FxHashSet<_>>();

        Self {
            semantic,
            analysis,
            value_flow: RefCell::new(analysis.value_flow()),
            facts,
            source,
            command_cover_memo: RefCell::new(FxHashMap::default()),
            terminal_inline_commands,
            terminal_exit_function_commands,
            memo: FxHashMap::default(),
            visiting: FxHashSet::default(),
            binding_value_stack: Vec::new(),
            s001_unset_before_call_memo: FxHashMap::default(),
            #[cfg(test)]
            uninitialized_reference_overrides: FxHashMap::default(),
        }
    }

    pub fn part_is_safe(&mut self, part: &WordPart, span: Span, query: SafeValueQuery) -> bool {
        if part_is_safe_special_parameter_access(part) {
            return true;
        }
        if self.span_is_after_unconditional_inline_terminator(span) {
            return false;
        }
        match part {
            WordPart::ZshQualifiedGlob(_) => query == SafeValueQuery::Quoted,
            WordPart::Parameter(parameter) => self.parameter_part_is_safe(parameter, span, query),
            WordPart::Literal(_) | WordPart::SingleQuoted { .. } => {
                self.literal_part_is_safe(part, span, query)
            }
            WordPart::DoubleQuoted { parts, .. } => parts
                .iter()
                .all(|part| self.part_is_safe(&part.kind, part.span, query)),
            WordPart::Variable(name) => self.name_is_safe(name, span, query),
            WordPart::ArithmeticExpansion { .. } => true,
            WordPart::Length(_) | WordPart::ArrayLength(_) => true,
            WordPart::ArrayAccess(reference) => {
                (query == SafeValueQuery::Quoted || !reference.has_array_selector())
                    && self.reference_is_safe(reference, span, query)
            }
            WordPart::Substring { reference, .. } => self.reference_is_safe(reference, span, query),
            WordPart::Transformation {
                reference,
                operator,
            } => self.transformation_is_safe(reference, *operator, span, query),
            WordPart::IndirectExpansion {
                reference,
                operator,
                operand_word_ast,
                ..
            } => {
                self.indirect_name_is_safe(reference, span, query)
                    && operator.as_deref().is_none_or(|operator| {
                        self.parameter_operator_is_safe(
                            &reference.name,
                            operator,
                            operand_word_ast.as_deref(),
                            span,
                            query,
                        )
                    })
            }
            WordPart::CommandSubstitution { .. }
            | WordPart::ArrayIndices(_)
            | WordPart::ArraySlice { .. }
            | WordPart::PrefixMatch { .. }
            | WordPart::ProcessSubstitution { .. } => query == SafeValueQuery::Quoted,
            WordPart::ParameterExpansion {
                reference,
                operator,
                operand_word_ast,
                ..
            } => self.parameter_expansion_is_safe(
                reference,
                operator,
                operand_word_ast.as_deref(),
                span,
                query,
            ),
        }
    }

    pub fn word_is_safe(&mut self, word: &Word, query: SafeValueQuery) -> bool {
        if self.span_is_after_unconditional_inline_terminator(word.span) {
            return false;
        }
        let Some(fact) = self.facts.words().any_word_fact(word.span) else {
            return false;
        };
        let analysis = fact.analysis();
        if query != SafeValueQuery::Quoted
            && (analysis.array_valued || analysis.hazards.command_or_process_substitution)
        {
            return false;
        }
        if fact.safe_value_special_parameter_access() {
            return true;
        }

        fact.parts_with_spans()
            .all(|(part, span)| self.part_is_safe(part, span, query))
    }

    fn uninitialized_reference_certainty_at(&self, span: Span) -> Option<UninitializedCertainty> {
        #[cfg(test)]
        if let Some(certainty) = self
            .uninitialized_reference_overrides
            .get(&FactSpan::new(span))
            .copied()
        {
            return Some(certainty);
        }

        self.analysis.uninitialized_reference_certainty_at(span)
    }

    #[cfg(test)]
    fn override_uninitialized_reference_certainty(
        &mut self,
        span: Span,
        certainty: UninitializedCertainty,
    ) {
        self.uninitialized_reference_overrides
            .insert(FactSpan::new(span), certainty);
    }

    #[cfg(test)]
    pub fn word_occurrence_is_safe(
        &mut self,
        fact: crate::WordOccurrenceRef<'_, 'a>,
        query: SafeValueQuery,
    ) -> bool {
        if self.span_is_after_unconditional_inline_terminator(fact.span()) {
            return false;
        }
        let analysis = fact.analysis();
        if query != SafeValueQuery::Quoted
            && (analysis.array_valued || analysis.hazards.command_or_process_substitution)
        {
            return false;
        }
        if fact.safe_value_special_parameter_access() {
            return true;
        }

        fact.parts_with_spans()
            .all(|(part, span)| self.part_is_safe(part, span, query))
    }

    fn word_fact(&self, word: &Word) -> Option<crate::WordOccurrenceRef<'_, 'a>> {
        self.facts.words().any_word_fact(word.span)
    }

    fn word_plain_scalar_reference_name(&self, word: &Word) -> Option<Name> {
        self.word_fact(word)
            .and_then(|fact| fact.safe_value_plain_scalar_reference_name().cloned())
    }

    fn word_has_arithmetic_expansion(&self, word: &Word) -> bool {
        self.word_fact(word)
            .is_some_and(|fact| fact.has_arithmetic_expansion())
    }

    fn word_contains_special_parameter_slice(&self, word: &Word) -> bool {
        self.word_fact(word)
            .is_some_and(|fact| fact.safe_value_contains_special_parameter_slice())
    }

    pub fn name_reference_is_safe(&mut self, name: &Name, at: Span, query: SafeValueQuery) -> bool {
        self.name_is_safe(name, at, query)
    }

    pub fn part_s001_quote_exposure(
        &mut self,
        part: &WordPart,
        span: Span,
        query: SafeValueQuery,
    ) -> S001QuoteExposure {
        if !query.is_field_context() {
            return if self.part_is_safe(part, span, query) {
                S001QuoteExposure::QuoteInertNonEmpty
            } else {
                S001QuoteExposure::Unsafe
            };
        }
        if self.span_is_after_unconditional_inline_terminator(span) {
            return S001QuoteExposure::Unsafe;
        }

        match part {
            WordPart::Literal(_) | WordPart::SingleQuoted { .. } => {
                self.literal_part_s001_quote_exposure(part, span, query)
            }
            WordPart::DoubleQuoted { parts, .. } => {
                let mut saw_empty = false;
                for part in parts {
                    match self.part_s001_quote_exposure(&part.kind, part.span, query) {
                        S001QuoteExposure::QuoteInertNonEmpty => {
                            return S001QuoteExposure::QuoteInertNonEmpty;
                        }
                        S001QuoteExposure::Empty => saw_empty = true,
                        S001QuoteExposure::Unsafe => return S001QuoteExposure::Unsafe,
                    }
                }
                if saw_empty {
                    S001QuoteExposure::Empty
                } else {
                    S001QuoteExposure::QuoteInertNonEmpty
                }
            }
            WordPart::Variable(name) => self.name_s001_quote_exposure(name, span, query),
            WordPart::Parameter(parameter) => {
                self.parameter_part_s001_quote_exposure(parameter, span, query)
            }
            WordPart::ArrayAccess(reference) => {
                if reference.has_array_selector() {
                    S001QuoteExposure::Unsafe
                } else {
                    self.name_s001_quote_exposure(&reference.name, span, query)
                }
            }
            WordPart::Substring { reference, .. } => {
                self.name_s001_quote_exposure(&reference.name, span, query)
            }
            WordPart::Transformation { reference, .. } => {
                self.name_s001_quote_exposure(&reference.name, span, query)
            }
            WordPart::IndirectExpansion { reference, .. } => {
                self.name_s001_quote_exposure(&reference.name, span, query)
            }
            WordPart::ArithmeticExpansion { .. }
            | WordPart::Length(_)
            | WordPart::ArrayLength(_) => S001QuoteExposure::QuoteInertNonEmpty,
            WordPart::ZshQualifiedGlob(_)
            | WordPart::CommandSubstitution { .. }
            | WordPart::ArrayIndices(_)
            | WordPart::ArraySlice { .. }
            | WordPart::PrefixMatch { .. }
            | WordPart::ProcessSubstitution { .. }
            | WordPart::ParameterExpansion { .. } => {
                if self.part_is_safe(part, span, query) {
                    S001QuoteExposure::QuoteInertNonEmpty
                } else {
                    S001QuoteExposure::Unsafe
                }
            }
        }
    }

    pub fn span_has_s001_function_unset_exposure(
        &mut self,
        span: Span,
        query: SafeValueQuery,
    ) -> bool {
        query.is_field_context() && self.s001_reference_function_unset_before_first_call(span)
    }

    pub fn part_has_s001_standalone_numeric_argv_exposure(
        &mut self,
        part: &WordPart,
        span: Span,
    ) -> bool {
        if self.span_is_return_argument(span) {
            return false;
        }
        let Some(name) = plain_scalar_reference_name_from_part(part) else {
            return false;
        };

        self.s001_name_has_only_numeric_value_bindings(&name, span)
    }

    pub fn part_has_s001_arithmetic_numeric_operand_exposure(
        &mut self,
        part: &WordPart,
        span: Span,
    ) -> bool {
        let Some(name) = plain_scalar_reference_name_from_part(part) else {
            return false;
        };

        self.s001_name_has_only_arithmetic_numeric_bindings(&name, span)
    }

    pub fn part_is_safe_initializer_command_substitution_self_reference(
        &mut self,
        part: &WordPart,
        span: Span,
        query: SafeValueQuery,
    ) -> bool {
        let Some(name) = plain_scalar_reference_name_from_part(part) else {
            return false;
        };

        self.semantic
            .bindings_for(&name)
            .iter()
            .copied()
            .any(|binding_id| {
                let binding = self.semantic.binding(binding_id);
                if binding.span.start.offset() > span.start.offset() {
                    return false;
                }
                let Some(word) = self
                    .facts
                    .assignments()
                    .binding_value(binding_id)
                    .and_then(|value| value.scalar_word())
                else {
                    return false;
                };
                if !span_contains(word.span, span) {
                    return false;
                }

                self.facts
                    .words()
                    .any_word_fact(word.span)
                    .is_some_and(|fact| {
                        fact.command_substitution_spans()
                            .iter()
                            .copied()
                            .any(|command_substitution| span_contains(command_substitution, span))
                    })
                    && !self.span_is_inside_loop_context(span)
                    && !self.span_is_inside_if_condition(span)
                    && {
                        self.binding_value_stack.push(binding_id);
                        let result = self.name_is_safe(&name, span, query);
                        self.binding_value_stack.pop();
                        result
                    }
            })
    }

    pub fn part_is_safe_initializer_command_substitution_static_setup_reference(
        &mut self,
        part: &WordPart,
        span: Span,
        query: SafeValueQuery,
    ) -> bool {
        if query != SafeValueQuery::Argv {
            return false;
        }
        let Some(name) = plain_scalar_reference_name_from_part(part) else {
            return false;
        };
        if !shell_name_is_uppercase_setup_value(name.as_str())
            || !self.span_is_inside_initializer_command_substitution(span)
            || self.span_is_inside_loop_context(span)
            || self.span_is_inside_if_condition(span)
        {
            return false;
        }

        let mut setup_bindings = Vec::new();
        for binding_id in self.semantic.bindings_for(&name).iter().copied() {
            let binding = self.semantic.binding(binding_id);
            if binding.span.start.offset() >= span.start.offset() {
                continue;
            }
            if binding.attributes.contains(BindingAttributes::LOCAL) {
                return false;
            }
            if !matches!(
                self.semantic.scope(binding.scope).kind,
                ScopeKind::File | ScopeKind::Function(_)
            ) {
                return false;
            }
            let setup_atom = self
                .facts
                .assignments()
                .binding_value(binding_id)
                .and_then(|value| value.scalar_word())
                .and_then(|word| static_word_text(word, self.source))
                .is_some_and(|text| literal_is_setup_atom(&text));
            if !setup_atom {
                return false;
            }
            setup_bindings.push(binding_id);
        }

        !setup_bindings.is_empty()
            && (self.bindings_cover_all_paths_to_reference(&setup_bindings, &name, span)
                || setup_bindings.iter().copied().any(|binding_id| {
                    let binding = self.semantic.binding(binding_id);
                    binding.span.end.offset() <= span.start.offset()
                        && self.binding_dominates_reference(binding_id, &name, span)
                }))
    }

    fn span_is_inside_initializer_command_substitution(&self, span: Span) -> bool {
        self.semantic.bindings().iter().any(|binding| {
            let Some(word) = self
                .facts
                .assignments()
                .binding_value(binding.id)
                .and_then(|value| value.scalar_word())
            else {
                return false;
            };
            span_contains(word.span, span)
                && self
                    .facts
                    .words()
                    .any_word_fact(word.span)
                    .is_some_and(|fact| {
                        fact.command_substitution_spans()
                            .iter()
                            .copied()
                            .any(|command_substitution| span_contains(command_substitution, span))
                    })
        })
    }

    fn span_is_inside_loop_context(&self, span: Span) -> bool {
        let mut current = self
            .facts
            .command_facts()
            .innermost_command_id_at(span.start.offset())
            .or_else(|| {
                self.facts
                    .command_facts()
                    .innermost_command_id_containing_offset(span.start.offset())
            });
        while let Some(command_id) = current {
            if matches!(
                self.facts.command_facts().command(command_id).command(),
                Command::Compound(
                    CompoundCommand::For(_)
                        | CompoundCommand::Repeat(_)
                        | CompoundCommand::Foreach(_)
                        | CompoundCommand::ArithmeticFor(_)
                        | CompoundCommand::While(_)
                        | CompoundCommand::Until(_)
                        | CompoundCommand::Select(_)
                )
            ) {
                return true;
            }
            current = self.facts.command_facts().command_parent_id(command_id);
        }

        false
    }

    fn span_is_inside_if_condition(&self, span: Span) -> bool {
        let mut current = self
            .facts
            .command_facts()
            .innermost_command_id_at(span.start.offset())
            .or_else(|| {
                self.facts
                    .command_facts()
                    .innermost_command_id_containing_offset(span.start.offset())
            });
        while let Some(command_id) = current {
            if let Command::Compound(CompoundCommand::If(command)) =
                self.facts.command_facts().command(command_id).command()
                && (span_contains(command.condition.span, span)
                    || command
                        .elif_branches
                        .iter()
                        .any(|(condition, _)| span_contains(condition.span, span)))
            {
                return true;
            }
            current = self.facts.command_facts().command_parent_id(command_id);
        }

        false
    }

    fn literal_part_is_safe(&self, part: &WordPart, span: Span, query: SafeValueQuery) -> bool {
        let word = Word {
            parts: vec![WordPartNode::new(part.clone(), span)],
            span,
            brace_syntax: Vec::new(),
        };
        if let Some(context) = query.operand_context()
            && self.literal_runtime_is_unsafe_for_safe_value(&word, context)
        {
            return false;
        }

        static_word_text(&word, self.source).is_some_and(|text| query.literal_is_safe(&text))
    }

    fn literal_part_s001_quote_exposure(
        &self,
        part: &WordPart,
        span: Span,
        query: SafeValueQuery,
    ) -> S001QuoteExposure {
        let word = Word {
            parts: vec![WordPartNode::new(part.clone(), span)],
            span,
            brace_syntax: Vec::new(),
        };
        if let Some(context) = query.operand_context()
            && self.literal_runtime_is_unsafe_for_safe_value(&word, context)
        {
            return S001QuoteExposure::Unsafe;
        }

        static_word_text(&word, self.source).map_or(S001QuoteExposure::Unsafe, |text| {
            if !query.literal_is_safe(&text) {
                S001QuoteExposure::Unsafe
            } else if text.is_empty() {
                S001QuoteExposure::Empty
            } else {
                S001QuoteExposure::QuoteInertNonEmpty
            }
        })
    }

    fn literal_runtime_is_unsafe_for_safe_value(
        &self,
        word: &Word,
        context: ExpansionContext,
    ) -> bool {
        let behavior = self
            .facts
            .command_facts()
            .expansion_behavior_at(word.span.start.offset());
        let runtime = analyze_literal_runtime(word, self.source, context, Some(&behavior));
        if !runtime.is_runtime_sensitive() {
            return false;
        }

        !self.in_binding_value()
            || !runtime.hazards.tilde_expansion
            || runtime.hazards.pathname_matching
            || runtime.hazards.brace_fanout
    }

    fn in_binding_value(&self) -> bool {
        !self.binding_value_stack.is_empty()
    }

    fn name_is_safe(&mut self, name: &Name, at: Span, query: SafeValueQuery) -> bool {
        if safe_special_parameter(name) {
            return true;
        }
        if self.case_cli_reachable_call_path_keeps_argument_bindings_unsafe(at, query) {
            let mut bindings = self.safe_bindings_for_name(name, at);
            self.drop_declarations_shadowed_by_covering_loop_bindings(&mut bindings, at);
            self.drop_outer_bindings_shadowed_by_covering_loop_bindings(&mut bindings, at);
            bindings.retain(|binding_id| {
                !self.binding_is_cleared_by_dominating_unset(*binding_id, name, at)
            });
            bindings.retain(|binding_id| {
                !self.binding_is_blocked_by_exit_like_function_call(*binding_id, at)
            });
            if self.local_declaration_status_capture_bindings_cover_reference(
                &bindings, name, at, query,
            ) {
                return true;
            }
            return safe_numeric_shell_variable(name);
        }
        if query == SafeValueQuery::NumericTestOperand
            && self.s001_function_reference_has_file_scope_integer_bindings(name, at)
        {
            return true;
        }

        let mut bindings = self.safe_bindings_for_name(name, at);
        self.drop_declarations_shadowed_by_covering_loop_bindings(&mut bindings, at);
        self.drop_outer_bindings_shadowed_by_covering_loop_bindings(&mut bindings, at);
        bindings.retain(|binding_id| {
            !self.binding_is_cleared_by_dominating_unset(*binding_id, name, at)
        });
        if query.is_field_context() {
            bindings.retain(|binding_id| {
                !self.binding_is_blocked_by_exit_like_function_call(*binding_id, at)
            });
        }
        let case_cli_scope = query
            .is_field_context()
            .then(|| self.case_cli_dispatch_scope_at(at.start.offset()))
            .flatten();
        if bindings.is_empty()
            && self.status_capture_declaration_probe_covers_reference(
                name,
                at,
                query,
                case_cli_scope,
            )
        {
            return true;
        }
        if bindings.is_empty() {
            return safe_numeric_shell_variable(name);
        }
        if self
            .local_declaration_status_capture_bindings_cover_reference(&bindings, name, at, query)
        {
            return true;
        }
        let binding_belongs_to_case_cli_scope = case_cli_scope.is_some_and(|scope| {
            bindings
                .iter()
                .copied()
                .any(|binding_id| self.binding_is_in_scope_or_descendant(binding_id, scope))
        });
        if query.is_field_context()
            && case_cli_scope.is_some()
            && !self.case_cli_dispatch_outer_bindings_can_stay_safe(&bindings, at, query)
            && !binding_belongs_to_case_cli_scope
        {
            return safe_numeric_shell_variable(name);
        }
        if query.is_field_context() && self.bindings_are_all_plain_empty_static_literals(&bindings)
        {
            return false;
        }
        if self.covering_optional_field_safe_bindings_can_stay_safe(&bindings, name, at, query) {
            return true;
        }
        if self.local_conditional_literal_bindings_can_stay_safe(&bindings, name, at, query) {
            return true;
        }
        if self.optional_field_safe_bindings_can_stay_safe(&bindings, query) {
            return true;
        }
        if self.status_capture_bindings_cover_reference(&bindings, name, at, query, case_cli_scope)
        {
            return true;
        }
        if self.status_capture_subset_covers_reference(&bindings, name, at, query, case_cli_scope) {
            return true;
        }
        let helper_bindings = self
            .called_helper_bindings_for_name(name, at)
            .into_iter()
            .collect::<FxHashSet<_>>();
        let needs_arg_path_coverage = query.is_field_context();
        let bindings_cover_all_paths = helper_bindings.is_empty()
            && needs_arg_path_coverage
            && self.value_sources_cover_all_paths_to_reference(&bindings, name, at);
        let unset_covers_reference = needs_arg_path_coverage
            && !bindings.is_empty()
            && self.unset_command_covers_reference(name, at);
        let direct_bindings = if helper_bindings.is_empty() {
            Vec::new()
        } else {
            bindings
                .iter()
                .copied()
                .filter(|binding_id| !helper_bindings.contains(binding_id))
                .collect::<Vec<_>>()
        };
        let direct_bindings_cover_all_paths = needs_arg_path_coverage
            && !direct_bindings.is_empty()
            && self.bindings_cover_all_paths_to_reference(&direct_bindings, name, at);
        let unsafe_helper_binding_writes_visible_local = self
            .enclosing_function_scope_at(at.start.offset())
            .is_some_and(|scope| {
                helper_bindings.iter().copied().any(|binding_id| {
                    self.binding_writes_visible_local_in_scope_before(binding_id, scope, at)
                        && !self.binding_is_safe(binding_id, at, query, case_cli_scope)
                })
            });
        if unsafe_helper_binding_writes_visible_local {
            return false;
        }
        if direct_bindings_cover_all_paths
            && self
                .enclosing_function_scope_at(at.start.offset())
                .is_some()
            && !self.span_is_exit_or_return_argument(at)
            && self.covering_direct_field_safe_bindings_can_stay_safe(&direct_bindings, query)
        {
            return true;
        }
        if needs_arg_path_coverage
            && !bindings_cover_all_paths
            && !direct_bindings_cover_all_paths
            && self.one_sided_bindings_preserve_safe_base(
                &bindings,
                name,
                at,
                query,
                case_cli_scope,
            )
        {
            return true;
        }
        if needs_arg_path_coverage
            && !direct_bindings_cover_all_paths
            && !bindings.is_empty()
            && bindings
                .iter()
                .copied()
                .all(|binding_id| helper_bindings.contains(&binding_id))
            && bindings
                .iter()
                .copied()
                .all(|binding_id| self.binding_is_one_sided_short_circuit_assignment(binding_id))
        {
            return false;
        }
        if needs_arg_path_coverage
            && !direct_bindings_cover_all_paths
            && bindings
                .iter()
                .copied()
                .all(|binding_id| self.binding_is_one_sided_append_assignment(binding_id))
        {
            return bindings
                .iter()
                .copied()
                .all(|binding_id| self.binding_is_safe(binding_id, at, query, case_cli_scope));
        }
        let direct_bindings_are_status_captures =
            direct_bindings.iter().copied().all(|binding_id| {
                self.binding_is_standalone_status_capture(binding_id, at, case_cli_scope)
            });
        if direct_bindings_cover_all_paths && direct_bindings_are_status_captures {
            bindings.retain(|binding_id| !helper_bindings.contains(binding_id));
        }
        let outer_bindings_cover_callers = !needs_arg_path_coverage
            || self.helper_outer_bindings_cover_all_caller_paths(name, at, &bindings);
        let reference_is_inside_function = self
            .enclosing_function_scope_at(at.start.offset())
            .is_some();
        if helper_bindings.is_empty()
            && needs_arg_path_coverage
            && !bindings_cover_all_paths
            && !unset_covers_reference
            && (!outer_bindings_cover_callers || !reference_is_inside_function)
        {
            return false;
        }
        if !outer_bindings_cover_callers && !direct_bindings_cover_all_paths {
            return false;
        }
        match self.uninitialized_reference_certainty_at(at) {
            Some(UninitializedCertainty::Definite) => {
                if bindings.iter().copied().any(|binding_id| {
                    !helper_bindings.contains(&binding_id)
                        && self.binding_is_guarded_before_reference(binding_id, at)
                }) {
                    return false;
                }
            }
            Some(UninitializedCertainty::Possible) => {
                let has_dominating_binding = bindings
                    .iter()
                    .copied()
                    .any(|binding_id| self.binding_dominates_reference(binding_id, name, at));
                if !has_dominating_binding
                    && !bindings_cover_all_paths
                    && !unset_covers_reference
                    && !bindings
                        .iter()
                        .copied()
                        .all(|binding_id| helper_bindings.contains(&binding_id))
                {
                    return false;
                }
            }
            None => {}
        }

        bindings
            .into_iter()
            .all(|binding_id| self.binding_is_safe(binding_id, at, query, case_cli_scope))
    }

    fn name_s001_quote_exposure(
        &mut self,
        name: &Name,
        at: Span,
        query: SafeValueQuery,
    ) -> S001QuoteExposure {
        if safe_special_parameter(name) {
            return S001QuoteExposure::QuoteInertNonEmpty;
        }
        if query.is_field_context() && self.s001_reference_function_unset_before_first_call(at) {
            return S001QuoteExposure::Unsafe;
        }
        if query == SafeValueQuery::NumericTestOperand
            && self.s001_function_reference_has_file_scope_integer_bindings(name, at)
        {
            return S001QuoteExposure::QuoteInertNonEmpty;
        }
        if self.name_is_safe(name, at, query) {
            return S001QuoteExposure::QuoteInertNonEmpty;
        }
        if !query.is_field_context() {
            return S001QuoteExposure::Unsafe;
        }

        let mut bindings = self.safe_bindings_for_name(name, at);
        self.drop_declarations_shadowed_by_covering_loop_bindings(&mut bindings, at);
        self.drop_outer_bindings_shadowed_by_covering_loop_bindings(&mut bindings, at);
        bindings.retain(|binding_id| {
            !self.binding_is_cleared_by_dominating_unset(*binding_id, name, at)
        });
        bindings.retain(|binding_id| {
            !self.binding_is_blocked_by_exit_like_function_call(*binding_id, at)
        });
        if !bindings.is_empty() {
            let dispatch_bindings = self.s001_top_level_dispatch_helper_bindings_before(name, at);
            if !dispatch_bindings.is_empty() {
                let mut combined = bindings.clone();
                combined.extend(dispatch_bindings);
                combined.sort_by_key(|binding_id| {
                    self.semantic.binding(*binding_id).span.start.offset()
                });
                combined.dedup();
                if let Some(exposure) =
                    self.s001_field_safe_binding_group_exposure(&combined, at, query)
                {
                    return exposure;
                }
            }
            return S001QuoteExposure::Unsafe;
        }

        let dispatch_bindings = self.s001_top_level_dispatch_helper_bindings_before(name, at);
        self.s001_field_safe_binding_group_exposure(&dispatch_bindings, at, query)
            .unwrap_or(S001QuoteExposure::Unsafe)
    }

    fn case_cli_dispatch_scope_at(&self, offset: usize) -> Option<ScopeId> {
        self.semantic
            .ancestor_scopes(self.semantic.scope_at(offset))
            .find(|scope| {
                self.facts
                    .command_facts()
                    .function_cli_dispatch_facts(*scope)
                    .exported_from_case_cli()
            })
    }

    fn case_cli_reachable_function_scope_at(&self, offset: usize) -> Option<ScopeId> {
        let scope = self.analysis.enclosing_function_scope_at(offset)?;
        self.facts
            .command_facts()
            .is_case_cli_reachable_function_scope(scope)
            .then_some(scope)
    }

    fn case_cli_reachable_call_path_keeps_argument_bindings_unsafe(
        &self,
        at: Span,
        query: SafeValueQuery,
    ) -> bool {
        if !query.is_field_context() || self.span_is_within_command_name(at) {
            return false;
        }
        let Some(scope) = self.case_cli_reachable_function_scope_at(at.start.offset()) else {
            return false;
        };

        self.facts
            .command_facts()
            .function_cli_dispatch_facts(scope)
            .exported_from_case_cli()
            || self.static_caller_is_case_cli_exported(scope, &mut FxHashSet::default())
            || self.named_function_call_sites(scope).is_empty()
    }

    fn static_caller_is_case_cli_exported(
        &self,
        scope: ScopeId,
        seen: &mut FxHashSet<ScopeId>,
    ) -> bool {
        if !seen.insert(scope) {
            return false;
        }

        self.named_function_call_sites(scope)
            .into_iter()
            .any(|(caller_scope, _)| {
                self.facts
                    .command_facts()
                    .function_cli_dispatch_facts(caller_scope)
                    .exported_from_case_cli()
                    || self.static_caller_is_case_cli_exported(caller_scope, seen)
            })
    }

    fn case_cli_dispatch_outer_bindings_can_stay_safe(
        &self,
        bindings: &[BindingId],
        at: Span,
        query: SafeValueQuery,
    ) -> bool {
        if query != SafeValueQuery::Argv || !self.is_argument_of_dynamic_command(at) {
            return false;
        }

        bindings
            .iter()
            .copied()
            .all(|binding_id| self.binding_is_quoted_static_literal(binding_id))
    }

    fn binding_is_in_scope_or_descendant(
        &self,
        binding_id: BindingId,
        ancestor_scope: ScopeId,
    ) -> bool {
        self.analysis
            .binding_is_in_scope_or_descendant(binding_id, ancestor_scope)
    }

    fn is_argument_of_dynamic_command(&self, at: Span) -> bool {
        self.facts.commands().iter().any(|command| {
            command.body_args().iter().any(|word| word.span == at)
                && command
                    .body_name_word()
                    .is_some_and(word_is_standalone_variable_like)
        })
    }

    fn span_is_exit_or_return_argument(&self, at: Span) -> bool {
        self.facts
            .command_facts()
            .innermost_command_id_at(at.start.offset())
            .or_else(|| {
                self.facts
                    .command_facts()
                    .innermost_command_id_containing_offset(at.start.offset())
            })
            .is_some_and(|command_id| {
                let command = self.facts.command_facts().command(command_id);
                match command.command() {
                    Command::Builtin(BuiltinCommand::Exit(command)) => {
                        command
                            .code
                            .as_ref()
                            .is_some_and(|word| span_contains(word.span, at))
                            || command
                                .extra_args
                                .iter()
                                .any(|word| span_contains(word.span, at))
                    }
                    Command::Builtin(BuiltinCommand::Return(command)) => {
                        command
                            .code
                            .as_ref()
                            .is_some_and(|word| span_contains(word.span, at))
                            || command
                                .extra_args
                                .iter()
                                .any(|word| span_contains(word.span, at))
                    }
                    Command::Simple(_)
                    | Command::Builtin(_)
                    | Command::Decl(_)
                    | Command::Binary(_)
                    | Command::Compound(_)
                    | Command::Function(_)
                    | Command::AnonymousFunction(_) => false,
                }
            })
    }

    fn span_is_return_argument(&self, at: Span) -> bool {
        self.facts
            .command_facts()
            .innermost_command_id_at(at.start.offset())
            .or_else(|| {
                self.facts
                    .command_facts()
                    .innermost_command_id_containing_offset(at.start.offset())
            })
            .is_some_and(|command_id| {
                let command = self.facts.command_facts().command(command_id);
                match command.command() {
                    Command::Builtin(BuiltinCommand::Return(command)) => {
                        command
                            .code
                            .as_ref()
                            .is_some_and(|word| span_contains(word.span, at))
                            || command
                                .extra_args
                                .iter()
                                .any(|word| span_contains(word.span, at))
                    }
                    Command::Simple(_)
                    | Command::Builtin(_)
                    | Command::Decl(_)
                    | Command::Binary(_)
                    | Command::Compound(_)
                    | Command::Function(_)
                    | Command::AnonymousFunction(_) => false,
                }
            })
    }

    fn span_is_within_command_name(&self, at: Span) -> bool {
        self.facts.commands().iter().any(|command| {
            command
                .body_name_word()
                .is_some_and(|word| span_contains(word.span, at))
        })
    }

    fn binding_is_blocked_by_exit_like_function_call(
        &self,
        binding_id: BindingId,
        at: Span,
    ) -> bool {
        let binding = self.semantic.binding(binding_id);
        self.facts
            .command_facts()
            .function_headers()
            .iter()
            .any(|header| {
                self.terminal_exit_function_commands
                    .contains(&header.command_id())
                    && header
                        .call_arity()
                        .zero_arg_call_spans()
                        .iter()
                        .filter_map(|call_span| self.command_for_name_word_span(*call_span))
                        .any(|command| {
                            !command.is_nested_word_command()
                                && command.body_args().is_empty()
                                && self.command_runs_in_unconditional_flow(command.id(), at)
                                && {
                                    let call_span = command.span_in_source(self.source);
                                    self.definition_command_resolves_at_call(
                                        header.command_id(),
                                        call_span,
                                    ) && call_span.end.offset() <= at.start.offset()
                                        && (call_span.start.offset() >= binding.span.end.offset()
                                            || call_span.end.offset()
                                                <= binding.span.start.offset())
                                }
                        })
            })
    }

    fn span_is_after_unconditional_inline_terminator(&self, at: Span) -> bool {
        for (end_offset, command_id) in &self.terminal_inline_commands {
            if *end_offset > at.start.offset() {
                break;
            }
            if self.command_runs_in_unconditional_flow(*command_id, at) {
                return true;
            }
        }

        false
    }

    fn definition_command_is_visible_at_call(
        &self,
        command_id: crate::facts::CommandId,
        call_span: Span,
    ) -> bool {
        let command = self.facts.command_facts().command(command_id);
        let command_scope = self.enclosing_function_scope_at(command.span().start.offset());
        let call_scope = self.enclosing_function_scope_at(call_span.start.offset());
        if command_scope.is_some() && command_scope != call_scope {
            return false;
        }
        if self.command_is_in_background_context(command_id) {
            return false;
        }

        let mut parent_id = self.facts.command_facts().command_parent_id(command_id);
        while let Some(id) = parent_id {
            if self.facts.command_facts().command_is_dominance_barrier(id) {
                return false;
            }
            parent_id = self.facts.command_facts().command_parent_id(id);
        }
        true
    }

    fn definition_command_resolves_at_call(
        &self,
        command_id: crate::facts::CommandId,
        call_span: Span,
    ) -> bool {
        if !self.definition_command_is_visible_at_call(command_id, call_span) {
            return false;
        }

        let command = self.facts.command_facts().command(command_id);
        let definition_scope = self.enclosing_function_scope_at(command.span().start.offset());
        let call_scope = self.enclosing_function_scope_at(call_span.start.offset());

        if definition_scope.is_none() && call_scope.is_some() {
            return true;
        }

        command.span_in_source(self.source).end.offset() <= call_span.start.offset()
    }

    fn command_for_name_word_span(
        &self,
        span: Span,
    ) -> Option<crate::facts::CommandFactRef<'a, 'a>> {
        self.facts.command_facts().command_for_name_word_span(span)
    }

    fn function_definition_command_for_scope(
        &self,
        scope: ScopeId,
    ) -> Option<crate::facts::CommandFactRef<'a, 'a>> {
        self.facts
            .command_facts()
            .function_definition_command(scope)
    }

    fn function_scope_resolves_at_call_site(&self, callee_scope: ScopeId, site: &CallSite) -> bool {
        if let Some(binding_id) = self
            .analysis
            .visible_function_binding_at_call(&site.callee, site.name_span)
        {
            return self.analysis.function_scope_for_binding(binding_id) == Some(callee_scope);
        }

        self.function_definition_command_for_scope(callee_scope)
            .is_some_and(|definition_command| {
                self.definition_command_resolves_at_call(definition_command.id(), site.span)
            })
    }

    fn s001_reference_function_unset_before_first_call(&mut self, at: Span) -> bool {
        let Some(scope) = self.enclosing_function_scope_at(at.start.offset()) else {
            return false;
        };
        if let Some(result) = self.s001_unset_before_call_memo.get(&scope) {
            return *result;
        }

        let result = self.s001_function_unset_before_first_call(scope);
        self.s001_unset_before_call_memo.insert(scope, result);
        result
    }

    fn s001_function_unset_before_first_call(&self, scope: ScopeId) -> bool {
        let Some(definition_command) = self.function_definition_command_for_scope(scope) else {
            return false;
        };
        let after_offset = definition_command.span_in_source(self.source).end.offset();
        let Some(first_unset) = self.s001_first_function_unset_event_after(scope, after_offset)
        else {
            return false;
        };

        let first_call = self.s001_first_function_call_event_after(scope, after_offset);
        first_call.is_none_or(|first_call| first_unset < first_call)
    }

    fn s001_first_function_call_event_after(
        &self,
        scope: ScopeId,
        after_offset: usize,
    ) -> Option<S001FunctionEventKey> {
        self.s001_function_call_event_keys_after_inner(
            scope,
            after_offset,
            &mut FxHashSet::default(),
        )
        .into_iter()
        .min()
    }

    fn s001_function_call_event_keys_after_inner(
        &self,
        scope: ScopeId,
        after_offset: usize,
        seen_scopes: &mut FxHashSet<ScopeId>,
    ) -> Vec<S001FunctionEventKey> {
        if !seen_scopes.insert(scope) {
            return Vec::new();
        }

        let result = (|| {
            let definition_command = self.function_definition_command_for_scope(scope)?;
            let function_kind = self.named_function_kind(scope)?;
            let mut event_keys = Vec::new();
            for function_name in function_kind.static_names() {
                for site in self.semantic.call_sites_for(function_name) {
                    if site.scope == scope {
                        continue;
                    }
                    let call_span = self
                        .command_for_name_word_span(site.span)
                        .map_or(site.span, |command| command.span_in_source(self.source));
                    if !self.definition_command_resolves_at_call(definition_command.id(), call_span)
                    {
                        continue;
                    }
                    match self.enclosing_function_scope_at(call_span.start.offset()) {
                        Some(caller_scope) => {
                            for mut event_key in self.s001_function_call_event_keys_after_inner(
                                caller_scope,
                                after_offset,
                                seen_scopes,
                            ) {
                                event_key.push(call_span.start.offset());
                                event_keys.push(event_key);
                            }
                        }
                        None => {
                            if call_span.start.offset() > after_offset {
                                event_keys.push(vec![call_span.start.offset()]);
                            }
                        }
                    }
                }
            }

            Some(event_keys)
        })()
        .unwrap_or_default();

        seen_scopes.remove(&scope);
        result
    }

    fn s001_first_function_unset_event_after(
        &self,
        target_scope: ScopeId,
        after_offset: usize,
    ) -> Option<S001FunctionEventKey> {
        let target_function = self.named_function_kind(target_scope)?;
        let mut first_key: Option<S001FunctionEventKey> = None;

        for command in self.facts.command_facts().structural_commands() {
            if !command.options().unset().is_some_and(|unset| {
                target_function
                    .static_names()
                    .iter()
                    .any(|name| unset.targets_function_name(self.source, name.as_str()))
            }) {
                continue;
            }
            if !self.command_runs_in_persistent_shell_context(command.id())
                || self.command_is_in_background_context(command.id())
                || self.command_has_dominance_barrier_ancestor(command.id())
            {
                continue;
            }

            let command_span = command.span_in_source(self.source);
            let event_key = match self.enclosing_function_scope_at(command_span.start.offset()) {
                Some(unsetter_scope) => {
                    if unsetter_scope == target_scope {
                        None
                    } else {
                        self.s001_first_function_call_event_after(unsetter_scope, after_offset)
                            .map(|mut event_key| {
                                event_key.push(command_span.start.offset());
                                event_key
                            })
                    }
                }
                None => (command_span.start.offset() > after_offset)
                    .then(|| vec![command_span.start.offset()]),
            };

            if let Some(event_key) = event_key {
                first_key = Some(match first_key {
                    Some(current) => current.min(event_key),
                    None => event_key,
                });
            }
        }

        first_key
    }

    fn command_has_dominance_barrier_ancestor(&self, command_id: crate::facts::CommandId) -> bool {
        let mut current = self.facts.command_facts().command_parent_id(command_id);
        while let Some(id) = current {
            if self.facts.command_facts().command_is_dominance_barrier(id) {
                return true;
            }
            current = self.facts.command_facts().command_parent_id(id);
        }
        false
    }

    fn command_runs_in_unconditional_flow(
        &self,
        command_id: crate::facts::CommandId,
        reference_at: Span,
    ) -> bool {
        let command = self.facts.command_facts().command(command_id);
        if self.enclosing_function_scope_at(command.span().start.offset())
            != self.enclosing_function_scope_at(reference_at.start.offset())
        {
            return false;
        }
        if self.command_is_in_background_context(command_id) {
            return false;
        }

        let mut parent_id = self.facts.command_facts().command_parent_id(command_id);
        while let Some(id) = parent_id {
            if self.facts.command_facts().command_is_dominance_barrier(id) {
                return false;
            }
            parent_id = self.facts.command_facts().command_parent_id(id);
        }
        true
    }

    fn command_is_in_background_context(&self, command_id: crate::facts::CommandId) -> bool {
        let mut current = Some(command_id);
        while let Some(id) = current {
            if matches!(
                self.facts.command_facts().command(id).stmt().terminator,
                Some(StmtTerminator::Background(_))
            ) {
                return true;
            }
            current = self.facts.command_facts().command_parent_id(id);
        }
        false
    }

    fn enclosing_function_scope_at(&self, offset: usize) -> Option<ScopeId> {
        self.analysis.enclosing_function_scope_at(offset)
    }

    fn binding_is_quoted_static_literal(&self, binding_id: BindingId) -> bool {
        self.facts
            .assignments()
            .binding_value(binding_id)
            .and_then(|value| value.scalar_word())
            .is_some_and(|word| {
                word.is_fully_quoted() && static_word_text(word, self.source).is_some()
            })
    }

    fn binding_is_plain_empty_static_literal(&self, binding_id: BindingId) -> bool {
        if self.binding_is_name_only_declaration(binding_id) {
            return true;
        }

        matches!(
            self.semantic.binding(binding_id).origin,
            BindingOrigin::Assignment {
                value: AssignmentValueOrigin::StaticLiteral,
                ..
            }
        ) && self
            .facts
            .assignments()
            .binding_value(binding_id)
            .and_then(|value| value.scalar_word())
            .and_then(|word| static_word_text(word, self.source))
            .is_some_and(|text| text.is_empty())
    }

    fn binding_is_name_only_declaration(&self, binding_id: BindingId) -> bool {
        let binding = self.semantic.binding(binding_id);
        matches!(binding.origin, BindingOrigin::Declaration { .. })
            && binding.attributes.contains(BindingAttributes::LOCAL)
            && !binding
                .attributes
                .contains(BindingAttributes::DECLARATION_INITIALIZED)
    }

    fn bindings_are_all_plain_empty_static_literals(&self, bindings: &[BindingId]) -> bool {
        !bindings.is_empty()
            && bindings
                .iter()
                .copied()
                .all(|binding_id| self.binding_is_plain_empty_static_literal(binding_id))
    }

    fn optional_field_safe_bindings_can_stay_safe(
        &self,
        bindings: &[BindingId],
        query: SafeValueQuery,
    ) -> bool {
        if !query.is_field_context() {
            return false;
        }

        let mut saw_name_only_declaration = false;
        let mut saw_field_safe_value = false;
        for binding_id in bindings.iter().copied() {
            if self.binding_is_name_only_declaration(binding_id) {
                saw_name_only_declaration = true;
                continue;
            }

            let binding = self.semantic.binding(binding_id);
            if !matches!(
                binding.origin,
                BindingOrigin::Assignment {
                    value: AssignmentValueOrigin::StaticLiteral,
                    ..
                } | BindingOrigin::Declaration { .. }
            ) {
                return false;
            }
            let Some(text) = self
                .facts
                .assignments()
                .binding_value(binding_id)
                .and_then(|value| value.scalar_word())
                .and_then(|word| static_word_text(word, self.source))
            else {
                return false;
            };
            if text.is_empty() || !query.literal_is_safe(&text) {
                return false;
            }
            saw_field_safe_value = true;
        }

        saw_name_only_declaration && saw_field_safe_value
    }

    fn covering_direct_field_safe_bindings_can_stay_safe(
        &mut self,
        bindings: &[BindingId],
        query: SafeValueQuery,
    ) -> bool {
        if !query.is_field_context() || bindings.is_empty() {
            return false;
        }

        let mut visiting = FxHashSet::default();
        self.field_safe_binding_group_class(bindings, query, &mut visiting)
            == Some(FieldSafeBindingClass::NonEmpty)
    }

    fn covering_optional_field_safe_bindings_can_stay_safe(
        &mut self,
        bindings: &[BindingId],
        name: &Name,
        at: Span,
        query: SafeValueQuery,
    ) -> bool {
        if !query.is_field_context()
            || bindings.is_empty()
            || bindings.iter().copied().any(|binding_id| {
                self.semantic.binding(binding_id).span.end.offset() > at.start.offset()
            })
            || !self.bindings_cover_all_paths_to_reference(bindings, name, at)
        {
            return false;
        }

        let mut saw_empty = false;
        let mut saw_non_empty = false;
        let mut visiting = FxHashSet::default();
        for binding_id in bindings.iter().copied() {
            match self.field_safe_binding_class(binding_id, query, &mut visiting) {
                Some(FieldSafeBindingClass::Empty) => saw_empty = true,
                Some(FieldSafeBindingClass::NonEmpty) => saw_non_empty = true,
                None => return false,
            }
        }

        saw_empty && saw_non_empty
    }

    fn local_conditional_literal_bindings_can_stay_safe(
        &self,
        bindings: &[BindingId],
        name: &Name,
        at: Span,
        query: SafeValueQuery,
    ) -> bool {
        if !query.is_field_context() || bindings.is_empty() {
            return false;
        }
        let Some(function_scope) = self.enclosing_function_scope_at(at.start.offset()) else {
            return false;
        };

        let mut saw_conditional_literal = false;
        let mut first_binding_start = at.start.offset();
        for binding_id in bindings.iter().copied() {
            let binding = self.semantic.binding(binding_id);
            if binding.span.end.offset() > at.start.offset()
                || !self.binding_is_in_scope_or_descendant(binding_id, function_scope)
            {
                return false;
            }
            first_binding_start = first_binding_start.min(binding.span.start.offset());

            let Some(value) = self.facts.assignments().binding_value(binding_id) else {
                return false;
            };
            if !value.conditional_assignment_shortcut() {
                return false;
            }
            let Some(text) = value
                .scalar_word()
                .and_then(|word| static_word_text(word, self.source))
            else {
                return false;
            };
            if text.is_empty() || !query.literal_is_safe(&text) {
                return false;
            }
            saw_conditional_literal = true;
        }

        saw_conditional_literal
            && self
                .semantic
                .bindings_for(name)
                .iter()
                .copied()
                .any(|binding_id| {
                    let binding = self.semantic.binding(binding_id);
                    binding.span.end.offset() <= first_binding_start
                        && self.semantic.binding_visible_at(binding_id, at)
                        && self.binding_is_in_scope_or_descendant(binding_id, function_scope)
                        && self.binding_is_name_only_declaration(binding_id)
                        && binding.attributes.contains(BindingAttributes::LOCAL)
                })
    }

    fn field_safe_binding_group_class(
        &mut self,
        bindings: &[BindingId],
        query: SafeValueQuery,
        visiting: &mut FxHashSet<BindingId>,
    ) -> Option<FieldSafeBindingClass> {
        let mut saw_non_empty = false;
        for binding_id in bindings.iter().copied() {
            match self.field_safe_binding_class(binding_id, query, visiting)? {
                FieldSafeBindingClass::Empty => {}
                FieldSafeBindingClass::NonEmpty => saw_non_empty = true,
            }
        }

        Some(if saw_non_empty {
            FieldSafeBindingClass::NonEmpty
        } else {
            FieldSafeBindingClass::Empty
        })
    }

    fn field_safe_binding_class(
        &mut self,
        binding_id: BindingId,
        query: SafeValueQuery,
        visiting: &mut FxHashSet<BindingId>,
    ) -> Option<FieldSafeBindingClass> {
        if !visiting.insert(binding_id) {
            return None;
        }

        let result = self.field_safe_binding_class_uncached(binding_id, query, visiting);
        visiting.remove(&binding_id);
        result
    }

    fn field_safe_binding_class_uncached(
        &mut self,
        binding_id: BindingId,
        query: SafeValueQuery,
        visiting: &mut FxHashSet<BindingId>,
    ) -> Option<FieldSafeBindingClass> {
        if self.binding_is_name_only_declaration(binding_id) {
            return Some(FieldSafeBindingClass::Empty);
        }

        let binding = self.semantic.binding(binding_id);
        if !matches!(
            binding.kind,
            BindingKind::Assignment | BindingKind::Declaration(_)
        ) || self
            .facts
            .assignments()
            .binding_value(binding_id)
            .is_some_and(|value| {
                value.one_sided_short_circuit_assignment()
                    || value.conditional_assignment_shortcut()
            })
        {
            return None;
        }

        match binding.origin {
            BindingOrigin::Assignment {
                value: AssignmentValueOrigin::StaticLiteral,
                ..
            }
            | BindingOrigin::Declaration { .. } => {
                self.static_field_safe_binding_class(binding_id, query)
            }
            BindingOrigin::Assignment {
                value: AssignmentValueOrigin::PlainScalarAccess,
                ..
            } => self.plain_scalar_field_safe_binding_class(binding_id, query, visiting),
            BindingOrigin::Assignment { .. }
            | BindingOrigin::LoopVariable { .. }
            | BindingOrigin::ParameterDefaultAssignment { .. }
            | BindingOrigin::Imported { .. }
            | BindingOrigin::FunctionDefinition { .. }
            | BindingOrigin::BuiltinTarget { .. }
            | BindingOrigin::ArithmeticAssignment { .. }
            | BindingOrigin::Nameref { .. } => None,
        }
    }

    fn s001_field_safe_binding_group_exposure(
        &mut self,
        bindings: &[BindingId],
        at: Span,
        query: SafeValueQuery,
    ) -> Option<S001QuoteExposure> {
        if bindings.is_empty() || !query.is_field_context() {
            return None;
        }

        let mut saw_empty = false;
        let mut saw_non_empty = false;
        let mut saw_value = false;
        let mut visiting = FxHashSet::default();
        for binding_id in bindings.iter().copied() {
            match self.field_safe_binding_class(binding_id, query, &mut visiting) {
                Some(FieldSafeBindingClass::Empty) => {
                    saw_empty = true;
                    saw_value = true;
                }
                Some(FieldSafeBindingClass::NonEmpty) => {
                    saw_non_empty = true;
                    saw_value = true;
                }
                None if self
                    .s001_transient_guard_binding_is_overwritten_by_field_safe_bindings(
                        binding_id, bindings, at, query,
                    ) => {}
                None => return None,
            }
        }

        if !saw_value {
            return None;
        }
        Some(if saw_non_empty {
            S001QuoteExposure::QuoteInertNonEmpty
        } else if saw_empty {
            S001QuoteExposure::Empty
        } else {
            return None;
        })
    }

    fn s001_transient_guard_binding_is_overwritten_by_field_safe_bindings(
        &mut self,
        binding_id: BindingId,
        bindings: &[BindingId],
        _at: Span,
        query: SafeValueQuery,
    ) -> bool {
        let binding = self.semantic.binding(binding_id);
        if !matches!(
            binding.origin,
            BindingOrigin::Assignment { .. } | BindingOrigin::Declaration { .. }
        ) {
            return false;
        }
        if self
            .facts
            .assignments()
            .binding_value(binding_id)
            .and_then(|value| value.scalar_word())
            .and_then(|word| static_word_text(word, self.source))
            .is_some_and(|text| query.literal_is_safe(&text))
        {
            return false;
        }
        let Some(function_end_span) = self.function_scope_end_span(binding.scope) else {
            return false;
        };

        let mut later_field_safe_bindings = Vec::new();
        let mut visiting = FxHashSet::default();
        for candidate_id in bindings.iter().copied() {
            let candidate = self.semantic.binding(candidate_id);
            if candidate_id == binding_id
                || candidate.scope != binding.scope
                || candidate.name != binding.name
                || candidate.span.start.offset() <= binding.span.start.offset()
            {
                continue;
            }
            if self
                .field_safe_binding_class(candidate_id, query, &mut visiting)
                .is_some()
            {
                later_field_safe_bindings.push(candidate_id);
            } else {
                return false;
            }
        }
        if later_field_safe_bindings.is_empty() {
            return false;
        }

        self.bindings_cover_all_paths_to_reference(
            &later_field_safe_bindings,
            &binding.name,
            function_end_span,
        ) || (later_field_safe_bindings.len() >= 2
            && self.span_is_inside_if_condition(binding.span))
    }

    fn static_field_safe_binding_class(
        &self,
        binding_id: BindingId,
        query: SafeValueQuery,
    ) -> Option<FieldSafeBindingClass> {
        let text = self
            .facts
            .assignments()
            .binding_value(binding_id)
            .and_then(|value| value.scalar_word())
            .and_then(|word| static_word_text(word, self.source))?;
        if text.is_empty() {
            Some(FieldSafeBindingClass::Empty)
        } else {
            query
                .literal_is_safe(&text)
                .then_some(FieldSafeBindingClass::NonEmpty)
        }
    }

    fn plain_scalar_field_safe_binding_class(
        &mut self,
        binding_id: BindingId,
        query: SafeValueQuery,
        visiting: &mut FxHashSet<BindingId>,
    ) -> Option<FieldSafeBindingClass> {
        if let Some(class) = self.static_field_safe_binding_class(binding_id, query) {
            return Some(class);
        }

        let word = self
            .facts
            .assignments()
            .binding_value(binding_id)
            .and_then(|value| value.scalar_word())?;
        let target_name = self.word_plain_scalar_reference_name(word)?;
        let binding_span = self.semantic.binding(binding_id).span;
        self.binding_value_stack.push(binding_id);
        let prior_bindings = self.safe_bindings_for_name(&target_name, binding_span);
        self.binding_value_stack.pop();
        if prior_bindings.is_empty()
            || !self.bindings_cover_all_paths_to_reference(
                &prior_bindings,
                &target_name,
                binding_span,
            )
        {
            return None;
        }

        self.field_safe_binding_group_class(&prior_bindings, query, visiting)
    }

    fn binding_is_safe(
        &mut self,
        binding_id: BindingId,
        at: Span,
        query: SafeValueQuery,
        case_cli_scope: Option<ScopeId>,
    ) -> bool {
        let binding = self.semantic.binding(binding_id);
        let binding_key = FactSpan::new(binding.span);
        if self.binding_has_effective_integer_attribute(binding_id) {
            return true;
        }
        if self.binding_value_is_standalone_status_capture(binding_id)
            && self.status_capture_binding_conflicts_with_caller_local(binding_id, at)
        {
            return false;
        }
        if matches!(
            query,
            SafeValueQuery::Argv
                | SafeValueQuery::RedirectTarget
                | SafeValueQuery::NumericTestOperand
        ) && self.binding_is_standalone_status_capture(binding_id, at, case_cli_scope)
        {
            return true;
        }

        let key = (binding_key, FactSpan::new(at), query, case_cli_scope);
        if let Some(result) = self.memo.get(&key) {
            return *result;
        }
        if !self.visiting.insert(key) {
            return false;
        }

        let result = match &binding.origin {
            BindingOrigin::Assignment {
                value:
                    AssignmentValueOrigin::PlainScalarAccess | AssignmentValueOrigin::StaticLiteral,
                ..
            }
            | BindingOrigin::Declaration { .. } => {
                if matches!(binding.kind, BindingKind::AppendAssignment) {
                    self.append_assignment_preserves_safe_value(binding_id, query, case_cli_scope)
                } else if self.binding_is_name_only_declaration(binding_id) {
                    true
                } else {
                    let binding_value = self.facts.assignments().binding_value(binding_id);
                    let scalar_word = binding_value.and_then(|value| value.scalar_word());
                    let case_cli_status_capture_stays_unsafe = case_cli_scope
                        == Some(binding.scope)
                        && query.is_field_context()
                        && scalar_word.is_some_and(word_is_standalone_status_capture);
                    let conditional_assignment_shortcut_stays_unsafe =
                        binding_value.is_some_and(|value| {
                            value.conditional_assignment_shortcut()
                                && !self.conditional_assignment_shortcut_value_can_stay_safe(
                                    binding_id,
                                    scalar_word,
                                    query,
                                )
                        });
                    if case_cli_status_capture_stays_unsafe
                        || conditional_assignment_shortcut_stays_unsafe
                    {
                        false
                    } else {
                        scalar_word.is_some_and(|word| {
                            self.word_is_safe_for_binding_value(binding_id, word, query)
                        })
                    }
                }
            }
            BindingOrigin::LoopVariable {
                definition_span,
                items: LoopValueOrigin::StaticWords,
            } => {
                let words = self
                    .facts
                    .assignments()
                    .binding_value(binding_id)
                    .and_then(|value| value.loop_words())
                    .map(|words| words.to_vec());
                words.is_some_and(|words| {
                    !words.is_empty()
                        && (self.loop_variable_reference_stays_within_body(*definition_span, at)
                            || self.loop_variable_reference_stays_within_static_callers(
                                *definition_span,
                                at,
                            ))
                        && words.into_iter().all(|word| {
                            !self.word_contains_special_parameter_slice(word)
                                && self.word_is_safe(word, query)
                        })
                })
            }
            BindingOrigin::Assignment { .. }
            | BindingOrigin::LoopVariable { .. }
            | BindingOrigin::Imported { .. }
            | BindingOrigin::FunctionDefinition { .. }
            | BindingOrigin::BuiltinTarget { .. }
            | BindingOrigin::ArithmeticAssignment { .. }
            | BindingOrigin::Nameref { .. } => false,
            BindingOrigin::ParameterDefaultAssignment { .. } => self
                .parameter_default_assignment_preserves_safe_value(
                    binding_id,
                    query,
                    case_cli_scope,
                ),
        };

        self.visiting.remove(&key);
        self.memo.insert(key, result);
        result
    }

    fn binding_has_effective_integer_attribute(&self, binding_id: BindingId) -> bool {
        let binding = self.semantic.binding(binding_id);
        if binding.attributes.contains(BindingAttributes::INTEGER)
            || matches!(binding.kind, BindingKind::ArithmeticAssignment)
        {
            return true;
        }
        if !matches!(
            binding.kind,
            BindingKind::Assignment
                | BindingKind::AppendAssignment
                | BindingKind::Declaration(_)
                | BindingKind::ParameterDefaultAssignment
        ) {
            return false;
        }

        self.semantic
            .previous_visible_binding(&binding.name, binding.span, Some(binding.span))
            .is_some_and(|previous| {
                previous.attributes.contains(BindingAttributes::INTEGER)
                    && !self
                        .semantic
                        .binding_cleared_before(previous.id, binding.span)
            })
    }

    fn conditional_assignment_shortcut_value_can_stay_safe(
        &mut self,
        binding_id: BindingId,
        scalar_word: Option<&Word>,
        query: SafeValueQuery,
    ) -> bool {
        if query != SafeValueQuery::NumericTestOperand {
            return false;
        }
        let Some(word) = scalar_word else {
            return false;
        };
        if word_static_text_is_shell_integer(word, self.source) {
            return true;
        }
        self.word_has_arithmetic_expansion(word)
            && self.word_is_safe_for_binding_value(binding_id, word, query)
    }

    fn append_assignment_preserves_safe_value(
        &mut self,
        binding_id: BindingId,
        query: SafeValueQuery,
        _case_cli_scope: Option<ScopeId>,
    ) -> bool {
        let (name, binding_span) = {
            let binding = self.semantic.binding(binding_id);
            (binding.name.clone(), binding.span)
        };
        let Some(word) = self
            .facts
            .assignments()
            .binding_value(binding_id)
            .and_then(|value| value.scalar_word())
        else {
            return false;
        };
        if !self.word_is_safe_for_binding_value(binding_id, word, query) {
            return false;
        }

        self.binding_value_stack.push(binding_id);
        let prior_value_is_safe = self.name_is_safe(&name, binding_span, query)
            || self.append_prior_bindings_are_empty_safe(&name, binding_span, query);
        self.binding_value_stack.pop();
        prior_value_is_safe
    }

    fn append_prior_bindings_are_empty_safe(
        &self,
        name: &Name,
        binding_span: Span,
        query: SafeValueQuery,
    ) -> bool {
        if !query.is_field_context() {
            return false;
        }

        let mut prior_bindings = self.analysis.reaching_bindings_for_name(name, binding_span);
        self.retain_value_bindings(&mut prior_bindings);
        if let Some(current_binding) = self.current_binding_value_for_name(name) {
            prior_bindings.retain(|binding_id| *binding_id != current_binding);
        }
        if prior_bindings.is_empty()
            && let Some(previous) =
                self.semantic
                    .previous_visible_binding(name, binding_span, Some(binding_span))
            && self.binding_can_supply_parameter_value(previous.id)
        {
            prior_bindings.push(previous.id);
        }
        prior_bindings
            .sort_by_key(|binding_id| self.semantic.binding(*binding_id).span.start.offset());
        prior_bindings.dedup();

        self.bindings_are_all_plain_empty_static_literals(&prior_bindings)
            && self.bindings_cover_all_paths_to_reference(&prior_bindings, name, binding_span)
    }

    fn status_capture_bindings_cover_reference(
        &self,
        bindings: &[BindingId],
        name: &Name,
        at: Span,
        query: SafeValueQuery,
        case_cli_scope: Option<ScopeId>,
    ) -> bool {
        if !query.is_field_context() {
            return false;
        }

        let mut status_bindings = Vec::new();
        for binding_id in bindings.iter().copied() {
            let binding = self.semantic.binding(binding_id);
            match &binding.origin {
                BindingOrigin::Assignment {
                    value:
                        AssignmentValueOrigin::PlainScalarAccess
                        | AssignmentValueOrigin::StaticLiteral
                        | AssignmentValueOrigin::Unknown,
                    ..
                }
                | BindingOrigin::Declaration { .. }
                    if self.binding_is_standalone_status_capture(
                        binding_id,
                        at,
                        case_cli_scope,
                    ) =>
                {
                    status_bindings.push(binding_id);
                }
                BindingOrigin::Assignment { .. }
                | BindingOrigin::LoopVariable { .. }
                | BindingOrigin::ParameterDefaultAssignment { .. }
                | BindingOrigin::Imported { .. }
                | BindingOrigin::FunctionDefinition { .. }
                | BindingOrigin::BuiltinTarget { .. }
                | BindingOrigin::ArithmeticAssignment { .. }
                | BindingOrigin::Nameref { .. } => return false,
                BindingOrigin::Declaration { .. } => {}
            }
        }

        !status_bindings.is_empty()
            && self.bindings_cover_all_paths_to_reference(&status_bindings, name, at)
    }

    fn status_capture_subset_covers_reference(
        &self,
        bindings: &[BindingId],
        name: &Name,
        at: Span,
        query: SafeValueQuery,
        case_cli_scope: Option<ScopeId>,
    ) -> bool {
        if !query.is_field_context() {
            return false;
        }

        let status_bindings = bindings
            .iter()
            .copied()
            .filter(|binding_id| {
                self.semantic.binding(*binding_id).span.end.offset() <= at.start.offset()
            })
            .filter(|binding_id| {
                self.binding_is_standalone_status_capture(*binding_id, at, case_cli_scope)
            })
            .collect::<Vec<_>>();
        if status_bindings.is_empty()
            || !self.bindings_cover_all_paths_to_reference(&status_bindings, name, at)
        {
            return false;
        }

        let Some(reference_scope) = self.enclosing_function_scope_at(at.start.offset()) else {
            return true;
        };
        let first_status_offset = status_bindings
            .iter()
            .map(|binding_id| self.semantic.binding(*binding_id).span.start.offset())
            .min()
            .unwrap_or(at.start.offset());

        !bindings.iter().copied().any(|binding_id| {
            let binding = self.semantic.binding(binding_id);
            binding.scope == reference_scope
                && binding.span.start.offset() > first_status_offset
                && binding.span.start.offset() < at.start.offset()
                && !self.binding_is_standalone_status_capture(binding_id, at, case_cli_scope)
        })
    }

    fn binding_is_standalone_status_capture(
        &self,
        binding_id: BindingId,
        at: Span,
        case_cli_scope: Option<ScopeId>,
    ) -> bool {
        let binding = self.semantic.binding(binding_id);
        matches!(
            binding.origin,
            BindingOrigin::Assignment {
                value: AssignmentValueOrigin::PlainScalarAccess
                    | AssignmentValueOrigin::StaticLiteral
                    | AssignmentValueOrigin::Unknown,
                ..
            } | BindingOrigin::Declaration { .. }
        ) && case_cli_scope != Some(binding.scope)
            && self.binding_value_is_standalone_status_capture(binding_id)
            && !self.status_capture_binding_conflicts_with_caller_local(binding_id, at)
    }

    fn binding_is_local_declaration_status_capture(&self, binding_id: BindingId) -> bool {
        let binding = self.semantic.binding(binding_id);

        matches!(binding.kind, BindingKind::Declaration(_))
            && binding.attributes.contains(BindingAttributes::LOCAL)
            && binding
                .attributes
                .contains(BindingAttributes::DECLARATION_INITIALIZED)
            && self.binding_value_is_standalone_status_capture(binding_id)
    }

    fn local_declaration_status_capture_bindings_cover_reference(
        &self,
        bindings: &[BindingId],
        name: &Name,
        at: Span,
        query: SafeValueQuery,
    ) -> bool {
        query.is_field_context()
            && self.span_is_return_argument(at)
            && !bindings.is_empty()
            && bindings
                .iter()
                .copied()
                .all(|binding_id| self.binding_is_local_declaration_status_capture(binding_id))
            && self.bindings_cover_all_paths_to_reference(bindings, name, at)
    }

    fn binding_value_is_standalone_status_capture(&self, binding_id: BindingId) -> bool {
        self.facts
            .assignments()
            .binding_value(binding_id)
            .is_some_and(|value| value.standalone_status_or_pid_capture())
    }

    fn status_capture_binding_conflicts_with_caller_local(
        &self,
        binding_id: BindingId,
        at: Span,
    ) -> bool {
        let binding = self.semantic.binding(binding_id);
        if binding.attributes.contains(BindingAttributes::LOCAL) {
            return false;
        }
        if !self.status_capture_binding_is_in_short_circuit_branch(binding.span) {
            return false;
        }
        let Some(binding_scope) = self.enclosing_function_scope_at(binding.span.start.offset())
        else {
            return false;
        };
        if self.semantic.ancestor_scopes(binding_scope).any(|scope| {
            self.scope_has_visible_initialized_local_binding_before(
                &binding.name,
                scope,
                binding.span,
            )
        }) {
            return false;
        }

        let at_scope = self.semantic.scope_at(at.start.offset());
        let reference_is_in_binding_scope = self
            .semantic
            .scope_is_in_scope_or_descendant(at_scope, binding_scope);
        let relevant_call_sites = self
            .named_function_call_sites(binding_scope)
            .into_iter()
            .filter(|(_, span)| {
                reference_is_in_binding_scope
                    || self.call_site_dominates_use(*span, &binding.name, at)
            })
            .collect::<Vec<_>>();

        !relevant_call_sites.is_empty()
            && relevant_call_sites.into_iter().all(|(scope, span)| {
                let caller_scope = self
                    .enclosing_function_scope_at(span.start.offset())
                    .unwrap_or(scope);
                self.scope_has_visible_initialized_local_binding_before(
                    &binding.name,
                    caller_scope,
                    span,
                )
            })
    }

    fn status_capture_binding_is_in_short_circuit_branch(&self, span: Span) -> bool {
        self.facts.command_facts().lists().iter().any(|list| {
            list.segments()
                .iter()
                .skip(1)
                .any(|segment| span_contains(segment.span(), span))
        })
    }

    fn status_capture_declaration_probe_covers_reference(
        &self,
        name: &Name,
        at: Span,
        query: SafeValueQuery,
        case_cli_scope: Option<ScopeId>,
    ) -> bool {
        if !query.is_field_context() {
            return false;
        }

        self.facts
            .command_facts()
            .structural_commands()
            .any(|command| {
                command.span().end.offset() <= at.start.offset()
                    && self.command_blocks_cover_all_paths_to_reference(command, name, at)
                    && !case_cli_scope.is_some_and(|scope| {
                        self.offset_is_in_scope_or_descendant(command.span().start.offset(), scope)
                    })
                    && command
                        .declaration_assignment_probes()
                        .iter()
                        .any(|probe| probe.status_capture() && probe.target_name() == name.as_str())
            })
    }

    fn offset_is_in_scope_or_descendant(&self, offset: usize, ancestor_scope: ScopeId) -> bool {
        self.semantic
            .ancestor_scopes(self.semantic.scope_at(offset))
            .any(|scope| scope == ancestor_scope)
    }

    fn command_blocks_cover_all_paths_to_reference(
        &self,
        command: crate::facts::CommandFactRef<'_, 'a>,
        name: &Name,
        at: Span,
    ) -> bool {
        let key = (command.id(), name.clone(), FactSpan::new(at));
        if let Some(result) = self.command_cover_memo.borrow().get(&key) {
            return *result;
        }

        let result = self.command_blocks_cover_all_paths_to_reference_uncached(command, name, at);
        self.command_cover_memo.borrow_mut().insert(key, result);
        result
    }

    fn command_blocks_cover_all_paths_to_reference_uncached(
        &self,
        command: crate::facts::CommandFactRef<'_, 'a>,
        name: &Name,
        at: Span,
    ) -> bool {
        if self.enclosing_function_scope_at(command.span().start.offset())
            != self.enclosing_function_scope_at(at.start.offset())
        {
            return false;
        }
        if self.command_is_in_background_context(command.id()) {
            return false;
        }

        let Some(reference_id) = self.reference_id_for_name_at(name, at) else {
            return false;
        };
        let Some(reference_block) = self.block_for_reference(reference_id) else {
            return false;
        };

        let cover_blocks = self.analysis.block_ids_for_span(command.span());
        if cover_blocks.is_empty() {
            return false;
        }
        if cover_blocks.contains(&reference_block) {
            return true;
        }

        let entry = self
            .enclosing_function_scope_at(at.start.offset())
            .and_then(|scope| self.analysis.cfg().scope_entry(scope))
            .unwrap_or_else(|| self.analysis.cfg().entry());
        let cover_blocks = cover_blocks.iter().copied().collect::<FxHashSet<_>>();
        self.analysis
            .blocks_cover_all_paths_to_block(entry, reference_block, &cover_blocks)
    }

    fn unset_command_covers_reference(&self, name: &Name, at: Span) -> bool {
        self.facts
            .assignments()
            .unset_commands_for_name(name)
            .any(|command| {
                command.span().end.offset() <= at.start.offset()
                    && self.command_runs_in_persistent_shell_context(command.id())
                    && self.command_runs_in_unconditional_flow(command.id(), at)
                    && self.command_blocks_cover_all_paths_to_reference(command, name, at)
            })
    }

    fn binding_is_cleared_by_dominating_unset(
        &self,
        binding_id: BindingId,
        name: &Name,
        at: Span,
    ) -> bool {
        let binding = self.semantic.binding(binding_id);
        self.facts
            .assignments()
            .unset_commands_for_name(name)
            .any(|command| {
                command.span().start.offset() >= binding.span.end.offset()
                    && command.span().end.offset() <= at.start.offset()
                    && self.command_runs_in_persistent_shell_context(command.id())
                    && !self.command_is_in_background_context(command.id())
                    && self.command_blocks_cover_all_paths_to_reference(command, name, at)
            })
    }

    fn command_runs_in_persistent_shell_context(
        &self,
        command_id: crate::facts::CommandId,
    ) -> bool {
        let command = self.facts.command_facts().command(command_id);

        self.semantic
            .ancestor_scopes(command.scope())
            .next()
            .is_none_or(|scope| {
                matches!(
                    self.semantic.scope(scope).kind,
                    ScopeKind::Function(_) | ScopeKind::File
                )
            })
    }

    fn reference_is_safe(&mut self, reference: &VarRef, at: Span, query: SafeValueQuery) -> bool {
        if query != SafeValueQuery::Quoted && reference.has_array_selector() {
            return false;
        }
        self.name_is_safe(&reference.name, at, query)
    }

    fn word_is_safe_for_binding_value(
        &mut self,
        binding_id: BindingId,
        word: &Word,
        query: SafeValueQuery,
    ) -> bool {
        self.binding_value_stack.push(binding_id);
        let result = self.word_is_safe(word, query);
        self.binding_value_stack.pop();
        result
    }

    fn parameter_default_assignment_preserves_safe_value(
        &mut self,
        binding_id: BindingId,
        query: SafeValueQuery,
        case_cli_scope: Option<ScopeId>,
    ) -> bool {
        let (name, binding_span) = {
            let binding = self.semantic.binding(binding_id);
            (binding.name.clone(), binding.span)
        };
        let mut prior_bindings = self
            .analysis
            .reaching_bindings_for_name(&name, binding_span);
        self.retain_value_bindings(&mut prior_bindings);
        prior_bindings.retain(|prior_id| *prior_id != binding_id);
        if prior_bindings.is_empty()
            && let Some(previous) =
                self.semantic
                    .previous_visible_binding(&name, binding_span, Some(binding_span))
            && self.binding_can_supply_parameter_value(previous.id)
        {
            prior_bindings.push(previous.id);
        }
        prior_bindings.sort_by_key(|prior_id| self.semantic.binding(*prior_id).span.start.offset());
        prior_bindings.dedup();

        if prior_bindings.is_empty() {
            return safe_numeric_shell_variable(&name)
                && !self.unset_command_covers_reference(&name, binding_span);
        }
        if query.is_field_context() {
            if self.bindings_are_all_plain_empty_static_literals(&prior_bindings) {
                return false;
            }
            if !self.bindings_cover_all_paths_to_reference(&prior_bindings, &name, binding_span) {
                return false;
            }
        }

        prior_bindings
            .into_iter()
            .all(|prior_id| self.binding_is_safe(prior_id, binding_span, query, case_cli_scope))
    }

    fn loop_variable_reference_stays_within_body(&self, definition_span: Span, at: Span) -> bool {
        self.facts
            .command_facts()
            .for_headers()
            .iter()
            .any(|header| {
                header
                    .command()
                    .targets
                    .iter()
                    .any(|target| target.span == definition_span)
                    && span_contains(header.command().body.span, at)
            })
            || self
                .facts
                .command_facts()
                .select_headers()
                .iter()
                .any(|header| {
                    header.command().variable_span == definition_span
                        && span_contains(header.command().body.span, at)
                })
    }

    fn loop_variable_reference_stays_within_static_callers(
        &self,
        definition_span: Span,
        at: Span,
    ) -> bool {
        let Some(helper_scope) = self.enclosing_function_scope_at(at.start.offset()) else {
            return false;
        };
        self.loop_variable_scope_callers_stay_within_body(
            definition_span,
            helper_scope,
            &mut FxHashSet::default(),
        )
    }

    fn loop_variable_scope_callers_stay_within_body(
        &self,
        definition_span: Span,
        helper_scope: ScopeId,
        seen_scopes: &mut FxHashSet<ScopeId>,
    ) -> bool {
        if !seen_scopes.insert(helper_scope) {
            return false;
        }

        let caller_sites = self.named_function_call_sites(helper_scope);
        !caller_sites.is_empty()
            && caller_sites.into_iter().all(|(_, call_span)| {
                if self.loop_variable_reference_stays_within_body(definition_span, call_span) {
                    return true;
                }

                let caller_scope = self.semantic.scope_at(call_span.start.offset());
                let mut caller_seen = seen_scopes.clone();
                self.loop_variable_scope_callers_stay_within_body(
                    definition_span,
                    caller_scope,
                    &mut caller_seen,
                )
            })
    }

    fn indirect_name_is_safe(
        &mut self,
        reference: &VarRef,
        at: Span,
        query: SafeValueQuery,
    ) -> bool {
        if query != SafeValueQuery::Quoted && reference.has_array_selector() {
            return false;
        }
        self.reference_is_safe(reference, at, query)
    }

    fn safe_bindings_for_name(&mut self, name: &Name, at: Span) -> Vec<BindingId> {
        let mut bindings = self.visible_bindings_for_name_without_helpers(name, at);
        let mut helper_bindings = self.called_helper_bindings_for_name(name, at);
        self.retain_value_bindings(&mut helper_bindings);
        helper_bindings.extend(self.top_level_transitive_helper_bindings_before(name, at));
        self.retain_value_bindings(&mut helper_bindings);
        helper_bindings
            .sort_by_key(|binding_id| self.semantic.binding(*binding_id).span.start.offset());
        helper_bindings.dedup();
        let mut caller_bindings = self.caller_bindings_covering_all_static_call_sites(name, at);
        self.retain_value_bindings(&mut caller_bindings);
        let mut uncalled_function_bindings = self.uncalled_function_outer_bindings_at_end(name, at);
        self.retain_value_bindings(&mut uncalled_function_bindings);
        let function_scope = self.enclosing_function_scope_at(at.start.offset());
        let function_local_binding = function_scope.is_some_and(|scope| {
            bindings
                .iter()
                .copied()
                .any(|binding_id| self.binding_is_in_scope_or_descendant(binding_id, scope))
        });
        let caller_bindings_refine_outer_bindings =
            function_scope.is_some() && !function_local_binding && !caller_bindings.is_empty();
        if !uncalled_function_bindings.is_empty() && !function_local_binding {
            bindings = uncalled_function_bindings;
        }
        if caller_bindings_refine_outer_bindings
            && function_scope.is_some()
            && !self.bindings_are_static_loop_variables(&caller_bindings)
        {
            bindings = caller_bindings;
            bindings.extend(helper_bindings);
            bindings
                .sort_by_key(|binding_id| self.semantic.binding(*binding_id).span.start.offset());
            bindings.dedup();
        } else if !caller_bindings.is_empty()
            && function_scope.is_some()
            && !function_local_binding
            && self.bindings_are_static_loop_variables(&caller_bindings)
        {
            bindings = caller_bindings;
            bindings.extend(helper_bindings);
            bindings
                .sort_by_key(|binding_id| self.semantic.binding(*binding_id).span.start.offset());
            bindings.dedup();
        } else if bindings.is_empty() {
            bindings = caller_bindings;
            bindings.extend(helper_bindings);
            bindings
                .sort_by_key(|binding_id| self.semantic.binding(*binding_id).span.start.offset());
            bindings.dedup();
        } else if !helper_bindings.is_empty() {
            bindings.extend(helper_bindings);
            bindings
                .sort_by_key(|binding_id| self.semantic.binding(*binding_id).span.start.offset());
            bindings.dedup();
        }
        if let Some(scope) = function_scope
            && bindings.iter().copied().any(|binding_id| {
                self.binding_is_in_scope_or_descendant(binding_id, scope)
                    && self.binding_shadows_outer_scope_values(binding_id)
            })
        {
            bindings
                .retain(|binding_id| self.binding_is_in_scope_or_descendant(*binding_id, scope));
        }
        if let Some(scope) = function_scope {
            bindings.retain(|binding_id| {
                !self.binding_writes_visible_local_in_scope_before(*binding_id, scope, at)
            });
        }
        let reference_scope = self.semantic.scope_at(at.start.offset());
        bindings.retain(|binding_id| {
            self.semantic.binding(*binding_id).span.start.offset() <= at.start.offset()
                || !self.binding_is_in_scope_or_descendant(*binding_id, reference_scope)
                || self.future_binding_can_reach_reference(*binding_id, name, at)
        });

        self.retain_value_bindings(&mut bindings);
        bindings
    }

    fn bindings_are_static_loop_variables(&self, bindings: &[BindingId]) -> bool {
        !bindings.is_empty()
            && bindings.iter().copied().all(|binding_id| {
                matches!(
                    self.semantic.binding(binding_id).origin,
                    BindingOrigin::LoopVariable {
                        items: LoopValueOrigin::StaticWords,
                        ..
                    }
                )
            })
    }

    fn binding_shadows_outer_scope_values(&self, binding_id: BindingId) -> bool {
        let binding = self.semantic.binding(binding_id);
        binding.attributes.contains(BindingAttributes::LOCAL)
            || matches!(binding.origin, BindingOrigin::LoopVariable { .. })
    }

    fn uncalled_function_outer_bindings_at_end(&mut self, name: &Name, at: Span) -> Vec<BindingId> {
        let Some(helper_scope) = self.enclosing_function_scope_at(at.start.offset()) else {
            return Vec::new();
        };
        if self
            .facts
            .command_facts()
            .is_case_cli_reachable_function_scope(helper_scope)
            || !self.named_function_call_sites(helper_scope).is_empty()
        {
            return Vec::new();
        }
        let Some(file_scope) = self
            .semantic
            .ancestor_scopes(helper_scope)
            .find(|scope| matches!(self.semantic.scope(*scope).kind, ScopeKind::File))
        else {
            return Vec::new();
        };

        let eof = Position::new().advanced_by(self.source);
        let eof_span = Span::from_positions(eof, eof);
        let mut bindings = self.caller_branch_bindings_before(name, file_scope, eof_span);
        bindings.retain(|binding_id| {
            let binding = self.semantic.binding(*binding_id);
            binding.scope != helper_scope
                && !self.binding_is_in_scope_or_descendant(*binding_id, helper_scope)
        });
        let latest_unguarded = bindings
            .iter()
            .copied()
            .filter(|binding_id| !self.binding_is_guarded_before_reference(*binding_id, eof_span))
            .max_by_key(|binding_id| self.semantic.binding(*binding_id).span.start.offset());
        let Some(latest_unguarded) = latest_unguarded else {
            return Vec::new();
        };
        let latest_unguarded_offset = self.semantic.binding(latest_unguarded).span.start.offset();
        bindings.retain(|binding_id| {
            *binding_id == latest_unguarded
                || (self.semantic.binding(*binding_id).span.start.offset()
                    > latest_unguarded_offset
                    && self.binding_is_guarded_before_reference(*binding_id, eof_span))
        });
        bindings.sort_by_key(|binding_id| self.semantic.binding(*binding_id).span.start.offset());
        bindings.dedup();
        bindings
    }

    fn caller_bindings_covering_all_static_call_sites(
        &mut self,
        name: &Name,
        at: Span,
    ) -> Vec<BindingId> {
        let Some(helper_scope) = self.enclosing_function_scope_at(at.start.offset()) else {
            return Vec::new();
        };
        self.caller_bindings_covering_static_scope_call_sites(
            name,
            helper_scope,
            &mut FxHashSet::default(),
        )
        .unwrap_or_default()
    }

    fn caller_bindings_covering_static_scope_call_sites(
        &mut self,
        name: &Name,
        helper_scope: ScopeId,
        seen_scopes: &mut FxHashSet<ScopeId>,
    ) -> Option<Vec<BindingId>> {
        if !seen_scopes.insert(helper_scope) {
            return None;
        }

        let caller_sites = self.named_function_call_sites(helper_scope);
        if caller_sites.is_empty() {
            return None;
        }

        let mut bindings = Vec::new();
        for (scope, span) in caller_sites {
            let caller_scope = self
                .enclosing_function_scope_at(span.start.offset())
                .unwrap_or(scope);
            let mut branch = self.caller_branch_bindings_before(name, caller_scope, span);
            self.drop_declarations_shadowed_by_covering_loop_bindings(&mut branch, span);
            if branch
                .iter()
                .copied()
                .any(|binding_id| self.binding_is_in_scope_or_descendant(binding_id, caller_scope))
            {
                branch.retain(|binding_id| {
                    self.binding_is_in_scope_or_descendant(*binding_id, caller_scope)
                });
            }
            let call_span = self
                .command_for_name_word_span(span)
                .map_or(span, |command| command.span());
            let loop_branch = self.loop_bindings_covering_callsite(&branch, call_span);
            if !loop_branch.is_empty() {
                bindings.extend(loop_branch);
                continue;
            }
            if !branch.is_empty() && self.bindings_cover_all_paths_to_callsite(&branch, call_span) {
                bindings.extend(branch);
                continue;
            }

            let mut caller_seen = seen_scopes.clone();
            let transitive = self.caller_bindings_covering_static_scope_call_sites(
                name,
                caller_scope,
                &mut caller_seen,
            )?;
            if transitive.is_empty() {
                return None;
            }
            bindings.extend(transitive);
        }

        bindings.sort_by_key(|binding_id| self.semantic.binding(*binding_id).span.start.offset());
        bindings.dedup();
        Some(bindings)
    }

    fn loop_bindings_covering_callsite(
        &self,
        bindings: &[BindingId],
        call_span: Span,
    ) -> Vec<BindingId> {
        bindings
            .iter()
            .copied()
            .filter(|binding_id| {
                let binding = self.semantic.binding(*binding_id);
                let BindingOrigin::LoopVariable {
                    definition_span,
                    items: LoopValueOrigin::StaticWords,
                } = &binding.origin
                else {
                    return false;
                };
                self.loop_variable_reference_stays_within_body(*definition_span, call_span)
            })
            .collect()
    }

    fn visible_bindings_for_name_without_helpers(&self, name: &Name, at: Span) -> Vec<BindingId> {
        let synthetic_use_block = self
            .reference_id_for_name_at(name, at)
            .is_none()
            .then(|| self.block_for_name_reference_or_virtual_offset(name, at))
            .flatten();
        let mut bindings = self
            .value_flow
            .borrow()
            .reaching_value_bindings_for_name_with_synthetic_use_block(
                name,
                at,
                synthetic_use_block,
            );
        if let Some(current_binding) = self.current_binding_value_for_name(name) {
            if bindings.contains(&current_binding) {
                bindings = self.value_flow.borrow().reaching_value_bindings_bypassing(
                    name,
                    current_binding,
                    at,
                );
                if bindings.is_empty()
                    && let Some(previous) = self.semantic.previous_visible_binding(
                        name,
                        self.semantic.binding(current_binding).span,
                        Some(self.semantic.binding(current_binding).span),
                    )
                {
                    bindings.push(previous.id);
                }
            }
            bindings.retain(|binding_id| *binding_id != current_binding);
            bindings
                .sort_by_key(|binding_id| self.semantic.binding(*binding_id).span.start.offset());
            bindings.dedup();
            return bindings;
        }
        if bindings.len() == 1 {
            let mut expanded =
                self.value_flow
                    .borrow()
                    .reaching_value_bindings_bypassing(name, bindings[0], at);
            if !expanded.is_empty() {
                expanded.push(bindings[0]);
                expanded.sort_by_key(|binding_id| {
                    self.semantic.binding(*binding_id).span.start.offset()
                });
                expanded.dedup();
                bindings = expanded;
            }
        }

        bindings
    }

    fn retain_value_bindings(&self, bindings: &mut Vec<BindingId>) {
        bindings.retain(|binding_id| self.binding_can_supply_parameter_value(*binding_id));
    }

    fn binding_can_supply_parameter_value(&self, binding_id: BindingId) -> bool {
        self.value_flow
            .borrow()
            .binding_can_supply_parameter_value(binding_id)
    }

    fn future_binding_can_reach_reference(
        &self,
        binding_id: BindingId,
        name: &Name,
        at: Span,
    ) -> bool {
        let Some(binding_block) = self.block_for_binding(binding_id) else {
            return false;
        };
        let Some(reference_block) = self.block_for_name_reference_or_virtual_offset(name, at)
        else {
            return false;
        };
        if binding_block == reference_block {
            return true;
        }

        let cfg = self.analysis.cfg();
        let mut stack = vec![binding_block];
        let mut seen = FxHashSet::default();
        while let Some(block_id) = stack.pop() {
            if self.analysis.block_is_unreachable(block_id) || !seen.insert(block_id) {
                continue;
            }
            for (successor, _) in cfg.successors(block_id) {
                if *successor == reference_block {
                    return true;
                }
                stack.push(*successor);
            }
        }

        false
    }

    fn current_binding_value_for_name(&self, name: &Name) -> Option<BindingId> {
        self.binding_value_stack
            .iter()
            .rev()
            .copied()
            .find(|binding_id| &self.semantic.binding(*binding_id).name == name)
    }

    fn drop_declarations_shadowed_by_covering_loop_bindings(
        &self,
        bindings: &mut Vec<BindingId>,
        at: Span,
    ) {
        let covering_loop_bindings = bindings
            .iter()
            .copied()
            .filter_map(|binding_id| {
                let binding = self.semantic.binding(binding_id);
                let BindingOrigin::LoopVariable {
                    definition_span,
                    items: LoopValueOrigin::StaticWords,
                } = &binding.origin
                else {
                    return None;
                };
                (self.loop_variable_reference_stays_within_body(*definition_span, at)
                    || self
                        .loop_variable_reference_stays_within_static_callers(*definition_span, at))
                .then_some((binding.scope, definition_span.start.offset()))
            })
            .collect::<FxHashSet<_>>();
        if covering_loop_bindings.is_empty() {
            return;
        }

        bindings.retain(|binding_id| {
            let binding = self.semantic.binding(*binding_id);
            if !matches!(binding.origin, BindingOrigin::Declaration { .. }) {
                return true;
            }

            !covering_loop_bindings.iter().any(|(scope, loop_start)| {
                binding.scope == *scope && binding.span.start.offset() <= *loop_start
            })
        });
    }

    fn drop_outer_bindings_shadowed_by_covering_loop_bindings(
        &self,
        bindings: &mut Vec<BindingId>,
        at: Span,
    ) {
        let covering_loop_scopes = bindings
            .iter()
            .copied()
            .filter_map(|binding_id| {
                let binding = self.semantic.binding(binding_id);
                let BindingOrigin::LoopVariable {
                    definition_span,
                    items: LoopValueOrigin::StaticWords,
                } = &binding.origin
                else {
                    return None;
                };
                (self.loop_variable_reference_stays_within_body(*definition_span, at)
                    || self
                        .loop_variable_reference_stays_within_static_callers(*definition_span, at))
                .then_some((binding_id, binding.scope))
            })
            .collect::<Vec<_>>();
        if covering_loop_scopes.is_empty() {
            return;
        }

        bindings.retain(|binding_id| {
            if covering_loop_scopes
                .iter()
                .any(|(covering_id, _)| covering_id == binding_id)
            {
                return true;
            }

            let binding = self.semantic.binding(*binding_id);
            if matches!(
                binding.origin,
                BindingOrigin::LoopVariable {
                    items: LoopValueOrigin::StaticWords,
                    ..
                }
            ) {
                return false;
            }
            !covering_loop_scopes.iter().any(|(_, loop_scope)| {
                self.semantic
                    .scope_is_descendant_of(*loop_scope, binding.scope)
            })
        });
    }

    fn called_helper_bindings_for_name(&mut self, name: &Name, at: Span) -> Vec<BindingId> {
        self.value_flow
            .borrow_mut()
            .helper_value_bindings_before(name, at)
    }

    fn s001_top_level_dispatch_helper_bindings_before(
        &mut self,
        name: &Name,
        at: Span,
    ) -> Vec<BindingId> {
        if self
            .enclosing_function_scope_at(at.start.offset())
            .is_some()
        {
            return Vec::new();
        }

        let mut bindings = Vec::new();
        let scope = self.semantic.scope_at(at.start.offset());
        let dispatcher_scopes = self
            .value_flow
            .borrow()
            .called_function_scopes_before(scope, at.start.offset());
        for dispatcher_scope in dispatcher_scopes {
            bindings.extend(self.s001_branch_helper_bindings_in_scope(name, dispatcher_scope));
        }
        bindings.sort_by_key(|binding_id| self.semantic.binding(*binding_id).span.start.offset());
        bindings.dedup();
        bindings
    }

    fn s001_branch_helper_bindings_in_scope(
        &mut self,
        name: &Name,
        scope: ScopeId,
    ) -> Vec<BindingId> {
        let helper_scopes = self.helper_scopes_providing_name(name);
        if helper_scopes.is_empty() {
            return Vec::new();
        }

        let helper_scope_set = helper_scopes.iter().copied().collect::<FxHashSet<_>>();
        let mut call_sites = Vec::new();
        for callee_scope in helper_scopes {
            let Some(function_kind) = self.named_function_kind(callee_scope) else {
                continue;
            };
            for function_name in function_kind.static_names() {
                for site in self.semantic.call_sites_for(function_name) {
                    if site.scope == scope {
                        call_sites.push((callee_scope, site.span));
                    }
                }
            }
        }
        call_sites.sort_by_key(|(_, span)| (span.start.offset(), span.end.offset()));
        call_sites.dedup_by_key(|(callee_scope, span)| {
            (*callee_scope, span.start.offset(), span.end.offset())
        });
        if call_sites.len() < 2 {
            return Vec::new();
        }

        let mut bindings = Vec::new();
        for (callee_scope, _) in call_sites {
            bindings.extend(self.semantic.bindings_for(name).iter().copied().filter(
                |binding_id| {
                    let binding = self.semantic.binding(*binding_id);
                    binding.scope == callee_scope
                        && helper_scope_set.contains(&binding.scope)
                        && !binding.attributes.contains(BindingAttributes::LOCAL)
                },
            ));
        }
        bindings
    }

    fn top_level_transitive_helper_bindings_before(&self, name: &Name, at: Span) -> Vec<BindingId> {
        if self
            .enclosing_function_scope_at(at.start.offset())
            .is_some()
        {
            return Vec::new();
        }

        self.transitive_helper_bindings_before(name, at, !self.span_is_exit_or_return_argument(at))
    }

    fn called_helper_bindings_before(
        &mut self,
        name: &Name,
        scope: ScopeId,
        at: Span,
    ) -> Vec<BindingId> {
        self.value_flow
            .borrow_mut()
            .nonlocal_value_bindings_from_called_functions_before(name, scope, at)
    }

    fn helper_outer_bindings_cover_all_caller_paths(
        &mut self,
        name: &Name,
        at: Span,
        bindings: &[BindingId],
    ) -> bool {
        let Some(helper_scope) = self.enclosing_function_scope_at(at.start.offset()) else {
            return true;
        };
        if !bindings
            .iter()
            .copied()
            .any(|binding_id| !self.binding_is_in_scope_or_descendant(binding_id, helper_scope))
        {
            return true;
        }

        let caller_sites = self.named_function_call_sites(helper_scope);
        if caller_sites.is_empty() {
            return true;
        }

        caller_sites.into_iter().all(|(scope, span)| {
            let caller_scope = self
                .enclosing_function_scope_at(span.start.offset())
                .unwrap_or(scope);
            let mut branch = self.caller_branch_bindings_before(name, caller_scope, span);
            self.drop_declarations_shadowed_by_covering_loop_bindings(&mut branch, span);
            if branch
                .iter()
                .copied()
                .any(|binding_id| self.binding_is_in_scope_or_descendant(binding_id, caller_scope))
            {
                branch.retain(|binding_id| {
                    self.binding_is_in_scope_or_descendant(*binding_id, caller_scope)
                });
            }
            if branch.is_empty() {
                return false;
            }

            let helper_branch = self
                .called_helper_bindings_before(name, scope, span)
                .into_iter()
                .collect::<FxHashSet<_>>();
            let direct_branch = branch
                .into_iter()
                .filter(|binding_id| !helper_branch.contains(binding_id))
                .collect::<Vec<_>>();
            let call_span = self
                .command_for_name_word_span(span)
                .map_or(span, |command| command.span());

            direct_branch.is_empty()
                || !self
                    .loop_bindings_covering_callsite(&direct_branch, call_span)
                    .is_empty()
                || self.bindings_cover_all_paths_to_callsite(&direct_branch, call_span)
        })
    }

    fn named_function_call_sites(&self, scope: ScopeId) -> Vec<(ScopeId, Span)> {
        self.value_flow
            .borrow()
            .named_function_call_sites(scope)
            .into_iter()
            .map(|site| (site.scope, site.span))
            .collect()
    }

    fn caller_branch_bindings_before(
        &mut self,
        name: &Name,
        scope: ScopeId,
        at: Span,
    ) -> Vec<BindingId> {
        let mut branch = self.visible_bindings_for_name_without_helpers(name, at);
        branch.extend(
            self.value_flow
                .borrow()
                .ancestor_value_bindings_before(name, scope, at),
        );
        branch.extend(self.called_helper_bindings_before(name, scope, at));
        if matches!(self.semantic.scope(scope).kind, ScopeKind::File)
            && self.callsite_is_within_guarded_branch(at)
        {
            branch.extend(self.top_level_transitive_helper_bindings_before(name, at));
            self.drop_outer_bindings_shadowed_by_covering_top_level_helper_bindings(
                &mut branch,
                scope,
                at,
            );
        }
        if self.scope_has_visible_local_binding_before(name, scope, at) {
            branch.retain(|binding_id| self.semantic.binding(*binding_id).scope == scope);
        }
        branch.sort_by_key(|binding_id| self.semantic.binding(*binding_id).span.start.offset());
        branch.dedup();
        branch
    }

    fn callsite_is_within_guarded_branch(&self, at: Span) -> bool {
        let call_span = self
            .command_for_name_word_span(at)
            .map_or(at, |command| command.span());
        let Some(mut command_id) = self
            .facts
            .command_facts()
            .innermost_command_id_at(call_span.start.offset())
        else {
            return false;
        };
        while self.facts.command_facts().command(command_id).span() != call_span {
            let Some(parent_id) = self.facts.command_facts().command_parent_id(command_id) else {
                return false;
            };
            command_id = parent_id;
        }

        let mut current = self.facts.command_facts().command_parent_id(command_id);
        while let Some(command_id) = current {
            let command = self.facts.command_facts().command(command_id);
            if command.span().start.offset() < call_span.start.offset()
                && call_span.end.offset() <= command.span().end.offset()
                && self
                    .facts
                    .command_facts()
                    .command_is_dominance_barrier(command_id)
            {
                return true;
            }
            current = self.facts.command_facts().command_parent_id(command_id);
        }

        false
    }

    fn drop_outer_bindings_shadowed_by_covering_top_level_helper_bindings(
        &self,
        bindings: &mut Vec<BindingId>,
        scope: ScopeId,
        at: Span,
    ) {
        let helper_bindings = bindings
            .iter()
            .copied()
            .filter(|binding_id| {
                let binding_scope = self.semantic.binding(*binding_id).scope;
                self.semantic.scope_is_descendant_of(binding_scope, scope)
            })
            .collect::<Vec<_>>();
        if helper_bindings.is_empty() {
            return;
        }

        let call_span = self
            .command_for_name_word_span(at)
            .map_or(at, |command| command.span());
        if !self.bindings_cover_all_paths_to_callsite(&helper_bindings, call_span) {
            return;
        }

        bindings.retain(|binding_id| {
            let binding_scope = self.semantic.binding(*binding_id).scope;
            binding_scope != scope || helper_bindings.contains(binding_id)
        });
    }

    fn scope_has_visible_local_binding_before(
        &self,
        name: &Name,
        scope: ScopeId,
        at: Span,
    ) -> bool {
        self.semantic
            .bindings_for(name)
            .iter()
            .copied()
            .any(|binding_id| {
                let binding = self.semantic.binding(binding_id);
                binding.scope == scope
                    && binding.span.end.offset() <= at.start.offset()
                    && binding.attributes.contains(BindingAttributes::LOCAL)
            })
    }

    fn scope_has_visible_initialized_local_binding_before(
        &self,
        name: &Name,
        scope: ScopeId,
        at: Span,
    ) -> bool {
        let latest_local_start = self
            .semantic
            .bindings_for(name)
            .iter()
            .copied()
            .filter(|binding_id| {
                let binding = self.semantic.binding(*binding_id);
                binding.scope == scope
                    && binding.span.end.offset() <= at.start.offset()
                    && binding.attributes.contains(BindingAttributes::LOCAL)
            })
            .map(|binding_id| self.semantic.binding(binding_id).span.start.offset())
            .max();
        let Some(latest_local_start) = latest_local_start else {
            return false;
        };

        self.semantic
            .bindings_for(name)
            .iter()
            .copied()
            .any(|binding_id| {
                let binding = self.semantic.binding(binding_id);
                let initializes_local_value = match binding.origin {
                    BindingOrigin::Declaration { .. } => binding.attributes.intersects(
                        BindingAttributes::DECLARATION_INITIALIZED | BindingAttributes::INTEGER,
                    ),
                    BindingOrigin::Assignment { .. }
                    | BindingOrigin::LoopVariable { .. }
                    | BindingOrigin::ParameterDefaultAssignment { .. }
                    | BindingOrigin::Imported { .. }
                    | BindingOrigin::FunctionDefinition { .. }
                    | BindingOrigin::BuiltinTarget { .. }
                    | BindingOrigin::ArithmeticAssignment { .. }
                    | BindingOrigin::Nameref { .. } => {
                        self.binding_can_supply_parameter_value(binding_id)
                    }
                };
                binding.scope == scope
                    && binding.span.end.offset() <= at.start.offset()
                    && binding.span.start.offset() >= latest_local_start
                    && initializes_local_value
            })
    }

    fn binding_writes_visible_local_in_scope_before(
        &self,
        binding_id: BindingId,
        scope: ScopeId,
        at: Span,
    ) -> bool {
        let binding = self.semantic.binding(binding_id);
        if binding.scope == scope
            || binding.attributes.contains(BindingAttributes::LOCAL)
            || !self.scope_has_visible_local_binding_before(&binding.name, scope, at)
            || self.scope_has_visible_local_binding_before(
                &binding.name,
                binding.scope,
                binding.span,
            )
        {
            return false;
        }
        let Some(definition_command) = self.function_definition_command_for_scope(binding.scope)
        else {
            return false;
        };

        self.value_flow
            .borrow()
            .named_function_call_sites(binding.scope)
            .into_iter()
            .any(|site| {
                site.scope == scope
                    && self.function_scope_resolves_at_call_site(binding.scope, &site)
                    && self.scope_has_visible_local_binding_before(&binding.name, scope, site.span)
                    && self.definition_command_resolves_at_call(definition_command.id(), site.span)
                    && self.call_site_dominates_use(site.span, &binding.name, at)
            })
    }

    fn call_site_dominates_use(&self, call_span: Span, name: &Name, at: Span) -> bool {
        let _ = name;
        self.call_site_dominates_offset(call_span, at.start.offset())
    }

    fn function_scope_end_span(&self, scope: ScopeId) -> Option<Span> {
        self.facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| header.function_scope() == Some(scope))
            .map(|header| Span::at(header.function().span.end))
    }

    fn transitive_helper_bindings_before(
        &self,
        name: &Name,
        at: Span,
        relaxed: bool,
    ) -> Vec<BindingId> {
        let scope = self.semantic.scope_at(at.start.offset());
        let callee_scopes = if relaxed {
            self.value_flow
                .borrow()
                .transitively_called_function_scopes_before_relaxed(scope, at.start.offset())
        } else {
            self.value_flow
                .borrow()
                .transitively_called_function_scopes_before(scope, at.start.offset())
        };
        let mut bindings = callee_scopes
            .into_iter()
            .flat_map(|callee_scope| {
                self.semantic
                    .bindings_for(name)
                    .iter()
                    .copied()
                    .filter(move |binding_id| {
                        let binding = self.semantic.binding(*binding_id);
                        binding.scope == callee_scope
                            && !binding.attributes.contains(BindingAttributes::LOCAL)
                    })
            })
            .collect::<Vec<_>>();
        bindings.sort_by_key(|binding_id| self.semantic.binding(*binding_id).span.start.offset());
        bindings.dedup();
        bindings
    }

    fn call_site_dominates_offset(&self, call_span: Span, limit_offset: usize) -> bool {
        if call_span.start.offset() >= limit_offset {
            return false;
        }

        let Some(mut command_id) = self
            .facts
            .command_facts()
            .innermost_command_id_at(call_span.start.offset())
        else {
            return true;
        };
        while self.facts.command_facts().command(command_id).span() != call_span {
            let Some(parent_id) = self.facts.command_facts().command_parent_id(command_id) else {
                return true;
            };
            command_id = parent_id;
        }

        let mut current = self.facts.command_facts().command_parent_id(command_id);
        while let Some(command_id) = current {
            let command = self.facts.command_facts().command(command_id);
            if command.span().end.offset() > limit_offset {
                break;
            }
            if command.span().start.offset() < call_span.start.offset()
                && self
                    .facts
                    .command_facts()
                    .command_is_dominance_barrier(command_id)
            {
                return false;
            }
            current = self.facts.command_facts().command_parent_id(command_id);
        }

        true
    }

    fn binding_is_guarded_before_reference(&self, binding_id: BindingId, at: Span) -> bool {
        let binding = self.semantic.binding(binding_id);
        let Some(mut current) = self
            .facts
            .command_facts()
            .innermost_command_id_at(binding.span.start.offset())
            .and_then(|id| self.facts.command_facts().command_parent_id(id))
        else {
            return false;
        };

        loop {
            let command = self.facts.command_facts().command(current);
            if self
                .facts
                .command_facts()
                .command_is_dominance_barrier(current)
                && command.span().end.offset() <= at.start.offset()
            {
                return true;
            }

            let Some(parent_id) = self.facts.command_facts().command_parent_id(current) else {
                return false;
            };
            current = parent_id;
        }
    }

    fn binding_is_one_sided_short_circuit_assignment(&self, binding_id: BindingId) -> bool {
        self.facts
            .assignments()
            .binding_value(binding_id)
            .is_some_and(|value| value.one_sided_short_circuit_assignment())
    }

    fn binding_is_one_sided_append_assignment(&self, binding_id: BindingId) -> bool {
        matches!(
            self.semantic.binding(binding_id).kind,
            BindingKind::AppendAssignment
        ) && self.binding_is_one_sided_short_circuit_assignment(binding_id)
    }

    fn one_sided_bindings_preserve_safe_base(
        &mut self,
        bindings: &[BindingId],
        name: &Name,
        at: Span,
        query: SafeValueQuery,
        case_cli_scope: Option<ScopeId>,
    ) -> bool {
        let mut base_bindings = Vec::new();
        let mut saw_one_sided_binding = false;
        for binding_id in bindings.iter().copied() {
            if self.binding_is_one_sided_short_circuit_assignment(binding_id) {
                saw_one_sided_binding = true;
            } else {
                base_bindings.push(binding_id);
            }
        }

        saw_one_sided_binding
            && !base_bindings.is_empty()
            && self.bindings_cover_all_paths_to_reference(&base_bindings, name, at)
            && bindings
                .iter()
                .copied()
                .all(|binding_id| self.binding_is_safe(binding_id, at, query, case_cli_scope))
    }

    fn helper_scopes_providing_name(&self, name: &Name) -> Vec<ScopeId> {
        self.semantic
            .bindings_for(name)
            .iter()
            .copied()
            .filter_map(|binding_id| {
                let binding = self.semantic.binding(binding_id);
                (!binding.attributes.contains(BindingAttributes::LOCAL)
                    && matches!(
                        self.semantic.scope(binding.scope).kind,
                        ScopeKind::Function(_)
                    ))
                .then_some(binding.scope)
            })
            .collect::<FxHashSet<_>>()
            .into_iter()
            .collect()
    }

    fn named_function_kind(&self, scope: ScopeId) -> Option<&shuck_semantic::FunctionScopeKind> {
        match &self.semantic.scope(scope).kind {
            ScopeKind::Function(function) if !function.static_names().is_empty() => Some(function),
            ScopeKind::File
            | ScopeKind::Function(_)
            | ScopeKind::Subshell
            | ScopeKind::CommandSubstitution
            | ScopeKind::Pipeline => None,
        }
    }

    fn binding_dominates_reference(&self, binding_id: BindingId, name: &Name, at: Span) -> bool {
        self.analysis.binding_dominates_reference_from_flow_entry(
            binding_id,
            name,
            at,
            !self.binding_is_guarded_before_reference(binding_id, at),
        )
    }

    fn bindings_cover_all_paths_to_reference(
        &self,
        bindings: &[BindingId],
        name: &Name,
        at: Span,
    ) -> bool {
        self.value_source_blocks_cover_all_paths_to_reference(
            bindings,
            name,
            at,
            FxHashSet::default(),
        )
    }

    fn value_sources_cover_all_paths_to_reference(
        &self,
        bindings: &[BindingId],
        name: &Name,
        at: Span,
    ) -> bool {
        let unset_blocks = if bindings.is_empty() {
            Default::default()
        } else {
            self.unset_value_blocks_for_name_before_reference(name, at)
        };
        self.value_source_blocks_cover_all_paths_to_reference(bindings, name, at, unset_blocks)
    }

    fn value_source_blocks_cover_all_paths_to_reference(
        &self,
        bindings: &[BindingId],
        name: &Name,
        at: Span,
        mut cover_blocks: FxHashSet<BlockId>,
    ) -> bool {
        let Some(reference_block) = self.block_for_name_reference_or_virtual_offset(name, at)
        else {
            return true;
        };

        cover_blocks.extend(bindings.iter().copied().filter_map(|binding_id| {
            let binding_block = self.block_for_binding(binding_id)?;
            if (binding_block == reference_block
                && self.binding_is_guarded_before_reference(binding_id, at))
                || self.binding_is_one_sided_short_circuit_assignment(binding_id)
            {
                None
            } else {
                Some(binding_block)
            }
        }));
        if cover_blocks.contains(&reference_block) {
            return true;
        }

        let binding_scopes = bindings
            .iter()
            .copied()
            .map(|binding_id| self.semantic.binding(binding_id).scope)
            .collect::<Vec<_>>();
        let entry = self
            .analysis
            .flow_entry_block_for_binding_scopes(&binding_scopes, at.start.offset());
        self.analysis
            .blocks_cover_all_paths_to_block(entry, reference_block, &cover_blocks)
    }

    fn unset_value_blocks_for_name_before_reference(
        &self,
        name: &Name,
        at: Span,
    ) -> FxHashSet<BlockId> {
        self.facts
            .assignments()
            .unset_commands_for_name(name)
            .filter(|command| {
                command.span().end.offset() <= at.start.offset()
                    && self.enclosing_function_scope_at(command.span().start.offset())
                        == self.enclosing_function_scope_at(at.start.offset())
                    && self.command_runs_in_persistent_shell_context(command.id())
                    && !self.command_is_in_background_context(command.id())
                    && !self.command_is_in_boolean_list(command.id())
            })
            .flat_map(|command| {
                self.analysis
                    .block_ids_for_span(command.span())
                    .iter()
                    .copied()
            })
            .collect()
    }

    fn command_is_in_boolean_list(&self, command_id: crate::facts::CommandId) -> bool {
        let mut current = self.facts.command_facts().command_parent_id(command_id);
        while let Some(id) = current {
            if let Command::Binary(binary) = self.facts.command_facts().command(id).command()
                && matches!(binary.op, BinaryOp::And | BinaryOp::Or)
            {
                return true;
            }
            current = self.facts.command_facts().command_parent_id(id);
        }
        false
    }

    fn bindings_cover_all_paths_to_callsite(
        &self,
        bindings: &[BindingId],
        call_span: Span,
    ) -> bool {
        let call_blocks = self
            .analysis
            .cfg()
            .blocks()
            .iter()
            .filter(|block| block.commands.contains(&call_span))
            .map(|block| block.id)
            .collect::<FxHashSet<_>>();
        let cover_bindings = bindings
            .iter()
            .copied()
            .filter(|binding_id| {
                let Some(binding_block) = self.block_for_binding(*binding_id) else {
                    return false;
                };
                !((call_blocks.contains(&binding_block)
                    && self.binding_is_guarded_before_reference(*binding_id, call_span))
                    || self.binding_is_one_sided_short_circuit_assignment(*binding_id))
            })
            .collect::<Vec<_>>();
        self.value_flow
            .borrow()
            .value_bindings_cover_all_paths_to_span(&cover_bindings, call_span)
    }

    fn reference_id_for_name_at(&self, name: &Name, at: Span) -> Option<ReferenceId> {
        self.analysis.reference_id_for_name_at(name, at)
    }

    fn block_for_name_reference_or_virtual_offset(&self, name: &Name, at: Span) -> Option<BlockId> {
        if let Some(reference_id) = self.reference_id_for_name_at(name, at) {
            return self.block_for_reference(reference_id);
        }

        let command_id = self
            .facts
            .command_facts()
            .innermost_command_id_at(at.start.offset())
            .or_else(|| {
                self.facts
                    .command_facts()
                    .innermost_command_id_containing_offset(at.start.offset())
            })?;
        self.analysis
            .block_ids_for_span(self.facts.command_facts().command(command_id).span())
            .first()
            .copied()
    }

    fn block_for_binding(&self, binding_id: BindingId) -> Option<BlockId> {
        self.analysis.block_for_binding(binding_id)
    }

    fn block_for_reference(&self, reference_id: ReferenceId) -> Option<BlockId> {
        self.analysis.block_for_reference_id(reference_id)
    }

    fn transformation_is_safe(
        &mut self,
        reference: &VarRef,
        operator: char,
        at: Span,
        query: SafeValueQuery,
    ) -> bool {
        if query != SafeValueQuery::Quoted && reference.has_array_selector() {
            return false;
        }

        match operator {
            'Q' | 'K' | 'k' => true,
            _ => self.reference_is_safe(reference, at, query),
        }
    }

    fn parameter_part_is_safe(
        &mut self,
        parameter: &ParameterExpansion,
        at: Span,
        query: SafeValueQuery,
    ) -> bool {
        match &parameter.syntax {
            ParameterExpansionSyntax::Bourne(syntax) => match syntax {
                BourneParameterExpansion::Access { reference } => {
                    (query == SafeValueQuery::Quoted || !reference.has_array_selector())
                        && self.reference_is_safe(reference, at, query)
                }
                BourneParameterExpansion::Length { .. } => true,
                BourneParameterExpansion::Indices { .. }
                | BourneParameterExpansion::PrefixMatch { .. } => query == SafeValueQuery::Quoted,
                BourneParameterExpansion::Indirect {
                    reference,
                    operator,
                    operand_word_ast,
                    ..
                } => {
                    self.indirect_name_is_safe(reference, at, query)
                        && operator.as_deref().is_none_or(|operator| {
                            self.parameter_operator_is_safe(
                                &reference.name,
                                operator,
                                operand_word_ast.as_deref(),
                                at,
                                query,
                            )
                        })
                }
                BourneParameterExpansion::Slice { reference, .. } => {
                    if reference.has_array_selector() {
                        query == SafeValueQuery::Quoted
                    } else {
                        self.reference_is_safe(reference, at, query)
                            || self.slice_reference_static_literals_stay_safe(
                                reference, syntax, at, query,
                            )
                    }
                }
                BourneParameterExpansion::Operation {
                    reference,
                    operator,
                    operand_word_ast,
                    ..
                } => self.parameter_expansion_is_safe(
                    reference,
                    operator,
                    operand_word_ast.as_deref(),
                    at,
                    query,
                ),
                BourneParameterExpansion::Transformation {
                    reference,
                    operator,
                } => self.transformation_is_safe(reference, *operator, at, query),
            },
            ParameterExpansionSyntax::Zsh(_) => false,
        }
    }

    fn parameter_part_s001_quote_exposure(
        &mut self,
        parameter: &ParameterExpansion,
        at: Span,
        query: SafeValueQuery,
    ) -> S001QuoteExposure {
        match &parameter.syntax {
            ParameterExpansionSyntax::Bourne(BourneParameterExpansion::Access { reference })
                if !reference.has_array_selector() =>
            {
                self.name_s001_quote_exposure(&reference.name, at, query)
            }
            ParameterExpansionSyntax::Bourne(BourneParameterExpansion::Slice {
                reference, ..
            }) if !reference.has_array_selector() => {
                self.name_s001_quote_exposure(&reference.name, at, query)
            }
            ParameterExpansionSyntax::Bourne(BourneParameterExpansion::Length { .. }) => {
                S001QuoteExposure::QuoteInertNonEmpty
            }
            ParameterExpansionSyntax::Bourne(_) | ParameterExpansionSyntax::Zsh(_) => {
                if self.parameter_part_is_safe(parameter, at, query) {
                    S001QuoteExposure::QuoteInertNonEmpty
                } else {
                    S001QuoteExposure::Unsafe
                }
            }
        }
    }

    fn parameter_expansion_is_safe(
        &mut self,
        reference: &VarRef,
        operator: &ParameterOp,
        operand_word: Option<&Word>,
        _at: Span,
        query: SafeValueQuery,
    ) -> bool {
        if query != SafeValueQuery::Quoted && reference.has_array_selector() {
            return false;
        }

        self.parameter_operator_is_safe(
            &reference.name,
            operator,
            operand_word,
            reference.name_span,
            query,
        )
    }

    fn parameter_operator_is_safe(
        &mut self,
        name: &Name,
        operator: &ParameterOp,
        operand_word: Option<&Word>,
        at: Span,
        query: SafeValueQuery,
    ) -> bool {
        match operator {
            ParameterOp::UpperFirst
            | ParameterOp::UpperAll
            | ParameterOp::LowerFirst
            | ParameterOp::LowerAll
            | ParameterOp::RemovePrefixShort { .. }
            | ParameterOp::RemovePrefixLong { .. }
            | ParameterOp::RemoveSuffixShort { .. }
            | ParameterOp::RemoveSuffixLong { .. } => self.name_is_safe(name, at, query),
            ParameterOp::UseDefault | ParameterOp::AssignDefault => {
                self.name_is_safe(name, at, query)
                    || (query == SafeValueQuery::NumericTestOperand
                        && operand_word
                            .is_some_and(|word| self.word_is_safe_numeric_operand(word, at))
                        && self.name_has_numeric_loop_body_assignment(name, at))
            }
            ParameterOp::Error => self.name_is_safe(name, at, query),
            ParameterOp::UseReplacement => {
                operand_word.is_some_and(|word| self.word_is_static_safe_literal(word, query))
            }
            ParameterOp::ReplaceFirst {
                replacement_word_ast,
                ..
            }
            | ParameterOp::ReplaceAll {
                replacement_word_ast,
                ..
            } => {
                self.name_is_safe(name, at, query)
                    && self.word_is_static_safe_literal(replacement_word_ast, query)
            }
        }
    }

    fn slice_reference_static_literals_stay_safe(
        &mut self,
        reference: &VarRef,
        slice: &BourneParameterExpansion,
        at: Span,
        query: SafeValueQuery,
    ) -> bool {
        let BourneParameterExpansion::Slice {
            offset_word_ast,
            length_word_ast,
            ..
        } = slice
        else {
            return false;
        };
        let Some(offset_text) = static_word_text(offset_word_ast, self.source) else {
            return false;
        };
        let Ok(offset) = offset_text.parse::<isize>() else {
            return false;
        };
        let length = match length_word_ast {
            Some(word) => {
                let Some(length_text) = static_word_text(word, self.source) else {
                    return false;
                };
                let Ok(length) = length_text.parse::<usize>() else {
                    return false;
                };
                Some(length)
            }
            None => None,
        };

        let mut bindings = self.safe_bindings_for_name(&reference.name, at);
        self.drop_declarations_shadowed_by_covering_loop_bindings(&mut bindings, at);
        self.drop_outer_bindings_shadowed_by_covering_loop_bindings(&mut bindings, at);
        self.retain_value_bindings(&mut bindings);
        bindings.retain(|binding_id| {
            !self.binding_is_cleared_by_dominating_unset(*binding_id, &reference.name, at)
        });
        if query.is_field_context() {
            bindings.retain(|binding_id| {
                !self.binding_is_blocked_by_exit_like_function_call(*binding_id, at)
            });
        }
        if bindings.is_empty() {
            return false;
        }

        bindings.into_iter().all(|binding_id| {
            let Some(word) = self
                .facts
                .assignments()
                .binding_value(binding_id)
                .and_then(|value| value.scalar_word())
            else {
                return false;
            };
            let Some(text) = static_word_text(word, self.source) else {
                return false;
            };
            static_slice_result_is_safe(&text, offset, length, query)
        })
    }

    fn word_is_static_safe_literal(&self, word: &Word, query: SafeValueQuery) -> bool {
        let Some(text) = static_word_text(word, self.source) else {
            return false;
        };

        if word.is_fully_quoted() {
            match query {
                SafeValueQuery::Argv
                | SafeValueQuery::RedirectTarget
                | SafeValueQuery::NumericTestOperand
                | SafeValueQuery::Quoted => true,
                SafeValueQuery::Pattern | SafeValueQuery::Regex => query.literal_is_safe(&text),
            }
        } else {
            query.literal_is_safe(&text)
        }
    }

    fn word_is_safe_numeric_operand(&mut self, word: &Word, at: Span) -> bool {
        if word_static_text_is_shell_integer(word, self.source) {
            return true;
        }

        self.word_plain_scalar_reference_name(word)
            .is_some_and(|name| self.name_is_safe(&name, at, SafeValueQuery::NumericTestOperand))
    }

    fn name_has_numeric_loop_body_assignment(&mut self, name: &Name, at: Span) -> bool {
        let Some(body_span) = self.enclosing_while_body_for_condition_span(at) else {
            return false;
        };
        let bindings = self.semantic.bindings_for(name).to_vec();

        bindings.into_iter().any(|binding_id| {
            let binding = self.semantic.binding(binding_id);
            span_contains(body_span, binding.span)
                && self.binding_assigns_numeric_operand_value(binding_id, at)
        })
    }

    fn s001_function_reference_has_file_scope_integer_bindings(
        &mut self,
        name: &Name,
        at: Span,
    ) -> bool {
        let Some(function_scope) = self.enclosing_function_scope_at(at.start.offset()) else {
            return false;
        };
        if self
            .semantic
            .bindings_for(name)
            .iter()
            .copied()
            .any(|binding_id| {
                let binding = self.semantic.binding(binding_id);
                binding.scope == function_scope
                    && self.binding_can_supply_parameter_value(binding_id)
            })
        {
            return false;
        }

        let Some(file_scope) = self
            .semantic
            .ancestor_scopes(self.semantic.scope_at(at.start.offset()))
            .find(|scope| matches!(self.semantic.scope(*scope).kind, ScopeKind::File))
        else {
            return false;
        };

        let bindings = self
            .semantic
            .bindings_for(name)
            .iter()
            .copied()
            .filter(|binding_id| {
                let binding = self.semantic.binding(*binding_id);
                binding.scope == file_scope && self.binding_can_supply_parameter_value(*binding_id)
            })
            .collect::<Vec<_>>();

        if bindings.is_empty()
            || !bindings
                .iter()
                .copied()
                .all(|binding_id| self.binding_assigns_numeric_operand_value(binding_id, at))
        {
            return false;
        }

        let call_sites = self.named_function_call_sites(function_scope);
        call_sites.is_empty()
            || call_sites.into_iter().all(|(_, call_span)| {
                self.bindings_cover_all_paths_to_callsite(&bindings, call_span)
            })
    }

    fn s001_name_has_only_numeric_value_bindings(&mut self, name: &Name, at: Span) -> bool {
        let mut bindings = self.safe_bindings_for_name(name, at);
        self.drop_declarations_shadowed_by_covering_loop_bindings(&mut bindings, at);
        self.drop_outer_bindings_shadowed_by_covering_loop_bindings(&mut bindings, at);
        self.retain_value_bindings(&mut bindings);
        let mut transitive_helper_bindings = self.transitive_helper_bindings_before(
            name,
            at,
            !self.span_is_exit_or_return_argument(at),
        );
        self.retain_value_bindings(&mut transitive_helper_bindings);
        bindings.extend(transitive_helper_bindings.iter().copied());
        bindings.sort_by_key(|binding_id| self.semantic.binding(*binding_id).span.start.offset());
        bindings.dedup();
        if bindings.is_empty() {
            return false;
        }
        if bindings.iter().copied().any(|binding_id| {
            (self.binding_is_one_sided_short_circuit_assignment(binding_id)
                && !self.binding_assigns_static_integer_literal(binding_id))
                || self
                    .facts
                    .assignments()
                    .binding_value(binding_id)
                    .is_some_and(|value| {
                        value.conditional_assignment_shortcut()
                            && !self.binding_assigns_static_integer_literal(binding_id)
                            || value.scalar_word().is_some_and(|word| {
                                static_word_text(word, self.source)
                                    .is_some_and(|text| text.is_empty())
                            })
                    })
        }) {
            return false;
        }

        let has_covering_binding = self.bindings_cover_all_paths_to_reference(&bindings, name, at)
            || bindings.iter().copied().any(|binding_id| {
                let binding = self.semantic.binding(binding_id);
                binding.span.end.offset() <= at.start.offset()
                    && self.binding_dominates_reference(binding_id, name, at)
            });
        has_covering_binding
            && bindings.into_iter().all(|binding_id| {
                self.binding_assigns_s001_standalone_numeric_argv_value(binding_id)
            })
    }

    fn binding_assigns_s001_standalone_numeric_argv_value(&self, binding_id: BindingId) -> bool {
        let binding = self.semantic.binding(binding_id);
        if self.binding_has_effective_integer_attribute(binding_id)
            || matches!(binding.kind, BindingKind::ArithmeticAssignment)
        {
            return true;
        }

        let Some(word) = self
            .facts
            .assignments()
            .binding_value(binding_id)
            .and_then(|value| value.scalar_word())
        else {
            return false;
        };

        word_static_text_is_shell_integer(word, self.source) || word_is_arithmetic_expansion(word)
    }

    fn binding_assigns_static_integer_literal(&self, binding_id: BindingId) -> bool {
        self.facts
            .assignments()
            .binding_value(binding_id)
            .and_then(|value| value.scalar_word())
            .is_some_and(|word| word_static_text_is_shell_integer(word, self.source))
    }

    fn s001_name_has_only_arithmetic_numeric_bindings(&mut self, name: &Name, at: Span) -> bool {
        let mut bindings = self.safe_bindings_for_name(name, at);
        self.drop_declarations_shadowed_by_covering_loop_bindings(&mut bindings, at);
        self.drop_outer_bindings_shadowed_by_covering_loop_bindings(&mut bindings, at);
        self.retain_value_bindings(&mut bindings);
        if bindings.is_empty()
            || bindings.iter().copied().any(|binding_id| {
                self.binding_is_one_sided_short_circuit_assignment(binding_id)
                    || self
                        .facts
                        .assignments()
                        .binding_value(binding_id)
                        .is_some_and(|value| {
                            value.conditional_assignment_shortcut()
                                || value.scalar_word().is_some_and(|word| {
                                    static_word_text(word, self.source)
                                        .is_some_and(|text| text.is_empty())
                                })
                        })
            })
        {
            return false;
        }

        let has_covering_binding = self.bindings_cover_all_paths_to_reference(&bindings, name, at)
            || bindings.iter().copied().any(|binding_id| {
                let binding = self.semantic.binding(binding_id);
                binding.span.end.offset() <= at.start.offset()
                    && self.binding_dominates_reference(binding_id, name, at)
            });
        has_covering_binding
            && bindings
                .into_iter()
                .all(|binding_id| self.binding_assigns_s001_arithmetic_numeric_value(binding_id))
    }

    fn binding_assigns_s001_arithmetic_numeric_value(&self, binding_id: BindingId) -> bool {
        let binding = self.semantic.binding(binding_id);
        if self.binding_has_effective_integer_attribute(binding_id)
            || matches!(binding.kind, BindingKind::ArithmeticAssignment)
        {
            return true;
        }

        self.facts
            .assignments()
            .binding_value(binding_id)
            .and_then(|value| value.scalar_word())
            .is_some_and(word_is_arithmetic_expansion)
    }

    fn enclosing_while_body_for_condition_span(&self, at: Span) -> Option<Span> {
        self.facts
            .commands()
            .iter()
            .filter_map(|command| match command.command() {
                Command::Compound(CompoundCommand::While(command))
                    if span_contains(command.condition.span, at) =>
                {
                    Some(command.body.span)
                }
                Command::Simple(_)
                | Command::Builtin(_)
                | Command::Decl(_)
                | Command::Binary(_)
                | Command::Compound(_)
                | Command::Function(_)
                | Command::AnonymousFunction(_) => None,
            })
            .min_by_key(|span| span.end.offset() - span.start.offset())
    }

    fn binding_assigns_numeric_operand_value(&mut self, binding_id: BindingId, at: Span) -> bool {
        let binding = self.semantic.binding(binding_id);
        if self.binding_has_effective_integer_attribute(binding_id)
            || matches!(binding.kind, BindingKind::ArithmeticAssignment)
        {
            return true;
        }

        let Some(word) = self
            .facts
            .assignments()
            .binding_value(binding_id)
            .and_then(|value| value.scalar_word())
        else {
            return false;
        };

        if word_static_text_is_shell_integer(word, self.source)
            || self.word_has_arithmetic_expansion(word)
        {
            return true;
        }

        self.word_plain_scalar_reference_name(word)
            .is_some_and(|name| self.name_is_safe(&name, at, SafeValueQuery::NumericTestOperand))
    }
}

fn literal_is_field_safe(text: &str) -> bool {
    !text
        .chars()
        .any(|character| character.is_whitespace() || matches!(character, '*' | '?' | '['))
}

fn literal_is_pattern_safe(text: &str) -> bool {
    !text
        .chars()
        .any(|character| matches!(character, '*' | '?' | '[' | ']' | '|' | '(' | ')'))
}

fn literal_is_regex_safe(text: &str) -> bool {
    let mut escaped = false;

    for character in text.chars() {
        if escaped {
            return false;
        }

        if character == '\\' {
            escaped = true;
            continue;
        }

        if matches!(
            character,
            '.' | '[' | ']' | '(' | ')' | '{' | '}' | '*' | '+' | '?' | '|' | '^' | '$'
        ) {
            return false;
        }
    }

    !escaped
}

fn plain_scalar_reference_name_from_part(part: &WordPart) -> Option<Name> {
    match part {
        WordPart::Variable(name) if !matches!(name.as_str(), "@" | "*") => Some(name.clone()),
        WordPart::DoubleQuoted { parts, .. } => {
            let [part] = parts.as_slice() else {
                return None;
            };
            plain_scalar_reference_name_from_part(&part.kind)
        }
        WordPart::Parameter(parameter) => match &parameter.syntax {
            ParameterExpansionSyntax::Bourne(BourneParameterExpansion::Access { reference })
                if reference.subscript.is_none()
                    && !matches!(reference.name.as_str(), "@" | "*") =>
            {
                Some(reference.name.clone())
            }
            ParameterExpansionSyntax::Bourne(_) | ParameterExpansionSyntax::Zsh(_) => None,
        },
        WordPart::Literal(_)
        | WordPart::ZshQualifiedGlob(_)
        | WordPart::SingleQuoted { .. }
        | WordPart::Variable(_)
        | WordPart::CommandSubstitution { .. }
        | WordPart::ArithmeticExpansion { .. }
        | WordPart::ParameterExpansion { .. }
        | WordPart::Length(_)
        | WordPart::ArrayAccess(_)
        | WordPart::ArrayLength(_)
        | WordPart::ArrayIndices(_)
        | WordPart::Substring { .. }
        | WordPart::ArraySlice { .. }
        | WordPart::IndirectExpansion { .. }
        | WordPart::PrefixMatch { .. }
        | WordPart::ProcessSubstitution { .. }
        | WordPart::Transformation { .. } => None,
    }
}

fn span_contains(container: Span, inner: Span) -> bool {
    container.start.offset() <= inner.start.offset() && inner.end.offset() <= container.end.offset()
}

fn shell_name_is_uppercase_setup_value(text: &str) -> bool {
    let mut saw_uppercase = false;
    for character in text.chars() {
        if character.is_ascii_uppercase() {
            saw_uppercase = true;
            continue;
        }
        if character == '_' || character.is_ascii_digit() {
            continue;
        }
        return false;
    }

    saw_uppercase
}

fn literal_is_setup_atom(text: &str) -> bool {
    !text.is_empty()
        && text
            .bytes()
            .all(|byte| byte == b'_' || byte.is_ascii_uppercase() || byte.is_ascii_digit())
}

fn safe_special_parameter(name: &Name) -> bool {
    matches!(name.as_str(), "@" | "#" | "?" | "$" | "!" | "-")
}

fn part_is_safe_special_parameter_access(part: &WordPart) -> bool {
    match part {
        WordPart::Variable(name) => safe_special_parameter(name),
        WordPart::DoubleQuoted { parts, .. } => {
            matches!(
                parts.as_slice(),
                [part] if part_is_safe_special_parameter_access(&part.kind)
            )
        }
        WordPart::Parameter(parameter) => matches!(
            parameter.bourne(),
            Some(BourneParameterExpansion::Access { reference })
                if safe_special_parameter(&reference.name) && reference.subscript.is_none()
        ),
        WordPart::Literal(_)
        | WordPart::ZshQualifiedGlob(_)
        | WordPart::SingleQuoted { .. }
        | WordPart::CommandSubstitution { .. }
        | WordPart::ArithmeticExpansion { .. }
        | WordPart::ParameterExpansion { .. }
        | WordPart::Length(_)
        | WordPart::ArrayAccess(_)
        | WordPart::ArrayLength(_)
        | WordPart::ArrayIndices(_)
        | WordPart::Substring { .. }
        | WordPart::ArraySlice { .. }
        | WordPart::IndirectExpansion { .. }
        | WordPart::PrefixMatch { .. }
        | WordPart::ProcessSubstitution { .. }
        | WordPart::Transformation { .. } => false,
    }
}

fn safe_numeric_shell_variable(name: &Name) -> bool {
    matches!(
        name.as_str(),
        "BASHPID"
            | "COLUMNS"
            | "EUID"
            | "LINENO"
            | "OPTIND"
            | "PPID"
            | "RANDOM"
            | "SECONDS"
            | "UID"
    )
}

fn word_static_text_is_shell_integer(word: &Word, source: &str) -> bool {
    static_word_text(word, source).is_some_and(|text| shell_integer_text_is_safe(&text))
}

fn shell_integer_text_is_safe(text: &str) -> bool {
    let digits = text
        .strip_prefix(['+', '-'])
        .filter(|rest| !rest.is_empty())
        .unwrap_or(text);
    !digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit())
}

fn word_is_arithmetic_expansion(word: &Word) -> bool {
    matches!(
        word.parts.as_slice(),
        [part] if matches!(part.kind, WordPart::ArithmeticExpansion { .. })
    )
}

fn function_has_terminal_exit(function: &FunctionDef) -> bool {
    matches!(
        stmt_terminal_flow_kind(&function.body),
        TerminalFlowKind::Exit
    )
}

fn static_slice_result_is_safe(
    text: &str,
    offset: isize,
    length: Option<usize>,
    query: SafeValueQuery,
) -> bool {
    let chars = text.chars().collect::<Vec<_>>();
    let start = if offset < 0 {
        let distance = offset.unsigned_abs();
        if distance > chars.len() {
            return false;
        }
        chars.len().saturating_sub(distance)
    } else {
        offset as usize
    };
    if start > chars.len() {
        return false;
    }
    let end = length
        .map(|length| start.saturating_add(length).min(chars.len()))
        .unwrap_or(chars.len());
    let sliced = chars[start..end].iter().collect::<String>();
    !sliced.is_empty() && query.literal_is_safe(&sliced)
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TerminalFlowKind {
    None,
    MaybeExit,
    MaybeStop,
    Exit,
    Stop,
}

fn stmt_seq_terminal_flow_kind(commands: &StmtSeq) -> TerminalFlowKind {
    let mut saw_maybe_exit = false;
    let mut saw_maybe_stop = false;

    for stmt in commands.as_slice() {
        match stmt_terminal_flow_kind(stmt) {
            TerminalFlowKind::None => {}
            TerminalFlowKind::MaybeExit => saw_maybe_exit = true,
            TerminalFlowKind::MaybeStop => saw_maybe_stop = true,
            TerminalFlowKind::Exit => {
                return if saw_maybe_stop {
                    TerminalFlowKind::Stop
                } else {
                    TerminalFlowKind::Exit
                };
            }
            TerminalFlowKind::Stop => return TerminalFlowKind::Stop,
        }
    }

    if saw_maybe_stop {
        TerminalFlowKind::MaybeStop
    } else if saw_maybe_exit {
        TerminalFlowKind::MaybeExit
    } else {
        TerminalFlowKind::None
    }
}

fn stmt_terminal_flow_kind(stmt: &Stmt) -> TerminalFlowKind {
    if matches!(stmt.terminator, Some(StmtTerminator::Background(_))) {
        return TerminalFlowKind::None;
    }

    command_terminal_flow_kind(&stmt.command)
}

fn command_terminal_flow_kind(command: &Command) -> TerminalFlowKind {
    match command {
        Command::Builtin(BuiltinCommand::Exit(_)) => TerminalFlowKind::Exit,
        Command::Builtin(BuiltinCommand::Return(_)) => TerminalFlowKind::Stop,
        Command::Compound(CompoundCommand::If(command)) => alternative_terminal_flow_kind(
            std::iter::once(stmt_seq_terminal_flow_kind(&command.then_branch))
                .chain(
                    command
                        .elif_branches
                        .iter()
                        .map(|(_, body)| stmt_seq_terminal_flow_kind(body)),
                )
                .chain(command.else_branch.iter().map(stmt_seq_terminal_flow_kind)),
            command.else_branch.is_none(),
        ),
        Command::Compound(CompoundCommand::For(command)) => {
            maybe_stop_terminal_flow_kind(stmt_seq_terminal_flow_kind(&command.body))
        }
        Command::Compound(CompoundCommand::Repeat(command)) => {
            maybe_stop_terminal_flow_kind(stmt_seq_terminal_flow_kind(&command.body))
        }
        Command::Compound(CompoundCommand::Foreach(command)) => {
            maybe_stop_terminal_flow_kind(stmt_seq_terminal_flow_kind(&command.body))
        }
        Command::Compound(CompoundCommand::ArithmeticFor(command)) => {
            maybe_stop_terminal_flow_kind(stmt_seq_terminal_flow_kind(&command.body))
        }
        Command::Compound(CompoundCommand::While(command)) => {
            maybe_stop_terminal_flow_kind(stmt_seq_terminal_flow_kind(&command.body))
        }
        Command::Compound(CompoundCommand::Until(command)) => {
            maybe_stop_terminal_flow_kind(stmt_seq_terminal_flow_kind(&command.body))
        }
        Command::Compound(CompoundCommand::Select(command)) => {
            maybe_stop_terminal_flow_kind(stmt_seq_terminal_flow_kind(&command.body))
        }
        Command::Compound(CompoundCommand::Case(command)) => alternative_terminal_flow_kind(
            command
                .cases
                .iter()
                .map(|case| stmt_seq_terminal_flow_kind(&case.body)),
            true,
        ),
        Command::Compound(CompoundCommand::BraceGroup(body)) => stmt_seq_terminal_flow_kind(body),
        Command::Compound(CompoundCommand::Time(command)) => command
            .command
            .as_deref()
            .map_or(TerminalFlowKind::None, stmt_terminal_flow_kind),
        Command::Simple(_)
        | Command::Builtin(_)
        | Command::Decl(_)
        | Command::Binary(_)
        | Command::Compound(_)
        | Command::Function(_)
        | Command::AnonymousFunction(_) => TerminalFlowKind::None,
    }
}

fn maybe_stop_terminal_flow_kind(flow: TerminalFlowKind) -> TerminalFlowKind {
    match flow {
        TerminalFlowKind::None => TerminalFlowKind::None,
        TerminalFlowKind::MaybeExit | TerminalFlowKind::Exit => TerminalFlowKind::MaybeExit,
        TerminalFlowKind::MaybeStop | TerminalFlowKind::Stop => TerminalFlowKind::MaybeStop,
    }
}

fn alternative_terminal_flow_kind(
    branches: impl IntoIterator<Item = TerminalFlowKind>,
    can_skip_all: bool,
) -> TerminalFlowKind {
    let mut saw_none = can_skip_all;
    let mut saw_maybe_exit = false;
    let mut saw_maybe_stop = false;
    let mut saw_exit = false;
    let mut saw_stop = false;

    for flow in branches {
        match flow {
            TerminalFlowKind::None => saw_none = true,
            TerminalFlowKind::MaybeExit => saw_maybe_exit = true,
            TerminalFlowKind::MaybeStop => saw_maybe_stop = true,
            TerminalFlowKind::Exit => saw_exit = true,
            TerminalFlowKind::Stop => saw_stop = true,
        }
    }

    if saw_maybe_stop || ((saw_none || saw_maybe_exit) && saw_stop) {
        return TerminalFlowKind::MaybeStop;
    }
    if saw_maybe_exit || (saw_none && saw_exit) {
        return TerminalFlowKind::MaybeExit;
    }
    if saw_exit && !saw_stop {
        return TerminalFlowKind::Exit;
    }
    if saw_exit || saw_stop {
        return TerminalFlowKind::Stop;
    }

    TerminalFlowKind::None
}

#[cfg(test)]
mod tests {
    use shuck_ast::{Command, Name, RedirectKind};
    use shuck_indexer::Indexer;
    use shuck_parser::parser::Parser;
    use shuck_semantic::{
        BindingOrigin, ContractCertainty, FileContract, ProvidedBinding, ProvidedBindingKind,
        SemanticBuildOptions, UninitializedCertainty,
    };

    use super::{
        SafeValueIndex, SafeValueQuery, function_has_terminal_exit, static_slice_result_is_safe,
    };
    use crate::{ExpansionContext, LinterFacts, LinterSemanticArtifacts};

    #[test]
    fn maps_pattern_and_regex_contexts_into_safe_value_queries() {
        use shuck_ast::RedirectKind;

        assert_eq!(
            SafeValueQuery::from_context(ExpansionContext::CommandArgument),
            Some(SafeValueQuery::Argv)
        );
        assert_eq!(
            SafeValueQuery::from_context(ExpansionContext::HereString),
            Some(SafeValueQuery::Argv)
        );
        assert_eq!(
            SafeValueQuery::from_context(ExpansionContext::CommandName),
            Some(SafeValueQuery::Argv)
        );
        assert_eq!(
            SafeValueQuery::from_context(ExpansionContext::RedirectTarget(RedirectKind::Output)),
            Some(SafeValueQuery::RedirectTarget)
        );
        assert_eq!(
            SafeValueQuery::from_context(ExpansionContext::DescriptorDupTarget(
                RedirectKind::DupOutput
            )),
            Some(SafeValueQuery::RedirectTarget)
        );
        assert_eq!(
            SafeValueQuery::from_context(ExpansionContext::CasePattern),
            Some(SafeValueQuery::Pattern)
        );
        assert_eq!(
            SafeValueQuery::from_context(ExpansionContext::ConditionalPattern),
            Some(SafeValueQuery::Pattern)
        );
        assert_eq!(
            SafeValueQuery::from_context(ExpansionContext::ParameterPattern),
            Some(SafeValueQuery::Pattern)
        );
        assert_eq!(
            SafeValueQuery::from_context(ExpansionContext::RegexOperand),
            Some(SafeValueQuery::Regex)
        );
        assert_eq!(
            SafeValueQuery::from_context(ExpansionContext::StringTestOperand),
            None
        );
    }

    #[test]
    fn quoted_query_treats_prefix_matches_as_safe_only_when_quoted() {
        let source = "#!/bin/bash\nprintf '%s\\n' \"${!HOME@}\" ${!HOME@}\n";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let Command::Simple(command) = &output.file.body[0].command else {
            panic!("expected simple command");
        };

        assert!(safe_values.word_is_safe(&command.args[1], SafeValueQuery::Quoted));
        assert!(!safe_values.word_is_safe(&command.args[2], SafeValueQuery::Argv));
    }

    #[test]
    fn treats_zsh_parameter_modifiers_as_dynamic_unknown_values() {
        let source = "print ${(m)foo}\n";
        let output = Parser::with_dialect(source, shuck_parser::parser::ShellDialect::Zsh)
            .parse()
            .unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let Command::Simple(command) = &output.file.body[0].command else {
            panic!("expected simple command");
        };

        assert!(!safe_values.word_is_safe(&command.args[0], SafeValueQuery::Argv));
        assert!(!safe_values.word_is_safe(&command.args[0], SafeValueQuery::Quoted));
    }

    #[test]
    fn keeps_typed_zsh_parameter_operations_conservative() {
        let source = "print ${(m)foo#${needle}} ${(S)foo/$pattern/$replacement} ${(m)foo:$offset:${length}}\n";
        let output = Parser::with_dialect(source, shuck_parser::parser::ShellDialect::Zsh)
            .parse()
            .unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let Command::Simple(command) = &output.file.body[0].command else {
            panic!("expected simple command");
        };

        assert!(
            command
                .args
                .iter()
                .all(|word| !safe_values.word_is_safe(word, SafeValueQuery::Argv))
        );
        assert!(
            command
                .args
                .iter()
                .all(|word| !safe_values.word_is_safe(word, SafeValueQuery::Quoted))
        );
    }

    #[test]
    fn conditional_safe_fallbacks_do_not_hide_unsafe_bindings() {
        let source = "\
#!/bin/bash
foo=$(printf '%s' \"$1\")
if [ \"$foo\" = \"\" ]; then foo=0; fi
[ $foo -eq 1 ]
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let Command::Simple(command) = &output.file.body[2].command else {
            panic!("expected simple test command");
        };

        assert!(!safe_values.word_is_safe(&command.args[0], SafeValueQuery::Argv));
    }

    #[test]
    fn conditionally_initialized_names_stay_unsafe() {
        let source = "\
#!/bin/bash
if [ \"$1\" = yes ]; then
  foo=0
fi
[ $foo -eq 1 ]
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let Command::Simple(command) = &output.file.body[1].command else {
            panic!("expected simple test command");
        };

        assert!(!safe_values.word_is_safe(&command.args[0], SafeValueQuery::Argv));
    }

    #[test]
    fn reassigned_ppid_stops_being_treated_as_runtime_safe() {
        let source = "\
#!/bin/sh
PPID='a b'
printf '%s\\n' $PPID
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let Command::Simple(command) = &output.file.body[1].command else {
            panic!("expected simple command");
        };

        assert!(!safe_values.word_is_safe(&command.args[1], SafeValueQuery::Argv));
    }

    #[test]
    fn loop_bindings_derived_from_at_slices_stay_unsafe() {
        let source = "\
#!/bin/bash
f() {
  for v in ${@:2}; do
    del $v
  done
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$v"
            })
            .expect("expected loop-body command argument");

        assert!(!safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn direct_at_slices_do_not_become_safe_value_failures() {
        let source = "\
#!/bin/bash
f() {
  dns_set ${@:2}
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "${@:2}"
            })
            .expect("expected direct slice command argument");

        assert!(safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn plain_access_bindings_stay_safe_but_parameter_operations_do_not_propagate() {
        let source = "\
#!/bin/bash
base=Foobar
copy=$base
mixed=$base-1.0
lower=${base,}
trimmed=${base#Foo}
count=${#base}
printf '%s\\n' $copy $mixed $lower $trimmed $count
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let Command::Simple(command) = &output.file.body[6].command else {
            panic!("expected simple command");
        };

        assert!(safe_values.word_is_safe(&command.args[1], SafeValueQuery::Argv));
        assert!(safe_values.word_is_safe(&command.args[2], SafeValueQuery::Argv));
        assert!(!safe_values.word_is_safe(&command.args[3], SafeValueQuery::Argv));
        assert!(!safe_values.word_is_safe(&command.args[4], SafeValueQuery::Argv));
        assert!(!safe_values.word_is_safe(&command.args[5], SafeValueQuery::Argv));
    }

    #[test]
    fn loop_bindings_from_expanded_words_do_not_propagate() {
        let source = "\
#!/bin/bash
name=neverball
for i in $name prefix$name; do
  printf '%s\\n' $i $i.png
done
for size in 16 32; do
  printf '%s\\n' $size ${size}x${size}!
done
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let unsafe_words = facts
            .words()
            .word_facts()
            .iter()
            .filter(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && matches!(fact.span().slice(source), "$i" | "$i.png")
            })
            .collect::<Vec<_>>();
        assert_eq!(unsafe_words.len(), 2, "expected unsafe loop-body words");
        assert!(
            unsafe_words
                .iter()
                .all(|fact| !safe_values.word_occurrence_is_safe(*fact, SafeValueQuery::Argv))
        );

        let safe_words = facts
            .words()
            .word_facts()
            .iter()
            .filter(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && matches!(fact.span().slice(source), "$size" | "${size}x${size}!")
            })
            .collect::<Vec<_>>();
        assert_eq!(safe_words.len(), 2, "expected safe literal-loop words");
        assert!(
            safe_words
                .iter()
                .all(|fact| safe_values.word_occurrence_is_safe(*fact, SafeValueQuery::Argv))
        );
    }

    #[test]
    fn assignment_ternary_bindings_do_not_propagate_safe_values() {
        let source = "\
#!/bin/bash
true && w='-w' || w=''
if true; then
  flag='-w'
else
  flag=''
fi
iptables $w -t nat -N chain
iptables $flag -t nat -N chain
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let short_circuit_word = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$w"
            })
            .expect("expected short-circuit command argument");
        let if_else_word = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$flag"
            })
            .expect("expected if/else command argument");

        assert!(!safe_values.word_occurrence_is_safe(short_circuit_word, SafeValueQuery::Argv));
        assert!(safe_values.word_occurrence_is_safe(if_else_word, SafeValueQuery::Argv));
    }

    #[test]
    fn numeric_assignment_ternary_bindings_stay_safe() {
        let source = "\
#!/bin/bash
I=1
while [ $I -le 3 ]; do
  [[ -z $SPEED ]] && I=$(( I + 1 )) || I=11
done
J=1
while [ $J -le 3 ]; do
  [[ -z $SPEED ]] && J=+11 || J=-1
done
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let loop_words = facts
            .words()
            .word_facts()
            .iter()
            .filter(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && matches!(fact.span().slice(source), "$I" | "$J")
            })
            .collect::<Vec<_>>();

        assert_eq!(loop_words.len(), 2);
        for fact in loop_words {
            assert!(!safe_values.word_occurrence_is_safe(fact, SafeValueQuery::Argv));
            assert!(safe_values.word_occurrence_is_safe(fact, SafeValueQuery::NumericTestOperand));
        }
    }

    #[test]
    fn nested_guarded_assignment_ternaries_stay_unsafe() {
        let source = "\
#!/bin/bash
f() {
  [ \"$1\" = iptables ] && {
    true && w='-w' || w=''
  }
  [ \"$1\" = ip6tables ] && {
    true && w='-w' || w=''
  }
  iptables $w -t nat -N chain
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$w"
            })
            .expect("expected guarded function command argument");

        assert!(!safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn conditional_same_block_initializers_do_not_dominate_later_uses() {
        let source = "\
#!/bin/bash
counter=0
while [ \"$counter\" -eq 0 ]; do
  if [ \"$1\" = validate ] || [ \"$1\" = install ]; then
    validate=validate
  fi
  steamcmd ${validate} +quit
  break
done
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "${validate}"
            })
            .expect("expected conditional initializer command argument");

        assert!(!safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn branch_ladder_pipeline_uses_stay_unsafe_after_guarded_initializers() {
        let source = "\
#!/bin/bash
if [ \"$1\" = validate ] || [ \"$1\" = install ]; then
  validate=validate
fi

if [ \"$mode\" = a ]; then
  steamcmd ${validate} +quit | tee /dev/null
elif [ \"$mode\" = b ]; then
  steamcmd ${validate} +runscript foo | tee /dev/null
else
  steamcmd ${validate} +app_update 1 | tee /dev/null
fi
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let validate_uses = facts
            .words()
            .word_facts()
            .iter()
            .filter(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "${validate}"
            })
            .collect::<Vec<_>>();

        assert_eq!(validate_uses.len(), 3, "expected all sibling pipeline uses");
        assert!(
            validate_uses
                .into_iter()
                .all(|fact| !safe_values.word_occurrence_is_safe(fact, SafeValueQuery::Argv))
        );
    }

    #[test]
    fn function_local_guarded_initializers_do_not_dominate_later_uses() {
        let source = "\
#!/bin/bash
f() {
  if [ \"$1\" = validate ]; then
    validate=validate
  fi
  echo ${validate}
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "${validate}"
            })
            .expect("expected function-local validate command argument");

        assert!(!safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn function_local_pipeline_uses_stay_unsafe_after_guarded_initializers() {
        let source = "\
#!/bin/bash
f() {
  if [ \"$1\" = validate ]; then
    validate=validate
  fi
  while :; do
    if [ \"$appid\" = 90 ]; then
      if [ -n \"$branch\" ] && [ -n \"$beta\" ]; then
        echo ${validate} | tee /dev/null
      elif [ -n \"$branch\" ]; then
        echo ${validate} | tee /dev/null
      else
        echo ${validate} | tee /dev/null
      fi
    fi
    break
  done
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let validate_uses = facts
            .words()
            .word_facts()
            .iter()
            .filter(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "${validate}"
            })
            .collect::<Vec<_>>();

        assert_eq!(
            validate_uses.len(),
            3,
            "expected all function-local pipeline uses"
        );
        assert!(
            validate_uses
                .into_iter()
                .all(|fact| !safe_values.word_occurrence_is_safe(fact, SafeValueQuery::Argv))
        );
    }

    #[test]
    fn case_defaults_distinguish_maybe_uninitialized_from_explicit_empty_bindings() {
        let source = "\
#!/bin/bash
case $1 in
  nfs|dir)
    disk_ext=.qcow2
    ;;
  btrfs)
    disk_ext=.raw
    ;;
esac
printf '%s\\n' vm-${disk_ext:-}

case $2 in
  nfs|dir)
    disk_ext_with_default=.qcow2
    ;;
  btrfs)
    disk_ext_with_default=.raw
    ;;
  *)
    disk_ext_with_default=
    ;;
esac
printf '%s\\n' vm-${disk_ext_with_default:-}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let maybe_uninitialized = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "vm-${disk_ext:-}"
            })
            .expect("expected maybe-uninitialized command argument");
        let explicit_default = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "vm-${disk_ext_with_default:-}"
            })
            .expect("expected explicit-default command argument");

        assert!(!safe_values.word_occurrence_is_safe(maybe_uninitialized, SafeValueQuery::Argv));
        assert!(safe_values.word_occurrence_is_safe(explicit_default, SafeValueQuery::Argv));
    }

    #[test]
    fn unconditional_safe_overwrites_stay_safe() {
        let source = "\
#!/bin/bash
foo=$(printf '%s' \"$1\")
foo=0
[ $foo -eq 1 ]
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let Command::Simple(command) = &output.file.body[2].command else {
            panic!("expected simple test command");
        };

        assert!(safe_values.word_is_safe(&command.args[0], SafeValueQuery::Argv));
    }

    #[test]
    fn case_arm_safe_overwrites_stay_safe() {
        let source = "\
#!/bin/bash
foo=$BAR
case $1 in
    settings)
        foo=0
        [ $foo -eq 1 ]
        ;;
esac
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let Command::Compound(shuck_ast::CompoundCommand::Case(case_command)) =
            &output.file.body[1].command
        else {
            panic!("expected case command");
        };
        let Command::Simple(command) = &case_command.cases[0].body[1].command else {
            panic!("expected simple test command");
        };

        assert!(safe_values.word_is_safe(&command.args[0], SafeValueQuery::Argv));
    }

    #[test]
    fn if_else_safe_literal_bindings_stay_safe() {
        let source = "\
#!/bin/bash
if [ \"$1\" = h ]; then
  humanreadable=-h
else
  humanreadable=-m
fi
free ${humanreadable}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let Command::Simple(command) = &output.file.body[1].command else {
            panic!("expected simple command");
        };

        assert!(safe_values.word_is_safe(&command.args[0], SafeValueQuery::Argv));
    }

    #[test]
    fn if_else_safe_literal_bindings_stay_safe_inside_command_substitutions() {
        let source = "\
#!/bin/bash
if [ \"$1\" = h ]; then
  humanreadable=-h
else
  humanreadable=-m
fi
value=\"$(free ${humanreadable} | awk '{print $2}')\"
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.is_nested_word_command() && fact.span().slice(source) == "${humanreadable}"
            })
            .expect("expected nested command argument fact");

        assert!(safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn static_loop_variables_after_multiline_loops_stay_unsafe() {
        let source = "\
#!/bin/sh
for i in castool chdman; do
  [ -e $i ] && install -s -m0755 -oroot -groot $i $PKG/usr/games/
done
[ -e split ] && install -s -m0755 -oroot -groot $i $PKG/usr/games/$PRGNAM-split
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let after_loop_use = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$i"
                    && fact.span().start.line() == 5
            })
            .expect("expected post-loop $i word fact");
        let binding_id = safe_values
            .safe_bindings_for_name(&Name::new("i"), after_loop_use.span())
            .into_iter()
            .next()
            .expect("expected reaching loop binding");
        let definition_span = match &semantic.binding(binding_id).origin {
            BindingOrigin::LoopVariable {
                definition_span, ..
            } => *definition_span,
            other @ BindingOrigin::Assignment { .. }
            | other @ BindingOrigin::ParameterDefaultAssignment { .. }
            | other @ BindingOrigin::Imported { .. }
            | other @ BindingOrigin::FunctionDefinition { .. }
            | other @ BindingOrigin::BuiltinTarget { .. }
            | other @ BindingOrigin::ArithmeticAssignment { .. }
            | other @ BindingOrigin::Declaration { .. }
            | other @ BindingOrigin::Nameref { .. } => {
                panic!("expected loop-variable binding, got {other:?}")
            }
        };

        assert!(
            !safe_values
                .loop_variable_reference_stays_within_body(definition_span, after_loop_use.span())
        );
        let (part, part_span) = after_loop_use
            .parts_with_spans()
            .next()
            .expect("expected single-part loop variable word");
        assert!(!safe_values.part_is_safe(part, part_span, SafeValueQuery::Argv));
        assert!(!safe_values.word_occurrence_is_safe(after_loop_use, SafeValueQuery::Argv));
    }

    #[test]
    fn nested_command_substitution_arguments_with_dynamic_values_stay_unsafe() {
        let source = "\
#!/bin/sh
PRGNAM=cproc
GIT_SHA=$( git rev-parse --short HEAD )
DATE=$( git log --date=format:%Y%m%d --format=%cd | head -1 )
VERSION=${DATE}_${GIT_SHA}
echo \"MD5SUM=\\\"$( md5sum $PRGNAM-$VERSION.tar.xz | cut -d' ' -f1 )\\\"\"
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let version_use = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.is_nested_word_command()
                    && fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source).contains("VERSION")
            })
            .expect("expected nested command argument fact for VERSION");

        assert!(!safe_values.word_occurrence_is_safe(version_use, SafeValueQuery::Argv));
    }

    #[test]
    fn definite_uninitialized_bindings_stay_unsafe() {
        let source = "\
#!/bin/bash
if [ \"${PULSE:-yes}\" != \"yes\" ]; then
  pulseopt=\"--without-pulse\"
fi

config() {
  ./configure \\
    $pulseopt \\
    --prefix=/usr
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$pulseopt"
            })
            .expect("expected pulseopt command-argument fact");

        assert!(
            analysis
                .uninitialized_references()
                .iter()
                .any(|uninitialized| {
                    semantic.reference(uninitialized.reference).span == word_fact.span()
                }),
            "expected pulseopt to be marked uninitialized"
        );
        assert!(!safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn all_empty_static_literal_bindings_stay_unsafe_but_mixed_option_bindings_can_stay_safe() {
        let source = "\
#!/bin/bash
gl2ps=
if true; then
  libdirsuffix=
else
  libdirsuffix=
fi

if true; then
  mixedsuffix=64
else
  mixedsuffix=
fi

cmake $gl2ps -DOPT=1
mkdir -p /tmp/usr/lib${libdirsuffix}/ladspa
mkdir -p /tmp/usr/lib${mixedsuffix}/ladspa

if true; then
  opt=-n
else
  opt=
fi
printf '%s\\n' $opt hi
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let unsafe_words = facts
            .words()
            .word_facts()
            .iter()
            .filter(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && (fact.span().slice(source) == "$gl2ps"
                        || fact
                            .span()
                            .slice(source)
                            .contains("lib${libdirsuffix}/ladspa"))
            })
            .collect::<Vec<_>>();
        let safe_words = facts
            .words()
            .word_facts()
            .iter()
            .filter(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && (fact.span().slice(source) == "$opt"
                        || fact
                            .span()
                            .slice(source)
                            .contains("lib${mixedsuffix}/ladspa"))
            })
            .collect::<Vec<_>>();

        assert_eq!(unsafe_words.len(), 2, "expected both empty-only uses");
        assert!(
            unsafe_words
                .into_iter()
                .all(|fact| !safe_values.word_occurrence_is_safe(fact, SafeValueQuery::Argv))
        );
        assert_eq!(safe_words.len(), 2, "expected both mixed-value uses");
        assert!(
            safe_words
                .into_iter()
                .all(|fact| safe_values.word_occurrence_is_safe(fact, SafeValueQuery::Argv))
        );
    }

    #[test]
    fn helper_call_sequences_can_make_outer_safe_globals_unsafe() {
        let source = "\
#!/bin/bash
Region=default

GetRegion() {
  Region=\"$(printf '%s' \"$1\")\"
}

GetAMI() {
  aws ec2 describe-images --region $Region
}

GetRegion \"$1\"
GetAMI
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$Region"
            })
            .expect("expected Region command-argument fact");

        assert!(!safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn conditional_helper_globals_stay_unsafe_inside_nested_command_substitution_consumers() {
        let source = "\
#!/bin/bash
Region=default

GetRegion() {
  if [ \"$Region\" = default ]; then
    Region=\"$(printf '%s' \"$1\")\"
  fi
}

GetAMI() {
  AMI=$(aws ssm get-parameters --region $Region)
}

GetRegion \"$1\"
GetAMI
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$Region"
            })
            .expect("expected nested command-substitution helper-derived argument");

        assert!(!safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn helper_initialized_option_flags_stay_safe_across_top_level_call_sequences() {
        let source = "\
#!/bin/bash
fn_select_compression() {
  if command -v zstd >/dev/null 2>&1; then
    compressflag=--zstd
  elif command -v pigz >/dev/null 2>&1; then
    compressflag=--use-compress-program=pigz
  elif command -v gzip >/dev/null 2>&1; then
    compressflag=--gzip
  else
    compressflag=
  fi
}

fn_backup_check_lockfile() { :; }
fn_backup_create_lockfile() { :; }
fn_backup_init() { :; }
fn_backup_stop_server() { :; }
fn_backup_dir() { :; }

fn_backup_compression() {
  if [ -n \"${compressflag}\" ]; then
    tar ${compressflag} -hcf out.tar ./.
  else
    tar -hcf out.tar ./.
  fi
}

fn_select_compression
fn_backup_check_lockfile
fn_backup_create_lockfile
fn_backup_init
fn_backup_stop_server
fn_backup_dir
fn_backup_compression
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "${compressflag}"
            })
            .expect("expected helper-provided command argument fact");

        assert!(safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn case_cli_dispatch_entry_functions_do_not_inherit_outer_safe_globals() {
        let source = "\
#!/bin/sh
exec=/usr/sbin/collectd
pidfile=/var/run/collectd.pid
configfile=/etc/collectd.conf

start() {
  [ -x $exec ] || exit 5
  $exec -P $pidfile -C $configfile
}

case \"$1\" in
  start) $1 ;;
esac
exit $?
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let command_name = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandName)
                    && fact.span().slice(source) == "$exec"
            })
            .expect("expected dynamic command-name fact");
        let command_arg = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$pidfile"
            })
            .expect("expected dynamic command-argument fact");

        assert!(!safe_values.word_occurrence_is_safe(command_name, SafeValueQuery::Argv));
        assert!(!safe_values.word_occurrence_is_safe(command_arg, SafeValueQuery::Argv));
    }

    #[test]
    fn case_cli_dispatch_without_top_level_exit_keeps_outer_safe_globals() {
        let source = "\
#!/bin/sh
exec=/usr/sbin/collectd
pidfile=/var/run/collectd.pid
configfile=/etc/collectd.conf

start() {
  [ -x $exec ] || exit 5
  $exec -P $pidfile -C $configfile
}

case \"$1\" in
  start) $1 ;;
esac
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let command_name = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandName)
                    && fact.span().slice(source) == "$exec"
            })
            .expect("expected dynamic command-name fact");
        let command_arg = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$pidfile"
            })
            .expect("expected dynamic command-argument fact");

        assert!(safe_values.word_occurrence_is_safe(command_name, SafeValueQuery::Argv));
        assert!(safe_values.word_occurrence_is_safe(command_arg, SafeValueQuery::Argv));
    }

    #[test]
    fn case_cli_scope_memoization_does_not_leak_status_capture_unsafety_into_helpers() {
        let source = "\
#!/bin/sh
start() {
  status=$?
  printf '%s\\n' ${status}
}

case \"$1\" in
  start) $1 ;;
esac
exit $?
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let dispatched_use = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "${status}"
            })
            .expect("expected case-cli entrypoint status use");
        let binding_id = safe_values
            .safe_bindings_for_name(&Name::new("status"), dispatched_use.span())
            .into_iter()
            .next()
            .expect("expected reaching status binding");
        let case_cli_scope =
            safe_values.case_cli_dispatch_scope_at(dispatched_use.span().start.offset());

        assert_eq!(
            case_cli_scope,
            Some(semantic.binding(binding_id).scope),
            "expected the status binding to belong to the case-cli scope"
        );

        assert!(!safe_values.binding_is_safe(
            binding_id,
            dispatched_use.span(),
            SafeValueQuery::Argv,
            case_cli_scope
        ));
        assert!(safe_values.binding_is_safe(
            binding_id,
            dispatched_use.span(),
            SafeValueQuery::Argv,
            None
        ));
    }

    #[test]
    fn case_cli_dispatch_entry_functions_keep_local_command_names_but_not_arguments_safe() {
        let source = "\
#!/bin/sh
start() {
  local n=0
  echo $n
}

case \"$1\" in
  start) $1 ;;
esac
exit $?
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let command_arg = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$n"
            })
            .expect("expected case-cli local command argument");

        assert!(!safe_values.word_occurrence_is_safe(command_arg, SafeValueQuery::Argv));
    }

    #[test]
    fn case_cli_dispatch_entry_functions_with_literal_exit_keep_local_arguments_unsafe() {
        let source = "\
#!/bin/sh
start() {
  local n=0
  echo $n
}

case \"$1\" in
  start) $1 ;;
esac
exit 0
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let command_arg = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$n"
            })
            .expect("expected case-cli local command argument");

        assert!(!safe_values.word_occurrence_is_safe(command_arg, SafeValueQuery::Argv));
    }

    #[test]
    fn case_cli_dispatch_reachable_helpers_keep_local_arguments_unsafe() {
        let source = "\
#!/bin/sh
foo() {
  local n=0
  echo $n
}

bound() { foo; }
renew() { foo; }
deconfig() { :; }

case \"$1\" in
  deconfig|renew|bound) $1 ;;
esac
exit $?
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let command_arg = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$n"
            })
            .expect("expected case-cli helper command argument");

        assert!(!safe_values.word_occurrence_is_safe(command_arg, SafeValueQuery::Argv));
    }

    #[test]
    fn case_cli_dispatch_broadens_into_pre_dispatch_functions_even_when_patterns_skip_them() {
        let source = "\
#!/bin/sh
foo() {
  local n=0
  echo $n
}

bar() { :; }

case \"$1\" in
  bar) $1 ;;
esac
exit $?
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let command_arg = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$n"
            })
            .expect("expected pre-dispatch function command argument");

        assert!(!safe_values.word_occurrence_is_safe(command_arg, SafeValueQuery::Argv));
    }

    #[test]
    fn case_cli_dispatch_entry_functions_keep_nested_command_names_but_not_arguments_safe() {
        let source = "\
#!/bin/sh
start() {
  printf '%s\n' \"$(
    cmd=/bin/echo
    arg=hello
    $cmd $arg
  )\"
}

case \"$1\" in
  start) $1 ;;
esac
exit $?
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let command_name = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandName)
                    && fact.span().slice(source) == "$cmd"
            })
            .expect("expected nested command-substitution command name");
        let command_arg = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$arg"
            })
            .expect("expected nested command-substitution command argument");

        assert!(command_name.is_nested_word_command());
        assert!(command_arg.is_nested_word_command());
        assert!(!safe_values.word_occurrence_is_safe(command_arg, SafeValueQuery::Argv));
    }

    #[test]
    fn top_level_exit_broadens_function_arguments_without_dispatch() {
        let source = "\
#!/bin/sh
start() {
  local n=0
  echo $n
}
exit 0
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let command_arg = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$n"
            })
            .expect("expected function-local argument before top-level exit");

        assert!(!safe_values.word_occurrence_is_safe(command_arg, SafeValueQuery::Argv));
    }

    #[test]
    fn nested_function_arguments_stay_unsafe_without_top_level_exit() {
        let source = "\
#!/bin/bash
outer() {
  inner() {
    local good=0
    return $good
  }
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let command_arg = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$good"
            })
            .expect("expected nested-function return argument");

        assert!(!safe_values.word_occurrence_is_safe(command_arg, SafeValueQuery::Argv));
    }

    #[test]
    fn transitive_helper_calls_make_top_level_unsafe_bindings_visible() {
        let source = "\
#!/bin/bash
DISK_SIZE=\"32G\"

advanced_settings() {
  DISK_SIZE=\"$(get_size)\"
}

start_script() {
  advanced_settings
}

start_script
if [ -n \"$DISK_SIZE\" ]; then
  qm resize 100 scsi0 ${DISK_SIZE} >/dev/null
fi
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let command_arg = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "${DISK_SIZE}"
            })
            .expect("expected top-level helper-derived argument");

        assert!(!safe_values.word_occurrence_is_safe(command_arg, SafeValueQuery::Argv));
    }

    #[test]
    fn case_cli_dispatch_indirect_status_targets_stay_unsafe() {
        let source = "\
#!/bin/bash
start() {
  status=$?
  ref=status
  printf '%s\n' ${!ref}
}

case \"$1\" in
  start) $1 ;;
esac
exit $?
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let indirect_use = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "${!ref}"
            })
            .expect("expected indirect case-cli status use");

        assert!(!safe_values.word_occurrence_is_safe(indirect_use, SafeValueQuery::Argv));
    }

    #[test]
    fn helper_initialized_bindings_do_not_leak_across_distinct_callers() {
        let source = "\
#!/bin/bash
init_flag() {
  flag=-n
}

render() {
  printf '%s\\n' ${flag}
}

safe_path() {
  init_flag
  render
}

unsafe_path() {
  render
}

safe_path
unsafe_path
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "${flag}"
            })
            .expect("expected shared helper-derived command argument fact");

        assert!(!safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn helper_globals_inherit_caller_visible_bindings() {
        let source = "\
#!/bin/sh
SERVERNUM=99
find_free_servernum() {
  i=$SERVERNUM
  while [ -f /tmp/.X$i-lock ]; do
    i=$(($i + 1))
  done
  echo $i
}
set -- -n '1 2' -a --
while :; do
  case \"$1\" in
    -a|--auto-servernum) SERVERNUM=$(find_free_servernum) ;;
    -n|--server-num) SERVERNUM=\"$2\"; shift ;;
    --) shift; break ;;
    *) break ;;
  esac
  shift
done
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let unsafe_words = facts
            .words()
            .word_facts()
            .iter()
            .filter(|fact| {
                fact.span().slice(source).contains("$i")
                    && matches!(fact.span().start.line(), 5 | 8)
            })
            .collect::<Vec<_>>();

        assert_eq!(unsafe_words.len(), 2, "expected both helper-body $i uses");
        assert!(
            unsafe_words
                .iter()
                .all(|fact| !safe_values.word_occurrence_is_safe(*fact, SafeValueQuery::Argv))
        );
    }

    #[test]
    fn helper_globals_with_mixed_safe_and_unsafe_caller_branches_stay_unsafe() {
        let source = "\
#!/bin/sh
if [ -n \"$2\" ]; then
  UIPORT=\"$2\"
else
  UIPORT=\"8080\"
fi
do_start() {
  grep $UIPORT /dev/null
}
do_start
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$UIPORT"
            })
            .expect("expected helper command argument fact");
        let (part, part_span) = word_fact
            .parts_with_spans()
            .find(|(_, span)| span.slice(source) == "$UIPORT")
            .expect("expected UIPORT expansion part");
        let name = Name::from("UIPORT");
        let bindings = safe_values.safe_bindings_for_name(&name, part_span);

        assert_eq!(
            bindings.len(),
            2,
            "expected both caller-visible UIPORT bindings"
        );
        assert!(safe_values.bindings_cover_all_paths_to_reference(&bindings, &name, part_span));
        assert!(!safe_values.part_is_safe(part, part_span, SafeValueQuery::Argv));
        assert!(!safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn helper_globals_with_conditionally_initialized_caller_bindings_stay_unsafe() {
        let source = "\
#!/bin/bash
[ \"$1\" = 64 ] && extra=ENABLE_LIB64=1
run_make() {
  make $extra
}
run_make
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$extra"
            })
            .expect("expected helper command argument fact");

        assert!(!safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn helper_calls_inside_conditionals_do_not_count_as_definite_initializers() {
        let source = "\
#!/bin/bash
init_flag() {
  flag=-n
}

render() {
  if [ \"$1\" = yes ]; then
    init_flag
  fi
  printf '%s\\n' ${flag}
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "${flag}"
            })
            .expect("expected conditionally helper-initialized argument fact");

        assert!(!safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn guarded_numeric_helper_bindings_do_not_count_as_definite_values() {
        let source = "\
#!/bin/bash
init_count() {
  if [ \"$1\" = yes ]; then
    count=0
  fi
}
init_count
move_up $count
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$count"
            })
            .expect("expected numeric helper argument fact");
        let (part, part_span) = word_fact
            .parts_with_spans()
            .find(|(_, span)| span.slice(source) == "$count")
            .expect("expected count expansion part");

        assert!(!safe_values.part_has_s001_standalone_numeric_argv_exposure(part, part_span));
    }

    #[test]
    fn helper_writes_to_caller_local_keep_arguments_unsafe() {
        let source = "\
#!/bin/bash
helper() {
  value=$1
}

render() {
  local value=SAFE
  helper \"$1\"
  printf '%s\\n' ${value}
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "${value}"
            })
            .expect("expected helper-mutated local argument fact");
        let (part, part_span) = word_fact
            .parts_with_spans()
            .find(|(_, span)| span.slice(source) == "${value}")
            .expect("expected value expansion part");
        let name = Name::from("value");
        let helper_bindings = safe_values.called_helper_bindings_for_name(&name, part_span);

        assert_eq!(
            helper_bindings.len(),
            1,
            "expected helper assignment to remain visible through caller local"
        );
        assert!(!safe_values.part_is_safe(part, part_span, SafeValueQuery::Argv));

        assert!(!safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn safe_setup_helper_writes_do_not_suppress_sibling_helper_warnings() {
        let source = "\
#!/bin/bash
setup() {
  value=SAFE
}

use_value() {
  printf '%s\\n' ${value}
}

main() {
  local value
  setup
  use_value
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "${value}"
            })
            .expect("expected sibling helper argument fact");

        assert!(!safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn shadowed_helper_definition_does_not_make_safe_caller_local_unsafe() {
        let source = "\
#!/bin/bash
helper() {
  value=$1
}

render() {
  local value=SAFE
  helper() {
    :
  }
  helper
  printf '%s\\n' ${value}
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "${value}"
            })
            .expect("expected shadowed helper argument fact");

        assert!(safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn shadowed_helper_call_site_does_not_count_as_outer_helper_write() {
        let source = "\
#!/bin/bash
helper() {
  value=$1
}

render() {
  local value=SAFE
  helper() {
    :
  }
  helper
  printf '%s\\n' ${value}
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "${value}"
            })
            .expect("expected local argument fact");
        let render_scope = analysis
            .enclosing_function_scope_at(word_fact.span().start.offset())
            .expect("expected render scope");
        let outer_helper_binding = semantic
            .semantic()
            .bindings()
            .iter()
            .find(|binding| {
                binding.name == "value"
                    && binding.scope != render_scope
                    && matches!(binding.kind, shuck_semantic::BindingKind::Assignment)
            })
            .expect("expected outer helper binding");

        assert!(!safe_values.binding_writes_visible_local_in_scope_before(
            outer_helper_binding.id,
            render_scope,
            word_fact.span(),
        ));
    }

    #[test]
    fn helper_call_before_local_shadow_does_not_make_local_unsafe() {
        let source = "\
#!/bin/bash
helper() {
  value=$1
}

render() {
  helper \"$1\"
  local value=SAFE
  printf '%s\\n' ${value}
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "${value}"
            })
            .expect("expected local argument fact");

        assert!(safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn unrelated_caller_local_does_not_poison_status_capture_helper_use() {
        let source = "\
#!/bin/bash
helper() {
  false || ret=$?
  printf '%s\\n' $ret
}

shadowed_caller() {
  local ret=SAFE
  helper
}

clean_caller() {
  helper
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$ret"
            })
            .expect("expected helper status argument fact");
        let status_binding = semantic
            .bindings_for(&Name::from("ret"))
            .iter()
            .copied()
            .find(|binding_id| safe_values.binding_value_is_standalone_status_capture(*binding_id))
            .expect("expected status capture binding");

        assert!(
            !safe_values.status_capture_binding_conflicts_with_caller_local(
                status_binding,
                word_fact.span()
            )
        );
    }

    #[test]
    fn unset_static_slice_binding_does_not_stay_safe() {
        let source = "\
#!/bin/bash
sig=RS256
unset sig
openssl dgst -sha${sig:2} file
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "-sha${sig:2}"
            })
            .expect("expected sliced digest argument fact");

        assert!(!safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn escaped_declaration_status_capture_return_stays_safe() {
        let source = "\
#!/bin/bash
run() {
  make install ||
  {
    \\typeset ret=$?
    warn failed
    return $ret
  }
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$ret"
            })
            .expect("expected return status argument fact");

        assert!(safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn helper_initialized_bindings_stay_safe_when_all_callers_provide_distinct_values() {
        let source = "\
#!/bin/bash
init_flag_a() {
  flag=-a
}

init_flag_b() {
  flag=-b
}

render() {
  printf '%s\\n' ${flag}
}

safe_path_a() {
  init_flag_a
  render
}

safe_path_b() {
  init_flag_b
  render
}

safe_path_a
safe_path_b
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "${flag}"
            })
            .expect("expected multi-caller helper-derived argument fact");

        assert!(safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn top_level_helper_initializers_refine_later_helper_calls() {
        let source = "\
#!/bin/bash
start_reader() {
  sleep 1 &
  tarpid=$!
}

read_disc() {
  grep $tarpid /dev/null
}

tarpid=\"\"
mode=$1
if [[ $mode == read ]]; then
  start_reader
fi
if [[ $mode == read ]]; then
  read_disc
fi
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$tarpid"
            })
            .expect("expected helper command argument fact");

        assert!(safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn conditional_top_level_helper_initializers_do_not_refine_unconditional_helper_calls() {
        let source = "\
#!/bin/bash
start_reader() {
  sleep 1 &
  tarpid=$!
}

read_disc() {
  grep $tarpid /dev/null
}

tarpid=\"\"
mode=$1
if [[ $mode == read ]]; then
  start_reader
fi
read_disc
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$tarpid"
            })
            .expect("expected helper command argument fact");

        assert!(!safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn imported_bindings_stay_unsafe_without_known_values() {
        let source = "\
#!/bin/bash
printf '%s\\n' $pkgname
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build_with_options(
            &output.file,
            source,
            &indexer,
            SemanticBuildOptions {
                file_entry_contract: Some(FileContract {
                    required_reads: Vec::new(),
                    provided_bindings: vec![ProvidedBinding::new(
                        Name::from("pkgname"),
                        ProvidedBindingKind::Variable,
                        ContractCertainty::Definite,
                    )],
                    provided_functions: Vec::new(),
                    externally_consumed_bindings: false,
                    externally_consumed_binding_names: Vec::new(),
                    externally_consumed_binding_prefixes: Vec::new(),
                }),
                ..SemanticBuildOptions::default()
            },
        );
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "$pkgname"
            })
            .expect("expected imported command argument fact");

        assert!(!safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn quoted_replacement_operands_stay_safe_in_argv_context() {
        let source = "\
#!/bin/bash
bash ${debug:+\"-x\"} script
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "${debug:+\"-x\"}"
            })
            .expect("expected replacement-operator command argument fact");

        assert!(safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn quoted_replacement_operands_with_spaces_stay_safe_in_argv_context() {
        let source = "\
#!/bin/bash
printf '%s\\n' ${debug:+\"a b\"}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "${debug:+\"a b\"}"
            })
            .expect("expected replacement-operator command argument fact");

        assert!(safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn static_literal_slice_bindings_stay_safe_in_argv_context() {
        let source = "\
#!/bin/bash
if true; then
  sig=RS256
else
  sig=ES512
fi
openssl dgst -sha${sig:2} payload
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "-sha${sig:2}"
            })
            .expect("expected sliced command argument fact");

        assert!(safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn overly_negative_static_slice_offset_stays_unsafe() {
        assert!(!static_slice_result_is_safe(
            "RS256",
            -20,
            None,
            SafeValueQuery::Argv
        ));
    }

    #[test]
    fn dynamic_slice_bindings_stay_unsafe_in_argv_context() {
        let source = "\
#!/bin/bash
sig=$1
openssl dgst -sha${sig:2} payload
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "-sha${sig:2}"
            })
            .expect("expected sliced command argument fact");

        assert!(!safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn backgrounded_exit_like_definitions_do_not_block_safe_bindings() {
        let source = "\
#!/bin/sh
SAFE=foo
Exit() { exit 0; } &
Exit
echo /tmp/$SAFE
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "/tmp/$SAFE"
            })
            .expect("expected mixed path command argument");

        assert!(safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn exhaustive_safe_bindings_override_conservative_maybe_uninitialized_refs() {
        let source = "\
#!/bin/bash
if [ \"$ARCH\" = \"i386\" ]; then
  LIBDIRSUFFIX=\"\"
elif [ \"$ARCH\" = \"x86_64\" ]; then
  LIBDIRSUFFIX=\"64\"
else
  LIBDIRSUFFIX=\"\"
fi

TARGET=$ARCH-linux
VERSION=${TARGET}_$(date +%s)
if [ ! -r /pkg/usr/lib${LIBDIRSUFFIX}/gcc/${TARGET}/${VERSION}/specs ]; then
  :
fi
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source)
                        == "/pkg/usr/lib${LIBDIRSUFFIX}/gcc/${TARGET}/${VERSION}/specs"
            })
            .expect("expected mixed path command argument");
        let (part, part_span) = word_fact
            .parts_with_spans()
            .find(|(_, span)| span.slice(source) == "${LIBDIRSUFFIX}")
            .expect("expected LIBDIRSUFFIX part");
        let name = Name::from("LIBDIRSUFFIX");
        let bindings = safe_values.safe_bindings_for_name(&name, part_span);

        assert!(
            safe_values.bindings_cover_all_paths_to_reference(&bindings, &name, part_span),
            "expected exhaustive branch ladder to cover all paths"
        );

        safe_values.override_uninitialized_reference_certainty(
            part_span,
            UninitializedCertainty::Possible,
        );

        assert!(safe_values.part_is_safe(part, part_span, SafeValueQuery::Argv));
    }

    #[test]
    fn exit_like_function_calls_invalidate_prior_and_later_safe_bindings() {
        let source = "\
#!/bin/sh
OPTION_BINARY_FILE=\"../lynis\"
Exit() { exit 0; }
Exit
OPENBSD_CONTENTS=\"openbsd/+CONTENTS\"
FIND=$(sh -n ${OPTION_BINARY_FILE} ; echo $?)
echo x >> ${OPENBSD_CONTENTS}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);
        let exit_header = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(name, _)| name.as_str() == "Exit")
            })
            .expect("expected Exit function header");

        let nested_argument = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.is_nested_word_command()
                    && fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "${OPTION_BINARY_FILE}"
            })
            .expect("expected nested command argument fact");
        let redirect_target = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context()
                    == Some(ExpansionContext::RedirectTarget(RedirectKind::Append))
                    && fact.span().slice(source) == "${OPENBSD_CONTENTS}"
            })
            .expect("expected redirect target fact");

        assert!(function_has_terminal_exit(exit_header.function()));
        assert_eq!(exit_header.call_arity().zero_arg_call_spans().len(), 1);

        assert!(!safe_values.word_occurrence_is_safe(nested_argument, SafeValueQuery::Argv));
        assert!(
            !safe_values.word_occurrence_is_safe(redirect_target, SafeValueQuery::RedirectTarget)
        );
    }

    #[test]
    fn inline_returns_make_later_safe_bindings_unsafe() {
        let source = "\
#!/bin/sh
helper() {
  safe=foo
  return 0
  echo /tmp/$safe
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);

        let word_fact = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact.span().slice(source) == "/tmp/$safe"
            })
            .expect("expected mixed path command argument");

        assert!(!safe_values.word_occurrence_is_safe(word_fact, SafeValueQuery::Argv));
    }

    #[test]
    fn subshell_exit_does_not_make_function_terminal() {
        let source = "\
#!/bin/sh
helper() (
  exit 1
)
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let helper_header = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(name, _)| name.as_str() == "helper")
            })
            .expect("expected helper function header");

        assert!(!function_has_terminal_exit(helper_header.function()));
    }

    #[test]
    fn early_unconditional_exit_makes_function_terminal() {
        let source = "\
#!/bin/sh
helper() {
  exit 1
  :
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let helper_header = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(name, _)| name.as_str() == "helper")
            })
            .expect("expected helper function header");

        assert!(function_has_terminal_exit(helper_header.function()));
    }

    #[test]
    fn assigned_exit_makes_function_terminal() {
        let source = "\
#!/bin/sh
helper() {
  FOO=1 exit 1
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let helper_header = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(name, _)| name.as_str() == "helper")
            })
            .expect("expected helper function header");

        assert!(function_has_terminal_exit(helper_header.function()));
    }

    #[test]
    fn negated_exit_makes_function_terminal() {
        let source = "\
#!/bin/sh
helper() {
  ! exit 1
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let helper_header = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(name, _)| name.as_str() == "helper")
            })
            .expect("expected helper function header");

        assert!(function_has_terminal_exit(helper_header.function()));
    }

    #[test]
    fn extra_arg_exit_makes_function_terminal() {
        let source = "\
#!/bin/sh
helper() {
  exit 1 2
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let helper_header = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(name, _)| name.as_str() == "helper")
            })
            .expect("expected helper function header");

        assert!(function_has_terminal_exit(helper_header.function()));
    }

    #[test]
    fn all_if_branches_exiting_make_function_terminal() {
        let source = "\
#!/bin/sh
helper() {
  if [ \"$SKIP\" ]; then
    exit 0
  else
    exit 1
  fi
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let helper_header = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(name, _)| name.as_str() == "helper")
            })
            .expect("expected helper function header");

        assert!(function_has_terminal_exit(helper_header.function()));
    }

    #[test]
    fn conditional_return_before_exit_does_not_make_function_terminal() {
        let source = "\
#!/bin/sh
helper() {
  if [ \"$SKIP\" ]; then
    return 0
  fi
  exit 1
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let helper_header = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(name, _)| name.as_str() == "helper")
            })
            .expect("expected helper function header");

        assert!(!function_has_terminal_exit(helper_header.function()));
    }

    #[test]
    fn conditional_exit_before_exit_makes_function_terminal() {
        let source = "\
#!/bin/sh
helper() {
  if [ \"$SKIP\" ]; then
    exit 1
  fi
  exit 0
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let helper_header = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(name, _)| name.as_str() == "helper")
            })
            .expect("expected helper function header");

        assert!(function_has_terminal_exit(helper_header.function()));
    }

    #[test]
    fn return_before_exit_does_not_make_function_terminal() {
        let source = "\
#!/bin/sh
helper() {
  return 0
  exit 1
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let helper_header = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(name, _)| name.as_str() == "helper")
            })
            .expect("expected helper function header");

        assert!(!function_has_terminal_exit(helper_header.function()));
    }

    #[test]
    fn all_if_branches_returning_do_not_make_function_terminal() {
        let source = "\
#!/bin/sh
helper() {
  if [ \"$SKIP\" ]; then
    return 0
  else
    return 1
  fi
  exit 0
}
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let helper_header = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(name, _)| name.as_str() == "helper")
            })
            .expect("expected helper function header");

        assert!(!function_has_terminal_exit(helper_header.function()));
    }

    #[test]
    fn later_top_level_exit_helpers_block_same_function_bindings() {
        let source = "\
#!/bin/sh
SAFE=foo
wrapper() {
  Exit
  echo /tmp/$SAFE
}
Exit() { exit 0; }
";
        let output = Parser::new(source).parse().unwrap();
        let indexer = Indexer::new(source, &output);
        let semantic = LinterSemanticArtifacts::build(&output.file, source, &indexer);
        let analysis = semantic.semantic().analysis();
        let facts = LinterFacts::build(&output.file, source, &semantic, &indexer);
        let mut safe_values = SafeValueIndex::build(semantic.semantic(), &analysis, &facts, source);
        let target = facts
            .words()
            .word_facts()
            .iter()
            .find(|fact| {
                fact.expansion_context() == Some(ExpansionContext::CommandArgument)
                    && fact
                        .parts_with_spans()
                        .any(|(_, span)| span.slice(source) == "$SAFE")
            })
            .expect("expected same-function argument fact");

        assert!(!safe_values.word_occurrence_is_safe(target, SafeValueQuery::Argv));
    }
}
