Repository: casey/just
Pull request #2050 (2024-05-18 02:45:18)

`doc` attribute for docstrings
cf. https://github.com/casey/just/issues/1955 , it would be convenient if there was a way to specify a recipe documentation string with an attribute. Also this would make possible to provide a docstring that is different from actual comments in the justfile.

casey: I think this needs to be rebased on master, there are a bunch of extra commits in here.
neunenak: > I think this needs to be rebased on master, there are a bunch of extra commits in here.

The new code in this PR is on top of https://github.com/casey/just/pull/1842 since that changes how attribute parsing works. I'm not sure if there's a way to tell github to omit commits from another PR in the pull request UI. In any case, https://github.com/casey/just/pull/2050/commits/eda55cc584c3eea8fa3a64d15e402a99d45655bd has the code that is actually new in this PR.
casey: I'm going to cut a release tomorrow, which will be a monster one, since the features have piled up. I think this would be sweet, if you want to get it in. No worries if you don't have time though!
neunenak: > I'm going to cut a release tomorrow, which will be a monster one, since the features have piled up. I think this would be sweet, if you want to get it in. No worries if you don't have time though!

Yeah, it'd be great to get this in this release, although what I really care about is recipe groups so if this has to wait, not the end of the world. 

Patch:
diff --git a/README.md b/README.md
index 1b5adc47..f0a6fcd5 100644
--- a/README.md
+++ b/README.md
@@ -1079,6 +1079,27 @@ Available recipes:
     test # test stuff
 ```
 
+The `[doc]` attribute can be used to set or suppress a recipe's doc comment:
+
+```just
+# This comment won't appear
+[doc('Build stuff')]
+build:
+  ./bin/build
+
+# This one won't either
+[doc]
+test:
+  ./bin/test
+```
+
+```sh
+$ just --list
+Available recipes:
+    build # Build stuff
+    test
+```
+
 ### Variables and Substitution
 
 Variables, strings, concatenation, path joining, and substitution using `{{…}}`
@@ -1622,8 +1643,9 @@ Recipes may be annotated with attributes that change their behavior.
 | Name | Description |
 |------|-------------|
 | `[confirm]`<sup>1.17.0</sup> | Require confirmation prior to executing recipe. |
-| `[confirm('prompt')]`<sup>1.23.0</sup> | Require confirmation prior to executing recipe with a custom prompt. |
-| `[group('NAME"']`<sup>master</sup> | Put recipe in [recipe group](#recipe-groups) `NAME`.
+| `[confirm('PROMPT')]`<sup>1.23.0</sup> | Require confirmation prior to executing recipe with a custom prompt. |
+| `[doc('DOC')]`<sup>master</sup> | Set recipe's [documentation comment](#documentation-comments) to `DOC`. |
+| `[group('NAME"']`<sup>master</sup> | Put recipe in [recipe group](#recipe-groups) `NAME`. |
 | `[linux]`<sup>1.8.0</sup> | Enable recipe on Linux. |
 | `[macos]`<sup>1.8.0</sup> | Enable recipe on MacOS. |
 | `[no-cd]`<sup>1.9.0</sup> | Don't change directory before executing recipe. |
diff --git a/src/attribute.rs b/src/attribute.rs
index e650a907..bf657a06 100644
--- a/src/attribute.rs
+++ b/src/attribute.rs
@@ -10,6 +10,7 @@ use super::*;
 #[strum_discriminants(strum(serialize_all = "kebab-case"))]
 pub(crate) enum Attribute<'src> {
   Confirm(Option<StringLiteral<'src>>),
+  Doc(Option<StringLiteral<'src>>),
   Group(StringLiteral<'src>),
   Linux,
   Macos,
@@ -24,7 +25,7 @@ pub(crate) enum Attribute<'src> {
 impl AttributeDiscriminant {
   fn argument_range(self) -> RangeInclusive<usize> {
     match self {
-      Self::Confirm => 0..=1,
+      Self::Confirm | Self::Doc => 0..=1,
       Self::Group => 1..=1,
       Self::Linux
       | Self::Macos
@@ -72,6 +73,7 @@ impl<'src> Attribute<'src> {
 
     Ok(match discriminant {
       Confirm => Self::Confirm(argument),
+      Doc => Self::Doc(argument),
       Group => Self::Group(argument.unwrap()),
       Linux => Self::Linux,
       Macos => Self::Macos,
@@ -91,7 +93,8 @@ impl<'src> Attribute<'src> {
   fn argument(&self) -> Option<&StringLiteral> {
     match self {
       Self::Confirm(prompt) => prompt.as_ref(),
-      Self::Group(name) => Some(name),
+      Self::Doc(doc) => doc.as_ref(),
+      Self::Group(group) => Some(group),
       Self::Linux
       | Self::Macos
       | Self::NoCd
@@ -107,7 +110,6 @@ impl<'src> Attribute<'src> {
 impl<'src> Display for Attribute<'src> {
   fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
     write!(f, "{}", self.name())?;
-
     if let Some(argument) = self.argument() {
       write!(f, "({argument})")?;
     }
diff --git a/src/recipe.rs b/src/recipe.rs
index a6afe2f2..f974e394 100644
--- a/src/recipe.rs
+++ b/src/recipe.rs
@@ -450,6 +450,15 @@ impl<'src, D> Recipe<'src, D> {
       })
       .collect()
   }
+
+  pub(crate) fn doc(&self) -> Option<&str> {
+    for attribute in &self.attributes {
+      if let Attribute::Doc(doc) = attribute {
+        return doc.as_ref().map(|s| s.cooked.as_ref());
+      }
+    }
+    self.doc
+  }
 }
 
 impl<'src, D: Display> ColorDisplay for Recipe<'src, D> {
diff --git a/src/subcommand.rs b/src/subcommand.rs
index 18af7c03..f7733fc2 100644
--- a/src/subcommand.rs
+++ b/src/subcommand.rs
@@ -573,7 +573,7 @@ impl Subcommand {
           );
 
           let doc = if i == 0 {
-            recipe.doc.map(Cow::Borrowed)
+            recipe.doc().map(Cow::Borrowed)
           } else {
             Some(Cow::Owned(format!("alias for `{}`", recipe.name)))
           };


Test patch:
diff --git a/tests/attributes.rs b/tests/attributes.rs
index 055ec9e6..d527b0ef 100644
--- a/tests/attributes.rs
+++ b/tests/attributes.rs
@@ -129,3 +129,45 @@ fn unexpected_attribute_argument() {
     .status(1)
     .run();
 }
+
+#[test]
+fn doc_attribute() {
+  Test::new()
+    .justfile(
+      "
+    # Non-document comment
+    [doc('The real docstring')]
+    foo:
+      echo foo
+  ",
+    )
+    .args(["--list"])
+    .stdout(
+      "
+    Available recipes:
+        foo # The real docstring
+        ",
+    )
+    .run();
+}
+
+#[test]
+fn doc_attribute_suppress() {
+  Test::new()
+    .justfile(
+      "
+    # Non-document comment
+    [doc]
+    foo:
+      echo foo
+  ",
+    )
+    .args(["--list"])
+    .stdout(
+      "
+    Available recipes:
+        foo
+        ",
+    )
+    .run();
+}