Skip to content
15 changes: 15 additions & 0 deletions .cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,28 @@
version: '0.2'
language: en
words:
- arities
- hideable
- Konst
- notnull
- funct
- misdescribes
- toavaloniaint
- transactstring
- transacttstring
- undoable
- apimark
- ApiMark
- Avalonia
- typeparam
- APFS
- bodyless
- newslot
- virtuals
- buildmark
- byref
- casefold
- casetwin
- Cecil
- cref
- crefs
Expand Down
31 changes: 31 additions & 0 deletions .reviewmark.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,17 @@ reviews:
- docs/verification/api-mark-core/path-helpers.md
- src/ApiMark.Core/PathHelpers.cs
- test/ApiMark.Core.Tests/PathHelpersTests.cs
- id: ApiMark-Core-FileNameSanitizer
title: Review that ApiMark Core FileNameSanitizer Implementation is Correct
context:
- docs/design/api-mark-core.md
- docs/reqstream/api-mark-core.yaml
paths:
- docs/reqstream/api-mark-core/file-name-sanitizer.yaml
- docs/design/api-mark-core/file-name-sanitizer.md
- docs/verification/api-mark-core/file-name-sanitizer.md
- src/ApiMark.Core/FileNameSanitizer.cs
- test/ApiMark.Core.Tests/FileNameSanitizerTests.cs
- id: ApiMark-Core-GlobFileCollector
title: Review that ApiMark Core GlobFileCollector Implementation is Correct
context:
Expand Down Expand Up @@ -267,6 +278,11 @@ reviews:
- docs/verification/api-mark-dot-net/dot-net-emitter.md
- src/ApiMark.DotNet/DotNetEmitter.cs
- test/ApiMark.DotNet.Tests/DotNetEmitterTests.cs
- test/ApiMark.DotNet.Fixtures/**/*.cs
- test/ApiMark.DotNet.Fixtures.External/**/*.cs
- test/ApiMark.DotNet.Tests/DotNetEmitterInheritedMembersTests.cs
- test/ApiMark.DotNet.Tests/DotNetEmitterModifierAndConstraintTests.cs
- test/ApiMark.DotNet.Tests/DotNetEmitterInheritanceDocsTests.cs
- id: ApiMark-DotNet-DotNetEmitterGradualDisclosure
title: Review that ApiMark DotNet DotNetEmitterGradualDisclosure Implementation
is Correct
Expand All @@ -281,6 +297,12 @@ reviews:
- test/ApiMark.DotNet.Fixtures/**/*.cs
- test/ApiMark.DotNet.Fixtures.Global/*.csproj
- test/ApiMark.DotNet.Tests/DotNetEmitterGradualDisclosureTests.cs
- test/ApiMark.DotNet.Tests/DotNetEmitterGradualDisclosureGenericsTests.cs
- test/ApiMark.DotNet.Tests/DotNetEmitterBreadcrumbTests.cs
- test/ApiMark.DotNet.Tests/DotNetEmitterFileNameLegalityTests.cs
- test/ApiMark.DotNet.Fixtures/LegalNames/LegalNameShapes.cs
- test/ApiMark.DotNet.Tests/DotNetEmitterOverloadIndexTests.cs
- test/ApiMark.DotNet.Tests/DotNetEmitterInheritanceDocsTests.cs
- '!test/**/obj/**'
- '!test/**/bin/**'
- id: ApiMark-DotNet-DotNetEmitterSingleFile
Expand All @@ -295,6 +317,8 @@ reviews:
- src/ApiMark.DotNet/DotNetEmitterSingleFile.cs
- test/ApiMark.DotNet.Fixtures/**/*.cs
- test/ApiMark.DotNet.Tests/DotNetEmitterSingleFileTests.cs
- test/ApiMark.DotNet.Tests/DotNetEmitterOverloadIndexTests.cs
- test/ApiMark.DotNet.Tests/DotNetEmitterInheritanceDocsTests.cs
- '!test/**/obj/**'
- '!test/**/bin/**'
- id: ApiMark-DotNet-XmlDocReader
Expand Down Expand Up @@ -343,6 +367,9 @@ reviews:
- docs/verification/api-mark-dot-net/type-link-resolver.md
- src/ApiMark.DotNet/TypeLinkResolver.cs
- test/ApiMark.DotNet.Tests/TypeLinkResolverTests.cs
- test/ApiMark.DotNet.Tests/DotNetEmitterExternalNestedLinkTests.cs
- test/ApiMark.DotNet.Fixtures/NestedExternalConsumer.cs
- test/ApiMark.DotNet.Fixtures.External/NestedExternalTypes.cs
- id: ApiMark-DotNet-CrefTargetResolver
title: Review that ApiMark DotNet CrefTargetResolver Implementation is Correct
context:
Expand All @@ -365,6 +392,10 @@ reviews:
- docs/verification/api-mark-dot-net/type-name-simplifier.md
- src/ApiMark.DotNet/TypeNameSimplifier.cs
- test/ApiMark.DotNet.Tests/TypeNameSimplifierTests.cs
- test/ApiMark.DotNet.Tests/DotNetEmitterGenericNamingTests.cs
- test/ApiMark.DotNet.Tests/DotNetEmitterExternalSignatureTests.cs
- test/ApiMark.DotNet.Fixtures/GenericNamingFixtures.cs
- test/ApiMark.DotNet.Fixtures/ExternalSignatureShapes.cs

