Skip to content

Support more C# syntax - #29

Merged
djungelorm merged 24 commits into
mainfrom
csharp-syntax
Oct 18, 2026
Merged

djungelorm merged 24 commits into
mainfrom
csharp-syntax

Conversation

@djungelorm

@djungelorm djungelorm commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Fills most of the gaps in the C# syntax the cs domain supports, and fixes several Breathe signatures that failed to parse or rendered wrong.

Syntax. Property, indexer and event accessors (init, private set, expression bodies, add/remove), full indexer parameters, tuples, generic constraints, operators and conversion operators, records and primary constructors, enum underlying types and values, and a delegate object type. Smaller additions are int?[], int**, List<int>.Enumerator, the file, scoped and ref readonly modifiers, parameter attributes such as [Attr(new[] {1})], and string literals containing commas in default values.

Breathe. Indexers failed to parse and lost their parameters. Records and classes with primary constructors rendered as functions. Generic classes showed Doxygen names such as Box-1-g<T>. The generic parameters of an enclosing class, and of a type in its own base list, were linked as types, which warns in nitpicky mode. Breathe passes delegates to the function directive, so :cs:func: and :cs:meth: also resolve delegates.

Testing. The lint check compiles the Breathe test sources with dotnet build, and CI installs the .NET 10 SDK in the lint job. Each feature has parser cases, rendering checks, and a directive in test-basic, plus a Breathe source where Doxygen emits the construct.

Operators get .NET metadata names as targets, such as :cs:meth:V.op_Addition``. Overloads still share one target id. Doxygen drops init, generic constraints and the members of records, so Breathe output omits them.

The C# sources in tests/roots/test-breathe were only read by Doxygen, and did not compile. A new test builds them with dotnet build, against stubs for the Unity types. Fix the missing usings, return values, out assignment and Dispose method it found. CI installs the .NET 10 SDK.
Properties, indexers and events now take a list of get, set, init, add and remove accessors in any order, each with optional accessibility or readonly modifiers. An expression-bodied property renders as a getter, and a semicolon after a property initializer is dropped. A property without accessors renders without braces.

Breathe output still shows { get; } for these. Doxygen records init as a getter only, and Breathe ignores the accessor modifiers and event accessors that Doxygen records.
Indexer parameters now take any type, modifiers such as params and in, and default values. Indexers also take an explicit interface name, an expression body, or no accessors.
Doxygen writes an indexer as a property named this[...], so Breathe passes it to the property directive, which failed to parse it. The property directive now renders these as indexers, with indexer ids, index entries and roles. Indexer parameters are no longer in a desc_addname node, since Breathe removes those from signatures.
Tuple types such as (int, string) and (int count, string name) now work as return, parameter, property and field types, and as generic arguments. Each element type is linked, and tuples nest one level deep. split_sig also respects parentheses, so a default value such as new Foo(1, 2) stays in one parameter.

Doxygen mangles tuple return, field and property types, so for Breathe only tuples in parameters and generic arguments render correctly.
Type and method signatures now take where clauses, such as where T : class, new(). The constraints render after the signature. Constraint types are linked, except the generic parameters and keywords such as notnull.

Doxygen drops constraints, so they do not appear in Breathe output.
The function directive now takes operator signatures such as static V operator +(V a, V b), including checked and compound assignment operators, and implicit and explicit conversion operators. Their targets use the .NET metadata names, such as op_Addition and op_Implicit, so a reference is :cs:meth:`V.op_Addition`.

Doxygen drops the explicit keyword from a conversion operator, so a conversion operator without implicit or explicit renders as explicit.
A new record directive and role cover records, record classes and record structs, and :cs:type: finds them too. Classes, structs and records take primary constructor parameters, and a base type in the base list takes constructor arguments, such as : Base(x).

Doxygen writes records and types with primary constructors as functions, so the function directive renders these as types. Doxygen also drops the members of these types.
An enum signature now takes modifiers and an underlying type, such as public Color : byte, and the underlying type is linked. An enumerator signature takes a value, such as Red = 1, and its name and target are the name alone.

Breathe writes enum values with their initializers when breathe_show_enumvalue_initializer is set, so the Breathe test project sets it.
A new delegate directive and role describe delegates, and :cs:type: finds them too. A function signature with the delegate modifier, which is how Breathe passes delegates from Doxygen, also describes a delegate. Generic parameters with variance modifiers, such as out TResult, are not linked as types.
Types such as int?[], string?[]? and int** now parse. TypeSig holds the suffixes as one string in place of the separate array and pointer fields.

Doxygen writes string?[]? as string??[], so the Breathe test source avoids that type.
…utes

Types and members take the file modifier, and parameters take scoped and ref readonly. Parameters also take attributes such as [NotNullWhen(true)], which render with the parameter modifiers. Doxygen keeps these in the parameter list, so they render from Breathe too.
A default value such as ", " or ',' stays in one parameter.
Types such as List<int>.Enumerator and Outer<T>.Inner<U> now parse. Each nested type links to its path without generic arguments, such as Outer.Inner. When the outer type is in the Microsoft docs, the nested type links to the page of the outer type with the nested path appended, such as List-1.Enumerator.

split_sig takes a separator, which splits the nested path on dots outside brackets.
A type's generic parameters, such as T in class List<T>, now render as plain text in the signatures of its members and nested types, where they were linked as types.
Doxygen names a generic C# type with its arity, such as Box-1-g for Box<T>, so Breathe output showed class Box-1-g<T> and qualified its members with Box-1-g. The suffix is now dropped from type names.
The compile does not depend on the Python, Sphinx or Breathe version, and ran once in every test environment. CI installs the .NET SDK in the lint job instead.
Breathe passes delegates to the function directive, which describes them as delegates. References to them with :cs:func: and :cs:meth: still resolve.
A directive with several signatures, such as a delegate followed by a method, described every signature after the delegate as a delegate.
@djungelorm
djungelorm marked this pull request as ready for review October 18, 2026 21:22
@djungelorm
djungelorm merged commit 6253129 into main Oct 18, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant