use super::*;
#[derive(Debug)]
pub struct Analyzer<'a> {
config: Option<&'a Config>,
document: &'a Document,
}
impl<'a> From<&'a Document> for Analyzer<'a> {
fn from(document: &'a Document) -> Self {
Self {
config: None,
document,
}
}
}
impl<'a> Analyzer<'a> {
#[must_use]
pub fn analyze(&self) -> Vec<Diagnostic> {
let context = RuleContext::new(self.document);
let default = Config::default();
let config = self.config.unwrap_or(&default);
let mut diagnostics = inventory::iter::<&dyn Rule>
.into_iter()
.flat_map(|rule| {
rule
.run(&context)
.into_iter()
.filter_map(move |diagnostic| {
let rule_config = config.rule_config(rule.id());
Some(Diagnostic {
id: rule.id().to_string(),
display: rule.message().to_string(),
severity: rule_config.severity(diagnostic.severity)?,
..diagnostic
})
})
})
.collect::<Vec<_>>();
diagnostics.sort_by(|a, b| {
a.range
.start
.line
.cmp(&b.range.start.line)
.then_with(|| a.range.start.character.cmp(&b.range.start.character))
.then_with(|| a.message.cmp(&b.message))
});
diagnostics
}
#[must_use]
pub fn config(self, config: &'a Config) -> Self {
Self {
config: Some(config),
..self
}
}
}
#[cfg(test)]
mod tests {
use {super::*, indoc::indoc, pretty_assertions::assert_eq};
#[derive(Debug)]
struct Test {
config: Config,
document: Document,
messages: Vec<(&'static str, lsp::Range, Option<lsp::DiagnosticSeverity>)>,
}
impl Test {
fn config(self, config: Config) -> Self {
Self { config, ..self }
}
fn error(self, message: &'static str, range: lsp::Range) -> Self {
Self {
messages: self
.messages
.into_iter()
.chain([(message, range, Some(lsp::DiagnosticSeverity::ERROR))])
.collect(),
..self
}
}
fn new(content: &str) -> Self {
let uri = if cfg!(windows) {
"file:///C:/test.just"
} else {
"file:///test.just"
};
Self {
config: Config::default(),
document: Document::try_from(lsp::DidOpenTextDocumentParams {
text_document: lsp::TextDocumentItem {
uri: lsp::Url::parse(uri).unwrap(),
language_id: "just".to_string(),
version: 1,
text: content.to_string(),
},
})
.unwrap(),
messages: Vec::new(),
}
}
fn run(self) {
let Test {
config,
document,
messages,
} = self;
let analyzer = Analyzer::from(&document).config(&config);
let diagnostics = analyzer
.analyze()
.into_iter()
.map(lsp::Diagnostic::from)
.collect::<Vec<lsp::Diagnostic>>();
assert_eq!(
diagnostics.len(),
messages.len(),
"Expected diagnostics {:?} but got {:?}",
messages,
diagnostics,
);
for (diagnostic, (expected_message, expected_range, expected_severity)) in
diagnostics.into_iter().zip(messages)
{
assert_eq!(diagnostic.severity, expected_severity, "{diagnostic:?}");
assert_eq!(diagnostic.message, expected_message);
assert_eq!(diagnostic.range, expected_range);
}
}
fn warning(self, message: &'static str, range: lsp::Range) -> Self {
Self {
messages: self
.messages
.into_iter()
.chain([(message, range, Some(lsp::DiagnosticSeverity::WARNING))])
.collect(),
..self
}
}
}
#[test]
fn accepts_logical_operators() {
Test::new(indoc! {
"
foo := '' || 'bar'
bar := 'foo' && 'bar'
baz:
echo {{ foo }}
echo {{ bar }}
"
})
.run();
}
#[test]
fn alias_recipe_conflict_alias_then_recipe() {
Test::new(indoc! {
"
alias t := other
other:
echo \"other\"
t:
echo \"recipe\"
"
})
.error(
"Alias `t` is redefined as a recipe",
lsp::Range::at(5, 0, 5, 1),
)
.run();
}
#[test]
fn alias_recipe_conflict_recipe_then_alias() {
Test::new(indoc! {
"
other:
echo \"other\"
t:
echo \"recipe\"
alias t := other
"
})
.error(
"Recipe `t` is redefined as an alias",
lsp::Range::at(6, 6, 6, 7),
)
.run();
}
#[test]
fn aliases_basic() {
Test::new(indoc! {
"
foo:
echo \"foo\"
alias bar := foo
"
})
.run();
}
#[test]
fn aliases_duplicate() {
Test::new(indoc! {
"
foo:
echo \"foo\"
alias bar := foo
alias bar := foo
alias bar := foo
"
})
.error("Duplicate alias `bar`", lsp::Range::at(4, 0, 4, 16))
.error("Duplicate alias `bar`", lsp::Range::at(5, 0, 5, 16))
.run();
}
#[test]
fn aliases_missing_recipe() {
Test::new(indoc! {
"
foo:
echo \"foo\"
alias bar := baz
"
})
.error("Recipe `baz` not found", lsp::Range::at(3, 13, 3, 16))
.run();
}
#[test]
fn aliases_missing_target() {
Test::new(indoc! {
"
alias foo :=
"
})
.error("Missing identifier in alias", lsp::Range::at(0, 12, 0, 12))
.error("Recipe `` not found", lsp::Range::at(0, 12, 0, 12))
.run();
}
#[test]
fn all_four_os_groups_no_conflict() {
Test::new(indoc! {
"
[linux]
build:
echo \"Building on Linux\"
[macos]
build:
echo \"Building on macOS\"
[windows]
build:
echo \"Building on Windows\"
[dragonfly]
build:
echo \"Building on DragonFly BSD\"
[freebsd]
build:
echo \"Building on FreeBSD\"
[netbsd]
build:
echo \"Building on NetBSD\"
[openbsd]
build:
echo \"Building on OpenBSD\"
"
})
.run();
}
#[test]
fn analyze_complete() {
Test::new(indoc! {
"
foo:
echo \"foo\"
bar: missing
echo \"bar\"
alias baz := nonexistent
"
})
.error("Recipe `missing` not found", lsp::Range::at(3, 5, 3, 12))
.error(
"Recipe `nonexistent` not found",
lsp::Range::at(6, 13, 6, 24),
)
.run();
}
#[test]
fn arg_attribute_empty_parens() {
Test::new(indoc! {
"
[arg()]
bar foo:
echo {{foo}}
"
})
.error(
"Attribute `arg` got 0 arguments but takes at least 1 argument",
lsp::Range::at(0, 0, 1, 0),
)
.error("Missing identifier in value", lsp::Range::at(0, 5, 0, 5))
.run();
}
#[test]
fn arg_attribute_missing_parameter_name() {
Test::new(indoc! {
"
[arg]
bar foo:
echo {{foo}}
"
})
.error(
"Attribute `arg` got 0 arguments but takes at least 1 argument",
lsp::Range::at(0, 0, 1, 0),
)
.run();
}
#[test]
fn arg_attribute_unknown_kwarg() {
Test::new(indoc! {
"
[arg('name', bogus='x')]
foo name:
echo {{name}}
"
})
.error(
"Unknown `[arg]` keyword `bogus`, expected one of help, long, short, value, pattern",
lsp::Range::at(0, 13, 0, 22))
.run();
}
#[test]
fn arg_attribute_unknown_parameter() {
Test::new(indoc! {
"
[arg('missing', help='nope')]
foo name:
echo {{name}}
"
})
.error(
"`[arg]` references unknown parameter `missing`",
lsp::Range::at(0, 5, 0, 14),
)
.run();
}
#[test]
fn arg_attribute_valid() {
Test::new(indoc! {
"
[arg('foo', help=\"Help text\")]
bar foo:
echo {{foo}}
"
})
.run();
}
#[test]
fn arg_attribute_value_requires_long_or_short() {
Test::new(indoc! {
"
[arg('name', value='hi')]
foo name:
echo {{name}}
"
})
.error(
"`[arg]` `value=` requires `long=` or `short=`",
lsp::Range::at(0, 13, 0, 23),
)
.run();
}
#[test]
fn arg_attribute_value_with_long_ok() {
Test::new(indoc! {
"
[arg('name', long='name', value='hi')]
foo name:
echo {{name}}
"
})
.run();
}
#[test]
fn arg_attribute_with_long_option() {
Test::new(indoc! {
"
[arg('foo', long=\"foo-opt\")]
bar foo:
echo {{foo}}
"
})
.run();
}
#[test]
fn arg_attribute_with_multiple_options() {
Test::new(indoc! {
"
[arg('foo', long=\"foo-opt\", short=\"f\", value=\"default\")]
bar foo:
echo {{foo}}
"
})
.run();
}
#[test]
fn arg_attribute_with_pattern() {
Test::new(indoc! {
"
[arg('version', pattern=\"[0-9]+\\\\.[0-9]+\\\\.[0-9]+\")]
release version:
echo {{version}}
"
})
.run();
}
#[test]
fn arg_attribute_with_short_option() {
Test::new(indoc! {
"
[arg('foo', short=\"f\")]
bar foo:
echo {{foo}}
"
})
.run();
}
#[test]
fn assert_accepts_condition_operators() {
Test::new(indoc! {
"
foo name:
{{ assert(name == \"bar\", \"msg\") }}
{{ assert(name != \"bar\", \"msg\") }}
{{ assert(name =~ \"^[a-z]+$\", \"msg\") }}
"
})
.run();
}
#[test]
fn attribute_expression_requires_support() {
Test::new(indoc! {
"
foo := 'foo'
[group(foo)]
[group(f'bar')]
foo:
echo \"foo\"
"
})
.error(
"Attribute `group` arguments must be string literals",
lsp::Range::at(2, 7, 2, 10),
)
.error(
"Attribute `group` arguments must be string literals",
lsp::Range::at(3, 7, 3, 13),
)
.run();
}
#[test]
fn attribute_string_literal_expression() {
Test::new(indoc! {
"
[group('foo')]
[metadata(x'bar')]
foo:
echo \"foo\"
"
})
.run();
}
#[test]
fn attributes_correct() {
Test::new(indoc! {
"
[no-cd]
[linux]
[macos]
foo:
echo \"foo\"
[doc('Recipe documentation')]
bar:
echo \"bar\"
[default]
baz:
echo \"baz\"
"
})
.run();
}
#[test]
fn attributes_duplicate_default_between_recipes() {
Test::new(indoc! {
"
[default]
check:
echo \"check\"
[default]
ci:
echo \"ci\"
"
})
.error(
"Recipe `ci` has duplicate `[default]` attribute, which may only appear once per module", lsp::Range::at(4, 0, 5, 0))
.run();
}
#[test]
fn attributes_duplicate_default_on_same_recipe() {
Test::new(indoc! {
"
[default]
[default]
build:
echo \"build\"
"
})
.error(
"Recipe `build` has duplicate `[default]` attribute, which may only appear once per module", lsp::Range::at(1, 0, 2, 0))
.run();
}
#[test]
fn attributes_duplicate_group_attribute() {
Test::new(indoc! {
"
[group('dev')]
[group('dev')]
build:
echo \"build\"
"
})
.error(
"Recipe attribute `group` is duplicated",
lsp::Range::at(1, 0, 2, 0),
)
.run();
}
#[test]
fn attributes_duplicate_recipe_attribute() {
Test::new(indoc! {
"
[script]
[script]
build:
echo \"build\"
"
})
.error(
"Recipe attribute `script` is duplicated",
lsp::Range::at(1, 0, 2, 0),
)
.run();
}
#[test]
fn attributes_duplicate_working_directory_attribute() {
Test::new(indoc! {
"
[working-directory: 'foo']
[working-directory: 'bar']
build:
echo \"build\"
"
})
.error(
"Recipe attribute `working-directory` is duplicated",
lsp::Range::at(1, 0, 2, 0),
)
.run();
}
#[test]
fn attributes_extra_arguments() {
Test::new(indoc! {
"
[linux('invalid')]
foo:
echo \"foo\"
"
})
.error(
"Attribute `linux` got 1 argument but takes 0 arguments",
lsp::Range::at(0, 0, 1, 0),
)
.run();
Test::new(indoc! {
"
[default('invalid')]
foo:
echo \"foo\"
"
})
.error(
"Attribute `default` got 1 argument but takes 0 arguments",
lsp::Range::at(0, 0, 1, 0),
)
.run();
}
#[test]
fn attributes_inline_parameters_focused() {
Test::new(indoc! {
"
[group: 'foo', no-cd]
foo:
echo \"foo\"
"
})
.run();
}
#[test]
fn attributes_invalid_inline() {
Test::new(indoc! {
"
[group: 'foo', foo]
foo:
echo \"foo\"
"
})
.error("Unknown attribute `foo`", lsp::Range::at(0, 15, 0, 18))
.run();
}
#[test]
fn attributes_metadata_multiple_arguments() {
Test::new(indoc! {
"
[metadata('foo', 'bar')]
foo:
echo \"foo\"
"
})
.run();
}
#[test]
fn attributes_missing_arguments() {
Test::new(indoc! {
"
[extension]
foo:
#!/usr/bin/env bash
echo \"foo\"
"
})
.error(
"Attribute `extension` got 0 arguments but takes 1 argument",
lsp::Range::at(0, 0, 1, 0),
)
.run();
}
#[test]
fn attributes_more_arguments_than_required() {
Test::new(indoc! {
"
[group('foo', 'bar')]
foo:
echo \"foo\"
"
})
.error(
"Attribute `group` got 2 arguments but takes 1 argument",
lsp::Range::at(0, 0, 1, 0),
)
.run();
}
#[test]
fn attributes_multiple_group_attributes_allowed() {
Test::new(indoc! {
"
[group('lint')]
[group('rust')]
build:
echo \"build\"
"
})
.run();
}
#[test]
fn attributes_multiple_metadata_allowed() {
Test::new(indoc! {
"
[metadata('foo', 'bar')]
[metadata('baz', 'qux')]
foo:
echo \"foo\"
"
})
.run();
}
#[test]
fn attributes_no_cd_allowed_with_global_working_directory() {
Test::new(indoc! {
"
set working-directory := '/tmp'
[no-cd]
build:
echo \"build\"
"
})
.run();
}
#[test]
fn attributes_no_parameters_needed() {
Test::new(indoc! {
"
[script]
foo:
echo \"foo\"
"
})
.run();
Test::new(indoc! {
"
[confirm]
foo:
echo \"foo\"
"
})
.run();
Test::new(indoc! {
"
[default]
foo:
echo \"foo\"
"
})
.run();
}
#[test]
fn attributes_on_assignments() {
Test::new(indoc! {
"
[private]
secret := \"secret value\"
[private]
_db_url := \"postgres://user:pass@host:port/db\"
public_var := \"public value\"
test:
echo {{ secret }}
echo {{ _db_url }}
echo {{ public_var }}
"
})
.run();
}
#[test]
fn attributes_on_exported_assignments() {
Test::new(indoc! {
"
[private]
export PATH := '/usr/local/bin'
"
})
.run();
}
#[test]
fn attributes_unknown() {
Test::new(indoc! {
"
[unknown_attribute]
foo:
echo \"foo\"
"
})
.error(
"Unknown attribute `unknown_attribute`",
lsp::Range::at(0, 1, 0, 18),
)
.run();
}
#[test]
fn attributes_working_directory_conflicts_with_no_cd() {
Test::new(indoc! {
"
[no-cd]
[working-directory: '/tmp']
build:
echo \"build\"
"
})
.error(
"Recipe `build` can't combine `[working-directory]` with `[no-cd]`",
lsp::Range::at(1, 0, 2, 0),
)
.run();
}
#[test]
fn attributes_exit_message_conflicts_with_no_exit_message() {
Test::new(indoc! {
"
[exit-message]
[no-exit-message]
build:
echo \"build\"
"
})
.error(
"Recipe `build` can't combine `[exit-message]` with `[no-exit-message]`",
lsp::Range::at(0, 0, 1, 0),
)
.run();
}
#[test]
fn attributes_wrong_target() {
Test::new(indoc! {
"
[group: 'foo']
alias f := foo
foo:
echo \"foo\"
"
})
.error(
"Attribute `group` cannot be applied to alias target",
lsp::Range::at(0, 0, 1, 0),
)
.run();
}
#[test]
fn bsd_os_specific_no_conflict() {
Test::new(indoc! {
"
[dragonfly]
build:
echo \"Building on DragonFly BSD\"
[freebsd]
build:
echo \"Building on FreeBSD\"
[netbsd]
build:
echo \"Building on NetBSD\"
[openbsd]
build:
echo \"Building on OpenBSD\"
"
})
.run();
}
#[test]
fn circular_dependencies_long_chain() {
Test::new(indoc! {
"
foo: bar
echo \"foo\"
bar: baz
echo \"bar\"
baz: foo
echo \"baz\"
"
})
.error(
"Recipe `foo` has circular dependency `foo -> bar -> baz -> foo`",
lsp::Range::at(0, 0, 3, 0),
)
.error(
"Recipe `bar` has circular dependency `bar -> baz -> foo -> bar`",
lsp::Range::at(3, 0, 6, 0),
)
.error(
"Recipe `baz` has circular dependency `baz -> foo -> bar -> baz`",
lsp::Range::at(6, 0, 8, 0),
)
.run();
}
#[test]
fn circular_dependencies_multiple_cycles() {
Test::new(indoc! {
"
a: b
echo \"a\"
b: a
echo \"b\"
x: y
echo \"x\"
y: z
echo \"y\"
z: x
echo \"z\"
"
})
.error(
"Recipe `a` has circular dependency `a -> b -> a`",
lsp::Range::at(0, 0, 3, 0),
)
.error(
"Recipe `b` has circular dependency `b -> a -> b`",
lsp::Range::at(3, 0, 6, 0),
)
.error(
"Recipe `x` has circular dependency `x -> y -> z -> x`",
lsp::Range::at(6, 0, 9, 0),
)
.error(
"Recipe `y` has circular dependency `y -> z -> x -> y`",
lsp::Range::at(9, 0, 12, 0),
)
.error(
"Recipe `z` has circular dependency `z -> x -> y -> z`",
lsp::Range::at(12, 0, 14, 0),
)
.run();
}
#[test]
fn circular_dependencies_only_flags_cycle_members() {
Test::new(indoc! {
"
foo: bar
echo \"foo\"
bar: baz
echo \"bar\"
baz: bar
echo \"baz\"
"
})
.error(
"Recipe `bar` has circular dependency `bar -> baz -> bar`",
lsp::Range::at(3, 0, 6, 0),
)
.error(
"Recipe `baz` has circular dependency `baz -> bar -> baz`",
lsp::Range::at(6, 0, 8, 0),
)
.run();
}
#[test]
fn circular_dependencies_self() {
Test::new(indoc! {
"
foo: foo
echo \"foo\"
"
})
.error("Recipe `foo` depends on itself", lsp::Range::at(0, 0, 2, 0))
.run();
}
#[test]
fn circular_dependencies_simple() {
Test::new(indoc! {
"
foo: bar
echo \"foo\"
bar: foo
echo \"bar\"
"
})
.error(
"Recipe `foo` has circular dependency `foo -> bar -> foo`",
lsp::Range::at(0, 0, 3, 0),
)
.error(
"Recipe `bar` has circular dependency `bar -> foo -> bar`",
lsp::Range::at(3, 0, 5, 0),
)
.run();
}
#[test]
fn circular_dependencies_with_multiple_dependencies() {
Test::new(indoc! {
"
foo: bar baz
echo \"foo\"
bar:
echo \"bar\"
baz: qux
echo \"baz\"
qux: foo
echo \"qux\"
"
})
.error(
"Recipe `foo` has circular dependency `foo -> baz -> qux -> foo`",
lsp::Range::at(0, 0, 3, 0),
)
.error(
"Recipe `baz` has circular dependency `baz -> qux -> foo -> baz`",
lsp::Range::at(6, 0, 9, 0),
)
.error(
"Recipe `qux` has circular dependency `qux -> foo -> baz -> qux`",
lsp::Range::at(9, 0, 11, 0),
)
.run();
}
#[test]
fn comma_separated_os_attributes_no_conflict() {
Test::new(indoc! {
"
[private, unix]
hello:
@echo 'hello'
[private, windows]
hello:
@echo hello
"
})
.run();
}
#[test]
fn comma_separated_os_attributes_with_conflict() {
Test::new(indoc! {
"
[private, linux]
hello:
@echo 'hello on linux'
[private, linux]
hello:
@echo 'hello on linux again'
"
})
.error("Duplicate recipe name `hello`", lsp::Range::at(4, 0, 7, 0))
.run();
}
#[test]
fn comma_separated_unix_windows_no_conflict() {
Test::new(indoc! {
"
[unix]
[private]
build:
@echo 'building on unix'
[private, windows]
build:
@echo 'building on windows'
"
})
.run();
}
#[test]
fn confirm_attribute_accepts_expression() {
Test::new(indoc! {
r#"
[confirm("Deploy to " + env + "?")]
deploy env:
echo {{env}}
"#
})
.run();
}
#[test]
fn cross_parameter_default_references_preceding_parameter() {
Test::new(indoc! {
"
a := 'foo'
foo a b=a:
echo {{ a }} {{ b }}
"
})
.warning("Variable `a` appears unused", lsp::Range::at(0, 0, 0, 1))
.run();
}
#[test]
fn default_parameter_expression_functions() {
Test::new(indoc! {
"
build version=uppercase(\"1.0.0\"):
echo {{ version }}
"
})
.run();
}
#[test]
fn default_parameter_expression_with_env_call() {
Test::new(indoc! {
"
build target=(env('TARGET', 'debug')):
echo {{ target }}
"
})
.run();
}
#[test]
fn dir_aliases_recognized() {
Test::new(indoc! {
"
foo:
echo {{ home_dir() }}
echo {{ cache_dir() }}
echo {{ config_dir() }}
echo {{ config_local_dir() }}
echo {{ data_dir() }}
echo {{ data_local_dir() }}
echo {{ executable_dir() }}
echo {{ invocation_dir() }}
echo {{ invocation_dir_native() }}
echo {{ justfile_dir() }}
echo {{ source_dir() }}
echo {{ parent_dir('~/.config') }}
"
})
.run();
}
#[test]
fn duplicate_dragonfly_attribute() {
Test::new(indoc! {
"
[dragonfly]
[dragonfly]
build:
echo \"foo\"
"
})
.error(
"Recipe attribute `dragonfly` is duplicated",
lsp::Range::at(1, 0, 2, 0),
)
.run();
}
#[test]
fn duplicate_freebsd_attribute() {
Test::new(indoc! {
"
[freebsd]
[freebsd]
build:
echo \"foo\"
"
})
.error(
"Recipe attribute `freebsd` is duplicated",
lsp::Range::at(1, 0, 2, 0),
)
.run();
}
#[test]
fn duplicate_netbsd_attribute() {
Test::new(indoc! {
"
[netbsd]
[netbsd]
build:
echo \"foo\"
"
})
.error(
"Recipe attribute `netbsd` is duplicated",
lsp::Range::at(1, 0, 2, 0),
)
.run();
}
#[test]
fn duplicate_recipe_names() {
Test::new(indoc! {
"
foo:
echo foo
foo:
echo foo
foo:
echo foo
"
})
.error("Duplicate recipe name `foo`", lsp::Range::at(3, 0, 6, 0))
.error("Duplicate recipe name `foo`", lsp::Range::at(6, 0, 8, 0))
.run();
}
#[test]
fn duplicate_recipe_names_allowed_via_setting() {
Test::new(indoc! {
"
set allow-duplicate-recipes := true
foo:
echo foo
[linux]
foo:
echo foo on linux
"
})
.run();
}
#[test]
fn duplicate_recipes_with_same_os_attribute() {
Test::new(indoc! {
"
[linux]
build:
echo \"Building on Linux version 1\"
[linux]
build:
echo \"Building on Linux version 2\"
"
})
.error("Duplicate recipe name `build`", lsp::Range::at(4, 0, 7, 0))
.run();
}
#[test]
fn duplicate_variable_assignments() {
Test::new(indoc! {
"
foo := \"one\"
foo := \"two\"
recipe:
echo {{ foo }}
"
})
.error("Duplicate variable `foo`", lsp::Range::at(1, 0, 2, 0))
.run();
}
#[test]
fn duplicate_variable_assignments_allowed_via_setting() {
Test::new(indoc! {
"
set allow-duplicate-variables := true
foo := \"one\"
foo := \"two\"
recipe:
echo {{ foo }}
"
})
.run();
}
#[test]
fn env_attribute_duplicate_var_name() {
Test::new(indoc! {
"
[env('FOO', 'bar')]
[env('FOO', 'baz')]
foo:
echo \"$FOO\"
"
})
.error(
"Recipe attribute `env` is duplicated",
lsp::Range::at(1, 0, 2, 0),
)
.run();
}
#[test]
fn env_attribute_missing_value() {
Test::new(indoc! {
"
[env('FOO')]
foo:
echo \"$FOO\"
"
})
.error(
"Attribute `env` got 1 argument but takes 2 arguments",
lsp::Range::at(0, 0, 1, 0),
)
.run();
}
#[test]
fn env_attribute_multiple_vars_allowed() {
Test::new(indoc! {
"
[env('FOO', 'bar')]
[env('BAZ', 'qux')]
foo:
echo \"$FOO $BAZ\"
"
})
.run();
}
#[test]
fn env_attribute_too_many_arguments() {
Test::new(indoc! {
"
[env('FOO', 'bar', 'extra')]
foo:
echo \"$FOO\"
"
})
.error(
"Attribute `env` got 3 arguments but takes 2 arguments",
lsp::Range::at(0, 0, 1, 0),
)
.run();
}
#[test]
fn env_attribute_valid() {
Test::new(indoc! {
"
[env('FOO', 'bar')]
foo:
echo \"$FOO\"
"
})
.run();
}
#[test]
fn env_attribute_wrong_target() {
Test::new(indoc! {
"
[env('FOO', 'bar')]
alias baz := foo
foo:
echo \"foo\"
"
})
.error(
"Attribute `env` cannot be applied to alias target",
lsp::Range::at(0, 0, 1, 0),
)
.run();
}
#[test]
fn escaped_braces_are_treated_as_literal_text() {
Test::new(indoc! {
"
test:
echo \"{{{{hello}}\"
"
})
.run();
}
#[test]
fn exported_variables_not_warned() {
Test::new(indoc! {
"
foo := \"unused value\"
export bar := \"exported but unused\"
baz := \"used value\"
recipe:
echo {{ baz }}
"
})
.warning("Variable `foo` appears unused", lsp::Range::at(0, 0, 0, 3))
.run();
}
#[test]
fn expression_attribute_arguments() {
Test::new(indoc! {
"
bar := 'bar'
[confirm('foo' / bar)]
[env('FOO', bar)]
[working-directory('foo' / bar)]
foo:
echo \"foo\"
"
})
.run();
}
#[test]
fn extension_with_script_is_allowed() {
Test::new(indoc! {
"
[script]
[extension: '.sh']
foo:
echo \"foo\"
"
})
.run();
}
#[test]
fn extension_with_shebang_is_allowed() {
Test::new(indoc! {
"
[extension: '.sh']
foo:
#!/usr/bin/env bash
echo \"foo\"
"
})
.run();
}
#[test]
fn extension_without_script_or_shebang() {
Test::new(indoc! {
"
[extension: '.sh']
foo:
echo \"foo\"
"
})
.error(
"Recipe `foo` uses `[extension]` without `[script]` or a shebang",
lsp::Range::at(0, 0, 1, 0),
)
.run();
}
#[test]
fn format_strings_mark_variables_as_used() {
Test::new(indoc! {
r#"
name := "world"
greeting := f'Hello, {{name}}!'
foo:
echo {{greeting}}
"#
})
.run();
}
#[test]
fn format_strings_with_function_calls() {
Test::new(indoc! {
r"
info := f'arch: {{arch()}}'
foo:
echo {{info}}
"
})
.run();
}
#[test]
fn format_strings_with_undefined_variables() {
Test::new(indoc! {
r"
greeting := f'Hello, {{undefined_var}}!'
foo:
echo {{greeting}}
"
})
.error(
"Variable `undefined_var` not found",
lsp::Range::at(0, 23, 0, 36),
)
.run();
}
#[test]
fn format_strings_with_valid_variables() {
Test::new(indoc! {
r#"
name := "world"
greeting := f'Hello, {{name}}!'
foo:
echo {{greeting}}
"#
})
.run();
}
#[test]
fn function_calls_correct() {
Test::new(indoc! {
"
foo:
echo {{ arch() }}
echo {{ join(\"a\", \"b\", \"c\") }}
"
})
.run();
}
#[test]
fn function_calls_nested() {
Test::new(indoc! {
"
foo:
echo {{ replace(parent_directory('~/.config/nvim/init.lua'), '.', 'dot-') }}
"
})
.run();
}
#[test]
fn function_calls_too_few_args() {
Test::new(indoc! {
"
foo:
echo {{ replace() }}
"
})
.error(
"Function `replace` requires at least 3 arguments, but 0 provided",
lsp::Range::at(1, 10, 1, 19),
)
.run();
}
#[test]
fn function_calls_too_many_args() {
Test::new(indoc! {
"
foo:
echo {{ uppercase(\"hello\", \"extra\") }}
"
})
.error(
"Function `uppercase` accepts 1 argument, but 2 provided",
lsp::Range::at(1, 10, 1, 37),
)
.run();
}
#[test]
fn function_calls_unknown() {
Test::new(indoc! {
"
foo:
echo {{ unknown_function() }}
"
})
.error(
"Unknown function `unknown_function`",
lsp::Range::at(1, 10, 1, 26),
)
.run();
}
#[test]
fn import_format_string_skipped() {
Test::new(indoc! {
r#"
import f"{'nonexistent.just'}"
"#
})
.run();
}
#[test]
fn import_invalid_path() {
let expected = if cfg!(windows) {
"Import path does not exist: `C:\\nonexistent.just`"
} else {
"Import path does not exist: `/nonexistent.just`"
};
Test::new(indoc! {
"
import 'nonexistent.just'
"
})
.error(expected, lsp::Range::at(0, 7, 0, 25))
.run();
}
#[test]
fn import_optional_invalid_path() {
Test::new(indoc! {
"
import? 'nonexistent.just'
"
})
.run();
}
#[test]
fn import_shell_expanded_string_skipped() {
Test::new(indoc! {
"
import x'nonexistent.just'
"
})
.run();
}
#[test]
fn linux_openbsd_no_conflict() {
Test::new(indoc! {
"
[linux]
build:
echo \"Building on Linux\"
[openbsd]
build:
echo \"Building on OpenBSD\"
"
})
.run();
}
#[test]
fn linux_unix_conflict() {
Test::new(indoc! {
"
[linux]
build:
echo \"Building on Linux\"
[unix]
build:
echo \"Building on Unix systems\"
"
})
.error("Duplicate recipe name `build`", lsp::Range::at(4, 0, 7, 0))
.run();
}
#[test]
fn mixed_os_specific_and_regular_recipe() {
Test::new(indoc! {
"
[linux]
build:
echo \"Building on Linux\"
build:
echo \"Building on any OS\"
"
})
.error("Duplicate recipe name `build`", lsp::Range::at(4, 0, 6, 0))
.run();
}
#[test]
fn module_attributes_group() {
Test::new(indoc! {
"
[group: 'tools']
mod foo
"
})
.run();
}
#[test]
fn openbsd_macos_no_conflict() {
Test::new(indoc! {
"
[openbsd]
build:
echo \"Building on OpenBSD\"
[macos]
build:
echo \"Building on macOS\"
"
})
.run();
}
#[test]
fn os_specific_duplicate_recipes() {
Test::new(indoc! {
"
[linux]
build:
echo \"Building on Linux\"
[windows]
build:
echo \"Building on Windows\"
[macos]
build:
echo \"Building on macOS\"
"
})
.run();
}
#[test]
fn parallel_with_single_dependency_warns() {
Test::new(indoc! {
"
[parallel]
foo: bar
echo \"foo\"
bar:
echo \"bar\"
"
})
.warning(
"Recipe `foo` has only one dependency, so `[parallel]` has no effect",
lsp::Range::at(0, 0, 1, 0),
)
.run();
}
#[test]
fn parallel_without_dependencies_warns() {
Test::new(indoc! {
"
[parallel]
foo:
echo \"foo\"
"
})
.warning(
"Recipe `foo` has no dependencies, so `[parallel]` has no effect",
lsp::Range::at(0, 0, 1, 0),
)
.run();
}
#[test]
fn parameter_default_references_preceding_parameter() {
Test::new(indoc! {
"
@binstall crate bin=crate:
which {{bin}} 2>&1 >/dev/null || cargo binstall -y {{crate}}
"
})
.run();
}
#[test]
fn parenthesized_expression_default_uses_global_variable() {
Test::new(indoc! {
"
foo := 'foo'
bar := 'bar'
recipe x=(foo + bar):
echo {{ x }}
"
})
.run();
}
#[test]
fn parser_errors_invalid() {
Test::new(indoc! {
"
foo
echo \"foo\"
"
})
.error(
"Syntax error near `foo echo \"foo\"`",
lsp::Range::at(0, 0, 2, 0),
)
.run();
}
#[test]
fn parser_errors_valid() {
Test::new(indoc! {
"
foo:
echo \"foo\"
"
})
.run();
}
#[test]
fn parser_errors_valid_with_recipe_line_containing_only_open_brace() {
Test::new(indoc! {
r#"
foo bar="baz" qux="quux":
#!/usr/bin/env bash
cat <<'JSON'
{
"foo": "bar",
"{{ bar }}": "{{ qux }}"
}
JSON
"#
})
.run();
}
#[test]
fn parser_errors_valid_with_shell_expanded_strings() {
Test::new(indoc! {
r#"
import x'~/.config/just/common.just'
greeting := x"~/$USER/${GREETING:-hello}"
foo:
echo {{greeting}}
"#
})
.run();
}
#[test]
fn positional_arguments_attribute_marks_parameters_as_used() {
Test::new(indoc! {
"
[positional-arguments]
graph log:
./bin/graph $1
"
})
.run();
}
#[test]
fn positional_arguments_attribute_scope_is_limited() {
Test::new(indoc! {
"
[positional-arguments]
graph log:
./bin/graph $1
other data:
./bin/graph $1
"
})
.warning(
"Parameter `data` appears unused",
lsp::Range::at(4, 6, 4, 10),
)
.run();
}
#[test]
fn positional_arguments_disabled_still_warns() {
Test::new(indoc! {
"
graph log:
./bin/graph $1
"
})
.warning("Parameter `log` appears unused", lsp::Range::at(0, 6, 0, 9))
.run();
}
#[test]
fn positional_arguments_dollar_at_marks_all_as_used() {
Test::new(indoc! {
r#"
[positional-arguments]
run *args:
#!/usr/bin/env bash
exec "$@"
"#
})
.run();
}
#[test]
fn positional_arguments_only_mark_used_indices() {
Test::new(indoc! {
"
set positional-arguments := true
graph first second:
./bin/graph $2
"
})
.warning(
"Parameter `first` appears unused",
lsp::Range::at(2, 6, 2, 11),
)
.run();
}
#[test]
fn positional_arguments_setting_handles_multiple_parameters() {
Test::new(indoc! {
"
set positional-arguments := true
graph first second third:
./bin/graph $1 ${2} $3
"
})
.run();
}
#[test]
fn positional_arguments_setting_handles_multiple_parameters_unused() {
Test::new(indoc! {
"
set positional-arguments := true
graph first second third fourth:
./bin/graph $1 ${2} $3
"
})
.warning(
"Parameter `fourth` appears unused",
lsp::Range::at(2, 25, 2, 31),
)
.run();
}
#[test]
fn positional_arguments_setting_marks_parameters_as_used() {
Test::new(indoc! {
"
set positional-arguments := true
graph log:
./bin/graph $1
"
})
.run();
}
#[test]
fn positional_arguments_shebang_marks_all_as_used() {
Test::new(indoc! {
r"
[positional-arguments]
run *args:
#!/usr/bin/env -S deno run
console.log(Deno.args)
"
})
.run();
}
#[test]
fn recipe_consistent_indentation() {
Test::new("foo:\n echo \"foo\"\n echo \"bar\"\n").run();
}
#[test]
fn recipe_dependencies_correct() {
Test::new(indoc! {
"
foo:
echo \"foo\"
bar: foo
echo \"bar\"
"
})
.run();
}
#[test]
fn recipe_dependencies_duplicate_warns() {
Test::new(indoc! {
"
foo:
echo \"foo\"
bar: foo foo
echo \"bar\"
"
})
.warning(
"Recipe `bar` lists dependency `foo` more than once; just only runs it once, so it's redundant",
lsp::Range::at(3, 9, 3, 12))
.run();
}
#[test]
fn recipe_dependencies_duplicate_with_arguments_warns() {
Test::new(indoc! {
"
foo arg1:
echo \"{{arg1}}\"
bar: (foo `a`) (foo `a`)
echo \"bar\"
"
})
.warning(
"Recipe `bar` lists dependency `foo` with the same arguments more than once; just only runs it once, so it's redundant",
lsp::Range::at(3, 15, 3, 24))
.run();
}
#[test]
fn recipe_dependencies_missing() {
Test::new(indoc! {
"
foo:
echo \"foo\"
bar: baz
echo \"bar\"
"
})
.error("Recipe `baz` not found", lsp::Range::at(3, 5, 3, 8))
.run();
}
#[test]
fn recipe_dependencies_multiple_missing() {
Test::new(indoc! {
"
foo:
echo \"foo\"
bar: missing1 missing2
echo \"bar\"
"
})
.error("Recipe `missing1` not found", lsp::Range::at(3, 5, 3, 13))
.error("Recipe `missing2` not found", lsp::Range::at(3, 14, 3, 22))
.run();
}
#[test]
fn recipe_dependencies_with_different_arguments_no_warning() {
Test::new(indoc! {
"
foo arg1:
echo \"{{arg1}}\"
bar: (foo `a`) (foo `b`)
echo \"bar\"
"
})
.run();
}
#[test]
fn recipe_dependencies_with_expressions() {
Test::new(indoc! {
"
recipe-a param:
echo {{param}}
recipe-b param: (recipe-a (\"##\" + param + \"##\"))
echo \"recipe-b called with {{param}}\"
"
})
.run();
}
#[test]
fn recipe_dependencies_with_multiple_expression_arguments() {
Test::new(indoc! {
"
recipe-a a b:
echo {{a}} {{b}}
recipe-b param: (recipe-a (\"1\") (\"2\"))
echo \"recipe-b called with {{param}}\"
"
})
.run();
}
#[test]
fn recipe_inconsistent_indentation_between_lines() {
Test::new("foo:\n echo \"foo\"\n echo \"bar\"\n")
.error(
"Recipe line has inconsistent leading whitespace. Recipe started with `␠␠␠␠␠␠␠␠` but found line with `␠␠`", lsp::Range::at(3, 0, 3, 2))
.run();
}
#[test]
fn recipe_invocation_argument_count_correct() {
Test::new(indoc! {
"
foo arg1 arg2=\"default\":
echo \"{{arg1}} {{arg2}}\"
bar: (foo `value1`)
echo \"bar\"
"
})
.run();
}
#[test]
fn recipe_invocation_missing_args() {
Test::new(indoc! {
"
foo arg1 arg2:
echo \"{{arg1}} {{arg2}}\"
bar: (foo)
echo \"bar\"
"
})
.error(
"Dependency `foo` requires 2 arguments, but 0 provided",
lsp::Range::at(3, 5, 3, 10),
)
.run();
}
#[test]
fn recipe_invocation_one_or_more_variadic_requires_argument() {
Test::new(indoc! {
"
foo +args:
echo \"{{args}}\"
bar: (foo)
echo \"bar\"
"
})
.error(
"Dependency `foo` requires 1 argument, but 0 provided",
lsp::Range::at(3, 5, 3, 10),
)
.run();
}
#[test]
fn recipe_invocation_too_few_args() {
Test::new(indoc! {
"
foo arg1 arg2:
echo \"{{arg1}} {{arg2}}\"
bar: (foo `value1`)
echo \"bar\"
"
})
.error(
"Dependency `foo` requires 2 arguments, but 1 provided",
lsp::Range::at(3, 5, 3, 19),
)
.run();
}
#[test]
fn recipe_invocation_too_many_args() {
Test::new(indoc! {
"
foo arg1:
echo \"{{arg1}}\"
bar: (foo `value1` `value2` `value3`)
echo \"bar\"
"
})
.error(
"Dependency `foo` accepts 1 argument, but 3 provided",
lsp::Range::at(3, 5, 3, 37),
)
.run();
}
#[test]
fn recipe_invocation_unknown_variable() {
Test::new(indoc! {
"
foo arg1:
echo {{ arg1 }}
bar: (foo wow)
echo \"bar\"
"
})
.error("Variable `wow` not found", lsp::Range::at(3, 10, 3, 13))
.run();
}
#[test]
fn recipe_invocation_valid_variable() {
Test::new(indoc! {
"
wow := `foo`
foo arg1:
echo \"{{arg1}}\"
bar: (foo wow)
echo \"bar\"
"
})
.run();
}
#[test]
fn recipe_invocation_variadic_params() {
Test::new(indoc! {
"
foo arg1 +args:
echo \"{{arg1}} {{args}}\"
bar: (foo 'value1' 'value2' 'value3')
echo \"bar\"
"
})
.run();
}
#[test]
fn recipe_invocation_zero_or_more_variadic_accepts_no_arguments() {
Test::new(indoc! {
"
foo *args:
echo \"{{args}}\"
bar: (foo)
echo \"bar\"
"
})
.run();
}
#[test]
fn recipe_line_continuations_allow_extra_indentation() {
Test::new(indoc! {
"
update-mdbook-theme:
curl \\
https://example.com/resource \\
> docs/theme/index.hbs
"
})
.run();
}
#[test]
fn recipe_mixed_indentation_between_lines() {
Test::new(indoc! {
"
foo:
\techo \"foo\"
echo \"bar\"
"
})
.error(
"Recipe `foo` mixes tabs and spaces for indentation",
lsp::Range::at(3, 0, 3, 2),
)
.run();
}
#[test]
fn recipe_mixed_indentation_single_line_mix() {
Test::new(indoc! {
"
foo:
\t echo \"foo\"
"
})
.error(
"Recipe `foo` mixes tabs and spaces for indentation",
lsp::Range::at(2, 0, 2, 3),
)
.run();
}
#[test]
fn recipe_named_import() {
Test::new(indoc! {
r"
run: import
import:
body
"
})
.run();
}
#[test]
fn recipe_parameters_defaults_all() {
Test::new(indoc! {
"
recipe_with_defaults arg1=\"first\" arg2=\"second\":
echo \"{{arg1}} {{arg2}}\"
"
})
.run();
}
#[test]
fn recipe_parameters_duplicate() {
Test::new(indoc! {
"
recipe_with_duplicate_param arg1 arg1:
echo \"{{arg1}}\"
"
})
.error("Duplicate parameter `arg1`", lsp::Range::at(0, 33, 0, 37))
.run();
}
#[test]
fn recipe_parameters_order() {
Test::new(indoc! {
"
recipe_with_param_order arg1=\"default\" arg2:
echo \"{{arg1}} {{arg2}}\"
"
})
.error(
"Required parameter `arg2` follows a parameter with a default value",
lsp::Range::at(0, 39, 0, 43),
)
.run();
}
#[test]
fn recipe_parameters_valid() {
Test::new(indoc! {
"
valid_recipe arg1 arg2=\"default\":
echo \"{{arg1}} {{arg2}}\"
"
})
.run();
}
#[test]
fn recipe_parameters_variadic() {
Test::new(indoc! {
"
recipe_with_variadic arg1=\"default\" +args:
echo \"{{arg1}} {{args}}\"
"
})
.run();
}
#[test]
fn recipe_with_all_os_attributes() {
Test::new(indoc! {
"
[linux]
[windows]
[unix]
[macos]
[dragonfly]
[freebsd]
[netbsd]
[openbsd]
build:
echo \"Building everywhere\"
test:
echo \"Testing\"
"
})
.run();
}
#[test]
fn recipe_with_conflicting_multiple_os_attributes() {
Test::new(indoc! {
"
[linux]
[openbsd]
build:
echo \"Building on Linux and OpenBSD\"
[linux]
build:
echo \"Building on Linux again\"
"
})
.error("Duplicate recipe name `build`", lsp::Range::at(5, 0, 8, 0))
.run();
}
#[test]
fn recipe_with_multiple_os_attributes() {
Test::new(indoc! {
"
[windows]
[linux]
build:
echo \"Building on Linux or Windows\"
[linux]
build:
echo \"Building on macOS\"
[macos]
build:
echo \"Building on macOS\"
"
})
.error("Duplicate recipe name `build`", lsp::Range::at(5, 0, 9, 0))
.run();
}
#[test]
fn rule_config_off_suppresses_diagnostic() {
let config = serde_json::from_value::<Config>(serde_json::json!({
"rules": {
"unused-variables": "off"
}
}))
.unwrap();
Test::new(indoc! {
"
foo := \"unused value\"
recipe:
echo foo
"
})
.config(config)
.run();
}
#[test]
fn rule_config_overrides_severity_to_error() {
let config = serde_json::from_value::<Config>(serde_json::json!({
"rules": {
"unused-variables": "error"
}
}))
.unwrap();
Test::new(indoc! {
"
foo := \"unused value\"
recipe:
echo foo
"
})
.config(config)
.error("Variable `foo` appears unused", lsp::Range::at(0, 0, 0, 3))
.run();
}
#[test]
fn rule_config_overrides_severity_to_warning() {
let config = serde_json::from_value::<Config>(serde_json::json!({
"rules": {
"missing-dependencies": "warning"
}
}))
.unwrap();
Test::new(indoc! {
"
foo:
echo \"foo\"
bar: baz
echo \"bar\"
"
})
.config(config)
.warning("Recipe `baz` not found", lsp::Range::at(3, 5, 3, 8))
.run();
}
#[test]
fn script_attribute_with_shebang_conflict() {
Test::new(indoc! {
"
[script]
publish:
#!/usr/bin/env bash
echo \"publish\"
"
})
.error(
"Recipe `publish` has both shebang line and `[script]` attribute",
lsp::Range::at(0, 0, 1, 0),
)
.run();
}
#[test]
fn script_attribute_without_shebang_is_allowed() {
Test::new(indoc! {
"
[script]
publish:
echo \"publish\"
"
})
.run();
}
#[test]
fn set_export_suppresses_unused_variable_warnings() {
Test::new(indoc! {
"
set export
foo := 'bar'
baz := 'qux'
recipe:
echo $foo
"
})
.run();
}
#[test]
fn settings_boolean_shorthand() {
Test::new(indoc! {
"
set export
foo:
echo \"foo\"
"
})
.run();
}
#[test]
fn settings_boolean_type_correct() {
Test::new(indoc! {
"
set export := true
set dotenv-load := false
foo:
echo \"foo\"
"
})
.run();
}
#[test]
fn settings_boolean_type_error() {
Test::new(indoc! {
"
set export := 'foo'
foo:
echo \"foo\"
"
})
.error(
"Setting `export` expects a boolean value",
lsp::Range::at(0, 0, 1, 0),
)
.run();
}
#[test]
fn settings_boolean_type_error_with_expression() {
Test::new(indoc! {
"
env := 'true'
set export := env
foo:
echo \"foo\"
"
})
.error(
"Setting `export` expects a boolean value",
lsp::Range::at(1, 0, 2, 0),
)
.run();
}
#[test]
fn settings_duplicate() {
Test::new(indoc! {
"
set export := true
set shell := [\"bash\", \"-c\"]
set export := false
foo:
echo \"foo\"
"
})
.error("Duplicate setting `export`", lsp::Range::at(2, 0, 3, 0))
.run();
}
#[test]
fn settings_guards_recognized() {
Test::new(indoc! {
"
set guards
foo:
echo \"foo\"
"
})
.run();
}
#[test]
fn settings_lazy_recognized() {
Test::new(indoc! {
"
set lazy
foo:
echo \"foo\"
"
})
.run();
}
#[test]
fn settings_multiple_errors() {
Test::new(indoc! {
"
set unknown-setting := true
set export := false
set shell := ['bash']
set export := false
foo:
echo \"foo\"
"
})
.error(
"Unknown setting `unknown-setting`",
lsp::Range::at(0, 0, 1, 0),
)
.error("Duplicate setting `export`", lsp::Range::at(3, 0, 4, 0))
.run();
}
#[test]
fn settings_shell_array_accepts_shell_expanded_strings() {
Test::new(indoc! {
r#"
set shell := [x"${SHELL_BIN:-bash}", x"-c"]
foo:
echo "foo"
"#
})
.run();
}
#[test]
fn settings_string_type_correct() {
Test::new(indoc! {
"
set dotenv-path := \".env.development\"
foo:
echo \"foo\"
"
})
.run();
}
#[test]
fn settings_string_type_correct_with_expression() {
Test::new(indoc! {
"
env := 'development'
set dotenv-path := '.env.' + env
foo:
echo \"foo\"
"
})
.run();
}
#[test]
fn settings_string_type_correct_with_shell_expanded_string() {
Test::new(indoc! {
r#"
set dotenv-path := x"~/.env.${JUST_ENV:-development}"
foo:
echo "foo"
"#
})
.run();
}
#[test]
fn settings_string_type_error() {
Test::new(indoc! {
"
set dotenv-path := true
foo:
echo \"foo\"
"
})
.error(
"Setting `dotenv-path` expects a string value",
lsp::Range::at(0, 0, 1, 0),
)
.run();
}
#[test]
fn settings_unknown() {
Test::new(indoc! {
"
set unknown-setting := true
foo:
echo \"foo\"
"
})
.error(
"Unknown setting `unknown-setting`",
lsp::Range::at(0, 0, 1, 0),
)
.run();
}
#[test]
fn settings_unknown_with_expression() {
Test::new(indoc! {
"
value := 'bar'
set unknown-setting := value
foo:
echo \"foo\"
"
})
.error(
"Unknown setting `unknown-setting`",
lsp::Range::at(1, 0, 2, 0),
)
.run();
}
#[test]
fn shadowed_parameter_default_uses_global_variable() {
Test::new(indoc! {
"
a := 'default a'
b a=a:
echo {{ a }}
"
})
.run();
}
#[test]
fn shebang_recipe_is_exempt_from_inconsistent_indentation() {
Test::new(indoc! {
"
build-docs:
#!/usr/bin/env bash
mdbook build docs -d build
for language in ar de; do
echo $language
done
"
})
.run();
}
#[test]
fn should_recognize_recipe_parameters_in_dependency_arguments() {
Test::new(indoc! {
"
other-recipe var=\"else\":
echo {{ var }}
test var=\"something\": (other-recipe var)
"
})
.run();
}
#[test]
fn unexported_variables_warned() {
Test::new(indoc! {
"
foo := \"unused value\"
unexport BAR := \"unexported but unused\"
baz := \"used value\"
recipe:
echo {{ baz }}
"
})
.warning("Variable `foo` appears unused", lsp::Range::at(0, 0, 0, 3))
.warning("Variable `BAR` appears unused", lsp::Range::at(1, 9, 1, 12))
.run();
}
#[test]
fn unix_dragonfly_conflict() {
Test::new(indoc! {
"
[unix]
build:
echo \"Building on Unix systems\"
[dragonfly]
build:
echo \"Building on DragonFly BSD\"
"
})
.error("Duplicate recipe name `build`", lsp::Range::at(4, 0, 7, 0))
.run();
}
#[test]
fn unix_freebsd_conflict() {
Test::new(indoc! {
"
[unix]
build:
echo \"Building on Unix systems\"
[freebsd]
build:
echo \"Building on FreeBSD\"
"
})
.error("Duplicate recipe name `build`", lsp::Range::at(4, 0, 7, 0))
.run();
}
#[test]
fn unix_macos_conflicts() {
Test::new(indoc! {
"
[unix]
build:
echo \"Building on Unix systems\"
[macos]
build:
echo \"Building on macOS specifically\"
"
})
.error("Duplicate recipe name `build`", lsp::Range::at(4, 0, 7, 0))
.run();
}
#[test]
fn unix_netbsd_conflict() {
Test::new(indoc! {
"
[unix]
build:
echo \"Building on Unix systems\"
[netbsd]
build:
echo \"Building on NetBSD\"
"
})
.error("Duplicate recipe name `build`", lsp::Range::at(4, 0, 7, 0))
.run();
}
#[test]
fn unknown_default_recipe_parameter_reference() {
Test::new(indoc! {
"
recipe arg=foo:
echo {{ arg }}
"
})
.error("Variable `foo` not found", lsp::Range::at(0, 11, 0, 14))
.run();
}
#[test]
fn unreferenced_variable_in_expression() {
Test::new(indoc! {
"
foo:
echo {{ var }}
"
})
.error("Variable `var` not found", lsp::Range::at(1, 10, 1, 13))
.run();
}
#[test]
fn used_variables_no_warnings() {
Test::new(indoc! {
"
foo := \"used in recipe\"
bar := \"used as dependency arg\"
another arg:
echo {{ arg }}
recipe: (another bar)
echo {{ foo }}
"
})
.run();
}
#[test]
fn user_defined_function_body_references_variable() {
Test::new(indoc! {
"
base := \"hello\"
foo(x) := base + x
"
})
.run();
}
#[test]
fn user_defined_function_body_unknown_identifier() {
Test::new(indoc! {
"
foo(x) := x + unknown
"
})
.error("Variable `unknown` not found", lsp::Range::at(0, 14, 0, 21))
.run();
}
#[test]
fn user_defined_function_duplicate_parameters() {
Test::new(indoc! {
"
foo(bar, bar) := bar
"
})
.error("Duplicate parameter `bar`", lsp::Range::at(0, 9, 0, 12))
.run();
}
#[test]
fn user_defined_function_duplicates() {
Test::new(indoc! {
"
foo() := \"bar\"
foo() := \"baz\"
foo() := \"bat\"
"
})
.error("Duplicate function `foo`", lsp::Range::at(1, 0, 2, 0))
.error("Duplicate function `foo`", lsp::Range::at(2, 0, 3, 0))
.run();
}
#[test]
fn user_defined_function_no_params() {
Test::new(indoc! {
"
foo() := \"bar\"
baz:
echo {{ foo() }}
"
})
.run();
}
#[test]
fn user_defined_function_not_flagged_as_unknown() {
Test::new(indoc! {
"
foo(x) := x + \"!\"
bar:
echo {{ foo(\"baz\") }}
"
})
.run();
}
#[test]
fn user_defined_function_parameters_not_unresolved() {
Test::new(indoc! {
"
foo(x) := x + \"!\"
"
})
.run();
}
#[test]
fn user_defined_function_too_few_args() {
Test::new(indoc! {
"
foo(a, b) := a + b
bar:
echo {{ foo(\"a\") }}
"
})
.error(
"Function `foo` accepts 2 arguments, but 1 provided",
lsp::Range::at(3, 10, 3, 18),
)
.run();
}
#[test]
fn user_defined_function_wrong_arity() {
Test::new(indoc! {
"
foo(x) := x + \"!\"
bar:
echo {{ foo(\"a\", \"b\") }}
"
})
.error(
"Function `foo` accepts 1 argument, but 2 provided",
lsp::Range::at(3, 10, 3, 23),
)
.run();
}
#[test]
fn variables_and_parameters_same_name() {
Test::new(indoc! {
"
param := \"variable value\"
other := \"other value\"
recipe param:
# This should reference the parameter, not the variable
echo {{ param }}
echo {{ other }}
"
})
.warning(
"Variable `param` appears unused",
lsp::Range::at(0, 0, 0, 5),
)
.run();
}
#[test]
fn variables_used_after_hash_in_command() {
Test::new(indoc! {
"
flake := \"testflake\"
output := \"testoutput\"
test:
darwin-rebuild switch --flake {{ flake }}#{{ output }}
"
})
.run();
}
#[test]
fn variables_used_in_dependency_args() {
Test::new(indoc! {
"
used_arg := \"value\"
unused_var := \"not used\"
recipe: (another used_arg)
echo \"something\"
another arg:
echo {{ arg }}
"
})
.warning(
"Variable `unused_var` appears unused",
lsp::Range::at(1, 0, 1, 10),
)
.run();
}
#[test]
fn variables_used_in_multiple_recipes() {
Test::new(indoc! {
"
shared := \"shared value\"
only_in_first := \"first value\"
only_in_second := \"second value\"
never_used := \"unused\"
first:
echo {{ shared }}
echo {{ only_in_first }}
second:
echo {{ shared }}
echo {{ only_in_second }}
"
})
.warning(
"Variable `never_used` appears unused",
lsp::Range::at(3, 0, 3, 10),
)
.run();
}
#[test]
fn variables_used_in_recipe_default_parameters() {
Test::new(indoc! {
"
param_value := \"value\"
recipe arg=param_value:
echo {{ arg }}
"
})
.run();
}
#[test]
fn variables_used_in_recipe_dependencies() {
Test::new(indoc! {
"
param_value := \"value\"
unused := \"unused\"
recipe arg=\"default\": (another param_value)
echo {{ arg }}
another arg:
echo {{ arg }}
"
})
.warning(
"Variable `unused` appears unused",
lsp::Range::at(1, 0, 1, 6),
)
.run();
}
#[test]
fn warn_for_unused_non_exported_recipe_parameters() {
Test::new(indoc! {
"
foo bar:
echo foo
"
})
.warning("Parameter `bar` appears unused", lsp::Range::at(0, 4, 0, 7))
.run();
Test::new(indoc! {
"
foo $bar:
echo foo
"
})
.run();
Test::new(indoc! {
"
set export := false
foo bar:
echo foo
"
})
.warning("Parameter `bar` appears unused", lsp::Range::at(2, 4, 2, 7))
.run();
Test::new(indoc! {
"
set export
foo bar:
echo foo
"
})
.run();
}
#[test]
fn warn_for_unused_variables() {
Test::new(indoc! {
"
foo := \"unused value\"
bar := \"used value\"
recipe:
echo {{ bar }}
"
})
.warning("Variable `foo` appears unused", lsp::Range::at(0, 0, 0, 3))
.run();
}
#[test]
fn windows_recipe_conflicts_with_default() {
Test::new(indoc! {
"
[windows]
build:
echo \"Building on Windows\"
build:
echo \"Building on every OS\"
"
})
.error("Duplicate recipe name `build`", lsp::Range::at(4, 0, 6, 0))
.run();
}
}