shuck-semantic 0.0.19

Semantic analysis model for shell scripts with scopes, bindings, and dataflow
Documentation
use rustc_hash::FxHashMap;
use shuck_ast::Name;
use shuck_parser::ShellProfile;
use std::path::{Path, PathBuf};

use crate::SourcePathResolver;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ContractCertainty {
    Definite,
    Possible,
}

impl ContractCertainty {
    pub(crate) fn merge_same_site(self, other: Self) -> Self {
        match (self, other) {
            (Self::Definite, Self::Definite) => Self::Definite,
            _ => Self::Possible,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ProvidedBindingKind {
    Variable,
    Function,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ProvidedBinding {
    pub name: Name,
    pub kind: ProvidedBindingKind,
    pub certainty: ContractCertainty,
}

impl ProvidedBinding {
    pub fn new(name: Name, kind: ProvidedBindingKind, certainty: ContractCertainty) -> Self {
        Self {
            name,
            kind,
            certainty,
        }
    }
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FunctionContract {
    pub name: Name,
    pub required_reads: Vec<Name>,
    pub provided_bindings: Vec<ProvidedBinding>,
    pub origin_paths: Vec<PathBuf>,
}

impl FunctionContract {
    pub fn new(name: Name) -> Self {
        Self {
            name,
            required_reads: Vec::new(),
            provided_bindings: Vec::new(),
            origin_paths: Vec::new(),
        }
    }

    pub fn add_required_read(&mut self, name: Name) {
        if !self.required_reads.contains(&name) {
            self.required_reads.push(name);
        }
    }

    pub fn add_provided_binding(&mut self, binding: ProvidedBinding) {
        let mut merged = false;
        for existing in &mut self.provided_bindings {
            if existing.name == binding.name && existing.kind == binding.kind {
                existing.certainty = existing.certainty.merge_same_site(binding.certainty);
                merged = true;
                break;
            }
        }

        if !merged {
            self.provided_bindings.push(binding);
        }
    }

    pub fn add_origin_path(&mut self, path: PathBuf) {
        if !self.origin_paths.contains(&path) {
            self.origin_paths.push(path);
        }
    }

    pub(crate) fn merge_candidate_contracts(contracts: &[Self]) -> Option<Self> {
        let first = contracts.first()?;
        let mut merged = Self::new(first.name.clone());
        let total = contracts.len();
        let mut provided_counts: FxHashMap<(Name, ProvidedBindingKind), (usize, bool)> =
            FxHashMap::default();

        for contract in contracts {
            for name in &contract.required_reads {
                merged.add_required_read(name.clone());
            }
            for binding in &contract.provided_bindings {
                let entry = provided_counts
                    .entry((binding.name.clone(), binding.kind))
                    .or_insert((0, true));
                entry.0 += 1;
                entry.1 &= binding.certainty == ContractCertainty::Definite;
            }
            for path in &contract.origin_paths {
                merged.add_origin_path(path.clone());
            }
        }

        for ((name, kind), (present_count, all_definite)) in provided_counts {
            let certainty = if present_count == total && all_definite {
                ContractCertainty::Definite
            } else {
                ContractCertainty::Possible
            };
            merged.add_provided_binding(ProvidedBinding::new(name, kind, certainty));
        }

        Some(merged)
    }
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FileContract {
    pub required_reads: Vec<Name>,
    pub provided_bindings: Vec<ProvidedBinding>,
    pub provided_functions: Vec<FunctionContract>,
}

impl FileContract {
    pub fn add_required_read(&mut self, name: Name) {
        if !self.required_reads.contains(&name) {
            self.required_reads.push(name);
        }
    }

    pub fn add_provided_binding(&mut self, binding: ProvidedBinding) {
        let mut merged = false;
        for existing in &mut self.provided_bindings {
            if existing.name == binding.name && existing.kind == binding.kind {
                existing.certainty = existing.certainty.merge_same_site(binding.certainty);
                merged = true;
                break;
            }
        }

        if !merged {
            self.provided_bindings.push(binding);
        }
    }

    pub fn add_provided_function(&mut self, function: FunctionContract) {
        let mut merged = false;
        for existing in &mut self.provided_functions {
            if existing.name == function.name {
                for name in &function.required_reads {
                    existing.add_required_read(name.clone());
                }
                for binding in &function.provided_bindings {
                    existing.add_provided_binding(binding.clone());
                }
                for path in &function.origin_paths {
                    existing.add_origin_path(path.clone());
                }
                merged = true;
                break;
            }
        }

        if !merged {
            self.provided_functions.push(function);
        }
    }

    pub(crate) fn merge_candidate_contracts(contracts: &[Self]) -> Self {
        let mut merged = Self::default();
        let total = contracts.len();
        let mut provided_counts: FxHashMap<(Name, ProvidedBindingKind), (usize, bool)> =
            FxHashMap::default();
        let mut function_contracts_by_name: FxHashMap<Name, Vec<FunctionContract>> =
            FxHashMap::default();

        for contract in contracts {
            for name in &contract.required_reads {
                merged.add_required_read(name.clone());
            }
            for binding in &contract.provided_bindings {
                let entry = provided_counts
                    .entry((binding.name.clone(), binding.kind))
                    .or_insert((0, true));
                entry.0 += 1;
                entry.1 &= binding.certainty == ContractCertainty::Definite;
            }
            for function in &contract.provided_functions {
                function_contracts_by_name
                    .entry(function.name.clone())
                    .or_default()
                    .push(function.clone());
            }
        }

        for ((name, kind), (present_count, all_definite)) in provided_counts {
            let certainty = if present_count == total && all_definite {
                ContractCertainty::Definite
            } else {
                ContractCertainty::Possible
            };
            merged.add_provided_binding(ProvidedBinding::new(name, kind, certainty));
        }

        for functions in function_contracts_by_name.into_values() {
            if functions.len() != total {
                continue;
            }
            if let Some(function) = FunctionContract::merge_candidate_contracts(&functions) {
                merged.add_provided_function(function);
            }
        }

        merged
    }
}

#[derive(Clone, Default)]
pub struct SemanticBuildOptions<'a> {
    pub source_path: Option<&'a Path>,
    pub source_path_resolver: Option<&'a (dyn SourcePathResolver + Send + Sync)>,
    pub file_entry_contract: Option<FileContract>,
    pub analyzed_paths: Option<&'a rustc_hash::FxHashSet<PathBuf>>,
    pub shell_profile: Option<ShellProfile>,
}