Repository: casey/just
Pull request #2250 (2024-07-14 20:55:30)

Stabilize modules


Patch:
diff --git a/README.md b/README.md
index d8e1a0af..778af97b 100644
--- a/README.md
+++ b/README.md
@@ -667,10 +667,10 @@ $ cat foo.just
 mod bar
 $ cat bar.just
 baz:
-$ just --unstable foo bar
+$ just foo bar
 Available recipes:
     baz
-$ just --unstable foo::bar
+$ just foo::bar
 Available recipes:
     baz
 ```
@@ -3154,9 +3154,11 @@ Missing source files for optional imports do not produce an error.
 
 ### Modules<sup>1.19.0</sup>
 
-A `justfile` can declare modules using `mod` statements. `mod` statements are
-currently unstable, so you'll need to use the `--unstable` flag,
-`set unstable`, or set the `JUST_UNSTABLE` environment variable to use them.
+A `justfile` can declare modules using `mod` statements.
+
+`mod` statements were stabilized in `just`<sup>master</sup>. In earlier
+versions, 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`:
 
@@ -3181,14 +3183,14 @@ uses its own settings.
 Recipes in submodules can be invoked as subcommands:
 
 ```sh
-$ just --unstable bar b
+$ just bar b
 B
 ```
 
 Or with path syntax:
 
 ```sh
-$ just --unstable bar::b
+$ just bar::b
 B
 ```
 
diff --git a/src/analyzer.rs b/src/analyzer.rs
index b0f447ec..48c17c73 100644
--- a/src/analyzer.rs
+++ b/src/analyzer.rs
@@ -37,8 +37,6 @@ impl<'src> Analyzer<'src> {
 
     let mut warnings = Vec::new();
 
-    let mut unstable = BTreeSet::new();
-
     let mut modules: Table<Justfile> = Table::new();
 
     let mut unexports: HashSet<String> = HashSet::new();
@@ -94,8 +92,6 @@ impl<'src> Analyzer<'src> {
             doc,
             ..
           } => {
-            unstable.insert(Unstable::Modules);
-
             if let Some(absolute) = absolute {
               define(*name, "module", false)?;
               modules.insert(Self::analyze(
@@ -198,7 +194,7 @@ impl<'src> Analyzer<'src> {
       settings,
       source: root.into(),
       unexports,
-      unstable,
+      unstable_features: BTreeSet::new(),
       warnings,
     })
   }
diff --git a/src/config.rs b/src/config.rs
index 51252ba9..88a45def 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -720,13 +720,15 @@ impl Config {
     })
   }
 
-  pub(crate) fn require_unstable(&self, message: &str) -> RunResult<'static> {
-    if self.unstable {
+  pub(crate) fn require_unstable(
+    &self,
+    justfile: &Justfile,
+    unstable_feature: UnstableFeature,
+  ) -> RunResult<'static> {
+    if self.unstable || justfile.settings.unstable {
       Ok(())
     } else {
-      Err(Error::Unstable {
-        message: message.to_owned(),
-      })
+      Err(Error::UnstableFeature { unstable_feature })
     }
   }
 
diff --git a/src/error.rs b/src/error.rs
index 563f21b0..31a8bd4b 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -174,8 +174,8 @@ pub(crate) enum Error<'src> {
     recipe: String,
     suggestion: Option<Suggestion<'src>>,
   },
