Repository navigation
Description
What's wrong
#128 (fixed by #140) made two paths validate before writing: a property with no accessors, and an unknown OperatorKind. Three related paths are still unfixed. Two of them, which #128 itself named, write output first and throw afterwards. The third emits code that does not compile and raises no error.
- Unknown
ClassTemplate.Kind.ClassTemplate.WriteTo(CodeBlocker/Templates/ClassTemplate.cs:67-69) callsbase.WriteTofirst, which writes the doc comment, attributes and modifiers. Only after that does it callTypeKindKeywords.For(Kind), which throwsArgumentOutOfRangeException. For a nested class, the parent'susing Scopethen closes onto the unfinished line. - Unknown
AccessorTemplate.Kind.PropertyTemplate.WriteTowritesType Name, a newline and{, then indents. Only then doesAccessorTemplate.WriteToreachdefault: throw new InvalidOperationException("Unknown accessor kind ...")(AccessorTemplate.cs:107). - Expression accessor with a null body.
new AccessorTemplate { Kind = AccessorKind.Expression }leavesBodyFactory == null, and object-initializer syntax allows it.AccessorTemplate.cs:92-95passes that null toTemplateRendering.WriteExpressionBody, which writesget => ;. The output does not compile and nothing reports it.
Reproduction (main @ f86da28)
var outer = new ClassTemplate { Name = "Outer" };
outer.NestedClasses.Add(new ClassTemplate { Name = "Inner", Kind = (TypeKind)99, Keywords = { "public" } });
cb.AddClass(outer); // throws ArgumentOutOfRangeException; buffer: "class Outer\n{\n\tpublic }\n"
var c = new ClassTemplate { Name = "C" };
c.Members.Add(new FieldTemplate { Type = "int", Name = "a" });
c.Members.Add(new PropertyTemplate { Type = "int", Name = "P", Keywords = { "public" },
Getter = new AccessorTemplate { Kind = (AccessorKind)99 } });
cb.AddClass(c); // throws; buffer ends "\tpublic int P\n\t{\n\t}\n"
var d = new ClassTemplate { Name = "C" };
d.Members.Add(new PropertyTemplate { Type = "int", Name = "P",
Getter = new AccessorTemplate { Kind = AccessorKind.Expression } });
cb.AddClass(d); // no exception; emits "\t\tget => ;"Why it matters
A source generator might catch the exception and keep using the same CodeBlocker. It then emits corrupt source, the exact failure #128 set out to prevent. Cases 1 and 2 need an out-of-range enum cast to trigger. Case 3 needs only ordinary object-initializer code, and the generator ships C# that fails to compile with no diagnostic.
Suggested fix / acceptance criteria
-
ClassTemplate.WriteToresolves the type keyword (or validatesKind) before callingbase.WriteTo. -
PropertyTemplate.WriteTovalidatesGetterandSetterbefore writing anything:- each
Kindmust be a definedAccessorKind - an
Expression(andBlock) accessor must have a non-nullBodyFactory
On failure it throws
InvalidOperationExceptionand writes nothing. - each
-
Add tests in the style of the PropertyTemplate with no accessors writes half its declaration before throwing, leaving
public int Foo}in the output #128 tests, one per case: catch the exception and assert theCodeBlockeroutput is unchanged.
Activity
matt-edmondson commentedon Oct 18, 2026
Triage
Kind = Expressionwith noBodyFactory) is reachable from ordinary object-initializer code and silently emits non-compiling C# (get => ;), which raises the overall priority.ClassTemplate.WriteTo(resolve the keyword beforebase.WriteTo),PropertyTemplate.WriteTo(validateGetter/Setterkind and non-nullBodyFactorybefore writing). Tests in the style of the PropertyTemplate with no accessors writes half its declaration before throwing, leavingpublic int Foo}in the output #128 tests.public int Foo}in the output #128 (fixed by Reject an accessorless property or unknown operator kind before writing anything [patch] #140), which named cases 1 and 2.Generated by Claude Code