Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions docs/design/api-mark-cpp/cpp-generator.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,12 @@ glob and exclusion pattern strings that determine which header files contribute
documented public API. Gitignore-style semantics apply: patterns are evaluated
in order; the last matching pattern wins. Entries without a `!` prefix are
include patterns; entries with a `!` prefix are exclusion patterns (the `!`
is stripped before glob matching). When empty, all headers with recognized C++
extensions (`.h`, `.hpp`, `.hxx`, `.h++`) under all configured roots are
documented automatically without any pattern filtering.
is stripped before glob matching). Relative patterns are resolved against the
current working directory (CWD), matching the behavior of all other CLI glob tools
and allowing users to write patterns reflecting their project layout directly
(e.g. `include/**` when invoked from the project root). When empty, all headers
with recognized C++ extensions (`.h`, `.hpp`, `.hxx`, `.h++`) under all configured
roots are documented automatically without any pattern filtering.

Example — all headers except `detail/`, with one re-included:
`["include/**", "!include/detail/**", "include/detail/public_api.h"]`
Expand Down Expand Up @@ -140,8 +143,8 @@ a `CppEmitter` holding all parsed data.
Execution steps: call `CollectHeaderFiles()` which uses `GlobFileCollector.Collect()`
to build the selected-header set from `ApiHeaderPatterns` and `PublicIncludeRoots`;
when `ApiHeaderPatterns` is empty all headers under all roots are used directly;
when `ApiHeaderPatterns` is non-empty relative patterns are expanded against each
include root via `ExpandExplicitPatterns`; build Clang options from all configured
when `ApiHeaderPatterns` is non-empty relative patterns are resolved against the
current working directory (CWD) via `ExpandExplicitPatterns`; build Clang options from all configured
paths, defines, standard, and additional arguments; write a temporary combined
Comment thread
Malcolmnixon marked this conversation as resolved.
header that `#include`s all selected headers; invoke
`clang -Xclang -ast-dump=json -fparse-all-comments -fsyntax-only` on it, parse the
Expand All @@ -151,10 +154,12 @@ walking using the pre-built selected-header set; apply `Visibility` and
`CppEmitter` holding all parsed data.

**CppGenerator.ExpandExplicitPatterns** (private): Expands relative
`ApiHeaderPatterns` entries against each configured include root.
`ApiHeaderPatterns` entries against the current working directory (CWD).

- *Parameters*: `string cwd` — the absolute current working directory path, used as the
base for resolving relative patterns.
- *Returns*: `List<string>` — all patterns with relative entries resolved to
absolute paths under each root; absolute patterns passed through unchanged;
absolute paths under the CWD; absolute patterns passed through unchanged;
exclusion prefix `!` preserved.
Comment thread
Malcolmnixon marked this conversation as resolved.
Outdated

**CppEmitter.Emit** (implements `IApiEmitter`): Writes the full Markdown output tree using the
Expand Down
15 changes: 10 additions & 5 deletions docs/reqstream/api-mark-cpp/cpp-generator.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -217,15 +217,20 @@ sections:
title: >-
CppGenerator shall support both absolute and relative ApiHeaderPatterns
glob
patterns: relative patterns shall be expanded against each PublicIncludeRoot,
patterns: relative patterns shall be resolved from the current working
directory (CWD),
and absolute patterns shall be forwarded directly to GlobFileCollector.
justification: |
Relative patterns allow build authors to write root-agnostic patterns such as
"**/MyHeader.h" that resolve under every configured include root without
hard-coding directory paths. Absolute patterns allow headers on other drives or
CWD-relative patterns match how all other CLI glob tools work and allow
users to write patterns that reflect their project directory layout directly
(e.g. `include/**` when invoked from the project root) without needing to
hard-code absolute paths. Absolute patterns allow headers on other drives or
outside all configured roots to be included directly, supporting build layouts
where public headers are not all under a common tree.
tests: [GlobFileCollector_Collect_AbsolutePattern_FindsFiles]
tests:
- GlobFileCollector_Collect_AbsolutePattern_FindsFiles
- CppGenerator_Generate_ApiHeaderPatterns_CwdRelativePattern_OnlyMatchingFilesDocumented
- CppGenerator_Generate_ApiHeaderPatterns_CwdRelativeExclusionPattern_ExcludesMatchingFiles
- id: ApiMarkCpp-CppGenerator-EmitExternalTypesSection
title: CppGenerator shall emit an "External Types" section at the bottom
of each generated page that references at least one non-std external
Expand Down
17 changes: 16 additions & 1 deletion docs/verification/api-mark-cpp/cpp-generator.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ privileged configuration is required beyond a standard clang installation.
- When `ApiHeaderPatterns` is empty, all headers under configured PublicIncludeRoots with
recognized C++ extensions are documented. When patterns are configured, only headers whose last
matching pattern is a positive (non-`!`) pattern are included; gitignore last-match-wins
semantics apply.
semantics apply. Relative patterns are resolved from the current working directory so that users
can write patterns such as `include/**` when invoking the tool from the project root.
- Methods returning types documented within the same library emit Markdown links in the Returns
column of the Methods table; methods returning types not found in the library emit plain text.
- Types referenced in member signatures that are not documented within the library are tracked
Expand Down Expand Up @@ -262,6 +263,20 @@ Last-pattern-wins (gitignore) semantics are confirmed by this scenario, which is
output, confirming that the last matching pattern wins. This scenario is tested by
`CppGenerator_Generate_ApiHeaderPatterns_ExcludeWithoutReInclude_ExcludesHeader`.

