Skip to content

feat(doc): solar-based rewrite + vocs migration#14568

Merged
mablr merged 66 commits into
masterfrom
mablr/forge_doc_solar_rewrite
Jun 11, 2026
Merged

feat(doc): solar-based rewrite + vocs migration#14568
mablr merged 66 commits into
masterfrom
mablr/forge_doc_solar_rewrite

Conversation

@mablr

@mablr mablr commented May 4, 2026

Copy link
Copy Markdown
Member

Summary

Rewrite solar-based forge doc (#14447), migrate to vocs.

  • Generates vocs scafold folder with produced MDX files
  • Removes --serve feature to avoid depending on npm, now user has to run forge doc --watch, and then npm run dev in another terminal.
  • Sidebar restructured: within each directory group, pages are nested under type sub-groups (Contracts, Abstract Contracts, Libraries, Interfaces, …). Directories that contain only one type list items flat. Labels show just the contract name, no type. prefix.
  • MDX safety fixes: replace_inline_links now escapes bare < -> &lt; (fixes <email@example.com> parsed as JSX), unresolved {Ident} -> `Ident` inline code, bare { -> &#123;. Unnamed return parameters rendered as &lt;none&gt; instead of <none>. Inherited natspec content (from @inheritdoc resolution) now runs through the same sanitizer.
  • Constants/immutables section: constant and immutable state variables are rendered under a ## Constants section, separate from mutable ## State Variables.
  • CLI grouping: serve-related flags (--hostname, --port, --open) moved into a dedicated Serve options help section so they no longer appear under Watch options.

Closes #11884
Closes OSS-93

PR Checklist

  • Added Tests (updated)
  • Added Documentation
  • Breaking changes

Tested against vectorized/solady and ithacaxyz/account.

image

Comment thread crates/doc/src/hir_ext.rs Outdated
Comment thread crates/doc/src/utils.rs
Comment thread crates/doc/README.md
@zerosnacks zerosnacks requested a review from DaniPopes May 5, 2026 10:09

@zerosnacks zerosnacks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall super nice! Some small points of feedback

I would like to see a differentiation in style between @notice and @dev, the latter I think should be rendered in italic.

Image

Ref: http://localhost:5173/src/utils/g/library.EnumerableSetLib

In forge-std as example a problem with function overrides is visible, maybe we can come up with something that makes it easier to distinguish between them.

Image

@zerosnacks

zerosnacks commented May 5, 2026

Copy link
Copy Markdown
Contributor

I am also seeing brittleness with Vocs where just clicking around will cause FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory. Can reliably reproduce w/ forge-std and clicking around in the libraries section. Will report this to Vocs.

@zerosnacks

Copy link
Copy Markdown
Contributor

In forge-std, this looks off ;;

Screenshot 2026-05-05 at 16 10 12

@zerosnacks

Copy link
Copy Markdown
Contributor

From Amp's oracle review, I think these findings are broadly accurate, worth double checking:

Blockers

  1. UDVT xrefs land on non-existent pages. hir_ext.rs:62 indexes top-level
    UDVTs with prefix "udvt", but render.rs:113 writes the file as
    type.<Name>.mdx. So {MyType}/.../udvt.MyType while the file on
    disk is /.../type.MyType.

  2. name_to_page is built from the full HIR, not the emitted page set.
    builder.rs:67-81 filters via ignore/include_libraries, but
    builder.rs:97 walks every gcx.hir.item_ids(). Result: {Ident} and
    inheritance links can resolve to library/ignored pages that are never
    written. Map should be derived from the same filtered set, or pruned after
    render.

  3. @inheritdoc resolves by name only. hir_ext.rs:155-170 returns the
    first function in the linearised base whose name matches. For real
    same-name overloads (foo(uint256) / foo(address)) the wrong one wins.
    The can_generate_docs_for_overloaded_functions test in
    tests/cli/doc.rs:15-67 actually uses two different names, so this case
    is uncovered. Consider matching by signature (param type list).

  4. Render panics are swallowed and the command still returns Ok.
    builder.rs:155-178 catch_unwinds and logs an error!, but the loop
    continues and build() returns Ok(()). A user can end up with missing
    pages and a green exit. Suggest at minimum failing the build when any
    non-library file panics, or surfacing a non-zero exit + summary.

  5. Frontmatter is not YAML-escaped. render.rs:741-749 writes
    description: "{truncated}" from the first @notice. Quotes / newlines /
    backslashes in a notice break the YAML. The 120-char chars().take(120)
    truncation can also slice mid-entity (e.g. truncating in the middle of an
    &lt; produced by replace_inline_links). Either escape per YAML rules
    or use a block scalar (description: |).

  6. URLs use OS-native path separators. hir_ext.rs:345, vocs.rs:95,
    vocs.rs:102-106, and utils.rs:24 all build URLs via Path::display(),
    so on Windows you get backslashes in vocs routes and GitHub blob/ links.
    Foundry already standardises on path-slash's .to_slash_lossy() for
    exactly this — see e.g. crates/common/src/preprocessor/data.rs:84,
    crates/forge/src/cmd/inspect.rs:478, crates/forge/src/cmd/bind_json.rs:318,
    crates/config/src/lib.rs:1115. Suggest using the same here for any
    path that ends up in a URL or vocs config.

Major

  1. find_contract_id is suffix-based (render.rs:909). Two files
    sharing a tail (e.g. src/A.sol and lib/foo/src/A.sol) will resolve
    to whichever the iterator returns first. Comparing absolute paths after
    strip-prefix would be safer.

  2. Sidebar labels collide / nesting is flat.

    • vocs.rs:113-123: comment says "preserving type prefix in name for
      clarity", but name was already stripped — contract.Foo and
      interface.Foo both display as Foo.
    • vocs.rs:102-106: nested directories are emitted as a single label
      like src/token/extensions instead of a true tree.
    • No collision check across categories.
  3. forge doc --watch only watches config.src (watch.rs:388-391).
    Now that --watch is the documented preview flow (cmd/doc.rs:87-93),
    it should also include:

    • libraries (when --include-libraries is set)
    • the project README.md — currently picked up as the default homepage
      in vocs.rs:213 but never re-rendered when edited
    • any config.homepage override
    • the deployments dir (<root>/deployments or the --deployments path)
    • foundry config / doc config
      Otherwise the live preview goes silently stale on the most common edits
      (README + deployments).
  4. No stale-page pruning (builder.rs:182-194). Renames or deletions
    leave orphan MDX files — particularly painful in watch mode and for
    static deploys. Consider tracking the previous run's outputs (or
    truncating pages_dir for non-scaffold files) before writing.

  5. Markdown tables don't escape | or newlines (render.rs:840).
    @param/@return text containing | will shift columns. The MDX
    sanitiser handles </{ but not table delimiters.

  6. Homepage comment vs impl mismatch. vocs.rs:28 lists
    config.homepage -> <sources>/README.md -> <root>/README.md -> empty
    but vocs.rs:203-219 skips the <sources>/README.md step.

  7. master vs main inconsistency. utils.rs:22 falls back to
    "master" for source links; vocs.rs:53 hardcodes edit/main/{path}.
    Repos using a different default branch get one of the two broken.

@DaniPopes

Copy link
Copy Markdown
Member

code side lgtm, will leave iteration on frontend to you

mablr and others added 5 commits May 6, 2026 11:24
- Fix UDVT xref prefix mismatch (udvt -> type) so {MyType} links land on
  the actual type.MyType.mdx page.
- Build name_to_page from the filtered source set so cross-references
  cannot resolve to library/ignored pages that are never emitted.
- Resolve @inheritdoc by parameter-type signature first, falling back to
  name match, so overloads pick the right base function.
- Surface render panics: non-library files that panic during render now
  fail the build instead of silently producing missing pages.
- Properly YAML-escape frontmatter title and description fields
  (handles backslashes, quotes, control chars). Drop the 120-char
  description truncation; emit the full first @notice with whitespace
  collapsed.
- Use path-slash to_slash_lossy() for any path that ends up in a URL
  (vocs links, sidebar paths, GitHub blob/edit URLs) so generated docs
  stay correct on Windows.

Co-authored-by: Amp <amp@ampcode.com>
- find_contract_id: require absolute-path equality so contracts that
  share
  a file stem across src/ and lib/ no longer collide.
- watch_doc: also watch external libraries (when --include-libraries),
  the homepage README override, <root>/README.md, the deployments dir,
  and foundry.toml so the live preview no longer goes silently stale.
- Prune stale .mdx pages left behind by previous runs (renames, deletes)
  and remove now-empty directories; the vocs scaffold's own index.mdx is
  preserved.
- Markdown table cells now escape | (column separator) and convert
  newlines to <br/>, so @param/@return text containing pipes or line
  breaks no longer shifts columns.
- Homepage lookup now also tries <sources>/README.md between the
  explicit
  config.homepage override and <root>/README.md, matching the documented
  precedence comment.
- Source links fall back to GitHub's HEAD (default branch agnostic)
  instead of master; the vocs editLink now uses the detected current
  branch (or 'main' as last resort) instead of hardcoding 'main'.

Co-authored-by: Amp <amp@ampcode.com>
…ar_rewrite

Amp-Thread-ID: https://ampcode.com/threads/T-019dfced-b5e9-77f4-9ed1-397907ffa292
Co-authored-by: Amp <amp@ampcode.com>

# Conflicts:
#	crates/doc/Cargo.toml
figtracer
figtracer previously approved these changes Jun 5, 2026
@mablr mablr enabled auto-merge (squash) June 5, 2026 17:04
Comment thread crates/doc/src/hir_ext.rs
Comment thread crates/doc/src/hir_ext.rs Outdated
Comment thread crates/doc/src/vocs.rs Outdated
Comment thread crates/doc/src/builder.rs Outdated
@mablr mablr requested review from figtracer and stevencartavia June 8, 2026 07:40
Comment thread crates/doc/src/hir_ext.rs
.vars
.iter()
.map(|v| {
let raw = sm.span_to_snippet(v.ty.span).unwrap_or_default();

@grandizzy grandizzy Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Proposed: open a tracking issue for "derive canonical HIR/ABI parameter types for @inheritdoc overload matching", and land this characterization test now so the gap is recorded and self-corrects when fixed:

// Known limitation: `@inheritdoc` overload matching compares the raw *source text*
// of each parameter's type (only `uint`/`int` aliases are normalized, see
// `normalize_sol_type`). When a valid, compiling override spells a parameter type
// differently from the base declaration — here base `I.Status` vs override
// `Status` — the signatures fail to match and the inherited NatSpec is silently
// dropped. The `uint256` overload (verbatim match) inherits correctly, isolating
// the cause to type spelling rather than `@inheritdoc` resolution in general.
//
// CHARACTERIZATION test: it locks in the *current* (incomplete) behavior. The
// proper fix is to derive canonical HIR/ABI parameter types instead of source
// snippets. When that lands, the `!contains("Sets the account status")` assertion
// below will start failing — flip it to `contains(...)` at that point.
forgetest_init!(inheritdoc_overload_drops_docs_on_type_spelling, |prj, cmd| {
    prj.add_source(
        "I.sol",
        r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface I {
    enum Status { Inactive, Active }

    /// @notice Sets the account status.
    /// @param s the new status
    function configure(I.Status s) external;

    /// @notice Configures by raw id.
    /// @param id the raw id
    function configure(uint256 id) external;
}
"#,
    );

    prj.add_source(
        "C.sol",
        r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./I.sol";

contract C is I {
    /// @inheritdoc I
    function configure(Status s) external override {}

    /// @inheritdoc I
    function configure(uint256 id) external override {}
}
"#,
    );

    cmd.args(["doc"]).assert_success();

    let content =
        std::fs::read_to_string(prj.root().join("docs/src/pages/src/contract.C.mdx")).unwrap();

    // Control: the `uint256` overload matches verbatim and inherits correctly.
    assert!(
        content.contains("Configures by raw id"),
        "uint256 overload should inherit its notice (control case), found:\n{content}"
    );

    // Known limitation (see header comment): the `Status` overload's inherited
    // notice is currently dropped because `I.Status` != `Status` as raw text.
    // FIXME(<issue>): once canonical HIR types are used, this should be
    // `assert!(content.contains("Sets the account status"), ...)`.
    assert!(
        !content.contains("Sets the account status"),
        "EXPECTED-FAIL breadcrumb: `@inheritdoc` overload type matching was improved \
         to canonicalize types — update this test to assert the notice IS inherited.\n{content}"
    );
});

@grandizzy grandizzy self-requested a review June 11, 2026 06:27
@mablr mablr merged commit 1ebe9c2 into master Jun 11, 2026
19 checks passed
@mablr mablr deleted the mablr/forge_doc_solar_rewrite branch June 11, 2026 06:44
@github-project-automation github-project-automation Bot moved this to Done in Foundry Jun 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Replace solang in forge doc

8 participants