Documenting a struct and its fields in the same comment

After years of tinkering with Rust, I simply cannot get over documenting structs and enums in rustdoc.

What rustdoc wants:

/// Jobbler for jobbling the thing.
struct ThingJobbler
{
    /// Doodad by which the thing is jobbled.
    doodad:  Doodad,
    /// Exraneous nonsense.
    whatsit: Stuff,
    /// Thing to be jobbled.
    gadget:  Thing,
    
    /// Way out.
    ///
    /// This portal must have been painted prior to use or else something,
    /// something, something.
    portal:  Gate,
}

What I want

/** Jobbler for jobbling the thing.
 *
 *  - doodad:   Doodad by which the thing is jobbled.
 *  - whatsit:  Exraneous nonsense.
 *  - gadget:   Thing to be jobbled.
 *  - portal:   Way out.
 *              This portal must have been painted prior to use or else
 *              something, something, something.
 */
struct ThingJobbler
{
    doodad:  Doodad,
    whatsit: Stuff,
    gadget:  Thing,
    portal:  Gate,
}

I am looking for a way to do something more like the second way, but with the HTML output of the first. To me, this is orders of magnitude cleaner and categorically better in the actual code; you cannot convince me otherwise.

Is this possible with rustdoc? If not, what can I use instead of rustdoc.

2 Likes

You could write an attribute macro that transforms the documentation in the way you want. You would have to use the macro on every struct with this format of documentation, but other than that, it would straightforwardly work with rustdoc.

4 Likes

I'm with you!
Having started Rust just weeks ago, I was blasted away how easy rustdoc was to use and how nice and exhaustive the output was.
So, after having written thousands of pages of C-code documentation with doxygen, I went ahead. And was disappointed. You can't document function parameters and return results in a systematic way. Description of enums and struct's fields ... fail. At least, I found no description.

This is just a snippet of my C-code with doxygen:

  /**
   \brief Initializes an instance instantiated with APP_FormatterInstantiate().
   \param instance is the instance number.
   \param myMail is the instance's mailbox ID.
   \param masterMail is the master's (left hand side) mailbox ID.
   \param slaveMail is the slave's (right hand side) mailbox ID.
   \return true on success.
   */
  bool APP_FormatterInitialize(unsigned int instance, uint8_t myMail, uint8_t masterMail, uint8_t slaveMail);

or:

/**
 \brief Data for a single instance.
 */
