Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
22 changes: 10 additions & 12 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,10 @@ 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 patterns are forwarded directly to
`GlobFileCollector`, which resolves relative patterns against
`CppGeneratorOptions.WorkingDirectory` (or the process CWD when null) and resolves
absolute patterns from their own root prefix; 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 @@ -150,13 +155,6 @@ walking using the pre-built selected-header set; apply `Visibility` and
`IncludeDeprecated` filters; delete the temporary combined header file; return a
`CppEmitter` holding all parsed data.

**CppGenerator.ExpandExplicitPatterns** (private): Expands relative
`ApiHeaderPatterns` entries against each configured include root.

- *Returns*: `List<string>` — all patterns with relative entries resolved to
absolute paths under each root; absolute patterns passed through unchanged;
exclusion prefix `!` preserved.

**CppEmitter.Emit** (implements `IApiEmitter`): Writes the full Markdown output tree using the
format specified by `config.Format`.

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
36 changes: 26 additions & 10 deletions src/ApiMark.Core/GlobFileCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@ public static class GlobFileCollector
/// <c>**/*.h</c>), all results are taken as-is without additional filtering.
/// </para>
/// <para>
/// Non-existent pattern roots are silently skipped and contribute no files.
/// The method never throws for missing directories.
/// When an absolute pattern contains no glob metacharacters it is treated as a
/// literal file path: if the file exists and its extension appears in
/// <paramref name="languageExtensions"/> it is added to or removed from the result
/// set directly, without any directory traversal. Non-existent literal paths and
/// non-existent pattern roots are silently skipped; the method never throws for
/// missing files or directories.
/// </para>
/// </remarks>
/// <param name="patterns">
Expand Down Expand Up @@ -81,9 +85,20 @@ public static IReadOnlyList<string> Collect(
// Determine the filesystem root and the glob tail for this pattern
var (root, globTail) = ParsePattern(patternBody, workingDirectory);

if (globTail.Length == 0 || !Directory.Exists(root))
if (globTail.Length == 0)
{
// No glob portion — root is a literal path; select it if it exists as a file
// and its extension is in the allowed set (same gate applied to glob results)
if (File.Exists(root) && extensions.Contains(Path.GetExtension(root)))
{
AccumulateResults(collected, [Path.GetFullPath(root)], isExclusion);
}

continue;
}
Comment thread
Malcolmnixon marked this conversation as resolved.

if (!Directory.Exists(root))
{
// No glob portion, or the root directory does not exist — skip silently
continue;
}

Expand Down Expand Up @@ -163,12 +178,13 @@ private static bool HasBareStarFinalSegment(string globTail)
/// <see cref="Matcher"/>.
/// </summary>
/// <remarks>
/// For absolute patterns the root is the longest non-glob path prefix — segments
/// are included until the first one containing a glob metacharacter (<c>*</c>,
/// For fully-qualified absolute patterns the root is the longest non-glob path prefix —
/// segments are included until the first one containing a glob metacharacter (<c>*</c>,
/// <c>?</c>, <c>[</c>, <c>{</c>). The remainder from the last directory separator
/// before the first glob metacharacter to the end is the glob tail.
/// For relative patterns the root is <paramref name="workingDirectory"/> and the
/// entire pattern body is the glob tail.
/// For all other patterns (relative, or rooted-but-not-fully-qualified such as
/// <c>C:foo.h</c> or <c>\foo.h</c> on Windows) the root is <paramref name="workingDirectory"/>
/// and the entire pattern body is the glob tail, so they resolve against the working directory.
/// </remarks>
/// <param name="patternBody">The pattern with any leading <c>!</c> prefix already stripped.</param>
/// <param name="workingDirectory">Absolute path used as the root for relative patterns.</param>
Expand All @@ -178,13 +194,13 @@ private static bool HasBareStarFinalSegment(string globTail)
/// </returns>
private static (string Root, string GlobTail) ParsePattern(string patternBody, string workingDirectory)
{
if (Path.IsPathRooted(patternBody))
if (Path.IsPathFullyQualified(patternBody))
{
// Normalize backslashes to forward slashes for uniform metacharacter scanning
return SplitAbsolutePattern(patternBody.Replace('\\', '/'));
}

// Relative pattern — root is the configured working directory; tail is the full pattern body
// Relative or rooted-but-not-fully-qualified pattern — resolve against workingDirectory
return (workingDirectory, patternBody);
}

Expand Down
62 changes: 8 additions & 54 deletions src/ApiMark.Cpp/CppGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,10 @@ public IApiEmitter Parse(IContext context)
/// which restricts results to files with recognized C++ header extensions.
/// </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.
/// When patterns are provided, they are forwarded directly to
/// <see cref="GlobFileCollector"/>, which resolves relative patterns against
/// <see cref="CppGeneratorOptions.WorkingDirectory"/> and resolves absolute patterns
/// from their own root prefix.
/// </para>
/// </remarks>
/// <exception cref="DirectoryNotFoundException">
Expand All @@ -162,7 +161,7 @@ public IApiEmitter Parse(IContext context)
private List<string> CollectHeaderFiles()
{
var headerExtensions = new[] { ".h", ".hpp", ".hxx", ".h++" };
var cwd = Path.GetFullPath(Directory.GetCurrentDirectory());
var cwd = Path.GetFullPath(_options.WorkingDirectory ?? Directory.GetCurrentDirectory());

List<string> patterns;

Expand All @@ -183,59 +182,14 @@ 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: pass directly to GlobFileCollector, which resolves relative
// patterns against cwd and absolute patterns from their own root prefix.
patterns = [.. _options.ApiHeaderPatterns];
}

return GlobFileCollector.Collect(patterns, headerExtensions, cwd).ToList();
}

/// <summary>
/// Expands the explicit <see cref="CppGeneratorOptions.ApiHeaderPatterns"/> into absolute
/// 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.
/// </remarks>
/// <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()
{
var patterns = new List<string>();
foreach (var pattern in _options.ApiHeaderPatterns)
{
var isExclusion = pattern.StartsWith('!');
var body = isExclusion ? pattern.Substring(1).Trim() : pattern.Trim();

if (Path.IsPathRooted(body))
{
// Absolute pattern — pass through unchanged
patterns.Add(pattern);
}
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;
}));
}
}

return patterns;
}

// =========================================================================
// Error checking
// =========================================================================
Expand Down
27 changes: 20 additions & 7 deletions src/ApiMark.Cpp/CppGeneratorOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ 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
/// <see cref="WorkingDirectory"/> (or the process working directory when
/// <see cref="WorkingDirectory"/> is <see langword="null"/>) rather than against these
/// roots. Must contain at least one entry.
/// </remarks>
public IReadOnlyList<string> PublicIncludeRoots { get; set; } = [];

Expand All @@ -41,11 +44,12 @@ 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 <see cref="WorkingDirectory"/> (or the process working directory
/// when <see cref="WorkingDirectory"/> is <see langword="null"/>), 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 Expand Up @@ -113,4 +117,13 @@ public sealed class CppGeneratorOptions
/// located automatically (PATH, xcrun on macOS, vswhere on Windows).
/// </summary>
public string? ClangPath { get; set; }

/// <summary>
/// Gets or sets the directory used as the root for resolving relative
/// <see cref="ApiHeaderPatterns"/> entries. Defaults to <see langword="null"/>,
/// which means <see cref="Directory.GetCurrentDirectory"/> is used at parse time.
/// Set this in tests or programmatic callers to anchor patterns to a known directory
/// without mutating the process working directory.
/// </summary>
public string? WorkingDirectory { get; set; }
}
Loading
Loading