DocAnchor.Analyzers
1.10.3
dotnet add package DocAnchor.Analyzers --version 1.10.3
NuGet\Install-Package DocAnchor.Analyzers -Version 1.10.3
<PackageReference Include="DocAnchor.Analyzers" Version="1.10.3"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="DocAnchor.Analyzers" Version="1.10.3" />
<PackageReference Include="DocAnchor.Analyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add DocAnchor.Analyzers --version 1.10.3
#r "nuget: DocAnchor.Analyzers, 1.10.3"
#:package DocAnchor.Analyzers@1.10.3
#addin nuget:?package=DocAnchor.Analyzers&version=1.10.3
#tool nuget:?package=DocAnchor.Analyzers&version=1.10.3
DocAnchor
Narrative documentation, assembled from fragments that live where they apply.
A fragment is a small piece of prose with an id. It lives either inside a source comment, right at the line it describes, or in a standalone file. A skeleton is a hand written document that declares the reading order and pulls fragments into named slots. DocAnchor merges the two into plain Markdown that you commit, and it fails the build when a reference points at nothing.
one id space. two homes. one merge. one validator.
Why not an existing tool
Three kinds of tool already exist and each solves one half:
- Snippet embedders pull code into documents, but do not enforce anything and cannot hold prose in the code.
- Traceability tools enforce links between requirements and code, but do not produce a document anybody wants to read.
- Documentation generators render beautifully, and the one that does keep prose in comments ties the reading order to the structure of the code.
DocAnchor is the combination: prose in the code, narrative order outside it, and a hard failure on a broken reference.
Install
dotnet tool install --global DocAnchor.Cli
Optionally add the analyzer to a C# project, so a broken reference is a compiler error with a squiggle in the editor rather than a separate step:
dotnet add package DocAnchor.Analyzers
The analyzer reads the catalog file. Add it to the project that should be checked:
<ItemGroup>
<AdditionalFiles Include="$(MSBuildThisFileDirectory)../../docanchor.lock" />
</ItemGroup>
Quick start
docanchor build # merge skeletons, write documents and docanchor.lock
docanchor check # validate and compare against disk, write nothing, for CI
docanchor list # show every fragment, add --orphans for the unused ones
docanchor where <id>
docanchor baseline # park the coverage findings that exist today
docanchor baseline --verify # compare the baseline against what is found, write nothing
A worked example
A decision that lives in the code, at the line that enforces it:
/* doc: decision/reject-unbalanced-entry
A posting whose legs do not sum to zero is rejected with `UnbalancedEntryException`, and
nothing is written. A partial posting is never persisted.
Reason: a half written posting cannot be reconciled afterwards, so the cheaper failure is
to refuse the whole thing. The audit rule in [[decision/audit-log-is-append-only]] relies
on every stored posting being balanced.
*/
// snip: balance-check
if (total != 0)
{
throw new UnbalancedEntryException(posting.Id);
}
// snip-end
// docref: decision/audit-log-is-append-only
_audit.Add(posting);
A decision with no natural anchor in the code, as a standalone file
docs/fragments/decision/audit-log-is-append-only.md:
---
status: active
---
The audit log is append only. A correction is a new entry that references the entry it
corrects, never an update of the original row.
The skeleton docs/mdsource/01-posting-rules.source.md, which owns the order:
# Posting rules
The ledger treats a posting as one unit: either it is fully valid and persisted, or it is
refused with a reason.
## Balance
## Audit
docanchor build writes docs/01-posting-rules.md with the prose woven in, the code quoted
underneath it, the cross link resolved to the page that carries the target, and a source link back
to the exact line. A complete runnable version of this is in
samples/Example.Ledger.
Markers
| Marker | Where | Meaning |
| --- | --- | --- |
| doc: <id> | source comment or fragment file | defines a fragment, the rest of the comment is its text |
| docref: <id> | source comment | this code depends on that fragment, no prose here |
| snip: <name> … snip-end | source comment | quote the code between the markers verbatim |
| | skeleton | pull the fragment in here | | | skeleton | pull the snippet in here |
| [[<id>]] | fragment text | link to another fragment, resolved to whatever page carries it |
An id is <kind>/<slug>, lowercase, two to four segments. The recommended kinds are decision/
for a deliberate choice, constraint/ for an externally imposed limit, trap/ for a non obvious
failure mode somebody would otherwise "fix", and concept/ for a term the rest of the docs rely
on. The tool does not enforce the set.
Diagnostics
| Id | Meaning | Reported by |
|---|---|---|
| DA0001 | reference to an unknown fragment id | tool and analyzer |
| DA0002 | duplicate fragment id | tool and analyzer |
| DA0003 | fragment defined but no skeleton includes it | tool |
| DA0004 | merged output is stale | tool |
| DA0005 | malformed marker or invalid id syntax | tool and analyzer |
| DA0006 | dangling cross link inside fragment text | tool |
| DA0009 | catalog missing, so ids cannot be checked | analyzer |
| DA0101 to DA0202 | coverage rules, see below | analyzer and tool |
| DA0910 | a baseline entry parks a finding that no longer exists | tool |
Exit codes: 0 clean, 1 defects found, 2 usage or internal error.
What the analyzer cannot do
The analyzer sees one compilation at a time and never sees the skeletons, the standalone fragment
files or the other host languages. It therefore cannot report DA0003, DA0004 or DA0006. Those stay
in docanchor check.
Run both in continuous integration. A green build is not by itself evidence that the documentation is complete.
DA0001, DA0002 and DA0005 default to error severity, so they fail a build without needing
TreatWarningsAsErrors. Lower them in .editorconfig if you would rather start with warnings:
dotnet_diagnostic.DA0001.severity = warning
DA0002 is reported at compilation end, which means it appears on build but not always live in the editor. DA0001 and DA0005 appear live.
Coverage rules
The diagnostics above enforce that references resolve. Coverage rules enforce the other direction: certain things in the code are not allowed to exist without documentation.
Every coverage rule is off until docanchor.policy switches it on.
rule DA0101 = error
rule DA0102 = warning
rule DA0201 = error
extension-interface = Example.Plugins.IPostingHook
contract-namespace = Example.Contracts
serialization-attribute = JsonProperty
sql-invocation = ExecuteSqlRaw
configuration-access = IConfiguration
environment-access = GetEnvironmentVariable
| Id | Fires on | Needs |
|---|---|---|
| DA0101 | a type that derives from Exception |
|
| DA0102 | catch (Exception) or catch { } whose block never rethrows |
|
| DA0103 | a member carrying [Obsolete] |
|
| DA0104 | #pragma warning disable and [SuppressMessage] |
|
| DA0105 | a type implementing a configured interface | extension-interface |
| DA0106 | a public type in a configured namespace | contract-namespace |
| DA0107 | a call passing a string to a configured method, assembled ones included | sql-invocation |
| DA0108 | a type carrying a configured serialization attribute, on a member or a record parameter | serialization-attribute |
| DA0109 | reading a configuration key, by method or by indexer | configuration-access |
| DA0110 | reading an environment variable | environment-access |
| DA0111 | Thread.Sleep, Task.Delay, TimeSpan.From* with a literal |
|
| DA0201 | create table, view, procedure, function, trigger |
|
| DA0202 | drop, truncate, alter table ... drop column |
Where the rules run. DA0201 and DA0202 always run in docanchor check, because a SQL file is
never compiled. DA0101 to DA0111 run in both places by default: the analyzer decides them from the
semantic model during the build, the tool decides them from parsed syntax during a check, and both
use the same rule logic. Pick one side if you would rather not see a finding twice:
csharp-coverage = cli # only docanchor check
csharp-coverage = analyzer # only dotnet build
csharp-coverage = both # default
Both sides are held to the same finding set by a test that runs the analyzer and the syntactic scan over one corpus of awkward code, enums, positional records, delegates, two fields in one declaration, calls inside field initializers, and fails on any difference in rule, file or key. The places where the two cannot agree are named rather than hidden:
The tool resolves base types by their written name across the scanned files, so it follows
class A : B and class B : Exception, and it treats a name ending in Exception as one. It
cannot see a base type that lives in a referenced assembly, an alias from using X = Y, or the
difference between two types with the same simple name. For an indexer it finds the type of the
receiver from the declaration of that name in the same file, a field, a property, a parameter or a
local. Where a project is built with the analyzer, that verdict is the more precise one.
Suppression means the same on both sides: the tool reads #pragma warning disable DA0102 and
[SuppressMessage] out of the source itself.
Anchoring. In C# a node counts as documented when it or an ancestor up to the containing member
carries a doc: or docref: marker. The search stops at the member on purpose: one comment on a
class must not silence every catch inside it. In SQL the marker has to sit directly above the
statement, blank lines aside.
Suppression. C# uses the compiler's own mechanisms, #pragma warning disable DA0102 and
[SuppressMessage("DocAnchor.Coverage", "DA0102")]. Files that are not compiled use a comment:
-- docanchor-disable DA0202
drop table dbo.LegacyOrders;
docanchor-disable-file DA0201 silences a whole file.
Adoption in a grown codebase. Turning a rule on produces every existing violation at once, and the reliable reaction to that is switching the rule off again. Park the backlog instead:
docanchor baseline # parks every finding the tool sees itself, C# and SQL
For projects that only the analyzer checks, add its findings from the build log. The import takes
a file, a directory or a glob, reads both the 1.0 log that /p:ErrorLog= writes by default and the
2.1 log that /p:ErrorLog=x.sarif,version=2 writes, tolerates the several JSON documents that a
solution build concatenates into one file, and drops entries whose file lies outside the scan
patterns:
dotnet build /p:ErrorLog=artifacts/docanchor.sarif
docanchor baseline --import artifacts/*.sarif
docanchor.baseline is committed. Everything new fails from that moment on, and an entry that is no
longer needed is reported as DA0910, so the file can only shrink. Entries are keyed by declaration,
never by line, so unrelated edits do not invalidate them. A key names the declaration and, for a
finding that has no name of its own, the kind of node and its number inside that declaration:
DA0103 src/Ledger/Journal.cs Example.Ledger.Journal.Post<TEntry>(TEntry, string)
DA0107 src/Ledger/Journal.cs Example.Ledger.Journal.Assemble(int, bool)#call2
DA0109 src/Ledger/Journal.cs Example.Ledger.Journal.Read(string)#index0
DA0104 src/Ledger/Journal.cs pragma:CA1031#0
Only the side that alone evaluates a rule may retire its entries. The SQL rules are always the
tool's own; the C# rules are only under csharp-coverage = cli. Under both the analyzer sees
things the tool cannot, so a run without it leaves those entries alone instead of demanding their
removal, and a run that writes the file keeps them instead of dropping them. That is what makes
repeated imports for several projects accumulate rather than overwrite each other.
Compare the two sides whenever you want to know where they stand. This writes nothing and exits non zero on any difference:
docanchor baseline --verify --import artifacts/*.sarif
It reports what only the analyzer found, what only the syntactic scan found, entries that neither side still produces, and findings that nothing parks. It is also the only place that can retire a C# entry, because it is the only place where both answers exist.
Coming from 1.10 or earlier. The key format changed, so an existing docanchor.baseline no
longer matches. Delete it and write it again from a build that produced the analyzer's findings:
rm docanchor.baseline
dotnet build /p:ErrorLog=artifacts/docanchor.sarif
docanchor baseline --import artifacts/*.sarif
docanchor baseline --verify --import artifacts/*.sarif
Add the policy and the baseline to the projects that should be checked:
<ItemGroup>
<AdditionalFiles Include="$(MSBuildThisFileDirectory)../../docanchor.lock" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)../../docanchor.policy" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)../../docanchor.baseline" />
</ItemGroup>
Configuration
docanchor.json in the repository root. Every field has a default.
{
"skeletonRoot": "docs/mdsource",
"outputRoot": "docs",
"fragmentRoot": "docs/fragments",
"scan": ["src/**/*.cs", "build/**/*.ps1", "db/**/*.sql"],
"exclude": ["**/bin/**", "**/obj/**"]
}
C# is read through Roslyn, so verbatim strings, raw string literals and interpolations never produce a false marker. PowerShell, SQL and Markdown are read by a scanner that knows their string literals, here strings and fenced blocks. The skeleton root, output root and fragment root are never scanned for definitions, whatever the patterns say.
Continuous integration
docanchor check
That single command validates every reference, every skeleton and every merged document, and returns a non zero exit code with a report naming file, line, id and remedy.
Conventions that make it work
The tool cannot enforce these, and ignoring them produces a system that is more work than plain prose:
- A fragment never knows its neighbours. No "as described above". A fragment must be readable on its own, because it may appear on several pages.
- Order lives only in the skeleton. Reordering a chapter means editing one file and never touching code.
- One fact, one place, never restated. A skeleton may weave and connect, but must not repeat a fragment's content in its own words. The moment a fact exists twice in prose, the system is worse than no system.
- A fragment is a paragraph to half a page. Smaller and you administer confetti, larger and it is a chapter that belongs in a skeleton.
- Deleting is not an option, moving is. When code that carried a decision goes away, move the fragment to a standalone file or mark it superseded. DA0003 exists to force that choice.
Non goals
No renderer and no site generator, Markdown out is the contract. No API documentation generation. No requirements process, approval workflow or traceability matrices. No text generation. No PDF, localisation or diagrams. No watch mode. No host languages beyond C#, PowerShell, SQL and Markdown.
Version semantics, that is detecting "the decision changed and the code did not follow", is specified but not built. It is the most valuable future feature and deliberately waits until the id convention has settled in real use.
Its own documentation
DocAnchor documents itself with DocAnchor. See docs for the merged result and
docs/mdsource for the skeletons that produce it. docanchor check runs in its own build.
License
MIT.
Learn more about Target Frameworks and .NET Standard.
This package has no dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.