typedef struct {
  APP_FormatterStates_t state;  ///< State of FSM
  APP_FormatterTypes_t fmtType; ///< Type of formatting task.

  APP_MailboxId_t myMail; ///< Task's mailbox ID
  APP_MailboxId_t masterMail; ///< Task's master (left hand side).
  APP_MailboxId_t slaveMail; ///< Task's slave (right hand side)
...

This generates a nice output with a short description of each enum / parameter. Why can't rustdoc be at least as good as doxygen? rustdoc shines with the integration into the whole ecosystem and with all the references to crate methods. But developers want parameter descriptions that are not in a single block of text, but in list form.

Or is there a way that haven't found out yet? Not that I could find anything in the otherwise excellent Rust documentation of crates.

Quite puzzled!

Some discussion:

1 Like

I think the idiomatic Rust way (heavily influenced by the functional tradition in ocaml and haskell) would be to use "self documenting" parameters and return types, in a way that makes misusing the function quasi impossible and written documentation less relevant.

Using types that are correct by construction helps greatly with that way of doing things.

10 Likes

Having per-parameter doc comments would be occasionally useful, but not that often. It should in general be obvious what each parameter is for, documented by their type, name, and the function’s doc. Feeling the need to document parameters one by one is usually a smell, a sign that the interface could be tighter and more obvious.

Wrt structs and enums, not sure what you mean. Rustdoc understands field and variant doc comments perfectly well:

struct Foo {
    /// Some hopefully **useful** info and not just
    /// repeating what the name and type already say
    bar: Bar,
    /// And so on
    baz: Baz,
}
5 Likes

So, continuing your argument, the requirement by Rust's guidlines to include documentation and examples is fishy. This only shows that the interface is not selfexplanatory.
I have heard the argument "the code is self-explaining" since decades. And still, documentation tools evolved and have their place.

Really (screenshot of HTML)?


This is the result of:

enum XMLTagTypes {
    XMLTagEmpty,
    /// <x/>
    XMLTagOpen,
    /// <x>
    XMLTagClose,
    /// </x>
    XMLTagNone,
}

that was initially formatted like this:

enum XMLTagTypes {
    XMLTagEmpty, /// <x/>
    XMLTagOpen,  /// <x>
    XMLTagClose, /// </x>
    XMLTagNone,
}

And here an similarily commented example (initial formatting) how it looks like in doxygen:

And to add some icing to the top, doxygen generates call-graphs too:
Screenshot 2024-08-25 at 06-58-31 XCEPT Mainboard CommonSources_appActivityLEDs.c File Reference
Not saying that Rustdoc has to be exactly the same. But showing how exhaustive things can get.

1 Like

Yes really, it's a bit below your screenshot though.

Do I need new glasses?

I'm betting your <x></x> are interpreted by the markdown preprocessor as html.

Sidenote: don't be needlessly aggressive or obtuse, you clearly know lots of things. You could have deduced the above easily.

7 Likes

I don't think that '< x >' (remove blanks) is part of the markdown language. Markdown Reference
Anyhow, dustdoc completely removed my comment. The "bad" comment or "HTML" even is gone in the generated HTML (by looking at the HTML-source).
I'd have to write it like this:

    /// &lt;x/&gt;
    XMLTagEmpty,

I don't think this increases readability. But funny enough, rustdoc lets pass a '< b >' (remove blanks), despite the fact that bold is '_ bold _' or '* bold *' (remove blanks again) in markdown. That's the result of not separating domains. Markdown-domain and propably/supposedly/assumptional/misunderstood HTML.

An other point of Rust and documentation and Rust's formatting is, that it inserts newlines.
Starting with this:

enum Example {
One, /// useless in-line comment
}

It decides to insert a newline to get this:

enum Example {
 One,
/// Useless comment
}

and then complains about a comment that doesn't comment an item in the enum. I want my code to be compact. And not to double the line count. I'm not paid by LOC (lines of code).
So the effect of these decissions is, that I will not '///' comment my short hints to about what is going on. I see no gain why rustdok makes a goulasch of markdown and HTML. Use markdown and escape anything that might be invalid HTML within a comment.
doxygen solved this problem by a '///< comment'. Meaning: This comment is for the line it is in.

Sad sidenote: doxygen doesn't support Rust (anymore?)

You're also not paid to worry over formatting.

Doc comment /// comment in Rust are syntax sugar for a macro #[doc = "comment"] that is part of the language, you can't just place them anywhere you want.


Just be respectful.

5 Likes

Markdown understands inline HTML.

HTML source or browser devtools? If it's the latter, they might have "cleaned up" HTML for you, by e.g. removing closing tags which have no opening counterpart.

It's not that it "inserts newlines", it's just that the order is important. Doc comments are a form of attributes[1], and (outer) attributes are always associated with the item after it, by definition.


  1. Literally, you can use #[doc("something") instead of /// something - and by the way, the former can go on the same line as the item it documents ↩︎

8 Likes

Or… you could put the XML source code into an inline code section.

    /// `<x/>`
    XMLTagEmpty,

And if you do prefer the output being plain text instead of a code block, you can just escape the < character:

    /// \<x/>
    XMLTagEmpty,

I cannot confirm this. In fact, it does appear completely unmodified in the HTML source for me:

(Of course, as expected from your source code, it put the <x/> on the Open case, <x> on the Close case, and </x> on the None case.)

Notably though, I can also even see the existence of these documentation sections in by browser (chrome or firefox):

I’m noticing your screenshot looks different in this regard, which makes me wonder why it maybe didn’t work the same for you.

Edit: I’m also just noticing, there was a warning when documenting without properly escaping

warning: invalid self-closing HTML tag `x`
 --> src/main.rs:6:9
  |
6 |     /// <x/>
  |         ^^
  |
  = note: `#[warn(rustdoc::invalid_html_tags)]` on by default

warning: unclosed HTML tag `x`
 --> src/main.rs:8:9
  |
8 |     /// <x>
  |         ^^^

warning: unopened HTML tag `x`
  --> src/main.rs:10:9
   |
10 |     /// </x>
   |         ^^^^

warning: `…` (bin "…" doc) generated 3 warnings
6 Likes

I already showed a solution how this could be handled. With no negative influence to the current behaviour. And the fact that doxygen handles it, only shows that comments like these are common place Moving the comment to a new line only adds noise.

Just be more relaxed.

Ah thanks, I just looked at the summary. My pitty!

It was devtools. :frowning:

My source is re-formatted by inserting a newline. And adding noise. I'm doing software since 1977 (yes, I'm an old fart). And I have seen different styles of formatting (of source and comments - if there were any). I prefer a gentle style that isn't as intrusive as Rust's. Unfortunately, there is no way to opt out of some features.

