Skip to content

Commit 18ec979

Browse files
authored
Improve argument parsing and error handling for submodules (#2154)
1 parent e1b17fe commit 18ec979

File tree

8 files changed

+588
-155
lines changed

8 files changed

+588
-155
lines changed

src/argument_parser.rs

Lines changed: 403 additions & 0 deletions
Large diffs are not rendered by default.

src/error.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ pub(crate) enum Error<'src> {
9595
variable: String,
9696
suggestion: Option<Suggestion<'src>>,
9797
},
98+
ExpectedSubmoduleButFoundRecipe {
99+
path: String,
100+
},
98101
FormatCheckFoundDiff,
99102
FunctionCall {
100103
function: Name<'src>,
@@ -162,13 +165,13 @@ pub(crate) enum Error<'src> {
162165
line_number: Option<usize>,
163166
},
164167
UnknownSubmodule {
165-
path: ModulePath,
168+
path: String,
166169
},
167170
UnknownOverrides {
168171
overrides: Vec<String>,
169172
},
170-
UnknownRecipes {
171-
recipes: Vec<String>,
173+
UnknownRecipe {
174+
recipe: String,
172175
suggestion: Option<Suggestion<'src>>,
173176
},
174177
Unstable {
@@ -365,6 +368,9 @@ impl<'src> ColorDisplay for Error<'src> {
365368
write!(f, "\n{suggestion}")?;
366369
}
367370
}
371+
ExpectedSubmoduleButFoundRecipe { path } => {
372+
write!(f, "Expected submodule at `{path}` but found recipe.")?;
373+
},
368374
FormatCheckFoundDiff => {
369375
write!(f, "Formatted justfile differs from original.")?;
370376
}
@@ -447,10 +453,8 @@ impl<'src> ColorDisplay for Error<'src> {
447453
let overrides = List::and_ticked(overrides);
448454
write!(f, "{count} {overrides} overridden on the command line but not present in justfile")?;
449455
}
450-
UnknownRecipes { recipes, suggestion } => {
451-
let count = Count("recipe", recipes.len());
452-
let recipes = List::or_ticked(recipes);
453-
write!(f, "Justfile does not contain {count} {recipes}.")?;
456+
UnknownRecipe { recipe, suggestion } => {
457+
write!(f, "Justfile does not contain recipe `{recipe}`.")?;
454458
if let Some(suggestion) = suggestion {
455459
write!(f, "\n{suggestion}")?;
456460
}

src/justfile.rs

Lines changed: 69 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -173,66 +173,26 @@ impl<'src> Justfile<'src> {
173173
_ => {}
174174
}
175175

176-
let mut remaining: Vec<&str> = if !arguments.is_empty() {
177-
arguments.iter().map(String::as_str).collect()
178-
} else if let Some(recipe) = &self.default {
179-
recipe.check_can_be_default_recipe()?;
180-
vec![recipe.name()]
181-
} else if self.recipes.is_empty() {
182-
return Err(Error::NoRecipes);
183-
} else {
184-
return Err(Error::NoDefaultRecipe);
185-
};
176+
let arguments = arguments.iter().map(String::as_str).collect::<Vec<&str>>();
186177

187-
let mut missing = Vec::new();
188-
let mut invocations = Vec::new();
189-
let mut scopes = BTreeMap::new();
190-
let arena: Arena<Scope> = Arena::new();
191-
192-
while let Some(first) = remaining.first().copied() {
193-
if first.contains("::")
194-
&& !(first.starts_with(':') || first.ends_with(':') || first.contains(":::"))
195-
{
196-
remaining = first
197-
.split("::")
198-
.chain(remaining[1..].iter().copied())
199-
.collect();
200-
201-
continue;
202-
}
178+
let groups = ArgumentParser::parse_arguments(self, &arguments)?;
203179

204-
let rest = &remaining[1..];
180+
let arena: Arena<Scope> = Arena::new();
181+
let mut invocations = Vec::<Invocation>::new();
182+
let mut scopes = BTreeMap::new();
205183

206-
if let Some((invocation, consumed)) = self.invocation(
207-
0,
208-
&mut Vec::new(),
184+
for group in &groups {
185+
invocations.push(self.invocation(
209186
&arena,
210-
&mut scopes,
187+
&group.arguments,
211188
config,
212189
&dotenv,
213-
search,
214190
&scope,
215-
first,
216-
rest,
217-
)? {
218-
remaining = rest[consumed..].to_vec();
219-
invocations.push(invocation);
220-
} else {
221-
missing.push(first.to_string());
222-
remaining = rest.to_vec();
223-
}
224-
}
225-
226-
if !missing.is_empty() {
227-
let suggestion = if missing.len() == 1 {
228-
self.suggest_recipe(missing.first().unwrap())
229-
} else {
230-
None
231-
};
232-
return Err(Error::UnknownRecipes {
233-
recipes: missing,
234-
suggestion,
235-
});
191+
&group.path,
192+
0,
193+
&mut scopes,
194+
search,
195+
)?);
236196
}
237197

238198
let mut ran = Ran::default();
@@ -278,21 +238,29 @@ impl<'src> Justfile<'src> {
278238

279239
fn invocation<'run>(
280240
&'run self,
281-
depth: usize,
282-
path: &mut Vec<&'run str>,
283241
arena: &'run Arena<Scope<'src, 'run>>,
284-
scopes: &mut BTreeMap<Vec<&'run str>, &'run Scope<'src, 'run>>,
242+
arguments: &[&'run str],
285243
config: &'run Config,
286244
dotenv: &'run BTreeMap<String, String>,
287-
search: &'run Search,
288245
parent: &'run Scope<'src, 'run>,
289-
first: &'run str,
290-
rest: &[&'run str],
291-
) -> RunResult<'src, Option<(Invocation<'src, 'run>, usize)>> {
292-
if let Some(module) = self.modules.get(first) {
293-
path.push(first);
246+
path: &'run [String],
247+
position: usize,
248+
scopes: &mut BTreeMap<&'run [String], &'run Scope<'src, 'run>>,
249+
search: &'run Search,
250+
) -> RunResult<'src, Invocation<'src, 'run>> {
251+
if position + 1 == path.len() {
252+
let recipe = self.get_recipe(&path[position]).unwrap();
253+
Ok(Invocation {
254+
recipe,
255+
module_source: &self.source,
256+
arguments: arguments.into(),
257+
settings: &self.settings,
258+
scope: parent,
259+
})
260+
} else {
261+
let module = self.modules.get(&path[position]).unwrap();
294262

295-
let scope = if let Some(scope) = scopes.get(path) {
263+
let scope = if let Some(scope) = scopes.get(&path[..position]) {
296264
scope
297265
} else {
298266
let scope = Evaluator::evaluate_assignments(
@@ -304,76 +272,21 @@ impl<'src> Justfile<'src> {
304272
search,
305273
)?;
306274
let scope = arena.alloc(scope);
307-
scopes.insert(path.clone(), scope);
275+
scopes.insert(path, scope);
308276
scopes.get(path).unwrap()
309277
};
310278

311-
if rest.is_empty() {
312-
if let Some(recipe) = &module.default {
313-
recipe.check_can_be_default_recipe()?;
314-
return Ok(Some((
315-
Invocation {
316-
settings: &module.settings,
317-
recipe,
318-
arguments: Vec::new(),
319-
scope,
320-
module_source: &self.source,
321-
},
322-
depth,
323-
)));
324-
}
325-
Err(Error::NoDefaultRecipe)
326-
} else {
327-
module.invocation(
328-
depth + 1,
329-
path,
330-
arena,
331-
scopes,
332-
config,
333-
dotenv,
334-
search,
335-
scope,
336-
rest[0],
337-
&rest[1..],
338-
)
339-
}
340-
} else if let Some(recipe) = self.get_recipe(first) {
341-
if recipe.parameters.is_empty() {
342-
Ok(Some((
343-
Invocation {
344-
arguments: Vec::new(),
345-
recipe,
346-
scope: parent,
347-
settings: &self.settings,
348-
module_source: &self.source,
349-
},
350-
depth,
351-
)))
352-
} else {
353-
let argument_range = recipe.argument_range();
354-
let argument_count = cmp::min(rest.len(), recipe.max_arguments());
355-
if !argument_range.range_contains(&argument_count) {
356-
return Err(Error::ArgumentCountMismatch {
357-
recipe: recipe.name(),
358-
parameters: recipe.parameters.clone(),
359-
found: rest.len(),
360-
min: recipe.min_arguments(),
361-
max: recipe.max_arguments(),
362-
});
363-
}
364-
Ok(Some((
365-
Invocation {
366-
arguments: rest[..argument_count].to_vec(),
367-
recipe,
368-
scope: parent,
369-
settings: &self.settings,
370-
module_source: &self.source,
371-
},
372-
depth + argument_count,
373-
)))
374-
}
375-
} else {
376-
Ok(None)
279+
module.invocation(
280+
arena,
281+
arguments,
282+
config,
283+
dotenv,
284+
scope,
285+
path,
286+
position + 1,
287+
scopes,
288+
search,
289+
)
377290
}
378291
}
379292

@@ -523,34 +436,51 @@ mod tests {
523436
use Error::*;
524437

525438
run_error! {
526-
name: unknown_recipes,
439+
name: unknown_recipe_no_suggestion,
527440
src: "a:\nb:\nc:",
528-
args: ["a", "x", "y", "z"],
529-
error: UnknownRecipes {
530-
recipes,
441+
args: ["a", "xyz", "y", "z"],
442+
error: UnknownRecipe {
443+
recipe,
531444
suggestion,
532445
},
533446
check: {
534-
assert_eq!(recipes, &["x", "y", "z"]);
447+
assert_eq!(recipe, "xyz");
535448
assert_eq!(suggestion, None);
536449
}
537450
}
538451

539452
run_error! {
540-
name: unknown_recipes_show_alias_suggestion,
453+
name: unknown_recipe_with_suggestion,
454+
src: "a:\nb:\nc:",
455+
args: ["a", "x", "y", "z"],
456+
error: UnknownRecipe {
457+
recipe,
458+
suggestion,
459+
},
460+
check: {
461+
assert_eq!(recipe, "x");
462+
assert_eq!(suggestion, Some(Suggestion {
463+
name: "a",
464+
target: None,
465+
}));
466+
}
467+
}
468+
469+
run_error! {
470+
name: unknown_recipe_show_alias_suggestion,
541471
src: "
542472
foo:
543473
echo foo
544474
545475
alias z := foo
546476
",
547477
args: ["zz"],
548-
error: UnknownRecipes {
549-
recipes,
478+
error: UnknownRecipe {
479+
recipe,
550480
suggestion,
551481
},
552482
check: {
553-
assert_eq!(recipes, &["zz"]);
483+
assert_eq!(recipe, "zz");
554484
assert_eq!(suggestion, Some(Suggestion {
555485
name: "z",
556486
target: Some("foo"),

src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
pub(crate) use {
1717
crate::{
18-
alias::Alias, analyzer::Analyzer, assignment::Assignment,
18+
alias::Alias, analyzer::Analyzer, argument_parser::ArgumentParser, assignment::Assignment,
1919
assignment_resolver::AssignmentResolver, ast::Ast, attribute::Attribute, binding::Binding,
2020
color::Color, color_display::ColorDisplay, command_ext::CommandExt, compilation::Compilation,
2121
compile_error::CompileError, compile_error_kind::CompileErrorKind, compiler::Compiler,
@@ -113,6 +113,7 @@ pub mod summary;
113113

114114
mod alias;
115115
mod analyzer;
116+
mod argument_parser;
116117
mod assignment;
117118
mod assignment_resolver;
118119
mod ast;

src/subcommand.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ impl Subcommand {
150150
};
151151

152152
match Self::run_inner(config, loader, arguments, overrides, &search) {
153-
Err((err @ Error::UnknownRecipes { .. }, true)) => {
153+
Err((err @ Error::UnknownRecipe { .. }, true)) => {
154154
match search.justfile.parent().unwrap().parent() {
155155
Some(parent) => {
156156
unknown_recipes_errors.get_or_insert(err);
@@ -428,7 +428,9 @@ impl Subcommand {
428428
module = module
429429
.modules
430430
.get(name)
431-
.ok_or_else(|| Error::UnknownSubmodule { path: path.clone() })?;
431+
.ok_or_else(|| Error::UnknownSubmodule {
432+
path: path.to_string(),
433+
})?;
432434
}
433435

434436
Self::list_module(config, module, 0);
@@ -588,7 +590,9 @@ impl Subcommand {
588590
module = module
589591
.modules
590592
.get(name)
591-
.ok_or_else(|| Error::UnknownSubmodule { path: path.clone() })?;
593+
.ok_or_else(|| Error::UnknownSubmodule {
594+
path: path.to_string(),
595+
})?;
592596
}
593597

594598
let name = path.path.last().unwrap();
@@ -602,8 +606,8 @@ impl Subcommand {
602606
println!("{}", recipe.color_display(config.color.stdout()));
603607
Ok(())
604608
} else {
605-
Err(Error::UnknownRecipes {
606-
recipes: vec![name.to_owned()],
609+
Err(Error::UnknownRecipe {
610+
recipe: name.to_owned(),
607611
suggestion: module.suggest_recipe(name),
608612
})
609613
}

src/testing.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ macro_rules! run_error {
131131
}
132132

133133
macro_rules! assert_matches {
134-
($expression:expr, $( $pattern:pat_param )|+ $( if $guard:expr )?) => {
134+
($expression:expr, $( $pattern:pat_param )|+ $( if $guard:expr )? $(,)?) => {
135135
match $expression {
136136
$( $pattern )|+ $( if $guard )? => {}
137137
left => panic!(

tests/misc.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,7 @@ test! {
652652
justfile: "hello:",
653653
args: ("foo", "bar"),
654654
stdout: "",
655-
stderr: "error: Justfile does not contain recipes `foo` or `bar`.\n",
655+
stderr: "error: Justfile does not contain recipe `foo`.\n",
656656
status: EXIT_FAILURE,
657657
}
658658

0 commit comments

Comments
 (0)