-  Unstable {
-    message: String,
+  UnstableFeature {
+    unstable_feature: UnstableFeature,
   },
   WriteJustfile {
     justfile: PathBuf,
@@ -459,8 +459,8 @@ impl<'src> ColorDisplay for Error<'src> {
           write!(f, "\n{suggestion}")?;
         }
       }
-      Unstable { message } => {
-        write!(f, "{message} Invoke `just` with `--unstable`, set the `JUST_UNSTABLE` environment variable, or add `set unstable` to your `justfile` to enable unstable features.")?;
+      UnstableFeature { unstable_feature } => {
+        write!(f, "{unstable_feature} 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 c5be95df..52d9f01d 100644
--- a/src/justfile.rs
+++ b/src/justfile.rs
@@ -26,9 +26,9 @@ pub(crate) struct Justfile<'src> {
   #[serde(skip)]
   pub(crate) source: PathBuf,
   pub(crate) unexports: HashSet<String>,
-  pub(crate) warnings: Vec<Warning>,
   #[serde(skip)]
-  pub(crate) unstable: BTreeSet<Unstable>,
+  pub(crate) unstable_features: BTreeSet<UnstableFeature>,
+  pub(crate) warnings: Vec<Warning>,
 }
 
 impl<'src> Justfile<'src> {
@@ -228,12 +228,8 @@ impl<'src> Justfile<'src> {
   }
 
   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(),
-        });
-      }
+    if let Some(&unstable_feature) = self.unstable_features.iter().next() {
+      config.require_unstable(self, unstable_feature)?;
     }
 
     for module in self.modules.values() {
diff --git a/src/lib.rs b/src/lib.rs
index 127881e2..4245a15c 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -42,7 +42,7 @@ 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, unstable::Unstable, use_color::UseColor,
+    unresolved_recipe::UnresolvedRecipe, unstable_feature::UnstableFeature, use_color::UseColor,
     variables::Variables, verbosity::Verbosity, warning::Warning,
   },
   camino::Utf8Path,
@@ -204,7 +204,7 @@ mod token_kind;
 mod unindent;
 mod unresolved_dependency;
 mod unresolved_recipe;
-mod unstable;
+mod unstable_feature;
 mod use_color;
 mod variables;
 mod verbosity;
diff --git a/src/subcommand.rs b/src/subcommand.rs
index 865b28ff..c9cce978 100644
--- a/src/subcommand.rs
+++ b/src/subcommand.rs
@@ -79,7 +79,7 @@ impl Subcommand {
         justfile.run(config, &search, overrides, &[])?;
       }
       Dump => Self::dump(config, ast, justfile)?,
-      Format => Self::format(config, &search, src, ast)?,
+      Format => Self::format(config, &search, src, ast, justfile)?,
       Groups => Self::groups(config, justfile),
       List { path } => Self::list(config, justfile, path)?,
       Show { path } => Self::show(config, justfile, path)?,
@@ -337,8 +337,14 @@ impl Subcommand {
     Ok(())
   }
 
-  fn format(config: &Config, search: &Search, src: &str, ast: &Ast) -> RunResult<'static> {
-    config.require_unstable("The `--fmt` command is currently unstable.")?;
+  fn format(
+    config: &Config,
+    search: &Search,
+    src: &str,
+    ast: &Ast,
+    justfile: &Justfile,
+  ) -> RunResult<'static> {
+    config.require_unstable(justfile, UnstableFeature::FormatSubcommand)?;
 
     let formatted = ast.to_string();
 
diff --git a/src/unstable.rs b/src/unstable.rs
deleted file mode 100644
index 6647fb4a..00000000
--- a/src/unstable.rs
+++ /dev/null
@@ -1,12 +0,0 @@
-#[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/src/unstable_feature.rs b/src/unstable_feature.rs
new file mode 100644
index 00000000..84fb3a23
--- /dev/null
+++ b/src/unstable_feature.rs
@@ -0,0 +1,14 @@
+use super::*;
+
+#[derive(Copy, Clone, Debug, PartialEq, Ord, Eq, PartialOrd)]
+pub(crate) enum UnstableFeature {
+  FormatSubcommand,
+}
+
+impl Display for UnstableFeature {
+  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+    match self {
+      Self::FormatSubcommand => write!(f, "The `--fmt` command is currently unstable."),
+    }
+  }
+}