I'm not saying that things have to be strictly as I say. The suggestion of the initial post is acceptable.
But still, parameters do lack comments. And yes, I have read the discussion someone linked to (thanks, interesting read). But the only conclusion was that parameter comments lead to useless comments. But how about just ignoring them and so not use them? But no, it is better to block any different attitudes completely. The new inclusiveness I guess.

It's a tool, not a law. Think about it.
Related to this discussion in a much wider perspective: Who has read D. Knuth's book "Literate programming"?

Edit: Oh, I forgot! Thanks for the tip to escape with a leading '/'

1 Like

No, that's just how open source works. Nobody's interested in implementing that. So it doesn't happen.

And why nobody's interested: because as I wrote earlier Rust has roots in ocaml where using strong valid by construction types is the norm. I'm not saying this is right or wrong. I'm saying there's a cultural mismatch here.

8 Likes

That's quite funny!
OCaml is best in obfuscation. You have the .ml where it is common not to specify the types of parameters. Unless OCaml fails to guess what it is. This happens often enough. But you can (and should) have meaningful parameter names. Until you write the .mli that only shows the types and no parameter names. The result? Put back a meaning into the signature, repeat (copy & paste) the parameter names. Furthermore, .ml and .mli are quite detached. They are not directly linked together. Instead of having some include mechanism, the compiler only finds out at a leter time that .ml and .mli don't match.
Just have a look at the lousy documentation of OCaml-modules to see what you get. OCaml actively discourages documentation.
The output of OCaml's doc-generator is an eyesore and useless.

I tend to agree about ocaml documentation. But you see the issue then.

Each language has it's own style, and standardized tooling like rustfmt is immensely helpful. Get over it.

You could prototype this (and thus show the costs and benefits) using an attribute macro as suggested by @kpreid. Your code would look like:

#[my_doc_enhancer]
/** Jobbler for jobbling the thing.
 *
 *  - doodad:   Doodad by which the thing is jobbled.
 *  - whatsit:  Exraneous nonsense.
 *  - gadget:   Thing to be jobbled.
 *  - portal:   Way out.
 *              This portal must have been painted prior to use or else
 *              something, something, something.
 */
struct ThingJobbler
{
    doodad:  Doodad,
    whatsit: Stuff,
    gadget:  Thing,
    portal:  Gate,
}

Then, using syn's support for reading doc comments, you'd pick up your doc comment, and rewrite the struct as seen by the compiler from one big doc comment before the struct to something in current RustDoc format:

#[doc = r"Jobbler for jobbling the thing."]
#[doc = r""]
struct ThingJobbler
{
    #[doc = r"Doodad by which the thing is jobbled."]
    doodad:  Doodad,
    #[doc = r"Exraneous nonsense."]
    whatsit: Stuff,
    #[doc = r"Thing to be jobbled."]
    gadget:  Thing,
    
    #[doc = r"Way out."]
    #[doc = r"This portal must have been painted prior to use or else"]
    #[doc = r"something, something, something."]
    portal:  Gate,
}

You may benefit from using quote to write out the second form. Guide to Rust procedural macros | developerlife.com has some guidance on writing this sort of proc macro - you want to read the text, skip examples 1 through 4 (not relevant to this use case), and move onto example 5 to see an example of reading in an Item and writing out something based on it.

4 Likes