Add predefined constants (#2054)

This commit is contained in:
Casey Rodarmor 2024-05-18 16:12:11 -07:00 committed by GitHub
parent f3eebb74d8
commit 6907847a77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 128 additions and 31 deletions

View File

@ -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/*"]

View File

@ -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`<sup>master</sup> | `"0123456789abcdef"` |
| `HEXLOWER`<sup>master</sup> | `"0123456789abcdef"` |
| `HEXUPPER`<sup>master</sup> | `"0123456789ABCDEF"` |
```just
@foo:
echo {{HEX}}
```
```sh
$ just foo
0123456789abcdef
```
### Recipe Attributes
Recipes may be annotated with attributes that change their behavior.

View File

@ -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);

15
src/constants.rs Normal file
View File

@ -0,0 +1,15 @@
use super::*;
pub(crate) fn constants() -> &'static HashMap<&'static str, &'static str> {
static CONSTANTS: OnceLock<HashMap<&str, &str>> = OnceLock::new();
CONSTANTS.get_or_init(|| {
vec![
("HEX", "0123456789abcdef"),
("HEXLOWER", "0123456789abcdef"),
("HEXUPPER", "0123456789ABCDEF"),
]
.into_iter()
.collect()
})
}

View File

@ -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)?;

View File

@ -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"));
};

View File

@ -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;

View File

@ -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<String>) {

View File

@ -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 }));

View File

@ -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) {

45
tests/constants.rs Normal file
View File

@ -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();
}

View File

@ -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");

View File

@ -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;

View File

@ -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();
}