use {super::*, serde::Serialize};
type Scopes<'src, 'run> = BTreeMap<
Modulepath,
(
&'run Justfile<'src>,
&'run Scope<'src, 'run>,
&'run BTreeMap<String, String>,
),
>;
#[derive(Debug, PartialEq, Serialize)]
pub(crate) struct Justfile<'src> {
pub(crate) aliases: Table<'src, Alias<'src>>,
pub(crate) assignments: Table<'src, Assignment<'src>>,
#[serde(rename = "first", serialize_with = "keyed::serialize_option")]
pub(crate) default: Option<Arc<Recipe<'src>>>,
pub(crate) doc: Option<String>,
#[serde(skip)]
pub(crate) functions: Table<'src, FunctionDefinition<'src>>,
pub(crate) groups: Vec<StringLiteral<'src>>,
#[serde(skip)]
pub(crate) loaded: Vec<PathBuf>,
pub(crate) module_path: Modulepath,
pub(crate) modules: Table<'src, Self>,
#[serde(skip)]
pub(crate) name: Option<Name<'src>>,
#[serde(skip)]
pub(crate) private: bool,
pub(crate) recipes: Table<'src, Arc<Recipe<'src>>>,
pub(crate) settings: Settings,
pub(crate) source: PathBuf,
pub(crate) unexports: HashSet<String>,
#[serde(skip)]
pub(crate) unstable_features: BTreeSet<UnstableFeature>,
pub(crate) warnings: Vec<Warning>,
#[serde(skip)]
pub(crate) working_directory: PathBuf,
}
impl<'src> Justfile<'src> {
fn find_suggestion(
input: &str,
candidates: impl Iterator<Item = Suggestion<'src>>,
) -> Option<Suggestion<'src>> {
candidates
.map(|suggestion| (edit_distance(input, suggestion.name), suggestion))
.filter(|(distance, _suggestion)| *distance < 3)
.min_by_key(|(distance, _suggestion)| *distance)
.map(|(_distance, suggestion)| suggestion)
}
pub(crate) fn suggest_recipe(&self, input: &str) -> Option<Suggestion<'src>> {
Self::find_suggestion(
input,
self
.recipes
.values()
.filter(|recipe| recipe.is_public())
.map(|recipe| Suggestion {
name: recipe.name(),
target: None,
})
.chain(
self
.aliases
.values()
.filter(|alias| alias.is_public())
.map(|alias| Suggestion {
name: alias.name.lexeme(),
target: Some(alias.target.name.lexeme()),
}),
),
)
}
pub(crate) fn suggest_submodule(&self, input: &str) -> Option<Suggestion<'src>> {
Self::find_suggestion(
input,
self
.modules
.keys()
.map(|name| Suggestion { name, target: None }),
)
}
pub(crate) fn suggest_variable_or_submodule(&self, input: &str) -> Option<Suggestion<'src>> {
Self::find_suggestion(
input,
self
.assignments
.keys()
.chain(self.modules.keys())
.map(|name| Suggestion { name, target: None }),
)
}
fn evaluate_scopes<'run>(
&'run self,
config: &'run Config,
dotenv_arena: &'run Arena<BTreeMap<String, String>>,
overrides: &'run HashMap<Number, String>,
parent_dotenv: Option<&'run BTreeMap<String, String>>,
root: &'run Scope<'src, 'run>,
scope_arena: &'run Arena<Scope<'src, 'run>>,
scopes: &mut Scopes<'src, 'run>,
search: &'run Search,
variable_references: Option<&HashSet<Number>>,
) -> RunResult<'src> {
let dotenv = if config.load_dotenv {
let working_directory = if self.is_submodule() {
&self.working_directory
} else {
&search.working_directory
};
load_dotenv(config, &self.settings, working_directory)?
} else {
BTreeMap::new()
};
let dotenv = if let Some(parent_dotenv) = parent_dotenv {
parent_dotenv
.iter()
.map(|(key, value)| (key.clone(), value.clone()))
.chain(dotenv)
.collect()
} else {
dotenv
};
let dotenv = dotenv_arena.alloc(dotenv);
let scope = Evaluator::evaluate_assignments(
config,
dotenv,
self,
overrides,
root,
search,
variable_references,
)?;
let scope = scope_arena.alloc(scope);
scopes.insert(self.module_path.clone(), (self, scope, dotenv));
for module in self.modules.values() {
module.evaluate_scopes(
config,
dotenv_arena,
overrides,
Some(dotenv),
scope,
scope_arena,
scopes,
search,
variable_references,
)?;
}
Ok(())
}
pub(crate) fn run(
&self,
config: &Config,
search: &Search,
arguments: &[String],
overrides: &HashMap<Number, String>,
) -> RunResult<'src> {
let root = Scope::root();
let dotenv_arena = Arena::new();
let scope_arena = Arena::new();
let mut scopes = BTreeMap::new();
match &config.subcommand {
Subcommand::Choose { .. } | Subcommand::Run { .. } => {
let arguments = arguments.iter().map(String::as_str).collect::<Vec<&str>>();
let invocations = InvocationParser::parse_invocations(self, &arguments)?;
if config.one && invocations.len() > 1 {
return Err(Error::ExcessInvocations {
invocations: invocations.len(),
});
}
let variable_references = if self.settings.lazy {
let mut variable_references = HashSet::new();
let mut stack = Vec::new();
for invocation in &invocations {
stack.push(invocation.recipe);
}
while let Some(recipe) = stack.pop() {
variable_references.extend(&recipe.variable_references);
for dependency in &recipe.dependencies {
stack.push(&dependency.recipe);
}
}
Some(variable_references)
} else {
None
};
self.evaluate_scopes(
config,
&dotenv_arena,
overrides,
None,
&root,
&scope_arena,
&mut scopes,
search,
variable_references.as_ref(),
)?;
let ran = Ran::default();
for invocation in invocations {
Self::run_recipe(
&invocation.arguments,
config,
false,
&ran,
invocation.recipe,
&scopes,
search,
)?;
}
Ok(())
}
Subcommand::Command {
binary, arguments, ..
} => {
let mut command = if config.shell_command {
let mut command = self.settings.shell_command(config);
command.arg(binary);
command
} else {
Command::resolve(binary)
};
command
.args(arguments)
.current_dir(&search.working_directory);
self.evaluate_scopes(
config,
&dotenv_arena,
overrides,
None,
&root,
&scope_arena,
&mut scopes,
search,
Some(HashSet::new()).as_ref(),
)?;
let (_module, scope, dotenv) = scopes.get(&self.module_path).unwrap();
let scope = scope.child();
command.export(&self.settings, dotenv, &scope, &self.unexports);
let (result, caught) = command.status_guard();
let status = result.map_err(|io_error| Error::CommandInvoke {
binary: binary.clone(),
arguments: arguments.clone(),
io_error,
})?;
if !status.success() {
return Err(Error::CommandStatus {
binary: binary.clone(),
arguments: arguments.clone(),
status,
});
}
if let Some(signal) = caught {
return Err(Error::Interrupted { signal });
}
Ok(())
}
Subcommand::Evaluate { format, path } => {
let (module, variable, variable_references) = self.evaluation_target(path)?;
self.evaluate_scopes(
config,
&dotenv_arena,
overrides,
None,
&root,
&scope_arena,
&mut scopes,
search,
Some(&variable_references),
)?;
let scope = scopes.get(&module.module_path).unwrap().1;
if let Some(variable) = variable {
print!("{}", scope.value(variable).unwrap());
} else {
let width = scope.names().fold(0, |max, name| name.len().max(max));
for binding in scope.bindings() {
if !binding.private {
match format {
EvaluateFormat::Just => {
println!("{0:1$} := \"{2}\"", binding.name, width, binding.value);
}
EvaluateFormat::Shell => {
if binding.export || module.settings.export {
print!("export ");
}
print!("{}=\"", binding.name.lexeme().replace('-', "_"));
for c in binding.value.chars() {
if matches!(c, '!' | '"' | '$' | '\\' | '`') {
print!("\\");
}
print!("{c}");
}
println!("\"");
}
}
}
}
}
Ok(())
}
_ => unreachable!(),
}
}
pub(crate) fn evaluation_target<'a>(
&'a self,
path: &'a Modulepath,
) -> RunResult<'src, (&'a Justfile<'a>, Option<&'a str>, HashSet<Number>)> {
let mut current = self;
let mut variable = None;
for (i, component) in path.components.iter().enumerate() {
let last = i + 1 == path.components.len();
if last && current.assignments.contains_key(component) {
variable = Some(component.as_ref());
break;
}
if let Some(module) = current.modules.get(component) {
current = module;
} else if last {
return Err(Error::EvalUnknownSubmoduleOrVariable {
suggestion: current.suggest_variable_or_submodule(component),
component: component.into(),
});
} else {
return Err(Error::EvalUnknownSubmodule {
suggestion: current.suggest_submodule(component),
component: component.into(),
});
}
}
let variable_references = if let Some(variable) = variable {
HashSet::from([current.assignments.get(variable).unwrap().number])
} else {
current
.assignments
.values()
.filter(|assignment| !assignment.private)
.map(|assignment| assignment.number)
.collect()
};
Ok((current, variable, variable_references))
}
pub(crate) fn check_unstable(&self, config: &Config) -> RunResult<'src> {
if let Some(&unstable_feature) = self.unstable_features.iter().next() {
config.require_unstable(self, unstable_feature)?;
}
for module in self.modules.values() {
module.check_unstable(config)?;
}
Ok(())
}
pub(crate) fn get_alias(&self, name: &str) -> Option<&Alias<'src>> {
self.aliases.get(name)
}
pub(crate) fn get_recipe(&self, name: &str) -> Option<&Recipe<'src>> {
self
.recipes
.get(name)
.map(Arc::as_ref)
.or_else(|| self.aliases.get(name).map(|alias| alias.target.as_ref()))
}
pub(crate) fn is_submodule(&self) -> bool {
self.name.is_some()
}
pub(crate) fn name(&self) -> &'src str {
self.name.map(|name| name.lexeme()).unwrap_or_default()
}
fn run_recipe(
arguments: &[Vec<String>],
config: &Config,
is_dependency: bool,
ran: &Ran,
recipe: &Recipe<'src>,
scopes: &Scopes<'src, '_>,
search: &Search,
) -> RunResult<'src> {
let mutex = ran.mutex(recipe, arguments);
let mut guard = mutex.lock().unwrap();
if *guard {
return Ok(());
}
let (module, scope, dotenv) = scopes
.get(recipe.module_path())
.expect("failed to retrieve scope for module");
let context = ExecutionContext {
config,
dotenv,
module,
search,
};
let (outer, positional) = Evaluator::evaluate_parameters(
arguments,
&context,
is_dependency,
&recipe.parameters,
recipe,
scope,
)?;
let scope = outer.child();
let mut evaluator = Evaluator::new(&context, BTreeMap::new(), true, &scope);
if !config.yes && !recipe.confirm(&mut evaluator)? {
return Err(Error::NotConfirmed {
recipe: recipe.name(),
});
}
Self::run_dependencies(
config,
&context,
recipe.priors(),
&mut evaluator,
ran,
recipe,
scopes,
search,
)?;
recipe.run(&context, &scope, &positional, is_dependency)?;
Self::run_dependencies(
config,
&context,
recipe.subsequents(),
&mut evaluator,
&Ran::default(),
recipe,
scopes,
search,
)?;
*guard = true;
Ok(())
}
fn run_dependencies<'run>(
config: &Config,
context: &ExecutionContext<'src, 'run>,
dependencies: &[Dependency<'src>],
evaluator: &mut Evaluator<'src, 'run>,
ran: &Ran,
recipe: &Recipe<'src>,
scopes: &Scopes<'src, 'run>,
search: &Search,
) -> RunResult<'src> {
if context.config.no_dependencies {
return Ok(());
}
let mut evaluated = Vec::new();
for Dependency { recipe, arguments } in dependencies {
let mut grouped = Vec::new();
for group in arguments {
let evaluated_group = group
.iter()
.map(|argument| evaluator.evaluate_expression(argument))
.collect::<RunResult<Vec<String>>>()?;
grouped.push(evaluated_group);
}
evaluated.push((recipe, grouped));
}
if recipe.is_parallel() {
thread::scope::<_, RunResult>(|thread_scope| {
let mut handles = Vec::new();
for (recipe, arguments) in evaluated {
handles.push(thread_scope.spawn(move || {
Self::run_recipe(&arguments, config, true, ran, recipe, scopes, search)
}));
}
for handle in handles {
handle
.join()
.map_err(|_| Error::internal("parallel dependency thread panicked"))??;
}
Ok(())
})?;
} else {
for (recipe, arguments) in evaluated {
Self::run_recipe(&arguments, config, true, ran, recipe, scopes, search)?;
}
}
Ok(())
}
pub(crate) fn public_modules(&self, config: &Config) -> Vec<&Justfile> {
let mut modules = self
.modules
.values()
.filter(|module| !module.private)
.collect::<Vec<&Justfile>>();
if config.unsorted {
modules.sort_by_key(|module| {
module
.name
.map(|name| name.token.offset)
.unwrap_or_default()
});
}
modules
}
pub(crate) fn public_recipes(&self, config: &Config) -> Vec<&Recipe> {
let mut recipes = self
.recipes
.values()
.map(AsRef::as_ref)
.filter(|recipe| recipe.is_public())
.collect::<Vec<&Recipe>>();
if config.unsorted {
recipes.sort_by_key(|recipe| (&recipe.import_offsets, recipe.name.offset));
}
recipes
}
pub(crate) fn public_recipes_recursive(&self, config: &Config) -> Vec<&Recipe> {
let mut recipes = Vec::new();
let mut stack = vec![self];
while let Some(current) = stack.pop() {
for recipe in current.public_recipes(config) {
recipes.push(recipe);
}
for module in current.public_modules(config).into_iter().rev() {
stack.push(module);
}
}
recipes
}
pub(crate) fn public_aliases_recursive(&self, config: &Config) -> Vec<(&Alias, &Modulepath)> {
let mut aliases = Vec::new();
let mut stack = vec![self];
while let Some(current) = stack.pop() {
for alias in current.aliases.values() {
if alias.is_public() {
aliases.push((alias, ¤t.module_path));
}
}
for module in current.public_modules(config).into_iter().rev() {
stack.push(module);
}
}
aliases
}
pub(crate) fn groups(&self) -> Vec<&str> {
self
.groups
.iter()
.map(|group| group.cooked.as_str())
.collect()
}
pub(crate) fn public_groups(&self, config: &Config) -> Vec<String> {
let mut groups = Vec::new();
for recipe in self.recipes.values() {
if recipe.is_public() {
for group in recipe.groups() {
groups.push((recipe.import_offsets.as_slice(), recipe.name.offset, group));
}
}
}
for submodule in self.public_modules(config) {
for group in submodule.groups() {
groups.push((&[], submodule.name.unwrap().offset, group.to_string()));
}
}
if config.unsorted {
groups.sort();
} else {
groups.sort_by(|(_, _, a), (_, _, b)| a.cmp(b));
}
let mut seen = HashSet::new();
groups.retain(|(_, _, group)| seen.insert(group.clone()));
groups.into_iter().map(|(_, _, group)| group).collect()
}
}
impl ColorDisplay for Justfile<'_> {
fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {
let mut items = self.recipes.len() + self.assignments.len() + self.aliases.len();
for (name, assignment) in &self.assignments {
if assignment.export {
write!(f, "export ")?;
}
write!(f, "{name} := {}", assignment.value)?;
items -= 1;
if items != 0 {
write!(f, "\n\n")?;
}
}
for alias in self.aliases.values() {
write!(f, "{alias}")?;
items -= 1;
if items != 0 {
write!(f, "\n\n")?;
}
}
for recipe in self.recipes.values() {
write!(f, "{}", recipe.color_display(color))?;
items -= 1;
if items != 0 {
write!(f, "\n\n")?;
}
}
Ok(())
}
}
impl<'src> Keyed<'src> for Justfile<'src> {
fn key(&self) -> &'src str {
self.name()
}
}
#[cfg(test)]
mod tests {
use {super::*, Error::*, testing::compile};
run_error! {
name: unknown_recipe_no_suggestion,
src: "a:\nb:\nc:",
args: ["a", "xyz", "y", "z"],
error: UnknownRecipe {
recipe,
suggestion,
},
check: {
assert_eq!(recipe, "xyz");
assert_eq!(suggestion, None);
}
}
run_error! {
name: unknown_recipe_with_suggestion,
src: "a:\nb:\nc:",
args: ["a", "x", "y", "z"],
error: UnknownRecipe {
recipe,
suggestion,
},
check: {
assert_eq!(recipe, "x");
assert_eq!(suggestion, Some(Suggestion {
name: "a",
target: None,
}));
}
}
run_error! {
name: unknown_recipe_show_alias_suggestion,
src: "
foo:
echo foo
alias z := foo
",
args: ["zz"],
error: UnknownRecipe {
recipe,
suggestion,
},
check: {
assert_eq!(recipe, "zz");
assert_eq!(suggestion, Some(Suggestion {
name: "z",
target: Some("foo"),
}
));
}
}
run_error! {
name: code_error,
src: "
fail:
@exit 100
",
args: ["fail"],
error: Code {
recipe,
line_number,
code,
print_message,
},
check: {
assert_eq!(recipe, "fail");
assert_eq!(code, 100);
assert_eq!(line_number, Some(2));
assert!(print_message);
}
}
run_error! {
name: run_args,
src: "
a return code:
@x() { {{return}} {{code + '0'}}; }; x
",
args: ["a", "return", "15"],
error: Code {
recipe,
line_number,
code,
print_message,
},
check: {
assert_eq!(recipe, "a");
assert_eq!(code, 150);
assert_eq!(line_number, Some(2));
assert!(print_message);
}
}
run_error! {
name: missing_some_arguments,
src: "a b c d:",
args: ["a", "b", "c"],
error: PositionalArgumentCountMismatch {
recipe,
found,
min,
max,
},
check: {
assert_eq!(recipe.name(), "a");
assert_eq!(found, 2);
assert_eq!(min, 3);
assert_eq!(max, 3);
}
}
run_error! {
name: missing_some_arguments_variadic,
src: "a b c +d:",
args: ["a", "B", "C"],
error: PositionalArgumentCountMismatch {
recipe,
found,
min,
max,
},
check: {
assert_eq!(recipe.name(), "a");
assert_eq!(found, 2);
assert_eq!(min, 3);
assert_eq!(max, usize::MAX - 1);
}
}
run_error! {
name: missing_all_arguments,
src: "a b c d:\n echo {{b}}{{c}}{{d}}",
args: ["a"],
error: PositionalArgumentCountMismatch {
recipe,
found,
min,
max,
},
check: {
assert_eq!(recipe.name(), "a");
assert_eq!(found, 0);
assert_eq!(min, 3);
assert_eq!(max, 3);
}
}
run_error! {
name: missing_some_defaults,
src: "a b c d='hello':",
args: ["a", "b"],
error: PositionalArgumentCountMismatch {
recipe,
found,
min,
max,
},
check: {
assert_eq!(recipe.name(), "a");
assert_eq!(found, 1);
assert_eq!(min, 2);
assert_eq!(max, 3);
}
}
run_error! {
name: missing_all_defaults,
src: "a b c='r' d='h':",
args: ["a"],
error: PositionalArgumentCountMismatch {
recipe,
found,
min,
max,
},
check: {
assert_eq!(recipe.name(), "a");
assert_eq!(found, 0);
assert_eq!(min, 1);
assert_eq!(max, 3);
}
}
run_error! {
name: export_failure,
src: "
export foo := 'a'
baz := 'c'
export bar := 'b'
export abc := foo + bar + baz
wut:
echo $foo $bar $baz
",
args: ["--quiet", "wut"],
error: Code {
recipe,
line_number,
print_message,
..
},
check: {
assert_eq!(recipe, "wut");
assert_eq!(line_number, Some(7));
assert!(print_message);
}
}
fn case(input: &str, expected: &str) {
let justfile = compile(input);
let actual = format!("{}", justfile.color_display(Color::never()));
assert_eq!(actual, expected);
let reparsed = compile(&actual);
let redumped = format!("{}", reparsed.color_display(Color::never()));
assert_eq!(redumped, actual);
}
#[test]
fn parse_empty() {
case(
"
# hello
",
"",
);
}
#[test]
fn parse_string_default() {
case(
r#"
foo a="b\t":
"#,
r#"foo a="b\t":"#,
);
}
#[test]
fn parse_multiple() {
case(
"
a:
b:
", "a:
b:",
);
}
#[test]
fn parse_variadic() {
case(
"
foo +a:
",
"foo +a:",
);
}
#[test]
fn parse_variadic_string_default() {
case(
"
foo +a='Hello':
",
"foo +a='Hello':",
);
}
#[test]
fn parse_raw_string_default() {
case(
r"
foo a='b\t':
",
r"foo a='b\t':",
);
}
#[test]
fn parse_export() {
case(
"
export a := 'hello'
",
"export a := 'hello'",
);
}
#[test]
fn parse_alias_after_target() {
case(
"
foo:
echo a
alias f := foo
",
"alias f := foo
foo:
echo a",
);
}
#[test]
fn parse_alias_before_target() {
case(
"
alias f := foo
foo:
echo a
",
"alias f := foo
foo:
echo a",
);
}
#[test]
fn parse_alias_with_comment() {
case(
"
alias f := foo # comment
foo:
echo a
",
"alias f := foo
foo:
echo a",
);
}
#[test]
fn parse_complex() {
case(
"
x:
y:
z:
foo := \"xx\"
bar := foo
goodbye := \"y\"
hello a b c : x y z #hello
#! blah
#blarg
{{ foo + bar}}abc{{ goodbye\t + \"x\" }}xyz
1
2
3
",
"bar := foo
foo := \"xx\"
goodbye := \"y\"
hello a b c: x y z
#! blah
#blarg
{{ foo + bar }}abc{{ goodbye + \"x\" }}xyz
1
2
3
x:
y:
z:",
);
}
#[test]
fn parse_shebang() {
case(
"
practicum := 'hello'
install:
\t#!/bin/sh
\tif [[ -f {{practicum}} ]]; then
\t\treturn
\tfi
",
"practicum := 'hello'
install:
#!/bin/sh
if [[ -f {{ practicum }} ]]; then
\treturn
fi",
);
}
#[test]
fn parse_simple_shebang() {
case("a:\n #!\n print(1)", "a:\n #!\n print(1)");
}
#[test]
fn parse_assignments() {
case(
"a := '0'
c := a + b + a + b
b := '1'
",
"a := '0'
b := '1'
c := a + b + a + b",
);
}
#[test]
fn parse_assignment_backticks() {
case(
"a := `echo hello`
c := a + b + a + b
b := `echo goodbye`",
"a := `echo hello`
b := `echo goodbye`
c := a + b + a + b",
);
}
#[test]
fn parse_interpolation_backticks() {
case(
"a:
echo {{ `echo hello` + 'blarg' }} {{ `echo bob` }}",
"a:
echo {{ `echo hello` + 'blarg' }} {{ `echo bob` }}",
);
}
#[test]
fn eof_test() {
case("x:\ny:\nz:\na b c: x y z", "a b c: x y z\n\nx:\n\ny:\n\nz:");
}
#[test]
fn string_quote_escape() {
case(r#"a := "hello\"""#, r#"a := "hello\"""#);
}
#[test]
fn string_escapes() {
case(r#"a := "\n\t\r\"\\""#, r#"a := "\n\t\r\"\\""#);
}
#[test]
fn parameters() {
case(
"a b c:
{{b}} {{c}}",
"a b c:
{{ b }} {{ c }}",
);
}
#[test]
fn unary_functions() {
case(
"
x := arch()
a:
{{os()}} {{os_family()}} {{num_cpus()}}",
"x := arch()
a:
{{ os() }} {{ os_family() }} {{ num_cpus() }}",
);
}
#[test]
fn env_functions() {
case(
"
x := env_var('foo',)
a:
{{env_var_or_default('foo' + 'bar', 'baz',)}} {{env_var(env_var('baz'))}}",
"x := env_var('foo')
a:
{{ env_var_or_default('foo' + 'bar', 'baz') }} {{ env_var(env_var('baz')) }}",
);
}
#[test]
fn parameter_default_string() {
case(
"
f x='abc':
",
"f x='abc':",
);
}
#[test]
fn parameter_default_raw_string() {
case(
"
f x='abc':
",
"f x='abc':",
);
}
#[test]
fn parameter_default_backtick() {
case(
"
f x=`echo hello`:
",
"f x=`echo hello`:",
);
}
#[test]
fn parameter_default_concatenation_string() {
case(
"
f x=(`echo hello` + 'foo'):
",
"f x=(`echo hello` + 'foo'):",
);
}
#[test]
fn parameter_default_concatenation_variable() {
case(
"
x := '10'
f y=(`echo hello` + x) +z='foo':
",
"x := '10'
f y=(`echo hello` + x) +z='foo':",
);
}
#[test]
fn parameter_default_multiple() {
case(
"
x := '10'
f y=(`echo hello` + x) +z=('foo' + 'bar'):
",
"x := '10'
f y=(`echo hello` + x) +z=('foo' + 'bar'):",
);
}
#[test]
fn concatenation_in_group() {
case("x := ('0' + '1')", "x := ('0' + '1')");
}
#[test]
fn string_in_group() {
case("x := ('0' )", "x := ('0')");
}
#[rustfmt::skip]
#[test]
fn escaped_dos_newlines() {
case("@spam:\r
\t{ \\\r
\t\tfiglet test; \\\r
\t\tcargo build --color always 2>&1; \\\r
\t\tcargo test --color always -- --color always 2>&1; \\\r
\t} | less\r
",
"@spam:
{ \\
\tfiglet test; \\
\tcargo build --color always 2>&1; \\
\tcargo test --color always -- --color always 2>&1; \\
} | less");
}
}