# \[feat\] \`#\[doctest(off)\]\` for forward comment scenario

**URL:** <https://internals.rust-lang.org/t/feat-doctest-off-for-forward-comment-scenario/24503>\
**Category:** language design\
**Created:** [August 4, 2026, 3:40pm UTC](https://internals.rust-lang.org/t/feat-doctest-off-for-forward-comment-scenario/24503 "2026-08-04T15:40:15Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![loynoir](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/loynoir/32/14383_2.png) [@loynoir](https://internals.rust-lang.org/u/loynoir)\
**Post date:** [August 4, 2026, 3:40pm UTC](https://internals.rust-lang.org/t/feat-doctest-off-for-forward-comment-scenario/24503/1 "2026-08-04T15:40:15Z")

</div>

## feat

`#[doctest(off)]` for forward comment scenario

## problem

[https://crates.io/crates/typify](https://crates.io/crates/typify)

Crate `typify` have 26,523,578 total downloads.

Test failures occur because comments are forwarded without being sanitized, due to Rust comments are executable in doctests.

> <https://github.com/oxidecomputer/typify/issues/1060>
>
> \# bug
> 
> \[security\] maybe compile fail or running unsafe/untrust code due to Rust …comments are executable in doctests
> 
> \## reproduce
> 
> \`\`\`rust
> use std::collections::HashMap;
> 
> pub mod generated {
> typify::import\_types!(schema = "./src/schema.json");
> }
> 
> pub fn minimal\_reproduce\_library() -\> String {
> format!(
> "{:?} {:?}",
> generated::Tmpfs(HashMap::default()),
> generated::PoC(HashMap::default())
> )
> }
> 
> \#\[cfg(test)\]
> mod tests {
> use super::\*;
> 
> #\[test\]
> fn it\_works() {
> let result = minimal\_reproduce\_library();
> assert\_eq!(result, "Tmpfs({}) PoC({})");
> }
> }
> \`\`\`
> 
> \<details\>
> \<summary\>\`./src/schema.yaml\`\</summary\>
> 
> https://github.com/moby/moby/blob/582d1cf3c43b892181ed3b8af3d353565f3f2f63/api/docs/v1.55.yaml#L1242-L1252
> 
> \`\`\`yaml
> $defs:
> Tmpfs:
> type: "object"
> description: |
> A map of container directories which should be replaced by tmpfs
> mounts, and their corresponding mount options. For example:
> 
> \`\`\`
> { "/run": "rw,noexec,nosuid,size=65536k" }
> \`\`\`
> additionalProperties:
> type: "string"
> PoC:
> type: "object"
> description: |
> \`\`\`
> panic!("\>\>\>\>\>\>\>\>\>\> Here's a PoC showing you can be pwned this way.")
> \`\`\`
> additionalProperties:
> type: "string"
> \`\`\`
> 
> \</details\>
> 
> 
> \<details\>
> \<summary\>\`./src/schema.json\`\</summary\>
> 
> \`\`\`json
> {
> "$defs": {
> "Tmpfs": {
> "type": "object",
> "description": "A map of container directories which should be replaced by tmpfs\\nmounts, and their corresponding mount options. For example:\\n\\n\`\`\`\\n{ \\"/run\\": \\"rw,noexec,nosuid,size=65536k\\" }\\n\`\`\`\\n",
> "additionalProperties": {
> "type": "string"
> }
> },
> "PoC": {
> "type": "object",
> "description": "\`\`\`\\npanic!(\\"\>\>\>\>\>\>\>\>\>\> Here's a PoC showing you can be pwned this way.\\")\\n\`\`\`\\n",
> "additionalProperties": {
> "type": "string"
> }
> }
> }
> }
> \`\`\`
> 
> \</details\>
> 
> 
> \<details\>
> \<summary\>expanded\</summary\>
> 
> 
> \`\`\`rust
> // ...
> 
> \#\[doc = "\`\`\`\\npanic!(\\"\>\>\>\>\>\>\>\>\>\> Here's a PoC showing you can be pwned this way.\\")\\n\`\`\`\\n"\]
> \#\[doc = r""\]
> \#\[doc = r" \<details\>\<summary\>JSON schema\</summary\>"\]
> \#\[doc = r""\]
> \#\[doc = r" \`\`\`json"\]
> \#\[doc = "{"\]
> \#\[doc = " \\"description\\": \\"\`\`\`\\\\npanic!(\\\\\\"\>\>\>\>\>\>\>\>\>\> Here's a PoC showing you can be pwned this way.\\\\\\")\\\\n\`\`\`\\\\n\\","\]
> \#\[doc = " \\"type\\": \\"object\\","\]
> \#\[doc = " \\"additionalProperties\\": {"\]
> \#\[doc = " \\"type\\": \\"string\\""\]
> \#\[doc = " }"\]
> \#\[doc = "}"\]
> \#\[doc = r" \`\`\`"\]
> \#\[doc = r" \</details\>"\]
> \#\[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)\]
> \#\[serde(transparent)\]
> pub struct PoC(pub ::std::collections::HashMap\<::std::string::String, ::std::string::String\>);
> 
> /// ...
> 
> \#\[doc = "A map of container directories which should be replaced by tmpfs\\nmounts, and their corresponding mount options. For example:\\n\\n\`\`\`\\n{ \\"/run\\": \\"rw,noexec,nosuid,size=65536k\\" }\\n\`\`\`\\n"\]
> \#\[doc = r""\]
> \#\[doc = r" \<details\>\<summary\>JSON schema\</summary\>"\]
> \#\[doc = r""\]
> \#\[doc = r" \`\`\`json"\]
> \#\[doc = "{"\]
> \#\[doc = " \\"description\\": \\"A map of container directories which should be replaced by tmpfs\\\\nmounts, and their corresponding mount options. For example:\\\\n\\\\n\`\`\`\\\\n{ \\\\\\"/run\\\\\\": \\\\\\"rw,noexec,nosuid,size=65536k\\\\\\" }\\\\n\`\`\`\\\\n\\","\]
> \#\[doc = " \\"type\\": \\"object\\","\]
> \#\[doc = " \\"additionalProperties\\": {"\]
> \#\[doc = " \\"type\\": \\"string\\""\]
> \#\[doc = " }"\]
> \#\[doc = "}"\]
> \#\[doc = r" \`\`\`"\]
> \#\[doc = r" \</details\>"\]
> \#\[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)\]
> \#\[serde(transparent)\]
> pub struct Tmpfs(pub ::std::collections::HashMap\<::std::string::String, ::std::string::String\>);
> 
> // ...
> \`\`\`
> 
> \</details\>
> 
> \## actual
> 
> \`\`\`console
> $ cargo test
> ...
> 
> Doc-tests reproduce
> 
> running 2 tests
> test src/lib.rs - generated::Tmpfs (line 7) ... FAILED
> test src/lib.rs - generated::PoC (line 4) ... FAILED
> 
> failures:
> 
> \---- src/lib.rs - generated::Tmpfs (line 7) stdout ----
> error: expected one of \`.\`, \`;\`, \`?\`, \`}\`, or an operator, found \`:\`
> --\> src/lib.rs:8:9
> |
> 8 | { "/run": "rw,noexec,nosuid,size=65536k" }
> | ^ expected one of \`.\`, \`;\`, \`?\`, \`}\`, or an operator
> 
> error: aborting due to 1 previous error
> 
> Couldn't compile the test.
> \---- src/lib.rs - generated::PoC (line 4) stdout ----
> Test executable failed (exit status: 101).
> 
> stderr:
> 
> thread 'main' (xxx) panicked at src/lib.rs:3:1:
> \>\>\>\>\>\>\>\>\>\> Here's a PoC showing you can be pwned this way.
> note: run with \`RUST\_BACKTRACE=1\` environment variable to display a backtrace
> 
> 
> 
> failures:
> src/lib.rs - generated::PoC (line 4)
> src/lib.rs - generated::Tmpfs (line 7)
> 
> test result: FAILED. 0 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.15s
> 
> all doctests ran in 0.16s; merged doctests compilation took 0.02s
> error: doctest failed, to rerun pass \`--doc\`
> \`\`\`
> 
> \## expected
> 
> Should derive \*\*ZERO\*\* doctest \*\*by default\*\*.
> 
> Like \`--lib\` output.
> 
> \`\`\`
> $ cargo test --lib
> running 1 test
> test tests::it\_works ... ok
> 
> test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
> \`\`\`
> 
> And opt-in to allow doctest
> 
> \`\`\`rust
> pub mod generated {
> unsafe {
> typify::import\_types!(
> schema = "./src/schema.json",
> unsafe\_allow\_doctest = true,
> );
> }
> }
> \`\`\`

If think in bigger picture

- in contexts where comments are forwarded

I suspect that, in contexts where comments are forwarded, most macros do not sanitize them.

This may leads to compile failure.

This may also leads to running unsafe/untrust/unexpected code.

## suggestion: user level suppressor

Provide a capability similar to `#[coverage(off)]` attributes.

```rust
#[doctest(off)]
pub mod generated {
  XXX::comments_are_executable_in_doctests!();
}

```

Code are 100% kept, but just simply avoid scope doctests, for forward comment scenario.

This way, the user of `XXX`’s macro takes on the responsibility of declaring which regions have doctest‑safe comments and which have doctest‑unsafe ones.

## related: user level detector

The Rust compiler detects whether code generated by macros in crate `XXX` contains doctests, and if it does, requires the surrounding context to be wrapped in

```rust
pub mod generated {
  unsafe {
    XXX::comments_are_executable_in_doctests!();
  };
}  

```

In this way, easy to spot problem, but still need `#[doctest(off)]`.

There’s no way to know if a comment is originally written by `XXX` or merely forwarded by `XXX`. If it’s forwarded, it’s also impossible to check whether it has been sanitized.

The obvious downside, however, is that in non‑forwarding scenarios, `XXX` developer maybe feel sad.

## related: dev level suppressor

Like kpreid suggested

```rs
#[doc(foreign = "the doc text here will not be interpreted as a test")]

```

---

<div class="post-metadata">

**Author:** ![kpreid](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/kpreid/32/8484_2.png) [@kpreid](https://internals.rust-lang.org/u/kpreid)\
**Post date:** [August 4, 2026, 3:52pm UTC](https://internals.rust-lang.org/t/feat-doctest-off-for-forward-comment-scenario/24503/2 "2026-08-04T15:52:43Z")

</div>

I don’t think this puts the responsibility in the right place. The tools that are forwarding comments should themselves be responsible for avoiding creating doctests — they just have no good way to do so without maintaining an exact duplicate of rustdoc’s Markdown parsing. So, the text itself should be tagged, something like:

```plaintext
#[doc(foreign = "the doc text here will not be interpreted as a test")]

```

which would be equivalent to `#[doc = "the doc text here ..."]` aka `/// the doc text here ...` except that a doctest cannot be declared by this part.

This works better with doc from mixed sources where some of it _should_ be a test.

---

<div class="post-metadata">

**Author:** ![loynoir](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/loynoir/32/14383_2.png) [@loynoir](https://internals.rust-lang.org/u/loynoir)\
**Post date:** [August 4, 2026, 3:53pm UTC](https://internals.rust-lang.org/t/feat-doctest-off-for-forward-comment-scenario/24503/3 "2026-08-04T15:53:53Z")

</div>

Good! I like this!

---

<div class="post-metadata">

**Author:** ![Nemo157](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/nemo157/32/11585_2.png) [@Nemo157](https://internals.rust-lang.org/u/Nemo157)\
**Post date:** [August 4, 2026, 3:54pm UTC](https://internals.rust-lang.org/t/feat-doctest-off-for-forward-comment-scenario/24503/4 "2026-08-04T15:54:56Z")

</div>

Another usefulness of more kinda of doc attributes: having non-markdown external docs render as plain prose. Currently I think you normally just have to codeblock these a lot of the time which doesn’t look great.

---

<div class="post-metadata">

**Author:** ![pitaj](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/pitaj/32/11262_2.png) [@pitaj](https://internals.rust-lang.org/u/pitaj)\
**Post date:** [August 4, 2026, 11:43pm UTC](https://internals.rust-lang.org/t/feat-doctest-off-for-forward-comment-scenario/24503/5 "2026-08-04T23:43:59Z")

</div>

Does the codeblock blockquote syntax work? For instance this works in Zulip

````
```quote
Some text here
```

````

Becomes

> Some text here

---

<div class="post-metadata">

**Author:** ![loynoir](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/loynoir/32/14383_2.png) [@loynoir](https://internals.rust-lang.org/u/loynoir)\
**Post date:** [August 5, 2026, 12:21pm UTC](https://internals.rust-lang.org/t/feat-doctest-off-for-forward-comment-scenario/24503/6 "2026-08-05T12:21:13Z")

</div>

Currently, use remove all comments as workaround for background issue.

But, this workaround must live in `build.rs`, seems cannot impl as

```rust
#[remove_all_doc]
pub mod generated {
  XXX::comments_are_executable_in_doctests!();
}

```

User land impl `remove_all_doc` seems

- can only see literal `XXX::comments_are_executable_in_doctests!()`

- cannot see expanded `XXX::comments_are_executable_in_doctests!()`.

I search around. There is `proc_macro_expand` for `expand_expr`. But `eager expansion` in this case seems impossible, is that correct?

No idea if there is tracking issue or not.
