use super::*;
#[derive(Debug, Clone)]
pub struct FunctionHeaderFact<'a> {
command_id: CommandId,
function: &'a FunctionDef,
binding_id: Option<BindingId>,
scope_id: Option<ScopeId>,
call_arity: FunctionCallArityFacts,
}
impl<'a> FunctionHeaderFact<'a> {
pub fn command_id(&self) -> CommandId {
self.command_id
}
pub fn function(&self) -> &'a FunctionDef {
self.function
}
pub fn static_name_entry(&self) -> Option<(&'a Name, Span)> {
self.function.static_name_entries().next()
}
pub fn binding_id(&self) -> Option<BindingId> {
self.binding_id
}
pub fn function_scope(&self) -> Option<ScopeId> {
self.scope_id
}
pub fn call_arity(&self) -> &FunctionCallArityFacts {
&self.call_arity
}
pub fn function_span_in_source(&self, source: &str) -> Span {
trim_trailing_whitespace_span(self.function.span, source)
}
pub fn span_in_source(&self, source: &str) -> Span {
trim_trailing_whitespace_span(self.function.header.span(), source)
}
pub fn uses_function_keyword(&self) -> bool {
self.function.uses_function_keyword()
}
pub fn has_trailing_parens(&self) -> bool {
self.function.has_trailing_parens()
}
pub fn function_keyword_span(&self) -> Option<Span> {
self.function.header.function_keyword_span
}
pub fn trailing_parens_span(&self) -> Option<Span> {
self.function.header.trailing_parens_span
}
}
#[derive(Debug, Clone, Default)]
pub struct FunctionCallArityFacts {
call_count: usize,
min_arg_count: usize,
max_arg_count: usize,
zero_arg_call_spans: Vec<Span>,
zero_arg_diagnostic_spans: Vec<Span>,
}
impl FunctionCallArityFacts {
pub fn call_count(&self) -> usize {
self.call_count
}
pub fn min_arg_count(&self) -> Option<usize> {
(self.call_count != 0).then_some(self.min_arg_count)
}
pub fn max_arg_count(&self) -> Option<usize> {
(self.call_count != 0).then_some(self.max_arg_count)
}
pub fn called_only_without_args(&self) -> bool {
self.call_count != 0 && self.max_arg_count == 0
}
pub fn zero_arg_call_spans(&self) -> &[Span] {
&self.zero_arg_call_spans
}
pub fn zero_arg_diagnostic_spans(&self) -> &[Span] {
&self.zero_arg_diagnostic_spans
}
fn record_call(&mut self, arg_count: usize, name_span: Span, diagnostic_span: Span) {
if self.call_count == 0 {
self.min_arg_count = arg_count;
self.max_arg_count = arg_count;
} else {
self.min_arg_count = self.min_arg_count.min(arg_count);
self.max_arg_count = self.max_arg_count.max(arg_count);
}
if arg_count == 0 {
self.zero_arg_call_spans.push(name_span);
self.zero_arg_diagnostic_spans.push(diagnostic_span);
}
self.call_count += 1;
}
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct FunctionCliDispatchFacts {
exported_from_case_cli: bool,
dispatcher_span: Option<Span>,
}
impl FunctionCliDispatchFacts {
pub fn exported_from_case_cli(self) -> bool {
self.exported_from_case_cli
}
#[cfg(test)]
pub fn dispatcher_span(self) -> Option<Span> {
self.dispatcher_span
}
fn record_dispatch(&mut self, span: Span) {
self.exported_from_case_cli = true;
self.dispatcher_span.get_or_insert(span);
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct FunctionFactInput<'a> {
pub(crate) command_id: CommandId,
pub(crate) function: &'a FunctionDef,
}
#[cfg_attr(shuck_profiling, inline(never))]
pub(crate) fn build_function_header_facts<'a>(
semantic: &SemanticModel,
semantic_analysis: &SemanticAnalysis<'_>,
functions: &[FunctionFactInput<'a>],
commands: &[CommandFact<'a>],
command_fact_indices_by_id: &[Option<usize>],
source: &str,
) -> Vec<FunctionHeaderFact<'a>> {
let call_arity_by_binding = build_function_call_arity_facts(
semantic_analysis,
&functions
.iter()
.map(|input| input.function)
.collect::<Vec<_>>(),
commands,
command_fact_indices_by_id,
source,
);
functions
.iter()
.copied()
.map(|input| {
let binding_id = semantic.function_definition_binding_for_command_span(
semantic.command_span(input.command_id),
);
let scope_id = binding_id
.and_then(|binding_id| semantic_analysis.function_scope_for_binding(binding_id));
let call_arity = binding_id
.and_then(|binding_id| call_arity_by_binding.get(&binding_id).cloned())
.unwrap_or_default();
FunctionHeaderFact {
command_id: input.command_id,
function: input.function,
binding_id,
scope_id,
call_arity,
}
})
.collect()
}
pub(crate) fn build_function_doc_content_facts<'a>(
semantic: &SemanticModel,
headers: &[FunctionHeaderFact<'a>],
commands: &[CommandFact<'a>],
positional_parameter_facts: &FxHashMap<ScopeId, FunctionPositionalParameterFacts>,
source: &str,
line_index: &LineIndex,
comment_index: &CommentIndex,
) -> Vec<FunctionDocContentFact> {
let mut summaries = build_function_doc_body_summaries(semantic, commands, source);
let scopes_using_any_positional_parameters = build_function_scopes_using_positional_parameters(
semantic,
commands,
positional_parameter_facts,
);
headers
.iter()
.filter_map(|header| {
let (name, name_span) = header.static_name_entry()?;
let function_scope = header.function_scope()?;
let function_span = header.function_span_in_source(source);
let comment_block = leading_function_doc_block(
source,
line_index,
comment_index,
function_span.start.line,
);
let summary = summaries.remove(&function_scope).unwrap_or_default();
let uses_positional_parameters = positional_parameter_facts
.get(&function_scope)
.is_some_and(|facts| facts.uses_positional_parameters());
let uses_any_positional_parameters =
scopes_using_any_positional_parameters.contains(&function_scope);
Some(FunctionDocContentFact::new(
name,
name_span,
span_line_count(header.function().body.span),
comment_block.map(|block| block.span),
comment_block
.map(|block| block.sections)
.unwrap_or_default(),
FunctionDocBodyBehavior::new(
summary.uses_global_variables,
uses_positional_parameters,
uses_any_positional_parameters,
summary.writes_stdout,
summary.has_explicit_return,
),
))
})
.collect()
}
#[derive(Debug, Clone, Copy, Default)]
struct FunctionDocBodySummary {
uses_global_variables: bool,
writes_stdout: bool,
has_explicit_return: bool,
}
fn build_function_doc_body_summaries(
semantic: &SemanticModel,
commands: &[CommandFact<'_>],
source: &str,
) -> FxHashMap<ScopeId, FunctionDocBodySummary> {
let mut summaries = FxHashMap::<ScopeId, FunctionDocBodySummary>::default();
let reference_bindings = reference_binding_index(semantic);
let global_reference_skip_spans = function_doc_global_reference_skip_spans(commands, source);
for reference in semantic.references() {
if !reference_can_represent_global(reference, &global_reference_skip_spans) {
continue;
}
let Some(function_scope) = semantic.enclosing_function_scope(reference.scope) else {
continue;
};
if reference_is_global_variable_use(
semantic,
reference,
function_scope,
&reference_bindings,
) {
summaries
.entry(function_scope)
.or_default()
.uses_global_variables = true;
}
}
for binding in semantic.bindings() {
let Some(function_scope) = semantic.enclosing_function_scope(binding.scope) else {
continue;
};
if !binding_can_represent_global_write(semantic, binding, function_scope) {
continue;
}
summaries
.entry(function_scope)
.or_default()
.uses_global_variables = true;
}
for command in commands {
let Some(function_scope) = command.enclosing_function_scope() else {
continue;
};
let summary = summaries.entry(function_scope).or_default();
summary.writes_stdout |= function_body_command_writes_stdout(command, source);
summary.has_explicit_return |=
!command.is_nested_word_command() && command_has_explicit_return(command);
}
summaries
}
fn reference_binding_index(semantic: &SemanticModel) -> FxHashMap<ReferenceId, BindingId> {
let mut index = FxHashMap::default();
for binding in semantic.bindings() {
for reference in &binding.references {
index.insert(*reference, binding.id);
}
}
index
}
fn reference_can_represent_global(reference: &Reference, skip_spans: &FxHashSet<FactSpan>) -> bool {
!matches!(reference.kind, ReferenceKind::DeclarationName)
&& !is_special_or_positional_parameter_name(reference.name.as_str())
&& !skip_spans.contains(&FactSpan::new(reference.span))
}
fn reference_is_global_variable_use(
semantic: &SemanticModel,
reference: &Reference,
function_scope: ScopeId,
reference_bindings: &FxHashMap<ReferenceId, BindingId>,
) -> bool {
let Some(binding_id) = reference_bindings.get(&reference.id).copied() else {
return true;
};
let binding = semantic.binding(binding_id);
if matches!(binding.kind, BindingKind::FunctionDefinition) {
return false;
}
!binding.attributes.contains(BindingAttributes::LOCAL)
|| semantic.enclosing_function_scope(binding.scope) != Some(function_scope)
}
fn binding_can_represent_global_write(
semantic: &SemanticModel,
binding: &Binding,
function_scope: ScopeId,
) -> bool {
!binding.attributes.contains(BindingAttributes::LOCAL)
&& !matches!(
binding.kind,
BindingKind::FunctionDefinition | BindingKind::Imported
)
&& !is_special_or_positional_parameter_name(binding.name.as_str())
&& !binding_targets_prior_local(semantic, binding, function_scope)
}
fn binding_targets_prior_local(
semantic: &SemanticModel,
binding: &Binding,
function_scope: ScopeId,
) -> bool {
semantic
.bindings_for(&binding.name)
.iter()
.any(|candidate| {
let candidate = semantic.binding(*candidate);
candidate.id != binding.id
&& candidate.span.start.offset < binding.span.start.offset
&& candidate.attributes.contains(BindingAttributes::LOCAL)
&& semantic.enclosing_function_scope(candidate.scope) == Some(function_scope)
})
}
fn is_special_or_positional_parameter_name(name: &str) -> bool {
name.bytes().all(|byte| byte.is_ascii_digit())
|| matches!(name, "@" | "*" | "#" | "?" | "-" | "$" | "!")
}
fn function_body_command_writes_stdout(command: &CommandFact<'_>, source: &str) -> bool {
if command.is_nested_word_command() || command_stdout_is_redirected(command) {
return false;
}
command.normalized().effective_basename_is("echo")
|| (command.normalized().effective_basename_is("printf")
&& (!command.effective_name_is("printf")
|| !printf_assigns_to_variable(command, source)))
}
fn printf_assigns_to_variable(command: &CommandFact<'_>, source: &str) -> bool {
match command
.body_args()
.first()
.and_then(|word| static_word_text(word, source))
{
Some(text) if text == "-v" => command.body_args().len() >= 2,
Some(text) if text.starts_with("-v") && text.len() > 2 => true,
_ => false,
}
}
fn command_stdout_is_redirected(command: &CommandFact<'_>) -> bool {
command.redirects().iter().any(redirect_redirects_stdout)
}
fn redirect_redirects_stdout(redirect: &Redirect) -> bool {
if redirect.fd_var.is_some() {
return false;
}
match redirect.kind {
RedirectKind::Output
| RedirectKind::Clobber
| RedirectKind::Append
| RedirectKind::DupOutput => redirect.fd.unwrap_or(1) == 1,
RedirectKind::ReadWrite => redirect.fd == Some(1),
RedirectKind::OutputBoth => true,
RedirectKind::Input
| RedirectKind::HereDoc
| RedirectKind::HereDocStrip
| RedirectKind::HereString
| RedirectKind::DupInput => false,
}
}
fn command_has_explicit_return(command: &CommandFact<'_>) -> bool {
matches!(
command.command(),
Command::Builtin(BuiltinCommand::Return(command)) if command.code.is_some()
)
}
fn function_doc_global_reference_skip_spans(
commands: &[CommandFact<'_>],
source: &str,
) -> FxHashSet<FactSpan> {
commands
.iter()
.filter_map(|command| printf_assignment_target_span(command, source))
.map(FactSpan::new)
.collect()
}
fn printf_assignment_target_span(command: &CommandFact<'_>, source: &str) -> Option<Span> {
if !command.effective_name_is("printf") {
return None;
}
let args = command.body_args();
match args.first().and_then(|word| static_word_text(word, source)) {
Some(text) if text == "-v" => args.get(1).map(|word| word.span),
Some(text) if text.starts_with("-v") && text.len() > 2 => Some(args[0].span),
_ => None,
}
}
#[derive(Debug, Clone, Copy)]
struct LeadingFunctionDocBlock {
span: Span,
sections: FunctionDocSections,
}
fn leading_function_doc_block(
source: &str,
line_index: &LineIndex,
comment_index: &CommentIndex,
function_start_line: usize,
) -> Option<LeadingFunctionDocBlock> {
let mut line = function_start_line.checked_sub(1)?;
let mut block_start: Option<Position> = None;
let mut block_end: Option<Position> = None;
let mut sections = FunctionDocSections::default();
let mut has_doc_comment = false;
loop {
let Some(comment) = comment_index
.comments_on_line(line)
.iter()
.find(|comment| comment.is_own_line)
else {
break;
};
let line_start = usize::from(line_index.line_start(line)?);
let line_end = usize::from(line_index.line_range(line, source)?.end()).min(source.len());
let comment_start = usize::from(comment.range.start());
let comment_end = usize::from(comment.range.end()).min(line_end);
if comment_start < line_start || comment_start >= comment_end {
break;
}
let comment_text = &source[comment_start..comment_end];
let Some(comment_body) = function_doc_comment_body(comment_text) else {
break;
};
let line_start_position = Position {
line,
column: 1,
offset: line_start,
};
let comment_start_position =
line_start_position.advanced_by(&source[line_start..comment_start]);
let comment_end_position =
line_start_position.advanced_by(&source[line_start..comment_end]);
block_start = Some(comment_start_position);
block_end.get_or_insert(comment_end_position);
if is_function_doc_comment_body(comment_body) {
has_doc_comment = true;
sections.record_comment_body(comment_body);
}
let Some(previous_line) = line.checked_sub(1) else {
break;
};
line = previous_line;
}
if !has_doc_comment {
return None;
}
Some(LeadingFunctionDocBlock {
span: Span::from_positions(block_start?, block_end?),
sections,
})
}
fn function_doc_comment_body(comment_text: &str) -> Option<&str> {
let body = comment_text.strip_prefix('#')?.trim();
(!body.starts_with('!')).then_some(body)
}
fn is_function_doc_comment_body(body: &str) -> bool {
if body.is_empty() {
return false;
}
let lower = body.to_ascii_lowercase();
!lower.starts_with("shellcheck ") && !lower.starts_with("shuck:")
}
fn build_function_scopes_using_positional_parameters(
semantic: &SemanticModel,
commands: &[CommandFact<'_>],
positional_parameter_facts: &FxHashMap<ScopeId, FunctionPositionalParameterFacts>,
) -> FxHashSet<ScopeId> {
let local_reset_offsets_by_scope = local_positional_reset_offsets_by_scope(semantic, commands);
let mut scopes = positional_parameter_facts
.iter()
.filter_map(|(&scope, facts)| facts.uses_positional_parameters().then_some(scope))
.collect::<FxHashSet<_>>();
for reference in semantic.references() {
if !semantic.is_guarded_parameter_reference(reference.id)
|| !is_positional_parameter_reference_name(reference.name.as_str())
{
continue;
}
if reference_has_local_positional_reset(
semantic,
reference.scope,
reference.span.start.offset,
&local_reset_offsets_by_scope,
) {
continue;
}
if let Some(scope) = semantic.enclosing_function_scope(reference.scope) {
scopes.insert(scope);
}
}
scopes
}
fn is_positional_parameter_reference_name(name: &str) -> bool {
matches!(name, "@" | "*" | "#")
|| (!name.is_empty() && name != "0" && name.bytes().all(|byte| byte.is_ascii_digit()))
}
fn span_line_count(span: Span) -> usize {
span.end.line.saturating_sub(span.start.line) + 1
}
#[cfg_attr(shuck_profiling, inline(never))]
pub(crate) fn build_function_cli_dispatch_facts(
dispatches: &[CaseCliDispatch],
) -> FxHashMap<ScopeId, FunctionCliDispatchFacts> {
let mut facts = FxHashMap::<ScopeId, FunctionCliDispatchFacts>::default();
for dispatch in dispatches {
facts
.entry(dispatch.function_scope())
.or_default()
.record_dispatch(dispatch.dispatcher_span());
}
facts
}
pub(crate) fn build_function_parameter_fallback_spans(
commands: &[CommandFact<'_>],
command_fact_indices_by_id: &[Option<usize>],
structural_command_ids: &[CommandId],
source: &str,
) -> Vec<Span> {
let structural_commands = structural_command_ids
.iter()
.copied()
.filter_map(|id| {
command_fact_indices_by_id
.get(id.index())
.copied()
.flatten()
.and_then(|index| commands.get(index))
})
.collect::<Vec<_>>();
structural_commands
.windows(2)
.filter_map(|pair| function_parameter_fallback_span(pair, source))
.chain(
commands
.iter()
.filter_map(named_coproc_subshell_fallback_span),
)
.collect()
}
pub(crate) fn build_completion_registered_function_command_flags(
commands: &[CommandFact<'_>],
registered_scopes: &FxHashSet<ScopeId>,
) -> Vec<bool> {
let mut flags = vec![false; function_command_slot_count(commands)];
for command in commands {
flags[command.id().index()] = command
.enclosing_function_scope()
.is_some_and(|scope| registered_scopes.contains(&scope));
}
flags
}
pub(crate) fn build_completion_registered_function_scopes(
semantic: &SemanticModel,
semantic_analysis: &SemanticAnalysis<'_>,
commands: &[CommandFact<'_>],
command_fact_indices_by_id: &[Option<usize>],
lists: &[ListFact<'_>],
source: &str,
) -> FxHashSet<ScopeId> {
let mut function_candidates = Vec::new();
function_candidates.resize_with(function_command_slot_count(commands), || None);
for command in commands {
function_candidates[command.id().index()] =
completion_registered_function_candidate(semantic, command);
}
let mut file_level_function_candidates = Vec::new();
file_level_function_candidates.resize_with(function_command_slot_count(commands), || None);
for command in commands {
file_level_function_candidates[command.id().index()] =
file_level_completion_function_candidate(semantic, command);
}
let top_level_candidate_scopes = function_candidates
.iter()
.flatten()
.map(|candidate| candidate.scope)
.collect::<FxHashSet<_>>();
let file_level_candidate_scopes = file_level_function_candidates
.iter()
.flatten()
.map(|candidate| candidate.scope)
.collect::<FxHashSet<_>>();
let mut scopes = FxHashSet::default();
for list in lists {
for (index, segment) in list.segments().iter().enumerate() {
let Some(candidate) = function_candidates[segment.command_id().index()].as_ref() else {
continue;
};
if list.segments()[index + 1..].iter().any(|later_segment| {
let later_command = command_fact(
commands,
command_fact_indices_by_id,
later_segment.command_id(),
);
is_unconditional_completion_registration(semantic, later_command)
&& command_registers_completion_function(later_command, source, &candidate.name)
}) {
scopes.insert(candidate.scope);
}
}
}
for list in lists {
for (index, segment) in list.segments().iter().enumerate() {
let Some(candidate) =
file_level_function_candidates[segment.command_id().index()].as_ref()
else {
continue;
};
if list.segments()[index + 1..].iter().any(|later_segment| {
let later_command = command_fact(
commands,
command_fact_indices_by_id,
later_segment.command_id(),
);
is_same_branch_completion_registration(semantic, later_command)
&& command_registers_completion_function(later_command, source, &candidate.name)
}) {
scopes.insert(candidate.scope);
}
}
}
for command in commands {
let Some(candidate) = file_level_function_candidates[command.id().index()].as_ref() else {
continue;
};
let Some(parent) = nearest_structural_parent_command(semantic, command.id()) else {
continue;
};
if commands.iter().any(|later_command| {
later_command.span().start.offset > command.span().start.offset
&& nearest_structural_parent_command(semantic, later_command.id()) == Some(parent)
&& same_structural_branch_between(
source,
command.span().end.offset,
later_command.span().start.offset,
)
&& is_same_branch_completion_registration(semantic, later_command)
&& command_registers_completion_function(later_command, source, &candidate.name)
}) {
scopes.insert(candidate.scope);
}
}
for command in commands {
let Some(candidate) = function_candidates[command.id().index()].as_ref() else {
continue;
};
if commands.iter().any(|later_command| {
is_unconditional_completion_registration(semantic, later_command)
&& later_command.effective_or_literal_name() == Some("compdef")
&& command_registers_completion_function(later_command, source, &candidate.name)
}) {
scopes.insert(candidate.scope);
}
}
for scope in semantic.scopes() {
if !top_level_candidate_scopes.contains(&scope.id) {
continue;
}
let ScopeKind::Function(FunctionScopeKind::Named(names)) = &scope.kind else {
continue;
};
if commands.iter().any(|command| {
is_unconditional_completion_registration(semantic, command)
&& command.effective_or_literal_name() == Some("compdef")
&& names.iter().any(|name| {
command_registers_completion_function(command, source, name.as_str())
})
}) {
scopes.insert(scope.id);
}
}
extend_completion_registered_function_scopes_through_helpers(
semantic,
semantic_analysis,
commands,
&file_level_candidate_scopes,
source,
&mut scopes,
);
scopes
}
pub(crate) fn extend_completion_registered_function_scopes_through_helpers(
semantic: &SemanticModel,
semantic_analysis: &SemanticAnalysis<'_>,
commands: &[CommandFact<'_>],
candidate_scopes: &FxHashSet<ScopeId>,
source: &str,
scopes: &mut FxHashSet<ScopeId>,
) {
let candidate_bindings = semantic_analysis
.function_bindings_by_scope()
.filter(|(scope, _)| candidate_scopes.contains(scope))
.flat_map(|(scope, bindings)| bindings.iter().map(move |binding| (scope, *binding)))
.collect::<Vec<_>>();
let mut changed = true;
while changed {
changed = false;
for (target_scope, binding_id) in &candidate_bindings {
if scopes.contains(target_scope) {
continue;
}
let name = semantic.binding(*binding_id).name.clone();
let called_from_completion_scope = semantic_analysis
.function_call_arity_sites(&name)
.any(|(site, resolved_binding)| {
resolved_binding == *binding_id
&& semantic_analysis
.enclosing_function_scope_at(site.name_span.start.offset)
.is_some_and(|caller_scope| scopes.contains(&caller_scope))
});
let referenced_by_completion_arguments = commands.iter().any(|command| {
command.normalized().effective_or_literal_name() == Some("_arguments")
&& command
.enclosing_function_scope()
.is_some_and(|caller_scope| scopes.contains(&caller_scope))
&& command.body_args().iter().any(|word| {
static_word_text(word, source).is_some_and(|text| {
zsh_arguments_spec_invokes_function(
&text,
&semantic.binding(*binding_id).name,
)
})
})
});
if called_from_completion_scope || referenced_by_completion_arguments {
scopes.insert(*target_scope);
changed = true;
}
}
}
}
#[cfg_attr(shuck_profiling, inline(never))]
pub(crate) fn build_external_entrypoint_function_scopes(
semantic: &SemanticModel,
commands: &[CommandFact<'_>],
command_fact_indices_by_id: &[Option<usize>],
lists: &[ListFact<'_>],
source: &str,
) -> FxHashSet<ScopeId> {
let mut function_candidates = Vec::new();
function_candidates.resize_with(function_command_slot_count(commands), || None);
for command in commands {
function_candidates[command.id().index()] =
external_entrypoint_function_candidate(semantic, command);
}
let mut scopes = FxHashSet::default();
for candidate in function_candidates.iter().flatten() {
if is_zsh_special_hook_name(&candidate.name) {
scopes.insert(candidate.scope);
}
}
let mut zsh_widget_functions = FxHashMap::<Box<str>, Box<str>>::default();
let mut zsh_hook_targets = FxHashSet::<(Box<str>, ZshHookTarget)>::default();
for command in commands {
if !is_top_level_zsh_entrypoint_registration(semantic, command) {
continue;
}
match command_zsh_external_entrypoint_action(command, source) {
Some(ZshExternalEntrypointAction::RegisterWidget { widget, function }) => {
zsh_widget_functions.insert(widget, function);
}
Some(ZshExternalEntrypointAction::UnregisterWidgets { widgets }) => {
for widget in widgets {
zsh_widget_functions.remove(&widget);
}
}
Some(ZshExternalEntrypointAction::RegisterHookFunction { hook, function }) => {
zsh_hook_targets.insert((hook, ZshHookTarget::Function(function)));
}
Some(ZshExternalEntrypointAction::RegisterHookWidget { hook, widget }) => {
zsh_hook_targets.insert((hook, ZshHookTarget::Widget(widget)));
}
Some(ZshExternalEntrypointAction::UnregisterHookFunction { hook, function }) => {
zsh_hook_targets.remove(&(hook, ZshHookTarget::Function(function)));
}
Some(ZshExternalEntrypointAction::UnregisterHookWidget { hook, widget }) => {
zsh_hook_targets.remove(&(hook, ZshHookTarget::Widget(widget)));
}
Some(ZshExternalEntrypointAction::UnregisterHookFunctionPattern { hook, pattern }) => {
zsh_hook_targets.retain(|(registered_hook, registered_target)| {
registered_hook.as_ref() != hook.as_ref()
|| !registered_target.is_function_matching_pattern(&pattern)
});
}
Some(ZshExternalEntrypointAction::UnregisterHookWidgetPattern { hook, pattern }) => {
zsh_hook_targets.retain(|(registered_hook, registered_target)| {
registered_hook.as_ref() != hook.as_ref()
|| !registered_target.is_widget_matching_pattern(&pattern)
});
}
None => {}
}
}
for candidate in function_candidates.iter().flatten() {
if zsh_widget_functions
.values()
.any(|name| name.as_ref() == candidate.name.as_ref())
|| zsh_hook_targets
.iter()
.any(|(_, target)| target.matches_function(&candidate.name, &zsh_widget_functions))
{
scopes.insert(candidate.scope);
}
}
for list in lists {
for (index, segment) in list.segments().iter().enumerate() {
let Some(candidate) = function_candidates[segment.command_id().index()].as_ref() else {
continue;
};
if list.segments()[index + 1..].iter().any(|later_segment| {
command_registers_completion_function(
command_fact(
commands,
command_fact_indices_by_id,
later_segment.command_id(),
),
source,
&candidate.name,
)
}) {
scopes.insert(candidate.scope);
}
}
}
scopes
}
pub(crate) fn is_top_level_zsh_entrypoint_registration(
semantic: &SemanticModel,
command: &CommandFact<'_>,
) -> bool {
semantic.scope(command.scope()).parent.is_none()
&& !command.is_nested_word_command()
&& !has_structural_parent_command(semantic, command.id())
}
pub(crate) fn has_structural_parent_command(semantic: &SemanticModel, id: CommandId) -> bool {
nearest_structural_parent_command(semantic, id).is_some()
}
pub(crate) fn nearest_structural_parent_command(
semantic: &SemanticModel,
id: CommandId,
) -> Option<CommandId> {
let mut current = semantic.syntax_backed_command_parent_id(id);
while let Some(parent) = current {
if matches!(
semantic.command_kind(parent),
shuck_semantic::CommandKind::Compound(_)
| shuck_semantic::CommandKind::Function
| shuck_semantic::CommandKind::AnonymousFunction
) {
return Some(parent);
}
current = semantic.syntax_backed_command_parent_id(parent);
}
None
}
pub(crate) fn same_structural_branch_between(source: &str, start: usize, end: usize) -> bool {
let Some(between) = source.get(start..end) else {
return false;
};
!contains_zsh_structural_branch_boundary(between)
}
pub(crate) fn contains_zsh_structural_branch_boundary(text: &str) -> bool {
#[derive(Clone, Copy, PartialEq, Eq)]
enum Quote {
None,
Single,
Double,
}
let mut chars = text.chars().peekable();
let mut quote = Quote::None;
let mut in_comment = false;
let mut escaped = false;
let mut word = String::new();
let mut at_word_start = true;
while let Some(ch) = chars.next() {
if in_comment {
if ch == '\n' {
in_comment = false;
at_word_start = true;
}
continue;
}
match quote {
Quote::Single => {
if ch == '\'' {
quote = Quote::None;
}
continue;
}
Quote::Double => {
if escaped {
escaped = false;
continue;
}
if ch == '\\' {
escaped = true;
continue;
}
if ch == '"' {
quote = Quote::None;
}
continue;
}
Quote::None => {}
}
match ch {
'#' => {
if structural_boundary_word(&word) {
return true;
}
word.clear();
if at_word_start {
in_comment = true;
} else {
at_word_start = false;
}
}
'\'' => {
if structural_boundary_word(&word) {
return true;
}
word.clear();
at_word_start = false;
quote = Quote::Single;
}
'"' => {
if structural_boundary_word(&word) {
return true;
}
word.clear();
at_word_start = false;
quote = Quote::Double;
}
';' => {
if structural_boundary_word(&word) {
return true;
}
word.clear();
if matches!(chars.peek(), Some(';' | '&' | '|')) {
return true;
}
at_word_start = true;
}
_ if ch.is_whitespace() => {
if structural_boundary_word(&word) {
return true;
}
word.clear();
at_word_start = true;
}
_ if is_shell_identifier_char(ch) => {
word.push(ch);
at_word_start = false;
}
_ => {
if structural_boundary_word(&word) {
return true;
}
word.clear();
at_word_start = false;
}
}
}
structural_boundary_word(&word)
}
pub(crate) fn structural_boundary_word(word: &str) -> bool {
matches!(word, "else" | "elif")
}
pub(crate) fn is_shell_identifier_char(ch: char) -> bool {
ch == '_' || ch.is_ascii_alphanumeric()
}
pub(crate) fn is_unconditional_completion_registration(
semantic: &SemanticModel,
command: &CommandFact<'_>,
) -> bool {
if !is_top_level_zsh_entrypoint_registration(semantic, command) {
return false;
}
command.effective_or_literal_name() != Some("compdef")
|| !has_binary_parent_command(semantic, command.id())
}
pub(crate) fn is_same_branch_completion_registration(
semantic: &SemanticModel,
command: &CommandFact<'_>,
) -> bool {
matches!(
command.effective_or_literal_name(),
Some("compdef" | "complete")
) && !has_binary_parent_command(semantic, command.id())
}
pub(crate) fn has_binary_parent_command(semantic: &SemanticModel, id: CommandId) -> bool {
let mut current = semantic.syntax_backed_command_parent_id(id);
while let Some(parent) = current {
if matches!(
semantic.command_kind(parent),
shuck_semantic::CommandKind::Binary
) {
return true;
}
current = semantic.syntax_backed_command_parent_id(parent);
}
false
}
pub(crate) fn function_command_slot_count(commands: &[CommandFact<'_>]) -> usize {
commands
.iter()
.map(|command| command.id().index())
.max()
.map_or(0, |index| index + 1)
}
pub(crate) fn completion_registered_function_candidate(
semantic: &SemanticModel,
command: &CommandFact<'_>,
) -> Option<CompletionRegisteredFunctionCandidate> {
if !is_top_level_zsh_entrypoint_registration(semantic, command) {
return None;
}
let Command::Function(function) = command.command() else {
return None;
};
let (name, _) = function.static_name_entries().next()?;
let scope = semantic.scope_at(function.body.span.start.offset);
Some(CompletionRegisteredFunctionCandidate {
scope,
name: name.as_str().to_owned().into_boxed_str(),
})
}
pub(crate) fn file_level_completion_function_candidate(
semantic: &SemanticModel,
command: &CommandFact<'_>,
) -> Option<CompletionRegisteredFunctionCandidate> {
if command.is_nested_word_command()
|| semantic.enclosing_function_scope(command.scope()).is_some()
{
return None;
}
let Command::Function(function) = command.command() else {
return None;
};
let (name, _) = function.static_name_entries().next()?;
let scope = semantic.scope_at(function.body.span.start.offset);
Some(CompletionRegisteredFunctionCandidate {
scope,
name: name.as_str().to_owned().into_boxed_str(),
})
}
pub(crate) fn external_entrypoint_function_candidate(
semantic: &SemanticModel,
command: &CommandFact<'_>,
) -> Option<CompletionRegisteredFunctionCandidate> {
let Command::Function(function) = command.command() else {
return None;
};
let (name, _) = function.static_name_entries().next()?;
let scope = semantic.scope_at(function.body.span.start.offset);
let name_text = name.as_str();
Some(CompletionRegisteredFunctionCandidate {
scope,
name: name_text.to_owned().into_boxed_str(),
})
}
pub(crate) fn command_registers_completion_function(
command: &CommandFact<'_>,
source: &str,
expected_name: &str,
) -> bool {
let command_name = command.effective_or_literal_name();
if command_name == Some("compdef") {
let mut index = 0usize;
let args = command.body_args();
while let Some(word) = args.get(index) {
let Some(text) = static_word_text(word, source) else {
return false;
};
if text == "--" {
index += 1;
break;
}
let Some(flags) = text.strip_prefix('-') else {
break;
};
if flags.is_empty() || flags.starts_with('-') {
break;
}
if flags.contains('d') || flags.contains('D') {
return false;
}
index += 1;
for flag in flags.chars() {
if matches!(flag, 'p' | 'P') {
if index >= args.len() {
return false;
}
index += 1;
}
}
}
if let Some(word) = args.get(index) {
let Some(text) = static_word_text(word, source) else {
return false;
};
if text.contains('=') {
return false;
}
if text == expected_name {
return true;
}
return false;
}
return false;
}
if command_name == Some("complete") {
let mut expects_function_name = false;
for word in command.body_args() {
let Some(text) = static_word_text(word, source) else {
expects_function_name = false;
continue;
};
if expects_function_name {
return text == expected_name;
}
if text == "--" {
return false;
}
if text == "-F" || text == "--function" {
expects_function_name = true;
continue;
}
if let Some(name) = text.strip_prefix("-F")
&& !name.is_empty()
{
return name == expected_name;
}
if let Some(name) = text.strip_prefix("--function=") {
return name == expected_name;
}
}
}
false
}
pub(crate) fn zsh_arguments_spec_invokes_function(spec: &str, expected_name: &Name) -> bool {
spec.split(':')
.skip(1)
.map(str::trim)
.any(|segment| segment == expected_name.as_str())
}
pub(crate) enum ZshExternalEntrypointAction {
RegisterWidget {
widget: Box<str>,
function: Box<str>,
},
UnregisterWidgets {
widgets: Vec<Box<str>>,
},
RegisterHookFunction {
hook: Box<str>,
function: Box<str>,
},
RegisterHookWidget {
hook: Box<str>,
widget: Box<str>,
},
UnregisterHookFunction {
hook: Box<str>,
function: Box<str>,
},
UnregisterHookWidget {
hook: Box<str>,
widget: Box<str>,
},
UnregisterHookFunctionPattern {
hook: Box<str>,
pattern: Box<str>,
},
UnregisterHookWidgetPattern {
hook: Box<str>,
pattern: Box<str>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) enum ZshHookTarget {
Function(Box<str>),
Widget(Box<str>),
}
impl ZshHookTarget {
fn matches_function(
&self,
function: &str,
widget_functions: &FxHashMap<Box<str>, Box<str>>,
) -> bool {
match self {
Self::Function(name) => name.as_ref() == function,
Self::Widget(widget) => {
widget.as_ref() == function
|| widget_functions
.get(widget)
.is_some_and(|name| name.as_ref() == function)
}
}
}
fn is_function_matching_pattern(&self, pattern: &str) -> bool {
match self {
Self::Function(name) => zsh_hook_function_pattern_matches(pattern, name),
Self::Widget(_) => false,
}
}
fn is_widget_matching_pattern(&self, pattern: &str) -> bool {
match self {
Self::Function(_) => false,
Self::Widget(name) => zsh_hook_function_pattern_matches(pattern, name),
}
}
}
pub(crate) fn command_zsh_external_entrypoint_action(
command: &CommandFact<'_>,
source: &str,
) -> Option<ZshExternalEntrypointAction> {
match command.effective_or_literal_name()? {
"zle" => zle_external_entrypoint_action(command, source),
"add-zsh-hook" => {
add_zsh_hook_external_entrypoint_action(command, source, AddZshHookTargetKind::Function)
}
"add-zle-hook-widget" => {
add_zsh_hook_external_entrypoint_action(command, source, AddZshHookTargetKind::Widget)
}
_ => None,
}
}
pub(crate) fn zle_external_entrypoint_action(
command: &CommandFact<'_>,
source: &str,
) -> Option<ZshExternalEntrypointAction> {
let args = static_command_args(command, source)?;
if let Some(registration_index) = args.iter().position(|arg| arg == "-N") {
let operands = args[registration_index + 1..]
.iter()
.filter(|arg| !arg.starts_with('-'))
.collect::<Vec<_>>();
return match operands.as_slice() {
[widget] => Some(ZshExternalEntrypointAction::RegisterWidget {
widget: widget.as_str().into(),
function: widget.as_str().into(),
}),
[widget, function, ..] => Some(ZshExternalEntrypointAction::RegisterWidget {
widget: widget.as_str().into(),
function: function.as_str().into(),
}),
_ => None,
};
}
if let Some(removal_index) = args.iter().position(|arg| arg == "-D") {
let widgets = args[removal_index + 1..]
.iter()
.filter(|arg| !arg.starts_with('-'))
.map(|arg| arg.as_str().into())
.collect::<Vec<_>>();
if !widgets.is_empty() {
return Some(ZshExternalEntrypointAction::UnregisterWidgets { widgets });
}
}
None
}
pub(crate) fn add_zsh_hook_external_entrypoint_action(
command: &CommandFact<'_>,
source: &str,
target_kind: AddZshHookTargetKind,
) -> Option<ZshExternalEntrypointAction> {
let args = static_command_args(command, source)?;
let removal_mode = add_zsh_hook_removal_mode(&args);
let operands = args
.iter()
.filter(|arg| !arg.starts_with('-'))
.collect::<Vec<_>>();
match operands.as_slice() {
[hook, function, ..] if removal_mode == Some(AddZshHookRemovalMode::Exact) => {
match target_kind {
AddZshHookTargetKind::Function => {
Some(ZshExternalEntrypointAction::UnregisterHookFunction {
hook: hook.as_str().into(),
function: function.as_str().into(),
})
}
AddZshHookTargetKind::Widget => {
Some(ZshExternalEntrypointAction::UnregisterHookWidget {
hook: hook.as_str().into(),
widget: function.as_str().into(),
})
}
}
}
[hook, pattern, ..] if removal_mode == Some(AddZshHookRemovalMode::Pattern) => {
match target_kind {
AddZshHookTargetKind::Function => {
Some(ZshExternalEntrypointAction::UnregisterHookFunctionPattern {
hook: hook.as_str().into(),
pattern: pattern.as_str().into(),
})
}
AddZshHookTargetKind::Widget => {
Some(ZshExternalEntrypointAction::UnregisterHookWidgetPattern {
hook: hook.as_str().into(),
pattern: pattern.as_str().into(),
})
}
}
}
[hook, function, ..] if removal_mode.is_none() => match target_kind {
AddZshHookTargetKind::Function => {
Some(ZshExternalEntrypointAction::RegisterHookFunction {
hook: hook.as_str().into(),
function: function.as_str().into(),
})
}
AddZshHookTargetKind::Widget => Some(ZshExternalEntrypointAction::RegisterHookWidget {
hook: hook.as_str().into(),
widget: function.as_str().into(),
}),
},
_ => None,
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum AddZshHookTargetKind {
Function,
Widget,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AddZshHookRemovalMode {
Exact,
Pattern,
}
pub(crate) fn add_zsh_hook_removal_mode(args: &[String]) -> Option<AddZshHookRemovalMode> {
if args
.iter()
.any(|arg| add_zsh_hook_option_contains(arg, 'D'))
{
return Some(AddZshHookRemovalMode::Pattern);
}
args.iter()
.any(|arg| add_zsh_hook_option_contains(arg, 'd'))
.then_some(AddZshHookRemovalMode::Exact)
}
pub(crate) fn add_zsh_hook_option_contains(arg: &str, flag: char) -> bool {
arg.starts_with('-') && arg != "--" && arg.chars().skip(1).any(|c| c == flag)
}
pub(crate) fn zsh_hook_function_pattern_matches(pattern: &str, function: &str) -> bool {
if pattern_contains_unsupported_zsh_metachar(pattern) {
return true;
}
zsh_simple_glob_pattern_matches(pattern.as_bytes(), function.as_bytes())
}
pub(crate) fn zsh_simple_glob_pattern_matches(pattern: &[u8], text: &[u8]) -> bool {
if pattern.is_empty() {
return text.is_empty();
}
match pattern[0] {
b'*' => {
zsh_simple_glob_pattern_matches(&pattern[1..], text)
|| (!text.is_empty() && zsh_simple_glob_pattern_matches(pattern, &text[1..]))
}
b'?' => !text.is_empty() && zsh_simple_glob_pattern_matches(&pattern[1..], &text[1..]),
b'[' => zsh_bracket_pattern_matches(pattern, text),
b'<' if pattern.starts_with(b"<->") => zsh_numeric_pattern_matches(&pattern[3..], text),
literal => {
text.first().is_some_and(|byte| *byte == literal)
&& zsh_simple_glob_pattern_matches(&pattern[1..], &text[1..])
}
}
}
pub(crate) fn pattern_contains_unsupported_zsh_metachar(pattern: &str) -> bool {
let mut in_bracket_class = false;
for byte in pattern.bytes() {
match byte {
b'[' if !in_bracket_class => in_bracket_class = true,
b']' if in_bracket_class => in_bracket_class = false,
b'(' | b')' | b'|' | b'~' | b'#' if !in_bracket_class => return true,
_ => {}
}
}
false
}
pub(crate) fn zsh_numeric_pattern_matches(pattern_after_numeric: &[u8], text: &[u8]) -> bool {
let digit_count = text.iter().take_while(|byte| byte.is_ascii_digit()).count();
(1..=digit_count)
.any(|consumed| zsh_simple_glob_pattern_matches(pattern_after_numeric, &text[consumed..]))
}
pub(crate) fn zsh_bracket_pattern_matches(pattern: &[u8], text: &[u8]) -> bool {
let Some(&candidate) = text.first() else {
return false;
};
let Some(close_index) = pattern.iter().position(|byte| *byte == b']') else {
return candidate == b'[' && zsh_simple_glob_pattern_matches(&pattern[1..], &text[1..]);
};
if close_index == 1 {
return candidate == b'[' && zsh_simple_glob_pattern_matches(&pattern[1..], &text[1..]);
}
let class = &pattern[1..close_index];
let (negated, class) = match class {
[b'!' | b'^', rest @ ..] => (true, rest),
_ => (false, class),
};
let matched = zsh_bracket_class_contains(class, candidate);
if matched != negated {
zsh_simple_glob_pattern_matches(&pattern[close_index + 1..], &text[1..])
} else {
false
}
}
pub(crate) fn zsh_bracket_class_contains(class: &[u8], candidate: u8) -> bool {
let mut index = 0;
while index < class.len() {
if index + 2 < class.len() && class[index + 1] == b'-' {
if class[index] <= candidate && candidate <= class[index + 2] {
return true;
}
index += 3;
} else {
if class[index] == candidate {
return true;
}
index += 1;
}
}
false
}
pub(crate) fn static_command_args(command: &CommandFact<'_>, source: &str) -> Option<Vec<String>> {
command
.body_args()
.iter()
.map(|word| static_word_text(word, source).map(|text| text.into_owned()))
.collect()
}
pub(crate) fn is_zsh_special_hook_name(name: &str) -> bool {
matches!(
name,
"precmd"
| "preexec"
| "chpwd"
| "periodic"
| "zshaddhistory"
| "zsh_directory_name"
| "zshexit"
)
}
#[derive(Debug, Clone)]
pub(crate) struct CompletionRegisteredFunctionCandidate {
scope: ScopeId,
name: Box<str>,
}
pub(crate) fn function_parameter_fallback_span(
pair: &[&CommandFact<'_>],
source: &str,
) -> Option<Span> {
let [first, second] = pair else {
return None;
};
let name = first.normalized().effective_or_literal_name()?;
if !is_plausible_shell_function_name(name) || !first.normalized().body_args().is_empty() {
return None;
}
if !matches!(first.command(), Command::Simple(_)) {
return None;
}
let Command::Compound(CompoundCommand::Subshell(commands)) = second.command() else {
return None;
};
if commands.is_empty() {
return None;
}
if first.span().start.line != second.span().start.line {
return None;
}
let tail = source.get(second.span().end.offset..)?;
if !matches!(next_function_body_delimiter(tail), Some('{') | Some('(')) {
return None;
}
let text = first.span().slice(source);
let relative = text.find('(')?;
let start = first.span().start.advanced_by(&text[..relative]);
Some(Span::from_positions(start, start.advanced_by("(")))
}
pub(crate) fn named_coproc_subshell_fallback_span(command: &CommandFact<'_>) -> Option<Span> {
let Command::Compound(CompoundCommand::Coproc(coproc)) = command.command() else {
return None;
};
coproc.name_span?;
let Command::Compound(CompoundCommand::Subshell(commands)) = &coproc.body.command else {
return None;
};
if commands.is_empty() {
return None;
}
let body_start = coproc.body.span.start;
if coproc.span.start.line != body_start.line {
return None;
}
Some(Span::from_positions(body_start, body_start))
}
pub(crate) fn build_function_call_arity_facts<'a>(
semantic_analysis: &SemanticAnalysis<'_>,
functions: &[&FunctionDef],
commands: &[CommandFact<'a>],
command_fact_indices_by_id: &[Option<usize>],
source: &str,
) -> FxHashMap<BindingId, FunctionCallArityFacts> {
let mut facts = FxHashMap::<BindingId, FunctionCallArityFacts>::default();
let mut seen_names = FxHashSet::default();
let mut unique_function_names: Vec<&Name> = Vec::with_capacity(functions.len());
for function in functions {
let Some((name, _)) = function.static_name_entries().next() else {
continue;
};
if seen_names.insert(name.clone()) {
unique_function_names.push(name);
}
}
if unique_function_names.is_empty() {
return facts;
}
let mut offsets = Vec::new();
for name in &unique_function_names {
for (site, _) in semantic_analysis.function_call_arity_sites(name) {
offsets.push(site.name_span.start.offset);
}
}
if offsets.is_empty() {
return facts;
}
let command_ids_by_offset = build_innermost_command_ids_by_offset(commands, offsets);
for name in unique_function_names {
for (site, binding_id) in semantic_analysis.function_call_arity_sites(name) {
let Some(command_id) = precomputed_command_id_for_offset(
&command_ids_by_offset,
site.name_span.start.offset,
) else {
continue;
};
let command = command_fact(commands, command_fact_indices_by_id, command_id);
if !command.wrappers().is_empty()
|| command.effective_or_literal_name() != Some(name.as_str())
{
continue;
}
let Some(name_word) = command.body_name_word() else {
continue;
};
facts.entry(binding_id).or_default().record_call(
site.arg_count,
name_word.span,
function_call_diagnostic_span(command, name_word.span, source),
);
}
}
facts
}
pub(crate) fn function_call_diagnostic_span(
command: &CommandFact<'_>,
name_span: Span,
source: &str,
) -> Span {
if command.redirects().is_empty() {
return name_span;
}
trim_trailing_whitespace_span(command.stmt().span, source)
}
pub(crate) fn function_body_without_braces_span(function: &FunctionDef) -> Option<Span> {
match &function.body.command {
Command::Compound(
CompoundCommand::BraceGroup(_)
| CompoundCommand::Subshell(_)
| CompoundCommand::Arithmetic(_),
) => None,
Command::Compound(_) => Some(function.body.span),
Command::Simple(_)
| Command::Decl(_)
| Command::Builtin(_)
| Command::Binary(_)
| Command::Function(_)
| Command::AnonymousFunction(_) => None,
}
}
pub(crate) fn next_function_body_delimiter(text: &str) -> Option<char> {
let mut tail = text;
loop {
tail = trim_shell_layout_prefix(tail);
if let Some(rest) = tail.strip_prefix('#') {
tail = rest.split_once('\n').map_or("", |(_, rest)| rest);
continue;
}
return tail.chars().next();
}
}
pub(crate) fn trim_shell_layout_prefix(text: &str) -> &str {
let mut tail = text;
loop {
tail = tail.trim_start_matches([' ', '\t', '\r', '\n']);
if let Some(rest) = tail
.strip_prefix("\\\r\n")
.or_else(|| tail.strip_prefix("\\\n"))
{
tail = rest;
continue;
}
return tail;
}
}
pub(crate) fn is_plausible_shell_function_name(name: &str) -> bool {
let Some(first) = name.chars().next() else {
return false;
};
if !matches!(first, 'a'..='z' | 'A'..='Z' | '_') {
return false;
}
if !name
.chars()
.all(|ch| matches!(ch, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-'))
{
return false;
}
!matches!(
name,
"!" | "{"
| "}"
| "if"
| "then"
| "else"
| "elif"
| "fi"
| "do"
| "done"
| "case"
| "esac"
| "for"
| "in"
| "while"
| "until"
| "time"
| "[["
| "]]"
| "function"
| "select"
| "coproc"
)
}
pub(crate) fn build_function_positional_parameter_facts(
semantic: &SemanticModel,
commands: &[CommandFact<'_>],
positional_parameter_fragments: &[PositionalParameterFragmentFact],
) -> FxHashMap<ScopeId, FunctionPositionalParameterFacts> {
let local_reset_offsets_by_scope = local_positional_reset_offsets_by_scope(semantic, commands);
let mut facts = semantic
.function_positional_reference_summary(&local_reset_offsets_by_scope)
.into_iter()
.map(|(scope, summary)| {
(
scope,
FunctionPositionalParameterFacts {
required_arg_count: summary.required_arg_count(),
uses_unprotected_positional_parameters: summary
.uses_unprotected_positional_parameters(),
resets_positional_parameters: false,
},
)
})
.collect::<FxHashMap<_, _>>();
for fragment in positional_parameter_fragments {
let fragment_offset = fragment.span().start.offset;
let fragment_scope = semantic.scope_at(fragment_offset);
if reference_has_local_positional_reset(
semantic,
fragment_scope,
fragment_offset,
&local_reset_offsets_by_scope,
) {
continue;
}
let Some(scope) = semantic.enclosing_function_scope(fragment_scope) else {
continue;
};
let entry = facts.entry(scope).or_default();
if !fragment.is_guarded() {
entry.uses_unprotected_positional_parameters = true;
}
}
for command in commands {
let Some(scope) =
semantic.enclosing_function_scope_without_transient_boundary(command.scope())
else {
continue;
};
if command
.options()
.set()
.is_some_and(|set| set.resets_positional_parameters())
{
facts.entry(scope).or_default().resets_positional_parameters = true;
}
}
facts
}
fn local_positional_reset_offsets_by_scope(
semantic: &SemanticModel,
commands: &[CommandFact<'_>],
) -> FxHashMap<ScopeId, Vec<usize>> {
let mut local_reset_offsets_by_scope: FxHashMap<ScopeId, Vec<usize>> = FxHashMap::default();
for command in commands {
if !command
.options()
.set()
.is_some_and(|set| set.resets_positional_parameters())
{
continue;
}
let offset = command.span().start.offset;
if let Some(scope) = semantic.innermost_transient_scope_within_function(command.scope()) {
local_reset_offsets_by_scope
.entry(scope)
.or_default()
.push(offset);
}
}
local_reset_offsets_by_scope
}
pub(crate) fn reference_has_local_positional_reset(
semantic: &SemanticModel,
scope: ScopeId,
offset: usize,
local_reset_offsets_by_scope: &FxHashMap<ScopeId, Vec<usize>>,
) -> bool {
semantic
.transient_ancestor_scopes_within_function(scope)
.any(|transient_scope| {
local_reset_offsets_by_scope
.get(&transient_scope)
.is_some_and(|offsets| offsets.iter().any(|reset_offset| *reset_offset < offset))
})
}