diff --git a/src/alias.rs b/src/alias.rs index 5d95849911..18280546fe 100644 --- a/src/alias.rs +++ b/src/alias.rs @@ -3,7 +3,7 @@ use super::*; /// An alias, e.g. `name := target` #[derive(Debug, PartialEq, Clone, Serialize)] pub(crate) struct Alias<'src, T = Rc>> { - pub(crate) attributes: BTreeSet>, + pub(crate) attributes: AttributeSet<'src>, pub(crate) name: Name<'src>, #[serde( bound(serialize = "T: Keyed<'src>"), @@ -26,7 +26,7 @@ impl<'src> Alias<'src, Name<'src>> { impl Alias<'_> { pub(crate) fn is_private(&self) -> bool { - self.name.lexeme().starts_with('_') || self.attributes.contains(&Attribute::Private) + self.name.lexeme().starts_with('_') || self.attributes.contains(AttributeDiscriminant::Private) } } diff --git a/src/analyzer.rs b/src/analyzer.rs index 0929294c5d..ff5dd18754 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -72,17 +72,21 @@ impl<'run, 'src> Analyzer<'run, 'src> { } => { let mut doc_attr: Option<&str> = None; let mut groups = Vec::new(); + attributes.ensure_valid_attributes( + "Module", + **name, + &[AttributeDiscriminant::Doc, AttributeDiscriminant::Group], + )?; + for attribute in attributes { - if let Attribute::Doc(ref doc) = attribute { - doc_attr = Some(doc.as_ref().map(|s| s.cooked.as_ref()).unwrap_or_default()); - } else if let Attribute::Group(ref group) = attribute { - groups.push(group.cooked.clone()); - } else { - return Err(name.token.error(InvalidAttribute { - item_kind: "Module", - item_name: name.lexeme(), - attribute: attribute.clone(), - })); + match attribute { + Attribute::Doc(ref doc) => { + doc_attr = Some(doc.as_ref().map(|s| s.cooked.as_ref()).unwrap_or_default()); + } + Attribute::Group(ref group) => { + groups.push(group.cooked.clone()); + } + _ => unreachable!(), } } @@ -170,11 +174,9 @@ impl<'run, 'src> Analyzer<'run, 'src> { } for recipe in recipes.values() { - for attribute in &recipe.attributes { - if let Attribute::Script(_) = attribute { - unstable_features.insert(UnstableFeature::ScriptAttribute); - break; - } + if recipe.attributes.contains(AttributeDiscriminant::Script) { + unstable_features.insert(UnstableFeature::ScriptAttribute); + break; } } @@ -284,11 +286,7 @@ impl<'run, 'src> Analyzer<'run, 'src> { } if !recipe.is_script() { - if let Some(attribute) = recipe - .attributes - .iter() - .find(|attribute| matches!(attribute, Attribute::Extension(_))) - { + if let Some(attribute) = recipe.attributes.get(AttributeDiscriminant::Extension) { return Err(recipe.name.error(InvalidAttribute { item_kind: "Recipe", item_name: recipe.name.lexeme(), @@ -301,16 +299,11 @@ impl<'run, 'src> Analyzer<'run, 'src> { } fn analyze_alias(alias: &Alias<'src, Name<'src>>) -> CompileResult<'src> { - for attribute in &alias.attributes { - if *attribute != Attribute::Private { - return Err(alias.name.token.error(InvalidAttribute { - item_kind: "Alias", - item_name: alias.name.lexeme(), - attribute: attribute.clone(), - })); - } - } - + alias.attributes.ensure_valid_attributes( + "Alias", + *alias.name, + &[AttributeDiscriminant::Private], + )?; Ok(()) } diff --git a/src/attribute_set.rs b/src/attribute_set.rs new file mode 100644 index 0000000000..49d10d2e82 --- /dev/null +++ b/src/attribute_set.rs @@ -0,0 +1,60 @@ +use {super::*, std::collections}; + +#[derive(Default, Debug, Clone, PartialEq, Serialize)] +pub(crate) struct AttributeSet<'src>(BTreeSet>); + +impl<'src> AttributeSet<'src> { + pub(crate) fn len(&self) -> usize { + self.0.len() + } + + pub(crate) fn contains(&self, target: AttributeDiscriminant) -> bool { + self.0.iter().any(|attr| attr.discriminant() == target) + } + + pub(crate) fn get(&self, discriminant: AttributeDiscriminant) -> Option<&Attribute<'src>> { + self + .0 + .iter() + .find(|attr| discriminant == attr.discriminant()) + } + + pub(crate) fn iter<'a>(&'a self) -> collections::btree_set::Iter<'a, Attribute<'src>> { + self.0.iter() + } + + pub(crate) fn ensure_valid_attributes( + &self, + item_kind: &'static str, + item_token: Token<'src>, + valid: &[AttributeDiscriminant], + ) -> Result<(), CompileError<'src>> { + for attribute in &self.0 { + let discriminant = attribute.discriminant(); + if !valid.contains(&discriminant) { + return Err(item_token.error(CompileErrorKind::InvalidAttribute { + item_kind, + item_name: item_token.lexeme(), + attribute: attribute.clone(), + })); + } + } + Ok(()) + } +} + +impl<'src> FromIterator> for AttributeSet<'src> { + fn from_iter>>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} + +impl<'src, 'a> IntoIterator for &'a AttributeSet<'src> { + type Item = &'a Attribute<'src>; + + type IntoIter = collections::btree_set::Iter<'a, Attribute<'src>>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} diff --git a/src/item.rs b/src/item.rs index 875951f098..60e2831461 100644 --- a/src/item.rs +++ b/src/item.rs @@ -13,7 +13,7 @@ pub(crate) enum Item<'src> { relative: StringLiteral<'src>, }, Module { - attributes: BTreeSet>, + attributes: AttributeSet<'src>, absolute: Option, doc: Option<&'src str>, name: Name<'src>, diff --git a/src/lib.rs b/src/lib.rs index cda797c7c3..7ce669ea1a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,31 +6,96 @@ pub(crate) use { crate::{ - alias::Alias, analyzer::Analyzer, argument_parser::ArgumentParser, assignment::Assignment, - assignment_resolver::AssignmentResolver, ast::Ast, attribute::Attribute, binding::Binding, - color::Color, color_display::ColorDisplay, command_color::CommandColor, - command_ext::CommandExt, compilation::Compilation, compile_error::CompileError, - compile_error_kind::CompileErrorKind, compiler::Compiler, condition::Condition, - conditional_operator::ConditionalOperator, config::Config, config_error::ConfigError, - constants::constants, count::Count, delimiter::Delimiter, dependency::Dependency, - dump_format::DumpFormat, enclosure::Enclosure, error::Error, evaluator::Evaluator, - execution_context::ExecutionContext, executor::Executor, expression::Expression, - fragment::Fragment, function::Function, interpreter::Interpreter, - interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item, - justfile::Justfile, keyed::Keyed, keyword::Keyword, lexer::Lexer, line::Line, list::List, - load_dotenv::load_dotenv, loader::Loader, module_path::ModulePath, name::Name, - namepath::Namepath, ordinal::Ordinal, output::output, output_error::OutputError, - parameter::Parameter, parameter_kind::ParameterKind, parser::Parser, platform::Platform, - platform_interface::PlatformInterface, position::Position, positional::Positional, ran::Ran, - range_ext::RangeExt, recipe::Recipe, recipe_resolver::RecipeResolver, - recipe_signature::RecipeSignature, scope::Scope, search::Search, search_config::SearchConfig, - search_error::SearchError, set::Set, setting::Setting, settings::Settings, shebang::Shebang, - show_whitespace::ShowWhitespace, source::Source, string_delimiter::StringDelimiter, - 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_feature::UnstableFeature, use_color::UseColor, variables::Variables, - verbosity::Verbosity, warning::Warning, + alias::Alias, + analyzer::Analyzer, + argument_parser::ArgumentParser, + assignment::Assignment, + assignment_resolver::AssignmentResolver, + ast::Ast, + attribute::{Attribute, AttributeDiscriminant}, + attribute_set::AttributeSet, + binding::Binding, + color::Color, + color_display::ColorDisplay, + command_color::CommandColor, + command_ext::CommandExt, + compilation::Compilation, + compile_error::CompileError, + compile_error_kind::CompileErrorKind, + compiler::Compiler, + condition::Condition, + conditional_operator::ConditionalOperator, + config::Config, + config_error::ConfigError, + constants::constants, + count::Count, + delimiter::Delimiter, + dependency::Dependency, + dump_format::DumpFormat, + enclosure::Enclosure, + error::Error, + evaluator::Evaluator, + execution_context::ExecutionContext, + executor::Executor, + expression::Expression, + fragment::Fragment, + function::Function, + interpreter::Interpreter, + interrupt_guard::InterruptGuard, + interrupt_handler::InterruptHandler, + item::Item, + justfile::Justfile, + keyed::Keyed, + keyword::Keyword, + lexer::Lexer, + line::Line, + list::List, + load_dotenv::load_dotenv, + loader::Loader, + module_path::ModulePath, + name::Name, + namepath::Namepath, + ordinal::Ordinal, + output::output, + output_error::OutputError, + parameter::Parameter, + parameter_kind::ParameterKind, + parser::Parser, + platform::Platform, + platform_interface::PlatformInterface, + position::Position, + positional::Positional, + ran::Ran, + range_ext::RangeExt, + recipe::Recipe, + recipe_resolver::RecipeResolver, + recipe_signature::RecipeSignature, + scope::Scope, + search::Search, + search_config::SearchConfig, + search_error::SearchError, + set::Set, + setting::Setting, + settings::Settings, + shebang::Shebang, + show_whitespace::ShowWhitespace, + source::Source, + string_delimiter::StringDelimiter, + 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_feature::UnstableFeature, + use_color::UseColor, + variables::Variables, + verbosity::Verbosity, + warning::Warning, }, camino::Utf8Path, clap::ValueEnum, @@ -120,6 +185,7 @@ mod assignment; mod assignment_resolver; mod ast; mod attribute; +mod attribute_set; mod binding; mod color; mod color_display; diff --git a/src/parser.rs b/src/parser.rs index 32a5575159..5903e5b492 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -462,7 +462,7 @@ impl<'run, 'src> Parser<'run, 'src> { /// Parse an alias, e.g `alias name := target` fn parse_alias( &mut self, - attributes: BTreeSet>, + attributes: AttributeSet<'src>, ) -> CompileResult<'src, Alias<'src, Name<'src>>> { self.presume_keyword(Keyword::Alias)?; let name = self.parse_name()?; @@ -480,24 +480,16 @@ impl<'run, 'src> Parser<'run, 'src> { fn parse_assignment( &mut self, export: bool, - attributes: BTreeSet>, + attributes: AttributeSet<'src>, ) -> CompileResult<'src, Assignment<'src>> { let name = self.parse_name()?; self.presume(ColonEquals)?; let value = self.parse_expression()?; self.expect_eol()?; - let private = attributes.contains(&Attribute::Private); + let private = attributes.contains(AttributeDiscriminant::Private); - for attribute in attributes { - if attribute != Attribute::Private { - return Err(name.error(CompileErrorKind::InvalidAttribute { - item_kind: "Assignment", - item_name: name.lexeme(), - attribute, - })); - } - } + attributes.ensure_valid_attributes("Assignment", *name, &[AttributeDiscriminant::Private])?; Ok(Assignment { constant: false, @@ -863,7 +855,7 @@ impl<'run, 'src> Parser<'run, 'src> { &mut self, doc: Option<&'src str>, quiet: bool, - attributes: BTreeSet>, + attributes: AttributeSet<'src>, ) -> CompileResult<'src, UnresolvedRecipe<'src>> { let name = self.parse_name()?; @@ -924,9 +916,7 @@ impl<'run, 'src> Parser<'run, 'src> { let body = self.parse_body()?; let shebang = body.first().map_or(false, Line::is_shebang); - let script = attributes - .iter() - .any(|attribute| matches!(attribute, Attribute::Script(_))); + let script = attributes.contains(AttributeDiscriminant::Script); if shebang && script { return Err(name.error(CompileErrorKind::ShebangAndScriptAttribute { @@ -934,23 +924,18 @@ impl<'run, 'src> Parser<'run, 'src> { })); } - let working_directory = attributes - .iter() - .any(|attribute| matches!(attribute, Attribute::WorkingDirectory(_))); - - if working_directory { - for attribute in &attributes { - if let Attribute::NoCd = attribute { - return Err( - name.error(CompileErrorKind::NoCdAndWorkingDirectoryAttribute { - recipe: name.lexeme(), - }), - ); - } - } + let working_directory = attributes.contains(AttributeDiscriminant::WorkingDirectory); + + if working_directory && attributes.contains(AttributeDiscriminant::NoCd) { + return Err( + name.error(CompileErrorKind::NoCdAndWorkingDirectoryAttribute { + recipe: name.lexeme(), + }), + ); } - let private = name.lexeme().starts_with('_') || attributes.contains(&Attribute::Private); + let private = + name.lexeme().starts_with('_') || attributes.contains(AttributeDiscriminant::Private); let mut doc = doc.map(ToOwned::to_owned); @@ -1138,9 +1123,7 @@ impl<'run, 'src> Parser<'run, 'src> { } /// Item attributes, i.e., `[macos]` or `[confirm: "warning!"]` - fn parse_attributes( - &mut self, - ) -> CompileResult<'src, Option<(Token<'src>, BTreeSet>)>> { + fn parse_attributes(&mut self) -> CompileResult<'src, Option<(Token<'src>, AttributeSet<'src>)>> { let mut attributes = BTreeMap::new(); let mut discriminants = BTreeMap::new(); diff --git a/src/recipe.rs b/src/recipe.rs index ac7fdd6596..d53a44bb40 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -19,7 +19,7 @@ fn error_from_signal(recipe: &str, line_number: Option, exit_status: Exit /// A recipe, e.g. `foo: bar baz` #[derive(PartialEq, Debug, Clone, Serialize)] pub(crate) struct Recipe<'src, D = Dependency<'src>> { - pub(crate) attributes: BTreeSet>, + pub(crate) attributes: AttributeSet<'src>, pub(crate) body: Vec>, pub(crate) dependencies: Vec, pub(crate) doc: Option, @@ -66,20 +66,20 @@ impl<'src, D> Recipe<'src, D> { } pub(crate) fn confirm(&self) -> RunResult<'src, bool> { - for attribute in &self.attributes { - if let Attribute::Confirm(prompt) = attribute { - if let Some(prompt) = prompt { - eprint!("{} ", prompt.cooked); - } else { - eprint!("Run recipe `{}`? ", self.name); - } - let mut line = String::new(); - std::io::stdin() - .read_line(&mut line) - .map_err(|io_error| Error::GetConfirmation { io_error })?; - let line = line.trim().to_lowercase(); - return Ok(line == "y" || line == "yes"); + if let Some(Attribute::Confirm(ref prompt)) = + self.attributes.get(AttributeDiscriminant::Confirm) + { + if let Some(prompt) = prompt { + eprint!("{} ", prompt.cooked); + } else { + eprint!("Run recipe `{}`? ", self.name); } + let mut line = String::new(); + std::io::stdin() + .read_line(&mut line) + .map_err(|io_error| Error::GetConfirmation { io_error })?; + let line = line.trim().to_lowercase(); + return Ok(line == "y" || line == "yes"); } Ok(true) } @@ -97,7 +97,7 @@ impl<'src, D> Recipe<'src, D> { } pub(crate) fn is_public(&self) -> bool { - !self.private && !self.attributes.contains(&Attribute::Private) + !self.private && !self.attributes.contains(AttributeDiscriminant::Private) } pub(crate) fn is_script(&self) -> bool { @@ -105,19 +105,22 @@ impl<'src, D> Recipe<'src, D> { } pub(crate) fn takes_positional_arguments(&self, settings: &Settings) -> bool { - settings.positional_arguments || self.attributes.contains(&Attribute::PositionalArguments) + settings.positional_arguments + || self + .attributes + .contains(AttributeDiscriminant::PositionalArguments) } pub(crate) fn change_directory(&self) -> bool { - !self.attributes.contains(&Attribute::NoCd) + !self.attributes.contains(AttributeDiscriminant::NoCd) } pub(crate) fn enabled(&self) -> bool { - let linux = self.attributes.contains(&Attribute::Linux); - let macos = self.attributes.contains(&Attribute::Macos); - let openbsd = self.attributes.contains(&Attribute::Openbsd); - let unix = self.attributes.contains(&Attribute::Unix); - let windows = self.attributes.contains(&Attribute::Windows); + 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)) @@ -129,7 +132,9 @@ impl<'src, D> Recipe<'src, D> { } fn print_exit_message(&self) -> bool { - !self.attributes.contains(&Attribute::NoExitMessage) + !self + .attributes + .contains(AttributeDiscriminant::NoExitMessage) } fn working_directory<'a>(&'a self, context: &'a ExecutionContext) -> Option { @@ -149,7 +154,7 @@ impl<'src, D> Recipe<'src, D> { } fn no_quiet(&self) -> bool { - self.attributes.contains(&Attribute::NoQuiet) + self.attributes.contains(AttributeDiscriminant::NoQuiet) } pub(crate) fn run<'run>( @@ -351,10 +356,8 @@ impl<'src, D> Recipe<'src, D> { return Ok(()); } - let executor = if let Some(Attribute::Script(interpreter)) = self - .attributes - .iter() - .find(|attribute| matches!(attribute, Attribute::Script(_))) + let executor = if let Some(Attribute::Script(interpreter)) = + self.attributes.get(AttributeDiscriminant::Script) { Executor::Command( interpreter