test(server): check OpenAPI descriptions with the pulldown-cmark rustdoc link guard (#2330) - #2398
Merged
Merged
Conversation
…doc link guard (#2330) velesdb-server's hand-written raw-text scan passed an autolink to a path and bare item paths rustdoc resolves ([SegmentInfo], [u64]). The guard velesdb-memory built after #2261 moves into velesdb-rustdoc-guard, a test-only crate that is never published, taken by both crates as a path-only dev-dependency. Measured against the server's old tables, it also gains the three forms only the old scan caught: backticks inside a label, the bare [&] primitive, and a mailto::X path as a link target.
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 25 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
Review found labels rustdoc 1.90 links that the guard passed: spaces around a call or macro suffix ([foo ()], [vec !], [vec !()]), the bare never and unit primitives ([!], [!][], [()]), and a spaced disambiguator ([struct @foo], [fn @ f]), which velesdb-memory's rewrite rewrites, so the guard no longer flagged at least what the rewrite rewrites. The label is now read in rustdoc's order: backticks, fragment, one-word disambiguator and call suffix removed with their spaces, then the path. one_pass_is_final's mix gains a spaced disambiguator token.
…s path characters
Review found `[a, b][](crate::Foo)` passing: accepting every reference
folds `[a, b][]` into a collapsed link and hides the inline link a
client renders. The guard now also parses with no reference invented,
and flags what either reading flags. It allowed `(){}@` anywhere in a
path; it now keeps the marks rustdoc's should_ignore_link keeps, so
`[f(x)]` passes as rustdoc ignores it. A spaced disambiguator is
covered by explicit cases in velesdb-memory, which the pseudo-random
mix of one_pass_is_final could not draw.
…c 1.90 Strip generics and the empty path segments they leave before reading a bare primitive, count their depth with a sign, check rustdoc's path marks over the whole path, and read a code span as its code. Against rustdoc 1.90 on 42,621 generated labels, the guard flags every label rustdoc links, and nothing rustdoc neither links nor warns about. The docs no longer claim rustdoc trims a kind before its @: it warns about that form.
velesdb-memory's rewrite stripped generics before checking rustdoc's path characters, so it rewrote labels rustdoc and the guard leave as written ([Result<(), u8>], [Vec<f32.5>]). It now checks the whole path first, and one_pass_is_final draws <, > and . so it would see that gap. The guard takes rustdoc's early return on a / anywhere in a label.
…eads a path The rewrite checked rustdoc's path marks before dropping backticks, so it stopped rewriting [Vec<`u8`>], which rustdoc links, and it still rewrote [S0#a/b], which rustdoc shows as written. It now takes the guard's steps in rustdoc's order, and one_pass_is_final draws / and #.
…ustdoc-link-guard
Dropping every backtick makes the rewrite remove links develop left as written ([f`()`], [``Foo``], [x](crate::`Foo`)), which rustdoc links, and refuse [x](a#b/c). The CHANGELOG states it and a test pins it. The spaced disambiguator doc comment is back on its own test.
The rewrite reads a label, an inline destination and a reference definition alike, so develop also rewrote [x](Foo<'a>) and [y]: a#b/c, which it now refuses. The CHANGELOG says it once for every target, and a test pins the destination and definition cases.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2330
What
velesdb-server checked its published OpenAPI descriptions for rustdoc link syntax with a hand-written raw-text scan (#2270). velesdb-memory had already replaced the same kind of scan with pulldown-cmark after #2261's 40 review rounds. The server now uses that guard. The guard is shared, not copied, and it stays out of velesdb-core.
velesdb-rustdoc-guard(publish = false). It starts from the guard invelesdb-memory/tests/support/rustdoc_link_guard.rs, moved withgit mv; the review rounds below then corrected how it reads a bare label. Its JSON walk takes the keys to read (description, plussummaryfor OpenAPI) and escapes pointers per RFC 6901, as the server's did.version. Cargo strips that when they publish, socargo publish --dry-run, the release list and version sync don't see it. Both manifests carry a comment on this. One consequence, stated in velesdb-memory's manifest: its packaged tests no longer compile outside the workspace. They already readdocs/reference/mcp-tools.json, which the package does not ship, so they only ran from the repository before this change too.pulldown-cmarkis pinned once in[workspace.dependencies]. velesdb-memory's runtime rewrite and the guard use the same version. Rounds 4 and 5 below change what that rewrite reads as a path, and the CHANGELOG states the change.holds_rustdoc_link,brackets_a_path,target_start,is_url,collect_rustdoc_linksand their four tests. The tables became the guard's own tests.Red first
I compiled the server's old scanner on its own. It returned
falsefor each of these, and the guard flags each one:see <crate::Point>.: an autolink to a path;see [SegmentInfo] and [optional].: bare item paths, which rustdoc 1.90 resolves or warns about;see [u64] and [Self].:[u64]resolves to the primitive.The guard, measured against the old tables and against rustdoc
I ran the guard on all 55 strings in the server's tables. The old scan flagged three forms the guard did not, and all three are real rustdoc links: a label with backticks inside (
[stream_traverse`()`],[vec`!`]), which rustdoc drops before reading the path; the bare[&]reference primitive; and amailto::Xlink target, which is a path and not an address.Review round 1 found more outside that table:
[foo ()],[vec !],[vec !()],[!]and[!][], each of which rustdoc 1.90 links. It also found[struct @Foo]and[fn @ f], which velesdb-memory's rewrite rewrites and the guard passed, so "the guard flags at least what the rewrite rewrites" was false.names_an_itemno longer handles these one by one. It now reads a label in the order rustdoc'spreprocess_linkdoes: backticks dropped, the item before a#, a one-word disambiguator and a call or macro suffix removed along with the spaces around them, then the path tested, with the bare!,()and&primitives accepted (round 3 found that rustdoc keeps()as prose). Against the previous guard,flags_each_link_formmisses exactly those 8 new strings, and all of them pass now. For the three fixes from the table, a mutation removing each one makes the test fail on exactly its string. A spaced disambiguator is covered by explicit cases in velesdb-memory (a_spaced_disambiguator_is_rewritten_and_flagged).one_pass_is_final's pseudo-random mix can't draw a known kind before a spaced@, so no token was added there.Review round 2 found two more gaps.
[a, b][](crate::Foo), a client renders the inline link[](crate::Foo). The guard, which accepts every reference so as to see what rustdoc might link, read[a, b][]as a collapsed reference and never saw that link. The old server scan did catch it, through its][. The guard now reads each text twice: once as rustdoc does, and once as a client renders it, with no reference invented. What either reading flags is a link.(){}@anywhere in a path. rustdoc'sshould_ignore_linkallows only:_<>, !*&;. So[f(x)]and[x()y]now pass, as rustdoc ignores them, and(){}only count as a suffix that gets stripped. Round 3 below found the labels it still read differently.Each new test fails against the guard it fixes.
flags_each_link_formmisses[a, b][](crate::Foo)andpasses_web_links_code_and_prose_bracketsflags[f(x)]under the round-2 guard, and the spaced-disambiguator case fails under the round-1 guard. The reviewer's randomized check (300,000 labels of 1 to 7 tokens, asserting that the guard flags every text the rewrite rewrites) findssee [(){}][](crate::Foo)] endin the round-2 guard, and nothing in 36,450 rewritten texts in this one.Review round 3 measured the guard against rustdoc 1.90 on 3,202 labels, and I repeated the measurement at a larger scale.
[!<u8>]and[::<u8>&]passed, and rustdoc links them to the never and reference primitives. The guard tested for a bare primitive before it stripped generics, and rustdoc strips generics first. rustdoc also drops the empty::segments that stripping leaves, and counts generic depth with a sign, so[><f]reads asf.[()],[!{}],[Result<(), u8>],[Vec<f32.5>],[Option<&'static str>]and[`()`]were flagged. rustdoc checks its path characters against the whole path, generics included, and keeps[()]as prose. It also drops a code span's backticks before it reads the label, where the guard took any single code span for a path.@. It warns about[struct @Foo]and[fn @ f]and shows the brackets as written, and it trims only after the@([fn@ f]). The guard still flags the spaced forms: failing closed costs nothing where rustdoc warns anyway. Its docs and velesdb-memory's test comment now say so.names_an_itemnow takespreprocess_link's steps in rustdoc's order:/anywhere means no item (added in round 4);#fragment;[!]stays the never primitive);I measured it against
cargo +1.90 doc(the rendered HTML and the JSON warnings) on two corpora: 22,621 labels (every sequence of 1 to 3 of 28 tokens, plus the named cases) and 20,000 random labels of 4 to 7 tokens. On both, 0 labels rustdoc links pass the guard, and the guard flags 0 labels that rustdoc neither links nor warns about. Every other flag is a label rustdoc warns about: an unresolved path, an unknown kind, or malformed generics. Two things are left out of the count, both by design: themailto:link rustdoc renders from an e-mail autolink inside a label, which is publishable, and the images the guard flags.Red first:
flags_each_link_formreportsthe guard misses "see [!<u8>].", andpasses_web_links_code_and_prose_bracketsgets["[()]", "[ () ]", "[!{}]", "[()]"].[><f];[::<u8>&];[Result<(), u8>],[Vec<f32.5>]and[Option<&'static str>].Review round 4 found two things.
is_rustdoc_targetstill stripped generics first, so the rewrite kept rewriting labels that rustdoc and the guard leave alone:[Result<(), u8>],[Vec<f32.5>],[Option<&'static str>],[Vec<a/b>].one_pass_is_finalhad no<,>or.token, so it could not see this. The rewrite now checks rustdoc's characters before it strips generics. This is a behaviour change in velesdb-memory's published-schema rewrite, stated in the CHANGELOG: such a label stays as written, as rustdoc shows it. The committedmcp-tools.jsonsnapshot is unchanged./step.preprocess_linkreturns early on any/. The guard skipped that step and flagged[a#/]and[S0#a/b], which rustdoc neither links nor warns about.Red first:
one_pass_is_finalfails against the round-4 rewrite withthe guard misses "[<a\">crate::x!]> # b]b]".a_path_rustdoc_ignores_is_left_as_writtenfails withleft: Some("see Result<(), u8>.")./step makespasses_web_links_code_and_prose_bracketsflag["[a#/]", "[S0#a/b]", "[Vec<u8>#x/y]"].A third differential against rustdoc 1.90 found 0 misses and 0 silent over-flags. It ran on 20,000 random labels of 2 to 6 tokens, and adds
/,a/b,#,",|,=,+andéto the tokens.Review round 5 found that round 4's rewrite fix was itself out of rustdoc's order.
[Vec<`u8`>], which rustdoc 1.90 links. It stripped only a single enclosing code span./step. The rewrite still rewrote[S0#a/b]toS0, which rustdoc shows as written and the guard now passes.is_rustdoc_targetnow takes the guard's steps in rustdoc's order: it stops on a/, drops every backtick, then checks characters over the whole path.one_pass_is_finalalso draws/and#.Red first: against the round-5 rewrite,
code_inside_a_path_is_rewritten_and_flagged(renamed in round 6) fails withleft: None;a_path_rustdoc_ignores_is_left_as_written, with the/cases, fails withleft: Some("see a.");one_pass_is_finalfails withthe guard misses "`b`)\n\n[a# `/>\t][".All 853
velesdb-memory --features http --libtests pass after the fix.Review round 6 found one more undeclared rewrite change and a misplaced doc comment.
[f`()`],[Foo]and[x](crate::`Foo`). rustdoc links all three, and the guard flags them. It also refuses[x](a#b/c), which develop rewrote tox. The CHANGELOG now states both.code_inside_a_path_is_rewritten_and_flaggedpins the backtick cases, and round 7'sa_target_rustdoc_reads_as_no_item_is_refusedpins the/refusal. Put back develop's single code-span strip and that test fails withleft: None, right: Some("see Vec<u8>.").Review round 7 found that the CHANGELOG, and the round-4 and round-6 sections above, scoped the rewrite change too narrowly: they covered labels and one inline link.
is_rustdoc_targetreads every target the same way, whether a label, an inline destination or a reference definition. So develop also rewrote[x](Foo<'a>),[x](Vec<f32.5>#a),[x](Vec<a/b>)and[x][y]with[y]: a#b/c, which HEAD refuses (left as written, flagged by the guard). HEAD also removes[x](crate::Foo), whose lone backtick rustdoc drops. The CHANGELOG now states the change once, for every target. The newa_target_rustdoc_reads_as_no_item_is_refusedpins the destination and definition cases; with develop'sschema_walks.rsit fails withleft: Some("see x."), right: None`.Strings the old scan failed as a documented cost now pass, because rustdoc does not link them: code spans (
`[x](crate::y)`,`&[Vec<f32>]`), web links whose text holds code or a path ([`Point`](https://…),[issue #2261](https://…)), and prose brackets ([#2261],[`asc`, `desc`]). The other way round, a bare[Point]or[sic], and a VelesQL pattern written outside a code span (-[:KNOWS]->), now fail. That's the cost of failing closed, andflags_prose_that_reads_as_an_item_pathstates it. The committeddocs/openapi.jsonpasses: its bracketed text all sits in code spans or code blocks.Also
deny.toml:licenses.clarifyentry for the new crate, like every other workspace crate.CONTRIBUTING.md: crate map row.ARCHITECTURE.mddescribes the runtime layers, so a test-only crate is not listed there.CHANGELOG.md: Changed entry, stating what the guard now flags and passes compared with develop.Gates (local, aarch64, replayed from
ci.yml; from round 5 on, reduced to the changed crates to spare the machine; CI runs the whole workspace)-D warnings -D clippy::pedanticwith both of CI's workspace feature setsvelesdb-rustdoc-guard(4),velesdb-server --lib(default features, and--all-featuresfor the OpenAPI tests),velesdb-memory --features http, andvelesdb-memory --lib --no-default-features --features extract,persistencecargo publish -p velesdb-memory --dry-run --lockedpasses, which shows the path-only dev-dependency is stripped.velesdb-server's dry-run fails against the velesdb-core on crates.io, which does not haveparse_search_modeyet. That has nothing to do with this change, and CI does not run it.cargo deny check licenses bans sources, rustdoc-D warningson the new crate, andcheck-inline-tests,check-file-budgets,check-doc-freshness,check-feature-claims,check-ai-attributionandcheck-version-sync/// … see [SegmentInfo].added toTraversalResultItem's doc comment,test_openapi_descriptions_carry_no_rustdoc_linkfails at/components/schemas/TraversalResultItem/description