Generalize CodeBlocker into a code-generation substrate - #87
Merged
Merged
Conversation
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Work towards ktsu-dev/Semantics#181 — extracting the general-purpose code-generation stack out of
Semantics.SourceGeneratorsso more than one project can use it. This is layer 1: everything that needs no Roslyn dependency.Closes #81, closes #82, closes #83, closes #84, closes #85, closes #86.
Configurable line terminator (#81)
CodeBlockerwrites throughIndentedTextWriter, which terminates lines withEnvironment.NewLine, so the same calls produced CRLF on Windows and LF everywhere else. Generated code that is committed to a repository has to be byte-identical wherever it was produced —ktsu.Semanticscarries a post-processing pass purely to undo this.CodeBlocker(StringWriter, string indentString, string newLineString)andCreate(string indentString, string newLineString).NewLinesclass naming the usual choices (Lf,CrLf,Host), andNewLineStringreporting the terminator back.The terminator is set on both the writer and the
IndentedTextWriter: the latter forwards to its inner writer on modern targets, but this package also ships fornetstandard2.0, where the running framework suppliesIndentedTextWriterand that forwarding is not guaranteed.The default is unchanged (
Environment.NewLine), so existing callers see exactly the output they see today and determinism is opt-in. That is a deliberate deviation from the issue's first acceptance criterion, which asked for byte-identical output across platforms by default — flipping it would silently change the bytes every current Windows consumer gets. Happy to make that flip in a[major]if you'd prefer.Any
TextWriter, not justStringWriter(#82)IndentedTextWritertakes anyTextWriter, so the restriction was self-imposed. It ruled out generating straight to a file or to a writer supplied by a build task.CodeBlocker(TextWriter)plus indent/terminator overloads. TheStringWriterconstructors stay and delegate, so no caller changes.ToString()returns the buffered code when the writer is aStringWriterand the type name otherwise, rather than throwing — debuggers callToString()freely.IsBufferedtells callers which case they are in.Create()made for itself is disposed, never a caller-supplied one. (Verified: disposing anIndentedTextWriterdoes not reach the writer it wraps.)Scopes and directive helpers (#83)
DelimiterScopeas a shared base, withParenScopeandBracketScopeover it, plusIndentScopefor continuation lines.RegionScope,DirectiveScope,PragmaScope. None indents its body, because a directive does not nest code.CodeBlockerExtensions:WriteAutoGeneratedHeader,WriteNullableEnable/Disable,WriteFileScopedNamespace,WriteUsings— each owning the blank line that conventionally follows it.ScopeandScopeWithTrailingSemicolonare deliberately not reparented ontoDelimiterScope: their tests pinNullReferenceExceptionfor a nullCodeBlocker, and the new types validate properly withArgumentNullException.The C# template object model (#84, #85, #86)
ktsu.Semanticsbuilt a declarative C# syntax object model on top ofCodeBlocker, and it is the most reusable part of that repository's generator stack — ~500 lines with no reference to physics or anything else specific to it. Ported here asktsu.CodeBlocker.Templates, public and documented.The port could not ship as it stood, so #85's fixes land with it rather than after. Rendering one file that exercised every template kind showed the model only ever produced valid C# for the narrow shapes Semantics happens to use:
};, and the kind came from free text inKeywords. NowTypeKind— class, struct, interface, record, record struct, enum.CodeBlocker.NewLine()goes throughIndentedTextWriter.WriteLineNoTabs, which does not re-arm the writer's pending-tab flag. Every place that used it to terminate an open declaration line was silently losing the indent of whatever came next. Those useWriteLine()now — that one was the root cause of most of the misindentation.AccessorTemplate,Auto/Expression/Block, optional modifier) rather than callbacks compared by reference. A caller-supplied body is no longer mistaken for an automatic accessor,private setis expressible, and a property with no accessors throws instead of renderingint Value;— a field.this(...)constructor chaining. Attributes go on their own line.ClassTemplateExtensions.AddInheritance— which wrote its base list twice and was called by nothing — is not carried over.XML documentation is data too (#86).
DocCommentescapes text content by default (a description containing<or&used to emit malformed XML), orders tags canonically, prefixes multi-line descriptions, and can validate itsparam/typeparamentries against the member — so a mismatch is reportable as a build diagnostic rather than surfacing as CS1572 inside generated source. The verbatimCommentslist stays as the escape hatch.Tests
167 passed, 0 failedon Linux in Release.TemplateGoldenTestsrenders one file using every template kind and pins the result exactly — and that expected text has been compiled as C# to confirm it is valid. A golden file is the only thing that catches a regression in the layout as a whole, which is the model's whole job. Alongside it: per-template render tests,DocCommentTests,NewLineTests(exact bytes for LF/CRLF/custom terminators and cross-configuration determinism),TextWriterTests,ScopesTests,CodeBlockerExtensionsTests.This branch also merged
main'sd4a7153, which fixed the pre-existing Linux test failures by expectingEnvironment.NewLine. This PR had done the same thing differently (a CRLF-pinned factory); merged together they contradicted each other and 45 tests failed on ubuntu. Main's approach won and the factory is gone — expecting the host terminator is the better fit for tests exercising the default, and the pinned behaviour has its own coverage inNewLineTests.Notes for the reviewer
README.mdgains sections for line endings, non-string writers, the new scopes, file preambles, and the template model, plus API-reference entries for every new type.CHANGELOG.mdis generated by the release pipeline from commit tags, so it is untouched; feature commits carry[minor].-p:CustomAfterMicrosoftCommonTargets=…to dropktsu.Sdk.Analyzers(this container's SDK ships Roslyn 5.0; the analyzer wants 5.9), so the KTSU rules could not run locally — CI caughtKTSU0003and it is fixed. Sonar's three new issues were likewise invisible here (sonarcloud.io is unreachable from this environment), so they were reproduced locally by wiring inSonarAnalyzer.CSharpthe wayktsu.Semanticsdoes, and all three are fixed. One new issue remains that I cannot see — the local reproduction is tuned to Semantics' quality profile and reports nothing outside files identical tomain. Worth a glance from someone with dashboard access; the gate passes either way.