syncdoc

Crates.iosyncdoc
lib.rssyncdoc
version0.5.2
created_at2025-11-10 19:45:22.257575+00
updated_at2025-11-20 17:37:05.116394+00
descriptionProcedural macro attributes to inject documentation from external files
homepagehttps://github.com/lmmx/syncdoc
repositoryhttps://github.com/lmmx/syncdoc
max_upload_size
id1926002
size61,053
Louis Maddox (lmmx)

documentation

https://docs.rs/syncdoc

README

syncdoc

crates.io documentation MIT/Apache-2.0 licensed pre-commit.ci status free of syn

syncdoc is a procedural macro that automatically injects documentation from external files into your Rust code, eliminating the need to manually maintain inline doc comments.

Use syncdoc when you want to keep documentation separate from implementation.

Stick with inline docs when you prefer co-location of docs and code.

Motivation

Extensive documentation is great for users, but inline docstrings make code hard to read:

/// This is a very long doc comment
/// that spans many lines and makes
/// the actual code hard to see...
/// [more lines]
mod A {
    /// And another long doc comment
    /// [many more lines]
    fn b() { ... }

    /// Yet another long doc comment
    /// [many more lines]
    fn c() { ... }
}

One solution is typically to add an #[include_str!] attribute pointing to a file, but this creates line noise of its own (relative paths ascending to the doc files).

syncdoc solves this by automatically resolving documentation from external files like include_str! according to each item's subpath.

One #[omnidoc] attribute call produces multiple such #[doc = !include_str(...)] annotations.

The example below is for a scenario where docs-path has been set in Cargo.toml. Syncdoc never assumes where your docs live. When migrating, it stores them by default in docs/ under the Cargo manifest dir (the dir with Cargo.toml in) and writes the docs-path metadata in Cargo.toml for you.

use syncdoc::omnidoc;

#[omnidoc] // Docs from docs/A.md
mod A {
    fn b() { ... }  // Docs from docs/A/b.md
    fn c() { ... }  // Docs from docs/A/c.md
}

Installation

Add syncdoc to your Cargo.toml:

[dependencies]
syncdoc = "0.1"

Setup

docs-path (recommended)

To avoid specifying path in every attribute, add a default to your Cargo.toml (it must be set one way or the other or the build will error).

[package.metadata.syncdoc]
docs-path = "docs"

Now you can use #[omnidoc] without arguments - syncdoc calculates the correct relative path automatically (thanks to this little trick specifically).

cfg-attr (optional)

To generate #[cfg_attr(doc, doc = "...")] instead of #[doc = "..."] (meaning your docstrings will be #[cfg(doc)]-gated (so cargo doc will generate them but cargo build/check/test will not), set the cfg-attr key to "doc" in your Cargo.toml.

[package.metadata.syncdoc]
cfg-attr = "doc"

See the Build Configuration section below for more details.

Migration

The CLI automatically migrates code from doc comments to syncdoc #[omnidoc] attributes.

CLI Installation

  • pre-built binary: cargo binstall syncdoc (requires cargo-binstall),
  • build from source: cargo install syncdoc --features cli

CLI Usage

Commit your code before running with -c/--cut or -r/--rewrite as they modify source files.

Usage: syncdoc [OPTIONS] <SOURCE>

Migrate Rust documentation to external markdown files.

Arguments:
  <SOURCE>           Path to source directory to process (default: 'src')

Options:
  -d, --docs <dir>   Path to docs directory (default: 'docs' or from Cargo.toml if set)
  -m, --migrate      Swap doc comments for #[omnidoc] (cut + add + touch)
  -c, --cut          Cut out doc comments from source files
  -a, --add          Rewrite code with #[omnidoc] attributes
  -t, --touch        Touch empty markdown files for any that don't exist
      --inline-paths Use inline path= parameters instead of Cargo.toml
  -n, --dry-run      Preview changes without writing files
  -v, --verbose      Show verbose output
  -h, --help         Show this help message

Examples

  • 'Sync' the docs dir with the docstrings in src/
syncdoc
  • Preview a full migration without running it
syncdoc --migrate --dry-run (or `-m -n` for short)
  • Full migration: cut docs, add attributes, and touch missing files
syncdoc --migrate (or `-m` for short, equal to `--cut --add --touch`)
  • Migrate with inline paths instead of Cargo.toml config
syncdoc --migrate --inline-paths

syncdoc-migrate

