From ab3da9bf64661079b75ec310fc17d61c540e569d Mon Sep 17 00:00:00 2001 From: Archie Date: Wed, 22 Jan 2025 01:00:20 +0000 Subject: [PATCH] Add `no-exit-message` Setting and `[exit-message]` attribute (#2568) --- src/attribute.rs | 6 +- src/compile_error.rs | 4 + src/compile_error_kind.rs | 3 + src/keyword.rs | 1 + src/node.rs | 1 + src/parser.rs | 17 +- src/recipe.rs | 18 +- src/setting.rs | 2 + src/settings.rs | 4 + tests/json.rs | 1 + tests/no_exit_message.rs | 374 ++++++++++++++++++++++++++------------ 11 files changed, 307 insertions(+), 124 deletions(-) diff --git a/src/attribute.rs b/src/attribute.rs index 4ec813f076..fb2fef4f89 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -11,6 +11,7 @@ use super::*; pub(crate) enum Attribute<'src> { Confirm(Option>), Doc(Option>), + ExitMessage, Extension(StringLiteral<'src>), Group(StringLiteral<'src>), Linux, @@ -32,7 +33,8 @@ impl AttributeDiscriminant { match self { Self::Confirm | Self::Doc => 0..=1, Self::Group | Self::Extension | Self::WorkingDirectory => 1..=1, - Self::Linux + Self::ExitMessage + | Self::Linux | Self::Macos | Self::NoCd | Self::NoExitMessage @@ -78,6 +80,7 @@ impl<'src> Attribute<'src> { Ok(match discriminant { AttributeDiscriminant::Confirm => Self::Confirm(arguments.into_iter().next()), AttributeDiscriminant::Doc => Self::Doc(arguments.into_iter().next()), + AttributeDiscriminant::ExitMessage => Self::ExitMessage, AttributeDiscriminant::Extension => Self::Extension(arguments.into_iter().next().unwrap()), AttributeDiscriminant::Group => Self::Group(arguments.into_iter().next().unwrap()), AttributeDiscriminant::Linux => Self::Linux, @@ -129,6 +132,7 @@ impl Display for Attribute<'_> { Self::Script(Some(shell)) => write!(f, "({shell})")?, Self::Confirm(None) | Self::Doc(None) + | Self::ExitMessage | Self::Linux | Self::Macos | Self::NoCd diff --git a/src/compile_error.rs b/src/compile_error.rs index ce53c12f70..b09a1e5eb3 100644 --- a/src/compile_error.rs +++ b/src/compile_error.rs @@ -121,6 +121,10 @@ impl Display for CompileError<'_> { DuplicateUnexport { variable } => { write!(f, "Variable `{variable}` is unexported multiple times") } + ExitMessageAndNoExitMessageAttribute { recipe } => write!( + f, + "Recipe `{recipe}` has both `[exit-message]` and `[no-exit-message]` attributes" + ), ExpectedKeyword { expected, found } => { let expected = List::or_ticked(expected); if found.kind == TokenKind::Identifier { diff --git a/src/compile_error_kind.rs b/src/compile_error_kind.rs index 09e2eb337c..ca6f6fa7a6 100644 --- a/src/compile_error_kind.rs +++ b/src/compile_error_kind.rs @@ -47,6 +47,9 @@ pub(crate) enum CompileErrorKind<'src> { DuplicateUnexport { variable: &'src str, }, + ExitMessageAndNoExitMessageAttribute { + recipe: &'src str, + }, ExpectedKeyword { expected: Vec, found: Token<'src>, diff --git a/src/keyword.rs b/src/keyword.rs index ff06c39d27..41c9aaa7c8 100644 --- a/src/keyword.rs +++ b/src/keyword.rs @@ -19,6 +19,7 @@ pub(crate) enum Keyword { IgnoreComments, Import, Mod, + NoExitMessage, PositionalArguments, Quiet, ScriptInterpreter, diff --git a/src/node.rs b/src/node.rs index 6bfb042af8..c39dc07469 100644 --- a/src/node.rs +++ b/src/node.rs @@ -291,6 +291,7 @@ impl<'src> Node<'src> for Set<'src> { | Setting::DotenvRequired(value) | Setting::Export(value) | Setting::Fallback(value) + | Setting::NoExitMessage(value) | Setting::PositionalArguments(value) | Setting::Quiet(value) | Setting::Unstable(value) diff --git a/src/parser.rs b/src/parser.rs index 348032a065..42e9f26cca 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -951,9 +951,9 @@ impl<'run, 'src> Parser<'run, 'src> { })); } - let working_directory = attributes.contains(AttributeDiscriminant::WorkingDirectory); - - if working_directory && attributes.contains(AttributeDiscriminant::NoCd) { + if attributes.contains(AttributeDiscriminant::WorkingDirectory) + && attributes.contains(AttributeDiscriminant::NoCd) + { return Err( name.error(CompileErrorKind::NoCdAndWorkingDirectoryAttribute { recipe: name.lexeme(), @@ -961,6 +961,16 @@ impl<'run, 'src> Parser<'run, 'src> { ); } + if attributes.contains(AttributeDiscriminant::ExitMessage) + && attributes.contains(AttributeDiscriminant::NoExitMessage) + { + return Err( + name.error(CompileErrorKind::ExitMessageAndNoExitMessageAttribute { + recipe: name.lexeme(), + }), + ); + } + let private = name.lexeme().starts_with('_') || attributes.contains(AttributeDiscriminant::Private); @@ -1093,6 +1103,7 @@ impl<'run, 'src> Parser<'run, 'src> { Keyword::Export => Some(Setting::Export(self.parse_set_bool()?)), Keyword::Fallback => Some(Setting::Fallback(self.parse_set_bool()?)), Keyword::IgnoreComments => Some(Setting::IgnoreComments(self.parse_set_bool()?)), + Keyword::NoExitMessage => Some(Setting::NoExitMessage(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()?)), diff --git a/src/recipe.rs b/src/recipe.rs index 113653777a..1f5cb9812a 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -131,10 +131,16 @@ impl<'src, D> Recipe<'src, D> { || (cfg!(windows) && windows) } - fn print_exit_message(&self) -> bool { - !self - .attributes - .contains(AttributeDiscriminant::NoExitMessage) + fn print_exit_message(&self, settings: &Settings) -> bool { + if self.attributes.contains(AttributeDiscriminant::ExitMessage) { + true + } else if settings.no_exit_message { + false + } else { + !self + .attributes + .contains(AttributeDiscriminant::NoExitMessage) + } } fn working_directory<'a>(&'a self, context: &'a ExecutionContext) -> Option { @@ -304,7 +310,7 @@ impl<'src, D> Recipe<'src, D> { recipe: self.name(), line_number: Some(line_number), code, - print_message: self.print_exit_message(), + print_message: self.print_exit_message(&context.module.settings), }); } } else { @@ -449,7 +455,7 @@ impl<'src, D> Recipe<'src, D> { recipe: self.name(), line_number: None, code, - print_message: self.print_exit_message(), + print_message: self.print_exit_message(&context.module.settings), }) } }, diff --git a/src/setting.rs b/src/setting.rs index ec45f30144..f187f41897 100644 --- a/src/setting.rs +++ b/src/setting.rs @@ -11,6 +11,7 @@ pub(crate) enum Setting<'src> { Export(bool), Fallback(bool), IgnoreComments(bool), + NoExitMessage(bool), PositionalArguments(bool), Quiet(bool), ScriptInterpreter(Interpreter<'src>), @@ -32,6 +33,7 @@ impl Display for Setting<'_> { | Self::Export(value) | Self::Fallback(value) | Self::IgnoreComments(value) + | Self::NoExitMessage(value) | Self::PositionalArguments(value) | Self::Quiet(value) | Self::Unstable(value) diff --git a/src/settings.rs b/src/settings.rs index 4d0f9ea249..6ab3335398 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -16,6 +16,7 @@ pub(crate) struct Settings<'src> { pub(crate) export: bool, pub(crate) fallback: bool, pub(crate) ignore_comments: bool, + pub(crate) no_exit_message: bool, pub(crate) positional_arguments: bool, pub(crate) quiet: bool, #[serde(skip)] @@ -61,6 +62,9 @@ impl<'src> Settings<'src> { Setting::IgnoreComments(ignore_comments) => { settings.ignore_comments = ignore_comments; } + Setting::NoExitMessage(no_exit_message) => { + settings.no_exit_message = no_exit_message; + } Setting::PositionalArguments(positional_arguments) => { settings.positional_arguments = positional_arguments; } diff --git a/tests/json.rs b/tests/json.rs index 4e740e52e7..d7a1a1c391 100644 --- a/tests/json.rs +++ b/tests/json.rs @@ -84,6 +84,7 @@ struct Settings<'a> { export: bool, fallback: bool, ignore_comments: bool, + no_exit_message: bool, positional_arguments: bool, quiet: bool, shell: Option>, diff --git a/tests/no_exit_message.rs b/tests/no_exit_message.rs index 95771982d3..c55dbc82ec 100644 --- a/tests/no_exit_message.rs +++ b/tests/no_exit_message.rs @@ -1,131 +1,277 @@ use super::*; -test! { - name: recipe_exit_message_suppressed, - justfile: r#" -# This is a doc comment -[no-exit-message] -hello: - @echo "Hello, World!" - @exit 100 -"#, - stdout: "Hello, World!\n", - stderr: "", - status: 100, +#[test] +fn recipe_exit_message_suppressed() { + Test::new() + .justfile( + " + # This is a doc comment + [no-exit-message] + hello: + @echo 'Hello, World!' + @exit 100 + ", + ) + .stdout("Hello, World!\n") + .status(100) + .run(); } -test! { - name: silent_recipe_exit_message_suppressed, - justfile: r#" -# This is a doc comment -[no-exit-message] -@hello: - echo "Hello, World!" - exit 100 -"#, - stdout: "Hello, World!\n", - stderr: "", - status: 100, +#[test] +fn silent_recipe_exit_message_suppressed() { + Test::new() + .justfile( + " + # This is a doc comment + [no-exit-message] + @hello: + echo 'Hello, World!' + exit 100 + ", + ) + .stdout("Hello, World!\n") + .status(100) + .run(); } -test! { - name: recipe_has_doc_comment, - justfile: r" -# This is a doc comment -[no-exit-message] -hello: - @exit 100 -", - args: ("--list"), - stdout: " - Available recipes: - hello # This is a doc comment - ", +#[test] +fn recipe_has_doc_comment() { + Test::new() + .justfile( + " + # This is a doc comment + [no-exit-message] + hello: + @exit 100 + ", + ) + .arg("--list") + .stdout( + " + Available recipes: + hello # This is a doc comment + ", + ) + .run(); } -test! { - name: unknown_attribute, - justfile: r" -# This is a doc comment -[unknown-attribute] -hello: - @exit 100 -", - stderr: r" -error: Unknown attribute `unknown-attribute` - ——▶ justfile:2:2 - │ -2 │ [unknown-attribute] - │ ^^^^^^^^^^^^^^^^^ -", - status: EXIT_FAILURE, +#[test] +fn unknown_attribute() { + Test::new() + .justfile( + " + # This is a doc comment + [unknown-attribute] + hello: + @exit 100 + ", + ) + .stderr( + " + error: Unknown attribute `unknown-attribute` + ——▶ justfile:2:2 + │ + 2 │ [unknown-attribute] + │ ^^^^^^^^^^^^^^^^^ + ", + ) + .status(EXIT_FAILURE) + .run(); } -test! { - name: empty_attribute, - justfile: r" -# This is a doc comment -[] -hello: - @exit 100 -", - stderr: r" -error: Expected identifier, but found ']' - ——▶ justfile:2:2 - │ -2 │ [] - │ ^ -", - status: EXIT_FAILURE, +#[test] +fn empty_attribute() { + Test::new() + .justfile( + " + # This is a doc comment + [] + hello: + @exit 100 + ", + ) + .stderr( + " + error: Expected identifier, but found ']' + ——▶ justfile:2:2 + │ + 2 │ [] + │ ^ + ", + ) + .status(EXIT_FAILURE) + .run(); } -test! { - name: extraneous_attribute_before_comment, - justfile: r" -[no-exit-message] -# This is a doc comment -hello: - @exit 100 -", - stderr: r" -error: Extraneous attribute - ——▶ justfile:1:1 - │ -1 │ [no-exit-message] - │ ^ -", - - status: EXIT_FAILURE, +#[test] +fn extraneous_attribute_before_comment() { + Test::new() + .justfile( + " + [no-exit-message] + # This is a doc comment + hello: + @exit 100 + ", + ) + .stderr( + " + error: Extraneous attribute + ——▶ justfile:1:1 + │ + 1 │ [no-exit-message] + │ ^ + ", + ) + .status(EXIT_FAILURE) + .run(); } -test! { - name: extraneous_attribute_before_empty_line, - justfile: r" -[no-exit-message] - -hello: - @exit 100 -", - stderr: " - error: Extraneous attribute - ——▶ justfile:1:1 - │ - 1 │ [no-exit-message] - │ ^ - ", - status: EXIT_FAILURE, +#[test] +fn extraneous_attribute_before_empty_line() { + Test::new() + .justfile( + " + [no-exit-message] + + hello: + @exit 100 + ", + ) + .stderr( + " + error: Extraneous attribute + ——▶ justfile:1:1 + │ + 1 │ [no-exit-message] + │ ^ + ", + ) + .status(EXIT_FAILURE) + .run(); +} + +#[test] +fn shebang_exit_message_suppressed() { + Test::new() + .justfile( + " + [no-exit-message] + hello: + #!/usr/bin/env bash + echo 'Hello, World!' + exit 100 + ", + ) + .stdout("Hello, World!\n") + .status(100) + .run(); +} + +#[test] +fn no_exit_message() { + Test::new() + .justfile( + " + [no-exit-message] + @hello: + echo 'Hello, World!' + exit 100 + ", + ) + .stdout("Hello, World!\n") + .status(100) + .run(); +} + +#[test] +fn exit_message() { + Test::new() + .justfile( + " + [exit-message] + @hello: + echo 'Hello, World!' + exit 100 + ", + ) + .stdout("Hello, World!\n") + .status(100) + .stderr("error: Recipe `hello` failed on line 4 with exit code 100\n") + .run(); +} + +#[test] +fn recipe_exit_message_setting_suppressed() { + Test::new() + .justfile( + " + set no-exit-message + + # This is a doc comment + hello: + @echo 'Hello, World!' + @exit 100 + ", + ) + .stdout("Hello, World!\n") + .status(100) + .run(); +} + +#[test] +fn shebang_exit_message_setting_suppressed() { + Test::new() + .justfile( + " + set no-exit-message + + hello: + #!/usr/bin/env bash + echo 'Hello, World!' + exit 100 + ", + ) + .stdout("Hello, World!\n") + .status(100) + .run(); +} + +#[test] +fn exit_message_override_no_exit_setting() { + Test::new() + .justfile( + " + set no-exit-message + + [exit-message] + fail: + @exit 100 + ", + ) + .status(100) + .stderr("error: Recipe `fail` failed on line 5 with exit code 100\n") + .run(); } -test! { - name: shebang_exit_message_suppressed, - justfile: r" -[no-exit-message] -hello: - #!/usr/bin/env bash - echo 'Hello, World!' - exit 100 -", - stdout: "Hello, World!\n", - stderr: "", - status: 100, +#[test] +fn exit_message_and_no_exit_message_compile_forbidden() { + Test::new() + .justfile( + " + [exit-message, no-exit-message] + bar: + ", + ) + .stderr( + " + error: Recipe `bar` has both `[exit-message]` and `[no-exit-message]` attributes + ——▶ justfile:2:1 + │ + 2 │ bar: + │ ^^^ + ", + ) + .status(EXIT_FAILURE) + .run(); }