**CWD-relative pattern selects only matching files**: Verifies that a relative pattern resolved
from the current working directory (e.g. a path of the form
`test/ApiMark.Cpp.Fixtures/include/fixtures/SampleClass.h` when invoked from the repo root)
selects only the file it names and no other headers. This scenario would fail with the old
include-root-expansion behavior because that behavior joined the pattern onto each include root,
producing a doubled path that matches nothing. This scenario is tested by
`CppGenerator_Generate_ApiHeaderPatterns_CwdRelativePattern_OnlyMatchingFilesDocumented`.

**CWD-relative exclusion pattern removes matching files**: Verifies that a `!`-prefixed relative
pattern resolved from the current working directory removes the named file from the documented
header set while leaving all other headers present. This scenario confirms that CWD-relative
resolution applies consistently to both inclusion and exclusion patterns. This scenario is tested
by `CppGenerator_Generate_ApiHeaderPatterns_CwdRelativeExclusionPattern_ExcludesMatchingFiles`.

**Intra-library return type emits a Markdown link in the Returns cell**: Verifies that when a
method returns a type that is itself documented within the same library, the Returns column in the
Methods table contains a Markdown link to that type's page rather than plain text. This gives AI
Expand Down
45 changes: 23 additions & 22 deletions src/ApiMark.Cpp/CppGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,10 @@ public IApiEmitter Parse(IContext context)
/// </para>
/// <para>
/// When patterns are provided, absolute patterns are forwarded to
/// <see cref="GlobFileCollector"/> unchanged. Relative patterns are expanded
/// against each <see cref="CppGeneratorOptions.PublicIncludeRoots"/> entry so
/// that callers can write root-agnostic patterns such as <c>**/MyHeader.h</c>
/// and have them resolved under every configured include root.
/// <see cref="GlobFileCollector"/> unchanged. Relative patterns are resolved against
/// the current working directory via <see cref="ExpandExplicitPatterns"/> so that
/// callers can write patterns such as <c>include/**</c> when invoking the tool from
/// the project root, matching the resolution behavior of all other CLI glob tools.
/// </para>
/// </remarks>
/// <exception cref="DirectoryNotFoundException">
Expand Down Expand Up @@ -183,10 +183,10 @@ private List<string> CollectHeaderFiles()
}
else
{
// Explicit patterns: forward absolute patterns unchanged; expand relative patterns
// against each include root so root-agnostic globs like "**/MyHeader.h" resolve
// correctly under every configured root without requiring callers to know the roots.
patterns = ExpandExplicitPatterns();
// Explicit patterns: forward absolute patterns unchanged; resolve relative patterns
// against the CWD so users can write patterns like "include/**" when invoked from
// the project root, matching how all other CLI glob tools behave.
patterns = ExpandExplicitPatterns(cwd);
}

