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
17 changes: 16 additions & 1 deletion .github/codeql-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,20 @@ paths-ignore:
- '**/bin/**'
# Query filters to disable specific queries for certain paths
query-filters:
# Suppress warnings in all generated build output under obj/
# Suppress warnings in generated build output under obj/ (for example the source-generated
# regex implementations), which cannot be modified.
- exclude:
id: cs/useless-assignment-to-local
paths: ['**/obj/**/*.cs']
- exclude:
id: cs/missed-ternary-operator
paths: ['**/obj/**/*.cs']
- exclude:
id: cs/useless-cast-to-self
paths: ['**/obj/**/*.cs']
- exclude:
id: cs/unused-label
paths: ['**/obj/**/*.cs']
- exclude:
id: cs/catch-of-all-exceptions
paths: [src/ApiMark.Tool/Cli/Context.cs, src/ApiMark.Tool/Program.cs]
Expand All @@ -26,3 +33,11 @@ query-filters:
- exclude:
id: cs/missed-readonly-modifier
paths: ['**/VhdlAst/Antlr/**']
# Test fixtures deliberately model C# API shapes (explicit IEquatable without Equals(object),
# `new` field hiding) so the emitters can be tested against them.
- exclude:
id: cs/class-missing-equals
paths: ['test/ApiMark.DotNet.Fixtures*/**']
- exclude:
id: cs/field-masks-base-field
paths: ['test/ApiMark.DotNet.Fixtures*/**']
6 changes: 3 additions & 3 deletions docs/design/api-mark-cpp/documentation-coverage-checker.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ lack a non-empty Doxygen summary.
recursion depth, which does not re-apply the filter to nested classes).

**CheckClass** (private): Checks a single class for a missing summary, then
checks its visible members and fields (tier-filtered via `IsVisibleMember`),
its type aliases, and — unconditionally, since already pre-filtered to
public — its nested classes.
delegates to `CheckClassMembers` and `CheckClassFields` (visible members and fields,
tier-filtered via `IsVisibleMember`), then checks its type aliases and —
unconditionally, since already pre-filtered to public — its nested classes.

**CheckFunction / CheckEnum / CheckTypeAlias** (private): Check a single free
function, enum (and its values), or type alias for a missing summary.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,10 @@ for a single method (or one overload) onto the caller-supplied writer.
**WriteMethodOverloadPage** (private static): Writes a single shared Markdown page for a
pure method overload group (all methods sharing the same exact case-sensitive sanitized file name).

