Skip to content

Close the correctness and coverage gaps in the C# template object model #85

Description

@matt-edmondson

Summary

Follow-up to #84. The template model being ported from ktsu.Semantics was grown against one consumer, and it shows: several behaviours are correct only for the shapes that repository happens to emit, and one extension method is outright broken. Before the model is public API here, these need fixing — otherwise the first non-Semantics generator hits them immediately.

Each item below is independently verifiable; they can land as separate PRs.

1. ClassTemplate always emits a trailing semicolon

ClassTemplate.WriteMembersTo wraps every type body in ScopeWithTrailingSemicolon:

using (new ScopeWithTrailingSemicolon(codeBlocker))

so every generated type closes with };. That is valid but non-idiomatic for class, struct, and interface, and is only actually wanted for the record forms Semantics emits.

Fix: introduce a type-kind concept (class / struct / interface / record / record struct / enum) and choose the closing form from it, rather than deriving the kind from free-text Keywords and always appending ;.

2. ClassTemplateExtensions.AddInheritance is dead and broken

It writes the : separator and the base list, then falls through and writes : and the base list again:

codeBlocker.Write(string.Join(", ", baseAndInterfaces));
if (hasBaseClassOrInterfaces) { codeBlocker.NewLine(); }
if (string.IsNullOrEmpty(classTemplate.BaseClass) && classTemplate.Interfaces.Count == 0) { return codeBlocker; }
codeBlocker.Write(" : ");   // <-- second time

Nothing calls it — ClassTemplate.WriteBaseClassAndInterfacesTo is the live path. Fix: delete it (or fix it and make the private method delegate to it, so there is one implementation).

3. MethodTemplate and ConstructorTemplate duplicate their rendering

WriteParametersTo and WriteBodyTo are copy-pasted between the two, with one divergence that looks unintentional: the method version tests bodyLineCount == 0 for an empty body while the constructor version tests bodyString.Length == 0. bodyLineCount is never 0 — string.Split always returns at least one element — so the method's empty-body branch is unreachable and an empty method body renders differently from an empty constructor body.

Fix: hoist both into a shared base (or helper) and settle on one empty-body rule.

4. Body rendering splits on Environment.NewLine

Both body writers do:

string[] bodyLines = bodyString.Split([Environment.NewLine], StringSplitOptions.None);

which makes the single-line-vs-multi-line layout decision depend on the host OS. Pairs with #81 — split on the configured newline (or on any of \r\n / \n).

5. PropertyTemplate detects auto-accessors by delegate reference equality

public static readonly Action<CodeBlocker> AutoGet = (sw) => sw.Write("get;");
...
private bool CanUseShorthand => (GetterFactory == AutoGet || GetterFactory is null) && ...

Working out whether a property is an auto-property by comparing Action<CodeBlocker> instances by reference is fragile — a caller who writes their own sw => sw.Write("get;") gets the long form, and the fields have to stay single fixed instances forever (the code already carries a comment saying so).

Fix: model accessors as data — an enum or small AccessorTemplate record with Auto / Expression / Block plus an optional accessibility modifier — and keep the shorthand decision on the structure rather than on object identity. This also unlocks accessor-level modifiers (private set), which the current model cannot express.

6. Missing declaration kinds and modifiers

The model cannot currently express, and generators fall back to raw WriteLine for:

  • enum declarations and their members
  • interface declarations (only expressible as a ClassTemplate with the right keywords)
  • generic type parameters and arity — ClassTemplate.Name carries "Foo<T>" as literal text, and Constraints is a list of raw strings
  • operator declarations (public static X operator *(A a, B b)) — Semantics' QuantitiesGenerator emits hundreds of these entirely by hand
  • conversion operators (implicit / explicit)
  • events, indexers, and delegates
  • attributes on their own line — TemplateBaseExtensions.AddAttributes writes [Attr] inline, which produces very long lines for members carrying several attributes
  • positional record parameters

Not all of these need to land at once; the priority order for the known consumers is operators → generic parameters → enum → interface.

Acceptance criteria

Context

Part of ktsu-dev/Semantics#181. Depends on #84.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions