|
| 1 | +# CodeWriter |
| 2 | + |
| 3 | +`CodeWriter` is the structured, allocation-conscious writer used to build generated C# source. Instead |
| 4 | +of concatenating strings or writing raw text, generators describe *what* to emit — declarations, |
| 5 | +statements, scopes — and the writer handles indentation, blank-line separation, generated attributes, |
| 6 | +and deterministic layout. |
| 7 | + |
| 8 | +This page uses the current best-practice API: bare semantic names (`Class`, `Method`, `Property`), the |
| 9 | +minimal-parameter overloads with an optional `configure` callback, and structured statements |
| 10 | +(`Return`, `MethodCall`, `Assignment`) instead of raw text. |
| 11 | + |
| 12 | +## Primitives |
| 13 | + |
| 14 | +Use the raw primitives for low-level text that has no structured equivalent: |
| 15 | + |
| 16 | +```csharp |
| 17 | +writer.Write("partial"); // no trailing line feed |
| 18 | +writer.Line("// generated"); // line feed appended |
| 19 | +writer.Append("text"); // Write alias |
| 20 | +writer.AppendLine("text"); // Line alias |
| 21 | +writer.Comment("Explains the next member."); |
| 22 | +writer.Indent(); // increase indentation |
| 23 | +writer.NewLine(); |
| 24 | +``` |
| 25 | + |
| 26 | +`Write`/`Line`/`Append`/`AppendLine` are the only methods that retain a verb prefix: everything |
| 27 | +semantic drops it because the receiver is already a writer. |
| 28 | + |
| 29 | +## Declarations |
| 30 | + |
| 31 | +Each declaration writer has: |
| 32 | + |
| 33 | +- a **minimal overload** taking name/type/accessibility plus an optional `configure` callback |
| 34 | + (`options => options with { ... }`); and |
| 35 | +- a **scope form** (`...Scope`) returning a `BlockScope` for `using` when you need fine-grained control. |
| 36 | + |
| 37 | +```csharp |
| 38 | +writer.Class( |
| 39 | + "OrderService", |
| 40 | + TypeDeclarationAccessibility.Public, |
| 41 | + options => options with { IsSealed = true, IsPartial = false }, |
| 42 | + body => |
| 43 | + { |
| 44 | + body.Field("_total", TypeIdentity.Create<decimal>().AsTypeReference(), TypeDeclarationAccessibility.Private); |
| 45 | + |
| 46 | + body.Constructor( |
| 47 | + "OrderService", |
| 48 | + TypeDeclarationAccessibility.Public, |
| 49 | + options => options with |
| 50 | + { |
| 51 | + Parameters = [new("total", TypeIdentity.Create<decimal>().AsTypeReference())], |
| 52 | + }, |
| 53 | + constructorBody => constructorBody.Assignment("_total", "total") |
| 54 | + ); |
| 55 | + |
| 56 | + body.Property( |
| 57 | + "Total", |
| 58 | + TypeIdentity.Create<decimal>().AsTypeReference(), |
| 59 | + TypeDeclarationAccessibility.Public |
| 60 | + ); |
| 61 | + } |
| 62 | +); |
| 63 | +``` |
| 64 | + |
| 65 | +The same pattern applies to `Struct`, `RecordClass`, `RecordStruct`, `Interface`, `Enum` (+ `EnumField`), |
| 66 | +`Type` (kind-driven), `Delegate`, `AttributeClass`, `Method`/`PartialMethod`/`MethodExpression`, |
| 67 | +`Property`/`PropertyExpression`, `Indexer`, `Field`, and `Operator`. |
| 68 | + |
| 69 | +### Scope forms |
| 70 | + |
| 71 | +```csharp |
| 72 | +using (writer.ClassScope("OrderService", TypeDeclarationAccessibility.Public)) |
| 73 | +using (writer.MethodScope("Apply", PurviewTypeLibrary.System.Void, TypeDeclarationAccessibility.Public)) |
| 74 | +{ |
| 75 | + writer.MethodCall("Validate"); |
| 76 | +} |
| 77 | +``` |
| 78 | + |
| 79 | +Scope forms are ideal when a declaration spans multiple calls, loops, or conditional content. The |
| 80 | +`using` statement is mandatory — the closing token and indentation are written on dispose, and the |
| 81 | +`DiscardedCodeWriterScopeAnalyzer` (PSGFR17) flags scope returns that are dropped. |
| 82 | + |
| 83 | +## Statements |
| 84 | + |
| 85 | +Emit executable statements through the structured statement methods rather than raw `Line`: |
| 86 | + |
| 87 | +```csharp |
| 88 | +writer.MethodCall("Process", "item"); // Process(item); |
| 89 | +writer.AwaitedMethodCall("SaveAsync", "cancellationToken"); // await SaveAsync(cancellationToken); |
| 90 | +writer.MethodCallOn("variable", "Process", "item"); // variable.Process(item); |
| 91 | +writer.AwaitedMethodCallOn("service", "LoadAsync", "token"); // await service.LoadAsync(token); |
| 92 | +writer.Return("value"); // return value; |
| 93 | +writer.Throw(TypeIdentity.Create<InvalidOperationException>(), "Failed."); // throw new ...; |
| 94 | +writer.Assignment("_total", "value"); // _total = value; |
| 95 | +writer.IfBlock("value is null", body => body.Return("null")); |
| 96 | +writer.Foreach("var item in items", body => body.MethodCallOn("item", "Process")); |
| 97 | +``` |
| 98 | + |
| 99 | +`MethodCall`/`AwaitedMethodCall` write a call without a receiver — `Process(item);` or |
| 100 | +`await SaveAsync(token);`. Use `MethodCallOn`/`AwaitedMethodCallOn` (or the `receiver` parameter on the |
| 101 | +`IEnumerable` overloads) for a call on a variable, including generic arguments: |
| 102 | + |
| 103 | +```csharp |
| 104 | +writer.MethodCall("Create", ["x"], receiver: "factory", genericArguments: [TypeReference.Create<string>()]); |
| 105 | +// factory.Create<string>(x); |
| 106 | +``` |
| 107 | + |
| 108 | +### Conditional compilation blocks |
| 109 | + |
| 110 | +`HashDefines`/`HashDefinesScope` write a `#if`/`#endif` block with both directives at **column zero**. |
| 111 | +The body keeps the surrounding indentation — file-level directives and their content stay at column |
| 112 | +zero, while class members inside the block stay at the same indent as their siblings: |
| 113 | + |
| 114 | +```csharp |
| 115 | +using (writer.HashDefinesScope("!EXCLUDE_PURVIEW_TELEMETRY_LOGGING")) |
| 116 | +{ |
| 117 | + writer.FileScopedNamespace("Example"); |
| 118 | + writer.Enum("Mode", TypeDeclarationAccessibility.Public, fields: [new("Default", 0)]); |
| 119 | +} |
| 120 | + |
| 121 | +// Equivalent action form: |
| 122 | +writer.HashDefines("NET", body => body.Line("// NET only")); |
| 123 | +``` |
| 124 | + |
| 125 | +Emits: |
| 126 | + |
| 127 | +```csharp |
| 128 | +#if !EXCLUDE_PURVIEW_TELEMETRY_LOGGING |
| 129 | +namespace Example; |
| 130 | +... |
| 131 | +#endif |
| 132 | +``` |
| 133 | + |
| 134 | +At file level these blocks are self-spacing: a blank line is ensured before the `#if` and after the |
| 135 | +`#endif`, so directive sections remain separated without explicit `NewLine()` calls. |
| 136 | + |
| 137 | +`HashElse()` writes the `#else` directive at column zero between the two bodies: |
| 138 | + |
| 139 | +```csharp |
| 140 | +using (writer.HashDefinesScope("NET48_OR_GREATER || PURVIEW_TELEMETRY_NON_NULLABLE")) |
| 141 | +{ |
| 142 | + writer.Property("name", TypeIdentity.Create<string>().AsTypeReference(), TypeDeclarationAccessibility.Public, |
| 143 | + options => options with { HasSetter = true, IncludeGeneratedAttributes = false }); |
| 144 | + writer.HashElse(); |
| 145 | + writer.Property("name", TypeIdentity.Create<string>().MakeNullable(writer), TypeDeclarationAccessibility.Public, |
| 146 | + options => options with { HasSetter = true, IncludeGeneratedAttributes = false }); |
| 147 | +} |
| 148 | +``` |
| 149 | + |
| 150 | +Emits: |
| 151 | + |
| 152 | +```csharp |
| 153 | +#if NET48_OR_GREATER || PURVIEW_TELEMETRY_NON_NULLABLE |
| 154 | +public string name { get; set; } |
| 155 | +#else |
| 156 | +public string? name { get; set; } |
| 157 | +#endif |
| 158 | +``` |
| 159 | + |
| 160 | +`EmptyScope()` returns a no-op scope so a block can be wrapped only when a guard requires it: |
| 161 | + |
| 162 | +```csharp |
| 163 | +using var scope = wrapInExcludeLoggingGuard |
| 164 | + ? writer.EmptyScope() |
| 165 | + : writer.HashDefinesScope("EXCLUDE_PURVIEW_TELEMETRY_LOGGING"); |
| 166 | +``` |
| 167 | + |
| 168 | +### Pragma warning suppression |
| 169 | + |
| 170 | +`PragmaDisable` writes a single `#pragma warning disable` directive at column zero for one or more |
| 171 | +warning codes. At file level it is self-spacing (blank lines are ensured around the directive): |
| 172 | + |
| 173 | +```csharp |
| 174 | +writer.PragmaDisable("CS8625", "CS0618"); |
| 175 | +// #pragma warning disable CS8625 CS0618 |
| 176 | +``` |
| 177 | + |
| 178 | +For a scoped disable that restores the warnings when the scope is disposed, use `OpenPragmasScope`: |
| 179 | + |
| 180 | +```csharp |
| 181 | +using (writer.OpenPragmasScope("CS0618")) |
| 182 | +{ |
| 183 | + writer.Line("ObsoleteCall();"); |
| 184 | +} |
| 185 | +// #pragma warning disable CS0618 |
| 186 | +// ObsoleteCall(); |
| 187 | +// #pragma warning restore CS0618 |
| 188 | +``` |
| 189 | + |
| 190 | +The full header pattern — nullable directive, conditional `#nullable enable`, and a disabled warning — |
| 191 | +can be expressed entirely through the structured APIs (the file-level directives are self-spacing, so |
| 192 | +no explicit `NewLine()` calls are needed): |
| 193 | + |
| 194 | +```csharp |
| 195 | +writer.AutoGeneratedHeader(nullableDirective: NullableDirectiveMode.Disable); |
| 196 | +writer.HashDefines("!NET48_OR_GREATER && !PURVIEW_TELEMETRY_NON_NULLABLE", hashWriter => hashWriter.Line("#nullable enable")); |
| 197 | +writer.PragmaDisable("CS8625"); |
| 198 | +writer.FileScopedNamespace("Purview.Telemetry"); |
| 199 | +``` |
| 200 | + |
| 201 | +Emits: |
| 202 | + |
| 203 | +```csharp |
| 204 | +// <auto-generated /> |
| 205 | +// This code was generated by ExampleGenerator (version 1.0.0). |
| 206 | +// Changes to this file will be lost when the source generator runs again. |
| 207 | +
|
| 208 | +#if !NET48_OR_GREATER && !PURVIEW_TELEMETRY_NON_NULLABLE |
| 209 | +#nullable enable |
| 210 | +#endif |
| 211 | + |
| 212 | +#pragma warning disable CS8625 |
| 213 | + |
| 214 | +namespace Purview.Telemetry; |
| 215 | +``` |
| 216 | + |
| 217 | +### Conditional compilation returns |
| 218 | + |
| 219 | +`NetConditionalReturn` writes a `return` for an interpolated string using the best invariant-culture |
| 220 | +API on each target framework, guarded by `#if NET`: |
| 221 | + |
| 222 | +```csharp |
| 223 | +writer.Method( |
| 224 | + "Format", |
| 225 | + TypeIdentity.Create<string>().AsTypeReference(), |
| 226 | + TypeDeclarationAccessibility.Public, |
| 227 | + null, |
| 228 | + body => body.NetConditionalReturn("Value: {_value}") |
| 229 | +); |
| 230 | +``` |
| 231 | + |
| 232 | +Emits: |
| 233 | + |
| 234 | +```csharp |
| 235 | +#if NET |
| 236 | + return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"Value: {_value}"); |
| 237 | +#else |
| 238 | + return global::System.FormattableString.Invariant($"Value: {_value}"); |
| 239 | +#endif |
| 240 | +``` |
| 241 | + |
| 242 | +## Default accessibility |
| 243 | + |
| 244 | +`CodeWriter` applies a default accessibility for each member kind when a declaration does not specify |
| 245 | +one. Set the defaults on `GenerationSettings` (to apply across a generation) or on the writer itself |
| 246 | +(to override per writer). Each value is `null`-able, so setting a kind back to `null` omits the |
| 247 | +modifier entirely. |
| 248 | + |
| 249 | +| Setting | Default | |
| 250 | +|---|---| |
| 251 | +| `DefaultTypeAccessibility` | `Public` | |
| 252 | +| `DefaultPropertyAccessibility` | `Public` | |
| 253 | +| `DefaultPropertyGetterAccessibility` | `Public` | |
| 254 | +| `DefaultPropertySetterAccessibility` | `Public` | |
| 255 | +| `DefaultFieldAccessibility` | `Private` | |
| 256 | +| `DefaultMethodAccessibility` | `Public` | |
| 257 | +| `DefaultConstructorAccessibility` | `Public` | |
| 258 | +| `DefaultIndexerAccessibility` | `Public` | |
| 259 | +| `DefaultOperatorAccessibility` | `Public` | |
| 260 | + |
| 261 | +```csharp |
| 262 | +var writer = generationContext.CreateCodeWriter(); |
| 263 | +writer.Field("_total", TypeReference.Create<decimal>()); // private int _total; (DefaultFieldAccessibility) |
| 264 | +writer.Property("Total", TypeReference.Create<decimal>()); // public decimal Total { get; } |
| 265 | +``` |
| 266 | + |
| 267 | +An explicit accessibility always wins over the default: |
| 268 | + |
| 269 | +```csharp |
| 270 | +writer.Property("Total", TypeReference.Create<decimal>(), TypeDeclarationAccessibility.Internal); |
| 271 | +// internal decimal Total { get; } |
| 272 | +``` |
| 273 | + |
| 274 | +Accessor (getter/setter) defaults are emitted only when they are **more restrictive** than the |
| 275 | +property's own accessibility — C# forbids an accessor modifier that is equal to or more permissive |
| 276 | +than the property (CS0273). With the public defaults, a public property keeps bare `{ get; set; }`: |
| 277 | + |
| 278 | +```csharp |
| 279 | +writer.DefaultPropertySetterAccessibility = TypeDeclarationAccessibility.Private; |
| 280 | +writer.Property("Name", TypeReference.Create<string>(), TypeDeclarationAccessibility.Public, |
| 281 | + options => options with { HasSetter = true }); |
| 282 | +// public string Name { get; private set; } |
| 283 | +``` |
| 284 | + |
| 285 | +## Guidance |
| 286 | + |
| 287 | +- Prefer the minimal overloads with a `configure` callback over constructing `*DeclarationOptions` |
| 288 | + values manually — the `PreferMinimalCodeWriterOverloadAnalyzer` (PSGFR20) flags the verbose form. |
| 289 | +- Prefer structured declarations and statements over raw text — `PreferStructuredCodeWriterApiAnalyzer` |
| 290 | + (PSGFR18) and `PreferStructuredCodeWriterStatementAnalyzer` (PSGFR19) flag raw emission. |
| 291 | +- Always consume scope-returning methods with `using` (PSGFR17). |
| 292 | +- Keep every value emitted through the structured API so layout stays deterministic and the analyzers |
| 293 | + can guide callers back to the best practice. |
| 294 | + |
| 295 | +## Samples |
| 296 | + |
| 297 | +The [`SourceGeneratorFramework.ExampleGenerator`](../src/src/SourceGeneratorFramework.ExampleGenerator) |
| 298 | +reference implementation demonstrates these APIs end-to-end, including the `CodeWriterSampleGenerator`, |
| 299 | +which compiles a best-practice sample class for every `[GenerateCodeWriterSample]` target. |
0 commit comments