Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
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
21 changes: 17 additions & 4 deletions src/ApiMark.Core/GlobFileCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@ 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 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 +84,19 @@ 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
if (File.Exists(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
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
95 changes: 95 additions & 0 deletions test/ApiMark.Core.Tests/GlobFileCollectorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,37 @@ public void GlobFileCollector_Collect_RelativeVhdPattern_FindsVhdFiles()
}
}

/// <summary>
/// Verifies that a relative literal path (no glob metacharacters) selects exactly
/// the named file and no others, resolved relative to the working directory.
/// </summary>
[Fact]
public void GlobFileCollector_Collect_RelativeLiteralFilePath_SelectsExactFile()
{
// Arrange: create an isolated temp directory with two .vhd files in a subdirectory
var tempDir = CreateTempDirectory();
try
{
var subDir = Path.Join(tempDir, "src");
Directory.CreateDirectory(subDir);
var targetFile = Path.Join(subDir, "design.vhd");
var otherFile = Path.Join(subDir, "other.vhd");
File.WriteAllText(targetFile, string.Empty);
File.WriteAllText(otherFile, string.Empty);

// Act: supply a relative literal path (no glob chars); workingDirectory is the parent
var result = GlobFileCollector.Collect(["src/design.vhd"], VhdlExtensions, tempDir);

// Assert: only the named file is returned
Assert.Single(result);
Assert.Equal(Path.GetFullPath(targetFile), result[0]);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}

/// <summary>
/// Verifies that a bare-star pattern <c>**/*</c> with VHDL extensions finds both
/// <c>.vhd</c> and <c>.vhdl</c> files but excludes files with other extensions.
Expand Down Expand Up @@ -147,6 +178,70 @@ public void GlobFileCollector_Collect_AbsolutePattern_FindsFiles()
}
}

/// <summary>
/// Verifies that an absolute literal path (no glob metacharacters) selects exactly
/// the named file and ignores all other files in the same directory.
/// </summary>
[Fact]
public void GlobFileCollector_Collect_LiteralAbsoluteFilePath_SelectsExactFile()
{
// Arrange: create a temp directory with two .vhd files
var tempDir = CreateTempDirectory();
try
{
var fileA = Path.Join(tempDir, "alpha.vhd");
var fileB = Path.Join(tempDir, "beta.vhd");
File.WriteAllText(fileA, string.Empty);
File.WriteAllText(fileB, string.Empty);

// Act: pass an absolute literal path — no glob metacharacters
var result = GlobFileCollector.Collect(
[fileA],
VhdlExtensions,
workingDirectory: Path.GetTempPath());

// Assert: only the explicitly named file is returned
Assert.Single(result);
Assert.Equal(Path.GetFullPath(fileA), result[0]);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}

/// <summary>
/// Verifies that an absolute literal exclusion path (no glob metacharacters) removes
/// exactly the named file from an otherwise full match set.
/// </summary>
[Fact]
public void GlobFileCollector_Collect_LiteralAbsoluteExclusionPath_RemovesExactFile()
{
// Arrange: create a temp directory with two .vhd files
var tempDir = CreateTempDirectory();
try
{
var fileA = Path.Join(tempDir, "alpha.vhd");
var fileB = Path.Join(tempDir, "beta.vhd");
File.WriteAllText(fileA, string.Empty);
File.WriteAllText(fileB, string.Empty);

// Act: include all .vhd files then remove one via a literal absolute exclusion path
var result = GlobFileCollector.Collect(
[$"{tempDir}/**/*.vhd", $"!{fileA}"],
VhdlExtensions,
workingDirectory: Path.GetTempPath());

// Assert: only the non-excluded file is returned
Assert.Single(result);
Assert.Equal(Path.GetFullPath(fileB), result[0]);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}

// =========================================================================
// Exclusion pattern tests
// =========================================================================
Expand Down
Loading
Loading