diff --git a/src/attribute.rs b/src/attribute.rs index 4ec813f076..041af8b0de 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 { inverted: bool }, + Macos { inverted: bool }, NoCd, NoExitMessage, NoQuiet, - Openbsd, + Openbsd { inverted: bool }, PositionalArguments, Private, Script(Option>), - Unix, - Windows, + Unix { inverted: bool }, + Windows { inverted: bool }, WorkingDirectory(StringLiteral<'src>), } @@ -51,6 +51,7 @@ impl<'src> Attribute<'src> { pub(crate) fn new( name: Name<'src>, arguments: Vec>, + inverted: 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 (inverted, discriminant) { + (inverted, AttributeDiscriminant::Linux) => Self::Linux { inverted }, + (inverted, AttributeDiscriminant::Macos) => Self::Macos { inverted }, + (inverted, AttributeDiscriminant::Unix) => Self::Unix { inverted }, + (inverted, AttributeDiscriminant::Windows) => Self::Windows { inverted }, + (inverted, AttributeDiscriminant::Openbsd) => Self::Openbsd { inverted }, + + (true, _attr) => { + return Err(name.error(CompileErrorKind::InvalidInvertedAttribute { + attr_name: name.lexeme(), + })) + } + + (false, AttributeDiscriminant::Confirm) => Self::Confirm(arguments.into_iter().next()), + (false, AttributeDiscriminant::Doc) => Self::Doc(arguments.into_iter().next()), + (false, AttributeDiscriminant::Extension) => { + Self::Extension(arguments.into_iter().next().unwrap()) + } + (false, AttributeDiscriminant::Group) => Self::Group(arguments.into_iter().next().unwrap()), + (false, AttributeDiscriminant::NoCd) => Self::NoCd, + (false, AttributeDiscriminant::NoExitMessage) => Self::NoExitMessage, + (false, AttributeDiscriminant::NoQuiet) => Self::NoQuiet, + (false, AttributeDiscriminant::PositionalArguments) => Self::PositionalArguments, + (false, AttributeDiscriminant::Private) => Self::Private, + (false, 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 => { + (false, 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 { inverted } + | Self::Macos { inverted } + | Self::Unix { inverted } + | Self::Openbsd { inverted } + | Self::Windows { inverted } => { + if *inverted { + write!(f, "not({name})")?; + } else { + write!(f, "{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 49d10d2e82..96c1c049a4 100644 --- a/src/attribute_set.rs +++ b/src/attribute_set.rs @@ -1,5 +1,11 @@ use {super::*, std::collections}; +#[derive(Debug, Copy, Clone)] +pub(crate) enum InvertedStatus { + Normal, + Inverted, +} + #[derive(Default, Debug, Clone, PartialEq, Serialize)] pub(crate) struct AttributeSet<'src>(BTreeSet>); @@ -12,6 +18,28 @@ impl<'src> AttributeSet<'src> { self.0.iter().any(|attr| attr.discriminant() == target) } + pub(crate) fn contains_invertible( + &self, + target: AttributeDiscriminant, + ) -> Option { + self.get(target).and_then(|attr| { + Some(match attr { + Attribute::Linux { inverted } + | Attribute::Macos { inverted } + | Attribute::Openbsd { inverted } + | Attribute::Unix { inverted } + | Attribute::Windows { inverted } => { + if *inverted { + InvertedStatus::Inverted + } else { + InvertedStatus::Normal + } + } + _ => return None, + }) + }) + } + 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 2258c13b3b..a43b33699f 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1135,7 +1135,18 @@ impl<'run, 'src> Parser<'run, 'src> { token.get_or_insert(bracket); loop { - let name = self.parse_name()?; + let (name, inverted) = { + let mut i = false; + let mut n = self.parse_name()?; + if n.lexeme() == "not" { + i = true; + self.expect(ParenL)?; + n = self.parse_name()?; + self.expect(ParenR)?; + } + + (n, i) + }; let mut arguments = Vec::new(); @@ -1152,7 +1163,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() { @@ -2668,6 +2679,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..0e9631a386 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -116,19 +116,81 @@ 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 attribute_set::InvertedStatus; + + struct Systems { + linux: bool, + macos: bool, + openbsd: bool, + unix: bool, + windows: bool, + } + + 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 systems = Systems { + linux: matches!(linux, Some(InvertedStatus::Normal)), + macos: matches!(macos, Some(InvertedStatus::Normal)), + openbsd: matches!(openbsd, Some(InvertedStatus::Normal)), + unix: matches!(unix, Some(InvertedStatus::Normal)), + windows: matches!(windows, Some(InvertedStatus::Normal)), + }; + + let disabled = Systems { + linux: matches!(linux, Some(InvertedStatus::Inverted)), + macos: matches!(macos, Some(InvertedStatus::Inverted)), + openbsd: matches!(openbsd, Some(InvertedStatus::Inverted)), + unix: matches!(unix, Some(InvertedStatus::Inverted)), + windows: matches!(windows, Some(InvertedStatus::Inverted)), + }; + + if cfg!(target_os = "linux") { + return !(disabled.linux || disabled.unix) + && ((systems.linux || systems.unix) + || (!systems.openbsd && !systems.windows && !systems.macos)); + } + + if cfg!(target_os = "openbsd") { + return !disabled.openbsd + && (systems.openbsd || (!systems.windows && !systems.macos && !systems.linux)); + } + + if cfg!(target_os = "windows") || cfg!(windows) { + return !disabled.windows + && (systems.windows + || (!systems.openbsd && !systems.unix && !systems.macos && !systems.linux)); + } + + if cfg!(target_os = "macos") { + return !disabled.macos + && (systems.macos || (!systems.openbsd && !systems.windows && !systems.linux)); + } + + if cfg!(unix) { + return !(disabled.unix) && (systems.unix || !systems.windows); + } + false } fn print_exit_message(&self) -> bool { diff --git a/tests/attributes.rs b/tests/attributes.rs index 80393f1aa2..189e94f2a7 100644 --- a/tests/attributes.rs +++ b/tests/attributes.rs @@ -254,3 +254,21 @@ fn duplicate_non_repeatable_attributes_are_forbidden() { .status(EXIT_FAILURE) .run(); } + +#[test] +fn invertible_attributes() { + Test::new() + .justfile( + " + [not(windows)] + non-windows-recipe: + exit 0 + + [windows] + windows-recipe: + exit 0 + ", + ) + .stderr("exit 0\n") + .run(); +}