The migration CLI uses a standard diffing algorithm (Myers, as used git), and should be able to correctly identify how to migrate your code to the omnidoc macro. Syncdoc relies on rustfmt and will not work if rustfmt cannot process your code. The only effects from a "round trip" through syncdoc migrate/restore should be to 'fix' docstrings (move them above attribute macros, trim leading/trailing blank lines).

After running, you should inspect the git diff and cargo check the output to confirm the codegen builds. If you run the -m/--migrate flag it should touch all the files it requires so the result still builds.

Please send feedback on anything it gets wrong, ideally with a minimal repro.

Usage

Apply the #[omnidoc] attribute to any struct, function, enum, impl block, or inline module:

use syncdoc::omnidoc;

#[omnidoc]
mod my_functions {
    fn foo(x: i32) -> i32 {
        x * 2
    }

    fn bar(y: i32) -> i32 {
        y + 1
    }
}

This will look for documentation in:

  • docs/my_functions/foo.md
  • docs/my_functions/bar.md

Note: you cannot use a proc macro on an external module, see this tracking issue.

A workaround to document an entire module is to inline the entire module (mod mymodule { ... }) then re-export it with pub use mymodule::*;. If you do, note that the name of the inner module is the name the macro will look for at the path.

If that isn't to your liking, then just use it on impl blocks etc. and use a regular syncdoc::omnidoc attribute for individual items.

Documenting Impl Blocks

syncdoc also works on impl blocks:

use syncdoc::omnidoc;

struct Calculator;

#[omnidoc]
impl Calculator {
    pub fn new() -> Self {
        Self
    }

    pub fn add(&self, a: i32, b: i32) -> i32 {
        a + b
    }
}

Documentation files:

  • docs/Calculator/new.md
  • docs/Calculator/add.md

Single Function Documentation

You can also document individual functions.

use syncdoc::omnidoc;

#[omnidoc]
fn func1() {
    // -> docs/func1.md
    // = omnidoc(path) to root docs dir + submodule + fn name + .md
}

Documenting Structs and Enums

syncdoc automatically documents struct fields and enum variants:

use syncdoc::omnidoc;

#[omnidoc]
mod types {
    struct Config {
        port: u16,
        host: String,
    }

    enum Status {
        Active,
        Inactive,
        Error(String),
    }
}

Documentation files:

  • docs/types/Config.md - struct documentation
  • docs/types/Config/port.md - field documentation
  • docs/types/Config/host.md - field documentation
  • docs/types/Status.md - enum documentation
  • docs/types/Status/Active.md - variant documentation
  • docs/types/Status/Inactive.md - variant documentation
  • docs/types/Status/Error.md - variant documentation

How It Works

syncdoc uses a procedural macro to inject #[doc = include_str!("path")] attributes before function definitions.

It uses proc-macro2 (it's free of syn!) to parse tokens rather than doing full AST creation.

Implementation Details

The macro:

  1. Parses tokens to find function definitions
  2. Constructs doc paths based on module hierarchy and function names
  3. Injects doc attributes using include_str! for compile-time validation
  4. Preserves existing attributes and doesn't interfere with other macros

For examples of the generated output, see the test snapshots which show the exact documentation attributes injected for various code patterns.

What Gets Documented

  • Regular functions: fn foo() { ... }
  • Generic functions: fn foo<T>(x: T) { ... }
  • Methods in impl blocks: impl MyStruct { fn method(&self) { ... } }
  • Trait default methods: trait MyTrait { fn method() { ... } }
  • Struct fields: struct Foo { field: i32 }
  • Enum variants: enum Bar { Variant1, Variant2(i32) }
  • Type aliases: type MyType = String;
  • Constants: const X: i32 = 42;
  • Statics: static Y: i32 = 42;

Build Configuration

For faster builds, you can configure syncdoc to only generate documentation during cargo doc:

Example Macro invocation TOML settings required Generated attribute form
demo_cfg_attr_call #[cfg_attr(doc, syncdoc::omnidoc)] ❌ none #[doc = include_str!(...)]
demo_cfg_attr_toml #[syncdoc::omnidoc] cfg-attr = "doc" #[cfg_attr(doc, doc = include_str!(...))]

Option 1 gates the macro itself, at the call site. Option 2 gates the generated attributes, configured in TOML (it can also be done at the call site, but I'd recommended to do it in Cargo.toml to reduce the line noise in your code).

When using either approach, gate the missing_docs lint (if using it):

#![cfg_attr(doc, deny(missing_docs))]

License

This project is licensed under either of:

at your option.

Commit count: 0

cargo fmt