# ApiMarkCpp - Specials
- id: ApiMark-Cpp-Architecture
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type page — consuming only as much context as the task requires.
- 🔍 **Gradual Disclosure** - Index → namespace → type → member detail
- 🗂️ **Multiple Output Formats** - Gradual-disclosure (file-per-type) or single `api.md` via `--format`
- 💡 **Example Code Blocks** - `<example><code>` (C#) and `@code`/`@endcode` (Doxygen) blocks rendered in output
- 🔷 **C#/.NET Support** - Mono.Cecil + XML documentation comments
- 🔷 **C#/.NET Support** - Mono.Cecil + XML documentation comments, inherited-member listings, cross-assembly `<inheritdoc/>` and `<see cref>` links
- 📝 **Documentation Coverage Enforcement** - opt-in `--enforce-docs` warns or fails the build on undocumented API surface (.NET, C++, VHDL)
- ➕ **C++ Support** - `clang -ast-dump=json` + Doxygen-style comments
- 🔶 **VHDL Support** - Entities, packages, and subprograms from ANTLR4 vhdl2008 grammar + `--!` doc comments
Expand Down
6 changes: 6 additions & 0 deletions docs/design/api-mark-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ flowchart TD
FileMarkdownWriter --> |implements| IMarkdownWriter
IMarkdownWriterFactory --> |creates| IMarkdownWriter
FileMarkdownWriterFactory --> |uses| PathHelpers
FileMarkdownWriterFactory --> |validates with| FileNameSanitizer
IApiEmitter --> |Emit uses| IMarkdownWriterFactory
IApiEmitter --> |Emit uses| EmitConfig
IApiEmitter --> |Emit uses| IContext
Expand Down Expand Up @@ -180,6 +181,11 @@ language-agnostic result types for a documentation-coverage scan.
cross-platform path-normalization helpers used both internally by ApiMarkCore
implementations and directly by other systems.

**FileNameSanitizer (public)**: Public static utility that converts declaration names
into file and folder name segments that are legal on Windows and Linux. Used by
`FileMarkdownWriterFactory` for validation and by the language emitters for naming. See
`docs/design/api-mark-core/file-name-sanitizer.md`.

- *Type*: In-process .NET public API.
- *Role*: Shared utility — `FileMarkdownWriterFactory` uses `SafePathCombine`
to validate path segments before creating directories or files;
Expand Down
16 changes: 10 additions & 6 deletions docs/design/api-mark-core/file-markdown-writer-factory.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,20 @@ Creates a new `FileMarkdownWriter` targeting `{outputDirectory}/{subFolder}/{nam
- *Preconditions*: `name` is not null, empty, or whitespace.
- *Algorithm*:
1. Reject `name` if null or whitespace; throw `ArgumentException`.
2. Compute the target directory: when `subFolder` is null or whitespace, use
2. Reject `name`, and every `/`-separated segment of `subFolder`, that fails
`FileNameSanitizer.IsLegalSegment`; throw `ArgumentException`.
3. Compute the target directory: when `subFolder` is null or whitespace, use
`_outputDirectory` directly; otherwise call `PathHelpers.SafePathCombine`
to combine `_outputDirectory` with `subFolder`.
3. Call `Directory.CreateDirectory(targetDirectory)` to ensure the directory
4. Call `Directory.CreateDirectory(targetDirectory)` to ensure the directory
tree exists before writing.
4. Compose the full file path by calling `PathHelpers.SafePathCombine` with
5. Compose the full file path by calling `PathHelpers.SafePathCombine` with
`targetDirectory` and `name + ".md"`.
5. Construct a `StreamWriter` over the file path using `UTF8Encoding` with
6. Construct a `StreamWriter` over the file path using `UTF8Encoding` with
`encoderShouldEmitUTF8Identifier: false` (UTF-8 without BOM) and `append: false`.
6. Wrap the stream writer in a new `FileMarkdownWriter` and return it.
- *Throws*: `ArgumentException` when `name` is null, empty, or whitespace.
7. Wrap the stream writer in a new `FileMarkdownWriter` and return it.
- *Throws*: `ArgumentException` when `name` is null, empty, or whitespace, or when `name`
or a `subFolder` segment is not a legal Windows and Linux name.

### Error Handling

Expand All @@ -75,6 +78,7 @@ Creates a new `FileMarkdownWriter` targeting `{outputDirectory}/{subFolder}/{nam

- **PathHelpers** — `SafePathCombine` is used to combine the output root with the
caller-supplied subFolder and the file name, preventing directory-traversal attacks.
- **FileNameSanitizer** — `IsLegalSegment` validates each folder segment and the file name.

### Callers

Expand Down
90 changes: 90 additions & 0 deletions docs/design/api-mark-core/file-name-sanitizer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
## FileNameSanitizer

![FileNameSanitizer Structure](ApiMarkCoreView.svg)

<!-- All sections below are MANDATORY. If a section does not apply, write
"N/A - {justification}" rather than removing it. -->

### Purpose

FileNameSanitizer is the single, auditable point that turns arbitrary declaration names
into file and folder name segments that are legal on both Windows and Linux. Every
language emitter calls it for every generated path segment, so the same name yields the
same segment on every platform and links always agree with the files they point to.

### Data Model

FileNameSanitizer is a `public static` class with no instance state. It holds three
immutable lookup sets: the characters replaced for portability, the Windows reserved
device names, and the hash length (eight hexadecimal characters). All members are pure
functions and are safe to call concurrently.

### Key Methods

**FileNameSanitizer.SanitizeSegment** (`public`): Converts a raw name to a legal segment.

- *Parameters*: `string rawName` — the raw name without separators or extension.
- *Returns*: `string` — a non-empty legal segment.
- *Rules*, applied in order:
1. `<` becomes `{` and `>` becomes `}` (the brace notation of XML documentation
identifiers); neither counts as a lossy substitution.
2. Each character from `: " | ? * \ /`, control characters, DEL, `[ ] ( ) # % ^`,
backtick, space, and a literal `{` or `}` becomes `-` (lossy). Literal braces are
lossy so they cannot collide with the mapped `<` and `>` (`A<B>` is `A{B}`, while
`A{B}` is `A-B--<hash>`).
3. Trailing dots become `-` (lossy). An empty result becomes `_`.
4. An input that is otherwise unchanged but already ends in `-` plus eight lowercase
hexadecimal characters, or whose part before the first dot is a reserved device name
followed by `-` (for example `CON-`), also counts as lossy, so it cannot equal the
output produced for another input.
5. When a lossy substitution occurred, `-` and the first eight lowercase hexadecimal
characters of the SHA-256 of the UTF-8 original are appended so distinct originals
stay distinct.
6. When the part before the first dot is a reserved device name (compared
case-insensitively, including the superscript digit variants), `-` is appended to
that part: `CON` becomes `CON-` and `Con.Foo` becomes `Con-.Foo`.
- *Throws*: `ArgumentNullException` when `rawName` is null.

**FileNameSanitizer.IsLegalSegment** (`public`): Returns whether a segment is non-empty,
has none of `< > | ? * : " / \` or control characters, does not end in a dot or space,
and is not a reserved device name.

**FileNameSanitizer.Hash** (`public`): Returns the eight-character hash suffix used above.

### Design Decisions

- **Distinctness guarantee**: the mapping is deterministic, and distinct inputs yield
distinct outputs except for a collision of the truncated 32-bit SHA-256 hash between two
lossy inputs. Outputs without a hash tail are produced only by inputs with no
substitution other than `<`/`>` and the reserved-name suffix, which is a one-to-one
mapping; hashed
outputs end in `-<hash>`, a form no output without a hash can have. The mapping is not
idempotent: a hashed output re-sanitized is hashed again.

- The mapping never calls `Path.GetInvalidFileNameChars`, which differs per operating
system; fixed rules keep output identical everywhere.
- `-` is the replacement and reserved-name suffix because it cannot appear in a C#
identifier; it is only otherwise produced by the generic-arity flattening (`Foo-2`).
Hash suffixes are applied only when information is lost so ordinary names stay readable.
- Case-insensitive collisions between distinct names cannot be solved by a stateless
function; callers that can see sibling names (the .NET emitter) handle them on top of
this unit.

### Error Handling

`SanitizeSegment` throws `ArgumentNullException` for a null argument and otherwise never
throws. `IsLegalSegment` accepts null and returns `false`.

### Dependencies

N/A - FileNameSanitizer depends only on the .NET base class library
(`System.Security.Cryptography`, `System.Text`).

### Callers

- **FileMarkdownWriterFactory** — validates every sub-folder segment and file name with
`IsLegalSegment`.
- **ApiMark.DotNet.DotNetEmitter**, **TypeLinkResolver** — sanitize member, type, and
namespace names.
- **ApiMark.Cpp.CppEmitter** and **ApiMark.Vhdl.VhdlEmitter** — `SanitizeFileName`
delegates to `SanitizeSegment`.
11 changes: 11 additions & 0 deletions docs/design/api-mark-cpp/cpp-emitter-gradual-disclosure.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ and dedicated pages for enums, type aliases, and grouped operator overloads.
| Operators (class) | `{Namespace}/{TypeName}/operators.md` |
| Operators (namespace) | `{Namespace}/operators.md` |

Every `{...}` segment is passed through `CppEmitter.SanitizeFileName` (namespace keys,
type, member, enum, alias, and function names; folders via `CppEmitter.SanitizePath`), so
`Foo<int>` is written as `Foo{int}.md` and a type named `Aux` as `Aux-.md`. Relative links
in tables use the same sanitized names while link text keeps the raw name. Each name is sanitized
exactly once: page writers receive the raw name and the emitter passes the already-sanitized
output folder (`CppEmitter.SanitizePath`) to `Linkify`. A declaration or member named `operators`
is written as `operators-` so it cannot overwrite the shared operators page, and a namespace
named `api` gets the root page `api-` (`CppEmitter.GetNamespacePageName`) so it cannot overwrite
the index. Only real operator overload names (`CppEmitter.IsOperatorName`) go to the operators page.
The global namespace key (`global`) is already legal and unchanged.

**CppEmitterGradualDisclosure instance fields** (private): state supplied at construction.

- `_emitter`: `CppEmitter` — parent emitter providing options, visibility helpers,
Expand Down
16 changes: 14 additions & 2 deletions docs/design/api-mark-cpp/cpp-emitter.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,20 @@ by `WriteFunctionContent`.

#### File and page helpers

- **SanitizeFileName** — replaces any invalid filesystem character reported by
`Path.GetInvalidFileNameChars()` with `_`.
- **SanitizeFileName** — delegates to `ApiMark.Core.FileNameSanitizer.SanitizeSegment` so
names are legal on Windows and Linux (reserved device names get a `-` suffix; template
brackets become `{`/`}`; other illegal characters become `-` plus a hash).
Tests are clang-free (synthetic models). A result equal to `operators` (case-insensitive) is
moved aside to `operators-`, mirroring the DotNet emitter, so a member or declaration page can
never overwrite the shared operators page of its folder.
- **GetNamespacePageName** — `SanitizeFileName` plus a `-` suffix when the result equals the reserved
root index name `api` (case-insensitive), so a namespace page never overwrites `api.md`.
- **IsOperatorName** — true only for real operator overload names (`operator` followed by a symbol,
space, or end), so identifiers such as `operators` or `operator_count` are ordinary members.
- **SanitizePath** — splits a `/`-separated folder or page-key path, sanitizes each
non-empty segment with `SanitizeFileName`, and rejoins them; returns an empty string
for an empty path. `WriteCombinedMemberPage` uses it for its folder and sanitizes its
file name exactly once (callers pass the raw lowercase key, which also stays the heading).
- **GetIncludePath** — derives the canonical `#include` path relative to the
longest matching public include root.
- **GetMemberBaseName** — returns the class name for constructors and the member
Expand Down
5 changes: 5 additions & 0 deletions docs/design/api-mark-cpp/cpp-type-link-resolver.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ private `_knownTypes` field.
`IndexOf(shortName, startIdx)` to locate the exact token, then reconstructs the
result as `original[..idx] + linked + original[(idx + shortName.Length)..]`.
A null `currentFolder` is treated as an empty string (root-level file).
The target page key is sanitized per `/` segment with `CppEmitter.SanitizePath`, exactly once.
`currentFolder` is the output folder exactly as the emitter writes it (already sanitized by the
emitter) and is used as is, never sanitized again, so links match the legal file names written
by the emitter without detours.
`knownTypes` values stay raw page keys.
- **FindPageKey** — performs exact qualified lookup first, then a short-name scan
that returns null when the short name is ambiguous.
- **StripQualifiers** — repeatedly removes leading and trailing `const`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ caller-supplied parameter that may differ from it.
concrete kinds above, so the fallback is a defensive default that keeps the
result well-formed rather than throwing.

`HasDocumentation` also returns true when `XmlDocReader.HasInheritDoc` reports a bare `<inheritdoc/>`, so
a member whose inherited summary cannot be resolved is not reported as undocumented.

### Error Handling

`Check` does not throw for missing documentation — every undocumented item is
Expand Down
Loading
Loading