use compact_str::CompactString;
use rustc_hash::FxHashSet;
use shuck_ast::Span;
use shuck_semantic::BindingId;
use super::function_arity::zsh_function_arity_is_externally_defined;
use crate::{Checker, Rule, Violation};
pub struct FunctionReferencesUnsetParam {
pub name: CompactString,
}
impl Violation for FunctionReferencesUnsetParam {
fn rule() -> Rule {
Rule::FunctionReferencesUnsetParam
}
fn message(&self) -> String {
format!("function `{}` is called with too few arguments", self.name)
}
}
pub fn function_references_unset_param(checker: &mut Checker) {
let mut reported = FxHashSet::<BindingId>::default();
let mut violations = Vec::<(Span, CompactString)>::new();
for header in checker.facts().command_facts().function_headers() {
let Some((name, _)) = header.static_name_entry() else {
continue;
};
let Some(binding_id) = header.binding_id() else {
continue;
};
if !reported.insert(binding_id) {
continue;
}
let Some(function_scope) = header.function_scope() else {
continue;
};
if checker.is_suppressed_at(
Rule::FunctionCalledWithoutArgs,
header.function_span_in_source(checker.source()),
) {
continue;
}
let positional = checker
.facts()
.command_facts()
.function_positional_parameter_facts(function_scope);
if !positional.uses_positional_parameters() || positional.resets_positional_parameters() {
continue;
}
if zsh_function_arity_is_externally_defined(checker, function_scope) {
continue;
}
let call_arity = header.call_arity();
if !call_arity.called_only_without_args() {
continue;
}
violations.extend(
call_arity
.zero_arg_diagnostic_spans()
.iter()
.copied()
.map(|span| (span, name.as_str().into())),
);
}
for (span, name) in violations {
checker.report(FunctionReferencesUnsetParam { name }, span);
}
}
#[cfg(test)]
mod tests {
use shuck_indexer::Indexer;
use shuck_parser::parser::Parser;
use crate::test::test_snippet;
use crate::{
AnalysisRequest, LinterSettings, Rule, ShellCheckCodeMap, ShellDialect, parse_directives,
};
#[test]
fn reports_zero_argument_call_sites_for_functions_that_read_positional_parameters() {
let source = "\
#!/bin/sh
greet() { echo \"$1 $2\"; }
greet
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].span.slice(source), "greet");
assert_eq!(diagnostics[0].span.start.line, 3);
}
#[test]
fn reports_each_zero_argument_call_site_when_all_calls_omit_arguments() {
let source = "\
#!/bin/sh
greet() { echo \"$1\"; }
greet
greet
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert_eq!(diagnostics.len(), 2);
assert_eq!(diagnostics[0].span.slice(source), "greet");
assert_eq!(diagnostics[0].span.start.line, 3);
assert_eq!(diagnostics[1].span.slice(source), "greet");
assert_eq!(diagnostics[1].span.start.line, 4);
}
#[test]
fn ignores_functions_called_with_enough_arguments() {
let source = "\
#!/bin/sh
greet() { echo \"$1 $2\"; }
greet a b
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:?}");
}
#[test]
fn ignores_functions_with_mixed_arity_calls() {
let source = "\
#!/bin/sh
greet() { echo \"$1 $2\"; }
greet a b
greet
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:?}");
}
#[test]
fn reports_functions_that_read_special_positional_parameters() {
let source = "\
#!/bin/sh
usage() { echo \"usage: ${##*/} <files>\"; }
usage
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].span.slice(source), "usage");
assert_eq!(diagnostics[0].span.start.line, 3);
}
#[test]
fn ignores_zsh_widget_and_hook_callbacks_with_optional_arguments() {
let source = "\
#!/bin/zsh
single_operand_widget() {
print -r -- \"$1\"
}
_zsh_autosuggest_widget_accept() {
local original_widget=\"$1\"
shift || true
\"$original_widget\" \"$@\"
}
precmd_refresh() {
(( $# )) && print -r -- \"$1\"
}
zle -N autosuggest-accept _zsh_autosuggest_widget_accept
zle -N single_operand_widget
add-zsh-hook precmd precmd_refresh
_zsh_autosuggest_widget_accept
single_operand_widget
precmd_refresh
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam)
.with_shell(ShellDialect::Zsh),
);
assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:?}");
}
#[test]
fn ignores_zsh_call_sites_for_optional_first_positional_parameter() {
let source = "\
#!/bin/zsh
retlog() {
if [[ -z $1 ]]; then
print -r -- /var/log/nginx/access.log
else
print -r -- \"$1\"
fi
}
phase() {
local mode=components
[[ \"${1:-}\" == --phase=* ]] && mode=\"${1#--phase=}\"
print -r -- \"$mode\"
}
retlog
phase
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam)
.with_shell(ShellDialect::Zsh),
);
assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:?}");
}
#[test]
fn reports_zsh_required_argument_after_first_positional_validation() {
let source = "\
#!/bin/zsh
requires_name() {
if [[ -n \"$1\" ]]; then
print -r -- ok
fi
print -r -- \"$2\"
}
requires_name
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam)
.with_shell(ShellDialect::Zsh),
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].span.slice(source), "requires_name");
assert_eq!(diagnostics[0].span.start.line, 8);
}
#[test]
fn ignores_zsh_standard_hook_call_sites() {
let source = "\
#!/bin/zsh
preexec() { print -r -- \"$1\"; }
preexec
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam)
.with_shell(ShellDialect::Zsh),
);
assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:?}");
}
#[test]
fn ignores_zsh_directory_name_call_sites() {
let source = "\
#!/bin/zsh
zsh_directory_name() { print -r -- \"$1\"; }
zsh_directory_name
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam)
.with_shell(ShellDialect::Zsh),
);
assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:?}");
}
#[test]
fn reports_zsh_call_sites_missing_required_arguments() {
let source = "\
#!/bin/zsh
greet() { print -r -- \"$1 $2\"; }
greet
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam)
.with_shell(ShellDialect::Zsh),
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].span.slice(source), "greet");
assert_eq!(diagnostics[0].span.start.line, 3);
}
#[test]
fn reports_zsh_call_sites_removed_from_hooks() {
let source = "\
#!/bin/zsh
removed_precmd() { print -r -- \"$1\"; }
removed_chpwd() { print -r -- \"$1\"; }
add-zsh-hook chpwd removed_chpwd
add-zsh-hook -d precmd removed_precmd
add-zsh-hook -UD chpwd removed_chpwd
removed_precmd
removed_chpwd
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam)
.with_shell(ShellDialect::Zsh),
);
assert_eq!(diagnostics.len(), 2);
assert_eq!(diagnostics[0].span.slice(source), "removed_precmd");
assert_eq!(diagnostics[0].span.start.line, 7);
assert_eq!(diagnostics[1].span.slice(source), "removed_chpwd");
assert_eq!(diagnostics[1].span.start.line, 8);
}
#[test]
fn reports_zsh_call_sites_removed_from_hooks_by_pattern() {
let source = "\
#!/bin/zsh
cb_one() { print -r -- \"$1\"; }
cb_two() { print -r -- \"$1\"; }
cb_keep() { print -r -- \"$1\"; }
add-zsh-hook precmd cb_one
add-zsh-hook precmd cb_two
add-zsh-hook precmd cb_keep
add-zsh-hook -D precmd 'cb_t*'
cb_one
cb_two
cb_keep
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam)
.with_shell(ShellDialect::Zsh),
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].span.slice(source), "cb_two");
assert_eq!(diagnostics[0].span.start.line, 10);
}
#[test]
fn reports_zsh_call_sites_removed_from_hooks_by_richer_patterns() {
let source = "\
#!/bin/zsh
cb_1() { print -r -- \"$1\"; }
cb_2() { print -r -- \"$1\"; }
cb_10() { print -r -- \"$1\"; }
cb_a() { print -r -- \"$1\"; }
cb_x() { print -r -- \"$1\"; }
cb_keep() { print -r -- \"$1\"; }
add-zsh-hook precmd cb_1
add-zsh-hook precmd cb_2
add-zsh-hook precmd cb_10
add-zsh-hook precmd cb_a
add-zsh-hook precmd cb_x
add-zsh-hook precmd cb_keep
add-zsh-hook -D precmd 'cb_[12]'
add-zsh-hook -D precmd 'cb_<->'
add-zsh-hook -D precmd 'cb_[^x]'
cb_1
cb_2
cb_10
cb_a
cb_x
cb_keep
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam)
.with_shell(ShellDialect::Zsh),
);
assert_eq!(diagnostics.len(), 4);
assert_eq!(diagnostics[0].span.slice(source), "cb_1");
assert_eq!(diagnostics[0].span.start.line, 17);
assert_eq!(diagnostics[1].span.slice(source), "cb_2");
assert_eq!(diagnostics[1].span.start.line, 18);
assert_eq!(diagnostics[2].span.slice(source), "cb_10");
assert_eq!(diagnostics[2].span.start.line, 19);
assert_eq!(diagnostics[3].span.slice(source), "cb_a");
assert_eq!(diagnostics[3].span.start.line, 20);
}
#[test]
fn reports_zsh_call_sites_removed_from_widgets() {
let source = "\
#!/bin/zsh
removed_widget() { print -r -- \"$1\"; }
zle -N removed-widget removed_widget
zle -D removed-widget
removed_widget
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam)
.with_shell(ShellDialect::Zsh),
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].span.slice(source), "removed_widget");
assert_eq!(diagnostics[0].span.start.line, 5);
}
#[test]
fn ignores_zsh_call_sites_still_registered_as_widgets() {
let source = "\
#!/bin/zsh
shared_widget() { print -r -- \"$1\"; }
zle -N first-widget shared_widget
zle -N second-widget shared_widget
zle -D first-widget
shared_widget
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam)
.with_shell(ShellDialect::Zsh),
);
assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:?}");
}
#[test]
fn ignores_zsh_call_sites_registered_as_zle_hook_widgets() {
let source = "\
#!/bin/zsh
line_init_widget() { print -r -- \"$1\"; }
line_init_impl() { print -r -- \"$1\"; }
zle -N line-init-alias line_init_impl
add-zle-hook-widget line-init line_init_widget
add-zle-hook-widget line-init line-init-alias
line_init_widget
line_init_impl
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam)
.with_shell(ShellDialect::Zsh),
);
assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:?}");
}
#[test]
fn reports_zsh_call_sites_registered_by_unexecuted_setup_functions() {
let source = "\
#!/bin/zsh
latent_widget() { print -r -- \"$1\"; }
latent_hook() { print -r -- \"$1\"; }
setup_widget() { zle -N latent-widget latent_widget; }
setup_hook() { add-zsh-hook precmd latent_hook; }
latent_widget
latent_hook
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam)
.with_shell(ShellDialect::Zsh),
);
assert_eq!(diagnostics.len(), 2);
assert_eq!(diagnostics[0].span.slice(source), "latent_widget");
assert_eq!(diagnostics[0].span.start.line, 6);
assert_eq!(diagnostics[1].span.slice(source), "latent_hook");
assert_eq!(diagnostics[1].span.start.line, 7);
}
#[test]
fn reports_zsh_call_sites_with_dynamic_widget_registration() {
let source = "\
#!/bin/zsh
dynamic_widget() { print -r -- \"$1 $2\"; }
zle -N \"$widget_name\" dynamic_widget
dynamic_widget
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam)
.with_shell(ShellDialect::Zsh),
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].span.slice(source), "dynamic_widget");
assert_eq!(diagnostics[0].span.start.line, 4);
}
#[test]
fn ignores_zsh_call_sites_for_special_positional_forwarders() {
let source = "\
#!/bin/zsh
forward() { print -r -- \"$@\"; }
forward
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam)
.with_shell(ShellDialect::Zsh),
);
assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:?}");
}
#[test]
fn ignores_zsh_call_sites_for_argument_count_checks() {
let source = "\
#!/bin/zsh
has_args() { (( $# )) && print -r -- ok; }
has_args
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam)
.with_shell(ShellDialect::Zsh),
);
assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:?}");
}
#[test]
fn ignores_guarded_or_reset_positional_parameters() {
let source = "\
#!/bin/sh
greet() {
set -- hello
echo \"${1:-default}\" \"$#\"
}
greet
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:?}");
}
#[test]
fn ignores_guarded_special_positional_parameters() {
let source = "\
#!/bin/sh
greet() { printf '%s\n' \"${@:-fallback}\"; }
greet
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:?}");
}
#[test]
fn ignores_nested_guarded_special_positional_parameters() {
let source = "\
#!/bin/sh
greet() { printf '%s\n' \"${name:-${@}}\"; }
greet
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:?}");
}
#[test]
fn later_redefinitions_do_not_inherit_argumented_calls_from_earlier_bindings() {
let source = "\
#!/bin/sh
greet() { echo \"$2\"; }
greet ok ok
greet() { echo \"$2\"; }
greet
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].span.slice(source), "greet");
assert_eq!(diagnostics[0].span.start.line, 5);
}
#[test]
fn nested_scopes_resolve_calls_to_the_visible_binding() {
let source = "\
#!/bin/sh
foo() { echo \"$2\"; }
wrapper() {
foo
foo() { :; }
}
wrapper
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].span.slice(source), "foo");
assert_eq!(diagnostics[0].span.start.line, 4);
}
#[test]
fn ignores_plain_set_operands_that_reset_positional_parameters() {
let source = "\
#!/bin/sh
greet() {
set hello world
echo \"$2\"
}
greet
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:?}");
}
#[test]
fn ignores_zero_argument_call_sites_after_set_plus_option_toggle() {
let source = "\
#!/bin/sh
greet() {
set +x
printf '%s\n' \"$2\"
}
greet
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:?}");
}
#[test]
fn ignores_zero_argument_call_sites_after_bare_set_plus_o() {
let source = "\
#!/bin/sh
greet() {
set +o
printf '%s\n' \"$2\"
}
greet
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:?}");
}
#[test]
fn still_reports_zero_argument_call_sites_after_set_minus_option_toggle() {
let source = "\
#!/bin/sh
greet() {
set -x
printf '%s\n' \"$2\"
}
greet
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].span.slice(source), "greet");
assert_eq!(diagnostics[0].span.start.line, 7);
}
#[test]
fn same_name_functions_in_different_scopes_do_not_mask_each_other() {
let source = "\
#!/bin/sh
outer_without_args() {
inner() { echo \"$1 $2\"; }
inner
}
outer_with_args() {
inner() { echo \"$1 $2\"; }
inner ok yes
}
outer_without_args
outer_with_args
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].span.slice(source), "inner");
assert_eq!(diagnostics[0].span.start.line, 4);
}
#[test]
fn nested_command_substitutions_still_count_toward_required_arg_count() {
let source = "\
#!/bin/sh
greet() {
printf '%s\n' \"$(printf '%s' \"$1 $2\")\"
}
greet
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].span.slice(source), "greet");
assert_eq!(diagnostics[0].span.start.line, 6);
}
#[test]
fn backtick_substitutions_still_count_as_zero_arg_calls() {
let source = "\
#!/bin/sh
greet() { printf '%s\n' \"$1 $2\"; }
value=\"`greet`\"
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].span.slice(source), "greet");
}
#[test]
fn earlier_calls_in_same_scope_still_count_toward_mixed_arity() {
let source = "\
#!/bin/sh
greet ok yes
greet() { echo \"$1 $2\"; }
greet
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:?}");
}
#[test]
fn wrapper_resolved_targets_do_not_suppress_direct_zero_arg_reports() {
let source = "\
#!/usr/bin/env bash
greet() { echo \"$1 $2\"; }
command greet ok yes
greet
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].span.slice(source), "greet");
assert_eq!(diagnostics[0].span.start.line, 4);
}
#[test]
fn quoted_static_calls_with_arguments_still_suppress_zero_arg_reports() {
let source = "\
#!/usr/bin/env bash
greet() { echo \"$1 $2\"; }
\"greet\" ok yes
greet
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:?}");
}
#[test]
fn nested_set_commands_still_count_as_positional_parameter_resets() {
let source = "\
#!/bin/sh
greet() {
printf '%s\n' 'hello world' | while read -r first second; do
set -- \"$first\" \"$second\"
printf '%s\n' \"$2\"
done
}
greet
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:?}");
}
#[test]
fn command_substitution_set_does_not_reset_outer_function_positional_parameters() {
let source = "\
#!/bin/sh
greet() {
status=$(set -- hello world)
echo \"$2\"
}
greet
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].span.slice(source), "greet");
assert_eq!(diagnostics[0].span.start.line, 6);
}
#[test]
fn command_substitution_local_reset_still_protects_nested_positional_parameter_use() {
let source = "\
#!/bin/sh
greet() {
status=\"$(
set -- hello world
printf '%s\n' \"$2\"
)\"
}
greet
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:?}");
}
#[test]
fn pipeline_set_does_not_reset_outer_function_positional_parameters() {
let source = "\
#!/bin/sh
greet() {
printf '%s\n' 'hello world' | while read -r first second; do
set -- \"$first\" \"$second\"
done
echo \"$2\"
}
greet
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].span.slice(source), "greet");
assert_eq!(diagnostics[0].span.start.line, 9);
}
#[test]
fn ignores_dynamic_set_operands_that_reset_positional_parameters() {
let source = "\
#!/bin/sh
greet() {
line='hello world'
set $line
echo \"$2\"
}
greet
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:?}");
}
#[test]
fn ignores_calls_with_arguments_inside_parameter_expansion_defaults() {
let source = "\
#!/usr/bin/env bash
GetBuildVersion() {
local build_revision=\"${1}\"
printf '%s\n' \"$build_revision\"
}
BUILD_VERSION=\"${BUILD_VERSION:-\"$(GetBuildVersion \"${BUILD_REVISION}\")\"}\"
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam),
);
assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:?}");
}
#[test]
fn respects_suppressed_paired_function_argument_warning() {
let source = "\
#!/bin/sh
# shellcheck disable=SC2120
greet() { echo \"$1\"; }
greet
";
let parse_result = Parser::new(source).parse().unwrap();
let indexer = Indexer::new(source, &parse_result);
let directives = parse_directives(
source,
indexer.comment_index(),
&ShellCheckCodeMap::default(),
);
let settings = LinterSettings::for_rule(Rule::FunctionReferencesUnsetParam);
let diagnostics = AnalysisRequest::from_parse_result(&parse_result, source, &settings)
.with_directives(&directives)
.lint();
assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:?}");
}
}