use std::collections::BTreeSet;
use std::path::Path;
use shuck_ast::{Name, NormalizedCommand};
use shuck_parser::ShellProfile;
use shuck_semantic::{FileContract, FileEntryContractCollector, FileEntryContractCollectorFactory};
use crate::ShellDialect;
mod path;
mod signals;
mod source_scan;
mod sourced_runtime;
#[cfg(test)]
mod tests;
mod zsh_caller_arrays;
mod zsh_config;
mod zsh_module_metadata;
mod zsh_paths;
mod zsh_runtime;
struct AmbientContractProvider {
matches: fn(&AmbientContractCollector<'_>, ShellDialect) -> bool,
build: fn(&AmbientContractCollector<'_>, ShellDialect) -> FileContract,
}
pub(crate) struct AmbientContractCollector<'a> {
source: &'a str,
shell: ShellDialect,
signals: signals::AmbientSignals<'a>,
completion_initializer_invoked: bool,
caller_scoped_array_length_names: BTreeSet<Name>,
}
pub(crate) struct AmbientContractCollectorFactory;
impl FileEntryContractCollectorFactory for AmbientContractCollectorFactory {
fn collector_for_file<'a>(
&self,
source: &'a str,
path: Option<&'a Path>,
shell_profile: &ShellProfile,
) -> Option<Box<dyn FileEntryContractCollector + 'a>> {
Some(Box::new(AmbientContractCollector::new(
source,
path,
shell_dialect_from_profile(shell_profile),
)))
}
}
impl<'a> AmbientContractCollector<'a> {
pub(crate) fn new(source: &'a str, path: Option<&'a Path>, shell: ShellDialect) -> Self {
let mut caller_scoped_array_length_names = BTreeSet::new();
if shell == ShellDialect::Zsh {
zsh_caller_arrays::collect_caller_scoped_array_length_names_from_source(
source,
&mut caller_scoped_array_length_names,
);
}
Self {
source,
shell,
signals: signals::AmbientSignals::new(source, path),
completion_initializer_invoked: false,
caller_scoped_array_length_names,
}
}
fn file_entry_contract(&self) -> Option<FileContract> {
self.signals.path()?;
let mut merged = FileContract::default();
let mut matched = false;
for provider in providers() {
if (provider.matches)(self, self.shell) {
matched = true;
merge_contract(&mut merged, (provider.build)(self, self.shell));
}
}
matched.then_some(merged)
}
fn source_signals(&self) -> &signals::SourceSignals<'a> {
self.signals.source()
}
fn path_signals(&self) -> &signals::PathSignals {
self.signals
.path()
.expect("ambient contracts are only built for path-backed files")
}
}
impl FileEntryContractCollector for AmbientContractCollector<'_> {
fn observe_simple_command(&mut self, command: &NormalizedCommand<'_>) {
self.completion_initializer_invoked |=
sourced_runtime::normalized_command_invokes_completion_initializer(
command,
self.source,
);
if self.shell == ShellDialect::Zsh {
for word in command.body_args() {
zsh_caller_arrays::collect_caller_scoped_array_length_names(
word,
&mut self.caller_scoped_array_length_names,
);
}
}
}
fn finish(&self) -> Option<FileContract> {
self.file_entry_contract()
}
}
fn providers() -> &'static [AmbientContractProvider] {
&[
AmbientContractProvider {
matches: sourced_runtime::matches_sourced_runtime_contract,
build: sourced_runtime::build_sourced_runtime_contract,
},
AmbientContractProvider {
matches: zsh_runtime::matches_zsh_ambient_runtime_contract,
build: zsh_runtime::build_zsh_ambient_runtime_contract,
},
AmbientContractProvider {
matches: zsh_config::matches_zsh_config_contract,
build: zsh_config::build_zsh_config_contract,
},
AmbientContractProvider {
matches: zsh_module_metadata::matches_zsh_module_metadata_contract,
build: zsh_module_metadata::build_zsh_module_metadata_contract,
},
AmbientContractProvider {
matches: zsh_caller_arrays::matches_zsh_caller_scoped_array_contract,
build: zsh_caller_arrays::build_zsh_caller_scoped_array_contract,
},
]
}
fn merge_contract(merged: &mut FileContract, contract: FileContract) {
merged.externally_consumed_bindings |= contract.externally_consumed_bindings;
for name in contract.required_reads {
merged.add_required_read(name);
}
for name in contract.externally_consumed_binding_names {
merged.add_externally_consumed_binding_name(name);
}
for binding in contract.provided_bindings {
merged.add_provided_binding(binding);
}
for function in contract.provided_functions {
merged.add_provided_function(function);
}
for prefix in contract.externally_consumed_binding_prefixes {
merged.add_externally_consumed_binding_prefix(prefix);
}
}
fn shell_dialect_from_profile(profile: &ShellProfile) -> ShellDialect {
match profile.dialect {
shuck_parser::ShellDialect::Zsh => ShellDialect::Zsh,
shuck_parser::ShellDialect::Mksh => ShellDialect::Mksh,
shuck_parser::ShellDialect::Posix => ShellDialect::Sh,
shuck_parser::ShellDialect::Bash => ShellDialect::Bash,
}
}