return GlobFileCollector.Collect(patterns, headerExtensions, cwd).ToList();
Expand All @@ -197,17 +197,21 @@ private List<string> CollectHeaderFiles()
/// glob patterns ready for <see cref="GlobFileCollector.Collect"/>.
/// </summary>
/// <remarks>
/// Absolute patterns are forwarded unchanged. Relative patterns are resolved against
/// every entry in <see cref="CppGeneratorOptions.PublicIncludeRoots"/> so that
/// root-agnostic globs such as <c>**/MyHeader.h</c> match under all configured roots
/// without requiring callers to hard-code root paths. Exclusion prefixes (<c>!</c>)
/// are preserved on the expanded output entries.
/// Absolute patterns are forwarded unchanged. Relative patterns are resolved against the
/// current working directory (<paramref name="cwd"/>) so that patterns such as
/// <c>include/**</c> work correctly when the tool is invoked from the project root,
/// matching the resolution behavior of all other CLI glob tools. Exclusion prefixes
/// (<c>!</c>) are preserved on the expanded output entries.
/// </remarks>
/// <param name="cwd">
/// The absolute path of the current working directory, used as the base for resolving
/// relative patterns. Must be a fully-qualified, rooted path.
/// </param>
/// <returns>
/// An ordered list of absolute-path glob patterns with exclusion prefixes preserved,
/// ready to pass directly to <see cref="GlobFileCollector.Collect"/>.
/// </returns>
private List<string> ExpandExplicitPatterns()
private List<string> ExpandExplicitPatterns(string cwd)
{
var patterns = new List<string>();
foreach (var pattern in _options.ApiHeaderPatterns)
Expand All @@ -222,14 +226,11 @@ private List<string> ExpandExplicitPatterns()
}
else
{
// Relative pattern — expand against each include root so callers can write
// root-agnostic patterns without knowing the configured include paths
patterns.AddRange(
_options.PublicIncludeRoots.Select(root =>
{
var expanded = Path.Join(Path.GetFullPath(root), body);
return isExclusion ? "!" + expanded : expanded;
}));
// Relative pattern — resolve against the CWD so users can write patterns like
// "include/**" when invoking the tool from the project root, consistent with
// how all other CLI glob tools resolve relative patterns.
var expanded = Path.Join(cwd, body);
patterns.Add(isExclusion ? "!" + expanded : expanded);
Comment thread
Malcolmnixon marked this conversation as resolved.
Outdated
}
}

Expand Down
16 changes: 9 additions & 7 deletions src/ApiMark.Cpp/CppGeneratorOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ public sealed class CppGeneratorOptions
/// <remarks>
/// Each root serves two purposes: (1) it is passed to Clang as an <c>-I</c> path so
/// headers can find each other during AST parsing, and (2) it is the base directory
/// against which <see cref="ApiHeaderPatterns"/> globs are evaluated to select which
/// headers appear in the generated documentation. Must contain at least one entry.
/// from which a declaration's canonical <c>#include</c> path is derived. When
/// <see cref="ApiHeaderPatterns"/> is non-empty, relative patterns are resolved against
/// the current working directory rather than against these roots. Must contain at least
/// one entry.
/// </remarks>
public IReadOnlyList<string> PublicIncludeRoots { get; set; } = [];

Expand All @@ -41,11 +43,11 @@ public sealed class CppGeneratorOptions
/// <remarks>
/// <para>
/// Both absolute and relative glob patterns are supported. Relative patterns are
/// expanded against each <see cref="PublicIncludeRoots"/> entry so that callers
/// can write root-agnostic patterns such as <c>**/MyHeader.h</c> and have them
/// resolved under every configured include root. Absolute patterns determine their
/// own root from the non-glob path prefix, allowing headers outside any include
/// root or on other drives to be included directly.
/// resolved against the current working directory (CWD) so that callers can write
/// patterns such as <c>include/**</c> when invoking the tool from the project root,
/// matching the resolution behavior of all other CLI glob tools. Absolute patterns
/// determine their own root from the non-glob path prefix, allowing headers outside
/// any include root or on other drives to be included directly.
/// </para>
/// <para>
/// Patterns whose final segment is a bare <c>*</c> (e.g. <c>include/**/*</c>,
Expand Down
77 changes: 77 additions & 0 deletions test/ApiMark.Cpp.Tests/CppGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,83 @@ public void CppGenerator_Generate_ApiHeaderPatterns_TransitiveInclude_ExcludesNo
"Expected Circle to be absent: InheritanceClass.h was not selected by --api-headers");
}

