From d6669e0b97b89146930342b26c4ef2ccbdc8f30a Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sun, 7 Jul 2024 20:45:03 -0700 Subject: [PATCH] Allow enabling unstable features with `set unstable` (#2237) --- README.md | 15 +++++++------ src/analyzer.rs | 5 +++++ src/argument_parser.rs | 14 ++++++------ src/compiler.rs | 15 ++++--------- src/error.rs | 2 +- src/justfile.rs | 18 ++++++++++++++++ src/keyword.rs | 1 + src/lib.rs | 5 +++-- src/node.rs | 1 + src/parser.rs | 1 + src/setting.rs | 2 ++ src/settings.rs | 4 ++++ src/subcommand.rs | 4 +++- src/summary.rs | 2 +- src/unstable.rs | 12 +++++++++++ tests/fmt.rs | 5 +---- tests/json.rs | 20 ++++++++++++++++++ tests/modules.rs | 6 ++---- tests/test.rs | 2 +- tests/unstable.rs | 48 ++++++++++++++++++++++++++++++++++++------ 20 files changed, 136 insertions(+), 46 deletions(-) create mode 100644 src/unstable.rs diff --git a/README.md b/README.md index a992d62f8d..1f5d6ad089 100644 --- a/README.md +++ b/README.md @@ -379,11 +379,11 @@ There will never be a `just` 2.0. Any desirable backwards-incompatible changes will be opt-in on a per-`justfile` basis, so users may migrate at their leisure. -Features that aren't yet ready for stabilization are gated behind the -`--unstable` flag. Features enabled by `--unstable` may change in backwards -incompatible ways at any time. Unstable features can also be enabled by setting -the environment variable `JUST_UNSTABLE` to any value other than `false`, `0`, -or the empty string. +Features that aren't yet ready for stabilization are marked as unstable and may +be changed or removed at any time. Using unstable features produces an error by +default, which can be suppressed with by passing the `--unstable` flag, +`set unstable`, or setting the environment variable `JUST_UNSTABLE`, to any +value other than `false`, `0`, or the empty string. Editor Support -------------- @@ -820,6 +820,7 @@ foo: | `positional-arguments` | boolean | `false` | Pass positional arguments. | | `shell` | `[COMMAND, ARGS…]` | - | Set the command used to invoke recipes and evaluate backticks. | | `tempdir` | string | - | Create temporary directories in `tempdir` instead of the system default temporary directory. | +| `unstable`master | boolean | `false` | Enable unstable features. | | `windows-powershell` | boolean | `false` | Use PowerShell on Windows as default shell. (Deprecated. Use `windows-shell` instead. | | `windows-shell` | `[COMMAND, ARGS…]` | - | Set the command used to invoke recipes and evaluate backticks. | @@ -3154,8 +3155,8 @@ Missing source files for optional imports do not produce an error. ### Modules1.19.0 A `justfile` can declare modules using `mod` statements. `mod` statements are -currently unstable, so you'll need to use the `--unstable` flag, or set the -`JUST_UNSTABLE` environment variable to use them. +currently unstable, so you'll need to use the `--unstable` flag, +`set unstable`, or set the `JUST_UNSTABLE` environment variable to use them. If you have the following `justfile`: diff --git a/src/analyzer.rs b/src/analyzer.rs index 928d9fda6a..b0f447ec1a 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -37,6 +37,8 @@ impl<'src> Analyzer<'src> { let mut warnings = Vec::new(); + let mut unstable = BTreeSet::new(); + let mut modules: Table = Table::new(); let mut unexports: HashSet = HashSet::new(); @@ -92,6 +94,8 @@ impl<'src> Analyzer<'src> { doc, .. } => { + unstable.insert(Unstable::Modules); + if let Some(absolute) = absolute { define(*name, "module", false)?; modules.insert(Self::analyze( @@ -194,6 +198,7 @@ impl<'src> Analyzer<'src> { settings, source: root.into(), unexports, + unstable, warnings, }) } diff --git a/src/argument_parser.rs b/src/argument_parser.rs index 85dacccea6..091ffd89b9 100644 --- a/src/argument_parser.rs +++ b/src/argument_parser.rs @@ -252,7 +252,7 @@ mod tests { fs::write(&path, "mod foo").unwrap(); fs::create_dir(tempdir.path().join("foo")).unwrap(); fs::write(tempdir.path().join("foo/mod.just"), "bar:").unwrap(); - let compilation = Compiler::compile(true, &loader, &path).unwrap(); + let compilation = Compiler::compile(&loader, &path).unwrap(); assert_eq!( ArgumentParser::parse_arguments(&compilation.justfile, &["foo", "bar"]).unwrap(), @@ -271,7 +271,7 @@ mod tests { fs::write(&path, "mod foo").unwrap(); fs::create_dir(tempdir.path().join("foo")).unwrap(); fs::write(tempdir.path().join("foo/mod.just"), "bar:").unwrap(); - let compilation = Compiler::compile(true, &loader, &path).unwrap(); + let compilation = Compiler::compile(&loader, &path).unwrap(); assert_matches!( ArgumentParser::parse_arguments(&compilation.justfile, &["foo", "zzz"]).unwrap_err(), @@ -289,7 +289,7 @@ mod tests { tempdir.write("foo.just", "bar:"); let loader = Loader::new(); - let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap(); + let compilation = Compiler::compile(&loader, &tempdir.path().join("justfile")).unwrap(); assert_matches!( ArgumentParser::parse_arguments(&compilation.justfile, &["foo::zzz"]).unwrap_err(), @@ -307,7 +307,7 @@ mod tests { tempdir.write("foo.just", "bar:"); let loader = Loader::new(); - let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap(); + let compilation = Compiler::compile(&loader, &tempdir.path().join("justfile")).unwrap(); assert_matches!( ArgumentParser::parse_arguments(&compilation.justfile, &["foo::bar::baz"]).unwrap_err(), @@ -323,7 +323,7 @@ mod tests { tempdir.write("justfile", ""); let loader = Loader::new(); - let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap(); + let compilation = Compiler::compile(&loader, &tempdir.path().join("justfile")).unwrap(); assert_matches!( ArgumentParser::parse_arguments(&compilation.justfile, &[]).unwrap_err(), @@ -337,7 +337,7 @@ mod tests { tempdir.write("justfile", "foo bar:"); let loader = Loader::new(); - let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap(); + let compilation = Compiler::compile(&loader, &tempdir.path().join("justfile")).unwrap(); assert_matches!( ArgumentParser::parse_arguments(&compilation.justfile, &[]).unwrap_err(), @@ -355,7 +355,7 @@ mod tests { tempdir.write("foo.just", "bar:"); let loader = Loader::new(); - let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap(); + let compilation = Compiler::compile(&loader, &tempdir.path().join("justfile")).unwrap(); assert_matches!( ArgumentParser::parse_arguments(&compilation.justfile, &[]).unwrap_err(), diff --git a/src/compiler.rs b/src/compiler.rs index 31ad36274d..a977be586c 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -4,14 +4,13 @@ pub(crate) struct Compiler; impl Compiler { pub(crate) fn compile<'src>( - unstable: bool, loader: &'src Loader, root: &Path, ) -> RunResult<'src, Compilation<'src>> { let mut asts = HashMap::::new(); + let mut loaded = Vec::new(); let mut paths = HashMap::::new(); let mut srcs = HashMap::::new(); - let mut loaded = Vec::new(); let mut stack = Vec::new(); stack.push(Source::root(root)); @@ -42,12 +41,6 @@ impl Compiler { relative, .. } => { - if !unstable { - return Err(Error::Unstable { - message: "Modules are currently unstable.".into(), - }); - } - let parent = current.path.parent().unwrap(); let import = if let Some(relative) = relative { @@ -112,9 +105,9 @@ impl Compiler { Ok(Compilation { asts, - srcs, justfile, root: root.into(), + srcs, }) } @@ -225,7 +218,7 @@ recipe_b: recipe_c let loader = Loader::new(); let justfile_a_path = tmp.path().join("justfile"); - let compilation = Compiler::compile(false, &loader, &justfile_a_path).unwrap(); + let compilation = Compiler::compile(&loader, &justfile_a_path).unwrap(); assert_eq!(compilation.root_src(), justfile_a); } @@ -242,7 +235,7 @@ recipe_b: recipe_c let loader = Loader::new(); let justfile_a_path = tmp.path().join("justfile"); - let loader_output = Compiler::compile(false, &loader, &justfile_a_path).unwrap_err(); + let loader_output = Compiler::compile(&loader, &justfile_a_path).unwrap_err(); assert_matches!(loader_output, Error::CircularImport { current, import } if current == tmp.path().join("subdir").join("b").lexiclean() && diff --git a/src/error.rs b/src/error.rs index ca2acb153a..d2e8704f2c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -460,7 +460,7 @@ impl<'src> ColorDisplay for Error<'src> { } } Unstable { message } => { - write!(f, "{message} Invoke `just` with the `--unstable` flag to enable unstable features.")?; + write!(f, "{message} Invoke `just` with `--unstable`, set the `JUST_UNSTABLE` environment variable, or add `set unstable` to your `justfile` to enable unstable features.")?; } WriteJustfile { justfile, io_error } => { let justfile = justfile.display(); diff --git a/src/justfile.rs b/src/justfile.rs index 19503923f1..c5be95df8c 100644 --- a/src/justfile.rs +++ b/src/justfile.rs @@ -27,6 +27,8 @@ pub(crate) struct Justfile<'src> { pub(crate) source: PathBuf, pub(crate) unexports: HashSet, pub(crate) warnings: Vec, + #[serde(skip)] + pub(crate) unstable: BTreeSet, } impl<'src> Justfile<'src> { @@ -225,6 +227,22 @@ impl<'src> Justfile<'src> { Ok(()) } + pub(crate) fn check_unstable(&self, config: &Config) -> RunResult<'src> { + if !config.unstable && !self.settings.unstable { + if let Some(unstable) = self.unstable.iter().next() { + return Err(Error::Unstable { + message: unstable.message(), + }); + } + } + + 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) } diff --git a/src/keyword.rs b/src/keyword.rs index d57032ae40..7f8b5b3f45 100644 --- a/src/keyword.rs +++ b/src/keyword.rs @@ -26,6 +26,7 @@ pub(crate) enum Keyword { Tempdir, True, Unexport, + Unstable, WindowsPowershell, WindowsShell, X, diff --git a/src/lib.rs b/src/lib.rs index 50e54e90bc..127881e2d9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,8 +42,8 @@ pub(crate) use { shell::Shell, show_whitespace::ShowWhitespace, source::Source, string_kind::StringKind, string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table, thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency, - unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables, - verbosity::Verbosity, warning::Warning, + unresolved_recipe::UnresolvedRecipe, unstable::Unstable, use_color::UseColor, + variables::Variables, verbosity::Verbosity, warning::Warning, }, camino::Utf8Path, clap::ValueEnum, @@ -204,6 +204,7 @@ mod token_kind; mod unindent; mod unresolved_dependency; mod unresolved_recipe; +mod unstable; mod use_color; mod variables; mod verbosity; diff --git a/src/node.rs b/src/node.rs index 2fd73e303b..1de9bdcb1f 100644 --- a/src/node.rs +++ b/src/node.rs @@ -294,6 +294,7 @@ impl<'src> Node<'src> for Set<'src> { | Setting::Fallback(value) | Setting::PositionalArguments(value) | Setting::Quiet(value) + | Setting::Unstable(value) | Setting::WindowsPowerShell(value) | Setting::IgnoreComments(value) => { set.push_mut(value.to_string()); diff --git a/src/parser.rs b/src/parser.rs index 90389a29f0..6a18d5f931 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -936,6 +936,7 @@ impl<'run, 'src> Parser<'run, 'src> { Keyword::IgnoreComments => Some(Setting::IgnoreComments(self.parse_set_bool()?)), Keyword::PositionalArguments => Some(Setting::PositionalArguments(self.parse_set_bool()?)), Keyword::Quiet => Some(Setting::Quiet(self.parse_set_bool()?)), + Keyword::Unstable => Some(Setting::Unstable(self.parse_set_bool()?)), Keyword::WindowsPowershell => Some(Setting::WindowsPowerShell(self.parse_set_bool()?)), _ => None, }; diff --git a/src/setting.rs b/src/setting.rs index e6cb515565..f19dab4769 100644 --- a/src/setting.rs +++ b/src/setting.rs @@ -15,6 +15,7 @@ pub(crate) enum Setting<'src> { Quiet(bool), Shell(Shell<'src>), Tempdir(String), + Unstable(bool), WindowsPowerShell(bool), WindowsShell(Shell<'src>), } @@ -31,6 +32,7 @@ impl<'src> Display for Setting<'src> { | Self::IgnoreComments(value) | Self::PositionalArguments(value) | Self::Quiet(value) + | Self::Unstable(value) | Self::WindowsPowerShell(value) => write!(f, "{value}"), Self::Shell(shell) | Self::WindowsShell(shell) => write!(f, "{shell}"), Self::DotenvFilename(value) | Self::DotenvPath(value) | Self::Tempdir(value) => { diff --git a/src/settings.rs b/src/settings.rs index ae6c4b1c44..338f2cc727 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -20,6 +20,7 @@ pub(crate) struct Settings<'src> { pub(crate) quiet: bool, pub(crate) shell: Option>, pub(crate) tempdir: Option, + pub(crate) unstable: bool, pub(crate) windows_powershell: bool, pub(crate) windows_shell: Option>, } @@ -66,6 +67,9 @@ impl<'src> Settings<'src> { Setting::Shell(shell) => { settings.shell = Some(shell); } + Setting::Unstable(unstable) => { + settings.unstable = unstable; + } Setting::WindowsPowerShell(windows_powershell) => { settings.windows_powershell = windows_powershell; } diff --git a/src/subcommand.rs b/src/subcommand.rs index 65c80a8320..6c405b747d 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -190,7 +190,9 @@ impl Subcommand { loader: &'src Loader, search: &Search, ) -> RunResult<'src, Compilation<'src>> { - let compilation = Compiler::compile(config.unstable, loader, &search.justfile)?; + let compilation = Compiler::compile(loader, &search.justfile)?; + + compilation.justfile.check_unstable(config)?; if config.verbosity.loud() { for warning in &compilation.justfile.warnings { diff --git a/src/summary.rs b/src/summary.rs index 65a28ba1f9..ee3a8d1155 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -28,7 +28,7 @@ mod full { pub fn summary(path: &Path) -> io::Result> { let loader = Loader::new(); - match Compiler::compile(false, &loader, path) { + match Compiler::compile(&loader, path) { Ok(compilation) => Ok(Ok(Summary::new(&compilation.justfile))), Err(error) => Ok(Err(if let Error::Compile { compile_error } = error { compile_error.to_string() diff --git a/src/unstable.rs b/src/unstable.rs new file mode 100644 index 0000000000..6647fb4ad6 --- /dev/null +++ b/src/unstable.rs @@ -0,0 +1,12 @@ +#[derive(Copy, Clone, Debug, PartialEq, Ord, Eq, PartialOrd)] +pub(crate) enum Unstable { + Modules, +} + +impl Unstable { + pub(crate) fn message(self) -> String { + match self { + Self::Modules => "Modules are currently unstable.".into(), + } + } +} diff --git a/tests/fmt.rs b/tests/fmt.rs index ba050fc3e2..29a12ba467 100644 --- a/tests/fmt.rs +++ b/tests/fmt.rs @@ -4,10 +4,7 @@ test! { name: unstable_not_passed, justfile: "", args: ("--fmt"), - stderr: " - error: The `--fmt` command is currently unstable. \ - Invoke `just` with the `--unstable` flag to enable unstable features. - ", + stderr_regex: "error: The `--fmt` command is currently unstable..*", status: EXIT_FAILURE, } diff --git a/tests/json.rs b/tests/json.rs index 9b827ca425..a88bdefd02 100644 --- a/tests/json.rs +++ b/tests/json.rs @@ -56,7 +56,9 @@ fn alias() { "quiet": false, "shell": null, "tempdir" : null, + "unstable": false, "ignore_comments": false, + "unstable": false, "windows_powershell": false, "windows_shell": null, }, @@ -98,6 +100,7 @@ fn assignment() { "quiet": false, "shell": null, "tempdir" : null, + "unstable": false, "windows_powershell": false, "windows_shell": null, }, @@ -153,6 +156,7 @@ fn body() { "quiet": false, "shell": null, "tempdir" : null, + "unstable": false, "windows_powershell": false, "windows_shell": null, }, @@ -220,6 +224,7 @@ fn dependencies() { "quiet": false, "shell": null, "tempdir" : null, + "unstable": false, "windows_powershell": false, "windows_shell": null, }, @@ -325,6 +330,7 @@ fn dependency_argument() { "quiet": false, "shell": null, "tempdir" : null, + "unstable": false, "windows_powershell": false, "windows_shell": null, }, @@ -392,6 +398,7 @@ fn duplicate_recipes() { "quiet": false, "shell": null, "tempdir" : null, + "unstable": false, "windows_powershell": false, "windows_shell": null, }, @@ -437,6 +444,7 @@ fn duplicate_variables() { "quiet": false, "shell": null, "tempdir" : null, + "unstable": false, "windows_powershell": false, "windows_shell": null, }, @@ -485,6 +493,7 @@ fn doc_comment() { "quiet": false, "shell": null, "tempdir" : null, + "unstable": false, "windows_powershell": false, "windows_shell": null, }, @@ -519,6 +528,7 @@ fn empty_justfile() { "quiet": false, "shell": null, "tempdir" : null, + "unstable": false, "windows_powershell": false, "windows_shell": null, }, @@ -674,6 +684,7 @@ fn parameters() { "quiet": false, "shell": null, "tempdir" : null, + "unstable": false, "windows_powershell": false, "windows_shell": null, }, @@ -762,6 +773,7 @@ fn priors() { "quiet": false, "shell": null, "tempdir" : null, + "unstable": false, "windows_powershell": false, "windows_shell": null, }, @@ -810,6 +822,7 @@ fn private() { "quiet": false, "shell": null, "tempdir" : null, + "unstable": false, "windows_powershell": false, "windows_shell": null, }, @@ -858,6 +871,7 @@ fn quiet() { "quiet": false, "shell": null, "tempdir" : null, + "unstable": false, "windows_powershell": false, "windows_shell": null, }, @@ -921,6 +935,7 @@ fn settings() { "command": "a", }, "tempdir": null, + "unstable": false, "windows_powershell": false, "windows_shell": null, }, @@ -972,6 +987,7 @@ fn shebang() { "quiet": false, "shell": null, "tempdir": null, + "unstable": false, "windows_powershell": false, "windows_shell": null, }, @@ -1020,6 +1036,7 @@ fn simple() { "quiet": false, "shell": null, "tempdir": null, + "unstable": false, "windows_powershell": false, "windows_shell": null, }, @@ -1070,6 +1087,7 @@ fn attribute() { "quiet": false, "shell": null, "tempdir" : null, + "unstable": false, "ignore_comments": false, "windows_powershell": false, "windows_shell": null, @@ -1136,6 +1154,7 @@ fn module() { "quiet": false, "shell": null, "tempdir" : null, + "unstable": false, "ignore_comments": false, "windows_powershell": false, "windows_shell": null, @@ -1158,6 +1177,7 @@ fn module() { "quiet": false, "shell": null, "tempdir" : null, + "unstable": false, "ignore_comments": false, "windows_powershell": false, "windows_shell": null, diff --git a/tests/modules.rs b/tests/modules.rs index 369285856c..178394208f 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -8,10 +8,8 @@ fn modules_are_unstable() { mod foo ", ) - .stderr( - "error: Modules are currently unstable. \ - Invoke `just` with the `--unstable` flag to enable unstable features.\n", - ) + .write("foo.just", "") + .stderr_regex("error: Modules are currently unstable..*") .status(EXIT_FAILURE) .run(); } diff --git a/tests/test.rs b/tests/test.rs index 29350351d6..288f6bd352 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -150,7 +150,7 @@ impl Test { } pub(crate) fn stderr_regex(mut self, stderr_regex: impl AsRef) -> Self { - self.stderr_regex = Some(Regex::new(&format!("^{}$", stderr_regex.as_ref())).unwrap()); + self.stderr_regex = Some(Regex::new(&format!("^(?s){}$", stderr_regex.as_ref())).unwrap()); self } diff --git a/tests/unstable.rs b/tests/unstable.rs index 23a5fa6ede..ad196cf7b8 100644 --- a/tests/unstable.rs +++ b/tests/unstable.rs @@ -26,12 +26,12 @@ default: "#; for val in ["0", "", "false"] { Test::new() - .justfile(justfile) - .args(["--fmt"]) - .env("JUST_UNSTABLE", val) - .status(EXIT_FAILURE) - .stderr("error: The `--fmt` command is currently unstable. Invoke `just` with the `--unstable` flag to enable unstable features.\n") - .run(); + .justfile(justfile) + .args(["--fmt"]) + .env("JUST_UNSTABLE", val) + .status(EXIT_FAILURE) + .stderr_regex("error: The `--fmt` command is currently unstable.*") + .run(); } } @@ -45,6 +45,40 @@ default: .justfile(justfile) .args(["--fmt"]) .status(EXIT_FAILURE) - .stderr("error: The `--fmt` command is currently unstable. Invoke `just` with the `--unstable` flag to enable unstable features.\n") + .stderr_regex("error: The `--fmt` command is currently unstable.*") + .run(); +} + +#[test] +fn set_unstable_with_setting() { + Test::new() + .justfile( + " + set unstable + + mod foo + ", + ) + .write("foo.just", "@bar:\n echo BAR") + .args(["foo", "bar"]) + .stdout("BAR\n") + .run(); +} + +#[test] +fn unstable_setting_does_not_affect_submodules() { + Test::new() + .justfile( + " + set unstable + + mod foo + ", + ) + .write("foo.just", "mod bar") + .write("bar.just", "baz:\n echo hello") + .args(["foo", "bar"]) + .stderr_regex("error: Modules are currently unstable.*") + .status(EXIT_FAILURE) .run(); }