A docstring that opens with a fenced code block collapses into its one-line summary, delimiters and all

#9 · open · 0 comments

View on GitHub ↗

dazuma

## Summary `DocstringSummary#smart_summary` turns every newline into a space before any markup conversion runs, so a docstring whose first block is a fenced code block is flattened into the summary with its ` ``` ` delimiters intact. The summary then reaches `index.md`, the `description:` frontmatter, and the parent's `## Member Summary` bullet. This is the residue of #8. That issue fixed the *body* — a fence in an RDoc-dialect docstring now renders as a fence — and this is the same input taking a different path, one #8's fix structurally cannot reach. ## The mechanism Both summary call sites are `markdownify(smart_summary(object.docstring))` (`templates/default/module/agentdocs/setup.rb:40`, `templates/default/fulldoc/agentdocs/setup.rb:130`), so **`smart_summary` runs first**, and its first act is `lib/yard/agentdocs/docstring_summary.rb:98`: ```ruby stripped = strip_provenance(docstring).gsub(/[\r\n](?![\r\n])/, " ").strip ``` By the time `markdownify` sees the text there is no multi-line block left to protect. Nor should `markdownify` rescue it afterwards: the result is a column-0 ` ``` ` whose info string contains a backtick, which CommonMark (and `HybridMarkdown#parse_fence_opener`, and #8's implementation) correctly declines to treat as a fence opener. ## Worked example `rbs-4.2.0/sig/directives.rbs:9` — a docstring that is *nothing but* a fence, with no prose sentence anywhere in it: ``` # ``` # use Foo, Foo::Bar as FBar, Foo:Baz::* # ``` # class Use < Base ``` | step | value | |---|---| | raw docstring | `"```\nuse Foo, ...\n```\n"` | | after line 98's newline collapse | `"``` use Foo, Foo::Bar as FBar, Foo:Baz::* ```"` | | after `terminal_punctuate` | `"``` use Foo, Foo::Bar as FBar, Foo:Baz::* ```."` | | after `markdownify` | unchanged | The body of `RBS/AST/Directives/Use.md` renders correctly (that is #8's fix): ``` use Foo, Foo::Bar as FBar, Foo:Baz::* ``` The summary does not, in three places: ``` index.md:54 * [`RBS::AST::Directives::Use`](...) - ``` use Foo, ... ```. RBS/AST/Directives.md:16 - [`Use`](Directives/Use.md) — ``` use Foo, ... ```. RBS/AST/Directives/Use.md:4 description: "``` use Foo, ... ```." ``` **Severity is currently cosmetic, not structural.** A one-line fence body happens to re-parse as an inline code span, so the two list rows render as `<code>use Foo, Foo::Bar as FBar, Foo:Baz::*</code>`; the `description:` frontmatter is the uglier one, since that is consumed as plain text. A multi-line fence body would be materially worse, and nothing prevents one. ## Measured scope Over the locally installed gem set, 147 contiguous comment blocks open with a column-0 fence, but almost none surface in a bundle: - **141** are `Prism::Translation::RubyParser::Compiler`'s methods. That class is Ruby-private (confirmed against the registry: `visibility: private`), so it is omitted under the "Visibility policy: Ruby-scope privacy omitted" decision. - **6** are in `rbs`'s sig files, 3 each in 3.10.0 and 4.2.0. Only `Directives::Use` reaches a rendered page; `Environment::UseMap`'s docstring does not surface at all. So this is one docstring in the whole corpus today, which is the argument for not rushing it. ## How an empty summary already renders Relevant because two of the options below produce one. All three sites already special-case it — it is the path every undocumented object takes: - **`index.md`**: `index_summary_suffix` (`templates/default/fulldoc/agentdocs/setup.rb:128`) returns `""` and the row drops its ` - ` separator, e.g. prism's `* [`Prism::ParseResult::Comments::_Target`](.../_Target.md)`. - **Frontmatter**: `frontmatter_description` (`templates/default/module/agentdocs/setup.rb:39`) returns `nil` and `frontmatter` omits the key rather than emitting `description: ""`. Still OKF-conformant — §4 makes `type` REQUIRED and `description` only *recommended*. - **Parent's Member Summary bullet**: `summary_suffix` (`lib/yard/agentdocs/text_layout.rb:26`) drops its `" — "`, e.g. `rbs-4.2.0/RDoc.md:14`'s `- [`Parser`](RDoc/Parser.md)`. ## Options How the `index.md` row comes out under each: ``` today * [`RBS::AST::Directives::Use`](...) - ``` use Foo, Foo::Bar as FBar, Foo:Baz::* ```. option 1 * [`RBS::AST::Directives::Use`](...) option 2 * [`RBS::AST::Directives::Use`](...) - `use Foo, Foo::Bar as FBar, Foo:Baz::*`. option 3 * [`RBS::AST::Directives::Use`](...) - `use Foo, Foo::Bar as FBar, Foo:Baz::*` ... ``` 1. **Skip a leading fence when extracting.** Summarize on the first sentence of the first *prose* paragraph, so a docstring that opens with an example and then explains itself summarizes on the explanation. The better rule in general, and it degrades worst on exactly the shape we have: `Use` has no prose at all, so the summary is empty and `index.md` — the discovery surface — carries nothing about it, when the example was the only thing the docstring ever said. 2. **Re-frame the collapsed block as an inline code span.** One backtick instead of three, stray delimiters dropped. Keeps the content in the row and is honest about what a one-line summary can carry, but a multi-line fence body still ends up as one run-on span. 3. **Take only the fence's first content line**, elided with `" ..."`, reusing `terminal_punctuate`'s existing "there's more" convention. Bounded for a multi-line body; arbitrary about which line matters. 4. **Leave it.** One docstring in the corpus, and the two list rows already render as a code span. 5. **Hybrid of 1 and 2**: skip the leading fence; if that leaves nothing, fall back to the fence's content as an inline code span (capped at its first line, per option 3, if it is multi-line). Gets the right answer on both shapes, at the cost of being two rules instead of one, and of a summary that is sometimes prose and sometimes an example — a consumer reading `index.md` cannot tell which it is looking at. Not yet decided; filed to be triaged rather than picked up. ## Notes for whoever picks this up - The fix belongs in `smart_summary`/`end_index`, **not** in `markdownify` — by the time the converter runs, the block is gone. That also means it is independent of the markup dialect: this bites a `--markup markdown` gem exactly as hard. - `DocstringSummary` is documented as a byte-for-byte port of `YARD::Docstring#summary` with three enumerated deviations, and `test/test_docstring_summary.rb` asserts agreement with real `Docstring#summary` over YARD's own spec scenarios. Any option here is a fourth deviation and should be recorded in that module's doc comment and under "Decisions" in `docs/dev/DESIGN.md` the same way the other three are. Check whether a chosen option breaks the port-parity assertions before committing to it. - Per the TDD loop, this lands as `examples/` fixture changes first. The shape to encode is a class whose docstring opens with a fence — plus, if the hybrid is chosen, one with a fence *followed* by prose, since those two take different branches.

Comments