/// <summary>
/// Validates that a CWD-relative <see cref="CppGeneratorOptions.ApiHeaderPatterns"/> entry
/// selects only the file it names when resolved from the current working directory, confirming
/// that relative patterns are resolved from the CWD and not from each include root.
/// </summary>
/// <remarks>
/// This test would fail with the old include-root-expansion behavior because that behavior
/// joined the pattern onto each include root, producing a doubled path that matches nothing.
/// The pattern is computed at runtime via <see cref="Path.GetRelativePath"/> so that the test
/// works on any developer machine or CI environment regardless of where the repo is checked out.
/// </remarks>
[Fact]
public void CppGenerator_Generate_ApiHeaderPatterns_CwdRelativePattern_OnlyMatchingFilesDocumented()
{
// Arrange: compute a CWD-relative path to SampleClass.h that is NOT root-agnostic.
// Using a non-**/ prefix means the pattern can only resolve correctly from the CWD.
var absoluteSampleClassPath = Path.Join(FixturePaths.GetFixtureNamespaceDir(), "SampleClass.h");
var cwdRelativePattern = Path.GetRelativePath(Directory.GetCurrentDirectory(), absoluteSampleClassPath);

var options = new CppGeneratorOptions
{
LibraryName = "Fixtures",
PublicIncludeRoots = [FixturePaths.GetFixtureIncludeDir()],
ApiHeaderPatterns = [cwdRelativePattern],
};
var factory = new InMemoryMarkdownWriterFactory();
var generator = new CppGenerator(options);

// Act
generator.Parse(new InMemoryContext()).Emit(factory, new EmitConfig(), new InMemoryContext());
Comment thread
Malcolmnixon marked this conversation as resolved.
Outdated

// Assert: SampleClass page must exist because its header was selected by the CWD-relative pattern
Assert.True(
factory.Writers.ContainsKey("fixtures/SampleClass"),
"Expected SampleClass page when selected by a CWD-relative pattern");

// Assert: SampleStatus page must not exist because SampleEnum.h was not selected
Assert.False(
factory.Writers.ContainsKey("fixtures/SampleStatus"),
"Expected SampleStatus to be absent when not matched by the CWD-relative include pattern");
}

/// <summary>
/// Validates that a CWD-relative exclusion pattern removes the named file from the documented
/// header set while leaving all other headers present, confirming that CWD-relative resolution
/// applies to both inclusion and exclusion patterns.
/// </summary>
[Fact]
public void CppGenerator_Generate_ApiHeaderPatterns_CwdRelativeExclusionPattern_ExcludesMatchingFiles()
{
// Arrange: compute a CWD-relative path to SampleClass.h and use it as an exclusion.
var absoluteSampleClassPath = Path.Join(FixturePaths.GetFixtureNamespaceDir(), "SampleClass.h");
var cwdRelativeExclusion = "!" + Path.GetRelativePath(Directory.GetCurrentDirectory(), absoluteSampleClassPath);

var options = new CppGeneratorOptions
{
LibraryName = "Fixtures",
PublicIncludeRoots = [FixturePaths.GetFixtureIncludeDir()],
ApiHeaderPatterns = ["**/*", cwdRelativeExclusion],
};
var factory = new InMemoryMarkdownWriterFactory();
var generator = new CppGenerator(options);

// Act
generator.Parse(new InMemoryContext()).Emit(factory, new EmitConfig(), new InMemoryContext());
Comment thread
Malcolmnixon marked this conversation as resolved.
Outdated

// Assert: SampleClass page must not exist because its header was excluded by the CWD-relative exclusion pattern
Assert.False(
factory.Writers.ContainsKey("fixtures/SampleClass"),
"Expected SampleClass to be absent when its header is excluded by a CWD-relative exclusion pattern");

// Assert: SampleStatus page must still exist because SampleEnum.h was not excluded
Assert.True(
factory.Writers.ContainsKey("fixtures/SampleStatus"),
"Expected SampleStatus to be present when only SampleClass.h is excluded by CWD-relative pattern");
}

/// <summary>
/// Validates that the type page for a <c>final</c> class contains the <c>final</c>
/// keyword in its signature block so that AI readers immediately know the class
Expand Down
Loading