diff --git a/src/attribute.rs b/src/attribute.rs index 4ec813f076..2206f5bff6 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -13,17 +13,17 @@ pub(crate) enum Attribute<'src> { Doc(Option>), Extension(StringLiteral<'src>), Group(StringLiteral<'src>), - Linux, - Macos, + Linux { enabled: bool }, + Macos { enabled: bool }, NoCd, NoExitMessage, NoQuiet, - Openbsd, + Openbsd { enabled: bool }, PositionalArguments, Private, Script(Option>), - Unix, - Windows, + Unix { enabled: bool }, + Windows { enabled: bool }, WorkingDirectory(StringLiteral<'src>), } @@ -51,6 +51,7 @@ impl<'src> Attribute<'src> { pub(crate) fn new( name: Name<'src>, arguments: Vec>, + enabled: bool, ) -> CompileResult<'src, Self> { let discriminant = name .lexeme() @@ -75,29 +76,38 @@ impl<'src> Attribute<'src> { ); } - Ok(match discriminant { - AttributeDiscriminant::Confirm => Self::Confirm(arguments.into_iter().next()), - AttributeDiscriminant::Doc => Self::Doc(arguments.into_iter().next()), - AttributeDiscriminant::Extension => Self::Extension(arguments.into_iter().next().unwrap()), - AttributeDiscriminant::Group => Self::Group(arguments.into_iter().next().unwrap()), - AttributeDiscriminant::Linux => Self::Linux, - AttributeDiscriminant::Macos => Self::Macos, - AttributeDiscriminant::NoCd => Self::NoCd, - AttributeDiscriminant::NoExitMessage => Self::NoExitMessage, - AttributeDiscriminant::NoQuiet => Self::NoQuiet, - AttributeDiscriminant::Openbsd => Self::Openbsd, - AttributeDiscriminant::PositionalArguments => Self::PositionalArguments, - AttributeDiscriminant::Private => Self::Private, - AttributeDiscriminant::Script => Self::Script({ + Ok(match (enabled, discriminant) { + (enabled, AttributeDiscriminant::Linux) => Self::Linux { enabled }, + (enabled, AttributeDiscriminant::Macos) => Self::Macos { enabled }, + (enabled, AttributeDiscriminant::Unix) => Self::Unix { enabled }, + (enabled, AttributeDiscriminant::Windows) => Self::Windows { enabled }, + (enabled, AttributeDiscriminant::Openbsd) => Self::Openbsd { enabled }, + + (false, _attr) => { + return Err(name.error(CompileErrorKind::InvalidInvertedAttribute { + attr_name: name.lexeme(), + })) + } + + (true, AttributeDiscriminant::Confirm) => Self::Confirm(arguments.into_iter().next()), + (true, AttributeDiscriminant::Doc) => Self::Doc(arguments.into_iter().next()), + (true, AttributeDiscriminant::Extension) => { + Self::Extension(arguments.into_iter().next().unwrap()) + } + (true, AttributeDiscriminant::Group) => Self::Group(arguments.into_iter().next().unwrap()), + (true, AttributeDiscriminant::NoCd) => Self::NoCd, + (true, AttributeDiscriminant::NoExitMessage) => Self::NoExitMessage, + (true, AttributeDiscriminant::NoQuiet) => Self::NoQuiet, + (true, AttributeDiscriminant::PositionalArguments) => Self::PositionalArguments, + (true, AttributeDiscriminant::Private) => Self::Private, + (true, AttributeDiscriminant::Script) => Self::Script({ let mut arguments = arguments.into_iter(); arguments.next().map(|command| Interpreter { command, arguments: arguments.collect(), }) }), - AttributeDiscriminant::Unix => Self::Unix, - AttributeDiscriminant::Windows => Self::Windows, - AttributeDiscriminant::WorkingDirectory => { + (true, AttributeDiscriminant::WorkingDirectory) => { Self::WorkingDirectory(arguments.into_iter().next().unwrap()) } }) @@ -118,28 +128,34 @@ impl<'src> Attribute<'src> { impl Display for Attribute<'_> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{}", self.name())?; + let name = self.name(); match self { Self::Confirm(Some(argument)) | Self::Doc(Some(argument)) | Self::Extension(argument) | Self::Group(argument) - | Self::WorkingDirectory(argument) => write!(f, "({argument})")?, - Self::Script(Some(shell)) => write!(f, "({shell})")?, + | Self::WorkingDirectory(argument) => write!(f, "{name}({argument})")?, + Self::Script(Some(shell)) => write!(f, "{name}({shell})")?, + Self::Linux { enabled } + | Self::Macos { enabled } + | Self::Unix { enabled } + | Self::Openbsd { enabled } + | Self::Windows { enabled } => { + if *enabled { + write!(f, "{name}")?; + } else { + write!(f, "not({name})")?; + } + } Self::Confirm(None) | Self::Doc(None) - | Self::Linux - | Self::Macos | Self::NoCd | Self::NoExitMessage | Self::NoQuiet - | Self::Openbsd | Self::PositionalArguments | Self::Private - | Self::Script(None) - | Self::Unix - | Self::Windows => {} + | Self::Script(None) => write!(f, "{name}")?, } Ok(()) diff --git a/src/attribute_set.rs b/src/attribute_set.rs index a40b001d1a..837e6e168d 100644 --- a/src/attribute_set.rs +++ b/src/attribute_set.rs @@ -12,6 +12,17 @@ impl<'src> AttributeSet<'src> { self.0.iter().any(|attr| attr.discriminant() == target) } + pub(crate) fn contains_invertible(&self, target: AttributeDiscriminant) -> Option { + self.get(target).map(|attr| match attr { + Attribute::Linux { enabled } + | Attribute::Macos { enabled } + | Attribute::Openbsd { enabled } + | Attribute::Unix { enabled } + | Attribute::Windows { enabled } => *enabled, + _ => panic!("contains_invertible called with non-invertible attribute"), + }) + } + pub(crate) fn get(&self, discriminant: AttributeDiscriminant) -> Option<&Attribute<'src>> { self .0 diff --git a/src/compile_error.rs b/src/compile_error.rs index ce53c12f70..133c732c74 100644 --- a/src/compile_error.rs +++ b/src/compile_error.rs @@ -186,6 +186,9 @@ impl Display for CompileError<'_> { _ => character.escape_default().collect(), } ), + InvalidInvertedAttribute { attr_name } => { + write!(f, "{attr_name} cannot be inverted with `not()`") + } MismatchedClosingDelimiter { open, open_line, diff --git a/src/compile_error_kind.rs b/src/compile_error_kind.rs index 09e2eb337c..fd4b64f614 100644 --- a/src/compile_error_kind.rs +++ b/src/compile_error_kind.rs @@ -79,6 +79,9 @@ pub(crate) enum CompileErrorKind<'src> { InvalidEscapeSequence { character: char, }, + InvalidInvertedAttribute { + attr_name: &'src str, + }, MismatchedClosingDelimiter { close: Delimiter, open: Delimiter, diff --git a/src/parser.rs b/src/parser.rs index eea3e6c78e..c14a06cb5e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1160,7 +1160,17 @@ impl<'run, 'src> Parser<'run, 'src> { token.get_or_insert(bracket); loop { - let name = self.parse_name()?; + let (name, inverted) = { + let name = self.parse_name()?; + if name.lexeme() == "not" { + self.expect(ParenL)?; + let name = self.parse_name()?; + self.expect(ParenR)?; + (name, true) + } else { + (name, false) + } + }; let mut arguments = Vec::new(); @@ -1177,7 +1187,7 @@ impl<'run, 'src> Parser<'run, 'src> { self.expect(ParenR)?; } - let attribute = Attribute::new(name, arguments)?; + let attribute = Attribute::new(name, arguments, !inverted)?; let first = attributes.get(&attribute).or_else(|| { if attribute.repeatable() { @@ -2693,6 +2703,17 @@ mod tests { kind: UnknownAttribute { attribute: "unknown" }, } + error! { + name: invalid_invertable_attribute, + input: "[not(private)]\nsome_recipe:\n @exit 3", + offset: 5, + line: 0, + column: 5, + width: 7, + kind: InvalidInvertedAttribute { attr_name: "private" }, + + } + error! { name: set_unknown, input: "set shall := []", diff --git a/src/recipe.rs b/src/recipe.rs index d53a44bb40..e4335ca2e5 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -16,6 +16,76 @@ fn error_from_signal(recipe: &str, line_number: Option, exit_status: Exit } } +#[derive(Debug)] +struct SystemMap { + windows: bool, + macos: bool, + linux: bool, + openbsd: bool, + unix: bool, +} + +#[derive(Debug, Clone, Copy)] +enum System { + Windows, + MacOS, + Linux, + OpenBSD, + Unix, +} + +impl System { + fn current() -> System { + use System::*; + if cfg!(target_os = "linux") { + return Linux; + } + if cfg!(target_os = "openbsd") { + return OpenBSD; + } + if cfg!(target_os = "macos") { + return MacOS; + } + if cfg!(target_os = "windows") || cfg!(windows) { + return Windows; + } + if cfg!(unix) { + return Unix; + } + panic!("No recognized system"); + } + + fn enabled(self, enabled: SystemMap, disabled: SystemMap) -> bool { + match self { + System::Windows => { + !disabled.windows + && (enabled.windows + || !(enabled.macos || enabled.linux || enabled.openbsd || enabled.unix)) + } + System::MacOS => { + !disabled.macos + && ((enabled.macos || enabled.unix) + || !(enabled.windows || enabled.linux || enabled.openbsd)) + } + System::Linux => { + !disabled.linux + && ((enabled.linux || enabled.unix) + || !(enabled.windows || enabled.macos || enabled.openbsd)) + } + System::OpenBSD => { + !disabled.openbsd + && ((enabled.openbsd || enabled.unix) + || !(enabled.windows || enabled.macos || enabled.linux)) + } + System::Unix => { + !disabled.unix + && (enabled.unix + || !(enabled.windows || enabled.macos || enabled.linux || enabled.openbsd)) + } + } + } +} + /// A recipe, e.g. `foo: bar baz` #[derive(PartialEq, Debug, Clone, Serialize)] pub(crate) struct Recipe<'src, D = Dependency<'src>> { @@ -116,19 +186,48 @@ impl<'src, D> Recipe<'src, D> { } pub(crate) fn enabled(&self) -> bool { - let linux = self.attributes.contains(AttributeDiscriminant::Linux); - let macos = self.attributes.contains(AttributeDiscriminant::Macos); - let openbsd = self.attributes.contains(AttributeDiscriminant::Openbsd); - let unix = self.attributes.contains(AttributeDiscriminant::Unix); - let windows = self.attributes.contains(AttributeDiscriminant::Windows); - - (!windows && !linux && !macos && !openbsd && !unix) - || (cfg!(target_os = "linux") && (linux || unix)) - || (cfg!(target_os = "macos") && (macos || unix)) - || (cfg!(target_os = "openbsd") && (openbsd || unix)) - || (cfg!(target_os = "windows") && windows) - || (cfg!(unix) && unix) - || (cfg!(windows) && windows) + use std::ops::Not; + + let linux = self + .attributes + .contains_invertible(AttributeDiscriminant::Linux); + let macos = self + .attributes + .contains_invertible(AttributeDiscriminant::Macos); + let openbsd = self + .attributes + .contains_invertible(AttributeDiscriminant::Openbsd); + let unix = self + .attributes + .contains_invertible(AttributeDiscriminant::Unix); + let windows = self + .attributes + .contains_invertible(AttributeDiscriminant::Windows); + + if [linux, macos, openbsd, unix, windows] + .into_iter() + .all(|x| x.is_none()) + { + return true; + } + + let enabled = SystemMap { + windows: windows.unwrap_or(false), + macos: macos.unwrap_or(false), + linux: linux.unwrap_or(false), + openbsd: openbsd.unwrap_or(false), + unix: unix.unwrap_or(false), + }; + + let disabled = SystemMap { + linux: linux.is_some_and(bool::not), + macos: macos.is_some_and(bool::not), + openbsd: openbsd.is_some_and(bool::not), + unix: unix.is_some_and(bool::not), + windows: windows.is_some_and(bool::not), + }; + + System::current().enabled(enabled, disabled) } fn print_exit_message(&self) -> bool { diff --git a/tests/attributes.rs b/tests/attributes.rs index 80393f1aa2..4d2a4cf761 100644 --- a/tests/attributes.rs +++ b/tests/attributes.rs @@ -44,6 +44,30 @@ fn duplicate_attributes_are_disallowed() { .run(); } +#[test] +fn conflicting_invertible_attributes_are_disallowed() { + Test::new() + .justfile( + " + [windows] + [not(windows)] + foo: + echo bar + ", + ) + .stderr( + " + error: Recipe attribute `windows` first used on line 1 is duplicated on line 2 + ——▶ justfile:2:6 + │ + 2 │ [not(windows)] + │ ^^^^^^^ + ", + ) + .status(1) + .run(); +} + #[test] fn multiple_attributes_one_line() { Test::new() @@ -254,3 +278,27 @@ fn duplicate_non_repeatable_attributes_are_forbidden() { .status(EXIT_FAILURE) .run(); } + +#[test] +fn invertible_attributes() { + let test = Test::new().justfile( + " + [not(windows)] + non-windows-recipe: + echo 'non-windows' + + [windows] + windows-recipe: + echo 'windows' + ", + ); + + #[cfg(windows)] + test.stdout("windows\n").stderr("echo 'windows'\n").run(); + + #[cfg(not(windows))] + test + .stdout("non-windows\n") + .stderr("echo 'non-windows'\n") + .run(); +}