From db52d95146ca8e85497ea06a762afd8b560a9215 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Tue, 28 May 2024 20:06:30 -0700 Subject: [PATCH] Add `module_file()` and `module_directory()` functions (#2105) --- src/analyzer.rs | 1 + src/evaluator.rs | 33 +++++++----- src/function.rs | 40 ++++++++++++++ src/justfile.rs | 43 ++++++++++----- src/parser.rs | 10 ++-- src/recipe.rs | 24 +++++---- src/recipe_context.rs | 2 + tests/functions.rs | 123 ++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 233 insertions(+), 43 deletions(-) diff --git a/src/analyzer.rs b/src/analyzer.rs index 822b812..ed9fb37 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -166,6 +166,7 @@ impl<'src> Analyzer<'src> { name, recipes, settings, + source: root.into(), warnings, }) } diff --git a/src/evaluator.rs b/src/evaluator.rs index 2794b46..1e029c7 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -4,9 +4,10 @@ pub(crate) struct Evaluator<'src: 'run, 'run> { pub(crate) assignments: Option<&'run Table<'src, Assignment<'src>>>, pub(crate) config: &'run Config, pub(crate) dotenv: &'run BTreeMap, + pub(crate) module_source: &'run Path, pub(crate) scope: Scope<'src, 'run>, - pub(crate) settings: &'run Settings<'run>, pub(crate) search: &'run Search, + pub(crate) settings: &'run Settings<'run>, } impl<'src, 'run> Evaluator<'src, 'run> { @@ -14,17 +15,19 @@ impl<'src, 'run> Evaluator<'src, 'run> { assignments: &'run Table<'src, Assignment<'src>>, config: &'run Config, dotenv: &'run BTreeMap, - overrides: Scope<'src, 'run>, - settings: &'run Settings<'run>, + module_source: &'run Path, + scope: Scope<'src, 'run>, search: &'run Search, + settings: &'run Settings<'run>, ) -> RunResult<'src, Scope<'src, 'run>> { let mut evaluator = Self { - scope: overrides, assignments: Some(assignments), config, dotenv, - settings, + module_source, + scope, search, + settings, }; for assignment in assignments.values() { @@ -250,21 +253,23 @@ impl<'src, 'run> Evaluator<'src, 'run> { } pub(crate) fn evaluate_parameters( + arguments: &[String], config: &'run Config, dotenv: &'run BTreeMap, + module_source: &'run Path, parameters: &[Parameter<'src>], - arguments: &[String], scope: &'run Scope<'src, 'run>, - settings: &'run Settings, search: &'run Search, + settings: &'run Settings, ) -> RunResult<'src, (Scope<'src, 'run>, Vec)> { let mut evaluator = Self { assignments: None, + config, + dotenv, + module_source, scope: scope.child(), search, settings, - dotenv, - config, }; let mut scope = scope.child(); @@ -307,17 +312,19 @@ impl<'src, 'run> Evaluator<'src, 'run> { pub(crate) fn recipe_evaluator( config: &'run Config, dotenv: &'run BTreeMap, + module_source: &'run Path, scope: &'run Scope<'src, 'run>, - settings: &'run Settings, search: &'run Search, - ) -> Evaluator<'src, 'run> { + settings: &'run Settings, + ) -> Self { Self { assignments: None, + config, + dotenv, + module_source, scope: Scope::child(scope), search, settings, - dotenv, - config, } } } diff --git a/src/function.rs b/src/function.rs index 52fcf00..e6820c9 100644 --- a/src/function.rs +++ b/src/function.rs @@ -67,6 +67,8 @@ pub(crate) fn get(name: &str) -> Option { "kebabcase" => Unary(kebabcase), "lowercamelcase" => Unary(lowercamelcase), "lowercase" => Unary(lowercase), + "module_directory" => Nullary(module_directory), + "module_file" => Nullary(module_file), "num_cpus" => Nullary(num_cpus), "os" => Nullary(os), "os_family" => Nullary(os_family), @@ -406,6 +408,44 @@ fn lowercase(_context: Context, s: &str) -> Result { Ok(s.to_lowercase()) } +fn module_directory(context: Context) -> Result { + context + .evaluator + .search + .justfile + .parent() + .unwrap() + .join(context.evaluator.module_source) + .parent() + .unwrap() + .to_str() + .map(str::to_owned) + .ok_or_else(|| { + format!( + "Module directory is not valid unicode: {}", + context.evaluator.module_source.parent().unwrap().display(), + ) + }) +} + +fn module_file(context: Context) -> Result { + context + .evaluator + .search + .justfile + .parent() + .unwrap() + .join(context.evaluator.module_source) + .to_str() + .map(str::to_owned) + .ok_or_else(|| { + format!( + "Module file path is not valid unicode: {}", + context.evaluator.module_source.display(), + ) + }) +} + fn num_cpus(_context: Context) -> Result { let num = num_cpus::get(); Ok(num.to_string()) diff --git a/src/justfile.rs b/src/justfile.rs index a74a94c..e86f6c8 100644 --- a/src/justfile.rs +++ b/src/justfile.rs @@ -3,9 +3,10 @@ use {super::*, serde::Serialize}; #[derive(Debug)] struct Invocation<'src: 'run, 'run> { arguments: Vec<&'run str>, + module_source: &'run Path, recipe: &'run Recipe<'src>, - settings: &'run Settings<'src>, scope: &'run Scope<'src, 'run>, + settings: &'run Settings<'src>, } #[derive(Debug, PartialEq, Serialize)] @@ -21,6 +22,8 @@ pub(crate) struct Justfile<'src> { pub(crate) name: Option>, pub(crate) recipes: Table<'src, Rc>>, pub(crate) settings: Settings<'src>, + #[serde(skip)] + pub(crate) source: PathBuf, pub(crate) warnings: Vec, } @@ -106,9 +109,10 @@ impl<'src> Justfile<'src> { &self.assignments, config, dotenv, + &self.source, scope, - &self.settings, search, + &self.settings, ) } @@ -276,10 +280,12 @@ impl<'src> Justfile<'src> { let mut ran = Ran::default(); for invocation in invocations { let context = RecipeContext { - settings: invocation.settings, config, + dotenv: &dotenv, + module_source: invocation.module_source, scope: invocation.scope, search, + settings: invocation.settings, }; Self::run_recipe( @@ -290,7 +296,6 @@ impl<'src> Justfile<'src> { .map(str::to_string) .collect::>(), &context, - &dotenv, &mut ran, invocation.recipe, search, @@ -346,6 +351,7 @@ impl<'src> Justfile<'src> { recipe, arguments: Vec::new(), scope, + module_source: &self.source, }, depth, ))); @@ -373,6 +379,7 @@ impl<'src> Justfile<'src> { recipe, scope: parent, settings: &self.settings, + module_source: &self.source, }, depth, ))) @@ -394,6 +401,7 @@ impl<'src> Justfile<'src> { recipe, scope: parent, settings: &self.settings, + module_source: &self.source, }, depth + argument_count, ))) @@ -410,7 +418,6 @@ impl<'src> Justfile<'src> { fn run_recipe( arguments: &[String], context: &RecipeContext<'src, '_>, - dotenv: &BTreeMap, ran: &mut Ran<'src>, recipe: &Recipe<'src>, search: &Search, @@ -426,19 +433,26 @@ impl<'src> Justfile<'src> { } let (outer, positional) = Evaluator::evaluate_parameters( - context.config, - dotenv, - &recipe.parameters, arguments, + context.config, + context.dotenv, + context.module_source, + &recipe.parameters, context.scope, - context.settings, search, + context.settings, )?; let scope = outer.child(); - let mut evaluator = - Evaluator::recipe_evaluator(context.config, dotenv, &scope, context.settings, search); + let mut evaluator = Evaluator::recipe_evaluator( + context.config, + context.dotenv, + context.module_source, + &scope, + search, + context.settings, + ); if !context.config.no_dependencies { for Dependency { recipe, arguments } in recipe.dependencies.iter().take(recipe.priors) { @@ -447,11 +461,11 @@ impl<'src> Justfile<'src> { .map(|argument| evaluator.evaluate_expression(argument)) .collect::>>()?; - Self::run_recipe(&arguments, context, dotenv, ran, recipe, search)?; + Self::run_recipe(&arguments, context, ran, recipe, search)?; } } - recipe.run(context, dotenv, scope.child(), search, &positional)?; + recipe.run(context, &scope, &positional)?; if !context.config.no_dependencies { let mut ran = Ran::default(); @@ -463,11 +477,12 @@ impl<'src> Justfile<'src> { evaluated.push(evaluator.evaluate_expression(argument)?); } - Self::run_recipe(&evaluated, context, dotenv, &mut ran, recipe, search)?; + Self::run_recipe(&evaluated, context, &mut ran, recipe, search)?; } } ran.ran(&recipe.namepath, arguments.to_vec()); + Ok(()) } diff --git a/src/parser.rs b/src/parser.rs index 3a05fea..415e9c7 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -18,11 +18,11 @@ use {super::*, TokenKind::*}; /// All methods starting with `parse_*` parse and return a language construct. /// /// The parser tracks an expected set of tokens as it parses. This set contains -/// all tokens which would have been accepted at the current point in the parse. -/// Whenever the parser tests for a token that would be accepted, but does not -/// find it, it adds that token to the set. When the parser accepts a token, the -/// set is cleared. If the parser finds a token which is unexpected, the -/// contents of the set is printed in the resultant error message. +/// all tokens which would have been accepted at the current point in the +/// parse. Whenever the parser tests for a token that would be accepted, but +/// does not find it, it adds that token to the set. When the parser accepts a +/// token, the set is cleared. If the parser finds a token which is unexpected, +/// the elements of the set are printed in the resultant error message. pub(crate) struct Parser<'run, 'src> { expected_tokens: BTreeSet, file_depth: u32, diff --git a/src/recipe.rs b/src/recipe.rs index 62c6d27..446c7bb 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -147,9 +147,7 @@ impl<'src, D> Recipe<'src, D> { pub(crate) fn run<'run>( &self, context: &RecipeContext<'src, 'run>, - dotenv: &BTreeMap, - scope: Scope<'src, 'run>, - search: &'run Search, + scope: &Scope<'src, 'run>, positional: &[String], ) -> RunResult<'src, ()> { let config = &context.config; @@ -164,20 +162,25 @@ impl<'src, D> Recipe<'src, D> { ); } - let evaluator = - Evaluator::recipe_evaluator(context.config, dotenv, &scope, context.settings, search); + let evaluator = Evaluator::recipe_evaluator( + context.config, + context.dotenv, + context.module_source, + scope, + context.search, + context.settings, + ); if self.shebang { - self.run_shebang(context, dotenv, &scope, positional, config, evaluator) + self.run_shebang(context, scope, positional, config, evaluator) } else { - self.run_linewise(context, dotenv, &scope, positional, config, evaluator) + self.run_linewise(context, scope, positional, config, evaluator) } } fn run_linewise<'run>( &self, context: &RecipeContext<'src, 'run>, - dotenv: &BTreeMap, scope: &Scope<'src, 'run>, positional: &[String], config: &Config, @@ -264,7 +267,7 @@ impl<'src, D> Recipe<'src, D> { cmd.stdout(Stdio::null()); } - cmd.export(context.settings, dotenv, scope); + cmd.export(context.settings, context.dotenv, scope); match InterruptHandler::guard(|| cmd.status()) { Ok(exit_status) => { @@ -298,7 +301,6 @@ impl<'src, D> Recipe<'src, D> { pub(crate) fn run_shebang<'run>( &self, context: &RecipeContext<'src, 'run>, - dotenv: &BTreeMap, scope: &Scope<'src, 'run>, positional: &[String], config: &Config, @@ -411,7 +413,7 @@ impl<'src, D> Recipe<'src, D> { command.args(positional); } - command.export(context.settings, dotenv, scope); + command.export(context.settings, context.dotenv, scope); // run it! match InterruptHandler::guard(|| command.status()) { diff --git a/src/recipe_context.rs b/src/recipe_context.rs index 0e46f5f..f1ed5c6 100644 --- a/src/recipe_context.rs +++ b/src/recipe_context.rs @@ -2,6 +2,8 @@ use super::*; pub(crate) struct RecipeContext<'src: 'run, 'run> { pub(crate) config: &'run Config, + pub(crate) dotenv: &'run BTreeMap, + pub(crate) module_source: &'run Path, pub(crate) scope: &'run Scope<'src, 'run>, pub(crate) search: &'run Search, pub(crate) settings: &'run Settings<'src>, diff --git a/tests/functions.rs b/tests/functions.rs index 514f598..caf772c 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -904,3 +904,126 @@ fn source_directory() { .stdout_regex(r".*[/\\]foo\n") .run(); } + +#[test] +fn module_paths() { + Test::new() + .write( + "foo/bar.just", + " +imf := module_file() +imd := module_directory() + +import-outer: import-inner + +@import-inner pmf=module_file() pmd=module_directory(): + echo import + echo '{{ imf }}' + echo '{{ imd }}' + echo '{{ pmf }}' + echo '{{ pmd }}' + echo '{{ module_file() }}' + echo '{{ module_directory() }}' + ", + ) + .write( + "baz/mod.just", + " +import 'foo/bar.just' + +mmf := module_file() +mmd := module_directory() + +outer: inner + +@inner pmf=module_file() pmd=module_directory(): + echo module + echo '{{ mmf }}' + echo '{{ mmd }}' + echo '{{ pmf }}' + echo '{{ pmd }}' + echo '{{ module_file() }}' + echo '{{ module_directory() }}' + ", + ) + .write( + "baz/foo/bar.just", + " +imf := module_file() +imd := module_directory() + +import-outer: import-inner + +@import-inner pmf=module_file() pmd=module_directory(): + echo import + echo '{{ imf }}' + echo '{{ imd }}' + echo '{{ pmf }}' + echo '{{ pmd }}' + echo '{{ module_file() }}' + echo '{{ module_directory() }}' + ", + ) + .justfile( + " + import 'foo/bar.just' + mod baz + + rmf := module_file() + rmd := module_directory() + + outer: inner + + @inner pmf=module_file() pmd=module_directory(): + echo root + echo '{{ rmf }}' + echo '{{ rmd }}' + echo '{{ pmf }}' + echo '{{ pmd }}' + echo '{{ module_file() }}' + echo '{{ module_directory() }}' + ", + ) + .test_round_trip(false) + .args([ + "--unstable", + "outer", + "import-outer", + "baz", + "outer", + "baz", + "import-outer", + ]) + .stdout_regex( + r"root +.*[/\\]just-test-tempdir......[/\\]justfile +.*[/\\]just-test-tempdir...... +.*[/\\]just-test-tempdir......[/\\]justfile +.*[/\\]just-test-tempdir...... +.*[/\\]just-test-tempdir......[/\\]justfile +.*[/\\]just-test-tempdir...... +import +.*[/\\]just-test-tempdir......[/\\]justfile +.*[/\\]just-test-tempdir...... +.*[/\\]just-test-tempdir......[/\\]justfile +.*[/\\]just-test-tempdir...... +.*[/\\]just-test-tempdir......[/\\]justfile +.*[/\\]just-test-tempdir...... +module +.*[/\\]just-test-tempdir......[/\\]baz[/\\]mod.just +.*[/\\]just-test-tempdir......[/\\]baz +.*[/\\]just-test-tempdir......[/\\]baz[/\\]mod.just +.*[/\\]just-test-tempdir......[/\\]baz +.*[/\\]just-test-tempdir......[/\\]baz[/\\]mod.just +.*[/\\]just-test-tempdir......[/\\]baz +import +.*[/\\]just-test-tempdir......[/\\]baz[/\\]mod.just +.*[/\\]just-test-tempdir......[/\\]baz +.*[/\\]just-test-tempdir......[/\\]baz[/\\]mod.just +.*[/\\]just-test-tempdir......[/\\]baz +.*[/\\]just-test-tempdir......[/\\]baz[/\\]mod.just +.*[/\\]just-test-tempdir......[/\\]baz +", + ) + .run(); +}