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.
Summary
Follow-up to #84. The template model being ported from
ktsu.Semanticswas 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.
ClassTemplatealways emits a trailing semicolonClassTemplate.WriteMembersTowraps every type body inScopeWithTrailingSemicolon:so every generated type closes with
};. That is valid but non-idiomatic forclass,struct, andinterface, and is only actually wanted for therecordforms 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-textKeywordsand always appending;.2.
ClassTemplateExtensions.AddInheritanceis dead and brokenIt writes the
:separator and the base list, then falls through and writes:and the base list again:Nothing calls it —
ClassTemplate.WriteBaseClassAndInterfacesTois the live path. Fix: delete it (or fix it and make the private method delegate to it, so there is one implementation).3.
MethodTemplateandConstructorTemplateduplicate their renderingWriteParametersToandWriteBodyToare copy-pasted between the two, with one divergence that looks unintentional: the method version testsbodyLineCount == 0for an empty body while the constructor version testsbodyString.Length == 0.bodyLineCountis never 0 —string.Splitalways 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.NewLineBoth body writers do:
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.
PropertyTemplatedetects auto-accessors by delegate reference equalityWorking out whether a property is an auto-property by comparing
Action<CodeBlocker>instances by reference is fragile — a caller who writes their ownsw => 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
AccessorTemplaterecord withAuto/Expression/Blockplus 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
WriteLinefor:enumdeclarations and their membersinterfacedeclarations (only expressible as aClassTemplatewith the right keywords)ClassTemplate.Namecarries"Foo<T>"as literal text, andConstraintsis a list of raw stringspublic static X operator *(A a, B b)) — Semantics'QuantitiesGeneratoremits hundreds of these entirely by handimplicit/explicit)TemplateBaseExtensions.AddAttributeswrites[Attr]inline, which produces very long lines for members carrying several attributesNot 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.