dejadoc 0.4.0

Find duplicated Rust doctests and functions across a workspace
Documentation

cargo-dejadoc

CI codecov Quality Gate crates.io docs.rs

Whoa, deja vu. A doctest went past us, and then another that looked just like it.

cargo dejadoc scans and canonicalizes every doctest your workspace's rustdoc would run, and every function, reporting any duplicates. Use it in your GitHub CI!

A pull request review by dejadoc, with an inline comment on a duplicated doctest and a suggestion that removes the copy

The flags under cargo dejadoc --help restrict the package, set the threshold and minimum token counts, turn the function check off, point at a config file, and switch the output to JSON. Without flags, parameters come from .dejadoc.toml at the workspace root.

The first copy of a group is the one to keep. A copy whose code names its own item comes first, so a test copied from parse onto lex stays on parse. Otherwise file and line order decides.

To keep a copy on purpose, write dejadoc after rust on the opening line of its code block. rustdoc ignores the word and runs the doctest as before.

/// ```rust,dejadoc
/// let parsed = mycrate::parse("1");
/// ```

Functions compare within their module, in every target and under every cfg. Two match when they differ only in their name, visibility, local names, formatting, or attributes such as cfg, inline and lint levels. A test marker, should_panic or ignore keeps them apart. Copies on different self types are reported as a generic or a macro waiting to happen, without a suggestion to delete. Functions under 30 tokens are skipped. A // dejadoc: allow line above a function keeps it.

In CI, one workflow covers it. The action installs the crate, scans, and posts the findings as a pull request review, with every other input optional.

on: pull_request
jobs:
  dejadoc:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: LucaCappelletti94/cargo-dejadoc@v1
        with:
          # Review the pull request instead of failing the job. Drop it to gate a push.
          pr-number: ${{ github.event.pull_request.number }}
          # Report duplicates that predate the pull request too, default false.
          only-new: false
          # Report a group from this many copies, default 2.
          threshold: 2
          # Ignore doctests below this token count, default 0.
          min-tokens: 0
          # Check for duplicated functions, default true.
          functions: true
          # Ignore functions below this token count, default 30.
          fn-min-tokens: 30
          # Scan bin and example doctests as well as lib ones, default false.
          all-targets: true
          # Restrict to one workspace member, and scan from a subdirectory.
          package: mycrate
          cwd: .
          # Annotate each copy to remove, default true.
          annotations: true
          # Render the review in the job summary instead of posting it.
          dry-run: false
          # Use the cargo dejadoc already on the path instead of installing one, default true.
          install: true

Review mode reports only the duplicates your pull request introduces, comparing each site against a scan of the base commit. Comments land on the copies to remove, a kept copy never gets one. Each comment links the copy that survives and GitHub renders those lines right in the comment, long ones collapsed behind a show link. The link points at the base commit for pre-existing copies and at the first added copy for groups the pull request created, and the removal suggestion deletes the copy together with an empty line left behind. Sites GitHub cannot anchor get permalinks in the review body.

Every run also writes the report to the job summary and one annotation per copy to remove, which GitHub anchors to the line in the diff. Annotations are workflow output rather than API calls, so they need no token and a fork pull request shows the findings whatever its token allows. The --github flag prints them from the command line too, beside the usual report.

A fork's GITHUB_TOKEN is read-only, so the review cannot be posted there and the job passes on the annotations and the summary alone. Only a review comment can carry the removal suggestion, so keeping that on forks takes two workflows, the unprivileged one preparing the review and a privileged one posting it without ever checking out the pull request head.

# .github/workflows/dejadoc.yml, no permissions, runs on the pull request.
on: pull_request
jobs:
  dejadoc:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: LucaCappelletti94/cargo-dejadoc@v1
        with:
          pr-number: ${{ github.event.pull_request.number }}
          mode: prepare
          payload: dejadoc-payload
      - uses: actions/upload-artifact@v4
        with:
          name: dejadoc-payload
          path: dejadoc-payload
# .github/workflows/dejadoc-post.yml, writes, never checks out the head.
on:
  workflow_run:
    workflows: [dejadoc]
    types: [completed]
jobs:
  post:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dejadoc-payload
          path: dejadoc-payload
          run-id: ${{ github.event.workflow_run.id }}
          github-token: ${{ github.token }}
      - uses: LucaCappelletti94/cargo-dejadoc@v1
        with:
          mode: post
          payload: dejadoc-payload

The payload holds the pull request number, the head commit, and one rendered comment per copy with the lines it covers. The posting job reads only that, so it needs no checkout, no toolchain and no dejadoc install, and untrusted code never runs beside the write token, the workflow_run pattern. pull_request_target grants the same access in one job, and the action still accepts it, but its recipe checks out the pull request head in a privileged job, so it is not the path this README recommends.

The library builds the same report in memory, so a project's own task runner can gate on it with dejadoc = { version = "0.3", default-features = false, features = ["std"] }, which leaves the CLI and its clap dependency out. The scan compiles nothing, so a crate whose features are mutually exclusive needs one run rather than one per feature set.

let report = dejadoc::Dejadoc::default().run("tests/fixtures/dupws").unwrap();
assert_eq!(report.groups.len(), 2);

Coding agents get the same guidance from the dejadoc skill, installed with the skills CLI.

npx skills add LucaCappelletti94/cargo-dejadoc