- *Parameters*: `IMarkdownWriterFactory factory`, `string namespaceName`,
`string namespaceFolderPath`, `TypeDefinition type`, `IReadOnlyList<MethodDefinition>
overloads` — ordered list of overload methods (at least one element),
`XmlDocReader xmlDocs`, `TypeLinkResolver resolver`.
- *Parameters*: `TypePageWriteContext ctx` — the type-page context supplying the factory,
namespace name, namespace folder path, type, XML docs, resolver, and cross-reference
link context (re-scoped to the overload page's folder), and `List<MethodDefinition>
overloads` — ordered list of overload methods (at least one element).
- *Returns*: `void`
- *Algorithm*: Computes `sanitizedName = BuildMethodFileName(overloads[0], type)`;
creates `{namespaceFolderPath}/{GetTypeFileName(type)}/{sanitizedName}.md` via the
Expand Down Expand Up @@ -149,10 +149,10 @@ in table cells.
**WriteTypeOperatorsPage** (private static): Writes the `operators.md` page for
a type that defines operator overloads.

- *Parameters*: `IMarkdownWriterFactory factory`, `string namespaceName`,
`string namespaceFolderPath`, `TypeDefinition type`,
`IReadOnlyList<MethodDefinition> operatorMethods` — the operator methods to
document, `XmlDocReader xmlDocs`, `TypeLinkResolver resolver`.
- *Parameters*: `TypePageWriteContext ctx` — the type-page context supplying the factory,
namespace name, namespace folder path, type, XML docs, resolver, and cross-reference
link context (re-scoped to the operators page's folder), and
`IReadOnlyList<MethodDefinition> operatorMethods` — the operator methods to document.
- *Returns*: `void`
- *Algorithm*: Creates `{namespaceFolderPath}/{GetTypeFileName(type)}/operators.md`
via the factory; writes an H1 heading; for each operator method writes an H2
Expand Down
18 changes: 4 additions & 14 deletions src/ApiMark.Core/FileNameRules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
/// </param>
/// <returns>A legal, non-empty segment.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="rawName"/> is null.</exception>
public static string Escape(string rawName, out bool transformed)

Check warning on line 73 in src/ApiMark.Core/FileNameRules.cs

View workflow job for this annotation

GitHub Actions / Build / Build ubuntu-latest

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

Check warning on line 73 in src/ApiMark.Core/FileNameRules.cs

View workflow job for this annotation

GitHub Actions / Build / Build windows-latest

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

Check warning on line 73 in src/ApiMark.Core/FileNameRules.cs

View workflow job for this annotation

GitHub Actions / Build / Build macos-latest

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.
{
ArgumentNullException.ThrowIfNull(rawName);

Expand All @@ -80,20 +80,10 @@
var builder = new StringBuilder(normalized.Length);
foreach (var c in normalized)
{
if (c is '{' or '}')
{
builder.Append(c);
transformed = true;
}
else if (IsReplacedCharacter(c))
{
builder.Append(ReplacementCharacter);
transformed = true;
}
else
{
builder.Append(c);
}
var isStructural = c is '{' or '}';
var isReplaced = !isStructural && IsReplacedCharacter(c);
builder.Append(isReplaced ? ReplacementCharacter : c);
transformed |= isStructural || isReplaced;
}

// Windows silently strips trailing dots and spaces, so they must not appear.
Expand Down
146 changes: 79 additions & 67 deletions src/ApiMark.Cpp/CppAst/ClangAstParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1290,18 +1290,11 @@ private void ParseClass(
? CppAccessibility.Public
: CppAccessibility.Private;

var members = new List<CppFunction>();
var fields = new List<CppField>();
var baseTypes = new List<CppBaseType>();
var nestedClasses = new List<CppClass>();
var typeAliases = new List<CppTypeAlias>();
CppDocComment? doc = null;

// Deprecated if isDeprecated flag is set; DeprecatedAttr in inner also triggers
var isDeprecated = node.TryGetProperty(IsDeprecatedProperty, out var dep) && dep.GetBoolean();

// Final if FinalAttr appears in the inner node array
var isFinal = false;
var location = GetCurrentSourceLocation(node);

// Clang 18+ surfaces base class information in a top-level "bases" array on the
Expand All @@ -1310,39 +1303,72 @@ private void ParseClass(
// Both paths are retained so the parser works across clang versions.
var hasTopLevelBases = CollectTopLevelBaseTypes(node, baseTypes);

var state = new ClassBodyState(baseTypes, hasTopLevelBases, currentAccess, isDeprecated);

if (node.TryGetProperty(InnerProperty, out var inner))
{
foreach (var child in inner.EnumerateArray())
{
ProcessClassBodyChild(
child,
nsQualName,
name,
members,
fields,
nestedClasses,
typeAliases,
baseTypes,
hasTopLevelBases,
ref currentAccess,
ref isDeprecated,
ref isFinal,
ref doc);
ProcessClassBodyChild(child, nsQualName, name, state);
}
}

return new CppClass(
name,
baseTypes,
state.BaseTypes,
templateParams ?? [],
members,
fields,
nestedClasses,
typeAliases,
isDeprecated,
isFinal,
state.Members,
state.Fields,
state.NestedClasses,
state.TypeAliases,
state.IsDeprecated,
state.IsFinal,
location,
doc);
state.Doc);
}

/// <summary>
/// Accumulators and mutable state shared across the walk of a single class body.
/// </summary>
/// <param name="baseTypes">Base types, pre-populated from the top-level <c>bases</c> array when present.</param>
/// <param name="hasTopLevelBases">Whether base types were already collected from the top-level <c>bases</c> array.</param>
/// <param name="access">The initial access level (public for <c>struct</c>, private for <c>class</c>).</param>
/// <param name="isDeprecated">The initial deprecated flag.</param>
private sealed class ClassBodyState(
List<CppBaseType> baseTypes,
bool hasTopLevelBases,
CppAccessibility access,
bool isDeprecated)
{
/// <summary>Gets the base types, pre-populated from the top-level <c>bases</c> array when present.</summary>
public List<CppBaseType> BaseTypes { get; } = baseTypes;

/// <summary>Gets a value indicating whether base types were already collected from the top-level <c>bases</c> array.</summary>
public bool HasTopLevelBases { get; } = hasTopLevelBases;

/// <summary>Gets the parsed constructors, destructors, and methods.</summary>
public List<CppFunction> Members { get; } = [];

/// <summary>Gets the parsed fields.</summary>
public List<CppField> Fields { get; } = [];

/// <summary>Gets the parsed nested classes.</summary>
public List<CppClass> NestedClasses { get; } = [];

/// <summary>Gets the parsed class-scoped type aliases.</summary>
public List<CppTypeAlias> TypeAliases { get; } = [];

/// <summary>Gets or sets the current access level, updated by <c>AccessSpecDecl</c> children.</summary>
public CppAccessibility Access { get; set; } = access;

/// <summary>Gets or sets a value indicating whether the class is deprecated.</summary>
public bool IsDeprecated { get; set; } = isDeprecated;

/// <summary>Gets or sets a value indicating whether the class is final.</summary>
public bool IsFinal { get; set; }

/// <summary>Gets or sets the class documentation comment.</summary>
public CppDocComment? Doc { get; set; }
}

/// <summary>
Expand Down Expand Up @@ -1384,30 +1410,12 @@ private static bool CollectTopLevelBaseTypes(JsonElement node, List<CppBaseType>
/// <param name="child">The class-body child JSON node to process.</param>
/// <param name="nsQualName">The qualified name of the enclosing namespace.</param>
/// <param name="className">The name of the enclosing class.</param>
/// <param name="members">Accumulator list that receives parsed constructors/destructors/methods.</param>
/// <param name="fields">Accumulator list that receives parsed fields.</param>
/// <param name="nestedClasses">Accumulator list that receives parsed nested classes.</param>
/// <param name="typeAliases">Accumulator list that receives parsed class-scoped type aliases.</param>
/// <param name="baseTypes">Accumulator list that receives base types from legacy <c>CXXBaseSpecifier</c> children.</param>
/// <param name="hasTopLevelBases">Whether base types were already collected from the top-level <c>bases</c> array.</param>
/// <param name="currentAccess">The current access level, updated in place by <c>AccessSpecDecl</c> children.</param>
/// <param name="isDeprecated">Whether the class is deprecated, set in place by a <c>DeprecatedAttr</c> child.</param>
/// <param name="isFinal">Whether the class is final, set in place by a <c>FinalAttr</c> child.</param>
/// <param name="doc">The class documentation comment, set in place by a <c>FullComment</c> child.</param>
/// <param name="state">The accumulators and mutable state shared across the class body walk.</param>
private void ProcessClassBodyChild(
JsonElement child,
string nsQualName,
string className,
List<CppFunction> members,
List<CppField> fields,
List<CppClass> nestedClasses,
List<CppTypeAlias> typeAliases,
List<CppBaseType> baseTypes,
bool hasTopLevelBases,
ref CppAccessibility currentAccess,
ref bool isDeprecated,
ref bool isFinal,
ref CppDocComment? doc)
ClassBodyState state)
{
UpdateCurrentFile(child);
var kind = GetKind(child);
Expand All @@ -1416,18 +1424,18 @@ private void ProcessClassBodyChild(
{
case "AccessSpecDecl":
// Transition the current access level for all following members
currentAccess = ParseAccessSpec(child);
state.Access = ParseAccessSpec(child);
break;

case "CXXConstructorDecl":
case "CXXDestructorDecl":
case "CXXMethodDecl":
{
// ParseMethod returns null for destructors and implicit members
var method = ParseMethod(child, currentAccess);
var method = ParseMethod(child, state.Access);
if (method != null)
{
members.Add(method);
state.Members.Add(method);
}

break;
Expand All @@ -1437,41 +1445,41 @@ private void ProcessClassBodyChild(
case "VarDecl":
{
// VarDecl covers static constexpr members inside the class body
var field = ParseField(child, currentAccess);
var field = ParseField(child, state.Access);
if (field != null)
{
fields.Add(field);
state.Fields.Add(field);
}

break;
}

case CxxRecordDeclKind:
HandleNestedCxxRecord(child, nsQualName, className, currentAccess, nestedClasses);
HandleNestedCxxRecord(child, nsQualName, className, state.Access, state.NestedClasses);
break;

case "ClassTemplateDecl":
HandleClassTemplate(child, nsQualName, className, currentAccess, nestedClasses);
HandleClassTemplate(child, nsQualName, className, state.Access, state.NestedClasses);
break;

case "TypeAliasDecl":
HandleTypeAliasInClass(child, currentAccess, typeAliases);
HandleTypeAliasInClass(child, state.Access, state.TypeAliases);
break;

case "CXXBaseSpecifier":
HandleCxxBaseSpecifier(child, hasTopLevelBases, baseTypes);
HandleCxxBaseSpecifier(child, state.HasTopLevelBases, state.BaseTypes);
break;

case FullCommentKind:
doc = ParseFullComment(child);
state.Doc = ParseFullComment(child);
break;

case DeprecatedAttrKind:
isDeprecated = true;
state.IsDeprecated = true;
break;

case "FinalAttr":
isFinal = true;
state.IsFinal = true;
break;
}
}
Expand Down Expand Up @@ -2048,13 +2056,9 @@ private void ParseTypeAlias(JsonElement node, string nsQualName)

// VarDecl inside a class body is always a static member
var kind = GetKind(node);
var isStatic = kind == "VarDecl" ||
(node.TryGetProperty("storageClass", out var sc) && sc.GetString() == "static");
var isStatic = kind == "VarDecl" || GetStorageClass(node) == "static";

var typeName = node.TryGetProperty("type", out var t) &&
t.TryGetProperty(QualTypeProperty, out var qt)
? qt.GetString() ?? string.Empty
: string.Empty;
var typeName = GetQualType(node);

var isDeprecated = node.TryGetProperty(IsDeprecatedProperty, out var dep) && dep.GetBoolean();
var isConstexpr = node.TryGetProperty("constexpr", out var constexprProp) && constexprProp.GetBoolean();
Expand Down Expand Up @@ -2861,6 +2865,14 @@ private static string GetQualType(JsonElement node)
: string.Empty;
}

/// <summary>Returns the <c>storageClass</c> string from a JSON AST node, or <see langword="null"/> when absent.</summary>
/// <param name="node">The JSON element to inspect.</param>
/// <returns>The storage class, or <see langword="null"/>.</returns>
private static string? GetStorageClass(JsonElement node)
{
return node.TryGetProperty("storageClass", out var sc) ? sc.GetString() : null;
}

// =========================================================================
// Namespace builder management
// =========================================================================
Expand Down
2 changes: 1 addition & 1 deletion src/ApiMark.Cpp/CppEmitterGradualDisclosure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,7 @@ private void WriteFreeFunctionPage(
IMarkdownWriterFactory factory,
string nsRegistryKey,
string nsDisplayName,
IReadOnlyList<CppFunction> overloads,
List<CppFunction> overloads,
CppTypeLinkResolver cppResolver)
{
var fnKey = CppNames.FunctionKey(nsRegistryKey, overloads[0].Name);
Expand Down
5 changes: 3 additions & 2 deletions src/ApiMark.Cpp/CppNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,9 +247,10 @@ private static void RegisterClass(SymbolNameRegistry registry, string parentKey,
RegisterClass(registry, key, nested);
}

foreach (var member in cls.Members.Where(m => m.IsConstructor || !CppEmitter.IsOperatorName(m.Name)))
foreach (var name in cls.Members
.Where(m => m.IsConstructor || !CppEmitter.IsOperatorName(m.Name))
.Select(m => CppEmitter.GetMemberBaseName(m, cls.Name)))
{
var name = CppEmitter.GetMemberBaseName(member, cls.Name);
RegisterOnce(registry, MemberKey(key, name), key, name, SymbolKind.Member);
}

Expand Down
Loading
Loading