Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1531,6 +1531,8 @@ Available recipes:
test
```

The value of `[doc]` may be a const expression<sup>master</sup>.

### Groups

Recipes and modules may be annotated with one or more group names:
Expand Down Expand Up @@ -4689,7 +4691,7 @@ change their behavior.
| `[confirm]`<sup>1.17.0</sup> | recipe | Require confirmation prior to executing recipe. |
| `[continue(SIGNALS)]`<sup>1.54.0</sup> | recipe | Continue execution normally if a command is interrupted by any of `SIGNALS` and exits successfully. Defaults to `SIGINT`. |
| `[default]`<sup>1.43.0</sup> | recipe | Use recipe as module's default recipe. |
| `[doc(DOC)]`<sup>1.27.0</sup> | module, recipe | Set recipe or module's [documentation comment](#documentation-comments) to `DOC`. |
| `[doc(DOC)]`<sup>1.27.0</sup> | module, recipe | Set recipe or module's [documentation comment](#documentation-comments) to `DOC`. May be a const expression<sup>master</sup>. |
| `[dragonfly]`<sup>1.47.0</sup> | any<sup>master</sup> | Enable item on DragonFly BSD. |
| `[env(NAME, VALUE)]` <sup>1.47.0</sup> | recipe | Set environment variable `NAME` to `VALUE` for recipe. `NAME` and `VALUE` may be expressions<sup>1.51.0</sup>. |
| `[extension(EXT)]`<sup>1.32.0</sup> | recipe | Set shebang recipe script's file extension to `EXT`. `EXT` should include a period if one is desired. |
Expand Down
47 changes: 32 additions & 15 deletions src/analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ impl<'run, 'src> Analyzer<'run, 'src> {
let mut definitions = HashMap::new();
let mut imports = HashSet::new();
let mut list_features = Vec::new();
let mut module_docs: Vec<(&str, &Expression)> = Vec::new();
let mut unstable_features = BTreeSet::new();

let mut stack = Vec::new();
Expand Down Expand Up @@ -104,6 +105,9 @@ impl<'run, 'src> Analyzer<'run, 'src> {
attributes.private(),
absolute,
)?);
if let Some(Attribute::Doc(Some(expression))) = attributes.get(AttributeKind::Doc) {
module_docs.push((name.lexeme(), expression));
}
} else if *optional {
absent_modules.insert(name.lexeme().to_string());
}
Expand Down Expand Up @@ -218,23 +222,27 @@ impl<'run, 'src> Analyzer<'run, 'src> {
.iter()
.flat_map(|recipe| &recipe.attributes)
.flat_map(|attribute| {
let (help, pattern) = if let Attribute::Arg {
help_property,
pattern_property,
..
} = attribute
{
(help_property.as_ref(), pattern_property.as_ref())
} else {
(None, None)
};

help
.into_iter()
.chain(pattern)
.map(|(_, expression)| expression)
let mut expressions = Vec::new();
match attribute {
Attribute::Arg {
help_property,
pattern_property,
..
} => {
if let Some((_, expression)) = help_property {
expressions.push(expression);
}
if let Some((_, expression)) = pattern_property {
expressions.push(expression);
}
}
Attribute::Doc(Some(expression)) => expressions.push(expression),
_ => {}
}
expressions
}),
)
.chain(module_docs.iter().map(|(_, expression)| *expression))
.flat_map(|expression| expression.references())
.filter_map(|reference| {
if let Reference::Variable(variable) = reference {
Expand Down Expand Up @@ -266,6 +274,15 @@ impl<'run, 'src> Analyzer<'run, 'src> {
}
}

for (name, expression) in module_docs {
let value = evaluator.evaluate_value_const(expression)?;
self.modules.get_mut(name).unwrap().doc = if value.is_empty() {
None
} else {
Some(value.join())
};
}

let mut deduplicated_recipes = Table::<'src, UnresolvedRecipe<'src>>::default();
for recipe in self.recipes {
Self::define(
Expand Down
22 changes: 16 additions & 6 deletions src/attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub(crate) enum Attribute<'src> {
Confirm(Option<Expression<'src>>),
Continue(BTreeSet<Signal>),
Default,
Doc(Option<StringLiteral<'src>>),
Doc(Option<Expression<'src>>),
Dragonfly,
Env(Expression<'src>, Expression<'src>),
ExitMessage,
Expand All @@ -66,7 +66,10 @@ pub(crate) enum Attribute<'src> {

impl AttributeKind {
fn accepts_expressions(self) -> bool {
matches!(self, Self::Confirm | Self::Env | Self::WorkingDirectory)
matches!(
self,
Self::Confirm | Self::Doc | Self::Env | Self::WorkingDirectory
)
}

pub(crate) fn accepts_keyword_arguments(self) -> bool {
Expand Down Expand Up @@ -173,6 +176,9 @@ impl<'src> Attribute<'src> {
AttributeKind::Confirm => Ok(Self::Confirm(
arguments.into_iter().next().map(|(_, expr)| expr),
)),
AttributeKind::Doc => Ok(Self::Doc(
arguments.into_iter().next().map(|(_, expr)| expr),
)),
AttributeKind::Env => {
let mut arguments = arguments.into_iter();
let (_, key) = arguments.next().unwrap();
Expand Down Expand Up @@ -330,11 +336,13 @@ impl<'src> Attribute<'src> {
})
.collect::<CompileResult<BTreeSet<Signal>>>()?,
),
AttributeKind::Confirm | AttributeKind::Env | AttributeKind::WorkingDirectory => {
AttributeKind::Confirm
| AttributeKind::Doc
| AttributeKind::Env
| AttributeKind::WorkingDirectory => {
unreachable!()
}
AttributeKind::Default => Self::Default,
AttributeKind::Doc => Self::Doc(arguments.into_iter().next()),
AttributeKind::Dragonfly => Self::Dragonfly,
AttributeKind::ExitMessage => Self::ExitMessage,
AttributeKind::Extension => Self::Extension(arguments.into_iter().next().unwrap()),
Expand Down Expand Up @@ -512,10 +520,12 @@ impl Display for Attribute<'_> {
write!(f, "({})", arguments.join(", "))?;
}
}
Self::Confirm(Some(argument)) | Self::WorkingDirectory(argument) => {
Self::Confirm(Some(argument))
| Self::Doc(Some(argument))
| Self::WorkingDirectory(argument) => {
write!(f, "({argument})")?;
}
Self::Doc(Some(argument)) | Self::Extension(argument) | Self::Group(argument) => {
Self::Extension(argument) | Self::Group(argument) => {
write!(f, "({argument})")?;
}
Self::Continue(signals) => {
Expand Down
6 changes: 2 additions & 4 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,10 +418,8 @@ impl<'run, 'src> Parser<'run, 'src> {
}

fn take_doc_comment(&mut self, attributes: &AttributeSet<'src>) -> Option<String> {
for attribute in attributes {
if let Attribute::Doc(doc) = attribute {
return doc.as_ref().map(|doc| doc.cooked.clone());
}
if attributes.contains(AttributeKind::Doc) {
return None;
}

let mut items = self.items.iter().rev();
Expand Down
4 changes: 4 additions & 0 deletions src/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ impl<'key, V: Keyed<'key>> Table<'key, V> {
self.map.get(key)
}

pub(crate) fn get_mut(&mut self, key: &str) -> Option<&mut V> {
self.map.get_mut(key)
}

pub(crate) fn into_values(self) -> btree_map::IntoValues<&'key str, V> {
self.map.into_values()
}
Expand Down
12 changes: 12 additions & 0 deletions src/unresolved_recipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ impl<'src> UnresolvedRecipe<'src> {
Attribute::Confirm(Some(expression)) | Attribute::WorkingDirectory(expression) => {
resolve_expression(expression, &self.parameters)?;
}
Attribute::Doc(Some(expression)) => {
resolve_expression(expression, &[])?;
}
Attribute::Arg {
help_property,
pattern_property,
Expand All @@ -103,6 +106,15 @@ impl<'src> UnresolvedRecipe<'src> {
.attributes
.into_items()
.map(|(mut attribute, name)| {
if let Attribute::Doc(Some(expression)) = &attribute {
let value = evaluator.evaluate_value_const(expression)?;
self.doc = if value.is_empty() {
None
} else {
Some(value.join())
};
}

if let Attribute::Arg {
help,
help_property: Some((_key, expression)),
Expand Down
105 changes: 105 additions & 0 deletions tests/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,111 @@ fn doc_multiline() {
.success();
}

#[test]
fn doc_attribute_may_be_expression() {
Test::new()
.justfile(
"
prefix := 'hello '
[doc(prefix + 'world')]
foo:
",
)
.args(["--list"])
.stdout(
"
Available recipes:
foo # hello world
",
)
.success();
}

#[test]
fn doc_attribute_list_is_joined() {
Test::new()
.justfile(
"
set lists
[doc(['hello', 'world'])]
foo:
",
)
.env("JUST_UNSTABLE", "1")
.args(["--list"])
.stdout(
"
Available recipes:
foo # hello world
",
)
.success();
}

#[test]
fn doc_attribute_empty_list_is_no_doc() {
Test::new()
.justfile(
"
set lists
[doc([])]
foo:
",
)
.env("JUST_UNSTABLE", "1")
.args(["--list"])
.stdout(
"
Available recipes:
foo
",
)
.success();
}

#[test]
fn doc_attribute_cannot_reference_undefined_variable() {
Test::new()
.justfile(
"
[doc(undefined)]
foo:
",
)
.stderr(
"
error: variable `undefined` not defined
——▶ justfile:1:6
1 │ [doc(undefined)]
│ ^^^^^^^^^
",
)
.failure();
}

#[test]
fn doc_attribute_cannot_reference_non_const_variable() {
Test::new()
.justfile(
"
bar := `echo BAR`
[doc(bar)]
foo:
",
)
.stderr(
"
error: cannot access non-const variable `bar` in const context
——▶ justfile:2:6
2 │ [doc(bar)]
│ ^^^
",
)
.failure();
}

#[test]
fn extension() {
Test::new()
Expand Down
14 changes: 14 additions & 0 deletions tests/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1184,6 +1184,20 @@ fn doc_attribute_suppresses_comment() {
);
}

#[test]
fn doc_attribute_expression() {
assert_dump(
"
[doc('f' + 'oo')]
foo:
",
"
[doc('f' + 'oo')]
foo:
",
);
}

#[test]
fn unchanged_justfiles_are_not_written_to_disk() {
let tmp = tempdir();
Expand Down
34 changes: 34 additions & 0 deletions tests/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,40 @@ fn module() {
);
}

#[test]
fn module_doc_attribute_empty_string() {
case_with_submodule(
"
[doc('')]
mod foo
",
Some(("foo.just", "bar:")),
Module {
modules: [(
"foo",
Module {
doc: Some(""),
first: Some("bar"),
module_path: "foo",
source: "foo.just".into(),
recipes: [(
"bar",
Recipe {
name: "bar",
namepath: "foo::bar",
..default()
},
)]
.into(),
..default()
},
)]
.into(),
..default()
},
);
}

#[test]
fn module_group() {
case_with_submodule(
Expand Down
Loading
Loading