Test patch:
diff --git a/tests/choose.rs b/tests/choose.rs
index 001b9ae5..0af9ed1b 100644
--- a/tests/choose.rs
+++ b/tests/choose.rs
@@ -86,7 +86,6 @@ fn recipes_in_submodules_can_be_chosen() {
     .args(["--unstable", "--choose"])
     .env("JUST_CHOOSER", "head -n10")
     .write("bar.just", "baz:\n echo BAZ")
-    .test_round_trip(false)
     .justfile(
       "
         mod bar
diff --git a/tests/functions.rs b/tests/functions.rs
index 4ebd87e0..439b87a4 100644
--- a/tests/functions.rs
+++ b/tests/functions.rs
@@ -864,7 +864,6 @@ fn source_file() {
 
   Test::new()
     .args(["--evaluate", "x"])
-    .test_round_trip(false)
     .justfile(
       "
         import 'foo.just'
@@ -875,8 +874,7 @@ fn source_file() {
     .run();
 
   Test::new()
-    .args(["--unstable", "foo", "bar"])
-    .test_round_trip(false)
+    .args(["foo", "bar"])
     .justfile(
       "
         mod foo
@@ -890,8 +888,7 @@ fn source_file() {
 #[test]
 fn source_directory() {
   Test::new()
-    .args(["--unstable", "foo", "bar"])
-    .test_round_trip(false)
+    .args(["foo", "bar"])
     .justfile(
       "
         mod foo
@@ -984,9 +981,7 @@ import-outer: import-inner
           echo '{{ module_directory() }}'
       ",
     )
-    .test_round_trip(false)
     .args([
-      "--unstable",
       "outer",
       "import-outer",
       "baz",
diff --git a/tests/imports.rs b/tests/imports.rs
index 448af29d..693a67cd 100644
--- a/tests/imports.rs
+++ b/tests/imports.rs
@@ -17,7 +17,6 @@ fn import_succeeds() {
           @echo A
       ",
     )
-    .test_round_trip(false)
     .arg("a")
     .stdout("B\nA\n")
     .run();
@@ -34,7 +33,6 @@ fn missing_import_file_error() {
           @echo A
       ",
     )
-    .test_round_trip(false)
     .arg("a")
     .status(EXIT_FAILURE)
     .stderr(
@@ -60,7 +58,6 @@ fn missing_optional_imports_are_ignored() {
           @echo A
       ",
     )
-    .test_round_trip(false)
     .arg("a")
     .stdout("A\n")
     .run();
@@ -79,7 +76,6 @@ fn trailing_spaces_after_import_are_ignored() {
         @echo A
     ",
     )
-    .test_round_trip(false)
     .stdout("A\n")
     .run();
 }
@@ -99,7 +95,6 @@ fn import_after_recipe() {
       import './import.justfile'
       ",
     )
-    .test_round_trip(false)
     .stdout("A\n")
     .run();
 }
@@ -126,7 +121,6 @@ fn import_recipes_are_not_default() {
       "import.justfile": "bar:",
     })
     .justfile("import './import.justfile'")
-    .test_round_trip(false)
     .status(EXIT_FAILURE)
     .stderr("error: Justfile contains no default recipe.\n")
     .run();
@@ -143,7 +137,6 @@ fn listed_recipes_in_imports_are_in_load_order() {
     )
     .write("import.justfile", "bar:")
     .args(["--list", "--unsorted"])
-    .test_round_trip(false)
     .stdout(
       "
       Available recipes:
@@ -190,7 +183,6 @@ fn recipes_in_import_are_overridden_by_recipes_in_parent() {
         set allow-duplicate-recipes
       ",
     )
-    .test_round_trip(false)
     .arg("a")
     .stdout("ROOT\n")
     .run();
@@ -216,7 +208,6 @@ fn variables_in_import_are_overridden_by_variables_in_parent() {
         @echo {{f}}
     ",
     )
-    .test_round_trip(false)
     .arg("a")
     .stdout("bar\n")
     .run();
@@ -232,7 +223,6 @@ fn import_paths_beginning_with_tilde_are_expanded_to_homdir() {
         import '~/mod.just'
       ",
     )
-    .test_round_trip(false)
     .arg("foo")
     .stdout("FOOBAR\n")
     .env("HOME", "foobar")
@@ -248,7 +238,6 @@ fn imports_dump_correctly() {
         import './import.justfile'
       ",
     )
-    .test_round_trip(false)
     .arg("--dump")
     .stdout("import './import.justfile'\n")
     .run();
@@ -263,7 +252,6 @@ fn optional_imports_dump_correctly() {
         import? './import.justfile'
       ",
     )
-    .test_round_trip(false)
     .arg("--dump")
     .stdout("import? './import.justfile'\n")
     .run();
@@ -279,7 +267,6 @@ fn imports_in_root_run_in_justfile_directory() {
         import 'foo/import.justfile'
       ",
     )
-    .test_round_trip(false)
     .arg("bar")
     .stdout("BAZ")
     .run();
@@ -292,8 +279,6 @@ fn imports_in_submodules_run_in_submodule_directory() {
     .write("foo/mod.just", "import 'import.just'")
     .write("foo/import.just", "bar:\n @cat baz")
     .write("foo/baz", "BAZ")
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("foo")
     .arg("bar")
     .stdout("BAZ")
@@ -306,7 +291,6 @@ fn nested_import_paths_are_relative_to_containing_submodule() {
     .justfile("import 'foo/import.just'")
     .write("foo/import.just", "import 'bar.just'")
     .write("foo/bar.just", "bar:\n @echo BAR")
-    .test_round_trip(false)
     .arg("bar")
     .stdout("BAR\n")
     .run();
@@ -319,8 +303,6 @@ fn recipes_in_nested_imports_run_in_parent_module() {
     .write("foo/import.just", "import 'bar/import.just'")
     .write("foo/bar/import.just", "bar:\n @cat baz")
     .write("baz", "BAZ")
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("bar")
     .stdout("BAZ")
     .run();
@@ -339,7 +321,6 @@ fn shebang_recipes_in_imports_in_root_run_in_justfile_directory() {
         import 'foo/import.justfile'
       ",
     )
-    .test_round_trip(false)
     .arg("bar")
     .stdout("BAZ")
     .run();
@@ -357,7 +338,6 @@ fn recipes_imported_in_root_run_in_command_line_provided_working_directory() {
       "--justfile",
       "subdir/a.justfile",
     ])
-    .test_round_trip(false)
     .stdout("BAZBAZ")
     .run();
 }
diff --git a/tests/json.rs b/tests/json.rs
index a88bdefd..70b19304 100644
--- a/tests/json.rs
+++ b/tests/json.rs
@@ -3,7 +3,7 @@ use super::*;
 fn case(justfile: &str, value: Value) {
   Test::new()
     .justfile(justfile)
-    .args(["--dump", "--dump-format", "json", "--unstable"])
+    .args(["--dump", "--dump-format", "json"])
     .stdout(format!("{}\n", serde_json::to_string(&value).unwrap()))
     .run();
 }
@@ -1110,8 +1110,7 @@ fn module() {
     .tree(tree! {
       "foo.just": "bar:",
     })
-    .args(["--dump", "--dump-format", "json", "--unstable"])
-    .test_round_trip(false)
+    .args(["--dump", "--dump-format", "json"])
     .stdout(format!(
       "{}\n",
       serde_json::to_string(&json!({
diff --git a/tests/list.rs b/tests/list.rs
index 4b9d56dc..4ba8f707 100644
--- a/tests/list.rs
+++ b/tests/list.rs
@@ -12,8 +12,7 @@ fn modules_unsorted() {
         mod bar
       ",
     )
-    .test_round_trip(false)
-    .args(["--unstable", "--list", "--unsorted"])
+    .args(["--list", "--unsorted"])
     .stdout(
       "
         Available recipes:
@@ -156,8 +155,7 @@ fn list_submodule() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .args(["--unstable", "--list", "foo"])
+    .args(["--list", "foo"])
     .stdout(
       "
       Available recipes:
@@ -177,8 +175,7 @@ fn list_nested_submodule() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .args(["--unstable", "--list", "foo", "bar"])
+    .args(["--list", "foo", "bar"])
     .stdout(
       "
         Available recipes:
@@ -195,8 +192,7 @@ fn list_nested_submodule() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .args(["--unstable", "--list", "foo::bar"])
+    .args(["--list", "foo::bar"])
     .stdout(
       "
         Available recipes:
@@ -209,7 +205,7 @@ fn list_nested_submodule() {
 #[test]
 fn list_invalid_path() {
   Test::new()
-    .args(["--unstable", "--list", "$hello"])
+    .args(["--list", "$hello"])
     .stderr("error: Invalid module path `$hello`\n")
     .status(1)
     .run();
@@ -218,7 +214,7 @@ fn list_invalid_path() {
 #[test]
 fn list_unknown_submodule() {
   Test::new()
-    .args(["--unstable", "--list", "hello"])
+    .args(["--list", "hello"])
     .stderr("error: Justfile does not contain submodule `hello`\n")
     .status(1)
     .run();
@@ -236,8 +232,7 @@ fn list_with_groups_in_modules() {
       ",
     )
     .write("bar.just", "[group('BAZ')]\nbaz:")
-    .test_round_trip(false)
-    .args(["--unstable", "--list", "--list-submodules"])
+    .args(["--list", "--list-submodules"])
     .stdout(
       "
         Available recipes:
@@ -261,8 +256,7 @@ fn list_displays_recipes_in_submodules() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .args(["--unstable", "--list", "--list-submodules"])
+    .args(["--list", "--list-submodules"])
     .stdout(
       "
       Available recipes:
@@ -285,8 +279,7 @@ fn modules_are_space_separated_in_output() {
         mod bar
       ",
     )
-    .test_round_trip(false)
-    .args(["--unstable", "--list", "--list-submodules"])
+    .args(["--list", "--list-submodules"])
     .stdout(
       "
       Available recipes:
@@ -319,8 +312,7 @@ barbarbar:
       ",
     )
     .justfile("mod foo")
-    .test_round_trip(false)
-    .args(["--unstable", "--list", "--list-submodules"])
+    .args(["--list", "--list-submodules"])
     .stdout(
       "
         Available recipes:
@@ -341,8 +333,7 @@ fn nested_modules_are_properly_indented() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .args(["--unstable", "--list", "--list-submodules"])
+    .args(["--list", "--list-submodules"])
     .stdout(
       "
       Available recipes:
@@ -364,8 +355,7 @@ fn module_doc_rendered() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .args(["--unstable", "--list"])
+    .args(["--list"])
     .stdout(
       "
         Available recipes:
@@ -393,8 +383,7 @@ fn module_doc_aligned() {
             @echo Hi
       ",
     )
-    .test_round_trip(false)
-    .args(["--unstable", "--list"])
+    .args(["--list"])
     .stdout(
       "
         Available recipes:
@@ -418,8 +407,7 @@ fn space_before_submodules_following_groups() {
         bar:
       ",
     )
-    .test_round_trip(false)
-    .args(["--unstable", "--list"])
+    .args(["--list"])
     .stdout(
       "
         Available recipes:
@@ -441,8 +429,7 @@ fn no_space_before_submodules_not_following_groups() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .args(["--unstable", "--list"])
+    .args(["--list"])
     .stdout(
       "
         Available recipes:
diff --git a/tests/modules.rs b/tests/modules.rs
index 46a06bca..b01957d3 100644
--- a/tests/modules.rs
+++ b/tests/modules.rs
@@ -1,16 +1,16 @@
 use super::*;
 
 #[test]
-fn modules_are_unstable() {
+fn modules_are_stable() {
   Test::new()
     .justfile(
       "
         mod foo
       ",
     )
-    .write("foo.just", "")
-    .stderr_regex("error: Modules are currently unstable..*")
-    .status(EXIT_FAILURE)
+    .write("foo.just", "@bar:\n echo ok")
+    .args(["foo", "bar"])
+    .stdout("ok\n")
     .run();
 }
 
@@ -23,8 +23,6 @@ fn default_recipe_in_submodule_must_have_no_arguments() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("foo")
     .stderr("error: Recipe `foo` cannot be used as default recipe since it requires at least 1 argument.\n")
     .status(EXIT_FAILURE)
@@ -40,8 +38,6 @@ fn module_recipes_can_be_run_as_subcommands() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("foo")
     .arg("foo")
     .stdout("FOO\n")
@@ -57,8 +53,6 @@ fn module_recipes_can_be_run_with_path_syntax() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("foo::foo")
     .stdout("FOO\n")
     .run();
@@ -74,8 +68,6 @@ fn nested_module_recipes_can_be_run_with_path_syntax() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("foo::bar::baz")
     .stdout("BAZ\n")
     .run();
@@ -84,21 +76,18 @@ fn nested_module_recipes_can_be_run_with_path_syntax() {
 #[test]
 fn invalid_path_syntax() {
   Test::new()
-    .test_round_trip(false)
     .arg(":foo::foo")
     .stderr("error: Justfile does not contain recipe `:foo::foo`.\n")
     .status(EXIT_FAILURE)
     .run();
 
   Test::new()
-    .test_round_trip(false)
     .arg("foo::foo:")
     .stderr("error: Justfile does not contain recipe `foo::foo:`.\n")
     .status(EXIT_FAILURE)
     .run();
 
   Test::new()
-    .test_round_trip(false)
     .arg("foo:::foo")
     .stderr("error: Justfile does not contain recipe `foo:::foo`.\n")
     .status(EXIT_FAILURE)
@@ -108,7 +97,6 @@ fn invalid_path_syntax() {
 #[test]
 fn missing_recipe_after_invalid_path() {
   Test::new()
-    .test_round_trip(false)
     .arg(":foo::foo")
     .arg("bar")
     .stderr("error: Justfile does not contain recipe `:foo::foo`.\n")
@@ -126,8 +114,6 @@ fn assignments_are_evaluated_in_modules() {
         bar := 'PARENT'
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("foo")
     .arg("foo")
     .stdout("CHILD\n")
@@ -143,8 +129,6 @@ fn module_subcommand_runs_default_recipe() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("foo")
     .stdout("FOO\n")
     .run();
@@ -160,8 +144,6 @@ fn modules_can_contain_other_modules() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("foo")
     .arg("bar")
     .arg("baz")
@@ -179,8 +161,6 @@ fn circular_module_imports_are_detected() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("foo")
     .arg("bar")
     .arg("baz")
@@ -207,8 +187,6 @@ foo:
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("foo")
     .arg("foo")
     .stdout("FOO\n")
@@ -229,9 +207,7 @@ foo:
         set allow-duplicate-recipes
       ",
     )
-    .test_round_trip(false)
     .status(EXIT_FAILURE)
-    .arg("--unstable")
     .arg("foo")
     .arg("foo")
     .stderr(
@@ -265,9 +241,7 @@ fn modules_conflict_with_recipes() {
         │ ^^^
     ",
     )
-    .test_round_trip(false)
     .status(EXIT_FAILURE)
-    .arg("--unstable")
     .run();
 }
 
@@ -291,9 +265,7 @@ fn modules_conflict_with_aliases() {
         │       ^^^
     ",
     )
-    .test_round_trip(false)
     .status(EXIT_FAILURE)
-    .arg("--unstable")
     .run();
 }
 
@@ -309,7 +281,6 @@ fn modules_conflict_with_other_modules() {
         bar:
       ",
     )
-    .test_round_trip(false)
     .status(EXIT_FAILURE)
     .stderr(
       "
@@ -320,7 +291,6 @@ fn modules_conflict_with_other_modules() {
         │     ^^^
     ",
     )
-    .arg("--unstable")
     .run();
 }
 
@@ -333,8 +303,6 @@ fn modules_are_dumped_correctly() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("--dump")
     .stdout("mod foo\n")
     .run();
@@ -349,8 +317,6 @@ fn optional_modules_are_dumped_correctly() {
         mod? foo
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("--dump")
     .stdout("mod? foo\n")
     .run();
@@ -365,8 +331,6 @@ fn modules_can_be_in_subdirectory() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("foo")
     .arg("foo")
     .stdout("FOO\n")
@@ -382,8 +346,6 @@ fn modules_in_subdirectory_can_be_named_justfile() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("foo")
     .arg("foo")
     .stdout("FOO\n")
@@ -399,8 +361,6 @@ fn modules_in_subdirectory_can_be_named_justfile_with_any_case() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("foo")
     .arg("foo")
     .stdout("FOO\n")
@@ -416,8 +376,6 @@ fn modules_in_subdirectory_can_have_leading_dot() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("foo")
     .arg("foo")
     .stdout("FOO\n")
@@ -434,8 +392,6 @@ fn modules_require_unambiguous_file() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .status(EXIT_FAILURE)
     .stderr(
       "
@@ -458,8 +414,6 @@ fn missing_module_file_error() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .status(EXIT_FAILURE)
     .stderr(
       "
@@ -484,8 +438,6 @@ fn missing_optional_modules_do_not_trigger_error() {
           @echo BAR
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .stdout("BAR\n")
     .run();
 }
@@ -501,8 +453,6 @@ fn missing_optional_modules_do_not_conflict() {
       ",
     )
     .write("baz.just", "baz:\n @echo BAZ")
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("foo")
     .arg("baz")
     .stdout("BAZ\n")
@@ -521,8 +471,7 @@ fn root_dotenv_is_available_to_submodules() {
     )
     .write("foo.just", "foo:\n @echo $DOTENV_KEY")
     .write(".env", "DOTENV_KEY=dotenv-value")
-    .test_round_trip(false)
-    .args(["--unstable", "foo", "foo"])
+    .args(["foo", "foo"])
     .stdout("dotenv-value\n")
     .run();
 }
@@ -542,8 +491,7 @@ fn dotenv_settings_in_submodule_are_ignored() {
       "set dotenv-load := false\nfoo:\n @echo $DOTENV_KEY",
     )
     .write(".env", "DOTENV_KEY=dotenv-value")
-    .test_round_trip(false)
-    .args(["--unstable", "foo", "foo"])
+    .args(["foo", "foo"])
     .stdout("dotenv-value\n")
     .run();
 }
@@ -557,8 +505,6 @@ fn modules_may_specify_path() {
         mod foo 'commands/foo.just'
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("foo")
     .arg("foo")
     .stdout("FOO\n")
@@ -574,8 +520,6 @@ fn modules_may_specify_path_to_directory() {
         mod foo 'commands/bar'
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("foo")
     .arg("foo")
     .stdout("FOO\n")
@@ -591,8 +535,6 @@ fn modules_with_paths_are_dumped_correctly() {
         mod foo 'commands/foo.just'
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("--dump")
     .stdout("mod foo 'commands/foo.just'\n")
     .run();
@@ -607,8 +549,6 @@ fn optional_modules_with_paths_are_dumped_correctly() {
         mod? foo 'commands/foo.just'
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("--dump")
     .stdout("mod? foo 'commands/foo.just'\n")
     .run();
@@ -623,7 +563,6 @@ fn recipes_may_be_named_mod() {
           @echo FOO
       ",
     )
-    .test_round_trip(false)
     .arg("mod")
     .arg("bar")
     .stdout("FOO\n")
@@ -640,8 +579,6 @@ fn submodule_linewise_recipes_run_in_submodule_directory() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("foo")
     .arg("foo")
     .stdout("BAR")
@@ -658,8 +595,6 @@ fn submodule_shebang_recipes_run_in_submodule_directory() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("foo")
     .arg("foo")
     .stdout("BAR")
@@ -676,8 +611,6 @@ fn module_paths_beginning_with_tilde_are_expanded_to_homdir() {
         mod foo '~/mod.just'
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("foo")
     .arg("foo")
     .stdout("FOOBAR\n")
@@ -697,8 +630,6 @@ fn recipes_with_same_name_are_both_run() {
           @echo ROOT
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("foo::bar")
     .arg("bar")
     .stdout("MODULE\nROOT\n")
@@ -708,7 +639,7 @@ fn recipes_with_same_name_are_both_run() {
 #[test]
 fn submodule_recipe_not_found_error_message() {
   Test::new()
-    .args(["--unstable", "foo::bar"])
+    .args(["foo::bar"])
     .stderr("error: Justfile does not contain submodule `foo`\n")
     .status(1)
     .run();
@@ -723,8 +654,7 @@ fn submodule_recipe_not_found_spaced_error_message() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .args(["--unstable", "foo", "baz"])
+    .args(["foo", "baz"])
     .stderr("error: Justfile does not contain recipe `foo baz`.\nDid you mean `bar`?\n")
     .status(1)
     .run();
@@ -739,8 +669,7 @@ fn submodule_recipe_not_found_colon_separated_error_message() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .args(["--unstable", "foo::baz"])
+    .args(["foo::baz"])
     .stderr("error: Justfile does not contain recipe `foo::baz`.\nDid you mean `bar`?\n")
     .status(1)
     .run();
@@ -758,7 +687,7 @@ fn colon_separated_path_does_not_run_recipes() {
           @echo BAR
       ",
     )
-    .args(["--unstable", "foo::bar"])
+    .args(["foo::bar"])
     .stderr("error: Expected submodule at `foo` but found recipe.\n")
     .status(1)
     .run();
@@ -779,8 +708,7 @@ fn expected_submodule_but_found_recipe_in_submodule_error() {
   Test::new()
     .justfile("mod foo")
     .write("foo.just", "bar:")
-    .test_round_trip(false)
-    .args(["--unstable", "foo::bar::baz"])
+    .args(["foo::bar::baz"])
     .stderr("error: Expected submodule at `foo::bar` but found recipe.\n")
     .status(1)
     .run();
@@ -805,8 +733,7 @@ fn comments_can_follow_modules() {
         mod foo # this is foo
       ",
     )
-    .test_round_trip(false)
-    .args(["--unstable", "foo", "foo"])
+    .args(["foo", "foo"])
     .stdout("FOO\n")
     .run();
 }
diff --git a/tests/shell_expansion.rs b/tests/shell_expansion.rs
index 87086d55..67fdc07d 100644
--- a/tests/shell_expansion.rs
+++ b/tests/shell_expansion.rs
@@ -66,7 +66,7 @@ fn shell_expanded_strings_are_dumped_correctly() {
       ",
     )
     .env("JUST_TEST_VARIABLE", "FOO")
-    .args(["--dump", "--unstable"])
+    .args(["--dump"])
     .stdout("x := x'$JUST_TEST_VARIABLE'\n")
     .run();
 }
@@ -114,9 +114,8 @@ fn shell_expanded_strings_can_be_used_in_mod_paths() {
     )
     .write("mod.just", "@bar:\n echo BAR")
     .env("JUST_TEST_VARIABLE", "mod.just")
-    .args(["--unstable", "foo", "bar"])
+    .args(["foo", "bar"])
     .stdout("BAR\n")
-    .test_round_trip(false)
     .run();
 }
 
diff --git a/tests/show.rs b/tests/show.rs
index 17aaa473..a66f9089 100644
--- a/tests/show.rs
+++ b/tests/show.rs
@@ -110,8 +110,7 @@ fn show_recipe_at_path() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .args(["--unstable", "--show", "foo::bar"])
+    .args(["--show", "foo::bar"])
     .stdout("bar:\n    @echo MODULE\n")
     .run();
 }
@@ -134,8 +133,7 @@ fn show_space_separated_path() {
         mod foo
       ",
     )
-    .test_round_trip(false)
-    .args(["--unstable", "--show", "foo bar"])
+    .args(["--show", "foo bar"])
     .stdout("bar:\n    @echo MODULE\n")
     .run();
 }
diff --git a/tests/summary.rs b/tests/summary.rs
index 7b70baa8..9cafacc1 100644
--- a/tests/summary.rs
+++ b/tests/summary.rs
@@ -65,8 +65,6 @@ fn submodule_recipes() {
         bar:
       ",
     )
-    .test_round_trip(false)
-    .arg("--unstable")
     .arg("--summary")
     .stdout("bar foo::foo foo::bar::bar foo::bar::baz::baz foo::bar::baz::biz::biz\n")
     .run();
@@ -81,7 +79,6 @@ fn summary_implies_unstable() {
         mod foo
       ",
     )
-    .test_round_trip(false)
     .arg("--summary")
     .stdout("foo::foo\n")
     .run();
diff --git a/tests/test.rs b/tests/test.rs
index 288f6bd3..217983b9 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -169,6 +169,7 @@ impl Test {
     self
   }
 
+  #[allow(unused)]
   pub(crate) fn test_round_trip(mut self, test_round_trip: bool) -> Self {
     self.test_round_trip = test_round_trip;
     self
diff --git a/tests/unstable.rs b/tests/unstable.rs
index ad196cf7..f3fbfd4d 100644
--- a/tests/unstable.rs
+++ b/tests/unstable.rs
@@ -2,14 +2,9 @@ use super::*;
 
 #[test]
 fn set_unstable_true_with_env_var() {
-  let justfile = r#"
-default:
-    echo 'foo'
-  "#;
-
   for val in ["true", "some-arbitrary-string"] {
     Test::new()
-      .justfile(justfile)
+      .justfile("")
       .args(["--fmt"])
       .env("JUST_UNSTABLE", val)
       .status(EXIT_SUCCESS)
@@ -20,13 +15,9 @@ default:
 
 #[test]
 fn set_unstable_false_with_env_var() {
-  let justfile = r#"
-default:
-    echo 'foo'
-  "#;
   for val in ["0", "", "false"] {
     Test::new()
-      .justfile(justfile)
+      .justfile("")
       .args(["--fmt"])
       .env("JUST_UNSTABLE", val)
       .status(EXIT_FAILURE)
@@ -37,12 +28,8 @@ default:
 
 #[test]
 fn set_unstable_false_with_env_var_unset() {
-  let justfile = r#"
-default:
-    echo 'foo'
-  "#;
   Test::new()
-    .justfile(justfile)
+    .justfile("")
     .args(["--fmt"])
     .status(EXIT_FAILURE)
     .stderr_regex("error: The `--fmt` command is currently unstable.*")
@@ -52,19 +39,16 @@ default:
 #[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")
+    .justfile("set unstable")
+    .arg("--fmt")
+    .stderr_regex("Wrote justfile to .*")
     .run();
 }
 
+// This test should be re-enabled if we get a new unstable feature which is
+// encountered in source files. (As opposed to, for example, the unstable
+// `--fmt` subcommand, which is encountered on the command line.)
+#[cfg(any())]
 #[test]
 fn unstable_setting_does_not_affect_submodules() {
   Test::new()