From 6907847a77de291fe8855e2a58a0bcb475e58dd0 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sat, 18 May 2024 16:12:11 -0700 Subject: [PATCH] Add predefined constants (#2054) --- Cargo.toml | 2 +- README.md | 20 +++++++++++++++++ src/assignment_resolver.rs | 2 +- src/constants.rs | 15 +++++++++++++ src/justfile.rs | 2 +- src/lexer.rs | 11 +++------- src/lib.rs | 9 ++++---- src/ran.rs | 3 +-- src/recipe_resolver.rs | 5 +++-- src/scope.rs | 24 ++++++++++++++++++-- tests/constants.rs | 45 ++++++++++++++++++++++++++++++++++++++ tests/functions.rs | 9 -------- tests/lib.rs | 3 ++- tests/test.rs | 9 ++++++++ 14 files changed, 128 insertions(+), 31 deletions(-) create mode 100644 src/constants.rs create mode 100644 tests/constants.rs diff --git a/Cargo.toml b/Cargo.toml index 5ac4ca8..cf03201 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["command-line", "task", "runner", "development", "utility"] license = "CC0-1.0" readme = "crates-io-readme.md" repository = "https://github.com/casey/just" -rust-version = "1.63" +rust-version = "1.70" [workspace] members = [".", "crates/*"] diff --git a/README.md b/README.md index bf52875..06a51c5 100644 --- a/README.md +++ b/README.md @@ -1541,6 +1541,26 @@ and are implemented with the - `executable_directory()` - The user-specific executable directory. - `home_directory()` - The user's home directory. +### Constants + +A number of constants are predefined: + +| Name | Value | +|------|-------------| +| `HEX`master | `"0123456789abcdef"` | +| `HEXLOWER`master | `"0123456789abcdef"` | +| `HEXUPPER`master | `"0123456789ABCDEF"` | + +```just +@foo: + echo {{HEX}} +``` + +```sh +$ just foo +0123456789abcdef +``` + ### Recipe Attributes Recipes may be annotated with attributes that change their behavior. diff --git a/src/assignment_resolver.rs b/src/assignment_resolver.rs index 1835b4f..04a8936 100644 --- a/src/assignment_resolver.rs +++ b/src/assignment_resolver.rs @@ -128,7 +128,7 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> { Expression::StringLiteral { .. } | Expression::Backtick { .. } => Ok(()), Expression::Variable { name } => { let variable = name.lexeme(); - if self.evaluated.contains(variable) { + if self.evaluated.contains(variable) || constants().contains_key(variable) { Ok(()) } else if self.stack.contains(&variable) { self.stack.push(variable); diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..e9007ea --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,15 @@ +use super::*; + +pub(crate) fn constants() -> &'static HashMap<&'static str, &'static str> { + static CONSTANTS: OnceLock> = OnceLock::new(); + + CONSTANTS.get_or_init(|| { + vec![ + ("HEX", "0123456789abcdef"), + ("HEXLOWER", "0123456789abcdef"), + ("HEXUPPER", "0123456789ABCDEF"), + ] + .into_iter() + .collect() + }) +} diff --git a/src/justfile.rs b/src/justfile.rs index f8f2455..30985bf 100644 --- a/src/justfile.rs +++ b/src/justfile.rs @@ -135,7 +135,7 @@ impl<'src> Justfile<'src> { BTreeMap::new() }; - let root = Scope::new(); + let root = Scope::root(); let scope = self.scope(config, &dotenv, search, overrides, &root)?; diff --git a/src/lexer.rs b/src/lexer.rs index 4c25223..adbad1b 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -231,11 +231,8 @@ impl<'src> Lexer<'src> { // The width of the error site to highlight depends on the kind of error: let length = match kind { UnterminatedString | UnterminatedBacktick => { - let kind = match StringKind::from_token_start(self.lexeme()) { - Some(kind) => kind, - None => { - return self.internal_error("Lexer::error: expected string or backtick token start") - } + let Some(kind) = StringKind::from_token_start(self.lexeme()) else { + return self.internal_error("Lexer::error: expected string or backtick token start"); }; kind.delimiter().len() } @@ -813,9 +810,7 @@ impl<'src> Lexer<'src> { /// Cooked string: "[^"]*" # also processes escape sequences /// Raw string: '[^']*' fn lex_string(&mut self) -> CompileResult<'src> { - let kind = if let Some(kind) = StringKind::from_token_start(self.rest()) { - kind - } else { + let Some(kind) = StringKind::from_token_start(self.rest()) else { self.advance()?; return Err(self.internal_error("Lexer::lex_string: invalid string start")); }; diff --git a/src/lib.rs b/src/lib.rs index f3f0d0f..8616e13 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,9 +20,9 @@ pub(crate) use { color::Color, color_display::ColorDisplay, command_ext::CommandExt, compilation::Compilation, compile_error::CompileError, compile_error_kind::CompileErrorKind, compiler::Compiler, condition::Condition, conditional_operator::ConditionalOperator, config::Config, - config_error::ConfigError, count::Count, delimiter::Delimiter, dependency::Dependency, - dump_format::DumpFormat, enclosure::Enclosure, error::Error, evaluator::Evaluator, - expression::Expression, fragment::Fragment, function::Function, + config_error::ConfigError, constants::constants, count::Count, delimiter::Delimiter, + dependency::Dependency, dump_format::DumpFormat, enclosure::Enclosure, error::Error, + evaluator::Evaluator, expression::Expression, fragment::Fragment, function::Function, interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item, justfile::Justfile, keyed::Keyed, keyword::Keyword, lexer::Lexer, line::Line, list::List, load_dotenv::load_dotenv, loader::Loader, name::Name, namepath::Namepath, ordinal::Ordinal, @@ -53,7 +53,7 @@ pub(crate) use { process::{self, Command, ExitStatus, Stdio}, rc::Rc, str::{self, Chars}, - sync::{Mutex, MutexGuard}, + sync::{Mutex, MutexGuard, OnceLock}, vec, }, { @@ -128,6 +128,7 @@ mod condition; mod conditional_operator; mod config; mod config_error; +mod constants; mod count; mod delimiter; mod dependency; diff --git a/src/ran.rs b/src/ran.rs index a19c12d..7751e78 100644 --- a/src/ran.rs +++ b/src/ran.rs @@ -8,8 +8,7 @@ impl<'src> Ran<'src> { self .0 .get(recipe) - .map(|ran| ran.contains(arguments)) - .unwrap_or_default() + .is_some_and(|ran| ran.contains(arguments)) } pub(crate) fn ran(&mut self, recipe: &Namepath<'src>, arguments: Vec) { diff --git a/src/recipe_resolver.rs b/src/recipe_resolver.rs index 88e4978..5a962d8 100644 --- a/src/recipe_resolver.rs +++ b/src/recipe_resolver.rs @@ -58,8 +58,9 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> { parameters: &[Parameter], ) -> CompileResult<'src> { let name = variable.lexeme(); - let undefined = - !self.assignments.contains_key(name) && !parameters.iter().any(|p| p.name.lexeme() == name); + let undefined = !self.assignments.contains_key(name) + && !parameters.iter().any(|p| p.name.lexeme() == name) + && !constants().contains_key(name); if undefined { return Err(variable.error(UndefinedVariable { variable: name })); diff --git a/src/scope.rs b/src/scope.rs index 18aa17c..dd7888c 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -14,11 +14,31 @@ impl<'src, 'run> Scope<'src, 'run> { } } - pub(crate) fn new() -> Self { - Self { + pub(crate) fn root() -> Self { + let mut root = Self { parent: None, bindings: Table::new(), + }; + + for (key, value) in constants() { + root.bind( + false, + Name { + token: Token { + column: 0, + kind: TokenKind::Identifier, + length: key.len(), + line: 0, + offset: 0, + path: Path::new("PRELUDE"), + src: key, + }, + }, + (*value).into(), + ); } + + root } pub(crate) fn bind(&mut self, export: bool, name: Name<'src>, value: String) { diff --git a/tests/constants.rs b/tests/constants.rs new file mode 100644 index 0000000..59f8b9b --- /dev/null +++ b/tests/constants.rs @@ -0,0 +1,45 @@ +use super::*; + +#[test] +fn constants_are_defined() { + assert_eval_eq("HEX", "0123456789abcdef"); +} + +#[test] +fn constants_are_defined_in_recipe_bodies() { + Test::new() + .justfile( + " + @foo: + echo {{HEX}} + ", + ) + .stdout("0123456789abcdef\n") + .run(); +} + +#[test] +fn constants_are_defined_in_recipe_parameters() { + Test::new() + .justfile( + " + @foo hex=HEX: + echo {{hex}} + ", + ) + .stdout("0123456789abcdef\n") + .run(); +} + +#[test] +fn constants_can_be_redefined() { + Test::new() + .justfile( + " + HEX := 'foo' + ", + ) + .args(["--evaluate", "HEX"]) + .stdout("foo") + .run(); +} diff --git a/tests/functions.rs b/tests/functions.rs index a9fb98b..95bddfd 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -436,15 +436,6 @@ fn semver_matches() { .run(); } -fn assert_eval_eq(expression: &str, result: &str) { - Test::new() - .justfile(format!("x := {expression}")) - .args(["--evaluate", "x"]) - .stdout(result) - .unindent_stdout(false) - .run(); -} - #[test] fn trim_end_matches() { assert_eval_eq("trim_end_matches('foo', 'o')", "f"); diff --git a/tests/lib.rs b/tests/lib.rs index 26c9d12..d282d2d 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -3,7 +3,7 @@ pub(crate) use { assert_stdout::assert_stdout, assert_success::assert_success, tempdir::tempdir, - test::{Output, Test}, + test::{assert_eval_eq, Output, Test}, }, cradle::input::Input, executable_path::executable_path, @@ -46,6 +46,7 @@ mod command; mod completions; mod conditional; mod confirm; +mod constants; mod delimiters; mod directories; mod dotenv; diff --git a/tests/test.rs b/tests/test.rs index 65ee30a..5883043 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -312,3 +312,12 @@ fn test_round_trip(tmpdir: &Path) { assert_eq!(reparsed, dumped, "reparse mismatch"); } + +pub fn assert_eval_eq(expression: &str, result: &str) { + Test::new() + .justfile(format!("x := {expression}")) + .args(["--evaluate", "x"]) + .stdout(result) + .unindent_stdout(false) + .run(); +}