From c490bec241f2ed1f3fdfdc2736cb27cc5e3d697c Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Fri, 4 Sep 2026 11:49:53 +0100 Subject: [PATCH 1/2] feat: code writer supports more structured output --- AGENTS.md | 4 + README.md | 6 + docs/code-writer.md | 288 +++ .../AnalyzerReleases.Unshipped.md | 4 + .../CodeWriterLiteralClassifier.cs | 210 ++ .../PreferHashDefinesAnalyzer.cs | 79 + ...PreferMinimalCodeWriterOverloadAnalyzer.cs | 139 ++ .../PreferPragmaDisableAnalyzer.cs | 79 + .../PreferStructuredCodeWriterApiAnalyzer.cs | 71 +- ...erStructuredCodeWriterStatementAnalyzer.cs | 84 + .../Benchmarks/CodeWriterBenchmarks.cs | 8 +- .../CodeWriterSampleEmitter.cs | 124 + .../CodeWriterSampleGenerator.cs | 44 + .../README.md | 3 +- .../ServiceRegistrationEmitter.cs | 188 +- .../TypeLibrary.cs | 8 + .../AttributeDataModelGenerator.cs | 273 ++- .../Helpers/SourceEmitter.AttributeData.cs | 71 +- .../Helpers/SourceEmitter.cs | 2 +- .../CodeQuery.Members.cs | 93 +- ...r-source-generator-to-codewriter.prompt.md | 6 +- .../SKILL.md | 18 +- .../SourceGeneratorFramework/Sdk/README.md | 173 +- .../CodeWriter.DeclarationOverloads.cs | 962 ++++++++ .../SourceGeneratorShared/CodeWriter.Types.cs | 27 +- src/src/SourceGeneratorShared/CodeWriter.cs | 1399 +++++++----- ...eneratorInitializationContextExtensions.cs | 9 +- .../GenerationSettings.cs | 74 + .../NullableDirectiveMode.cs | 2 +- .../TypeDeclarationOptions.cs | 2 +- .../SourceGeneratorShared/XmlCommentWriter.cs | 18 +- .../DiscardedCodeWriterScopeAnalyzerTests.cs | 8 +- .../PreferHashDefinesAnalyzerTests.cs | 186 ++ ...rMinimalCodeWriterOverloadAnalyzerTests.cs | 270 +++ ...ferNullableContextOverloadAnalyzerTests.cs | 4 +- .../PreferPragmaDisableAnalyzerTests.cs | 161 ++ ...ferStructuredCodeWriterApiAnalyzerTests.cs | 162 +- ...ucturedCodeWriterStatementAnalyzerTests.cs | 337 +++ ...ableContextOverloadCodeFixProviderTests.cs | 8 +- .../CodeWriterSampleGeneratorTests.cs | 92 + .../CodeWriterSampleTestOptions.cs | 11 + .../ServiceRegistrationGeneratorTests.cs | 9 +- .../CodeQueryTests.cs | 58 + .../CodeWriterTests.cs | 2003 +++++++++++++---- .../Helpers/TypeHelpersTests.cs | 8 +- .../AlwaysNullableContextTestGenerator.cs | 8 +- .../TestGenerators/DiagnosticTestGenerator.cs | 2 +- .../ExplicitNullableContextTestGenerator.cs | 8 +- .../NullableContextTestGenerator.cs | 8 +- 49 files changed, 6408 insertions(+), 1403 deletions(-) create mode 100644 docs/code-writer.md create mode 100644 src/src/SourceGeneratorFramework.Analyzers/CodeWriterLiteralClassifier.cs create mode 100644 src/src/SourceGeneratorFramework.Analyzers/PreferHashDefinesAnalyzer.cs create mode 100644 src/src/SourceGeneratorFramework.Analyzers/PreferMinimalCodeWriterOverloadAnalyzer.cs create mode 100644 src/src/SourceGeneratorFramework.Analyzers/PreferPragmaDisableAnalyzer.cs create mode 100644 src/src/SourceGeneratorFramework.Analyzers/PreferStructuredCodeWriterStatementAnalyzer.cs create mode 100644 src/src/SourceGeneratorFramework.ExampleGenerator/CodeWriterSampleEmitter.cs create mode 100644 src/src/SourceGeneratorFramework.ExampleGenerator/CodeWriterSampleGenerator.cs create mode 100644 src/src/SourceGeneratorShared/CodeWriter.DeclarationOverloads.cs create mode 100644 src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferHashDefinesAnalyzerTests.cs create mode 100644 src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferMinimalCodeWriterOverloadAnalyzerTests.cs create mode 100644 src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferPragmaDisableAnalyzerTests.cs create mode 100644 src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferStructuredCodeWriterStatementAnalyzerTests.cs create mode 100644 src/tests/SourceGeneratorFramework.ExampleGenerator.UnitTests/CodeWriterSampleGeneratorTests.cs create mode 100644 src/tests/SourceGeneratorFramework.ExampleGenerator.UnitTests/CodeWriterSampleTestOptions.cs diff --git a/AGENTS.md b/AGENTS.md index 0fa68ac..a39a963 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,3 +5,7 @@ This repository contains custom build rules, analysers, and source generators. A ## Warnings and Suggestions Do **not** suppress or mute compiler, analyser, or build warnings by adding `` entries, `#pragma warning disable`, or similar directives in code or project files without explicit user direction. Warnings and suggestions are the responsibility of the developer/user to evaluate and mute. If a warning is raised, surface it to the user and let them decide whether to suppress it. + +## Documentation and Samples + +Any API change must update the corresponding documentation, including XML doc comments and `docs/` pages such as `docs/code-writer.md`. Samples (the `SourceGeneratorFramework.ExampleGenerator` reference implementation and benchmarks) must use the current best-practice APIs: the minimal-parameter `CodeWriter` overloads, structured statements (`MethodCall`, `Return`, `Assignment`, `Throw`, `Comment`, `NetConditionalReturn`) rather than raw text, and the current method names. Add or update a sample whenever an API addition or change warrants a demonstrable example, and cover it with unit tests. diff --git a/README.md b/README.md index 8609fc3..dc0e6bd 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,11 @@ A set of libraries for building and testing incremental C# source generators using Roslyn. +## Documentation + +- [Source generator & analyser best practices](docs/guide.md) +- [CodeWriter structured API reference](docs/code-writer.md) + ## Packages | Package | Description | Packable | @@ -45,6 +50,7 @@ normal reference with `ReferenceOutputAssembly="true"`. The complete pattern is The framework package includes `AttributeDataModelGenerator` (implemented in `Purview.SourceGeneratorFramework.Generators`), which generates `readonly record struct` parser models for .NET attributes. It removes the repetitive boilerplate of hand-writing `FromAttributeData` methods for every attribute you want to inspect in a source generator. Supported features: + - Manual mapping of named arguments, constructor arguments by index, and constructor arguments by name - Auto-discovery of all constructor parameters and public named properties - Nested generated models (e.g., a shared `ValidationAttributeData` model reused inside `RequiredAttributeData`) diff --git a/docs/code-writer.md b/docs/code-writer.md new file mode 100644 index 0000000..015f739 --- /dev/null +++ b/docs/code-writer.md @@ -0,0 +1,288 @@ +# CodeWriter + +`CodeWriter` is the structured, allocation-conscious writer used to build generated C# source. Instead +of concatenating strings or writing raw text, generators describe *what* to emit — declarations, +statements, scopes — and the writer handles indentation, blank-line separation, generated attributes, +and deterministic layout. + +This page uses the current best-practice API: bare semantic names (`Class`, `Method`, `Property`), the +minimal-parameter overloads with an optional `configure` callback, and structured statements +(`Return`, `MethodCall`, `Assignment`) instead of raw text. + +## Primitives + +Use the raw primitives for low-level text that has no structured equivalent: + +```csharp +writer.Write("partial"); // no trailing line feed +writer.Line("// generated"); // line feed appended +writer.Append("text"); // Write alias +writer.AppendLine("text"); // Line alias +writer.Comment("Explains the next member."); +writer.Indent(); // increase indentation +writer.NewLine(); +``` + +`Write`/`Line`/`Append`/`AppendLine` are the only methods that retain a verb prefix: everything +semantic drops it because the receiver is already a writer. + +## Declarations + +Each declaration writer has: + +- a **minimal overload** taking name/type/accessibility plus an optional `configure` callback + (`options => options with { ... }`); and +- a **scope form** (`...Scope`) returning a `BlockScope` for `using` when you need fine-grained control. + +```csharp +writer.Class( + "OrderService", + TypeDeclarationAccessibility.Public, + options => options with { IsSealed = true, IsPartial = false }, + body => + { + body.Field("_total", TypeIdentity.Create().AsTypeReference(), TypeDeclarationAccessibility.Private); + + body.Constructor( + "OrderService", + TypeDeclarationAccessibility.Public, + options => options with + { + Parameters = [new("total", TypeIdentity.Create().AsTypeReference())], + }, + constructorBody => constructorBody.Assignment("_total", "total") + ); + + body.Property( + "Total", + TypeIdentity.Create().AsTypeReference(), + TypeDeclarationAccessibility.Public + ); + } +); +``` + +The same pattern applies to `Struct`, `RecordClass`, `RecordStruct`, `Interface`, `Enum` (+ `EnumField`), +`Type` (kind-driven), `Delegate`, `AttributeClass`, `Method`/`PartialMethod`/`MethodExpression`, +`Property`/`PropertyExpression`, `Indexer`, `Field`, and `Operator`. + +### Scope forms + +```csharp +using (writer.ClassScope("OrderService", TypeDeclarationAccessibility.Public)) +using (writer.MethodScope("Apply", PurviewTypeLibrary.System.Void, TypeDeclarationAccessibility.Public)) +{ + writer.MethodCall("Validate"); +} +``` + +Scope forms are ideal when a declaration spans multiple calls, loops, or conditional content. The +`using` statement is mandatory — the closing token and indentation are written on dispose, and the +`DiscardedCodeWriterScopeAnalyzer` (PSGFR17) flags scope returns that are dropped. + +## Statements + +Emit executable statements through the structured statement methods rather than raw `Line`: + +```csharp +writer.MethodCall("Process", "item"); // Process(item); +writer.AwaitedMethodCall("SaveAsync", "cancellationToken"); // await SaveAsync(cancellationToken); +writer.Return("value"); // return value; +writer.Throw(TypeIdentity.Create(), "Failed."); // throw new ...; +writer.Assignment("_total", "value"); // _total = value; +writer.IfBlock("value is null", body => body.Return("null")); +writer.Foreach("var item in items", body => body.MethodCall("Process", "item")); +``` + +### Conditional compilation blocks + +`HashDefines`/`HashDefinesScope` write a `#if`/`#endif` block with both directives at **column zero**. +The body keeps the surrounding indentation — file-level directives and their content stay at column +zero, while class members inside the block stay at the same indent as their siblings: + +```csharp +using (writer.HashDefinesScope("!EXCLUDE_PURVIEW_TELEMETRY_LOGGING")) +{ + writer.FileScopedNamespace("Example"); + writer.Enum("Mode", TypeDeclarationAccessibility.Public, fields: [new("Default", 0)]); +} + +// Equivalent action form: +writer.HashDefines("NET", body => body.Line("// NET only")); +``` + +Emits: + +```csharp +#if !EXCLUDE_PURVIEW_TELEMETRY_LOGGING +namespace Example; +... +#endif +``` + +At file level these blocks are self-spacing: a blank line is ensured before the `#if` and after the +`#endif`, so directive sections remain separated without explicit `NewLine()` calls. + +`HashElse()` writes the `#else` directive at column zero between the two bodies: + +```csharp +using (writer.HashDefinesScope("NET48_OR_GREATER || PURVIEW_TELEMETRY_NON_NULLABLE")) +{ + writer.Property("name", TypeIdentity.Create().AsTypeReference(), TypeDeclarationAccessibility.Public, + options => options with { HasSetter = true, IncludeGeneratedAttributes = false }); + writer.HashElse(); + writer.Property("name", TypeIdentity.Create().MakeNullable(writer), TypeDeclarationAccessibility.Public, + options => options with { HasSetter = true, IncludeGeneratedAttributes = false }); +} +``` + +Emits: + +```csharp +#if NET48_OR_GREATER || PURVIEW_TELEMETRY_NON_NULLABLE +public string name { get; set; } +#else +public string? name { get; set; } +#endif +``` + +`EmptyScope()` returns a no-op scope so a block can be wrapped only when a guard requires it: + +```csharp +using var scope = wrapInExcludeLoggingGuard + ? writer.EmptyScope() + : writer.HashDefinesScope("EXCLUDE_PURVIEW_TELEMETRY_LOGGING"); +``` + +### Pragma warning suppression + +`PragmaDisable` writes a single `#pragma warning disable` directive at column zero for one or more +warning codes. At file level it is self-spacing (blank lines are ensured around the directive): + +```csharp +writer.PragmaDisable("CS8625", "CS0618"); +// #pragma warning disable CS8625 CS0618 +``` + +For a scoped disable that restores the warnings when the scope is disposed, use `OpenPragmasScope`: + +```csharp +using (writer.OpenPragmasScope("CS0618")) +{ + writer.Line("ObsoleteCall();"); +} +// #pragma warning disable CS0618 +// ObsoleteCall(); +// #pragma warning restore CS0618 +``` + +The full header pattern — nullable directive, conditional `#nullable enable`, and a disabled warning — +can be expressed entirely through the structured APIs (the file-level directives are self-spacing, so +no explicit `NewLine()` calls are needed): + +```csharp +writer.AutoGeneratedHeader(nullableDirective: NullableDirectiveMode.Disable); +writer.HashDefines("!NET48_OR_GREATER && !PURVIEW_TELEMETRY_NON_NULLABLE", hashWriter => hashWriter.Line("#nullable enable")); +writer.PragmaDisable("CS8625"); +writer.FileScopedNamespace("Purview.Telemetry"); +``` + +Emits: + +```csharp +// +// This code was generated by ExampleGenerator (version 1.0.0). +// Changes to this file will be lost when the source generator runs again. + +#if !NET48_OR_GREATER && !PURVIEW_TELEMETRY_NON_NULLABLE +#nullable enable +#endif + +#pragma warning disable CS8625 + +namespace Purview.Telemetry; +``` + +### Conditional compilation returns + +`NetConditionalReturn` writes a `return` for an interpolated string using the best invariant-culture +API on each target framework, guarded by `#if NET`: + +```csharp +writer.Method( + "Format", + TypeIdentity.Create().AsTypeReference(), + TypeDeclarationAccessibility.Public, + null, + body => body.NetConditionalReturn("Value: {_value}") +); +``` + +Emits: + +```csharp +#if NET + return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"Value: {_value}"); +#else + return global::System.FormattableString.Invariant($"Value: {_value}"); +#endif +``` + +## Default accessibility + +`CodeWriter` applies a default accessibility for each member kind when a declaration does not specify +one. Set the defaults on `GenerationSettings` (to apply across a generation) or on the writer itself +(to override per writer). Each value is `null`-able, so setting a kind back to `null` omits the +modifier entirely. + +| Setting | Default | +|---|---| +| `DefaultTypeAccessibility` | `Public` | +| `DefaultPropertyAccessibility` | `Public` | +| `DefaultPropertyGetterAccessibility` | `Public` | +| `DefaultPropertySetterAccessibility` | `Public` | +| `DefaultFieldAccessibility` | `Private` | +| `DefaultMethodAccessibility` | `Public` | +| `DefaultConstructorAccessibility` | `Public` | +| `DefaultIndexerAccessibility` | `Public` | +| `DefaultOperatorAccessibility` | `Public` | + +```csharp +var writer = generationContext.CreateCodeWriter(); +writer.Field("_total", TypeReference.Create()); // private int _total; (DefaultFieldAccessibility) +writer.Property("Total", TypeReference.Create()); // public decimal Total { get; } +``` + +An explicit accessibility always wins over the default: + +```csharp +writer.Property("Total", TypeReference.Create(), TypeDeclarationAccessibility.Internal); +// internal decimal Total { get; } +``` + +Accessor (getter/setter) defaults are emitted only when they are **more restrictive** than the +property's own accessibility — C# forbids an accessor modifier that is equal to or more permissive +than the property (CS0273). With the public defaults, a public property keeps bare `{ get; set; }`: + +```csharp +writer.DefaultPropertySetterAccessibility = TypeDeclarationAccessibility.Private; +writer.Property("Name", TypeReference.Create(), TypeDeclarationAccessibility.Public, + options => options with { HasSetter = true }); +// public string Name { get; private set; } +``` + +## Guidance + +- Prefer the minimal overloads with a `configure` callback over constructing `*DeclarationOptions` + values manually — the `PreferMinimalCodeWriterOverloadAnalyzer` (PSGFR20) flags the verbose form. +- Prefer structured declarations and statements over raw text — `PreferStructuredCodeWriterApiAnalyzer` + (PSGFR18) and `PreferStructuredCodeWriterStatementAnalyzer` (PSGFR19) flag raw emission. +- Always consume scope-returning methods with `using` (PSGFR17). +- Keep every value emitted through the structured API so layout stays deterministic and the analyzers + can guide callers back to the best practice. + +## Samples + +The [`SourceGeneratorFramework.ExampleGenerator`](../src/src/SourceGeneratorFramework.ExampleGenerator) +reference implementation demonstrates these APIs end-to-end, including the `CodeWriterSampleGenerator`, +which compiles a best-practice sample class for every `[GenerateCodeWriterSample]` target. diff --git a/src/src/SourceGeneratorFramework.Analyzers/AnalyzerReleases.Unshipped.md b/src/src/SourceGeneratorFramework.Analyzers/AnalyzerReleases.Unshipped.md index 5595ecf..1395e11 100644 --- a/src/src/SourceGeneratorFramework.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/src/SourceGeneratorFramework.Analyzers/AnalyzerReleases.Unshipped.md @@ -10,6 +10,10 @@ PSGFR15 | Purview.SourceGeneratorFramework | Warning | Pipeline model collection PSGFR16 | Purview.SourceGeneratorFramework | Info | Prefer the nullable-context overload PSGFR17 | Purview.SourceGeneratorFramework | Warning | Consume CodeWriter scopes with a using statement PSGFR18 | Purview.SourceGeneratorFramework | Info | Prefer a structured CodeWriter declaration API +PSGFR19 | Purview.SourceGeneratorFramework | Info | Prefer a structured CodeWriter statement API +PSGFR20 | Purview.SourceGeneratorFramework | Info | Prefer the minimal CodeWriter overload +PSGFR21 | Purview.SourceGeneratorFramework | Info | Prefer HashDefines for conditional compilation +PSGFR22 | Purview.SourceGeneratorFramework | Info | Prefer PragmaDisable for warning suppression ADM0001 | Target | Error | Target attribute type cannot be resolved ADM0002 | Property | Error | Property type is not supported for attribute extraction ADM0003 | Source | Error | Specified constructor index/name does not exist on the target attribute diff --git a/src/src/SourceGeneratorFramework.Analyzers/CodeWriterLiteralClassifier.cs b/src/src/SourceGeneratorFramework.Analyzers/CodeWriterLiteralClassifier.cs new file mode 100644 index 0000000..1d5930e --- /dev/null +++ b/src/src/SourceGeneratorFramework.Analyzers/CodeWriterLiteralClassifier.cs @@ -0,0 +1,210 @@ +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Purview.SourceGeneratorFramework.Analyzers; + +/// +/// Classifies text emitted through the raw CodeWriter text methods so analyzers can suggest a +/// structured declaration or statement API. +/// +static class CodeWriterLiteralClassifier +{ + static readonly string[] DeclarationStarts = + [ + "public ", + "internal ", + "private ", + "protected ", + "file ", + "static ", + "sealed ", + "abstract ", + "partial ", + "readonly ", + "ref ", + "required ", + "const ", + "class ", + "struct ", + "interface ", + "enum ", + "record ", + "delegate ", + "namespace ", + "global using ", + "using ", + ]; + + /// + /// Attempts to resolve the text that would be emitted by a raw text-emission call, handling plain, + /// interpolated, and raw string literals as well as constant expressions such as const + /// references and string concatenation. + /// + /// The first argument of the text-emission call. + /// The semantic model of the containing compilation. + /// The cancellation token. + /// The resolved text, or when it could not be resolved. + /// when the emitted text was resolved. + public static bool TryGetLiteralText( + ExpressionSyntax expression, + SemanticModel semanticModel, + CancellationToken cancellationToken, + out string? text + ) + { + switch (expression) + { + case LiteralExpressionSyntax { RawKind: (int)SyntaxKind.StringLiteralExpression } literal: + text = literal.Token.ValueText; + return true; + + case InterpolatedStringExpressionSyntax interpolated: + { + var builder = new StringBuilder(); + foreach (var content in interpolated.Contents) + { + if (content is InterpolatedStringTextSyntax textPart) + builder.Append(textPart.TextToken.ValueText); + } + + text = builder.ToString(); + return true; + } + + default: + break; + } + + var constant = semanticModel.GetConstantValue(expression, cancellationToken); + if (constant.HasValue && constant.Value is string value) + { + text = value; + return true; + } + + text = null; + return false; + } + + /// + /// Determines whether the trimmed value starts with a C# declaration keyword. + /// + public static bool StartsWithDeclaration(string value) + { + foreach (var prefix in DeclarationStarts) + { + if (!value.StartsWith(prefix, StringComparison.Ordinal)) + continue; + + // "using (var x = ...)" is a using statement inside a body, not a directive; don't flag it. + if (prefix == "using " && value.StartsWith("using (", StringComparison.Ordinal)) + return false; + + // "namespace global::" is a valid namespace declaration, but "global::" is not a declaration keyword. + return true; + } + + return false; + } + + /// + /// Determines whether the trimmed value is a #if, #else, or #endif preprocessor + /// directive that can be expressed with HashDefines/HashElse. + /// + public static bool IsHashDefine(string value) + { + var trimmed = value.TrimStart(); + return trimmed.StartsWith("#if ", StringComparison.Ordinal) + || trimmed.StartsWith("#else", StringComparison.Ordinal) + || trimmed.StartsWith("#endif", StringComparison.Ordinal); + } + + /// + /// Determines whether the trimmed value is a #pragma warning disable or + /// #pragma warning restore directive that can be expressed with PragmaDisable or + /// OpenPragmasScope. + /// + public static bool IsPragmaWarningDirective(string value) + { + var trimmed = value.TrimStart(); + return trimmed.StartsWith("#pragma warning disable", StringComparison.Ordinal) + || trimmed.StartsWith("#pragma warning restore", StringComparison.Ordinal); + } + + /// + /// Classifies a single-line emitted statement and returns the structured CodeWriter API that + /// can express it, or when no structured equivalent is recognized. + /// + public static string? ClassifyStatement(string value) + { + var trimmed = value.TrimStart(); + if (trimmed.Length == 0 || trimmed.Contains("\n") || trimmed.Contains("\r")) + return null; + + if (trimmed.StartsWith("//", StringComparison.Ordinal)) + return "Comment"; + + if (ClassifyUsingDirective(trimmed) is { } usingApi) + return usingApi; + + if (StartsWithDeclaration(trimmed)) + return null; + + return ClassifyExecutable(trimmed); + } + + static string? ClassifyUsingDirective(string trimmed) + { + if ( + !trimmed.StartsWith("using ", StringComparison.Ordinal) + && !trimmed.StartsWith("global using ", StringComparison.Ordinal) + ) + return null; + + // "using (var x = ...)" is a using statement inside a body, not a directive; don't flag it. + if (trimmed.StartsWith("using (", StringComparison.Ordinal) || !trimmed.EndsWith(";", StringComparison.Ordinal)) + return null; + + return trimmed.Contains(" = ") ? "UsingAlias" : "Using"; + } + + static string? ClassifyExecutable(string trimmed) + { + if ( + (trimmed == "return;" || trimmed.StartsWith("return ", StringComparison.Ordinal)) + && trimmed.EndsWith(";", StringComparison.Ordinal) + ) + return "Return"; + + if (trimmed.StartsWith("throw ", StringComparison.Ordinal) && trimmed.EndsWith(";", StringComparison.Ordinal)) + return "Throw"; + + if (trimmed.StartsWith("await ", StringComparison.Ordinal) && trimmed.EndsWith(";", StringComparison.Ordinal)) + return "AwaitedMethodCall"; + + if (trimmed.StartsWith("if (", StringComparison.Ordinal)) + return "IfBlock"; + + if (trimmed.StartsWith("foreach (", StringComparison.Ordinal)) + return "Foreach"; + + if (trimmed.StartsWith("while (", StringComparison.Ordinal)) + return "While"; + + if (trimmed.StartsWith("for (", StringComparison.Ordinal)) + return "For"; + + if (!trimmed.EndsWith(";", StringComparison.Ordinal)) + return null; + + if (trimmed.Contains(" = ")) + return "Assignment"; + + if (trimmed.Contains("(") && trimmed.EndsWith(");", StringComparison.Ordinal)) + return "MethodCall"; + + return null; + } +} diff --git a/src/src/SourceGeneratorFramework.Analyzers/PreferHashDefinesAnalyzer.cs b/src/src/SourceGeneratorFramework.Analyzers/PreferHashDefinesAnalyzer.cs new file mode 100644 index 0000000..f66cc11 --- /dev/null +++ b/src/src/SourceGeneratorFramework.Analyzers/PreferHashDefinesAnalyzer.cs @@ -0,0 +1,79 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Purview.SourceGeneratorFramework.Analyzers; + +/// +/// Flags raw CodeWriter text emission of a #if/#endif preprocessor directive, +/// suggesting HashDefines/HashDefinesScope instead. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class PreferHashDefinesAnalyzer : DiagnosticAnalyzer +{ + public const string DiagnosticId = "PSGFR21"; + + public static readonly DiagnosticDescriptor Rule = new( + DiagnosticId, + "Prefer HashDefines for conditional compilation", + "'{0}' with a preprocessor directive should use 'HashDefines' or 'HashElse'", + "Purview.SourceGeneratorFramework", + DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: "Emitting #if/#else/#endif directives through raw text bypasses the structured HashDefines/HashElse APIs that write directives at column zero." + ); + + public override ImmutableArray SupportedDiagnostics => [Rule]; + + public override void Initialize(AnalysisContext context) + { + if (context is null) + throw new ArgumentNullException(nameof(context)); + + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); + } + + static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) + { + if ( + context.Node + is not InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax member } invocation + ) + return; + + var name = member.Name.Identifier.Text; + if (name is not ("Write" or "Line" or "Append" or "AppendLine" or "MultiLine")) + return; + + if (invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression is not ExpressionSyntax expression) + return; + + if ( + context.SemanticModel.GetSymbolInfo(invocation, context.CancellationToken).Symbol + is not IMethodSymbol method + ) + return; + + if (method.ContainingType?.ToDisplayString() != "Purview.SourceGeneratorFramework.CodeWriter") + return; + + if ( + !CodeWriterLiteralClassifier.TryGetLiteralText( + expression, + context.SemanticModel, + context.CancellationToken, + out var value + ) + ) + return; + + if (value is null || !CodeWriterLiteralClassifier.IsHashDefine(value)) + return; + + context.ReportDiagnostic(Diagnostic.Create(Rule, invocation.GetLocation(), name)); + } +} diff --git a/src/src/SourceGeneratorFramework.Analyzers/PreferMinimalCodeWriterOverloadAnalyzer.cs b/src/src/SourceGeneratorFramework.Analyzers/PreferMinimalCodeWriterOverloadAnalyzer.cs new file mode 100644 index 0000000..3557163 --- /dev/null +++ b/src/src/SourceGeneratorFramework.Analyzers/PreferMinimalCodeWriterOverloadAnalyzer.cs @@ -0,0 +1,139 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Purview.SourceGeneratorFramework.Analyzers; + +/// +/// Flags a structured CodeWriter declaration call that constructs a *DeclarationOptions +/// value using only its primary-constructor arguments, suggesting the minimal overload that takes those +/// arguments directly. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class PreferMinimalCodeWriterOverloadAnalyzer : DiagnosticAnalyzer +{ + public const string DiagnosticId = "PSGFR20"; + + public static readonly DiagnosticDescriptor Rule = new( + DiagnosticId, + "Prefer the minimal CodeWriter overload", + "'{0}' should use the minimal overload '{1}'", + "Purview.SourceGeneratorFramework", + DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: "Constructing a declaration options value from only its primary-constructor arguments can be replaced by the minimal overload that takes those arguments directly." + ); + + public override ImmutableArray SupportedDiagnostics => [Rule]; + + public override void Initialize(AnalysisContext context) + { + if (context is null) + throw new ArgumentNullException(nameof(context)); + + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); + } + + static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) + { + if ( + context.Node + is not InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax member } invocation + ) + return; + + if (invocation.ArgumentList.Arguments.Count != 1) + return; + + var argumentExpression = invocation.ArgumentList.Arguments[0].Expression; + if (argumentExpression is not (ObjectCreationExpressionSyntax or ImplicitObjectCreationExpressionSyntax)) + return; + + // An object-initializer carries configuration the minimal overload cannot express. + if (HasObjectInitializer(argumentExpression)) + return; + + var semanticModel = context.SemanticModel; + if (semanticModel.GetSymbolInfo(invocation, context.CancellationToken).Symbol is not IMethodSymbol method) + return; + + if (method.ContainingType?.ToDisplayString() != "Purview.SourceGeneratorFramework.CodeWriter") + return; + + if ( + semanticModel.GetSymbolInfo(argumentExpression, context.CancellationToken).Symbol + is not IMethodSymbol constructor + ) + return; + + var constructorParameters = constructor.Parameters; + if (constructorParameters.Length == 0) + return; + + var methodName = member.Name.Identifier.Text; + var suggestion = FindMinimalOverload(method, methodName, constructorParameters); + if (suggestion is null) + return; + + context.ReportDiagnostic(Diagnostic.Create(Rule, invocation.GetLocation(), methodName, suggestion)); + } + + static string? FindMinimalOverload( + IMethodSymbol invoked, + string methodName, + ImmutableArray constructorParameters + ) + { + foreach (var candidate in invoked.ContainingType.GetMembers(methodName).OfType()) + { + if ( + candidate.Parameters.Length != constructorParameters.Length + && !( + candidate.Parameters.Length == constructorParameters.Length + 1 + && IsConfigureParameter(candidate.Parameters[candidate.Parameters.Length - 1]) + ) + ) + continue; + + var matches = true; + for (var index = 0; index < constructorParameters.Length; index++) + { + if ( + !SymbolEqualityComparer.Default.Equals( + candidate.Parameters[index].Type, + constructorParameters[index].Type + ) + ) + { + matches = false; + break; + } + } + + if (!matches) + continue; + + var arguments = string.Join(", ", constructorParameters.Select(static parameter => parameter.Name)); + return $"{methodName}({arguments})"; + } + + return null; + } + + static bool IsConfigureParameter(IParameterSymbol parameter) => + parameter.Type is INamedTypeSymbol { TypeArguments.Length: 2 } named + && named.Name == "Func" + && named.ContainingNamespace?.Name == "System"; + + static bool HasObjectInitializer(SyntaxNode expression) => + expression switch + { + ObjectCreationExpressionSyntax { Initializer: { Expressions.Count: > 0 } } => true, + ImplicitObjectCreationExpressionSyntax { Initializer: { Expressions.Count: > 0 } } => true, + _ => false, + }; +} diff --git a/src/src/SourceGeneratorFramework.Analyzers/PreferPragmaDisableAnalyzer.cs b/src/src/SourceGeneratorFramework.Analyzers/PreferPragmaDisableAnalyzer.cs new file mode 100644 index 0000000..371f967 --- /dev/null +++ b/src/src/SourceGeneratorFramework.Analyzers/PreferPragmaDisableAnalyzer.cs @@ -0,0 +1,79 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Purview.SourceGeneratorFramework.Analyzers; + +/// +/// Flags raw CodeWriter text emission of a #pragma warning disable/restore +/// directive, suggesting PragmaDisable or OpenPragmasScope instead. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class PreferPragmaDisableAnalyzer : DiagnosticAnalyzer +{ + public const string DiagnosticId = "PSGFR22"; + + public static readonly DiagnosticDescriptor Rule = new( + DiagnosticId, + "Prefer PragmaDisable for warning suppression", + "'{0}' with a pragma warning directive should use 'PragmaDisable' or 'OpenPragmasScope'", + "Purview.SourceGeneratorFramework", + DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: "Emitting #pragma warning directives through raw text bypasses the structured PragmaDisable/OpenPragmasScope APIs." + ); + + public override ImmutableArray SupportedDiagnostics => [Rule]; + + public override void Initialize(AnalysisContext context) + { + if (context is null) + throw new ArgumentNullException(nameof(context)); + + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); + } + + static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) + { + if ( + context.Node + is not InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax member } invocation + ) + return; + + var name = member.Name.Identifier.Text; + if (name is not ("Write" or "Line" or "Append" or "AppendLine" or "MultiLine")) + return; + + if (invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression is not ExpressionSyntax expression) + return; + + if ( + context.SemanticModel.GetSymbolInfo(invocation, context.CancellationToken).Symbol + is not IMethodSymbol method + ) + return; + + if (method.ContainingType?.ToDisplayString() != "Purview.SourceGeneratorFramework.CodeWriter") + return; + + if ( + !CodeWriterLiteralClassifier.TryGetLiteralText( + expression, + context.SemanticModel, + context.CancellationToken, + out var value + ) + ) + return; + + if (value is null || !CodeWriterLiteralClassifier.IsPragmaWarningDirective(value)) + return; + + context.ReportDiagnostic(Diagnostic.Create(Rule, invocation.GetLocation(), name)); + } +} diff --git a/src/src/SourceGeneratorFramework.Analyzers/PreferStructuredCodeWriterApiAnalyzer.cs b/src/src/SourceGeneratorFramework.Analyzers/PreferStructuredCodeWriterApiAnalyzer.cs index 013f302..63b8dcc 100644 --- a/src/src/SourceGeneratorFramework.Analyzers/PreferStructuredCodeWriterApiAnalyzer.cs +++ b/src/src/SourceGeneratorFramework.Analyzers/PreferStructuredCodeWriterApiAnalyzer.cs @@ -8,7 +8,8 @@ namespace Purview.SourceGeneratorFramework.Analyzers; /// /// Flags raw CodeWriter text emission that starts with a C# declaration keyword, suggesting a -/// structured declaration API such as WriteClass or WriteProperty instead. +/// structured declaration API such as Class or Property instead. Plain, +/// interpolated, and raw string literals as well as constant expressions are inspected. /// [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class PreferStructuredCodeWriterApiAnalyzer : DiagnosticAnalyzer @@ -18,39 +19,13 @@ public sealed class PreferStructuredCodeWriterApiAnalyzer : DiagnosticAnalyzer public static readonly DiagnosticDescriptor Rule = new( DiagnosticId, "Prefer a structured CodeWriter declaration API", - "'{0}' with a declaration should use a structured API such as WriteClass, WriteMethod, WriteProperty, or WriteField", + "'{0}' with a declaration should use a structured API such as Class, Method, Property, or Field", "Purview.SourceGeneratorFramework", DiagnosticSeverity.Info, isEnabledByDefault: true, description: "Emitting declaration syntax through raw text bypasses the structured, deterministic declaration APIs on CodeWriter." ); - static readonly string[] DeclarationStarts = - [ - "public ", - "internal ", - "private ", - "protected ", - "file ", - "static ", - "sealed ", - "abstract ", - "partial ", - "readonly ", - "ref ", - "required ", - "const ", - "class ", - "struct ", - "interface ", - "enum ", - "record ", - "delegate ", - "namespace ", - "global using ", - "using ", - ]; - public override ImmutableArray SupportedDiagnostics => [Rule]; public override void Initialize(AnalysisContext context) @@ -72,13 +47,10 @@ static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) return; var name = member.Name.Identifier.Text; - if (name is not ("Write" or "WriteLine" or "Append" or "AppendLine" or "MultiLine")) - return; - - if (invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression is not LiteralExpressionSyntax literal) + if (name is not ("Write" or "Line" or "Append" or "AppendLine" or "MultiLine")) return; - if (!literal.IsKind(SyntaxKind.StringLiteralExpression)) + if (invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression is not ExpressionSyntax expression) return; if ( @@ -90,26 +62,23 @@ is not IMethodSymbol method if (method.ContainingType?.ToDisplayString() != "Purview.SourceGeneratorFramework.CodeWriter") return; - var value = literal.Token.ValueText.TrimStart(); - if (StartsWithDeclaration(value)) - context.ReportDiagnostic(Diagnostic.Create(Rule, invocation.GetLocation(), name)); - } - - static bool StartsWithDeclaration(string value) - { - foreach (var prefix in DeclarationStarts) - { - if (!value.StartsWith(prefix, StringComparison.Ordinal)) - continue; + if ( + !CodeWriterLiteralClassifier.TryGetLiteralText( + expression, + context.SemanticModel, + context.CancellationToken, + out var value + ) + ) + return; - // "using (var x = ...)" is a using statement inside a body, not a directive; don't flag it. - if (prefix == "using " && value.StartsWith("using (", StringComparison.Ordinal)) - return false; + if (value is null) + return; - // "namespace global::" is a valid namespace declaration, but "global::" is not a declaration keyword. - return true; - } + var trimmed = value.TrimStart(); + if (!CodeWriterLiteralClassifier.StartsWithDeclaration(trimmed)) + return; - return false; + context.ReportDiagnostic(Diagnostic.Create(Rule, invocation.GetLocation(), name)); } } diff --git a/src/src/SourceGeneratorFramework.Analyzers/PreferStructuredCodeWriterStatementAnalyzer.cs b/src/src/SourceGeneratorFramework.Analyzers/PreferStructuredCodeWriterStatementAnalyzer.cs new file mode 100644 index 0000000..86d8441 --- /dev/null +++ b/src/src/SourceGeneratorFramework.Analyzers/PreferStructuredCodeWriterStatementAnalyzer.cs @@ -0,0 +1,84 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Purview.SourceGeneratorFramework.Analyzers; + +/// +/// Flags raw CodeWriter text emission that writes a C# statement, suggesting a structured +/// statement API such as Return, MethodCall, Throw, +/// Assignment, Using, or Comment instead. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class PreferStructuredCodeWriterStatementAnalyzer : DiagnosticAnalyzer +{ + public const string DiagnosticId = "PSGFR19"; + + public static readonly DiagnosticDescriptor Rule = new( + DiagnosticId, + "Prefer a structured CodeWriter statement API", + "'{0}' with a statement should use '{1}'", + "Purview.SourceGeneratorFramework", + DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: "Emitting statement syntax through raw text bypasses the structured, deterministic statement APIs on CodeWriter." + ); + + public override ImmutableArray SupportedDiagnostics => [Rule]; + + public override void Initialize(AnalysisContext context) + { + if (context is null) + throw new ArgumentNullException(nameof(context)); + + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); + } + + static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) + { + if ( + context.Node + is not InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax member } invocation + ) + return; + + var name = member.Name.Identifier.Text; + if (name is not ("Write" or "Line" or "Append" or "AppendLine" or "MultiLine")) + return; + + if (invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression is not ExpressionSyntax expression) + return; + + if ( + context.SemanticModel.GetSymbolInfo(invocation, context.CancellationToken).Symbol + is not IMethodSymbol method + ) + return; + + if (method.ContainingType?.ToDisplayString() != "Purview.SourceGeneratorFramework.CodeWriter") + return; + + if ( + !CodeWriterLiteralClassifier.TryGetLiteralText( + expression, + context.SemanticModel, + context.CancellationToken, + out var value + ) + ) + return; + + if (value is null) + return; + + var structuredApi = CodeWriterLiteralClassifier.ClassifyStatement(value); + if (structuredApi is null) + return; + + context.ReportDiagnostic(Diagnostic.Create(Rule, invocation.GetLocation(), name, structuredApi)); + } +} diff --git a/src/src/SourceGeneratorFramework.Benchmarks/Benchmarks/CodeWriterBenchmarks.cs b/src/src/SourceGeneratorFramework.Benchmarks/Benchmarks/CodeWriterBenchmarks.cs index dc1f8b6..016d09c 100644 --- a/src/src/SourceGeneratorFramework.Benchmarks/Benchmarks/CodeWriterBenchmarks.cs +++ b/src/src/SourceGeneratorFramework.Benchmarks/Benchmarks/CodeWriterBenchmarks.cs @@ -14,16 +14,16 @@ public void Setup() } [Benchmark] - public string WriteManyClasses() + public string ManyClasses() { var writer = new CodeWriter(_settings); - writer.WriteAutoGeneratedHeader(); + writer.AutoGeneratedHeader(); for (var i = 0; i < 1000; i++) { - using (writer.WriteClassScope(new TypeDeclarationOptions($"Class{i}", TypeDeclarationAccessibility.Public))) + using (writer.ClassScope(new TypeDeclarationOptions($"Class{i}", TypeDeclarationAccessibility.Public))) { - writer.WriteLine("// body"); + writer.Line("// body"); } } diff --git a/src/src/SourceGeneratorFramework.ExampleGenerator/CodeWriterSampleEmitter.cs b/src/src/SourceGeneratorFramework.ExampleGenerator/CodeWriterSampleEmitter.cs new file mode 100644 index 0000000..d4d967d --- /dev/null +++ b/src/src/SourceGeneratorFramework.ExampleGenerator/CodeWriterSampleEmitter.cs @@ -0,0 +1,124 @@ +using System.Text; +using Microsoft.CodeAnalysis.Text; + +namespace Purview.SourceGeneratorFramework.Examples; + +/// +/// Emits the GenerateCodeWriterSampleAttribute and the sample class generated by +/// , demonstrating the structured API. +/// +static class CodeWriterSampleEmitter +{ + /// + /// Emits the GenerateCodeWriterSampleAttribute marker attribute. + /// + public static void EmitAttribute(IncrementalGeneratorPostInitializationContext spc) + { + CodeWriter writer = new(GenerationSettings.Create(), throwOnUnclosedScopes: false); + + writer.AutoGeneratedHeader(); + writer.FileScopedNamespace(TypeLibrary.GenerateCodeWriterSampleAttribute); + + writer.AttributeClass( + TypeLibrary.GenerateCodeWriterSampleAttribute.Name, + TypeDeclarationAccessibility.Public, + AttributeTargets.Class, + _ => { }, + configure: options => options with { IsPartial = false } + ); + + spc.AddSource("GenerateCodeWriterSampleAttribute.g.cs", SourceText.From(writer.ToString(), Encoding.UTF8)); + } + + /// + /// Emits the sample class demonstrating the minimal overloads, scoped declarations, structured + /// statements, and . + /// + public static void Execute(SourceProductionContext spc, CodeWriterSampleTarget target) + { + if (string.IsNullOrWhiteSpace(target.TypeName)) + return; + + CodeWriter writer = new(GenerationSettings.Create(), throwOnUnclosedScopes: false); + + writer.AutoGeneratedHeader(); + writer.PragmaDisable("CS8625"); + writer.FileScopedNamespace(target.Namespace ?? "Purview.SourceGeneratorFramework.Examples.Samples"); + + writer.Comment("Generated by the CodeWriter sample generator."); + + writer.Class( + target.TypeName + "CodeWriterSample", + TypeDeclarationAccessibility.Public, + options => options with { IsSealed = true, IsPartial = false }, + body => + { + body.Field( + "_value", + TypeIdentity.Create().AsTypeReference(), + TypeDeclarationAccessibility.Private, + options => options with { IsReadOnly = true } + ); + + body.Constructor( + target.TypeName + "CodeWriterSample", + TypeDeclarationAccessibility.Public, + options => + options with + { + Parameters = [new("value", TypeIdentity.Create().AsTypeReference())], + }, + constructorBody => constructorBody.Assignment("_value", "value") + ); + + body.Property( + "Value", + TypeIdentity.Create().AsTypeReference(), + TypeDeclarationAccessibility.Public, + options => options with { HasGetter = true } + ); + + // The accessibility is omitted, so the writer's default (public) is applied. + body.Property("DefaultAccessibility", TypeIdentity.Create().AsTypeReference()); + + body.HashDefines( + "NET", + conditional => conditional.Comment("This member is emitted only for .NET targets.") + ); + + using (body.OpenPragmasScope("CS0618")) + { + body.Comment("Emitted with the warning suppressed."); + } + + body.Method( + "Describe", + TypeIdentity.Create().AsTypeReference(), + TypeDeclarationAccessibility.Public, + options => + options with + { + IsStatic = true, + Parameters = [new("value", TypeIdentity.Create().AsTypeReference())], + }, + methodBody => + { + methodBody.MethodCall("global::System.Console.WriteLine", "\"Describe\""); + methodBody.Return("value.ToString()"); + } + ); + + using ( + body.MethodScope( + "Format", + TypeIdentity.Create().AsTypeReference(), + TypeDeclarationAccessibility.Public + ) + ) + body.NetConditionalReturn("Value: {_value}"); + } + ); + + spc.AddSource($"{target.TypeName}.CodeWriterSample.g.cs", SourceText.From(writer.ToString(), Encoding.UTF8)); + } +} diff --git a/src/src/SourceGeneratorFramework.ExampleGenerator/CodeWriterSampleGenerator.cs b/src/src/SourceGeneratorFramework.ExampleGenerator/CodeWriterSampleGenerator.cs new file mode 100644 index 0000000..51c9fe5 --- /dev/null +++ b/src/src/SourceGeneratorFramework.ExampleGenerator/CodeWriterSampleGenerator.cs @@ -0,0 +1,44 @@ +namespace Purview.SourceGeneratorFramework.Examples; + +/// +/// Generates a sample class for every type annotated with the GenerateCodeWriterSampleAttribute, +/// demonstrating the structured API. +/// +[Generator] +public partial class CodeWriterSampleGenerator : IIncrementalGenerator +{ + /// + /// Initializes the generator pipeline. + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context + .RegisterEmbeddedAttribute() + .RegisterPostInitializationOutput(CodeWriterSampleEmitter.EmitAttribute); + + var targets = IncrementalPipeline.ForAttributeWithMetadataName( + context, + TypeLibrary.GenerateCodeWriterSampleAttribute, + static (ctx, ct) => + { + if (ctx.SemanticModel.GetDeclaredSymbol(ctx.TargetNode, ct) is not INamedTypeSymbol symbol) + return default; + + return new CodeWriterSampleTarget( + TypeName: symbol.Name, + Namespace: symbol.ContainingNamespace is { IsGlobalNamespace: false } containingNamespace + ? containingNamespace.ToDisplayString() + : null + ); + }, + trackingName: "ForAttribute_GenerateCodeWriterSample" + ); + + context.RegisterSourceOutput(targets, static (spc, target) => CodeWriterSampleEmitter.Execute(spc, target)); + } +} + +/// +/// Identifies a type annotated with the GenerateCodeWriterSampleAttribute. +/// +readonly record struct CodeWriterSampleTarget(string TypeName, string? Namespace); diff --git a/src/src/SourceGeneratorFramework.ExampleGenerator/README.md b/src/src/SourceGeneratorFramework.ExampleGenerator/README.md index b0fde1f..e7fdc21 100644 --- a/src/src/SourceGeneratorFramework.ExampleGenerator/README.md +++ b/src/src/SourceGeneratorFramework.ExampleGenerator/README.md @@ -27,7 +27,8 @@ Reference implementation of an incremental C# source generator built with `Purvi - Incremental pipeline with `IncrementalPipeline.ForAttributeWithMetadataName`. - Attribute-data model (`GenerateServiceAttributeData`) generated by `AttributeDataModelGenerator` using `[Argument]` for a constructor parameter and `[Property]` for a named property. - Enum string extraction from `TypedConstant` values (`IsEnum = true`). -- `CodeWriter` usage for all output, including post-initialization sources and attribute/enum declarations. +- `CodeWriter` usage for all output, including post-initialization sources and attribute/enum declarations. See the [CodeWriter API reference](../../docs/code-writer.md) for the structured API and best practices. +- A dedicated `CodeWriterSampleGenerator` that emits a best-practice sample class for every `[GenerateCodeWriterSample]` target, demonstrating minimal overloads, structured statements, scope usage, and `NetConditionalReturn`. - `TypeValueObject` / `TypeLibrary` helpers for safe type/namespace references. - Diagnostic reporting for invalid inputs (e.g. interfaces, static classes, nested classes, abstract classes). - Generator disabling via MSBuild properties. diff --git a/src/src/SourceGeneratorFramework.ExampleGenerator/ServiceRegistrationEmitter.cs b/src/src/SourceGeneratorFramework.ExampleGenerator/ServiceRegistrationEmitter.cs index f87a6cf..e27c430 100644 --- a/src/src/SourceGeneratorFramework.ExampleGenerator/ServiceRegistrationEmitter.cs +++ b/src/src/SourceGeneratorFramework.ExampleGenerator/ServiceRegistrationEmitter.cs @@ -19,45 +19,54 @@ public static void EmitAttributeAndEnum(IncrementalGeneratorPostInitializationCo throwOnUnclosedScopes: false ); - writer.WriteAutoGeneratedHeader(); - writer.WriteFileScopedNamespace(TypeLibrary.GenerateServiceAttribute); + writer.AutoGeneratedHeader(); + writer.FileScopedNamespace(TypeLibrary.GenerateServiceAttribute); - writer.WriteEnum( - new(TypeLibrary.ServiceLifetime, TypeDeclarationAccessibility.Public) { IsPartial = false }, + writer.Enum( + TypeLibrary.ServiceLifetime.Name, + TypeDeclarationAccessibility.Public, + options => options with { IsPartial = false }, ew => { - ew.WriteLine("Singleton = 0,"); - ew.WriteLine("Scoped = 1,"); - ew.WriteLine("Transient = 2,"); + ew.Line("Singleton = 0,"); + ew.Line("Scoped = 1,"); + ew.Line("Transient = 2,"); } ); - writer.WriteAttributeClass( - new(TypeLibrary.GenerateServiceAttribute, TypeDeclarationAccessibility.Public) { IsPartial = false }, + writer.AttributeClass( + TypeLibrary.GenerateServiceAttribute.Name, + TypeDeclarationAccessibility.Public, AttributeTargets.Class, cw => { - cw.WriteConstructor( - new("GenerateServiceAttribute", TypeDeclarationAccessibility.Public) - { - Parameters = - [ - new("lifetime", TypeLibrary.ServiceLifetime) { DefaultValue = "ServiceLifetime.Singleton" }, - ], - }, - body => body.WriteLine("Lifetime = lifetime;") + cw.Constructor( + "GenerateServiceAttribute", + TypeDeclarationAccessibility.Public, + options => + options with + { + Parameters = + [ + new("lifetime", TypeLibrary.ServiceLifetime) + { + DefaultValue = "ServiceLifetime.Singleton", + }, + ], + }, + body => body.Assignment("Lifetime", "lifetime") ); - cw.WriteProperty(new("Lifetime", TypeLibrary.ServiceLifetime, TypeDeclarationAccessibility.Public)); + cw.Property("Lifetime", TypeLibrary.ServiceLifetime, TypeDeclarationAccessibility.Public); - cw.WriteProperty( - new("Name", PurviewTypeLibrary.System.String.MakeNullable(cw), TypeDeclarationAccessibility.Public) - { - HasSetter = true, - Initializer = "null", - } + cw.Property( + "Name", + PurviewTypeLibrary.System.String.MakeNullable(cw), + TypeDeclarationAccessibility.Public, + options => options with { HasSetter = true, Initializer = "null" } ); - } + }, + configure: options => options with { IsPartial = false } ); spc.AddSource("GenerateServiceAttribute.g.cs", SourceText.From(writer.ToString(), Encoding.UTF8)); @@ -81,43 +90,44 @@ public static void Execute(SourceProductionContext spc, ServiceRegistrationGener } var writer = model.Context.CreateCodeWriter(); - writer.WriteAutoGeneratedHeader(); - writer.WriteFileScopedNamespace(TypeLibrary.ServiceCollectionExtensions); + writer.AutoGeneratedHeader(); + writer.FileScopedNamespace(TypeLibrary.ServiceCollectionExtensions); - writer.WriteClass( - new(TypeLibrary.ServiceCollectionExtensions, TypeDeclarationAccessibility.Public) - { - IsStatic = true, - IsPartial = false, - }, + writer.Class( + TypeLibrary.ServiceCollectionExtensions.Name, + TypeDeclarationAccessibility.Public, + options => options with { IsStatic = true, IsPartial = false }, cw => - cw.WriteMethod( - new( - "AddExampleServices", - TypeLibrary.Microsoft.Extensions.DependencyInjection.IServiceCollection, - TypeDeclarationAccessibility.Public - ) - { - IsStatic = true, - Parameters = - [ - new("services", TypeLibrary.Microsoft.Extensions.DependencyInjection.IServiceCollection) - { - IsThis = true, - }, - ], - }, + cw.Method( + "AddExampleServices", + TypeLibrary.Microsoft.Extensions.DependencyInjection.IServiceCollection, + TypeDeclarationAccessibility.Public, + options => + options with + { + IsStatic = true, + Parameters = + [ + new("services", TypeLibrary.Microsoft.Extensions.DependencyInjection.IServiceCollection) + { + IsThis = true, + }, + ], + }, body => { foreach (var target in model.Targets) { body.Comment($"Service name: {target.Name}"); - body.WriteLine( - $"{TypeLibrary.Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions}.Add{target.LifetimeMemberName}<{target.TypeName}>(services);" + body.MethodCall( + $"Add{target.LifetimeMemberName}", + ["services"], + TypeLibrary.Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.ToString(), + [new TypeReference(new TypeIdentity(target.TypeName, null))] ); } - body.WriteLine("return services;"); + body.Return("services"); } ) ); @@ -131,54 +141,52 @@ public static void Execute(SourceProductionContext spc, ServiceRegistrationGener static void EmitServiceInfo(SourceProductionContext spc, ServiceRegistrationGenerationModel model) { var writer = model.Context.CreateCodeWriter(); - writer.WriteAutoGeneratedHeader(); - writer.WriteFileScopedNamespace(TypeLibrary.ServiceInfo); + writer.AutoGeneratedHeader(); + writer.FileScopedNamespace(TypeLibrary.ServiceInfo); - writer.WriteClass( - new(TypeLibrary.ServiceInfo, TypeDeclarationAccessibility.Public) { IsStatic = true, IsPartial = false }, + writer.Class( + TypeLibrary.ServiceInfo.Name, + TypeDeclarationAccessibility.Public, + options => options with { IsStatic = true, IsPartial = false }, cw => { foreach (var target in model.Targets) { - cw.WriteClass( - new(target.ClassName) - { - Accessibility = TypeDeclarationAccessibility.Public, - IsStatic = true, - IsPartial = false, - }, + cw.Class( + target.ClassName, + TypeDeclarationAccessibility.Public, + options => options with { IsStatic = true, IsPartial = false }, inner => { - inner.WriteProperty( - new( - "Name", - TypeIdentity.Create().AsTypeReference(), - TypeDeclarationAccessibility.Public - ) - { - IsStatic = true, - ExpressionBody = $"\"{target.Name}\"", - } + inner.Property( + "Name", + TypeIdentity.Create().AsTypeReference(), + TypeDeclarationAccessibility.Public, + options => options with { IsStatic = true, ExpressionBody = $"\"{target.Name}\"" } ); - inner.WriteProperty( - new( - "Lifetime", - TypeIdentity.Create().AsTypeReference(), - TypeDeclarationAccessibility.Public - ) - { - IsStatic = true, - ExpressionBody = $"\"{target.LifetimeMemberName}\"", - } + inner.Property( + "Lifetime", + TypeIdentity.Create().AsTypeReference(), + TypeDeclarationAccessibility.Public, + options => + options with + { + IsStatic = true, + ExpressionBody = $"\"{target.LifetimeMemberName}\"", + } ); - inner.WriteProperty( - new("Type", PurviewTypeLibrary.System.Type, TypeDeclarationAccessibility.Public) - { - IsStatic = true, - ExpressionBody = $"typeof({target.TypeName})", - } + inner.Property( + "Type", + PurviewTypeLibrary.System.Type, + TypeDeclarationAccessibility.Public, + options => + options with + { + IsStatic = true, + ExpressionBody = $"typeof({target.TypeName})", + } ); } ); diff --git a/src/src/SourceGeneratorFramework.ExampleGenerator/TypeLibrary.cs b/src/src/SourceGeneratorFramework.ExampleGenerator/TypeLibrary.cs index 16bf75f..83f3cc0 100644 --- a/src/src/SourceGeneratorFramework.ExampleGenerator/TypeLibrary.cs +++ b/src/src/SourceGeneratorFramework.ExampleGenerator/TypeLibrary.cs @@ -65,4 +65,12 @@ public static class DependencyInjection /// The static ServiceInfo class. /// public static readonly TypeIdentity ServiceInfo = new(nameof(ServiceInfo), ExpamplesNamespace); + + /// + /// The [GenerateCodeWriterSample] attribute type. + /// + public static readonly TypeIdentity GenerateCodeWriterSampleAttribute = new( + nameof(GenerateCodeWriterSampleAttribute), + ExpamplesNamespace + ); } diff --git a/src/src/SourceGeneratorFramework.Generators/AttributeDataModelGenerator.cs b/src/src/SourceGeneratorFramework.Generators/AttributeDataModelGenerator.cs index cdf9d23..84adaac 100644 --- a/src/src/SourceGeneratorFramework.Generators/AttributeDataModelGenerator.cs +++ b/src/src/SourceGeneratorFramework.Generators/AttributeDataModelGenerator.cs @@ -60,23 +60,23 @@ GenerationContext generationContext var writer = generationContext.CreateCodeWriter(); - writer.WriteAutoGeneratedHeader(); - writer.WriteUsing("Purview.SourceGeneratorFramework").NewLine(); + writer.AutoGeneratedHeader(); + writer.Using("Purview.SourceGeneratorFramework").NewLine(); if (string.IsNullOrEmpty(target.Namespace)) { - WriteStruct(writer, target); + Struct(writer, target); } else { - using (writer.WriteBlockNamespaceScope(target.Namespace)) - WriteStruct(writer, target); + using (writer.BlockNamespaceScope(target.Namespace)) + Struct(writer, target); } spc.AddSource($"{target.StructName}.AttributeDataModel.g.cs", writer); } - static void WriteStruct(CodeWriter writer, AttributeDataModelTarget target) + static void Struct(CodeWriter writer, AttributeDataModelTarget target) { TypeDeclarationOptions options = new(target.StructName, target.Accessibility) { @@ -84,16 +84,16 @@ static void WriteStruct(CodeWriter writer, AttributeDataModelTarget target) IsPartial = true, }; - using (target.IsRecord ? writer.WriteRecordStructScope(options) : writer.WriteStructScope(options)) + using (target.IsRecord ? writer.RecordStructScope(options) : writer.StructScope(options)) { - writer.WriteProperty(new("Exists", TypeReference("bool"), TypeDeclarationAccessibility.Public)); + writer.Property(new("Exists", TypeReference("bool"), TypeDeclarationAccessibility.Public)); foreach (var property in target.Properties) { if (property.IsExplicit) continue; - writer.WriteProperty( + writer.Property( new( property.PropertyName, TypeReference(property.FullyQualifiedTypeName), @@ -105,33 +105,33 @@ static void WriteStruct(CodeWriter writer, AttributeDataModelTarget target) ); } - WriteConstructor(writer, target); - WriteTargetAttributeField(writer, target); - WriteEmptyField(writer, target); - WriteAllAttributeDataMethod(writer, target); - WriteAllAttributeDataSymbolMethod(writer, target); + Constructor(writer, target); + TargetAttributeField(writer, target); + EmptyField(writer, target); + AllAttributeDataMethod(writer, target); + AllAttributeDataSymbolMethod(writer, target); - WriteFromAttributeDataArrayMethod(writer, target); + FromAttributeDataArrayMethod(writer, target); // ISymbol attribute extraction methods with out parameters - WriteFromAttributeDataSymbolMethod(writer, target); - WriteTryFromAttributeDataSymbolMethod(writer, target); + FromAttributeDataSymbolMethod(writer, target); + TryFromAttributeDataSymbolMethod(writer, target); // AttributeData array extraction methods with out parameters - WriteFromAttributeDataArrayWithOutMethod(writer, target); - WriteTryFromAttributeDataArrayWithOutMethod(writer, target); + FromAttributeDataArrayWithOutMethod(writer, target); + TryFromAttributeDataArrayWithOutMethod(writer, target); // ISymbol attribute extraction methods with out parameters - WriteFromAttributeDataSymbolWithOutMethod(writer, target); - WriteTryFromAttributeDataSymbolWithOutMethod(writer, target); + FromAttributeDataSymbolWithOutMethod(writer, target); + TryFromAttributeDataSymbolWithOutMethod(writer, target); // AttributeData extraction methods - WriteFromAttributeDataMethod(writer, target); - WriteTryFromAttributeDataMethod(writer, target); + FromAttributeDataMethod(writer, target); + TryFromAttributeDataMethod(writer, target); } } - static void WriteConstructor(CodeWriter writer, AttributeDataModelTarget target) + static void Constructor(CodeWriter writer, AttributeDataModelTarget target) { var parameters = ImmutableArray.CreateBuilder(); parameters.Add(new("exists", TypeReference("bool"))); @@ -141,7 +141,7 @@ static void WriteConstructor(CodeWriter writer, AttributeDataModelTarget target) parameters.Add(new(property.PropertyName, TypeReference(property.FullyQualifiedTypeName))); } - writer.WriteConstructor( + writer.Constructor( new(target.StructName, TypeDeclarationAccessibility.Public) { Parameters = [.. parameters], @@ -149,11 +149,11 @@ static void WriteConstructor(CodeWriter writer, AttributeDataModelTarget target) }, body => { - body.WriteAssignment("Exists", "exists"); + body.Assignment("Exists", "exists"); foreach (var property in target.Properties) { if (!property.IsExplicit) - body.WriteAssignment($"this.{property.PropertyName}", property.PropertyName); + body.Assignment($"this.{property.PropertyName}", property.PropertyName); } } ); @@ -166,9 +166,9 @@ static void WriteConstructor(CodeWriter writer, AttributeDataModelTarget target) : $"this({string.Join(", ", target.PrimaryConstructorArguments)})"; } - static void WriteTargetAttributeField(CodeWriter writer, AttributeDataModelTarget target) + static void TargetAttributeField(CodeWriter writer, AttributeDataModelTarget target) { - writer.WriteField( + writer.Field( new( "TargetAttribute", GeneratorTypeLibrary.TypeValueObject.AsTypeReference(), @@ -182,7 +182,7 @@ static void WriteTargetAttributeField(CodeWriter writer, AttributeDataModelTarge ); } - static void WriteEmptyField(CodeWriter writer, AttributeDataModelTarget target) + static void EmptyField(CodeWriter writer, AttributeDataModelTarget target) { var values = new List { "false" }; foreach (var property in target.Properties) @@ -194,7 +194,7 @@ static void WriteEmptyField(CodeWriter writer, AttributeDataModelTarget target) values.Add(value); } - writer.WriteField( + writer.Field( new("Empty", TypeReference(target.StructName), TypeDeclarationAccessibility.Public) { IsStatic = true, @@ -204,7 +204,7 @@ static void WriteEmptyField(CodeWriter writer, AttributeDataModelTarget target) ); } - static void WriteAllAttributeDataMethod(CodeWriter writer, AttributeDataModelTarget target) + static void AllAttributeDataMethod(CodeWriter writer, AttributeDataModelTarget target) { var returnType = TypeReference( $"global::System.Collections.Generic.IEnumerable<({target.StructName} Instance, global::Microsoft.CodeAnalysis.AttributeData Attribute)>" @@ -227,43 +227,40 @@ static void WriteAllAttributeDataMethod(CodeWriter writer, AttributeDataModelTar ], }; - writer.WriteMethod( + writer.Method( methodOptions, body => { - body.WriteLine("for (var i = 0; i < attributes.Length; i++)"); - body.WriteBlock( + body.Line("for (var i = 0; i < attributes.Length; i++)"); + body.Block( null, loopBody => { - loopBody.WriteLine("var instance = FromAttributeData(attributes[i]);"); - loopBody.WriteLine("if (instance.Exists)"); - loopBody.WriteBlock( - null, - matchBody => matchBody.WriteLine("yield return (instance, attributes[i]);") - ); + loopBody.Line("var instance = FromAttributeData(attributes[i]);"); + loopBody.Line("if (instance.Exists)"); + loopBody.Block(null, matchBody => matchBody.Line("yield return (instance, attributes[i]);")); } ); } ); } - static void WriteAllAttributeDataSymbolMethod(CodeWriter writer, AttributeDataModelTarget target) + static void AllAttributeDataSymbolMethod(CodeWriter writer, AttributeDataModelTarget target) { var returnType = TypeReference( $"global::System.Collections.Generic.IEnumerable<({target.StructName} Instance, global::Microsoft.CodeAnalysis.AttributeData Attribute)>" ); - writer.WriteMethod( + writer.Method( new MethodDeclarationOptions("AllAttributeData", returnType, TypeDeclarationAccessibility.Public) { IsStatic = true, Parameters = [new("symbol", TypeReference("global::Microsoft.CodeAnalysis.ISymbol"))], }, - body => body.WriteLine("return AllAttributeData(symbol.GetAttributes());") + body => body.Line("return AllAttributeData(symbol.GetAttributes());") ); } - static void WriteFromAttributeDataArrayMethod(CodeWriter writer, AttributeDataModelTarget target) + static void FromAttributeDataArrayMethod(CodeWriter writer, AttributeDataModelTarget target) { MethodDeclarationOptions methodOptions = new( "FromAttributeData", @@ -283,40 +280,40 @@ static void WriteFromAttributeDataArrayMethod(CodeWriter writer, AttributeDataMo ], }; - writer.WriteMethod( + writer.Method( methodOptions, w => { - w.WriteLine("for (var i = 0; i < attributes.Length; i++)"); - w.WriteBlock( + w.Line("for (var i = 0; i < attributes.Length; i++)"); + w.Block( null, body => { - body.WriteLine("var result = FromAttributeData(attributes[i]);"); - body.WriteLine("if (result.Exists)"); - body.WriteBlock(null, inner => inner.WriteLine("return result;")); + body.Line("var result = FromAttributeData(attributes[i]);"); + body.Line("if (result.Exists)"); + body.Block(null, inner => inner.Line("return result;")); } ); - w.WriteLine("return Empty;"); + w.Line("return Empty;"); } ); } - static void WriteFromAttributeDataSymbolMethod(CodeWriter writer, AttributeDataModelTarget target) + static void FromAttributeDataSymbolMethod(CodeWriter writer, AttributeDataModelTarget target) { - writer.WriteMethod( + writer.Method( new("FromAttributeData", TypeReference(target.StructName), TypeDeclarationAccessibility.Public) { IsStatic = true, Parameters = [new("symbol", TypeReference("global::Microsoft.CodeAnalysis.ISymbol"))], }, - body => body.WriteLine("return FromAttributeData(symbol.GetAttributes());") + body => body.Line("return FromAttributeData(symbol.GetAttributes());") ); } - static void WriteTryFromAttributeDataSymbolMethod(CodeWriter writer, AttributeDataModelTarget target) + static void TryFromAttributeDataSymbolMethod(CodeWriter writer, AttributeDataModelTarget target) { - writer.WriteMethod( + writer.Method( new("TryFromAttributeData", PurviewTypeLibrary.System.Boolean, TypeDeclarationAccessibility.Public) { IsStatic = true, @@ -326,11 +323,11 @@ static void WriteTryFromAttributeDataSymbolMethod(CodeWriter writer, AttributeDa new("attributeData", TypeReference(target.StructName), ParameterModifier.Out), ], }, - body => body.WriteLine("return TryFromAttributeData(symbol.GetAttributes(), out attributeData, out _);") + body => body.Line("return TryFromAttributeData(symbol.GetAttributes(), out attributeData, out _);") ); } - static void WriteFromAttributeDataArrayWithOutMethod(CodeWriter writer, AttributeDataModelTarget target) + static void FromAttributeDataArrayWithOutMethod(CodeWriter writer, AttributeDataModelTarget target) { var methodOptions = new MethodDeclarationOptions( "FromAttributeData", @@ -354,34 +351,34 @@ static void WriteFromAttributeDataArrayWithOutMethod(CodeWriter writer, Attribut ], }; - writer.WriteMethod( + writer.Method( methodOptions, w => { - w.WriteLine("attribute = null;"); - w.WriteLine("for (var i = 0; i < attributes.Length; i++)"); - w.WriteBlock( + w.Line("attribute = null;"); + w.Line("for (var i = 0; i < attributes.Length; i++)"); + w.Block( null, body => { - body.WriteLine("var result = FromAttributeData(attributes[i]);"); - body.WriteLine("if (result.Exists)"); - body.WriteBlock( + body.Line("var result = FromAttributeData(attributes[i]);"); + body.Line("if (result.Exists)"); + body.Block( null, inner => { - inner.WriteLine("attribute = attributes[i];"); - inner.WriteLine("return result;"); + inner.Line("attribute = attributes[i];"); + inner.Line("return result;"); } ); } ); - w.WriteLine("return Empty;"); + w.Line("return Empty;"); } ); } - static void WriteTryFromAttributeDataArrayWithOutMethod(CodeWriter writer, AttributeDataModelTarget target) + static void TryFromAttributeDataArrayWithOutMethod(CodeWriter writer, AttributeDataModelTarget target) { var methodOptions = new MethodDeclarationOptions( "TryFromAttributeData", @@ -406,39 +403,39 @@ static void WriteTryFromAttributeDataArrayWithOutMethod(CodeWriter writer, Attri ], }; - writer.WriteMethod( + writer.Method( methodOptions, w => { - w.WriteAssignment("attributeData", "Empty"); - w.WriteAssignment("attribute", "null"); + w.Assignment("attributeData", "Empty"); + w.Assignment("attribute", "null"); - w.WriteLine("for (var i = 0; i < attributes.Length; i++)"); - w.WriteBlock( + w.Line("for (var i = 0; i < attributes.Length; i++)"); + w.Block( null, body => { - body.WriteLine("var result = FromAttributeData(attributes[i]);"); - body.WriteLine("if (result.Exists)"); - body.WriteBlock( + body.Line("var result = FromAttributeData(attributes[i]);"); + body.Line("if (result.Exists)"); + body.Block( null, inner => { - inner.WriteLine("attribute = attributes[i];"); - inner.WriteLine("attributeData = result;"); - inner.WriteLine("return true;"); + inner.Line("attribute = attributes[i];"); + inner.Line("attributeData = result;"); + inner.Line("return true;"); } ); } ); - w.WriteLine("return false;"); + w.Line("return false;"); } ); } - static void WriteFromAttributeDataSymbolWithOutMethod(CodeWriter writer, AttributeDataModelTarget target) + static void FromAttributeDataSymbolWithOutMethod(CodeWriter writer, AttributeDataModelTarget target) { - writer.WriteMethod( + writer.Method( new MethodDeclarationOptions( "FromAttributeData", TypeReference(target.StructName), @@ -455,13 +452,13 @@ static void WriteFromAttributeDataSymbolWithOutMethod(CodeWriter writer, Attribu }, ], }, - body => body.WriteLine("return FromAttributeData(symbol.GetAttributes(), out attribute);") + body => body.Line("return FromAttributeData(symbol.GetAttributes(), out attribute);") ); } - static void WriteTryFromAttributeDataSymbolWithOutMethod(CodeWriter writer, AttributeDataModelTarget target) + static void TryFromAttributeDataSymbolWithOutMethod(CodeWriter writer, AttributeDataModelTarget target) { - writer.WriteMethod( + writer.Method( new MethodDeclarationOptions( "TryFromAttributeData", TypeReference(PurviewTypeLibrary.System.Boolean), @@ -483,12 +480,11 @@ static void WriteTryFromAttributeDataSymbolWithOutMethod(CodeWriter writer, Attr }, ], }, - body => - body.WriteLine("return TryFromAttributeData(symbol.GetAttributes(), out attributeData, out attribute);") + body => body.Line("return TryFromAttributeData(symbol.GetAttributes(), out attributeData, out attribute);") ); } - static void WriteFromAttributeDataMethod(CodeWriter writer, AttributeDataModelTarget target) + static void FromAttributeDataMethod(CodeWriter writer, AttributeDataModelTarget target) { var methodOptions = new MethodDeclarationOptions( "FromAttributeData", @@ -500,25 +496,25 @@ static void WriteFromAttributeDataMethod(CodeWriter writer, AttributeDataModelTa Parameters = [new("attributeData", TypeReference("global::Microsoft.CodeAnalysis.AttributeData"))], }; - writer.WriteMethod( + writer.Method( methodOptions, w => { if (target.MatchByInheritance) { - w.WriteLine( + w.Line( "if (attributeData.AttributeClass is null || (!TargetAttribute.Equals(attributeData.AttributeClass) && !global::Purview.SourceGeneratorFramework.Helpers.TypeHelpers.InheritsFrom(attributeData.AttributeClass, TargetAttribute)))" ); } else { - w.WriteLine("if (!TargetAttribute.Equals(attributeData.AttributeClass))"); + w.Line("if (!TargetAttribute.Equals(attributeData.AttributeClass))"); } - w.WriteBlock(null, body => body.WriteLine("return Empty;")); + w.Block(null, body => body.Line("return Empty;")); foreach (var property in target.Properties) { - WritePropertyExtraction(w, property); + PropertyExtraction(w, property); } var returnValues = new List { "true" }; @@ -528,12 +524,12 @@ static void WriteFromAttributeDataMethod(CodeWriter writer, AttributeDataModelTa returnValues.Add(property.IsNonNullableReferenceType ? value + "!" : value); } - w.WriteLine($"return new({string.Join(", ", returnValues)});"); + w.Line($"return new({string.Join(", ", returnValues)});"); } ); } - static void WriteTryFromAttributeDataMethod(CodeWriter writer, AttributeDataModelTarget target) + static void TryFromAttributeDataMethod(CodeWriter writer, AttributeDataModelTarget target) { var methodOptions = new MethodDeclarationOptions( "TryFromAttributeData", @@ -549,26 +545,26 @@ static void WriteTryFromAttributeDataMethod(CodeWriter writer, AttributeDataMode ], }; - writer.WriteMethod( + writer.Method( methodOptions, w => { - w.WriteAssignment("attribute", "Empty"); + w.Assignment("attribute", "Empty"); if (target.MatchByInheritance) { - w.WriteLine( + w.Line( "if (attributeData.AttributeClass is null || (!TargetAttribute.Equals(attributeData.AttributeClass) && !global::Purview.SourceGeneratorFramework.Helpers.TypeHelpers.InheritsFrom(attributeData.AttributeClass, TargetAttribute)))" ); } else { - w.WriteLine("if (!TargetAttribute.Equals(attributeData.AttributeClass))"); + w.Line("if (!TargetAttribute.Equals(attributeData.AttributeClass))"); } - w.WriteBlock(null, body => body.WriteLine("return false;")); + w.Block(null, body => body.Line("return false;")); foreach (var property in target.Properties) { - WritePropertyExtraction(w, property); + PropertyExtraction(w, property); } var returnValues = new List { "true" }; @@ -578,14 +574,14 @@ static void WriteTryFromAttributeDataMethod(CodeWriter writer, AttributeDataMode returnValues.Add(property.IsNonNullableReferenceType ? value + "!" : value); } - w.WriteAssignment("attribute", $"new({string.Join(", ", returnValues)})"); + w.Assignment("attribute", $"new({string.Join(", ", returnValues)})"); - w.WriteReturn("attribute.Exists"); + w.Return("attribute.Exists"); } ); } - static void WritePropertyExtraction(CodeWriter writer, AttributeDataModelProperty property) + static void PropertyExtraction(CodeWriter writer, AttributeDataModelProperty property) { var variableName = ToCamelCase(property.PropertyName); var typeName = GetNonNullableTypeName(property.FullyQualifiedTypeName); @@ -596,20 +592,20 @@ static void WritePropertyExtraction(CodeWriter writer, AttributeDataModelPropert if (property.IsNestedModel) { - writer.WriteLine($"var {variableName} = {property.NestedModelTypeName}.FromAttributeData(attributeData);"); + writer.Line($"var {variableName} = {property.NestedModelTypeName}.FromAttributeData(attributeData);"); return; } if (property.IsEnum) { - WriteEnumPropertyExtraction(writer, property, variableName); + EnumPropertyExtraction(writer, property, variableName); return; } var sources = property.Sources.AsImmutableArray(); if (sources.IsEmpty) { - writer.WriteLine($"var {variableName} = default({typeName});"); + writer.Line($"var {variableName} = default({typeName});"); return; } @@ -618,7 +614,7 @@ static void WritePropertyExtraction(CodeWriter writer, AttributeDataModelPropert var source = sources[0]; if (property.HasDefaultValue) { - WriteSingleSourceExtractionWithDefault( + SingleSourceExtractionWithDefault( writer, source, variableName, @@ -628,12 +624,12 @@ static void WritePropertyExtraction(CodeWriter writer, AttributeDataModelPropert } else { - WriteSingleSourceExtraction(writer, source, variableName, typeName); + SingleSourceExtraction(writer, source, variableName, typeName); } return; } - WriteMultiSourceExtraction( + MultiSourceExtraction( writer, sources, variableName, @@ -644,18 +640,13 @@ static void WritePropertyExtraction(CodeWriter writer, AttributeDataModelPropert ); } - static void WriteSingleSourceExtraction( - CodeWriter writer, - PropertySource source, - string variableName, - string typeName - ) + static void SingleSourceExtraction(CodeWriter writer, PropertySource source, string variableName, string typeName) { var extraction = GetSingleSourceExtractionExpression(source, variableName, typeName); - writer.WriteLine(extraction); + writer.Line(extraction); } - static void WriteSingleSourceExtractionWithDefault( + static void SingleSourceExtractionWithDefault( CodeWriter writer, PropertySource source, string variableName, @@ -669,10 +660,10 @@ string defaultValueExpression typeName, defaultValueExpression ); - writer.WriteLine(extraction); + writer.Line(extraction); } - static void WriteMultiSourceExtraction( + static void MultiSourceExtraction( CodeWriter writer, ImmutableArray sources, string variableName, @@ -682,15 +673,15 @@ static void WriteMultiSourceExtraction( string defaultValueExpression ) { - writer.WriteLine($"{localTypeName} {variableName};"); + writer.Line($"{localTypeName} {variableName};"); - void WriteFallback(int index) + void Fallback(int index) { if (index >= sources.Length) { if (hasDefaultValue) { - writer.WriteLine($"{variableName} = {defaultValueExpression};"); + writer.Line($"{variableName} = {defaultValueExpression};"); } return; } @@ -701,19 +692,19 @@ void WriteFallback(int index) if (isLast) { - writer.WriteLine(methodCall + ";"); + writer.Line(methodCall + ";"); } else { - writer.WriteLine($"if (!{methodCall})"); - writer.WriteBlock(null, body => WriteFallback(index + 1)); + writer.Line($"if (!{methodCall})"); + writer.Block(null, body => Fallback(index + 1)); } } - WriteFallback(0); + Fallback(0); } - static void WriteEnumPropertyExtraction(CodeWriter writer, AttributeDataModelProperty property, string variableName) + static void EnumPropertyExtraction(CodeWriter writer, AttributeDataModelProperty property, string variableName) { var sources = property.Sources.AsImmutableArray(); var defaultValueExpression = property.HasDefaultValue ? property.DefaultValueExpression : "null"; @@ -721,11 +712,11 @@ static void WriteEnumPropertyExtraction(CodeWriter writer, AttributeDataModelPro if (sources.Length == 1) { var expression = GetEnumSingleSourceExtractionExpression(sources[0], defaultValueExpression); - writer.WriteLine($"var {variableName} = {expression};"); + writer.Line($"var {variableName} = {expression};"); return; } - WriteEnumMultiSourceExtraction(writer, sources, variableName, defaultValueExpression); + EnumMultiSourceExtraction(writer, sources, variableName, defaultValueExpression); } [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0072:Add missing cases")] @@ -743,7 +734,7 @@ static string GetEnumSingleSourceExtractionExpression(PropertySource source, str }; } - static void WriteEnumMultiSourceExtraction( + static void EnumMultiSourceExtraction( CodeWriter writer, ImmutableArray sources, string variableName, @@ -753,13 +744,13 @@ string defaultValueExpression var typeName = "global::Microsoft.CodeAnalysis.TypedConstant"; var tempName = $"__{variableName}Tc"; - writer.WriteLine($"{typeName} {tempName} = default;"); + writer.Line($"{typeName} {tempName} = default;"); - void WriteFallback(int index) + void Fallback(int index) { if (index >= sources.Length) { - writer.WriteLine($"{tempName} = default;"); + writer.Line($"{tempName} = default;"); return; } @@ -769,18 +760,18 @@ void WriteFallback(int index) if (isLast) { - writer.WriteLine(methodCall + ";"); + writer.Line(methodCall + ";"); } else { - writer.WriteLine($"if (!{methodCall})"); - writer.WriteBlock(null, body => WriteFallback(index + 1)); + writer.Line($"if (!{methodCall})"); + writer.Block(null, body => Fallback(index + 1)); } } - WriteFallback(0); + Fallback(0); - writer.WriteLine($"var {variableName} = {tempName}.ToEnumString() ?? {defaultValueExpression};"); + writer.Line($"var {variableName} = {tempName}.ToEnumString() ?? {defaultValueExpression};"); } static string GetSingleSourceExtractionExpression(PropertySource source, string variableName, string typeName) diff --git a/src/src/SourceGeneratorFramework.Generators/Helpers/SourceEmitter.AttributeData.cs b/src/src/SourceGeneratorFramework.Generators/Helpers/SourceEmitter.AttributeData.cs index 661b606..e69830d 100644 --- a/src/src/SourceGeneratorFramework.Generators/Helpers/SourceEmitter.AttributeData.cs +++ b/src/src/SourceGeneratorFramework.Generators/Helpers/SourceEmitter.AttributeData.cs @@ -11,40 +11,40 @@ static SourceText GenerateAttribute() return writer .XmlSummary("Generates parsing members for an attribute-data model.") - .WriteAttributeClass( + .AttributeClass( new(GeneratorTypeLibrary.Attirbutes.GenerateAttribute), AttributeTargets.Struct, bodyWriter => { bodyWriter .XmlSummary("Initializes the attribute for the target attribute type.") - .WriteConstructor( + .Constructor( new(GeneratorTypeLibrary.Attirbutes.GenerateAttribute, TypeDeclarationAccessibility.Public) { Parameters = [new("targetAttribute", PurviewTypeLibrary.System.Type)], }, constructorWriter => - constructorWriter.WriteLine( + constructorWriter.Line( $"TargetAttribute = targetAttribute ?? throw new global::System.ArgumentNullException(nameof(targetAttribute));" ) ); bodyWriter .XmlSummary("Initializes the attribute for the target attribute name.") - .WriteConstructor( + .Constructor( new(GeneratorTypeLibrary.Attirbutes.GenerateAttribute, TypeDeclarationAccessibility.Public) { Parameters = [new("targetAttributeName", PurviewTypeLibrary.System.String)], }, constructorWriter => - constructorWriter.WriteLine( + constructorWriter.Line( $"TargetAttributeName = targetAttributeName ?? throw new global::System.ArgumentNullException(nameof(targetAttributeName));" ) ); bodyWriter .XmlSummary("Gets the attribute type represented by the generated model.") - .WriteProperty( + .Property( new( "TargetAttribute", PurviewTypeLibrary.System.Type.MakeNullable(), @@ -54,7 +54,7 @@ static SourceText GenerateAttribute() bodyWriter .XmlSummary("Gets the attribute type name represented by the generated model.") - .WriteProperty( + .Property( new( "TargetAttributeName", PurviewTypeLibrary.System.String.MakeNullable(), @@ -64,7 +64,7 @@ static SourceText GenerateAttribute() bodyWriter .XmlSummary("Gets or sets whether derived attribute types are accepted.") - .WriteProperty( + .Property( new( "MatchByInheritance", PurviewTypeLibrary.System.Boolean, @@ -77,7 +77,7 @@ static SourceText GenerateAttribute() bodyWriter .XmlSummary("Gets or sets whether the attribute should be automatically discovered.") - .WriteProperty( + .Property( new("AutoDiscover", PurviewTypeLibrary.System.Boolean, TypeDeclarationAccessibility.Public) { IsInitOnly = true, @@ -93,7 +93,7 @@ static SourceText PropertyAttribute() return writer .XmlSummary("Marks a record parameter as a named attribute argument.") - .WriteAttributeClass( + .AttributeClass( new(GeneratorTypeLibrary.Attirbutes.PropertyAttribute), AttributeTargets.Parameter, bodyWriter => @@ -102,7 +102,7 @@ static SourceText PropertyAttribute() .XmlSummary( $"Initializes a new instance of the class." ) - .WriteConstructor( + .Constructor( new(GeneratorTypeLibrary.Attirbutes.PropertyAttribute, TypeDeclarationAccessibility.Public) { Parameters = @@ -113,12 +113,12 @@ static SourceText PropertyAttribute() }, ], }, - writeBody => writeBody.WriteLine("DefaultValue = defaultValue;") + writeBody => writeBody.Line("DefaultValue = defaultValue;") ); bodyWriter .XmlSummary("Gets or sets an optional named-property mapping.") - .WriteProperty( + .Property( new( "Name", PurviewTypeLibrary.System.String.MakeNullable(), @@ -131,7 +131,7 @@ static SourceText PropertyAttribute() bodyWriter .XmlSummary("Gets or sets the value used when the named argument is not specified.") - .WriteProperty( + .Property( new( "DefaultValue", PurviewTypeLibrary.System.Object.MakeNullable(), @@ -146,7 +146,7 @@ static SourceText PropertyAttribute() .XmlSummary( "Gets or sets a value indicating whether the property represents an enum whose type is not known to the generator." ) - .WriteProperty( + .Property( new("IsEnum", PurviewTypeLibrary.System.Boolean, TypeDeclarationAccessibility.Public) { IsInitOnly = true, @@ -162,7 +162,7 @@ static SourceText ArgumentAttribute() return writer .XmlSummary("Marks a record parameter as a constructor argument.") - .WriteAttributeClass( + .AttributeClass( new(GeneratorTypeLibrary.Attirbutes.ArgumentAttribute), AttributeTargets.Parameter, bodyWriter => @@ -176,7 +176,7 @@ static SourceText ArgumentAttribute() "The name of the constructor parameter. If this value is not specified, the parameter name will be used." ) .XmlParam("defaultValue", "The default value of the constructor parameter.") - .WriteConstructor( + .Constructor( new(GeneratorTypeLibrary.Attirbutes.ArgumentAttribute, TypeDeclarationAccessibility.Public) { Parameters = @@ -192,9 +192,7 @@ static SourceText ArgumentAttribute() ], }, writerBody => - writerBody - .WriteAssignment("Name", "name") - .WriteAssignment("DefaultValue", "defaultValue") + writerBody.Assignment("Name", "name").Assignment("DefaultValue", "defaultValue") ); bodyWriter @@ -203,7 +201,7 @@ static SourceText ArgumentAttribute() ) .XmlParam("index", "The index of the constructor parameter.") .XmlParam("defaultValue", "The default value of the constructor parameter.") - .WriteConstructor( + .Constructor( new(GeneratorTypeLibrary.Attirbutes.ArgumentAttribute, TypeDeclarationAccessibility.Public) { Parameters = @@ -215,8 +213,7 @@ static SourceText ArgumentAttribute() }, ], }, - writerBody => - writerBody.WriteLine("Index = index;").WriteLine("DefaultValue = defaultValue;") + writerBody => writerBody.Line("Index = index;").Line("DefaultValue = defaultValue;") ); bodyWriter @@ -226,7 +223,7 @@ static SourceText ArgumentAttribute() "The property uses a camel-case comparison to match the parameter name.", $"A property name of {CodeWriter.XmlInlineCode("MyProperty")} will match a constructor parameter named {CodeWriter.XmlInlineCode("myProperty")}." ) - .WriteProperty( + .Property( new( "Name", PurviewTypeLibrary.System.String.MakeNullable(), @@ -239,7 +236,7 @@ static SourceText ArgumentAttribute() bodyWriter .XmlSummary("Gets or sets the constructor argument index.") - .WriteProperty( + .Property( new("Index", PurviewTypeLibrary.System.Int32, TypeDeclarationAccessibility.Public) { IsInitOnly = true, @@ -249,7 +246,7 @@ static SourceText ArgumentAttribute() bodyWriter .XmlSummary("Gets or sets the value used when the constructor argument is not specified.") - .WriteProperty( + .Property( new( "DefaultValue", PurviewTypeLibrary.System.Object.MakeNullable(), @@ -264,7 +261,7 @@ static SourceText ArgumentAttribute() .XmlSummary( "Gets or sets a value indicating whether the argument represents an enum whose type is not known to the generator." ) - .WriteProperty( + .Property( new("IsEnum", PurviewTypeLibrary.System.Boolean, TypeDeclarationAccessibility.Public) { IsInitOnly = true, @@ -279,7 +276,7 @@ static SourceText NestedModelAttribute() var writer = CreateWriter(GeneratorTypeLibrary.Attirbutes.NestedModelAttribute); return writer .XmlSummary("Marks a record parameter as a nested generated attribute-data model.") - .WriteAttributeClass( + .AttributeClass( new(GeneratorTypeLibrary.Attirbutes.NestedModelAttribute), AttributeTargets.Parameter, bodyWriter => bodyWriter.Comment("Empty") @@ -291,7 +288,7 @@ static SourceText ExcludeAttribute() var writer = CreateWriter(GeneratorTypeLibrary.Attirbutes.ExcludeAttribute); return writer .XmlSummary("Excludes a record parameter from the generated attribute-data model.") - .WriteAttributeClass( + .AttributeClass( new(GeneratorTypeLibrary.Attirbutes.ExcludeAttribute), AttributeTargets.Parameter, bodyWriter => bodyWriter.Comment("Empty") @@ -304,14 +301,14 @@ static SourceText GenericTypeArgumentAttribute() return writer .XmlSummary("Marks a record parameter as a generic type argument of the attribute class.") - .WriteAttributeClass( + .AttributeClass( new(GeneratorTypeLibrary.Attirbutes.GenericTypeArgumentAttribute), AttributeTargets.Parameter, bodyWriter => { bodyWriter .XmlSummary("Initializes a new instance marking the first type argument.") - .WriteConstructor( + .Constructor( new( GeneratorTypeLibrary.Attirbutes.GenericTypeArgumentAttribute, TypeDeclarationAccessibility.Public @@ -321,7 +318,7 @@ static SourceText GenericTypeArgumentAttribute() bodyWriter .XmlSummary("Initializes a new instance marking the type argument at the specified index.") - .WriteConstructor( + .Constructor( new( GeneratorTypeLibrary.Attirbutes.GenericTypeArgumentAttribute, TypeDeclarationAccessibility.Public @@ -329,14 +326,14 @@ static SourceText GenericTypeArgumentAttribute() { Parameters = [new("index", PurviewTypeLibrary.System.Int32)], }, - constructorWriter => constructorWriter.WriteLine("Index = index;") + constructorWriter => constructorWriter.Line("Index = index;") ); bodyWriter .XmlSummary( "Initializes a new instance marking the type argument with the specified type parameter name." ) - .WriteConstructor( + .Constructor( new( GeneratorTypeLibrary.Attirbutes.GenericTypeArgumentAttribute, TypeDeclarationAccessibility.Public @@ -345,14 +342,14 @@ static SourceText GenericTypeArgumentAttribute() Parameters = [new("name", PurviewTypeLibrary.System.String)], }, constructorWriter => - constructorWriter.WriteLine( + constructorWriter.Line( "Name = name ?? throw new global::System.ArgumentNullException(nameof(name));" ) ); bodyWriter .XmlSummary("Gets or sets the type parameter name.") - .WriteProperty( + .Property( new( "Name", PurviewTypeLibrary.System.String.MakeNullable(), @@ -365,7 +362,7 @@ static SourceText GenericTypeArgumentAttribute() bodyWriter .XmlSummary("Gets or sets the type argument index.") - .WriteProperty( + .Property( new("Index", PurviewTypeLibrary.System.Int32, TypeDeclarationAccessibility.Public) { IsInitOnly = true, diff --git a/src/src/SourceGeneratorFramework.Generators/Helpers/SourceEmitter.cs b/src/src/SourceGeneratorFramework.Generators/Helpers/SourceEmitter.cs index 34979af..d5f331d 100644 --- a/src/src/SourceGeneratorFramework.Generators/Helpers/SourceEmitter.cs +++ b/src/src/SourceGeneratorFramework.Generators/Helpers/SourceEmitter.cs @@ -18,7 +18,7 @@ static CodeWriter CreateWriter(TypeReference type) { CodeWriter writer = new(GenerationSettings.Create()); - return writer.WriteAutoGeneratedHeader().WriteFileScopedNamespace(type); + return writer.AutoGeneratedHeader().FileScopedNamespace(type); } static string GetHintName(string name) => $"{name}.g.cs"; diff --git a/src/src/SourceGeneratorFramework.Testing/CodeQuery.Members.cs b/src/src/SourceGeneratorFramework.Testing/CodeQuery.Members.cs index 6c7ba95..28e37b9 100644 --- a/src/src/SourceGeneratorFramework.Testing/CodeQuery.Members.cs +++ b/src/src/SourceGeneratorFramework.Testing/CodeQuery.Members.cs @@ -11,38 +11,57 @@ public sealed partial class CodeQuery // --------------------------------------------------------------------------------------------- /// - /// Gets the first operator declaration matching the given token, such as == or implicit. + /// Gets the first operator declaration matching the given token, such as ==, optionally matching + /// its parameter types. /// /// No operator matched. - public OperatorDeclarationSyntax GetOperator(string operatorToken) => - TryGetOperator(operatorToken, out var @operator) + public OperatorDeclarationSyntax GetOperator(string operatorToken, params TypeReference[]? parameters) => + TryGetOperator(operatorToken, out var @operator, parameters) ? @operator! : throw new SyntaxNotFoundException( - $"No operator '{operatorToken}' was found in the {ScopeDescription()}." + $"No operator '{operatorToken}' was found in the {ScopeDescription()}{(parameters is { Length: > 0 } ? " with the specified parameters" : "")}." ); /// - /// Determines whether an operator declaration with the given token exists. + /// Determines whether an operator declaration with the given token exists, optionally matching its + /// parameter types. /// - public bool HasOperator(string operatorToken) => TryGetOperator(operatorToken, out _); + public bool HasOperator(string operatorToken, params TypeReference[]? parameters) => + TryGetOperator(operatorToken, out _, parameters); /// - /// Attempts to get the first operator declaration matching the given token. + /// Attempts to get the first operator declaration matching the given token, optionally matching its + /// parameter types. /// - public bool TryGetOperator(string operatorToken, out OperatorDeclarationSyntax? @operator) + /// + /// Parameter types are resolved through the query's with the same semantics as + /// : nullable value types are significant while nullable reference + /// annotations are metadata. When is or empty, the + /// token is matched alone. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1021:Avoid out parameters")] + public bool TryGetOperator( + string operatorToken, + out OperatorDeclarationSyntax? @operator, + params TypeReference[]? parameters + ) { if (string.IsNullOrWhiteSpace(operatorToken)) throw new ArgumentException("The operator token cannot be null or whitespace.", nameof(operatorToken)); + var expected = parameters ?? []; foreach (var tree in Trees) { foreach (var candidate in RootOf(tree).DescendantNodes().OfType()) { - if (candidate.OperatorToken.ValueText == operatorToken) - { - @operator = candidate; - return true; - } + if (candidate.OperatorToken.ValueText != operatorToken) + continue; + + if (expected.Length > 0 && !HasParameters(candidate, expected)) + continue; + + @operator = candidate; + return true; } } @@ -51,38 +70,60 @@ public bool TryGetOperator(string operatorToken, out OperatorDeclarationSyntax? } /// - /// Gets the first conversion operator matching the given keyword, such as implicit or explicit. + /// Gets the first conversion operator matching the given keyword, such as implicit or + /// explicit, optionally matching its parameter type. /// /// No conversion operator matched. - public ConversionOperatorDeclarationSyntax GetConversionOperator(string keyword) => - TryGetConversionOperator(keyword, out var conversion) + public ConversionOperatorDeclarationSyntax GetConversionOperator( + string keyword, + params TypeReference[]? parameters + ) => + TryGetConversionOperator(keyword, out var conversion, parameters) ? conversion! : throw new SyntaxNotFoundException( - $"No '{keyword}' conversion operator was found in the {ScopeDescription()}." + $"No '{keyword}' conversion operator was found in the {ScopeDescription()}{(parameters is { Length: > 0 } ? " with the specified parameters" : "")}." ); /// - /// Determines whether a conversion operator with the given keyword exists. + /// Determines whether a conversion operator with the given keyword exists, optionally matching its + /// parameter type. /// - public bool HasConversionOperator(string keyword) => TryGetConversionOperator(keyword, out _); + public bool HasConversionOperator(string keyword, params TypeReference[]? parameters) => + TryGetConversionOperator(keyword, out _, parameters); /// - /// Attempts to get the first conversion operator matching the given keyword. + /// Attempts to get the first conversion operator matching the given keyword, optionally matching its + /// parameter type. /// - public bool TryGetConversionOperator(string keyword, out ConversionOperatorDeclarationSyntax? conversion) + /// + /// Parameter types are resolved through the query's with the same semantics as + /// : nullable value types are significant while nullable reference + /// annotations are metadata. When is or empty, the + /// keyword is matched alone. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1021:Avoid out parameters")] + public bool TryGetConversionOperator( + string keyword, + out ConversionOperatorDeclarationSyntax? conversion, + params TypeReference[]? parameters + ) { if (keyword is not ("implicit" or "explicit")) throw new ArgumentException("The keyword must be 'implicit' or 'explicit'.", nameof(keyword)); + var expected = parameters ?? []; foreach (var tree in Trees) { foreach (var candidate in RootOf(tree).DescendantNodes().OfType()) { - if (candidate.ImplicitOrExplicitKeyword.ValueText == keyword) - { - conversion = candidate; - return true; - } + if (candidate.ImplicitOrExplicitKeyword.ValueText != keyword) + continue; + + if (expected.Length > 0 && !HasParameters(candidate, expected)) + continue; + + conversion = candidate; + return true; } } diff --git a/src/src/SourceGeneratorFramework/Sdk/.agents/prompts/refactor-source-generator-to-codewriter.prompt.md b/src/src/SourceGeneratorFramework/Sdk/.agents/prompts/refactor-source-generator-to-codewriter.prompt.md index d1bbb1f..e230d22 100644 --- a/src/src/SourceGeneratorFramework/Sdk/.agents/prompts/refactor-source-generator-to-codewriter.prompt.md +++ b/src/src/SourceGeneratorFramework/Sdk/.agents/prompts/refactor-source-generator-to-codewriter.prompt.md @@ -19,11 +19,11 @@ Refactor the selected legacy emitter implementation from manual `string` / `Stri ### Requirements 1. Use structured declaration APIs where applicable: - - `WriteClass/WriteStruct/WriteRecordClass/WriteInterface/WriteEnum` - - `WriteMethod`, `WriteProperty`, `WriteField`, `WriteConstructor` + - `Class/Struct/RecordClass/Interface/Enum` + - `Method`, `Property`, `Field`, `Constructor` 2. Use XML helper extensions instead of raw `///` composition: - `XmlSummary`, `XmlParam`, `XmlReturn`, `XmlRemarks`, `XmlCode` or `XmlCodeBlock` -3. Use `TypeReferenceOptions` when type text becomes complex (nullability, generics, arrays). +3. Use `TypeReference` when type text becomes complex (nullability, generics, arrays). 4. Ensure writer lifetime is output-scoped (`generationContext.CreateCodeWriter()` inside callback). 5. Preserve behavior, diagnostics, and generated names. 6. Keep changes minimal and focused; do not reformat unrelated logic. diff --git a/src/src/SourceGeneratorFramework/Sdk/.agents/skills/source-generator-codewriter-modernization/SKILL.md b/src/src/SourceGeneratorFramework/Sdk/.agents/skills/source-generator-codewriter-modernization/SKILL.md index 635486e..8501ca4 100644 --- a/src/src/SourceGeneratorFramework/Sdk/.agents/skills/source-generator-codewriter-modernization/SKILL.md +++ b/src/src/SourceGeneratorFramework/Sdk/.agents/skills/source-generator-codewriter-modernization/SKILL.md @@ -259,17 +259,17 @@ A generator project should normally include: ### File and namespace -- `WriteAutoGeneratedHeader(...)` -- `WriteUsing(...)` -- `WriteFileScopedNamespace(...)` or `WriteBlockNamespace(...)` +- `AutoGeneratedHeader(...)` +- `Using(...)` +- `FileScopedNamespace(...)` or `BlockNamespace(...)` - `OpenPragmasScope(...)` for warning suppression scopes ### Types and members -- Types: `WriteClass`, `WriteStruct`, `WriteRecordClass`, `WriteRecordStruct`, `WriteInterface`, `WriteEnum`, `WriteDelegate` -- Members: `WriteMethod`, `WriteMethodScope`, `WriteProperty`, `WriteField`, `WriteConstructor` +- Types: `Class`, `Struct`, `RecordClass`, `RecordStruct`, `Interface`, `Enum`, `Delegate` +- Members: `Method`, `MethodScope`, `Property`, `Field`, `Constructor` - Attributes: `AttributeDeclarationOptions`, `AttributeArgumentOptions` -- Type syntax: `TypeReferenceOptions` (nullable/generic/array/pointer-safe composition) +- Type syntax: `TypeReference` (nullable/generic/array/pointer-safe composition) ### XML documentation @@ -286,10 +286,10 @@ Static helpers: `CodeWriter.XmlInlineCode(...)`, `CodeWriter.XmlSee(...)`, `Code Apply this checklist in order: 1. **Move emission boundaries** — replace giant string assembly with phases: header, namespace, type, members. -2. **Replace manual braces/indentation** — use `using` scopes (`WriteClassScope`, `WriteMethodScope`, `OpenBlockScope`, `IndentedScope`). +2. **Replace manual braces/indentation** — use `using` scopes (`ClassScope`, `MethodScope`, `OpenBlockScope`, `IndentedScope`). 3. **Replace handwritten signatures** — use declaration option records. 4. **Replace raw XML lines** — use XML extension methods (`XmlSummary`, `XmlParam`, etc.). -5. **Normalize type strings** — use `TypeReferenceOptions`. +5. **Normalize type strings** — use `TypeReference`. 6. **Preserve semantics and ordering** — generated members and diagnostics must remain equivalent. 7. **Validate scope safety** — keep or enable `PurviewSourceGeneratorFrameworkValidateCodeWriterScopes` for tests/dev. @@ -297,7 +297,7 @@ Apply this checklist in order: - `StringBuilder.AppendLine("public class ...")` for declarations that can be structured. - Manually writing `{` / `}` around methods and types where scope APIs exist. -- Hard-coded nullable type suffixes and generic syntax in arbitrary strings when `TypeReferenceOptions` is available. +- Hard-coded nullable type suffixes and generic syntax in arbitrary strings when `TypeReference` is available. - Raw XML tag string composition when XML extension methods can enforce consistency. - Creating a `CodeWriter` before the output callback and passing it through the pipeline. - Storing a `CodeWriter` on `GenerationContext`, a custom context, or any incremental pipeline model. diff --git a/src/src/SourceGeneratorFramework/Sdk/README.md b/src/src/SourceGeneratorFramework/Sdk/README.md index 3203bf8..fb22957 100644 --- a/src/src/SourceGeneratorFramework/Sdk/README.md +++ b/src/src/SourceGeneratorFramework/Sdk/README.md @@ -134,20 +134,14 @@ public sealed class MyGenerator : IIncrementalGenerator { var (name, generationContext) = pair; var writer = generationContext.CreateCodeWriter(); - writer.WriteAutoGeneratedHeader(); - writer.WriteFileScopedNamespace("MyNamespace"); - using ( - writer.WriteClassScope( - new TypeDeclarationOptions(name) - { - Accessibility = TypeDeclarationAccessibility.Public, - IsStatic = true, - } - ) - ) - { - writer.WriteLine("// generated content"); - } + writer.AutoGeneratedHeader(); + writer.FileScopedNamespace("MyNamespace"); + writer.Class( + name, + TypeDeclarationAccessibility.Public, + options => options with { IsStatic = true }, + body => body.Comment("generated content") + ); spc.AddSource($"{name}.g.cs", writer.ToString()); } @@ -160,11 +154,11 @@ See [`SourceGeneratorFramework.ExampleGenerator`](../SourceGeneratorFramework.Ex ## Generated attributes and determinism -`CodeWriter` automatically stamps generated declarations with `[GeneratedCode]`, `[CompilerGenerated]`, and `[ExcludeFromCodeCoverage]` (where applicable) using the generator identity supplied to its constructor. The header written by `WriteAutoGeneratedHeader()` is deterministic and does not include a timestamp, so the same inputs always produce the same source. +`CodeWriter` automatically stamps generated declarations with `[GeneratedCode]`, `[CompilerGenerated]`, and `[ExcludeFromCodeCoverage]` (where applicable) using the generator identity supplied to its constructor. The header written by `AutoGeneratedHeader()` is deterministic and does not include a timestamp, so the same inputs always produce the same source. ### The `#nullable enable` directive -`WriteAutoGeneratedHeader()` emits `#nullable enable` according to a `NullableDirectiveMode`. The same mode also controls whether nullable *reference* annotations are rendered by type writing, so the directive and the emitted annotations always agree: +`AutoGeneratedHeader()` emits `#nullable enable` according to a `NullableDirectiveMode`. The same mode also controls whether nullable *reference* annotations are rendered by type writing, so the directive and the emitted annotations always agree: - `Auto` (default) — the framework reads the target compilation's nullable context when the pipeline creates the generation context and emits the directive only when nullable annotations are enabled. When the state is unknown (for example in post-initialization outputs or tests), the directive is still emitted. An explicitly configured `GenerationSettings.IsNullableContextEnabled` value takes precedence over the compilation's state. - `Always` — always emit `#nullable enable` and always render nullable reference annotations, even when the target compilation disables nullable. @@ -173,7 +167,7 @@ See [`SourceGeneratorFramework.ExampleGenerator`](../SourceGeneratorFramework.Ex Override it per call, or set a generator-wide default on `GenerationSettings`: ```csharp -writer.WriteAutoGeneratedHeader(nullableDirective: NullableDirectiveMode.Disable); +writer.AutoGeneratedHeader(nullableDirective: NullableDirectiveMode.Disable); var settings = GenerationSettings.Create() with { @@ -192,13 +186,13 @@ Two mechanisms cooperate: - **Context-aware composition** — pass the available `GenerationSettings` or `CodeWriter` to only append the annotation when nullable is enabled or unknown: ```csharp -writer.WriteType(PurviewTypeLibrary.System.String.MakeNullable(writer)); // elides "?" when nullable is off +writer.Type(PurviewTypeLibrary.System.String.MakeNullable(writer)); // elides "?" when nullable is off ``` -- **Context-aware rendering** — the writer elides reference annotations when it renders with nullable disabled. `WriteType(TypeReference)` renders a bare reference using the writer's nullable context, and `RenderFullNameForNullable(bool)` exposes the same behavior for direct string building: +- **Context-aware rendering** — the writer elides reference annotations when it renders with nullable disabled. `Type(TypeReference)` renders a bare reference using the writer's nullable context, and `RenderFullNameForNullable(bool)` exposes the same behavior for direct string building: ```csharp -writer.WriteType(TypeIdentity.Create().MakeNullable()); // "string" when nullable is off, "string?" when on +writer.Type(TypeIdentity.Create().MakeNullable()); // "string" when nullable is off, "string?" when on ``` Composition and rendering both resolve `NullableDirectiveMode` together with `IsNullableContextEnabled`, so an `Always` mode keeps the `?` even for a nullable-disabled target, and a `Disable` mode strips it even when the target enables nullable. @@ -253,7 +247,7 @@ IncrementalPipeline.RegisterSourceOutput( static (spc, name, generationContext) => { var writer = generationContext.CreateCodeWriter(); - writer.WriteLine($"// generated {name}"); + writer.Comment($"generated {name}"); spc.AddSource($"{name}.g.cs", writer.ToString()); } ); @@ -422,39 +416,44 @@ descriptor itself does not allocate an object; strings and `ImmutableArray` valu owned by the caller. ```csharp -using (writer.WriteMethodScope( - new MethodDeclarationOptions( - "CreateAsync", - new TypeReferenceOptions("Task").MakeGeneric(new TypeReferenceOptions("Result")) - ) +using (writer.MethodScope( + "CreateAsync", + new TypeIdentity("Task", "System.Threading.Tasks") + .MakeGeneric(new TypeIdentity("Result", null)) + .AsTypeReference(), + TypeDeclarationAccessibility.Public, + options => options with { - Accessibility = TypeDeclarationAccessibility.Public, IsStatic = true, IsAsync = true, Parameters = [ - new("request", new TypeReferenceOptions("Request")), - new("cancellationToken", new TypeReferenceOptions("CancellationToken")), + new("request", new TypeIdentity("Request", null)), + new("cancellationToken", new TypeIdentity("CancellationToken", null)), ], })) { - writer.WriteLine("return await ExecuteAsync(request, cancellationToken);"); + writer.Return("await ExecuteAsync(request, cancellationToken)"); } -writer.WriteProperty( - new PropertyDeclarationOptions("Name", new TypeReferenceOptions("string")) +writer.Property( + "Name", + TypeReference.Create(), + TypeDeclarationAccessibility.Public, + options => options with { - Accessibility = TypeDeclarationAccessibility.Public, HasSetter = true, IsInitOnly = true, Initializer = "string.Empty", } ); -writer.WriteField( - new FieldDeclarationOptions("Instance", "Service") +writer.Field( + "Instance", + new TypeIdentity("Service", null), + TypeDeclarationAccessibility.Private, + options => options with { - Accessibility = TypeDeclarationAccessibility.Private, IsStatic = true, IsReadOnly = true, Initializer = "new()", @@ -462,13 +461,13 @@ writer.WriteField( ); ``` -`WriteMethod` folds long parameter lists automatically. `WriteProperty` supports automatic +`Method` folds long parameter lists automatically. `Property` supports automatic accessors, expression bodies, and callback-generated getter/setter bodies. Structured methods and constructors return a disposable body scope; callback overloads are available when a complete member should be written in one call. `TypeDeclarationOptions.Kind` supports classes, structs, record classes, record structs, -interfaces, enums, and delegates. The matching `WriteInterface`, `WriteEnum`, and `WriteDelegate` +interfaces, enums, and delegates. The matching `Interface`, `Enum`, and `Delegate` helpers set the kind automatically. Interface inheritance is supplied through `Interfaces`, enums can specify `EnumUnderlyingType`, and delegates use `DelegateReturnType` and `DelegateParameters`. Generic delegate and interface constraints use the existing `GenericTypes` @@ -477,26 +476,30 @@ model. Attributes and parameters are structured as well; raw declaration fragments are not accepted: ```csharp -new MethodDeclarationOptions("TryGet", "bool") -{ - Accessibility = TypeDeclarationAccessibility.Public, - Attributes = [new("Obsolete")], - ReturnAttributes = [new("NotNull")], - Parameters = - [ - new("value", "string?") - { - Modifier = ParameterModifier.Out, - Attributes = - [ - new("NotNullWhen") - { - Arguments = [new("true")], - }, - ], - }, - ], -}; +writer.Method( + "TryGet", + TypeReference.Create(), + TypeDeclarationAccessibility.Public, + options => options with + { + Attributes = [new(TypeIdentity.Create())], + ReturnAttributes = [new(TypeIdentity.Create())], + Parameters = + [ + new("value", TypeReference.Create().Nullable(), ParameterModifier.Out) + { + Attributes = + [ + new(TypeIdentity.Create()) + { + Arguments = [new("true")], + }, + ], + }, + ], + }, + body => body.Return("false") +); ``` Every type, method, constructor, property, and field declaration exposes `Attributes`. Methods also @@ -504,20 +507,22 @@ expose `ReturnAttributes`; parameters expose their own `Attributes`. `AttributeA supports positional arguments, constructor-named arguments using `Name`, and property assignments using `Name` with `IsPropertyAssignment = true`. -All declaration type positions use `TypeReferenceOptions`. Nullability is therefore composed rather +All declaration type positions use `TypeReference`. Nullability is therefore composed rather than embedded in a type string: ```csharp -var widget = new TypeReferenceOptions("Widget").Nullable(); -var result = new TypeReferenceOptions("global::System.Collections.Generic.Dictionary") - .MakeGeneric(new TypeReferenceOptions("string"), widget) +var widget = new TypeIdentity("Widget", null).AsTypeReference().Nullable(); +var result = new TypeIdentity("Dictionary", "System.Collections.Generic") + .MakeGeneric(new TypeIdentity("string", "System"), new TypeIdentity("Widget", null)) + .AsTypeReference() .MakeArray() .Nullable(); new ParameterDeclarationOptions( "items", - new TypeReferenceOptions("global::System.Collections.Generic.List") - .MakeGeneric(widget) + new TypeIdentity("List", "System.Collections.Generic") + .MakeGeneric(new TypeIdentity("Widget", null)) + .AsTypeReference() ) { IsNullable = true, @@ -526,9 +531,9 @@ new ParameterDeclarationOptions( ``` For parameters, `IsNullable = true` is a convenience equivalent to calling `.Nullable()` on the -parameter's `TypeReferenceOptions`. If both are used, only one nullable annotation is emitted. +parameter's `TypeReference`. If both are used, only one nullable annotation is emitted. -`TypeReferenceOptions` supports nullable annotations, nested constructed generics, open generic +`TypeReference` supports nullable annotations, nested constructed generics, open generic arity, multidimensional and jagged arrays, pointers, and construction from `Type`, Roslyn `ITypeSymbol`, or `TypeValueObject`. Arbitrary expressions such as default values and initializers remain strings because they are expressions rather than type syntax. @@ -564,6 +569,30 @@ next member is formatted correctly only after the preceding method, constructor, closed. If XML documentation or attributes were written after the previous member, the separator is inserted before that trivia so it remains attached to the declaration it documents. +### Default member accessibility + +`CodeWriter` applies a default accessibility for each member kind when a declaration does not specify +one. Configure the defaults on `GenerationSettings` (to apply across a generation) or on the writer +itself (to override per writer). Each value is `null`-able, so setting a kind back to `null` omits the +modifier entirely. + +| Setting | Default | +|---|---| +| `DefaultTypeAccessibility` | `Public` | +| `DefaultPropertyAccessibility` | `Public` | +| `DefaultPropertyGetterAccessibility` | `Public` | +| `DefaultPropertySetterAccessibility` | `Public` | +| `DefaultFieldAccessibility` | `Private` | +| `DefaultMethodAccessibility` | `Public` | +| `DefaultConstructorAccessibility` | `Public` | +| `DefaultIndexerAccessibility` | `Public` | +| `DefaultOperatorAccessibility` | `Public` | + +An explicit accessibility always wins over the default. Accessor (getter/setter) defaults are emitted +only when they are **more restrictive** than the property's own accessibility (C# forbids an accessor +modifier that is equal to or more permissive than the property, CS0273), so a public property keeps +bare `{ get; set; }` accessors by default. + ## Detecting undisposed CodeWriter scopes `CodeWriter` can detect block or indentation scopes that have not been disposed before generated source is materialized. This validation is intended for development and automated tests and is disabled by default. @@ -614,9 +643,9 @@ diagnostic allocation during normal generator execution. Both `BlockScope` and `IndentScope` are tracked. Prefer `using` or callback-based blocks so scopes are always closed: ```csharp -writer.WriteBlock( +writer.Block( "if (value is null)", - body => body.WriteLine("return;") + body => body.Return() ); ``` @@ -837,6 +866,14 @@ The `Purview.SourceGeneratorFramework` package includes the `Purview.SourceGener | `PSGFR11` | Prefer `SyntaxProvider.ForAttributeWithMetadataName` over `CreateSyntaxProvider` for attribute-based detection. | | `PSGFR12` | Use `IIncrementalGenerator` / `RegisterSourceOutput` instead of `ISourceGenerator`. | | `PSGFR14` | Avoid `RegisterImplementationSourceOutput` unless implementation-only output is required. | +| `PSGFR15` | Pipeline model collection members should use sequence equality (e.g. `EquatableArray`). | +| `PSGFR16` | Prefer the nullable-context `Nullable()`/`MakeNullable()` overload so annotations honour the target compilation. | +| `PSGFR17` | Consume `CodeWriter` scope-returning methods (`...Scope`, `IndentedScope`) with `using`. | +| `PSGFR18` | Prefer structured declaration APIs (`Class`, `Method`, `Property`, `Field`) over raw declaration text. | +| `PSGFR19` | Prefer structured statement APIs (`Return`, `MethodCall`, `Throw`, `Assignment`, `Using`, `Comment`) over raw statement text. | +| `PSGFR20` | Prefer the minimal `CodeWriter` overloads over constructing `*DeclarationOptions` values manually. | +| `PSGFR21` | Prefer `HashDefines`/`HashDefinesScope` for `#if`/`#endif` conditional-compilation directives. | +| `PSGFR22` | Prefer `PragmaDisable`/`OpenPragmasScope` for `#pragma warning` directives. | ## License diff --git a/src/src/SourceGeneratorShared/CodeWriter.DeclarationOverloads.cs b/src/src/SourceGeneratorShared/CodeWriter.DeclarationOverloads.cs new file mode 100644 index 0000000..b15b45f --- /dev/null +++ b/src/src/SourceGeneratorShared/CodeWriter.DeclarationOverloads.cs @@ -0,0 +1,962 @@ +namespace Purview.SourceGeneratorFramework; + +partial class CodeWriter +{ + // --------------------------------------------------------------------------------------------- + // Methods + // --------------------------------------------------------------------------------------------- + + /// + /// Writes a structured method declaration using the minimal identifying properties and returns its + /// body scope. + /// + /// The method name. + /// The return type, or for void. + /// The optional accessibility. + /// An optional callback that configures the declaration. + /// The method body scope. + /// using (writer.MethodScope("Run")) writer.Line("return;"); + public BlockScope MethodScope( + string name, + TypeReference? returnType = null, + TypeDeclarationAccessibility? accessibility = null, + Func? configure = null + ) + { + var declaration = new MethodDeclarationOptions( + name, + returnType ?? PurviewTypeLibrary.System.Void, + accessibility + ); + if (configure is not null) + declaration = configure(declaration); + + return MethodScope(declaration); + } + + /// + /// Writes a structured method using the minimal identifying properties and invokes a callback for + /// its body. + /// + /// The method name. + /// The return type. + /// The optional accessibility. + /// An optional callback that configures the declaration, or for defaults. + /// The action that writes the method body. + /// The current writer. + /// writer.Method("Run", PurviewTypeLibrary.System.Void, TypeDeclarationAccessibility.Public, null, body => body.Line("return;")); + public CodeWriter Method( + string name, + TypeReference returnType, + TypeDeclarationAccessibility? accessibility, + Func? configure, + Action writeBody + ) + { + if (writeBody is null) + throw new ArgumentNullException(nameof(writeBody)); + + var declaration = new MethodDeclarationOptions(name, returnType, accessibility); + if (configure is not null) + declaration = configure(declaration); + + return Method(declaration, writeBody); + } + + /// + /// Writes a structured partial method using the minimal identifying properties. + /// + /// The method name. + /// The return type, or for void. + /// The optional accessibility. + /// An optional callback that configures the declaration. + /// The current writer. + /// writer.PartialMethod("OnChanged"); + public CodeWriter PartialMethod( + string name, + TypeReference? returnType = null, + TypeDeclarationAccessibility? accessibility = null, + Func? configure = null + ) + { + var declaration = new MethodDeclarationOptions( + name, + returnType ?? PurviewTypeLibrary.System.Void, + accessibility + ); + if (configure is not null) + declaration = configure(declaration); + + return PartialMethod(declaration); + } + + /// + /// Writes an expression-bodied method using the minimal identifying properties and an expression body. + /// + /// The method name. + /// The return type. + /// The optional accessibility. + /// The expression body without the leading =>. + /// An optional callback that configures the declaration. + /// The current writer. + /// writer.MethodExpression("Count", Type("int"), TypeDeclarationAccessibility.Public, "items.Count"); + public CodeWriter MethodExpression( + string name, + TypeReference returnType, + TypeDeclarationAccessibility? accessibility, + string expressionBody, + Func? configure = null + ) + { + if (string.IsNullOrWhiteSpace(expressionBody)) + { + throw new ArgumentException( + "An expression-bodied method must have a non-empty expression body.", + nameof(expressionBody) + ); + } + + var declaration = new MethodDeclarationOptions( + name, + returnType ?? PurviewTypeLibrary.System.Void, + accessibility + ) + { + ExpressionBody = expressionBody, + }; + if (configure is not null) + declaration = configure(declaration); + + return MethodExpression(declaration); + } + + /// + /// Writes an expression-bodied method using the minimal identifying properties and a callback for + /// the expression. + /// + /// The method name. + /// The return type. + /// The optional accessibility. + /// The action that writes the expression. + /// An optional callback that configures the declaration. + /// The current writer. + /// writer.MethodExpression("Count", Type("int"), TypeDeclarationAccessibility.Public, expression => expression.Write("items.Count")); + public CodeWriter MethodExpression( + string name, + TypeReference returnType, + TypeDeclarationAccessibility? accessibility, + Action writeExpression, + Func? configure = null + ) + { + if (writeExpression is null) + throw new ArgumentNullException(nameof(writeExpression)); + + var declaration = new MethodDeclarationOptions( + name, + returnType ?? PurviewTypeLibrary.System.Void, + accessibility + ); + if (configure is not null) + declaration = configure(declaration); + + return MethodExpression(declaration, writeExpression); + } + + // --------------------------------------------------------------------------------------------- + // Operators + // --------------------------------------------------------------------------------------------- + + /// + /// Writes a structured operator declaration using the minimal identifying properties and returns its + /// body scope. + /// + /// The operator token, such as ==; ignored for conversion operators. + /// The operator return type. + /// The left operand, or the single source parameter for unary and conversion operators. + /// The right operand, or for unary and conversion operators. + /// The optional accessibility. + /// An optional callback that configures the declaration, such as setting . + /// The operator body scope. + /// using (writer.OperatorScope("==", Type("bool"), left, right, TypeDeclarationAccessibility.Public)) { } + public BlockScope OperatorScope( + string operatorToken, + TypeReference returnType, + ParameterDeclarationOptions left, + ParameterDeclarationOptions right, + TypeDeclarationAccessibility? accessibility, + Func? configure = null + ) + { + if (returnType is null) + throw new ArgumentNullException(nameof(returnType)); + + var declaration = new OperatorDeclarationOptions(operatorToken, returnType, left, right, accessibility); + if (configure is not null) + declaration = configure(declaration); + + return OperatorScope(declaration); + } + + /// + /// Writes a structured operator declaration using the minimal identifying properties and invokes a + /// callback for its body. + /// + /// The operator token, such as ==; ignored for conversion operators. + /// The operator return type. + /// The left operand, or the single source parameter for unary and conversion operators. + /// The right operand, or for unary and conversion operators. + /// The optional accessibility. + /// An optional callback that configures the declaration, such as setting , or for defaults. + /// The action that writes the operator body. + /// The current writer. + /// writer.Operator("==", Type("bool"), left, right, TypeDeclarationAccessibility.Public, null, body => body.Return("left.Equals(right)")); + public CodeWriter Operator( + string operatorToken, + TypeReference returnType, + ParameterDeclarationOptions left, + ParameterDeclarationOptions right, + TypeDeclarationAccessibility? accessibility, + Func? configure, + Action writeBody + ) + { + if (writeBody is null) + throw new ArgumentNullException(nameof(writeBody)); + if (returnType is null) + throw new ArgumentNullException(nameof(returnType)); + + var declaration = new OperatorDeclarationOptions(operatorToken, returnType, left, right, accessibility); + if (configure is not null) + declaration = configure(declaration); + + return Operator(declaration, writeBody); + } + + // --------------------------------------------------------------------------------------------- + // Properties + // --------------------------------------------------------------------------------------------- + + /// + /// Writes an auto-property or expression-bodied property using the minimal identifying properties. + /// + /// The property name. + /// The property type. + /// The optional accessibility. + /// An optional callback that configures the declaration. + /// The current writer. + /// writer.Property("Name", Type("string")); + public CodeWriter Property( + string name, + TypeReference type, + TypeDeclarationAccessibility? accessibility = null, + Func? configure = null + ) + { + var declaration = new PropertyDeclarationOptions(name, type, accessibility); + if (configure is not null) + declaration = configure(declaration); + + return Property(declaration); + } + + /// + /// Writes a property with callback-generated accessor bodies using the minimal identifying + /// properties. + /// + /// The property name. + /// The property type. + /// The optional accessibility. + /// The action that writes the getter body, or for an auto getter. + /// The action that writes the setter body, or for an auto setter. + /// An optional callback that configures the declaration. + /// The current writer. + /// writer.Property("Value", Type("int"), TypeDeclarationAccessibility.Public, getter => getter.Line("return _value;"), null); + public CodeWriter Property( + string name, + TypeReference type, + TypeDeclarationAccessibility? accessibility, + Action? writeGetterBody, + Action? writeSetterBody, + Func? configure = null + ) + { + var declaration = new PropertyDeclarationOptions(name, type, accessibility); + if (configure is not null) + declaration = configure(declaration); + + return Property(declaration, writeGetterBody, writeSetterBody); + } + + /// + /// Writes an expression-bodied property using the minimal identifying properties and a callback for + /// the expression. + /// + /// The property name. + /// The property type. + /// The optional accessibility. + /// The action that writes the expression. + /// An optional callback that configures the declaration. + /// The current writer. + /// writer.PropertyExpression("Count", Type("int"), TypeDeclarationAccessibility.Public, expression => expression.Write("items.Count")); + public CodeWriter PropertyExpression( + string name, + TypeReference type, + TypeDeclarationAccessibility? accessibility, + Action writeExpression, + Func? configure = null + ) + { + if (writeExpression is null) + throw new ArgumentNullException(nameof(writeExpression)); + + var declaration = new PropertyDeclarationOptions(name, type, accessibility); + if (configure is not null) + declaration = configure(declaration); + + return PropertyExpression(declaration, writeExpression); + } + + // --------------------------------------------------------------------------------------------- + // Indexers + // --------------------------------------------------------------------------------------------- + + /// + /// Writes an indexer declaration with auto accessors or an expression body using the minimal + /// identifying properties. + /// + /// The indexer element type. + /// The optional accessibility. + /// The indexer parameters. + /// An optional callback that configures the declaration. + /// The current writer. + /// writer.Indexer(Type("string"), TypeDeclarationAccessibility.Public, new("index", Type("int"))); + public CodeWriter Indexer( + TypeReference type, + TypeDeclarationAccessibility? accessibility = null, + IEnumerable? parameters = null, + Func? configure = null + ) + { + if (type is null) + throw new ArgumentNullException(nameof(type)); + + var declaration = new IndexerDeclarationOptions(type, parameters is null ? [] : [.. parameters]); + if (accessibility is not null) + declaration = declaration with { Accessibility = accessibility }; + if (configure is not null) + declaration = configure(declaration); + + return Indexer(declaration); + } + + /// + /// Writes an indexer with callback-generated accessor bodies using the minimal identifying + /// properties. + /// + /// The indexer element type. + /// The optional accessibility. + /// The indexer parameters. + /// The action that writes the getter body, or for an auto getter. + /// The action that writes the setter body, or for an auto setter. + /// An optional callback that configures the declaration. + /// The current writer. + /// writer.Indexer(Type("string"), TypeDeclarationAccessibility.Public, [new("index", Type("int"))], getter => getter.Line("return _items[index];"), null); + public CodeWriter Indexer( + TypeReference type, + TypeDeclarationAccessibility? accessibility, + IEnumerable? parameters, + Action? writeGetterBody, + Action? writeSetterBody, + Func? configure = null + ) + { + if (type is null) + throw new ArgumentNullException(nameof(type)); + + var declaration = new IndexerDeclarationOptions(type, parameters is null ? [] : [.. parameters]); + if (accessibility is not null) + declaration = declaration with { Accessibility = accessibility }; + if (configure is not null) + declaration = configure(declaration); + + return Indexer(declaration, writeGetterBody, writeSetterBody); + } + + // --------------------------------------------------------------------------------------------- + // Fields + // --------------------------------------------------------------------------------------------- + + /// + /// Writes a field declaration using the minimal identifying properties. + /// + /// The field name. + /// The field type. + /// The optional accessibility. + /// An optional callback that configures the declaration. + /// The current writer. + /// writer.Field("_value", Type("int")); + public CodeWriter Field( + string name, + TypeReference type, + TypeDeclarationAccessibility? accessibility = null, + Func? configure = null + ) + { + var declaration = new FieldDeclarationOptions(name, type, accessibility); + if (configure is not null) + declaration = configure(declaration); + + return Field(declaration); + } + + // --------------------------------------------------------------------------------------------- + // Constructors + // --------------------------------------------------------------------------------------------- + + /// + /// Writes a structured constructor declaration using the minimal identifying properties and returns + /// its body scope. + /// + /// The name of the containing type. + /// The optional accessibility. + /// An optional callback that configures the declaration. + /// The constructor body scope. + /// using (writer.ConstructorScope("C")) writer.Line("// body"); + public BlockScope ConstructorScope( + string name, + TypeDeclarationAccessibility? accessibility = null, + Func? configure = null + ) + { + var declaration = new ConstructorDeclarationOptions(name, accessibility); + if (configure is not null) + declaration = configure(declaration); + + return ConstructorScope(declaration); + } + + /// + /// Writes a structured constructor using the minimal identifying properties and invokes a callback + /// for its body. + /// + /// The name of the containing type. + /// The optional accessibility. + /// An optional callback that configures the declaration, or for defaults. + /// The action that writes the constructor body. + /// The current writer. + /// writer.Constructor("C", TypeDeclarationAccessibility.Public, null, _ => { }); + public CodeWriter Constructor( + string name, + TypeDeclarationAccessibility? accessibility, + Func? configure, + Action writeBody + ) + { + if (writeBody is null) + throw new ArgumentNullException(nameof(writeBody)); + + var declaration = new ConstructorDeclarationOptions(name, accessibility); + if (configure is not null) + declaration = configure(declaration); + + return Constructor(declaration, writeBody); + } + + // --------------------------------------------------------------------------------------------- + // Types + // --------------------------------------------------------------------------------------------- + + /// + /// Writes a class declaration using the minimal identifying properties and returns its body scope. + /// + /// The class name. + /// The optional accessibility. + /// An optional callback that configures the declaration. + /// The class body scope. + /// using (writer.ClassScope("C")) writer.Line("// body"); + public BlockScope ClassScope( + string name, + TypeDeclarationAccessibility? accessibility = null, + Func? configure = null + ) + { + var declaration = new TypeDeclarationOptions(name, accessibility); + if (configure is not null) + declaration = configure(declaration); + + return ClassScope(declaration); + } + + /// + /// Writes a class declaration using the minimal identifying properties and invokes a callback for + /// its body. + /// + /// The class name. + /// The optional accessibility. + /// An optional callback that configures the declaration, or for defaults. + /// The action that writes the class body. + /// The current writer. + /// writer.Class("C", TypeDeclarationAccessibility.Public, null, _ => { }); + public CodeWriter Class( + string name, + TypeDeclarationAccessibility? accessibility, + Func? configure, + Action writeBody + ) + { + if (writeBody is null) + throw new ArgumentNullException(nameof(writeBody)); + + var declaration = new TypeDeclarationOptions(name, accessibility); + if (configure is not null) + declaration = configure(declaration); + + return Class(declaration, writeBody); + } + + /// + /// Writes a struct declaration using the minimal identifying properties and returns its body scope. + /// + /// The struct name. + /// The optional accessibility. + /// An optional callback that configures the declaration. + /// The struct body scope. + /// using (writer.StructScope("Value")) { } + public BlockScope StructScope( + string name, + TypeDeclarationAccessibility? accessibility = null, + Func? configure = null + ) + { + var declaration = new TypeDeclarationOptions(name, accessibility); + if (configure is not null) + declaration = configure(declaration); + + return StructScope(declaration); + } + + /// + /// Writes a struct declaration using the minimal identifying properties and invokes a callback for + /// its body. + /// + /// The struct name. + /// The optional accessibility. + /// An optional callback that configures the declaration, or for defaults. + /// The action that writes the struct body. + /// The current writer. + /// writer.Struct("Value", TypeDeclarationAccessibility.Public, null, _ => { }); + public CodeWriter Struct( + string name, + TypeDeclarationAccessibility? accessibility, + Func? configure, + Action writeBody + ) + { + if (writeBody is null) + throw new ArgumentNullException(nameof(writeBody)); + + var declaration = new TypeDeclarationOptions(name, accessibility); + if (configure is not null) + declaration = configure(declaration); + + return Struct(declaration, writeBody); + } + + /// + /// Writes a record class declaration using the minimal identifying properties and returns its body + /// scope. + /// + /// The record class name. + /// The optional accessibility. + /// An optional callback that configures the declaration. + /// The record body scope. + /// using (writer.RecordClassScope("Model")) { } + public BlockScope RecordClassScope( + string name, + TypeDeclarationAccessibility? accessibility = null, + Func? configure = null + ) + { + var declaration = new TypeDeclarationOptions(name, accessibility); + if (configure is not null) + declaration = configure(declaration); + + return RecordClassScope(declaration); + } + + /// + /// Writes a record class declaration using the minimal identifying properties and invokes a callback + /// for its body. + /// + /// The record class name. + /// The optional accessibility. + /// An optional callback that configures the declaration, or for defaults. + /// The action that writes the record body. + /// The current writer. + /// writer.RecordClass("Model", TypeDeclarationAccessibility.Public, null, _ => { }); + public CodeWriter RecordClass( + string name, + TypeDeclarationAccessibility? accessibility, + Func? configure, + Action writeBody + ) + { + if (writeBody is null) + throw new ArgumentNullException(nameof(writeBody)); + + var declaration = new TypeDeclarationOptions(name, accessibility); + if (configure is not null) + declaration = configure(declaration); + + return RecordClass(declaration, writeBody); + } + + /// + /// Writes a record struct declaration using the minimal identifying properties and returns its body + /// scope. + /// + /// The record struct name. + /// The optional accessibility. + /// An optional callback that configures the declaration. + /// The record body scope. + /// using (writer.RecordStructScope("Value")) { } + public BlockScope RecordStructScope( + string name, + TypeDeclarationAccessibility? accessibility = null, + Func? configure = null + ) + { + var declaration = new TypeDeclarationOptions(name, accessibility); + if (configure is not null) + declaration = configure(declaration); + + return RecordStructScope(declaration); + } + + /// + /// Writes a record struct declaration using the minimal identifying properties and invokes a callback + /// for its body. + /// + /// The record struct name. + /// The optional accessibility. + /// An optional callback that configures the declaration, or for defaults. + /// The action that writes the record body. + /// The current writer. + /// writer.RecordStruct("Value", TypeDeclarationAccessibility.Public, null, _ => { }); + public CodeWriter RecordStruct( + string name, + TypeDeclarationAccessibility? accessibility, + Func? configure, + Action writeBody + ) + { + if (writeBody is null) + throw new ArgumentNullException(nameof(writeBody)); + + var declaration = new TypeDeclarationOptions(name, accessibility); + if (configure is not null) + declaration = configure(declaration); + + return RecordStruct(declaration, writeBody); + } + + /// + /// Writes an interface declaration using the minimal identifying properties and returns its body + /// scope. + /// + /// The interface name. + /// The optional accessibility. + /// An optional callback that configures the declaration. + /// The interface body scope. + /// using (writer.InterfaceScope("IService")) { } + public BlockScope InterfaceScope( + string name, + TypeDeclarationAccessibility? accessibility = null, + Func? configure = null + ) + { + var declaration = new TypeDeclarationOptions(name, accessibility); + if (configure is not null) + declaration = configure(declaration); + + return InterfaceScope(declaration); + } + + /// + /// Writes an interface declaration using the minimal identifying properties and invokes a callback + /// for its body. + /// + /// The interface name. + /// The optional accessibility. + /// An optional callback that configures the declaration, or for defaults. + /// The action that writes the interface body. + /// The current writer. + /// writer.Interface("IService", TypeDeclarationAccessibility.Public, null, _ => { }); + public CodeWriter Interface( + string name, + TypeDeclarationAccessibility? accessibility, + Func? configure, + Action writeBody + ) + { + if (writeBody is null) + throw new ArgumentNullException(nameof(writeBody)); + + var declaration = new TypeDeclarationOptions(name, accessibility); + if (configure is not null) + declaration = configure(declaration); + + return Interface(declaration, writeBody); + } + + /// + /// Writes an enum declaration using the minimal identifying properties and returns its body scope. + /// + /// The enum name. + /// The optional accessibility. + /// An optional callback that configures the declaration. + /// The enum body scope. + /// using (writer.EnumScope("Status")) { } + public BlockScope EnumScope( + string name, + TypeDeclarationAccessibility? accessibility = null, + Func? configure = null + ) + { + var declaration = new TypeDeclarationOptions(name, accessibility); + if (configure is not null) + declaration = configure(declaration); + + return EnumScope(declaration); + } + + /// + /// Writes an enum declaration using the minimal identifying properties and invokes a callback for + /// its body. + /// + /// The enum name. + /// The optional accessibility. + /// An optional callback that configures the declaration, or for defaults. + /// The action that writes the enum body. + /// The current writer. + /// writer.Enum("Status", TypeDeclarationAccessibility.Public, null, _ => { }); + public CodeWriter Enum( + string name, + TypeDeclarationAccessibility? accessibility, + Func? configure, + Action writeBody + ) + { + if (writeBody is null) + throw new ArgumentNullException(nameof(writeBody)); + + var declaration = new TypeDeclarationOptions(name, accessibility); + if (configure is not null) + declaration = configure(declaration); + + return Enum(declaration, writeBody); + } + + /// + /// Writes an enum declaration using the minimal identifying properties and structured field + /// declarations. + /// + /// The enum name. + /// The optional accessibility. + /// The fields to write in declaration order. + /// An optional callback that configures the declaration. + /// The current writer. + /// writer.Enum("Status", fields: [new("Ready", 1)]); + public CodeWriter Enum( + string name, + TypeDeclarationAccessibility? accessibility = null, + IEnumerable? fields = null, + Func? configure = null + ) + { + var declaration = new TypeDeclarationOptions(name, accessibility); + if (configure is not null) + declaration = configure(declaration); + + if (fields is null) + return Enum(declaration, static _ => { }); + + return Enum(declaration, fields.ToArray()); + } + + /// + /// Writes a structured type declaration using the minimal identifying properties and returns its + /// body scope when the declaration has one. + /// + /// The type declaration kind. + /// The generated type name. + /// The optional accessibility. + /// An optional callback that configures the declaration. + /// The generated type body scope. + /// using (writer.TypeScope(TypeDeclarationKind.Interface, "IService")) { } + public BlockScope TypeScope( + TypeDeclarationKind kind, + string name, + TypeDeclarationAccessibility? accessibility = null, + Func? configure = null + ) + { + var declaration = new TypeDeclarationOptions(name, accessibility); + if (configure is not null) + declaration = configure(declaration); + + return TypeScope(declaration with { Kind = kind }); + } + + /// + /// Writes a structured type declaration using the minimal identifying properties and invokes a + /// callback for its body. + /// + /// The type declaration kind. + /// The generated type name. + /// The optional accessibility. + /// An optional callback that configures the declaration, or for defaults. + /// The action that writes the type body. + /// The current writer. + /// writer.Type(TypeDeclarationKind.Interface, "IService", TypeDeclarationAccessibility.Public, null, _ => { }); + public CodeWriter Type( + TypeDeclarationKind kind, + string name, + TypeDeclarationAccessibility? accessibility, + Func? configure, + Action writeBody + ) + { + if (writeBody is null) + throw new ArgumentNullException(nameof(writeBody)); + + var declaration = new TypeDeclarationOptions(name, accessibility); + if (configure is not null) + declaration = configure(declaration); + + return Type(declaration with { Kind = kind }, writeBody); + } + + /// + /// Writes an attribute class with an declaration using the + /// minimal identifying properties. + /// + /// The attribute class name. + /// The optional accessibility. + /// The declarations on which the generated attribute may be applied. + /// The action that writes the body of the attribute class. + /// Whether derived classes and overriding members inherit the attribute. + /// Whether more than one instance may be specified on one declaration. + /// An optional callback that configures the declaration. + /// The current writer. + /// writer.AttributeClass("MarkerAttribute", TypeDeclarationAccessibility.Public, AttributeTargets.Class, _ => { }); + public CodeWriter AttributeClass( + string name, + TypeDeclarationAccessibility? accessibility, + AttributeTargets targets, + Action bodyWriter, + bool inherited = false, + bool allowMultiple = false, + Func? configure = null + ) + { + if (bodyWriter is null) + throw new ArgumentNullException(nameof(bodyWriter)); + + var declaration = new TypeDeclarationOptions(name, accessibility); + if (configure is not null) + declaration = configure(declaration); + + return AttributeClass(declaration, targets, bodyWriter, inherited, allowMultiple); + } + + /// + /// Writes a complete delegate declaration using the minimal identifying properties. + /// + /// The delegate name. + /// The delegate return type. + /// The optional accessibility. + /// The delegate parameters. + /// An optional callback that configures the declaration. + /// The current writer. + /// writer.Delegate("Handler", Type("void"), TypeDeclarationAccessibility.Public, [new("value", Type("int"))]); + public CodeWriter Delegate( + string name, + TypeReference delegateReturnType, + TypeDeclarationAccessibility? accessibility = null, + IEnumerable? parameters = null, + Func? configure = null + ) + { + if (delegateReturnType is null) + throw new ArgumentNullException(nameof(delegateReturnType)); + + var declaration = new TypeDeclarationOptions(name, accessibility) + { + DelegateReturnType = delegateReturnType, + DelegateParameters = parameters is null ? [] : [.. parameters], + }; + if (configure is not null) + declaration = configure(declaration); + + return Delegate(declaration); + } + + /// + /// Writes a field in an enum declaration using the minimal identifying properties. + /// + /// The enum field name. + /// + /// The enum field value. Strings are emitted as C# expressions; other values are formatted using the + /// invariant culture. + /// + /// The lines written in the field's XML summary block. + /// The current writer. + /// writer.EnumField("Ready", 1); + public CodeWriter EnumField(string fieldName, object fieldValue, params string[] xmlSummary) + { + if (fieldValue is null) + throw new ArgumentNullException(nameof(fieldValue)); + + return EnumField(new EnumFieldDeclarationOptions(fieldName, fieldValue, xmlSummary)); + } + + // --------------------------------------------------------------------------------------------- + // Statements + // --------------------------------------------------------------------------------------------- + + /// + /// Writes a return statement that formats an interpolated string using the invariant culture through + /// the best API available on the target framework, guarded by a conditional-compilation block. + /// + /// + /// The interpolated message written between the $" and closing quote, such as + /// Argument '{value}' is required. + /// + /// The preprocessor symbol that selects the modern API, defaulting to NET. + /// The current writer. + /// writer.NetConditionalReturn("Argument '{value}' is required"); + public CodeWriter NetConditionalReturn(string interpolatedMessage, string symbol = "NET") + { + if (string.IsNullOrWhiteSpace(interpolatedMessage)) + { + throw new ArgumentException( + "The interpolated message cannot be null or whitespace.", + nameof(interpolatedMessage) + ); + } + + if (string.IsNullOrWhiteSpace(symbol)) + throw new ArgumentException("The preprocessor symbol cannot be null or whitespace.", nameof(symbol)); + + Line("#if " + symbol); + Write("return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $\"") + .Write(interpolatedMessage) + .Line("\");"); + Line("#else"); + Write("return global::System.FormattableString.Invariant($\"").Write(interpolatedMessage).Line("\");"); + Line("#endif"); + return this; + } +} diff --git a/src/src/SourceGeneratorShared/CodeWriter.Types.cs b/src/src/SourceGeneratorShared/CodeWriter.Types.cs index 209595e..991041c 100644 --- a/src/src/SourceGeneratorShared/CodeWriter.Types.cs +++ b/src/src/SourceGeneratorShared/CodeWriter.Types.cs @@ -49,7 +49,7 @@ void RestorePragmas(string[] pragmas) NewLine(); foreach (var pragma in pragmas) - Write("#pragma warning restore ").WriteLine(pragma); + Write("#pragma warning restore ").Line(pragma); } /// @@ -93,14 +93,26 @@ public struct BlockScope : IDisposable readonly int _scopeId; readonly int _completedItem; readonly int _itemIndent; - - internal BlockScope(CodeWriter writer, string? closingSeparator, int scopeId, int completedItem, int itemIndent) + readonly bool _closingAtColumnZero; + readonly bool _changesIndentation; + + internal BlockScope( + CodeWriter writer, + string? closingSeparator, + int scopeId, + int completedItem, + int itemIndent, + bool closingAtColumnZero = false, + bool changesIndentation = true + ) { _writer = writer; _closingSeparator = closingSeparator; _scopeId = scopeId; _completedItem = completedItem; _itemIndent = itemIndent; + _closingAtColumnZero = closingAtColumnZero; + _changesIndentation = changesIndentation; } /// @@ -113,7 +125,14 @@ public void Dispose() return; _writer = null; - writer.CloseBlock(_closingSeparator, _scopeId, _completedItem, _itemIndent); + writer.CloseBlock( + _closingSeparator, + _scopeId, + _completedItem, + _itemIndent, + _closingAtColumnZero, + _changesIndentation + ); } } diff --git a/src/src/SourceGeneratorShared/CodeWriter.cs b/src/src/SourceGeneratorShared/CodeWriter.cs index 691b2d1..7da9b75 100644 --- a/src/src/SourceGeneratorShared/CodeWriter.cs +++ b/src/src/SourceGeneratorShared/CodeWriter.cs @@ -79,6 +79,16 @@ public CodeWriter( IsNullableContextEnabled = settings.IsNullableContextEnabled; ThrowOnUnclosedScopes = throwOnUnclosedScopes; + DefaultTypeAccessibility = settings.DefaultTypeAccessibility; + DefaultPropertyAccessibility = settings.DefaultPropertyAccessibility; + DefaultPropertyGetterAccessibility = settings.DefaultPropertyGetterAccessibility; + DefaultPropertySetterAccessibility = settings.DefaultPropertySetterAccessibility; + DefaultFieldAccessibility = settings.DefaultFieldAccessibility; + DefaultMethodAccessibility = settings.DefaultMethodAccessibility; + DefaultConstructorAccessibility = settings.DefaultConstructorAccessibility; + DefaultIndexerAccessibility = settings.DefaultIndexerAccessibility; + DefaultOperatorAccessibility = settings.DefaultOperatorAccessibility; + _indentationSize = settings.IndentationSize > 0 ? settings.IndentationSize : DefaultIndentationSize; _maximumLineLength = settings.MaximumLineLength > 0 ? settings.MaximumLineLength : DefaultMaximumLineLength; _indentCharacter = settings.IndentationStyle == IndentationStyle.Spaces ? ' ' : '\t'; @@ -102,6 +112,15 @@ public CodeWriter( IsNullableContextEnabled = source.IsNullableContextEnabled; ThrowOnUnclosedScopes = source.ThrowOnUnclosedScopes; DefaultIncludeGeneratedAttributes = source.DefaultIncludeGeneratedAttributes; + DefaultTypeAccessibility = source.DefaultTypeAccessibility; + DefaultPropertyAccessibility = source.DefaultPropertyAccessibility; + DefaultPropertyGetterAccessibility = source.DefaultPropertyGetterAccessibility; + DefaultPropertySetterAccessibility = source.DefaultPropertySetterAccessibility; + DefaultFieldAccessibility = source.DefaultFieldAccessibility; + DefaultMethodAccessibility = source.DefaultMethodAccessibility; + DefaultConstructorAccessibility = source.DefaultConstructorAccessibility; + DefaultIndexerAccessibility = source.DefaultIndexerAccessibility; + DefaultOperatorAccessibility = source.DefaultOperatorAccessibility; _indentCharacter = source._indentCharacter; _indentationSize = source._indentationSize; _maximumLineLength = source._maximumLineLength; @@ -144,7 +163,7 @@ public CodeWriter( /// /// Gets or sets how nullable annotations and the #nullable enable directive are emitted by - /// and type rendering. The value is seeded from + /// and type rendering. The value is seeded from /// at construction. /// public NullableDirectiveMode NullableDirectiveMode { get; set; } @@ -169,11 +188,83 @@ public CodeWriter( /// public bool DefaultIncludeGeneratedAttributes { get; set; } = true; + /// + /// Gets or sets the default accessibility emitted for type declarations (classes, structs, records, + /// interfaces, enums, and delegates) when a declaration does not specify one. The default is + /// . Set to to omit the + /// modifier. + /// + public TypeDeclarationAccessibility? DefaultTypeAccessibility { get; set; } = TypeDeclarationAccessibility.Public; + + /// + /// Gets or sets the default accessibility emitted for properties and indexers when a declaration does + /// not specify one. The default is . Set to + /// to omit the modifier. + /// + public TypeDeclarationAccessibility? DefaultPropertyAccessibility { get; set; } = + TypeDeclarationAccessibility.Public; + + /// + /// Gets or sets the default accessibility emitted for property and indexer getters when a declaration + /// does not specify one. The default is . The + /// modifier is emitted only when it is more restrictive than the property's own accessibility; + /// otherwise the accessor inherits it. + /// + public TypeDeclarationAccessibility? DefaultPropertyGetterAccessibility { get; set; } = + TypeDeclarationAccessibility.Public; + + /// + /// Gets or sets the default accessibility emitted for property and indexer setters when a declaration + /// does not specify one. The default is . The + /// modifier is emitted only when it is more restrictive than the property's own accessibility; + /// otherwise the accessor inherits it. + /// + public TypeDeclarationAccessibility? DefaultPropertySetterAccessibility { get; set; } = + TypeDeclarationAccessibility.Public; + + /// + /// Gets or sets the default accessibility emitted for field declarations when a declaration does not + /// specify one. The default is . Set to + /// to omit the modifier. + /// + public TypeDeclarationAccessibility? DefaultFieldAccessibility { get; set; } = TypeDeclarationAccessibility.Private; + + /// + /// Gets or sets the default accessibility emitted for method declarations when a declaration does not + /// specify one. The default is . Set to + /// to omit the modifier. + /// + public TypeDeclarationAccessibility? DefaultMethodAccessibility { get; set; } = TypeDeclarationAccessibility.Public; + + /// + /// Gets or sets the default accessibility emitted for constructor declarations when a declaration does + /// not specify one. The default is . Set to + /// to omit the modifier. + /// + public TypeDeclarationAccessibility? DefaultConstructorAccessibility { get; set; } = + TypeDeclarationAccessibility.Public; + + /// + /// Gets or sets the default accessibility emitted for indexer declarations when a declaration does not + /// specify one. The default is . Set to + /// to omit the modifier. + /// + public TypeDeclarationAccessibility? DefaultIndexerAccessibility { get; set; } = + TypeDeclarationAccessibility.Public; + + /// + /// Gets or sets the default accessibility emitted for operator declarations when a declaration does + /// not specify one. The default is . Set to + /// to omit the modifier. + /// + public TypeDeclarationAccessibility? DefaultOperatorAccessibility { get; set; } = + TypeDeclarationAccessibility.Public; + /// /// Increases the current indentation level. /// /// The current writer. - /// writer.Indent().WriteLine("value"); + /// writer.Indent().Line("value"); public CodeWriter Indent() { _indentLevel++; @@ -187,7 +278,7 @@ public CodeWriter Indent() /// /// The current indentation level is zero. /// - /// writer.Indent().WriteLine("value").Unindent(); + /// writer.Indent().Line("value").Unindent(); public CodeWriter Unindent() { if (_indentLevel == 0) @@ -227,7 +318,7 @@ public CodeWriter EnsureNewLine() /// already at the start of a line. Calling it repeatedly does not add additional blank lines. /// /// The current writer. - /// writer.WriteMethodCall("Run").EnsureBlankLine().Comment("Explains the next member."); + /// writer.MethodCall("Run").EnsureBlankLine().Comment("Explains the next member."); public CodeWriter EnsureBlankLine() { if (_builder.Length == 0) @@ -245,13 +336,13 @@ public CodeWriter EnsureBlankLine() /// /// The value to write, or to write an empty line. /// The current writer. - /// writer.WriteLine("return value;"); - public CodeWriter WriteLine(string? value = null) + /// writer.Line("return value;"); + public CodeWriter Line(string? value = null) { if (value is null) return NewLine(); - WriteIndentIfRequired(); + IndentIfRequired(); _builder.Append(value); _builder.Append(NewLineCharacter); _atLineStart = true; @@ -270,27 +361,13 @@ public CodeWriter Comment(params string[] comments) return this; if (comments.Length == 1) - return Write("// ").WriteLine(comments[0]); + return Write("// ").Line(comments[0]); - WriteLine("/*"); + Line("/*"); for (var index = 0; index < comments.Length; index++) - Write(" * ").WriteLine(comments[index]); - - return WriteLine(" */"); - } - - /// - /// Writes the current indentation without writing content. - /// - /// The current writer. - /// writer.Indent().WriteIndent().Write("value"); - public CodeWriter WriteIndent() - { - if (_indentLevel != 0) - AppendIndentation(); + Write(" * ").Line(comments[index]); - _atLineStart = false; - return this; + return Line(" */"); } /// @@ -304,7 +381,7 @@ public CodeWriter Write(string? value) if (string.IsNullOrEmpty(value)) return this; - WriteIndentIfRequired(); + IndentIfRequired(); _builder.Append(value); return this; } @@ -317,7 +394,7 @@ public CodeWriter Write(string? value) /// writer.Write('{'); public CodeWriter Write(char value) { - WriteIndentIfRequired(); + IndentIfRequired(); _builder.Append(value); return this; } @@ -336,7 +413,7 @@ public CodeWriter Write(char value) /// The value to write, or for an empty line. /// The current writer. /// writer.AppendLine("return value;"); - public CodeWriter AppendLine(string? value = null) => WriteLine(value); + public CodeWriter AppendLine(string? value = null) => Line(value); /// /// Writes a value when the supplied condition is true. @@ -344,8 +421,8 @@ public CodeWriter Write(char value) /// Whether to write the value. /// The value to write. /// The current writer. - /// writer.WriteIf(includeValue, "value"); - public CodeWriter WriteIf(bool condition, string? value) => condition ? Write(value) : this; + /// writer.If(includeValue, "value"); + public CodeWriter If(bool condition, string? value) => condition ? Write(value) : this; /// /// Writes a line when the supplied condition is true. @@ -353,8 +430,8 @@ public CodeWriter Write(char value) /// Whether to write the line. /// The value to write. /// The current writer. - /// writer.WriteLineIf(includeValue, "value"); - public CodeWriter WriteLineIf(bool condition, string? value) => condition ? WriteLine(value) : this; + /// writer.LineIf(includeValue, "value"); + public CodeWriter LineIf(bool condition, string? value) => condition ? Line(value) : this; /// /// Writes a value surrounded by double quotes. @@ -385,7 +462,7 @@ public CodeWriter Quote(string? value = null) /// /// Optional content written before the opening brace. /// A scope that restores indentation and writes the closing token. - /// using (writer.OpenBlockScope("if (enabled)")) writer.WriteLine("Run();"); + /// using (writer.OpenBlockScope("if (enabled)")) writer.Line("Run();"); /// // if (enabled) /// // { /// // Run(); @@ -395,7 +472,7 @@ public CodeWriter Quote(string? value = null) /// /// Writes a complete block and invokes a callback for its body. /// - /// writer.OpenBlock("if (enabled)", body => body.WriteLine("Run();")); + /// writer.OpenBlock("if (enabled)", body => body.Line("Run();")); public CodeWriter OpenBlock(string? header, Action bodyWriter) { if (bodyWriter is null) @@ -414,7 +491,7 @@ public CodeWriter OpenBlock(string? header, Action bodyWriter) /// The opening token, or for none. /// The closing token, or for none. /// A scope that restores indentation and writes the closing token. - /// using (writer.OpenDelimitedBlockScope("items", "(", ");")) writer.WriteLine("value"); + /// using (writer.OpenDelimitedBlockScope("items", "(", ");")) writer.Line("value"); /// // items /// // ( /// // value @@ -428,7 +505,7 @@ public BlockScope OpenDelimitedBlockScope(string? header, string? openingToken, } if (openingToken is not null) - WriteLine(openingToken); + Line(openingToken); Indent(); return TrackOpenBlockScope(header, closingToken); @@ -437,7 +514,7 @@ public BlockScope OpenDelimitedBlockScope(string? header, string? openingToken, /// /// Writes a complete explicitly delimited block and invokes a callback for its body. /// - /// writer.OpenDelimitedBlock("items", "(", ");", body => body.WriteLine("value")); + /// writer.OpenDelimitedBlock("items", "(", ");", body => body.Line("value")); public CodeWriter OpenDelimitedBlock( string? header, string? openingToken, @@ -461,7 +538,7 @@ Action bodyWriter /// The closing token, or for none. /// A scope that restores indentation and writes the closing token. /// using (writer.OpenDelimitedBlockWithHeaderScope("Call", w => w.Write("(value)"), "{", "}")) - /// writer.WriteLine("Run();"); + /// writer.Line("Run();"); public BlockScope OpenDelimitedBlockWithHeaderScope( string? header, Action writeRemainingHeader, @@ -479,7 +556,7 @@ public BlockScope OpenDelimitedBlockWithHeaderScope( EnsureNewLine(); if (openingToken is not null) - WriteLine(openingToken); + Line(openingToken); Indent(); return TrackOpenBlockScope(header, closingToken); @@ -488,7 +565,7 @@ public BlockScope OpenDelimitedBlockWithHeaderScope( /// /// Writes a complete delimited block with a callback-completed header and body. /// - /// writer.OpenDelimitedBlockWithHeader("Call", w => w.Write("(value)"), "{", "}", body => body.WriteLine("Run();")); + /// writer.OpenDelimitedBlockWithHeader("Call", w => w.Write("(value)"), "{", "}", body => body.Line("Run();")); public CodeWriter OpenDelimitedBlockWithHeader( string? header, Action writeRemainingHeader, @@ -510,9 +587,8 @@ Action bodyWriter /// Optional content written before the opening token. /// The block body. /// The current writer. - /// writer.WriteBlock("if (enabled)", body => body.WriteLine("Run();")); - public CodeWriter WriteBlock(string? header, Action body) => - WriteDelimitedBlock(header, "{", "}", body); + /// writer.Block("if (enabled)", body => body.Line("Run();")); + public CodeWriter Block(string? header, Action body) => DelimitedBlock(header, "{", "}", body); /// /// Writes a complete scope using explicit opening and closing tokens. @@ -522,8 +598,8 @@ public CodeWriter WriteBlock(string? header, Action body) => /// The closing token, or for none. /// The block body. /// The current writer. - /// writer.WriteDelimitedBlock("items", "(", ");", body => body.WriteLine("value")); - public CodeWriter WriteDelimitedBlock( + /// writer.DelimitedBlock("items", "(", ");", body => body.Line("value")); + public CodeWriter DelimitedBlock( string? header, string? openingToken, string? closingToken, @@ -547,16 +623,16 @@ Action body /// The method body scope, or an empty scope when an abstract or expression-bodied method was /// emitted. /// - /// using (writer.WriteMethodScope(new MethodDeclarationOptions("Run"))) writer.WriteLine("return;"); - public BlockScope WriteMethodScope(MethodDeclarationOptions declaration) => - WriteMethodScope(declaration, expressionWriter: null); + /// using (writer.MethodScope(new MethodDeclarationOptions("Run"))) writer.Line("return;"); + public BlockScope MethodScope(MethodDeclarationOptions declaration) => + MethodScope(declaration, expressionWriter: null); - BlockScope WriteMethodScope(MethodDeclarationOptions declaration, Action? expressionWriter) + BlockScope MethodScope(MethodDeclarationOptions declaration, Action? expressionWriter) { if (declaration.ReturnType.IsEmpty) return default; - WriteMethodHeader(declaration); + MethodHeader(declaration); if (declaration.IsPartial) { @@ -569,15 +645,15 @@ BlockScope WriteMethodScope(MethodDeclarationOptions declaration, Action "); - WriteExpression(declaration.ExpressionBody, expressionWriter); - WriteLine(";"); + Expression(declaration.ExpressionBody, expressionWriter); + Line(";"); CompleteWrittenItem(WrittenItemKind.Method, _indentLevel); return default; } if (declaration.IsAbstract) { - WriteLine(";"); + Line(";"); CompleteWrittenItem(WrittenItemKind.Method, _indentLevel); return default; } @@ -586,19 +662,19 @@ BlockScope WriteMethodScope(MethodDeclarationOptions declaration, Action /// Writes a structured partial method declaration. /// - /// writer.WritePartialMethod(new MethodDeclarationOptions("OnChanged")); - public CodeWriter WritePartialMethod(MethodDeclarationOptions declaration) + /// writer.PartialMethod(new MethodDeclarationOptions("OnChanged")); + public CodeWriter PartialMethod(MethodDeclarationOptions declaration) { - WriteMethodScope(declaration with { IsPartial = true }); + MethodScope(declaration with { IsPartial = true }); return this; } /// /// Writes an expression-bodied method. /// - /// writer.WriteMethodExpression(new MethodDeclarationOptions("Count", "int") { ExpressionBody = "items.Count" }); - public CodeWriter WriteMethodExpression(MethodDeclarationOptions declaration) + /// writer.MethodExpression(new MethodDeclarationOptions("Count", "int") { ExpressionBody = "items.Count" }); + public CodeWriter MethodExpression(MethodDeclarationOptions declaration) { if (string.IsNullOrWhiteSpace(declaration.ExpressionBody)) { @@ -645,7 +718,7 @@ public CodeWriter WriteMethodExpression(MethodDeclarationOptions declaration) ); } - using (WriteMethodScope(declaration)) + using (MethodScope(declaration)) { // } @@ -656,8 +729,8 @@ public CodeWriter WriteMethodExpression(MethodDeclarationOptions declaration) /// /// Writes an expression-bodied method using a callback for the expression. /// - /// writer.WriteMethodExpression(new MethodDeclarationOptions("Count", "int"), expression => expression.Write("items.Count")); - public CodeWriter WriteMethodExpression(MethodDeclarationOptions declaration, Action writeExpression) + /// writer.MethodExpression(new MethodDeclarationOptions("Count", "int"), expression => expression.Write("items.Count")); + public CodeWriter MethodExpression(MethodDeclarationOptions declaration, Action writeExpression) { if (writeExpression is null) throw new ArgumentNullException(nameof(writeExpression)); @@ -667,7 +740,7 @@ public CodeWriter WriteMethodExpression(MethodDeclarationOptions declaration, Ac nameof(declaration) ); - using (WriteMethodScope(declaration with { ExpressionBody = string.Empty }, writeExpression)) + using (MethodScope(declaration with { ExpressionBody = string.Empty }, writeExpression)) { // } @@ -678,8 +751,8 @@ public CodeWriter WriteMethodExpression(MethodDeclarationOptions declaration, Ac /// /// Writes a structured method and invokes a callback for its body. /// - /// writer.WriteMethod(new MethodDeclarationOptions("Run"), body => body.WriteLine("return;")); - public CodeWriter WriteMethod(MethodDeclarationOptions declaration, Action writeBody) + /// writer.Method(new MethodDeclarationOptions("Run"), body => body.Line("return;")); + public CodeWriter Method(MethodDeclarationOptions declaration, Action writeBody) { if (writeBody is null) throw new ArgumentNullException(nameof(writeBody)); @@ -697,7 +770,7 @@ public CodeWriter WriteMethod(MethodDeclarationOptions declaration, Action /// The operator body scope, or an empty scope when an expression-bodied operator was emitted. /// - /// using (writer.WriteOperatorScope(new OperatorDeclarationOptions("==", TypeLibrary.System.Boolean, left, right))) writer.WriteLine("return left.Equals(right);"); - public BlockScope WriteOperatorScope(OperatorDeclarationOptions declaration) + /// using (writer.OperatorScope(new OperatorDeclarationOptions("==", TypeLibrary.System.Boolean, left, right))) writer.Line("return left.Equals(right);"); + public BlockScope OperatorScope(OperatorDeclarationOptions declaration) { if (declaration.ReturnType.IsEmpty) return default; @@ -728,31 +801,31 @@ public BlockScope WriteOperatorScope(OperatorDeclarationOptions declaration) BeginWrittenItem(WrittenItemKind.Method); if (declaration.IncludeGeneratedAttributes ?? DefaultIncludeGeneratedAttributes) - WriteGeneratedAttributes(includeCoverageExclusion: true, includeEmbeddedAttribute: false); + GeneratedAttributes(includeCoverageExclusion: true, includeEmbeddedAttribute: false); - WriteAttributes(declaration.Attributes); + Attributes(declaration.Attributes); - if (declaration.Accessibility is { } accessibility) - WriteAccessibility(accessibility).Write(' '); + if (ResolveAccessibility(declaration.Accessibility, DefaultOperatorAccessibility) is { } accessibility) + Accessibility(accessibility).Write(' '); - WriteIf(declaration.IsStatic, "static "); + If(declaration.IsStatic, "static "); switch (declaration.Kind) { case OperatorDeclarationKind.ImplicitConversion: case OperatorDeclarationKind.ExplicitConversion: Write(declaration.Kind == OperatorDeclarationKind.ImplicitConversion ? "implicit " : "explicit "); - Write("operator ").WriteTypeReference(declaration.ReturnType); - WriteParametersWithHeuristic([declaration.Left]); + Write("operator ").TypeReference(declaration.ReturnType); + ParametersWithHeuristic([declaration.Left]); break; case OperatorDeclarationKind.Unary: - WriteTypeReference(declaration.ReturnType).Write(" operator ").Write(declaration.OperatorToken); - WriteParametersWithHeuristic([declaration.Left]); + TypeReference(declaration.ReturnType).Write(" operator ").Write(declaration.OperatorToken); + ParametersWithHeuristic([declaration.Left]); break; case OperatorDeclarationKind.Binary: - WriteTypeReference(declaration.ReturnType).Write(" operator ").Write(declaration.OperatorToken); - WriteParametersWithHeuristic([declaration.Left, declaration.Right]); + TypeReference(declaration.ReturnType).Write(" operator ").Write(declaration.OperatorToken); + ParametersWithHeuristic([declaration.Left, declaration.Right]); break; default: @@ -762,8 +835,8 @@ public BlockScope WriteOperatorScope(OperatorDeclarationOptions declaration) if (declaration.ExpressionBody is not null) { Write(" => "); - WriteExpression(declaration.ExpressionBody, expressionWriter: null); - WriteLine(";"); + Expression(declaration.ExpressionBody, expressionWriter: null); + Line(";"); CompleteWrittenItem(WrittenItemKind.Method, _indentLevel); return default; } @@ -779,8 +852,8 @@ public BlockScope WriteOperatorScope(OperatorDeclarationOptions declaration) /// The action that writes the operator body. /// The current writer. /// The operator has an expression body. - /// writer.WriteOperator(new OperatorDeclarationOptions("==", TypeLibrary.System.Boolean, left, right), body => body.WriteLine("return left.Equals(right);")); - public CodeWriter WriteOperator(OperatorDeclarationOptions declaration, Action writeBody) + /// writer.Operator(new OperatorDeclarationOptions("==", TypeLibrary.System.Boolean, left, right), body => body.Line("return left.Equals(right);")); + public CodeWriter Operator(OperatorDeclarationOptions declaration, Action writeBody) { if (writeBody is null) throw new ArgumentNullException(nameof(writeBody)); @@ -790,7 +863,7 @@ public CodeWriter WriteOperator(OperatorDeclarationOptions declaration, Action /// Writes an auto-property or expression-bodied property. /// - /// writer.WriteProperty(new PropertyDeclarationOptions("Name", "string")); - public CodeWriter WriteProperty(PropertyDeclarationOptions declaration) + /// writer.Property(new PropertyDeclarationOptions("Name", "string")); + public CodeWriter Property(PropertyDeclarationOptions declaration) { if (declaration.Type.IsEmpty) return this; ValidatePropertyDeclaration(declaration); + var propertyAccessibility = ResolveAccessibility(declaration.Accessibility, DefaultPropertyAccessibility); BeginWrittenItem(WrittenItemKind.Property); if (declaration.IncludeGeneratedAttributes ?? DefaultIncludeGeneratedAttributes) - WriteGeneratedAttributes(includeCoverageExclusion: true, includeEmbeddedAttribute: false); - WriteAttributes(declaration.Attributes); - WritePropertyHeader(declaration); + GeneratedAttributes(includeCoverageExclusion: true, includeEmbeddedAttribute: false); + Attributes(declaration.Attributes); + PropertyHeader(declaration); if (declaration.ExpressionBody is not null) { Write(" => "); - WriteExpression(declaration.ExpressionBody, expressionWriter: null); - WriteLine(";"); + Expression(declaration.ExpressionBody, expressionWriter: null); + Line(";"); CompleteWrittenItem(WrittenItemKind.Property, _indentLevel); return this; } @@ -831,16 +905,35 @@ public CodeWriter WriteProperty(PropertyDeclarationOptions declaration) else { if (declaration.HasGetter) - WriteAccessor(declaration.GetterAccessibility, "get;"); + { + Accessor( + ResolveAccessorAccessibility( + declaration.GetterAccessibility, + DefaultPropertyGetterAccessibility, + propertyAccessibility + ), + "get;" + ); + } + if (declaration.HasSetter || declaration.IsInitOnly) - WriteAccessor(declaration.SetterAccessibility, declaration.IsInitOnly ? "init;" : "set;"); + { + Accessor( + ResolveAccessorAccessibility( + declaration.SetterAccessibility, + DefaultPropertySetterAccessibility, + propertyAccessibility + ), + declaration.IsInitOnly ? "init;" : "set;" + ); + } } Write("}"); if (declaration.Initializer is not null) { Write(" = "); - WriteExpression(declaration.Initializer, expressionWriter: null); + Expression(declaration.Initializer, expressionWriter: null); Write(';'); } NewLine(); @@ -851,11 +944,8 @@ public CodeWriter WriteProperty(PropertyDeclarationOptions declaration) /// /// Writes an expression-bodied property using a callback for the expression. /// - /// writer.WritePropertyExpression(new PropertyDeclarationOptions("Count", "int"), expression => expression.Write("items.Count")); - public CodeWriter WritePropertyExpression( - PropertyDeclarationOptions declaration, - Action writeExpression - ) + /// writer.PropertyExpression(new PropertyDeclarationOptions("Count", "int"), expression => expression.Write("items.Count")); + public CodeWriter PropertyExpression(PropertyDeclarationOptions declaration, Action writeExpression) { if (writeExpression is null) throw new ArgumentNullException(nameof(writeExpression)); @@ -873,11 +963,11 @@ Action writeExpression ValidatePropertyDeclaration(declaration with { ExpressionBody = "callback" }); BeginWrittenItem(WrittenItemKind.Property); if (declaration.IncludeGeneratedAttributes ?? DefaultIncludeGeneratedAttributes) - WriteGeneratedAttributes(includeCoverageExclusion: true, includeEmbeddedAttribute: false); - WriteAttributes(declaration.Attributes); - WritePropertyHeader(declaration).Write(" => "); - WriteExpression(null, writeExpression); - WriteLine(";"); + GeneratedAttributes(includeCoverageExclusion: true, includeEmbeddedAttribute: false); + Attributes(declaration.Attributes); + PropertyHeader(declaration).Write(" => "); + Expression(null, writeExpression); + Line(";"); CompleteWrittenItem(WrittenItemKind.Property, _indentLevel); return this; } @@ -885,8 +975,8 @@ Action writeExpression /// /// Writes a property with callback-generated accessor bodies. /// - /// writer.WriteProperty(new PropertyDeclarationOptions("Value", "int"), get => get.WriteLine("return _value;"), null); - public CodeWriter WriteProperty( + /// writer.Property(new PropertyDeclarationOptions("Value", "int"), get => get.Line("return _value;"), null); + public CodeWriter Property( PropertyDeclarationOptions declaration, Action? writeGetterBody, Action? writeSetterBody @@ -904,21 +994,39 @@ public CodeWriter WriteProperty( nameof(declaration) ); + var propertyAccessibility = ResolveAccessibility(declaration.Accessibility, DefaultPropertyAccessibility); BeginWrittenItem(WrittenItemKind.Property); if (declaration.IncludeGeneratedAttributes ?? DefaultIncludeGeneratedAttributes) - WriteGeneratedAttributes(includeCoverageExclusion: true, includeEmbeddedAttribute: false); - WriteAttributes(declaration.Attributes); - WritePropertyHeader(declaration).NewLine(); + GeneratedAttributes(includeCoverageExclusion: true, includeEmbeddedAttribute: false); + Attributes(declaration.Attributes); + PropertyHeader(declaration).NewLine(); using (OpenBlockScope()) { if (declaration.HasGetter) - WriteAccessorBody(declaration.GetterAccessibility, "get", writeGetterBody); + { + AccessorBody( + ResolveAccessorAccessibility( + declaration.GetterAccessibility, + DefaultPropertyGetterAccessibility, + propertyAccessibility + ), + "get", + writeGetterBody + ); + } + if (declaration.HasSetter || declaration.IsInitOnly) - WriteAccessorBody( - declaration.SetterAccessibility, + { + AccessorBody( + ResolveAccessorAccessibility( + declaration.SetterAccessibility, + DefaultPropertySetterAccessibility, + propertyAccessibility + ), declaration.IsInitOnly ? "init" : "set", writeSetterBody ); + } } CompleteWrittenItem(WrittenItemKind.Property, _indentLevel); return this; @@ -929,31 +1037,52 @@ public CodeWriter WriteProperty( /// /// The indexer declaration. /// The current writer. - /// writer.WriteIndexer(new IndexerDeclarationOptions(Type("string"), new("index", Type("int")))); - public CodeWriter WriteIndexer(IndexerDeclarationOptions declaration) + /// writer.Indexer(new IndexerDeclarationOptions(Type("string"), new("index", Type("int")))); + public CodeWriter Indexer(IndexerDeclarationOptions declaration) { if (declaration.Type.IsEmpty) return this; ValidateIndexerDeclaration(declaration); + var indexerAccessibility = ResolveAccessibility(declaration.Accessibility, DefaultIndexerAccessibility); BeginWrittenItem(WrittenItemKind.Property); if (declaration.IncludeGeneratedAttributes ?? DefaultIncludeGeneratedAttributes) - WriteGeneratedAttributes(includeCoverageExclusion: true, includeEmbeddedAttribute: false); - WriteAttributes(declaration.Attributes); - WriteIndexerHeader(declaration); + GeneratedAttributes(includeCoverageExclusion: true, includeEmbeddedAttribute: false); + Attributes(declaration.Attributes); + IndexerHeader(declaration); if (declaration.ExpressionBody is not null) { Write(" => "); - WriteExpression(declaration.ExpressionBody, expressionWriter: null); - WriteLine(";"); + Expression(declaration.ExpressionBody, expressionWriter: null); + Line(";"); CompleteWrittenItem(WrittenItemKind.Property, _indentLevel); return this; } Write(" { "); if (declaration.HasGetter) - WriteAccessor(declaration.GetterAccessibility, "get;"); + { + Accessor( + ResolveAccessorAccessibility( + declaration.GetterAccessibility, + DefaultPropertyGetterAccessibility, + indexerAccessibility + ), + "get;" + ); + } + if (declaration.HasSetter || declaration.IsInitOnly) - WriteAccessor(declaration.SetterAccessibility, declaration.IsInitOnly ? "init;" : "set;"); + { + Accessor( + ResolveAccessorAccessibility( + declaration.SetterAccessibility, + DefaultPropertySetterAccessibility, + indexerAccessibility + ), + declaration.IsInitOnly ? "init;" : "set;" + ); + } + Write("}"); NewLine(); CompleteWrittenItem(WrittenItemKind.Property, _indentLevel); @@ -967,8 +1096,8 @@ public CodeWriter WriteIndexer(IndexerDeclarationOptions declaration) /// The action that writes the getter body, or for an auto getter. /// The action that writes the setter body, or for an auto setter. /// The current writer. - /// writer.WriteIndexer(new IndexerDeclarationOptions(Type("string"), new("index", Type("int"))), get => get.WriteLine("return _items[index];"), null); - public CodeWriter WriteIndexer( + /// writer.Indexer(new IndexerDeclarationOptions(Type("string"), new("index", Type("int"))), get => get.Line("return _items[index];"), null); + public CodeWriter Indexer( IndexerDeclarationOptions declaration, Action? writeGetterBody, Action? writeSetterBody @@ -986,42 +1115,60 @@ public CodeWriter WriteIndexer( nameof(declaration) ); + var indexerAccessibility = ResolveAccessibility(declaration.Accessibility, DefaultIndexerAccessibility); BeginWrittenItem(WrittenItemKind.Property); if (declaration.IncludeGeneratedAttributes ?? DefaultIncludeGeneratedAttributes) - WriteGeneratedAttributes(includeCoverageExclusion: true, includeEmbeddedAttribute: false); - WriteAttributes(declaration.Attributes); - WriteIndexerHeader(declaration).NewLine(); + GeneratedAttributes(includeCoverageExclusion: true, includeEmbeddedAttribute: false); + Attributes(declaration.Attributes); + IndexerHeader(declaration).NewLine(); using (OpenBlockScope()) { if (declaration.HasGetter) - WriteAccessorBody(declaration.GetterAccessibility, "get", writeGetterBody); + { + AccessorBody( + ResolveAccessorAccessibility( + declaration.GetterAccessibility, + DefaultPropertyGetterAccessibility, + indexerAccessibility + ), + "get", + writeGetterBody + ); + } + if (declaration.HasSetter || declaration.IsInitOnly) - WriteAccessorBody( - declaration.SetterAccessibility, + { + AccessorBody( + ResolveAccessorAccessibility( + declaration.SetterAccessibility, + DefaultPropertySetterAccessibility, + indexerAccessibility + ), declaration.IsInitOnly ? "init" : "set", writeSetterBody ); + } } CompleteWrittenItem(WrittenItemKind.Property, _indentLevel); return this; } - CodeWriter WriteIndexerHeader(IndexerDeclarationOptions declaration) + CodeWriter IndexerHeader(IndexerDeclarationOptions declaration) { - WriteMemberModifiers( - declaration.Accessibility, + MemberModifiers( + ResolveAccessibility(declaration.Accessibility, DefaultIndexerAccessibility), declaration.IsStatic, declaration.IsAbstract, declaration.IsVirtual, declaration.IsOverride, declaration.IsSealed ); - WriteTypeReference(declaration.Type).Write(" this["); + TypeReference(declaration.Type).Write(" this["); for (var index = 0; index < declaration.Parameters.Length; index++) { if (index != 0) Write(", "); - WriteParameter(declaration.Parameters[index]); + Parameter(declaration.Parameters[index]); } return Write(']'); } @@ -1029,33 +1176,33 @@ CodeWriter WriteIndexerHeader(IndexerDeclarationOptions declaration) /// /// Writes a field declaration. /// - /// writer.WriteField(new FieldDeclarationOptions("_value", "int")); - public CodeWriter WriteField(FieldDeclarationOptions declaration) + /// writer.Field(new FieldDeclarationOptions("_value", "int")); + public CodeWriter Field(FieldDeclarationOptions declaration) { if (declaration.Type.IsEmpty) return this; ValidateFieldDeclaration(declaration); BeginWrittenItem(WrittenItemKind.Field); if (declaration.IncludeGeneratedAttributes ?? DefaultIncludeGeneratedAttributes) - WriteGeneratedAttributes(includeCoverageExclusion: false, includeEmbeddedAttribute: false); - WriteAttributes(declaration.Attributes); - if (declaration.Accessibility is { } accessibility) - WriteAccessibility(accessibility).Write(' '); - WriteIf(declaration.IsRequired, "required ") - .WriteIf(declaration.IsConst, "const ") - .WriteIf(declaration.IsStatic && !declaration.IsConst, "static ") - .WriteIf(declaration.IsReadOnly, "readonly ") - .WriteIf(declaration.IsVolatile, "volatile ") - .WriteIf(declaration.IsRefField, "ref ") - .WriteTypeReference(declaration.Type) + GeneratedAttributes(includeCoverageExclusion: false, includeEmbeddedAttribute: false); + Attributes(declaration.Attributes); + if (ResolveAccessibility(declaration.Accessibility, DefaultFieldAccessibility) is { } accessibility) + Accessibility(accessibility).Write(' '); + If(declaration.IsRequired, "required ") + .If(declaration.IsConst, "const ") + .If(declaration.IsStatic && !declaration.IsConst, "static ") + .If(declaration.IsReadOnly, "readonly ") + .If(declaration.IsVolatile, "volatile ") + .If(declaration.IsRefField, "ref ") + .TypeReference(declaration.Type) .Write(' ') .Write(declaration.Name); if (declaration.Initializer is not null) { Write(" = "); - WriteExpression(declaration.Initializer, expressionWriter: null); + Expression(declaration.Initializer, expressionWriter: null); } - WriteLine(";"); + Line(";"); CompleteWrittenItem(WrittenItemKind.Field, _indentLevel); return this; } @@ -1066,12 +1213,12 @@ public CodeWriter WriteField(FieldDeclarationOptions declaration) /// The namespace to import. /// Whether the directive is emitted as a global using. /// The current writer. - /// writer.WriteUsing("System"); // using System; - public CodeWriter WriteUsing(string namespaceName, bool isGlobal = false) + /// writer.Using("System"); // using System; + public CodeWriter Using(string namespaceName, bool isGlobal = false) { return string.IsNullOrWhiteSpace(namespaceName) ? throw new ArgumentException("Namespace cannot be null or whitespace.", nameof(namespaceName)) - : Write(isGlobal ? "global using " : "using ").Write(namespaceName).WriteLine(";"); + : Write(isGlobal ? "global using " : "using ").Write(namespaceName).Line(";"); } /// @@ -1080,16 +1227,16 @@ public CodeWriter WriteUsing(string namespaceName, bool isGlobal = false) /// The alias name. /// The aliased namespace or type. /// The current writer. - /// writer.WriteUsingAlias("Events", "global::Purview.Events"); // using Events = global::Purview.Events; - public CodeWriter WriteUsingAlias(string alias, string target) + /// writer.UsingAlias("Events", "global::Purview.Events"); // using Events = global::Purview.Events; + public CodeWriter UsingAlias(string alias, string target) { if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentException("Alias cannot be null or whitespace.", nameof(alias)); if (string.IsNullOrWhiteSpace(target)) throw new ArgumentException("Alias target cannot be null or whitespace.", nameof(target)); - // The alias directive is not indented, so we don't call WriteIndentIfRequired(). - return Write("using ").Write(alias).Write(" = ").Write(target).WriteLine(";"); + // The alias directive is not indented, so we don't call IndentIfRequired(). + return Write("using ").Write(alias).Write(" = ").Write(target).Line(";"); } /// @@ -1098,14 +1245,14 @@ public CodeWriter WriteUsingAlias(string alias, string target) /// /// The region name. /// The region scope. - /// using (writer.OpenRegionScope("Generated members")) writer.WriteLine("public int Value { get; }"); + /// using (writer.OpenRegionScope("Generated members")) writer.Line("public int Value { get; }"); public BlockScope OpenRegionScope(string name) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Region name cannot be null or whitespace.", nameof(name)); EnsureBlankLine(); - Write("#region ").WriteLine(name); + Write("#region ").Line(name); Indent(); return TrackOpenBlockScope(header: null, closingSeparator: "#endregion"); } @@ -1116,7 +1263,7 @@ public BlockScope OpenRegionScope(string name) /// The region name. /// The action that writes the region body. /// The current writer. - /// writer.OpenRegion("Generated members", body => body.WriteProperty(new PropertyDeclarationOptions("Value", "int"))); + /// writer.OpenRegion("Generated members", body => body.Property(new PropertyDeclarationOptions("Value", "int"))); public CodeWriter OpenRegion(string name, Action body) { if (body is null) @@ -1126,19 +1273,96 @@ public CodeWriter OpenRegion(string name, Action body) return this; } + /// + /// Writes a #if directive at column zero and returns a scope that writes #endif at column + /// zero when disposed. Content written inside the scope is indented normally. + /// + /// The preprocessor condition written after #if. + /// The conditional-compilation scope. + /// using (writer.HashDefinesScope("!EXCLUDE_PURVIEW_TELEMETRY_LOGGING")) writer.FileScopedNamespace("Example"); + public BlockScope HashDefinesScope(string expression) + { + ValidateStatementPart(expression, nameof(expression)); + if (_indentLevel == 0) + EnsureBlankLine(); + DirectiveLine("#if " + expression); + return TrackOpenBlockScope( + header: null, + closingSeparator: "#endif", + closingAtColumnZero: true, + changesIndentation: false + ); + } + + /// + /// Writes a #if/#endif block, with both directives at column zero, and invokes a callback + /// for its body. + /// + /// The preprocessor condition written after #if. + /// The action that writes the conditional block body. + /// The current writer. + /// writer.HashDefines("NET", body => body.Line("// NET only")); + public CodeWriter HashDefines(string expression, Action body) + { + if (body is null) + throw new ArgumentNullException(nameof(body)); + using (HashDefinesScope(expression)) + body(this); + return this; + } + + /// + /// Writes an #else directive at column zero, typically between the two bodies of a + /// HashDefinesScope block. The else body is written at the same indentation as the if body. + /// + /// The current writer. + /// using (writer.HashDefinesScope("NET")) { writer.Line("// NET"); writer.HashElse(); writer.Line("// other"); } + public CodeWriter HashElse() + { + DirectiveLine("#else"); + + // A directive between two declarations must not participate in member blank-line spacing, so + // reset the tracker to prevent BeginWrittenItem from inserting a blank line across the #else. + _lastWrittenItem = WrittenItemKind.None; + return this; + } + + /// + /// Returns a no-op scope that writes nothing and performs no indentation changes when disposed, so a + /// conditional block can be wrapped only when a guard requires it. + /// + /// A scope that does nothing when disposed. + /// using var scope = wrapped ? writer.EmptyScope() : writer.HashDefinesScope("EXCLUDE_PURVIEW_TELEMETRY_LOGGING"); + public BlockScope EmptyScope() => default; + + /// + /// Invokes a callback without opening any scope, mirroring the action form of the scope-returning + /// methods so conditional wrapping can choose between Empty and a real scope. + /// + /// The action to invoke. + /// The current writer. + /// writer.Empty(body => body.Line("value")); + public CodeWriter Empty(Action body) + { + if (body is null) + throw new ArgumentNullException(nameof(body)); + body(this); + return this; + } + /// /// Writes a block-scoped namespace and returns its body scope. /// /// The namespace, or to write nothing. /// The namespace body scope, or an empty scope when no namespace is supplied. - /// using (writer.WriteBlockNamespaceScope("Example")) writer.WriteLine("class C { }"); - public BlockScope WriteBlockNamespaceScope(string? namespaceName) + /// using (writer.BlockNamespaceScope("Example")) writer.Line("class C { }"); + public BlockScope BlockNamespaceScope(string? namespaceName) { if (string.IsNullOrWhiteSpace(namespaceName)) return default; BeginWrittenItem(WrittenItemKind.Namespace); - Write("namespace ").WriteLine(namespaceName); + Write("namespace ").Line(namespaceName); return OpenBlockScope(WrittenItemKind.Namespace); } @@ -1148,13 +1372,13 @@ public BlockScope WriteBlockNamespaceScope(string? namespaceName) /// The type reference whose namespace will be used, or a value with no namespace to omit the wrapper. /// The action that writes the namespace body. /// The current writer. - /// writer.WriteBlockNamespace(new TypeValueObject("C", "Example").AsTypeReference(), body => body.WriteLine("class C { }")); - public CodeWriter WriteBlockNamespace(TypeReference typeReference, Action bodyWriter) + /// writer.BlockNamespace(new TypeValueObject("C", "Example").AsTypeReference(), body => body.Line("class C { }")); + public CodeWriter BlockNamespace(TypeReference typeReference, Action bodyWriter) { if (bodyWriter is null) throw new ArgumentNullException(nameof(bodyWriter)); - using (WriteBlockNamespaceScope(typeReference)) + using (BlockNamespaceScope(typeReference)) bodyWriter(this); return this; @@ -1165,9 +1389,9 @@ public CodeWriter WriteBlockNamespace(TypeReference typeReference, Action /// The type reference whose namespace will be used, or a value with no namespace to return an empty scope. /// The namespace body scope, or an empty scope when no namespace is supplied. - /// using (writer.WriteBlockNamespaceScope(new TypeValueObject("C", "Example").AsTypeReference())) writer.WriteLine("class C { }"); - public IDisposable WriteBlockNamespaceScope(TypeReference? typeReference) => - typeReference is null ? NoOpScope.Instance : WriteBlockNamespaceScope(typeReference.Identity.Namespace); + /// using (writer.BlockNamespaceScope(new TypeValueObject("C", "Example").AsTypeReference())) writer.Line("class C { }"); + public IDisposable BlockNamespaceScope(TypeReference? typeReference) => + typeReference is null ? NoOpScope.Instance : BlockNamespaceScope(typeReference.Identity.Namespace); /// /// Writes a block-scoped namespace and invokes a callback for its body. @@ -1175,12 +1399,12 @@ public IDisposable WriteBlockNamespaceScope(TypeReference? typeReference) => /// The namespace, or to omit the wrapper. /// The action that writes the namespace body. /// The current writer. - /// writer.WriteBlockNamespace("Example", body => body.WriteLine("class C { }")); - public CodeWriter WriteBlockNamespace(string? namespaceName, Action bodyWriter) + /// writer.BlockNamespace("Example", body => body.Line("class C { }")); + public CodeWriter BlockNamespace(string? namespaceName, Action bodyWriter) { if (bodyWriter is null) throw new ArgumentNullException(nameof(bodyWriter)); - using (WriteBlockNamespaceScope(namespaceName)) + using (BlockNamespaceScope(namespaceName)) bodyWriter(this); return this; } @@ -1190,12 +1414,12 @@ public CodeWriter WriteBlockNamespace(string? namespaceName, Action /// /// The namespace, or to write nothing. /// The current writer. - /// writer.WriteFileScopedNamespace("Example"); // namespace Example; - public CodeWriter WriteFileScopedNamespace(string? namespaceName) + /// writer.FileScopedNamespace("Example"); // namespace Example; + public CodeWriter FileScopedNamespace(string? namespaceName) { return string.IsNullOrWhiteSpace(namespaceName) ? this - : Write("namespace ").Write(namespaceName).WriteLine(";").NewLine(); + : Write("namespace ").Write(namespaceName).Line(";").NewLine(); } /// @@ -1203,21 +1427,21 @@ public CodeWriter WriteFileScopedNamespace(string? namespaceName) /// /// The type reference whose namespace will be used, or a value with no namespace to write nothing. /// The current writer. - /// writer.WriteFileScopedNamespace(new TypeValueObject("C", "Example").AsTypeReference()); - public CodeWriter WriteFileScopedNamespace(TypeReference? typeReference) => - typeReference is null ? this : WriteFileScopedNamespace(typeReference.Identity.Namespace); + /// writer.FileScopedNamespace(new TypeValueObject("C", "Example").AsTypeReference()); + public CodeWriter FileScopedNamespace(TypeReference? typeReference) => + typeReference is null ? this : FileScopedNamespace(typeReference.Identity.Namespace); /// /// Writes a class declaration from structured options and returns its body scope. /// /// The class declaration options. /// The class body scope. - /// using (writer.WriteClassScope(new TypeDeclarationOptions("C"))) writer.WriteLine("// body"); - public BlockScope WriteClassScope(TypeDeclarationOptions declaration) + /// using (writer.ClassScope(new TypeDeclarationOptions("C"))) writer.Line("// body"); + public BlockScope ClassScope(TypeDeclarationOptions declaration) { return declaration is null ? throw new ArgumentNullException(nameof(declaration)) - : WriteTypeScope(declaration with { Kind = TypeDeclarationKind.Class }); + : TypeScope(declaration with { Kind = TypeDeclarationKind.Class }); } /// @@ -1226,13 +1450,13 @@ public BlockScope WriteClassScope(TypeDeclarationOptions declaration) /// The class declaration options. /// The action that writes the body of the class. /// The current writer. - /// writer.WriteClass(new TypeDeclarationOptions("C"), body => body.WriteLine("// body")); - public CodeWriter WriteClass(TypeDeclarationOptions declaration, Action bodyWriter) + /// writer.Class(new TypeDeclarationOptions("C"), body => body.Line("// body")); + public CodeWriter Class(TypeDeclarationOptions declaration, Action bodyWriter) { if (bodyWriter is null) throw new ArgumentNullException(nameof(bodyWriter)); - using (WriteClassScope(declaration)) + using (ClassScope(declaration)) bodyWriter(this); return this; @@ -1251,8 +1475,8 @@ public CodeWriter WriteClass(TypeDeclarationOptions declaration, ActionWhether derived classes and overriding members inherit the attribute. /// Whether more than one instance may be specified on one declaration. /// The current writer. - /// writer.WriteAttributeClass(new TypeDeclarationOptions("MarkerAttribute"), AttributeTargets.Class, _ => { }); - public CodeWriter WriteAttributeClass( + /// writer.AttributeClass(new TypeDeclarationOptions("MarkerAttribute"), AttributeTargets.Class, _ => { }); + public CodeWriter AttributeClass( TypeDeclarationOptions declaration, AttributeTargets targets, Action bodyWriter, @@ -1277,7 +1501,7 @@ public CodeWriter WriteAttributeClass( ], }; - return WriteClass( + return Class( declaration with { BaseType = declaration.BaseType ?? new TypeIdentity("Attribute", "System"), @@ -1309,12 +1533,12 @@ static string RenderAttributeTargets(AttributeTargets targets) /// /// The struct declaration options. /// The struct body scope. - /// using (writer.WriteStructScope(new TypeDeclarationOptions("Value"))) { } - public BlockScope WriteStructScope(TypeDeclarationOptions declaration) + /// using (writer.StructScope(new TypeDeclarationOptions("Value"))) { } + public BlockScope StructScope(TypeDeclarationOptions declaration) { return declaration is null ? throw new ArgumentNullException(nameof(declaration)) - : WriteTypeScope(declaration with { Kind = TypeDeclarationKind.Struct }); + : TypeScope(declaration with { Kind = TypeDeclarationKind.Struct }); } /// @@ -1323,13 +1547,13 @@ public BlockScope WriteStructScope(TypeDeclarationOptions declaration) /// The struct declaration options. /// The action that writes the body of the struct. /// The struct body scope. - /// writer.WriteStruct(new TypeDeclarationOptions("Value"), _ => { }); - public CodeWriter WriteStruct(TypeDeclarationOptions declaration, Action bodyWriter) + /// writer.Struct(new TypeDeclarationOptions("Value"), _ => { }); + public CodeWriter Struct(TypeDeclarationOptions declaration, Action bodyWriter) { if (bodyWriter is null) throw new ArgumentNullException(nameof(bodyWriter)); - using (WriteStructScope(declaration)) + using (StructScope(declaration)) bodyWriter(this); return this; @@ -1340,12 +1564,12 @@ public CodeWriter WriteStruct(TypeDeclarationOptions declaration, Action /// The record class declaration options. /// The record body scope. - /// using (writer.WriteRecordClassScope(new TypeDeclarationOptions("Model"))) { } - public BlockScope WriteRecordClassScope(TypeDeclarationOptions declaration) + /// using (writer.RecordClassScope(new TypeDeclarationOptions("Model"))) { } + public BlockScope RecordClassScope(TypeDeclarationOptions declaration) { return declaration is null ? throw new ArgumentNullException(nameof(declaration)) - : WriteTypeScope(declaration with { Kind = TypeDeclarationKind.RecordClass }); + : TypeScope(declaration with { Kind = TypeDeclarationKind.RecordClass }); } /// @@ -1354,13 +1578,13 @@ public BlockScope WriteRecordClassScope(TypeDeclarationOptions declaration) /// The record class declaration options. /// The action that writes the body of the record class. /// The current writer. - /// writer.WriteRecordClass(new TypeDeclarationOptions("Model"), _ => { }); - public CodeWriter WriteRecordClass(TypeDeclarationOptions declaration, Action bodyWriter) + /// writer.RecordClass(new TypeDeclarationOptions("Model"), _ => { }); + public CodeWriter RecordClass(TypeDeclarationOptions declaration, Action bodyWriter) { if (bodyWriter is null) throw new ArgumentNullException(nameof(bodyWriter)); - using (WriteRecordClassScope(declaration)) + using (RecordClassScope(declaration)) bodyWriter(this); return this; @@ -1371,12 +1595,12 @@ public CodeWriter WriteRecordClass(TypeDeclarationOptions declaration, Action /// The record struct declaration options. /// The record body scope. - /// using (writer.WriteRecordStructScope(new TypeDeclarationOptions("Value"))) { } - public BlockScope WriteRecordStructScope(TypeDeclarationOptions declaration) + /// using (writer.RecordStructScope(new TypeDeclarationOptions("Value"))) { } + public BlockScope RecordStructScope(TypeDeclarationOptions declaration) { return declaration is null ? throw new ArgumentNullException(nameof(declaration)) - : WriteTypeScope(declaration with { Kind = TypeDeclarationKind.RecordStruct }); + : TypeScope(declaration with { Kind = TypeDeclarationKind.RecordStruct }); } /// @@ -1385,13 +1609,13 @@ public BlockScope WriteRecordStructScope(TypeDeclarationOptions declaration) /// The record struct declaration options. /// The action that writes the body of the record struct. /// The current writer. - /// writer.WriteRecordStruct(new TypeDeclarationOptions("Value"), _ => { }); - public CodeWriter WriteRecordStruct(TypeDeclarationOptions declaration, Action bodyWriter) + /// writer.RecordStruct(new TypeDeclarationOptions("Value"), _ => { }); + public CodeWriter RecordStruct(TypeDeclarationOptions declaration, Action bodyWriter) { if (bodyWriter is null) throw new ArgumentNullException(nameof(bodyWriter)); - using (WriteRecordStructScope(declaration)) + using (RecordStructScope(declaration)) bodyWriter(this); return this; @@ -1400,21 +1624,21 @@ public CodeWriter WriteRecordStruct(TypeDeclarationOptions declaration, Action /// Writes an interface declaration and returns its body scope. /// - /// using (writer.WriteInterfaceScope(new TypeDeclarationOptions("IService"))) { } - public BlockScope WriteInterfaceScope(TypeDeclarationOptions declaration) => + /// using (writer.InterfaceScope(new TypeDeclarationOptions("IService"))) { } + public BlockScope InterfaceScope(TypeDeclarationOptions declaration) => declaration is null ? throw new ArgumentNullException(nameof(declaration)) - : WriteTypeScope(declaration with { Kind = TypeDeclarationKind.Interface }); + : TypeScope(declaration with { Kind = TypeDeclarationKind.Interface }); /// /// Writes an interface declaration and invokes a callback for its body. /// - /// writer.WriteInterface(new TypeDeclarationOptions("IService"), _ => { }); - public CodeWriter WriteInterface(TypeDeclarationOptions declaration, Action bodyWriter) + /// writer.Interface(new TypeDeclarationOptions("IService"), _ => { }); + public CodeWriter Interface(TypeDeclarationOptions declaration, Action bodyWriter) { if (bodyWriter is null) throw new ArgumentNullException(nameof(bodyWriter)); - using (WriteInterfaceScope(declaration)) + using (InterfaceScope(declaration)) bodyWriter(this); return this; } @@ -1422,22 +1646,22 @@ public CodeWriter WriteInterface(TypeDeclarationOptions declaration, Action /// Writes an enum declaration and returns its body scope. /// - /// using (writer.WriteEnumScope(new TypeDeclarationOptions("Status"))) { } - public BlockScope WriteEnumScope(TypeDeclarationOptions declaration) => + /// using (writer.EnumScope(new TypeDeclarationOptions("Status"))) { } + public BlockScope EnumScope(TypeDeclarationOptions declaration) => declaration is null ? throw new ArgumentNullException(nameof(declaration)) - : WriteTypeScope(declaration with { Kind = TypeDeclarationKind.Enum }); + : TypeScope(declaration with { Kind = TypeDeclarationKind.Enum }); /// /// Writes an enum declaration and invokes a callback for its body. /// - /// writer.WriteEnum(new TypeDeclarationOptions("Status"), _ => { }); - public CodeWriter WriteEnum(TypeDeclarationOptions declaration, Action bodyWriter) + /// writer.Enum(new TypeDeclarationOptions("Status"), _ => { }); + public CodeWriter Enum(TypeDeclarationOptions declaration, Action bodyWriter) { if (bodyWriter is null) throw new ArgumentNullException(nameof(bodyWriter)); - using (WriteEnumScope(declaration)) + using (EnumScope(declaration)) bodyWriter(this); return this; @@ -1449,8 +1673,8 @@ public CodeWriter WriteEnum(TypeDeclarationOptions declaration, ActionThe enum declaration options. /// The fields to write in declaration order. /// The current writer. - /// writer.WriteEnum(new TypeDeclarationOptions("Status"), new EnumFieldDeclarationOptions("Ready", 1)); - public CodeWriter WriteEnum(TypeDeclarationOptions declaration, params EnumFieldDeclarationOptions[] fields) + /// writer.Enum(new TypeDeclarationOptions("Status"), new EnumFieldDeclarationOptions("Ready", 1)); + public CodeWriter Enum(TypeDeclarationOptions declaration, params EnumFieldDeclarationOptions[] fields) { if (fields is null) throw new ArgumentNullException(nameof(fields)); @@ -1459,12 +1683,12 @@ public CodeWriter WriteEnum(TypeDeclarationOptions declaration, params EnumField for (var index = 0; index < fields.Length; index++) ValidateEnumFieldDeclaration(fields[index]); - return WriteEnum( + return Enum( declaration, body => { for (var index = 0; index < fields.Length; index++) - body.WriteEnumField(fields[index]); + body.EnumField(fields[index]); } ); } @@ -1474,13 +1698,13 @@ public CodeWriter WriteEnum(TypeDeclarationOptions declaration, params EnumField /// /// The enum field declaration options. /// The current writer. - /// writer.WriteEnumField(new EnumFieldDeclarationOptions("Ready", 1)); - public CodeWriter WriteEnumField(EnumFieldDeclarationOptions declaration) + /// writer.EnumField(new EnumFieldDeclarationOptions("Ready", 1)); + public CodeWriter EnumField(EnumFieldDeclarationOptions declaration) { ValidateEnumFieldDeclaration(declaration); if (!declaration.XmlSummary.IsDefaultOrEmpty) - WriteXmlSummary(declaration.XmlSummary); - WriteAttributes(declaration.Attributes); + XmlSummary(declaration.XmlSummary); + Attributes(declaration.Attributes); Write(declaration.FieldName); if (declaration.FieldValue is not null) { @@ -1490,32 +1714,32 @@ declaration.FieldValue as string ?? Convert.ToString(declaration.FieldValue, CultureInfo.InvariantCulture) ); } - return WriteLine(","); + return Line(","); } - void WriteXmlSummary(ImmutableArray summary) + void XmlSummary(ImmutableArray summary) { if (summary.Length == 1) { - Write("/// ").Write(summary[0]).WriteLine(""); + Write("/// ").Write(summary[0]).Line(""); return; } - WriteLine("/// "); + Line("/// "); for (var index = 0; index < summary.Length; index++) - Write("/// ").WriteLine(summary[index]); - WriteLine("/// "); + Write("/// ").Line(summary[index]); + Line("/// "); } /// /// Writes a complete delegate declaration. /// - /// writer.WriteDelegate(new TypeDeclarationOptions("Handler") { DelegateReturnType = "void" }); - public CodeWriter WriteDelegate(TypeDeclarationOptions declaration) + /// writer.Delegate(new TypeDeclarationOptions("Handler") { DelegateReturnType = "void" }); + public CodeWriter Delegate(TypeDeclarationOptions declaration) { if (declaration is null) throw new ArgumentNullException(nameof(declaration)); - WriteTypeScope(declaration with { Kind = TypeDeclarationKind.Delegate }); + TypeScope(declaration with { Kind = TypeDeclarationKind.Delegate }); return this; } @@ -1524,8 +1748,8 @@ public CodeWriter WriteDelegate(TypeDeclarationOptions declaration) /// /// The structured type declaration options. /// The generated type body scope. - /// using (writer.WriteTypeScope(new TypeDeclarationOptions("C"))) { } - public BlockScope WriteTypeScope(TypeDeclarationOptions declaration) + /// using (writer.TypeScope(new TypeDeclarationOptions("C"))) { } + public BlockScope TypeScope(TypeDeclarationOptions declaration) { if (declaration is null) throw new ArgumentNullException(nameof(declaration)); @@ -1535,7 +1759,7 @@ public BlockScope WriteTypeScope(TypeDeclarationOptions declaration) if (declaration.IncludeGeneratedAttributes ?? DefaultIncludeGeneratedAttributes) { - WriteGeneratedAttributes( + GeneratedAttributes( includeCoverageExclusion: declaration.Kind is TypeDeclarationKind.Class or TypeDeclarationKind.Struct @@ -1545,10 +1769,10 @@ or TypeDeclarationKind.RecordClass ); } - WriteAttributes(declaration.Attributes); + Attributes(declaration.Attributes); - if (declaration.Accessibility is { } accessibility) - WriteAccessibility(accessibility).Write(' '); + if (ResolveAccessibility(declaration.Accessibility, DefaultTypeAccessibility) is { } accessibility) + Accessibility(accessibility).Write(' '); var isStruct = declaration.Kind is TypeDeclarationKind.Struct or TypeDeclarationKind.RecordStruct; var isClass = declaration.Kind is TypeDeclarationKind.Class or TypeDeclarationKind.RecordClass; @@ -1572,7 +1796,7 @@ or TypeDeclarationKind.RecordClass Write("partial "); if (declaration.Kind == TypeDeclarationKind.Delegate) - Write("delegate ").WriteTypeReference(declaration.DelegateReturnType!).Write(' '); + Write("delegate ").TypeReference(declaration.DelegateReturnType!).Write(' '); Write( declaration.Kind switch @@ -1589,30 +1813,27 @@ or TypeDeclarationKind.RecordClass ) .Write(declaration.Name); - WriteGenericTypeParameters(declaration.GenericTypes); + GenericTypeParameters(declaration.GenericTypes); if (declaration.Kind == TypeDeclarationKind.Delegate) - WriteParametersWithHeuristic(declaration.DelegateParameters); + ParametersWithHeuristic(declaration.DelegateParameters); else - WriteParameterList( - declaration.PrimaryConstructorParameters, - declaration.ConstructorParametersOnSeparateLines - ); - WriteBaseTypes(declaration); + ParameterList(declaration.PrimaryConstructorParameters, declaration.ConstructorParametersOnSeparateLines); + BaseTypes(declaration); if (declaration.Kind == TypeDeclarationKind.Enum && declaration.EnumUnderlyingType is { IsEmpty: false }) - Write(" : ").WriteTypeReference(declaration.EnumUnderlyingType!); + Write(" : ").TypeReference(declaration.EnumUnderlyingType!); if (declaration.Kind == TypeDeclarationKind.Delegate) { if (HasGenericConstraints(declaration.GenericTypes)) NewLine(); - WriteMethodGenericConstraints(declaration.GenericTypes); - WriteLine(";"); + MethodGenericConstraints(declaration.GenericTypes); + Line(";"); CompleteWrittenItem(WrittenItemKind.Type, _indentLevel); return default; } NewLine(); - WriteGenericConstraints(declaration.GenericTypes); + GenericConstraints(declaration.GenericTypes); return OpenBlockScope(WrittenItemKind.Type); } @@ -1623,13 +1844,13 @@ or TypeDeclarationKind.RecordClass /// The structured type declaration options. /// The action that writes the type body. /// The current writer. - /// writer.WriteType(new TypeDeclarationOptions("C"), _ => { }); - public CodeWriter WriteType(TypeDeclarationOptions declaration, Action bodyWriter) + /// writer.Type(new TypeDeclarationOptions("C"), _ => { }); + public CodeWriter Type(TypeDeclarationOptions declaration, Action bodyWriter) { if (bodyWriter is null) throw new ArgumentNullException(nameof(bodyWriter)); - using (WriteTypeScope(declaration)) + using (TypeScope(declaration)) bodyWriter(this); return this; @@ -1640,31 +1861,31 @@ public CodeWriter WriteType(TypeDeclarationOptions declaration, Action /// The constructor declaration options. /// The constructor body scope. - /// using (writer.WriteConstructorScope(new ConstructorDeclarationOptions("C"))) writer.WriteLine("// body"); - public BlockScope WriteConstructorScope(ConstructorDeclarationOptions declaration) + /// using (writer.ConstructorScope(new ConstructorDeclarationOptions("C"))) writer.Line("// body"); + public BlockScope ConstructorScope(ConstructorDeclarationOptions declaration) { ValidateConstructorDeclaration(declaration); BeginWrittenItem(WrittenItemKind.Constructor); if (declaration.IncludeGeneratedAttributes ?? DefaultIncludeGeneratedAttributes) - WriteGeneratedAttributes(includeCoverageExclusion: true, includeEmbeddedAttribute: false); - WriteAttributes(declaration.Attributes); + GeneratedAttributes(includeCoverageExclusion: true, includeEmbeddedAttribute: false); + Attributes(declaration.Attributes); if (declaration.IsStatic) Write("static "); - else if (declaration.Accessibility is { } accessibility) - WriteAccessibility(accessibility).Write(' '); + else if (ResolveAccessibility(declaration.Accessibility, DefaultConstructorAccessibility) is { } accessibility) + Accessibility(accessibility).Write(' '); Write(declaration.Reference.Identity.Name); if (declaration.WriteParametersOnSeparateLines) - WriteParameterList(declaration.Parameters, writeOnSeparateLines: true, writeWhenEmpty: true); + ParameterList(declaration.Parameters, writeOnSeparateLines: true, writeWhenEmpty: true); else - WriteParametersWithHeuristic(declaration.Parameters); + ParametersWithHeuristic(declaration.Parameters); if (!string.IsNullOrWhiteSpace(declaration.Initializer)) { EnsureNewLine(); Indent(); - Write(": ").WriteLine(declaration.Initializer); + Write(": ").Line(declaration.Initializer); Unindent(); } @@ -1675,12 +1896,12 @@ public BlockScope WriteConstructorScope(ConstructorDeclarationOptions declaratio /// /// Writes a structured constructor and invokes a callback for its body. /// - /// writer.WriteConstructor(new ConstructorDeclarationOptions("C"), _ => { }); - public CodeWriter WriteConstructor(ConstructorDeclarationOptions declaration, Action writeBody) + /// writer.Constructor(new ConstructorDeclarationOptions("C"), _ => { }); + public CodeWriter Constructor(ConstructorDeclarationOptions declaration, Action writeBody) { if (writeBody is null) throw new ArgumentNullException(nameof(writeBody)); - using (WriteConstructorScope(declaration)) + using (ConstructorScope(declaration)) writeBody(this); return this; } @@ -1698,10 +1919,10 @@ public CodeWriter WriteConstructor(ConstructorDeclarationOptions declaration, Ac /// The pragmas to include in the header. /// The current writer. /// - /// writer.WriteAutoGeneratedHeader(pragmas: ["CS0618"]); - /// writer.WriteAutoGeneratedHeader(nullableDirective: NullableDirectiveMode.Disable); + /// writer.AutoGeneratedHeader(pragmas: ["CS0618"]); + /// writer.AutoGeneratedHeader(nullableDirective: NullableDirectiveMode.Disable); /// - public CodeWriter WriteAutoGeneratedHeader( + public CodeWriter AutoGeneratedHeader( string? generatorName = null, string? version = null, NullableDirectiveMode? nullableDirective = null, @@ -1713,27 +1934,27 @@ params string[] pragmas var mode = nullableDirective ?? NullableDirectiveMode; - WriteLine("// "); + Line("// "); if (!string.IsNullOrEmpty(generatorName)) { Write("// This code was generated by ").Write(generatorName); if (!string.IsNullOrEmpty(version)) Write(" (version ").Write(version).Write(')'); - WriteLine("."); + Line("."); } - WriteLine("// Changes to this file will be lost when the source generator runs again."); + Line("// Changes to this file will be lost when the source generator runs again."); if (ShouldWriteNullableDirective(mode, IsNullableContextEnabled)) - NewLine().WriteLine("#nullable enable"); + NewLine().Line("#nullable enable"); if (pragmas is not null && pragmas.Length > 0) { NewLine(); foreach (var pragma in pragmas) { - Write("#pragma warning disable ").WriteLine(pragma); + Write("#pragma warning disable ").Line(pragma); } } @@ -1762,8 +1983,8 @@ static bool ShouldWriteNullableDirective(NullableDirectiveMode mode, bool? isNul /// The generator name. /// The generator version, defaulting to 1.0.0.0. /// The current writer. - /// writer.WriteGeneratedCodeAttribute("MyGenerator", "1.0.0"); - public CodeWriter WriteGeneratedCodeAttribute(string generatorName, string? version = null) + /// writer.GeneratedCodeAttribute("MyGenerator", "1.0.0"); + public CodeWriter GeneratedCodeAttribute(string generatorName, string? version = null) { if (string.IsNullOrWhiteSpace(generatorName)) { @@ -1775,7 +1996,7 @@ public CodeWriter WriteGeneratedCodeAttribute(string generatorName, string? vers .Write(generatorName) .Write("\", \"") .Write(version ?? "1.0.0.0") - .WriteLine("\")]"); + .Line("\")]"); } /// @@ -1791,23 +2012,23 @@ public CodeWriter WriteGeneratedCodeAttribute(string generatorName, string? vers /// for ordinary generated members. /// /// Whether to emit and . - /// writer.WriteGeneratedAttributes(includeCoverageExclusion: true); - public CodeWriter WriteGeneratedAttributes( + /// writer.GeneratedAttributes(includeCoverageExclusion: true); + public CodeWriter GeneratedAttributes( bool includeCoverageExclusion = false, bool includeEmbeddedAttribute = false, bool includeGeneratedCodeAttribute = true ) { if (includeEmbeddedAttribute) - WriteLine("[global::Microsoft.CodeAnalysis.Embedded]"); + Line("[global::Microsoft.CodeAnalysis.Embedded]"); if (includeCoverageExclusion) - WriteLine("[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]"); + Line("[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]"); if (includeGeneratedCodeAttribute) { - WriteLine("[global::System.Runtime.CompilerServices.CompilerGenerated]"); - WriteGeneratedCodeAttribute(GeneratorName, GeneratorVersion); + Line("[global::System.Runtime.CompilerServices.CompilerGenerated]"); + GeneratedCodeAttribute(GeneratorName, GeneratorVersion); } return this; @@ -1818,7 +2039,7 @@ public CodeWriter WriteGeneratedAttributes( /// /// The warning codes to disable. /// A scope that writes the corresponding restore pragmas once. - /// using (writer.OpenPragmasScope("CS0618")) writer.WriteLine("ObsoleteCall();"); + /// using (writer.OpenPragmasScope("CS0618")) writer.Line("ObsoleteCall();"); public PragmaScope OpenPragmasScope(params string[] pragmas) { if (pragmas is null || pragmas.Length == 0) @@ -1826,11 +2047,36 @@ public PragmaScope OpenPragmasScope(params string[] pragmas) NewLine(); foreach (var pragma in pragmas) - Write("#pragma warning disable ").WriteLine(pragma); + Write("#pragma warning disable ").Line(pragma); return new PragmaScope(this, pragmas); } + /// + /// Writes a #pragma warning disable directive at column zero for one or more warning codes, + /// as a single directive. Use for a scoped disable that restores the + /// warnings when disposed. + /// + /// The warning codes to disable, such as CS8625. + /// The current writer. + /// writer.PragmaDisable("CS8625", "CS0618"); + public CodeWriter PragmaDisable(params string[] codes) + { + if (codes is null || codes.Length == 0) + throw new ArgumentException("At least one warning code is required.", nameof(codes)); + + for (var index = 0; index < codes.Length; index++) + ValidateStatementPart(codes[index], nameof(codes)); + + if (_indentLevel == 0) + EnsureBlankLine(); + DirectiveLine("#pragma warning disable " + string.Join(" ", codes)); + if (_indentLevel == 0) + EnsureBlankLine(); + + return this; + } + /// /// Writes each supplied part on its own line. /// @@ -1843,7 +2089,7 @@ public CodeWriter MultiLine(params string[] parts) throw new ArgumentNullException(nameof(parts)); for (var index = 0; index < parts.Length; index++) - WriteLine(parts[index]); + Line(parts[index]); return this; } @@ -1867,7 +2113,7 @@ public CodeWriter MultiLineParameters(params string[] parameters) for (var index = 0; index < parameters.Length; index++) { Write(parameters[index]); - WriteLine(index == parameters.Length - 1 ? ")" : ","); + Line(index == parameters.Length - 1 ? ")" : ","); } return Unindent(); @@ -1879,9 +2125,9 @@ public CodeWriter MultiLineParameters(params string[] parameters) /// The method name, optionally including a receiver. /// The argument expressions. /// The current writer. - /// writer.WriteMethodCall("Run", "value", "cancellationToken"); // Run(value, cancellationToken); - public CodeWriter WriteMethodCall(string methodName, params string[] arguments) => - WriteMethodCallCore(methodName, arguments, receiver: null, genericArguments: null, false, false); + /// writer.MethodCall("Run", "value", "cancellationToken"); // Run(value, cancellationToken); + public CodeWriter MethodCall(string methodName, params string[] arguments) => + MethodCallCore(methodName, arguments, receiver: null, genericArguments: null, false, false); /// /// Writes an awaited method invocation statement. @@ -1889,9 +2135,9 @@ public CodeWriter WriteMethodCall(string methodName, params string[] arguments) /// The method name, optionally including a receiver. /// The argument expressions. /// The current writer. - /// writer.WriteAwaitedMethodCall("LoadAsync", "cancellationToken"); // await LoadAsync(cancellationToken); - public CodeWriter WriteAwaitedMethodCall(string methodName, params string[] arguments) => - WriteMethodCallCore(methodName, arguments, receiver: null, genericArguments: null, false, true); + /// writer.AwaitedMethodCall("LoadAsync", "cancellationToken"); // await LoadAsync(cancellationToken); + public CodeWriter AwaitedMethodCall(string methodName, params string[] arguments) => + MethodCallCore(methodName, arguments, receiver: null, genericArguments: null, false, true); /// /// Writes a method invocation from structured argument declarations. @@ -1907,17 +2153,17 @@ public CodeWriter WriteAwaitedMethodCall(string methodName, params string[] argu /// Optional generic type arguments. /// Whether to force one argument per line. /// The current writer. - /// writer.WriteMethodCall("Copy", [ + /// writer.MethodCall("Copy", [ /// new("source"), /// new("destination") { Modifier = ParameterModifier.Out }]); - public CodeWriter WriteMethodCall( + public CodeWriter MethodCall( string methodName, IEnumerable arguments, string? receiver = null, IEnumerable? genericArguments = null, bool writeArgumentsOnSeparateLines = false ) => - WriteMethodCall( + MethodCall( methodName, (arguments ?? throw new ArgumentNullException(nameof(arguments))).Select(RenderCallArgument), receiver, @@ -1934,15 +2180,15 @@ public CodeWriter WriteMethodCall( /// Optional generic type arguments. /// Whether to force one argument per line. /// The current writer. - /// writer.WriteAwaitedMethodCall("LoadAsync", [new("token")], "service"); - public CodeWriter WriteAwaitedMethodCall( + /// writer.AwaitedMethodCall("LoadAsync", [new("token")], "service"); + public CodeWriter AwaitedMethodCall( string methodName, IEnumerable arguments, string? receiver = null, IEnumerable? genericArguments = null, bool writeArgumentsOnSeparateLines = false ) => - WriteMethodCallCore( + MethodCallCore( methodName, (arguments ?? throw new ArgumentNullException(nameof(arguments))).Select(RenderCallArgument), receiver, @@ -1960,16 +2206,16 @@ public CodeWriter WriteAwaitedMethodCall( /// Optional generic type arguments. /// Whether to force one argument per line. /// The current writer. - /// writer.WriteMethodCall("Create", ["value"], "factory", [TypeLibrary.System.String.AsTypeReference()]); - public CodeWriter WriteMethodCall( + /// writer.MethodCall("Create", ["value"], "factory", [TypeLibrary.System.String.AsTypeReference()]); + public CodeWriter MethodCall( string methodName, IEnumerable arguments, string? receiver = null, IEnumerable? genericArguments = null, bool writeArgumentsOnSeparateLines = false - ) => WriteMethodCallCore(methodName, arguments, receiver, genericArguments, writeArgumentsOnSeparateLines, false); + ) => MethodCallCore(methodName, arguments, receiver, genericArguments, writeArgumentsOnSeparateLines, false); - CodeWriter WriteMethodCallCore( + CodeWriter MethodCallCore( string methodName, IEnumerable arguments, string? receiver, @@ -2002,20 +2248,20 @@ bool isAwaited { if (index != 0) Write(", "); - WriteTypeReference(genericArgumentList[index]); + TypeReference(genericArgumentList[index]); } Write('>'); } Write('('); - if (WriteMethodCallArguments(argumentList, writeArgumentsOnSeparateLines)) + if (MethodCallArguments(argumentList, writeArgumentsOnSeparateLines)) return this; // If the arguments were written inline, we can write the closing parenthesis and semicolon on the same line. - return WriteLine(";"); + return Line(";"); } - bool WriteMethodCallArguments(string?[] arguments, bool writeOnSeparateLines, string multilineClosingSuffix = ";") + bool MethodCallArguments(string?[] arguments, bool writeOnSeparateLines, string multilineClosingSuffix = ";") { var inlineLength = CurrentLineLength + 2; for (var index = 0; index < arguments.Length; index++) @@ -2046,9 +2292,9 @@ bool isAwaited NewLine().Indent(); for (var index = 0; index < arguments.Length; index++) { - WriteExpression(arguments[index], expressionWriter: null); + Expression(arguments[index], expressionWriter: null); if (index != arguments.Length - 1) - WriteLine(","); + Line(","); else NewLine(); } @@ -2069,97 +2315,97 @@ bool isAwaited /// The target, such as value or var result. /// The assigned expression. /// Whether to force the value to be not null, by appending the null-forgiving operator (!). - /// writer.WriteAssignment("value", "CreateValue()"); // value = CreateValue(); - public CodeWriter WriteAssignment(string target, string value, bool forceNotNull = false) + /// writer.Assignment("value", "CreateValue()"); // value = CreateValue(); + public CodeWriter Assignment(string target, string value, bool forceNotNull = false) { ValidateStatementPart(target, nameof(target)); ValidateStatementPart(value, nameof(value)); Write(target).Write(" = "); - WriteExpression(value, expressionWriter: null); + Expression(value, expressionWriter: null); if (forceNotNull) Write("!"); - return WriteLine(";"); + return Line(";"); } /// /// Writes an assignment statement using a callback for a multiline expression. /// - /// writer.WriteAssignment("value", expression => expression.Write("new Value()")); - public CodeWriter WriteAssignment(string target, Action writeValue) + /// writer.Assignment("value", expression => expression.Write("new Value()")); + public CodeWriter Assignment(string target, Action writeValue) { ValidateStatementPart(target, nameof(target)); if (writeValue is null) throw new ArgumentNullException(nameof(writeValue)); Write(target).Write(" = "); - WriteExpression(null, writeValue); - return WriteLine(";"); + Expression(null, writeValue); + return Line(";"); } /// /// Writes an assignment whose value is a structured object-creation expression. /// - /// writer.WriteAssignment("@event", new ObjectCreationOptions(eventType, "propVal1", "propVal2")); - public CodeWriter WriteAssignment(string target, ObjectCreationOptions value, bool forceNotNull = false) + /// writer.Assignment("@event", new ObjectCreationOptions(eventType, "propVal1", "propVal2")); + public CodeWriter Assignment(string target, ObjectCreationOptions value, bool forceNotNull = false) { ValidateStatementPart(target, nameof(target)); Write(target).Write(" = "); - if (WriteObjectCreationExpression(value, forceNotNull)) + if (ObjectCreationExpression(value, forceNotNull)) return this; // If the object creation was written inline, we can write the closing semicolon on the same line. - return WriteLine(";"); + return Line(";"); } /// /// Writes a typed local or declaration assignment. /// - /// writer.WriteAssignment("var", "value", "CreateValue()"); - public CodeWriter WriteAssignment(string type, string name, string value, bool forceNotNull = false) + /// writer.Assignment("var", "value", "CreateValue()"); + public CodeWriter Assignment(string type, string name, string value, bool forceNotNull = false) { ValidateStatementPart(type, nameof(type)); ValidateStatementPart(name, nameof(name)); - return WriteAssignment($"{type} {name}", value, forceNotNull); + return Assignment($"{type} {name}", value, forceNotNull); } /// /// Writes a typed local or declaration assignment with a multiline expression. /// - /// writer.WriteAssignment("Value", "value", expression => expression.Write("CreateValue()")); - public CodeWriter WriteAssignment(string type, string name, Action writeValue) + /// writer.Assignment("Value", "value", expression => expression.Write("CreateValue()")); + public CodeWriter Assignment(string type, string name, Action writeValue) { ValidateStatementPart(type, nameof(type)); ValidateStatementPart(name, nameof(name)); - return WriteAssignment($"{type} {name}", writeValue); + return Assignment($"{type} {name}", writeValue); } /// /// Writes a typed local assignment whose value is a structured object creation. /// - /// writer.WriteAssignment("var", "@event", new ObjectCreationOptions(eventType, "propVal1", "propVal2")); - public CodeWriter WriteAssignment(string type, string name, ObjectCreationOptions value, bool forceNotNull = false) + /// writer.Assignment("var", "@event", new ObjectCreationOptions(eventType, "propVal1", "propVal2")); + public CodeWriter Assignment(string type, string name, ObjectCreationOptions value, bool forceNotNull = false) { ValidateStatementPart(type, nameof(type)); ValidateStatementPart(name, nameof(name)); - return WriteAssignment($"{type} {name}", value, forceNotNull); + return Assignment($"{type} {name}", value, forceNotNull); } - bool WriteObjectCreationExpression(ObjectCreationOptions value, bool forceNotNull) + bool ObjectCreationExpression(ObjectCreationOptions value, bool forceNotNull) { ValidateInitializerMembers(value); - Write("new ").WriteTypeReference(value.Reference); + Write("new ").TypeReference(value.Reference); var hasInitializer = !value.InitializerMembers.IsDefaultOrEmpty; string[] arguments = value.Arguments.IsDefault ? [] : [.. value.Arguments.Select(RenderCallArgument)]; if (arguments.Length > 0 || !hasInitializer) { - // WriteMethodCallArguments writes the closing parenthesis itself: inline or for empty + // MethodCallArguments writes the closing parenthesis itself: inline or for empty // arguments it emits ')' and returns false, while a multiline layout emits the closing // token and returns true. Write('('); if ( - WriteMethodCallArguments( + MethodCallArguments( arguments, value.WriteArgumentsOnSeparateLines, hasInitializer ? string.Empty @@ -2172,7 +2418,7 @@ bool WriteObjectCreationExpression(ObjectCreationOptions value, bool forceNotNul // follows, the initializer supplies the terminating semicolon via the caller. if (hasInitializer) { - WriteObjectInitializer(value, forceNotNull); + ObjectInitializer(value, forceNotNull); return false; } @@ -2182,7 +2428,7 @@ bool WriteObjectCreationExpression(ObjectCreationOptions value, bool forceNotNul if (hasInitializer) { - WriteObjectInitializer(value, forceNotNull); + ObjectInitializer(value, forceNotNull); return false; } @@ -2192,17 +2438,17 @@ bool WriteObjectCreationExpression(ObjectCreationOptions value, bool forceNotNul return false; } - bool WriteObjectInitializer(ObjectCreationOptions value, bool forceNotNull) + bool ObjectInitializer(ObjectCreationOptions value, bool forceNotNull) { if (value.WriteInitializerMembersOnSeparateLines) { EnsureNewLine(); - WriteLine("{"); + Line("{"); Indent(); for (var index = 0; index < value.InitializerMembers.Length; index++) { var member = value.InitializerMembers[index]; - Write(member.Name).Write(" = ").Write(member.Value).WriteLine(","); + Write(member.Name).Write(" = ").Write(member.Value).Line(","); } Unindent(); @@ -2245,81 +2491,110 @@ static void ValidateInitializerMembers(ObjectCreationOptions value) /// /// Writes a return statement. /// - /// writer.WriteReturn("value"); // return value; - public CodeWriter WriteReturn(string? expression = null) + /// writer.Return("value"); // return value; + public CodeWriter Return(string? expression = null) { if (string.IsNullOrWhiteSpace(expression)) - return WriteLine("return;"); + return Line("return;"); Write("return "); - WriteExpression(expression, expressionWriter: null); - return WriteLine(";"); + Expression(expression, expressionWriter: null); + return Line(";"); } /// /// Writes a return statement using a callback for a multiline expression. /// - /// writer.WriteReturn(expression => expression.Write("value")); - public CodeWriter WriteReturn(Action writeExpression) + /// writer.Return(expression => expression.Write("value")); + public CodeWriter Return(Action writeExpression) { if (writeExpression is null) throw new ArgumentNullException(nameof(writeExpression)); Write("return "); - WriteExpression(null, writeExpression); - return WriteLine(";"); + Expression(null, writeExpression); + return Line(";"); + } + + /// + /// Writes a return statement whose value is a structured object-creation expression. + /// + /// The object-creation expression to return. + /// Whether to force the value to be not null, by appending the null-forgiving operator (!). + /// writer.Return(new ObjectCreationOptions(Type("Order"), "customerId")); + public CodeWriter Return(ObjectCreationOptions value, bool forceNotNull = false) + { + Write("return "); + + if (ObjectCreationExpression(value, forceNotNull)) + return this; + + return Line(";"); } /// /// Writes a throw statement. /// - /// writer.WriteThrow("new InvalidOperationException()"); - public CodeWriter WriteThrow(string expression) + /// writer.Throw("new InvalidOperationException()"); + public CodeWriter Throw(string expression) { ValidateStatementPart(expression, nameof(expression)); Write("throw "); - WriteExpression(expression, expressionWriter: null); - return WriteLine(";"); + Expression(expression, expressionWriter: null); + return Line(";"); } /// - /// Writes a throw statement using a structured exception type and an optional message. + /// Writes a throw statement using a structured exception type, an optional message, and optional raw + /// constructor arguments. /// /// The exception type to throw. /// - /// The exception message written as a string literal, or to throw the - /// exception without a message. Backslashes and double quotes are escaped so raw literal text - /// can be supplied. + /// The exception message written as a string literal, or to throw the exception + /// without a message. Backslashes, double quotes, carriage returns, line feeds, and tabs are escaped so + /// raw literal text can be supplied. + /// + /// + /// Raw constructor argument expressions written verbatim after the message; never escaped. When + /// is , they are written as the sole constructor arguments. /// - /// writer.WriteThrow(TypeLibrary.System.InvalidOperationException, "Cannot be null."); - public CodeWriter WriteThrow(TypeReference exceptionType, string? message = null) + /// writer.Throw(TypeLibrary.System.ArgumentNullException, null, "nameof(value)"); + public CodeWriter Throw(TypeReference exceptionType, string? message = null, params string[] constructorArguments) { if (exceptionType.IsNullOrEmpty()) throw new ArgumentException("Exception type cannot be null or empty.", nameof(exceptionType)); Write("throw new "); - if (message is null) - WriteExpression($"{exceptionType}()", expressionWriter: null); + var arguments = constructorArguments ?? []; + if (message is null && arguments.Length == 0) + { + Expression($"{exceptionType}()", expressionWriter: null); + } else { - WriteExpression( - $"{exceptionType}(\"{message.Replace("\\", "\\\\").Replace("\"", "\\\"")}\")", - expressionWriter: null - ); + var rendered = new string[arguments.Length + (message is null ? 0 : 1)]; + var index = 0; + if (message is not null) + rendered[index++] = $"\"{EscapeStringLiteral(message)}\""; + + foreach (var argument in arguments) + rendered[index++] = argument; + + Expression($"{exceptionType}({string.Join(", ", rendered)})", expressionWriter: null); } - return WriteLine(";"); + return Line(";"); } /// /// Writes a throw statement using a callback for a multiline expression. /// - /// writer.WriteThrow(expression => expression.Write("new InvalidOperationException()")); - public CodeWriter WriteThrow(Action writeExpression) + /// writer.Throw(expression => expression.Write("new InvalidOperationException()")); + public CodeWriter Throw(Action writeExpression) { if (writeExpression is null) throw new ArgumentNullException(nameof(writeExpression)); Write("throw "); - WriteExpression(null, writeExpression); - return WriteLine(";"); + Expression(null, writeExpression); + return Line(";"); } /// @@ -2330,13 +2605,13 @@ public CodeWriter WriteThrow(Action writeExpression) /// The current writer. /// Thrown if the condition is null or whitespace. /// Thrown if the bodyWriter is null. - /// writer.WriteIfBlock("enabled", body => body.WriteReturn()); - public CodeWriter WriteIfBlock(string condition, Action bodyWriter) + /// writer.IfBlock("enabled", body => body.Return()); + public CodeWriter IfBlock(string condition, Action bodyWriter) { if (bodyWriter is null) throw new ArgumentNullException(nameof(bodyWriter)); - using (WriteIfBlockScope(condition)) + using (IfBlockScope(condition)) bodyWriter(this); return this; @@ -2345,13 +2620,13 @@ public CodeWriter WriteIfBlock(string condition, Action bodyWriter) /// /// Writes an if statement and returns its body scope. /// - /// using (writer.WriteIfBlockScope("enabled")) writer.WriteReturn(); - public BlockScope WriteIfBlockScope(string condition) + /// using (writer.IfBlockScope("enabled")) writer.Return(); + public BlockScope IfBlockScope(string condition) { ValidateStatementPart(condition, nameof(condition)); Write("if ("); - WriteExpression(condition, expressionWriter: null); - WriteLine(")"); + Expression(condition, expressionWriter: null); + Line(")"); return OpenBlockScope(); } @@ -2362,15 +2637,15 @@ public BlockScope WriteIfBlockScope(string condition) /// The action that writes the if body. /// The action that writes the else body, or to omit the else. /// The current writer. - /// writer.WriteIfElse("enabled", body => body.WriteReturn("value"), null); - public CodeWriter WriteIfElse(string condition, Action ifBody, Action? elseBody) + /// writer.IfElse("enabled", body => body.Return("value"), null); + public CodeWriter IfElse(string condition, Action ifBody, Action? elseBody) { if (ifBody is null) throw new ArgumentNullException(nameof(ifBody)); - using (WriteIfBlockScope(condition)) + using (IfBlockScope(condition)) ifBody(this); if (elseBody is not null) - WriteElse(elseBody); + Else(elseBody); return this; } @@ -2379,12 +2654,12 @@ public CodeWriter WriteIfElse(string condition, Action ifBody, Actio /// /// The action that writes the else body. /// The current writer. - /// writer.WriteIfBlock("enabled", body => body.WriteReturn("value")).WriteElse(body => body.WriteReturn("null")); - public CodeWriter WriteElse(Action body) + /// writer.IfBlock("enabled", body => body.Return("value")).Else(body => body.Return("null")); + public CodeWriter Else(Action body) { if (body is null) throw new ArgumentNullException(nameof(body)); - using (WriteElseScope()) + using (ElseScope()) body(this); return this; } @@ -2393,11 +2668,11 @@ public CodeWriter WriteElse(Action body) /// Writes an else block and returns its body scope. /// /// The else body scope. - /// using (writer.WriteIfBlockScope("enabled")) writer.WriteReturn("value"); using (writer.WriteElseScope()) writer.WriteReturn("null"); - public BlockScope WriteElseScope() + /// using (writer.IfBlockScope("enabled")) writer.Return("value"); using (writer.ElseScope()) writer.Return("null"); + public BlockScope ElseScope() { EnsureNewLine(); - WriteLine("else"); + Line("else"); return OpenBlockScope(); } @@ -2407,12 +2682,12 @@ public BlockScope WriteElseScope() /// The iterator declaration, such as var item in items. /// The action that writes the loop body. /// The current writer. - /// writer.WriteForeach("var item in items", body => body.WriteMethodCall("Process", "item")); - public CodeWriter WriteForeach(string iterator, Action body) + /// writer.Foreach("var item in items", body => body.MethodCall("Process", "item")); + public CodeWriter Foreach(string iterator, Action body) { if (body is null) throw new ArgumentNullException(nameof(body)); - using (WriteForeachScope(iterator)) + using (ForeachScope(iterator)) body(this); return this; } @@ -2422,11 +2697,11 @@ public CodeWriter WriteForeach(string iterator, Action body) /// /// The iterator declaration, such as var item in items. /// The loop body scope. - /// using (writer.WriteForeachScope("var item in items")) writer.WriteMethodCall("Process", "item"); - public BlockScope WriteForeachScope(string iterator) + /// using (writer.ForeachScope("var item in items")) writer.MethodCall("Process", "item"); + public BlockScope ForeachScope(string iterator) { ValidateStatementPart(iterator, nameof(iterator)); - Write("foreach (").Write(iterator).WriteLine(")"); + Write("foreach (").Write(iterator).Line(")"); return OpenBlockScope(); } @@ -2438,12 +2713,12 @@ public BlockScope WriteForeachScope(string iterator) /// The iterator expression, or for none. /// The action that writes the loop body. /// The current writer. - /// writer.WriteFor("int i = 0", "i < count", "i++", body => body.WriteMethodCall("Process", "items[i]")); - public CodeWriter WriteFor(string? initializer, string? condition, string? iterator, Action body) + /// writer.For("int i = 0", "i < count", "i++", body => body.MethodCall("Process", "items[i]")); + public CodeWriter For(string? initializer, string? condition, string? iterator, Action body) { if (body is null) throw new ArgumentNullException(nameof(body)); - using (WriteForScope(initializer, condition, iterator)) + using (ForScope(initializer, condition, iterator)) body(this); return this; } @@ -2455,13 +2730,13 @@ public CodeWriter WriteFor(string? initializer, string? condition, string? itera /// The condition expression, or for none. /// The iterator expression, or for none. /// The loop body scope. - /// using (writer.WriteForScope("int i = 0", "i < count", "i++")) writer.WriteMethodCall("Process", "items[i]"); - public BlockScope WriteForScope(string? initializer, string? condition, string? iterator) + /// using (writer.ForScope("int i = 0", "i < count", "i++")) writer.MethodCall("Process", "items[i]"); + public BlockScope ForScope(string? initializer, string? condition, string? iterator) { Write("for ("); Write(initializer).Write("; "); Write(condition).Write("; "); - Write(iterator).WriteLine(")"); + Write(iterator).Line(")"); return OpenBlockScope(); } @@ -2471,12 +2746,12 @@ public BlockScope WriteForScope(string? initializer, string? condition, string? /// The loop condition. /// The action that writes the loop body. /// The current writer. - /// writer.WriteWhile("queue.Count > 0", body => body.WriteMethodCall("Process", "queue.Dequeue()")); - public CodeWriter WriteWhile(string condition, Action body) + /// writer.While("queue.Count > 0", body => body.MethodCall("Process", "queue.Dequeue()")); + public CodeWriter While(string condition, Action body) { if (body is null) throw new ArgumentNullException(nameof(body)); - using (WriteWhileScope(condition)) + using (WhileScope(condition)) body(this); return this; } @@ -2486,11 +2761,11 @@ public CodeWriter WriteWhile(string condition, Action body) /// /// The loop condition. /// The loop body scope. - /// using (writer.WriteWhileScope("queue.Count > 0")) writer.WriteMethodCall("Process", "queue.Dequeue()"); - public BlockScope WriteWhileScope(string condition) + /// using (writer.WhileScope("queue.Count > 0")) writer.MethodCall("Process", "queue.Dequeue()"); + public BlockScope WhileScope(string condition) { ValidateStatementPart(condition, nameof(condition)); - Write("while (").Write(condition).WriteLine(")"); + Write("while (").Write(condition).Line(")"); return OpenBlockScope(); } @@ -2500,12 +2775,12 @@ public BlockScope WriteWhileScope(string condition) /// The trailing loop condition. /// The action that writes the loop body. /// The current writer. - /// writer.WriteDoWhile("!finished", body => body.WriteMethodCall("Advance")); - public CodeWriter WriteDoWhile(string condition, Action body) + /// writer.DoWhile("!finished", body => body.MethodCall("Advance")); + public CodeWriter DoWhile(string condition, Action body) { if (body is null) throw new ArgumentNullException(nameof(body)); - using (WriteDoWhileScope(condition)) + using (DoWhileScope(condition)) body(this); return this; } @@ -2515,8 +2790,8 @@ public CodeWriter WriteDoWhile(string condition, Action body) /// /// The trailing loop condition. /// The loop body scope, which writes } while (condition); when disposed. - /// using (writer.WriteDoWhileScope("!finished")) writer.WriteMethodCall("Advance"); - public BlockScope WriteDoWhileScope(string condition) + /// using (writer.DoWhileScope("!finished")) writer.MethodCall("Advance"); + public BlockScope DoWhileScope(string condition) { ValidateStatementPart(condition, nameof(condition)); return OpenDelimitedBlockScope("do", "{", "} while (" + condition + ");"); @@ -2527,12 +2802,12 @@ public BlockScope WriteDoWhileScope(string condition) /// /// The action that writes the try body. /// The current writer. - /// writer.WriteTry(body => body.WriteMethodCall("Run")); - public CodeWriter WriteTry(Action body) + /// writer.Try(body => body.MethodCall("Run")); + public CodeWriter Try(Action body) { if (body is null) throw new ArgumentNullException(nameof(body)); - using (WriteTryScope()) + using (TryScope()) body(this); return this; } @@ -2541,20 +2816,20 @@ public CodeWriter WriteTry(Action body) /// Writes a try block and returns its body scope. /// /// The try body scope. - /// using (writer.WriteTryScope()) writer.WriteMethodCall("Run"); - public BlockScope WriteTryScope() => OpenDelimitedBlockScope("try", "{", "}"); + /// using (writer.TryScope()) writer.MethodCall("Run"); + public BlockScope TryScope() => OpenDelimitedBlockScope("try", "{", "}"); /// /// Writes a catch block and invokes a callback for its body. /// /// The action that writes the catch body. /// The current writer. - /// writer.WriteCatch(body => body.WriteThrow(TypeLibrary.System.InvalidOperationException, "Failed")); - public CodeWriter WriteCatch(Action body) + /// writer.Catch(body => body.Throw(TypeLibrary.System.InvalidOperationException, "Failed")); + public CodeWriter Catch(Action body) { if (body is null) throw new ArgumentNullException(nameof(body)); - using (WriteCatchScope()) + using (CatchScope()) body(this); return this; } @@ -2566,12 +2841,12 @@ public CodeWriter WriteCatch(Action body) /// The exception variable name, or to omit it. /// The action that writes the catch body. /// The current writer. - /// writer.WriteCatch(TypeLibrary.System.Exception, "ex", body => body.WriteMethodCall("Log", "ex")); - public CodeWriter WriteCatch(TypeReference? exceptionType, string? name, Action body) + /// writer.Catch(TypeLibrary.System.Exception, "ex", body => body.MethodCall("Log", "ex")); + public CodeWriter Catch(TypeReference? exceptionType, string? name, Action body) { if (body is null) throw new ArgumentNullException(nameof(body)); - using (WriteCatchScope(exceptionType, name)) + using (CatchScope(exceptionType, name)) body(this); return this; } @@ -2582,18 +2857,18 @@ public CodeWriter WriteCatch(TypeReference? exceptionType, string? name, Action< /// The caught exception type, or for a bare catch. /// The exception variable name, or to omit it. /// The catch body scope. - /// using (writer.WriteCatchScope(TypeLibrary.System.Exception, "ex")) writer.WriteMethodCall("Log", "ex"); - public BlockScope WriteCatchScope(TypeReference? exceptionType = null, string? name = null) + /// using (writer.CatchScope(TypeLibrary.System.Exception, "ex")) writer.MethodCall("Log", "ex"); + public BlockScope CatchScope(TypeReference? exceptionType = null, string? name = null) { Write("catch"); if (exceptionType is not null) { - Write(" (").WriteTypeReference(exceptionType); + Write(" (").TypeReference(exceptionType); if (!string.IsNullOrWhiteSpace(name)) Write(' ').Write(name); Write(')'); } - WriteLine(); + Line(); return OpenBlockScope(); } @@ -2602,12 +2877,12 @@ public BlockScope WriteCatchScope(TypeReference? exceptionType = null, string? n /// /// The action that writes the finally body. /// The current writer. - /// writer.WriteFinally(body => body.WriteMethodCall("Dispose")); - public CodeWriter WriteFinally(Action body) + /// writer.Finally(body => body.MethodCall("Dispose")); + public CodeWriter Finally(Action body) { if (body is null) throw new ArgumentNullException(nameof(body)); - using (WriteFinallyScope()) + using (FinallyScope()) body(this); return this; } @@ -2616,10 +2891,10 @@ public CodeWriter WriteFinally(Action body) /// Writes a finally block and returns its body scope. /// /// The finally body scope. - /// using (writer.WriteFinallyScope()) writer.WriteMethodCall("Dispose"); - public BlockScope WriteFinallyScope() + /// using (writer.FinallyScope()) writer.MethodCall("Dispose"); + public BlockScope FinallyScope() { - WriteLine("finally"); + Line("finally"); return OpenBlockScope(); } @@ -2629,12 +2904,12 @@ public BlockScope WriteFinallyScope() /// The resource declaration, such as var stream = Open(). /// The action that writes the using body. /// The current writer. - /// writer.WriteUsingStatement("var stream = Open()", body => body.WriteMethodCall("Read", "stream")); - public CodeWriter WriteUsingStatement(string declaration, Action body) + /// writer.UsingStatement("var stream = Open()", body => body.MethodCall("Read", "stream")); + public CodeWriter UsingStatement(string declaration, Action body) { if (body is null) throw new ArgumentNullException(nameof(body)); - using (WriteUsingStatementScope(declaration)) + using (UsingStatementScope(declaration)) body(this); return this; } @@ -2644,11 +2919,11 @@ public CodeWriter WriteUsingStatement(string declaration, Action bod /// /// The resource declaration, such as var stream = Open(). /// The using body scope. - /// using (writer.WriteUsingStatementScope("var stream = Open()")) writer.WriteMethodCall("Read", "stream"); - public BlockScope WriteUsingStatementScope(string declaration) + /// using (writer.UsingStatementScope("var stream = Open()")) writer.MethodCall("Read", "stream"); + public BlockScope UsingStatementScope(string declaration) { ValidateStatementPart(declaration, nameof(declaration)); - Write("using (").Write(declaration).WriteLine(")"); + Write("using (").Write(declaration).Line(")"); return OpenBlockScope(); } @@ -2658,12 +2933,12 @@ public BlockScope WriteUsingStatementScope(string declaration) /// The lock expression. /// The action that writes the lock body. /// The current writer. - /// writer.WriteLockStatement("_gate", body => body.WriteMethodCall("Run")); - public CodeWriter WriteLockStatement(string expression, Action body) + /// writer.LockStatement("_gate", body => body.MethodCall("Run")); + public CodeWriter LockStatement(string expression, Action body) { if (body is null) throw new ArgumentNullException(nameof(body)); - using (WriteLockStatementScope(expression)) + using (LockStatementScope(expression)) body(this); return this; } @@ -2673,11 +2948,11 @@ public CodeWriter WriteLockStatement(string expression, Action body) /// /// The lock expression. /// The lock body scope. - /// using (writer.WriteLockStatementScope("_gate")) writer.WriteMethodCall("Run"); - public BlockScope WriteLockStatementScope(string expression) + /// using (writer.LockStatementScope("_gate")) writer.MethodCall("Run"); + public BlockScope LockStatementScope(string expression) { ValidateStatementPart(expression, nameof(expression)); - Write("lock (").Write(expression).WriteLine(")"); + Write("lock (").Write(expression).Line(")"); return OpenBlockScope(); } @@ -2709,8 +2984,8 @@ public CodeWriter MultiLineItems(params string[] items) /// /// The lines to write. /// The current writer. - /// writer.WriteLines(["first", null, "second"]); - public CodeWriter WriteLines(IEnumerable lines) + /// writer.Lines(["first", null, "second"]); + public CodeWriter Lines(IEnumerable lines) { if (lines is null) throw new ArgumentNullException(nameof(lines)); @@ -2718,7 +2993,7 @@ public CodeWriter WriteLines(IEnumerable lines) foreach (var line in lines) { if (line is not null) - WriteLine(line); + Line(line); } return this; @@ -2730,8 +3005,8 @@ public CodeWriter WriteLines(IEnumerable lines) /// The values to write. /// The delimiter written between values. /// The current writer. - /// writer.WriteDelimited(["a", "b"], " | "); // a | b - public CodeWriter WriteDelimited(IEnumerable items, string delimiter = ", ") + /// writer.Delimited(["a", "b"], " | "); // a | b + public CodeWriter Delimited(IEnumerable items, string delimiter = ", ") { if (items is null) throw new ArgumentNullException(nameof(items)); @@ -2758,8 +3033,8 @@ public CodeWriter WriteDelimited(IEnumerable items, string delimiter = /// The element expressions; spread elements such as ..source are passed verbatim. /// Whether to write one element per line. /// The current writer. - /// writer.WriteCollectionExpression(["first", "second", "..rest"]); // [first, second, ..rest] - public CodeWriter WriteCollectionExpression(IEnumerable items, bool writeOnSeparateLines = false) + /// writer.CollectionExpression(["first", "second", "..rest"]); // [first, second, ..rest] + public CodeWriter CollectionExpression(IEnumerable items, bool writeOnSeparateLines = false) { if (items is null) throw new ArgumentNullException(nameof(items)); @@ -2786,7 +3061,7 @@ public CodeWriter WriteCollectionExpression(IEnumerable items, bool wri { Write(elements[index]); if (index != elements.Length - 1) - WriteLine(","); + Line(","); else NewLine(); } @@ -2798,7 +3073,7 @@ public CodeWriter WriteCollectionExpression(IEnumerable items, bool wri /// Increases indentation until the returned scope is disposed. /// /// A scope that restores the indentation level. - /// using (writer.IndentedScope()) writer.WriteLine("value"); + /// using (writer.IndentedScope()) writer.Line("value"); public IndentScope IndentedScope() { Indent(); @@ -2820,7 +3095,7 @@ public IndentScope IndentedScope() /// /// The action to invoke while indented. /// The current writer. - /// writer.Indented(body => body.WriteLine("value")); + /// writer.Indented(body => body.Line("value")); public CodeWriter Indented(Action bodyWriter) { if (bodyWriter is null) @@ -2835,10 +3110,10 @@ public CodeWriter Indented(Action bodyWriter) /// /// The line to write before indenting. /// A scope that restores the indentation level. - /// using (writer.IndentedScope("if (enabled)")) writer.WriteLine("Run();"); + /// using (writer.IndentedScope("if (enabled)")) writer.Line("Run();"); public IndentScope IndentedScope(string line) { - WriteLine(line); + Line(line); return IndentedScope(); } @@ -2851,7 +3126,7 @@ public IndentScope IndentedScope(string line) /// The line to write before indenting. /// The action to invoke while indented. /// The current writer. - /// writer.Indented("if (enabled)", body => body.WriteLine("Run();")); + /// writer.Indented("if (enabled)", body => body.Line("Run();")); public CodeWriter Indented(string line, Action bodyWriter) { if (bodyWriter is null) @@ -2865,7 +3140,7 @@ public CodeWriter Indented(string line, Action bodyWriter) /// Creates the generated source string. /// /// The complete contents of the writer. - /// writer.WriteLine("class C { }"); + /// writer.Line("class C { }"); /// var source = writer.ToString(); // class C { } [SuppressMessage( "Design", @@ -2889,7 +3164,7 @@ public override string ToString() public static implicit operator Microsoft.CodeAnalysis.Text.SourceText(CodeWriter writer) => Microsoft.CodeAnalysis.Text.SourceText.From(writer.ToString(), Encoding.UTF8); - void WriteIndentIfRequired() + void IndentIfRequired() { if (!_atLineStart) return; @@ -2908,7 +3183,19 @@ void AppendIndentation() _builder.Append(' ', _indentLevel * _indentationSize); } - void WriteExpression(string? expression, Action? expressionWriter) + /// + /// Writes a preprocessor directive such as #if NET or #endif at column zero, without any + /// indentation, followed by a line feed. + /// + void DirectiveLine(string value) + { + EnsureNewLine(); + _builder.Append(value); + _builder.Append(NewLineCharacter); + _atLineStart = true; + } + + void Expression(string? expression, Action? expressionWriter) { var callback = expressionWriter; if (callback is not null) @@ -2943,7 +3230,15 @@ static void ValidateStatementPart(string? value, string parameterName) throw new ArgumentException("Statement text cannot be null or whitespace.", parameterName); } - void WriteParametersWithHeuristic(ImmutableArray parameters) + static string EscapeStringLiteral(string value) => + value + .Replace("\\", "\\\\") + .Replace("\"", "\\\"") + .Replace("\r", "\\r") + .Replace("\n", "\\n") + .Replace("\t", "\\t"); + + void ParametersWithHeuristic(ImmutableArray parameters) { if (parameters.IsDefault) parameters = []; @@ -2959,7 +3254,7 @@ void WriteParametersWithHeuristic(ImmutableArray pa { if (index != 0) Write(", "); - WriteParameter(parameters[index]); + Parameter(parameters[index]); } Write(')'); return; @@ -2968,9 +3263,9 @@ void WriteParametersWithHeuristic(ImmutableArray pa NewLine().Indent(); for (var index = 0; index < parameters.Length; index++) { - WriteParameter(parameters[index]); + Parameter(parameters[index]); if (index != parameters.Length - 1) - WriteLine(","); + Line(","); else NewLine(); } @@ -2978,13 +3273,13 @@ void WriteParametersWithHeuristic(ImmutableArray pa Write(')'); } - CodeWriter WriteParameter(ParameterDeclarationOptions parameter) + CodeWriter Parameter(ParameterDeclarationOptions parameter) { for (var index = 0; !parameter.Attributes.IsDefaultOrEmpty && index < parameter.Attributes.Length; index++) - WriteAttribute(parameter.Attributes[index], defaultTarget: null).Write(' '); - WriteIf(parameter.IsThis, "this ") - .WriteIf(parameter.IsScoped, "scoped ") - .WriteIf(parameter.IsParams, "params ") + Attribute(parameter.Attributes[index], defaultTarget: null).Write(' '); + If(parameter.IsThis, "this ") + .If(parameter.IsScoped, "scoped ") + .If(parameter.IsParams, "params ") .Write( parameter.Modifier switch { @@ -2996,7 +3291,7 @@ CodeWriter WriteParameter(ParameterDeclarationOptions parameter) _ => throw new ArgumentOutOfRangeException(nameof(parameter)), } ) - .WriteTypeReference(GetParameterType(parameter)) + .TypeReference(GetParameterType(parameter)) .Write(' ') .Write(parameter.Name); if (parameter.DefaultValue is not null) @@ -3050,14 +3345,14 @@ static string RenderCallArgument(MethodCallArgumentOptions argument) + argument.Value; } - void WriteAttributes(ImmutableArray attributes, string? defaultTarget = null) + void Attributes(ImmutableArray attributes, string? defaultTarget = null) { ValidateAttributes(attributes, nameof(attributes)); for (var index = 0; !attributes.IsDefaultOrEmpty && index < attributes.Length; index++) - WriteAttribute(attributes[index], defaultTarget).NewLine(); + Attribute(attributes[index], defaultTarget).NewLine(); } - CodeWriter WriteAttribute(AttributeDeclarationOptions attribute, string? defaultTarget) + CodeWriter Attribute(AttributeDeclarationOptions attribute, string? defaultTarget) { Write('['); var target = attribute.Target ?? defaultTarget; @@ -3101,7 +3396,7 @@ static int GetAttributeLength(AttributeDeclarationOptions attribute) return length; } - void WriteMemberModifiers( + void MemberModifiers( TypeDeclarationAccessibility? accessibility, bool isStatic, bool isAbstract, @@ -3113,20 +3408,20 @@ void WriteMemberModifiers( ) { if (accessibility is { } value) - WriteAccessibility(value).Write(' '); - WriteIf(isRequired, "required ") - .WriteIf(isReadOnly, "readonly ") - .WriteIf(isStatic, "static ") - .WriteIf(isSealed, "sealed ") - .WriteIf(isAbstract, "abstract ") - .WriteIf(isVirtual, "virtual ") - .WriteIf(isOverride, "override "); + Accessibility(value).Write(' '); + If(isRequired, "required ") + .If(isReadOnly, "readonly ") + .If(isStatic, "static ") + .If(isSealed, "sealed ") + .If(isAbstract, "abstract ") + .If(isVirtual, "virtual ") + .If(isOverride, "override "); } - CodeWriter WritePropertyHeader(PropertyDeclarationOptions declaration) + CodeWriter PropertyHeader(PropertyDeclarationOptions declaration) { - WriteMemberModifiers( - declaration.Accessibility, + MemberModifiers( + ResolveAccessibility(declaration.Accessibility, DefaultPropertyAccessibility), declaration.IsStatic, declaration.IsAbstract, declaration.IsVirtual, @@ -3134,23 +3429,23 @@ CodeWriter WritePropertyHeader(PropertyDeclarationOptions declaration) declaration.IsSealed, isRequired: declaration.IsRequired ); - return WriteTypeReference(declaration.Type).Write(' ').Write(declaration.Name); + return TypeReference(declaration.Type).Write(' ').Write(declaration.Name); } - void WriteAccessor(TypeDeclarationAccessibility? accessibility, string accessor) + void Accessor(TypeDeclarationAccessibility? accessibility, string accessor) { if (accessibility is { } value) - WriteAccessibility(value).Write(' '); + Accessibility(value).Write(' '); Write(accessor).Write(' '); } - void WriteAccessorBody(TypeDeclarationAccessibility? accessibility, string accessor, Action? writeBody) + void AccessorBody(TypeDeclarationAccessibility? accessibility, string accessor, Action? writeBody) { if (accessibility is { } value) - WriteAccessibility(value).Write(' '); + Accessibility(value).Write(' '); if (writeBody is null) { - Write(accessor).WriteLine(";"); + Write(accessor).Line(";"); return; } using (OpenBlockScope(accessor)) @@ -3186,7 +3481,7 @@ void CompleteWrittenItem(WrittenItemKind item, int indent) BlockScope OpenBlockScope(WrittenItemKind completedItem) { var itemIndent = _indentLevel; - WriteLine("{").Indent(); + Line("{").Indent(); return TrackOpenBlockScope(header: null, closingSeparator: "}", completedItem, itemIndent); } @@ -3209,7 +3504,9 @@ BlockScope TrackOpenBlockScope( string? header, string? closingSeparator, WrittenItemKind completedItem = WrittenItemKind.None, - int itemIndent = -1 + int itemIndent = -1, + bool closingAtColumnZero = false, + bool changesIndentation = true ) { return new BlockScope( @@ -3221,7 +3518,9 @@ BlockScope TrackOpenBlockScope( TracksOpenScopes ? new StackTrace(1, fNeedFileInfo: true).ToString() : string.Empty ), (int)completedItem, - itemIndent + itemIndent, + closingAtColumnZero, + changesIndentation ); } @@ -3236,13 +3535,36 @@ int OpenScope(string kind, string? header, string capturedStackTrace) return scopeId; } - void CloseBlock(string? closingSeparator, int scopeId, int completedItem, int itemIndent) + void CloseBlock( + string? closingSeparator, + int scopeId, + int completedItem, + int itemIndent, + bool closingAtColumnZero, + bool changesIndentation + ) { CloseScope(scopeId, "block"); - Unindent(); + if (changesIndentation) + Unindent(); + if (closingSeparator is not null) - WriteLine(closingSeparator); + { + if (closingAtColumnZero) + { + DirectiveLine(closingSeparator); + + // A directive between declarations must not participate in member blank-line spacing: + // advance the tracker so the next member's separator lands after the directive, and at + // file level ensure a trailing blank line so directive sections remain separated. + _lastWrittenItemEnd = _builder.Length; + if (_indentLevel == 0) + EnsureBlankLine(); + } + else + Line(closingSeparator); + } if (completedItem != (int)WrittenItemKind.None) CompleteWrittenItem((WrittenItemKind)completedItem, itemIndent); @@ -3265,7 +3587,7 @@ void CloseScope(int scopeId, string kind) OpenScopeCount--; } - CodeWriter WriteAccessibility(TypeDeclarationAccessibility accessibility) + CodeWriter Accessibility(TypeDeclarationAccessibility accessibility) { return Write( accessibility switch @@ -3282,7 +3604,44 @@ CodeWriter WriteAccessibility(TypeDeclarationAccessibility accessibility) ); } - void WriteGenericTypeParameters(ImmutableArray genericTypes) + static TypeDeclarationAccessibility? ResolveAccessibility( + TypeDeclarationAccessibility? explicitAccessibility, + TypeDeclarationAccessibility? defaultAccessibility + ) => explicitAccessibility ?? defaultAccessibility; + + static TypeDeclarationAccessibility? ResolveAccessorAccessibility( + TypeDeclarationAccessibility? explicitAccessibility, + TypeDeclarationAccessibility? defaultAccessibility, + TypeDeclarationAccessibility? propertyAccessibility + ) + { + var resolved = explicitAccessibility ?? defaultAccessibility; + if (resolved is null || propertyAccessibility is null) + return null; + + // C# forbids an accessor modifier that is equal to or more permissive than the property's own + // accessibility (CS0273). When the resolved accessor accessibility is not strictly more + // restrictive, the accessor inherits the property's accessibility instead. + return IsValidAccessorAccessibility(resolved.Value, propertyAccessibility.Value) ? resolved : null; + } + + static bool IsValidAccessorAccessibility( + TypeDeclarationAccessibility accessor, + TypeDeclarationAccessibility property + ) => + accessor switch + { + TypeDeclarationAccessibility.Private => true, + TypeDeclarationAccessibility.PrivateProtected => property + is not (TypeDeclarationAccessibility.Private or TypeDeclarationAccessibility.PrivateProtected), + TypeDeclarationAccessibility.Protected or TypeDeclarationAccessibility.Internal => property + is TypeDeclarationAccessibility.Public + or TypeDeclarationAccessibility.ProtectedInternal, + TypeDeclarationAccessibility.ProtectedInternal => property is TypeDeclarationAccessibility.Public, + _ => false, + }; + + void GenericTypeParameters(ImmutableArray genericTypes) { if (genericTypes.IsDefaultOrEmpty) return; @@ -3299,7 +3658,7 @@ void WriteGenericTypeParameters(ImmutableArray gene Write('>'); } - void WriteParameterList( + void ParameterList( ImmutableArray parameters, bool writeOnSeparateLines, bool writeWhenEmpty = false @@ -3322,7 +3681,7 @@ void WriteParameterList( if (writeOnSeparateLines) NewLine(); - WriteParameter(parameters[index]); + Parameter(parameters[index]); if (writeOnSeparateLines && index != parameters.Length - 1) Write(','); } @@ -3334,7 +3693,7 @@ void WriteParameterList( Write(')'); } - void WriteBaseTypes(TypeDeclarationOptions declaration) + void BaseTypes(TypeDeclarationOptions declaration) { var hasBaseType = declaration.BaseType is { IsEmpty: false }; var hasInterfaces = HasNonEmptyTypeReferences(declaration.Interfaces); @@ -3343,7 +3702,7 @@ void WriteBaseTypes(TypeDeclarationOptions declaration) Write(" : "); if (hasBaseType) - WriteTypeReference(declaration.BaseType!); + TypeReference(declaration.BaseType!); if (!hasInterfaces) return; @@ -3356,7 +3715,7 @@ void WriteBaseTypes(TypeDeclarationOptions declaration) if (wroteType) Write(", "); - WriteTypeReference(declaration.Interfaces[index]); + TypeReference(declaration.Interfaces[index]); wroteType = true; } } @@ -3371,7 +3730,7 @@ static bool HasNonEmptyTypeReferences(ImmutableArray types) return false; } - void WriteGenericConstraints(ImmutableArray genericTypes) + void GenericConstraints(ImmutableArray genericTypes) { if (genericTypes.IsDefaultOrEmpty) return; @@ -3565,7 +3924,7 @@ static void ValidateConstructorDeclaration(ConstructorDeclarationOptions declara throw new ArgumentException("A static constructor cannot specify accessibility.", nameof(declaration)); } - void WriteMethodGenericConstraints(ImmutableArray genericTypes) + void MethodGenericConstraints(ImmutableArray genericTypes) { if (genericTypes.IsDefaultOrEmpty) return; @@ -3804,7 +4163,7 @@ string parameterName /// string? are elided because they are invalid outside a nullable context. Nullable value /// types such as int? are always written. /// - public CodeWriter WriteType(TypeReference reference) + public CodeWriter Type(TypeReference reference) { if (reference is null) throw new ArgumentNullException(nameof(reference)); @@ -3816,7 +4175,7 @@ public CodeWriter WriteType(TypeReference reference) return Write(reference.RenderFullNameForNullable(ShouldUseNullableAnnotations)); } - CodeWriter WriteTypeReference(TypeReference reference) + CodeWriter TypeReference(TypeReference reference) { if (reference.IsEmpty) return this; diff --git a/src/src/SourceGeneratorShared/Extensions/Microsoft/CodeAnalysis/IncrementalGeneratorInitializationContextExtensions.cs b/src/src/SourceGeneratorShared/Extensions/Microsoft/CodeAnalysis/IncrementalGeneratorInitializationContextExtensions.cs index a485b93..b7fda24 100644 --- a/src/src/SourceGeneratorShared/Extensions/Microsoft/CodeAnalysis/IncrementalGeneratorInitializationContextExtensions.cs +++ b/src/src/SourceGeneratorShared/Extensions/Microsoft/CodeAnalysis/IncrementalGeneratorInitializationContextExtensions.cs @@ -35,16 +35,17 @@ public IncrementalGeneratorInitializationContext RegisterEmbeddedAttribute( context.RegisterPostInitializationOutput(spc => { CodeWriter writer = new(settings); - writer.WriteAutoGeneratedHeader(); - writer.WriteFileScopedNamespace( + writer.AutoGeneratedHeader(); + writer.FileScopedNamespace( PurviewTypeLibrary.Microsoft.CodeAnalysis.EmbeddedAttribute.AsTypeReference() ); // The compiler treats this special type as implicitly compiler-generated and embedded, - // so no generated attributes are applied here. - writer.WriteClass( + // so no generated attributes are applied here and it must remain internal (CS9271). + writer.Class( new(PurviewTypeLibrary.Microsoft.CodeAnalysis.EmbeddedAttribute) { + Accessibility = TypeDeclarationAccessibility.Internal, IsSealed = true, IsPartial = true, BaseType = PurviewTypeLibrary.System.Attribute, diff --git a/src/src/SourceGeneratorShared/GenerationSettings.cs b/src/src/SourceGeneratorShared/GenerationSettings.cs index 6ec7910..b506839 100644 --- a/src/src/SourceGeneratorShared/GenerationSettings.cs +++ b/src/src/SourceGeneratorShared/GenerationSettings.cs @@ -96,6 +96,80 @@ public GenerationSettings( /// public LanguageVersion? LanguageVersion { get; init; } + /// + /// Gets the default accessibility emitted for type declarations (classes, structs, records, + /// interfaces, enums, and delegates) when a declaration does not specify one. The default is + /// . Set to to omit the + /// modifier, matching the previous behaviour. + /// + public TypeDeclarationAccessibility? DefaultTypeAccessibility { get; init; } = TypeDeclarationAccessibility.Public; + + /// + /// Gets the default accessibility emitted for properties and indexers when a declaration does not + /// specify one. The default is . Set to + /// to omit the modifier, matching the previous behaviour. + /// + public TypeDeclarationAccessibility? DefaultPropertyAccessibility { get; init; } = + TypeDeclarationAccessibility.Public; + + /// + /// Gets the default accessibility emitted for property and indexer getters when a declaration does not + /// specify one. The default is . The modifier is + /// emitted only when it is more restrictive than the property's own accessibility; otherwise the + /// accessor inherits it. Set to to omit the modifier. + /// + public TypeDeclarationAccessibility? DefaultPropertyGetterAccessibility { get; init; } = + TypeDeclarationAccessibility.Public; + + /// + /// Gets the default accessibility emitted for property and indexer setters when a declaration does not + /// specify one. The default is . The modifier is + /// emitted only when it is more restrictive than the property's own accessibility; otherwise the + /// accessor inherits it. Set to to omit the modifier. + /// + public TypeDeclarationAccessibility? DefaultPropertySetterAccessibility { get; init; } = + TypeDeclarationAccessibility.Public; + + /// + /// Gets the default accessibility emitted for field declarations when a declaration does not specify + /// one. The default is . Set to + /// to omit the modifier, matching the previous behaviour. + /// + public TypeDeclarationAccessibility? DefaultFieldAccessibility { get; init; } = + TypeDeclarationAccessibility.Private; + + /// + /// Gets the default accessibility emitted for method declarations when a declaration does not specify + /// one. The default is . Set to + /// to omit the modifier, matching the previous behaviour. + /// + public TypeDeclarationAccessibility? DefaultMethodAccessibility { get; init; } = + TypeDeclarationAccessibility.Public; + + /// + /// Gets the default accessibility emitted for constructor declarations when a declaration does not + /// specify one. The default is . Set to + /// to omit the modifier, matching the previous behaviour. + /// + public TypeDeclarationAccessibility? DefaultConstructorAccessibility { get; init; } = + TypeDeclarationAccessibility.Public; + + /// + /// Gets the default accessibility emitted for indexer declarations when a declaration does not specify + /// one. The default is . Set to + /// to omit the modifier, matching the previous behaviour. + /// + public TypeDeclarationAccessibility? DefaultIndexerAccessibility { get; init; } = + TypeDeclarationAccessibility.Public; + + /// + /// Gets the default accessibility emitted for operator declarations when a declaration does not + /// specify one. The default is . Set to + /// to omit the modifier, matching the previous behaviour. + /// + public TypeDeclarationAccessibility? DefaultOperatorAccessibility { get; init; } = + TypeDeclarationAccessibility.Public; + /// /// Creates a new generation settings instance for the specified generator type, using the type name and assembly version. /// diff --git a/src/src/SourceGeneratorShared/NullableDirectiveMode.cs b/src/src/SourceGeneratorShared/NullableDirectiveMode.cs index dde0c05..643cc8e 100644 --- a/src/src/SourceGeneratorShared/NullableDirectiveMode.cs +++ b/src/src/SourceGeneratorShared/NullableDirectiveMode.cs @@ -1,7 +1,7 @@ namespace Purview.SourceGeneratorFramework; /// -/// Controls whether the #nullable enable directive is emitted by WriteAutoGeneratedHeader +/// Controls whether the #nullable enable directive is emitted by AutoGeneratedHeader /// and whether nullable reference annotations are rendered by type writing. Both stay in lockstep. /// public enum NullableDirectiveMode diff --git a/src/src/SourceGeneratorShared/TypeDeclarationOptions.cs b/src/src/SourceGeneratorShared/TypeDeclarationOptions.cs index daf34d3..61023d8 100644 --- a/src/src/SourceGeneratorShared/TypeDeclarationOptions.cs +++ b/src/src/SourceGeneratorShared/TypeDeclarationOptions.cs @@ -141,7 +141,7 @@ public TypeDeclarationOptions(TypeIdentity type, TypeDeclarationAccessibility? a /// /// Gets whether to emit on the type. - /// When , WriteAttributeClass enables it and other type-writing + /// When , AttributeClass enables it and other type-writing /// APIs leave it disabled. Set this explicitly to to opt a generated /// attribute out of embedding. /// diff --git a/src/src/SourceGeneratorShared/XmlCommentWriter.cs b/src/src/SourceGeneratorShared/XmlCommentWriter.cs index 4ea3297..46bf0e0 100644 --- a/src/src/SourceGeneratorShared/XmlCommentWriter.cs +++ b/src/src/SourceGeneratorShared/XmlCommentWriter.cs @@ -78,7 +78,7 @@ public CodeWriter XmlPermission(string cref, params string[] content) => /// /// Writes a self-closing XML <inheritdoc /> element. /// - public CodeWriter XmlInheritDoc() => writer.Write("/// ").WriteLine(BuildSelfClosingXmlTag("inheritdoc")); + public CodeWriter XmlInheritDoc() => writer.Write("/// ").Line(BuildSelfClosingXmlTag("inheritdoc")); /// /// Writes a self-closing XML <inheritdoc /> element for a member. @@ -86,7 +86,7 @@ public CodeWriter XmlPermission(string cref, params string[] content) => public CodeWriter XmlInheritDoc(string cref) => string.IsNullOrWhiteSpace(cref) ? throw new ArgumentException("The XML cref cannot be null or empty.", nameof(cref)) - : writer.Write("/// ").WriteLine(BuildSelfClosingXmlTag("inheritdoc", ("cref", cref))); + : writer.Write("/// ").Line(BuildSelfClosingXmlTag("inheritdoc", ("cref", cref))); /// /// Writes an XML <returns> documentation block with the specified content. @@ -178,7 +178,7 @@ public CodeWriter XmlSeeAlso(string cref, params string[] content) // If no content is provided, write a self-closing tag. Otherwise, write a block with the provided content. return content is null || content.Length == 0 - ? writer.Write("/// ").WriteLine(BuildSelfClosingXmlTag("seealso", ("cref", cref))) + ? writer.Write("/// ").Line(BuildSelfClosingXmlTag("seealso", ("cref", cref))) : XmlCore(writer, BuildXmlTag("seealso", ("cref", cref)), "seealso", content); } @@ -190,7 +190,7 @@ public CodeWriter XmlSeeAlso(TypeIdentity type, params string[] content) var cref = ToXmlCref(type); return content is null || content.Length == 0 - ? writer.Write("/// ").WriteLine(BuildSelfClosingXmlTag("seealso", ("cref", cref))) + ? writer.Write("/// ").Line(BuildSelfClosingXmlTag("seealso", ("cref", cref))) : XmlCore(writer, BuildXmlTag("seealso", ("cref", cref)), "seealso", content); } @@ -206,7 +206,7 @@ public CodeWriter XmlSeeAlso(TypeReference type, params string[] content) var cref = ToXmlCref(type); return content is null || content.Length == 0 - ? writer.Write("/// ").WriteLine(BuildSelfClosingXmlTag("seealso", ("cref", cref))) + ? writer.Write("/// ").Line(BuildSelfClosingXmlTag("seealso", ("cref", cref))) : XmlCore(writer, BuildXmlTag("seealso", ("cref", cref)), "seealso", content); } @@ -221,7 +221,7 @@ public CodeWriter XmlInclude(string file, string path) throw new ArgumentException("The include path cannot be null or empty.", nameof(path)); // Write a self-closing tag with the specified file and path attributes. - return writer.Write("/// ").WriteLine(BuildSelfClosingXmlTag("include", ("file", file), ("path", path))); + return writer.Write("/// ").Line(BuildSelfClosingXmlTag("include", ("file", file), ("path", path))); } /// @@ -365,7 +365,7 @@ CodeWriter XmlCore( .Write(content[0]) .Write(""); + .Line(">"); } var isMultiLine = startTag is not null; @@ -379,10 +379,10 @@ CodeWriter XmlCore( insideStart?.Invoke(writer); foreach (var line in content) - writer.Write("/// ").WriteLine(line); + writer.Write("/// ").Line(line); if (endTag is not null) - writer.Write("/// "); + writer.Write("/// "); insideEnd?.Invoke(writer); diff --git a/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/DiscardedCodeWriterScopeAnalyzerTests.cs b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/DiscardedCodeWriterScopeAnalyzerTests.cs index b194d58..9d2d50e 100644 --- a/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/DiscardedCodeWriterScopeAnalyzerTests.cs +++ b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/DiscardedCodeWriterScopeAnalyzerTests.cs @@ -18,7 +18,7 @@ class Emitter public void Emit() { var writer = new CodeWriter(new GenerationSettings("G")); - writer.WriteClassScope(new TypeDeclarationOptions("C")); + writer.ClassScope(new TypeDeclarationOptions("C")); } } """; @@ -55,9 +55,9 @@ class Emitter public void Emit() { var writer = new CodeWriter(new GenerationSettings("G")); - using (writer.WriteClassScope(new TypeDeclarationOptions("C"))) + using (writer.ClassScope(new TypeDeclarationOptions("C"))) { - writer.WriteLine("// body"); + writer.Line("// body"); } } } @@ -94,7 +94,7 @@ class Emitter public void Emit() { var writer = new CodeWriter(new GenerationSettings("G")); - writer.WriteProperty(new PropertyDeclarationOptions("Name", TypeIdentity.Create())); + writer.Property(new PropertyDeclarationOptions("Name", TypeIdentity.Create())); } } """; diff --git a/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferHashDefinesAnalyzerTests.cs b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferHashDefinesAnalyzerTests.cs new file mode 100644 index 0000000..717bea5 --- /dev/null +++ b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferHashDefinesAnalyzerTests.cs @@ -0,0 +1,186 @@ +using Purview.SourceGeneratorFramework.Testing; +using Purview.SourceGeneratorFramework.Testing.TUnit; + +namespace Purview.SourceGeneratorFramework.Analyzers; + +public sealed class PreferHashDefinesAnalyzerTests : TUnitDiagnosticAnalyzerTestBase +{ + static readonly AnalyzerTestOptions Options = new() + { + AdditionalAssemblyTypes = [typeof(CodeWriter), typeof(GenerationSettings)], + }; + + [Test] + public async Task Line_WithIfDirective_ReportsDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line("#if NET"); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasDiagnostics(1); + await Assert.That(result).HasDiagnostic(PreferHashDefinesAnalyzer.Rule.Id); + } + + [Test] + public async Task Line_WithEndIfDirective_ReportsDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line("#endif"); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasDiagnostics(1); + await Assert.That(result).HasDiagnostic(PreferHashDefinesAnalyzer.Rule.Id); + } + + [Test] + public async Task Line_WithElseDirective_ReportsDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line("#else"); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasDiagnostics(1); + await Assert.That(result).HasDiagnostic(PreferHashDefinesAnalyzer.Rule.Id); + } + + [Test] + public async Task Line_WithRegionDirective_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line("#region Generated"); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasNoDiagnostics(); + } + + [Test] + public async Task Line_WithStatement_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line("return value;"); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasNoDiagnostics(); + } + + [Test] + public async Task HashDefines_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.HashDefines("NET", body => body.Line("// NET only")); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasNoDiagnostics(); + } + + [Test] + public async Task Line_OnOtherType_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + class Emitter + { + public void Emit() + { + var writer = new OtherWriter(); + writer.Line("#if NET"); + } + } + + class OtherWriter + { + public void Line(string value) { } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasNoDiagnostics(); + } +} diff --git a/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferMinimalCodeWriterOverloadAnalyzerTests.cs b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferMinimalCodeWriterOverloadAnalyzerTests.cs new file mode 100644 index 0000000..44ebd96 --- /dev/null +++ b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferMinimalCodeWriterOverloadAnalyzerTests.cs @@ -0,0 +1,270 @@ +using Purview.SourceGeneratorFramework.Testing; +using Purview.SourceGeneratorFramework.Testing.TUnit; + +namespace Purview.SourceGeneratorFramework.Analyzers; + +public sealed class PreferMinimalCodeWriterOverloadAnalyzerTests + : TUnitDiagnosticAnalyzerTestBase +{ + static readonly AnalyzerTestOptions Options = new() + { + AdditionalAssemblyTypes = [typeof(CodeWriter), typeof(GenerationSettings), typeof(PropertyDeclarationOptions)], + }; + + [Test] + public async Task Property_WithBareOptions_WritesMinimalOverload(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Property(new PropertyDeclarationOptions("Name", TypeReference.Create(), TypeDeclarationAccessibility.Public)); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + var diagnostic = await Assert.That(result).HasDiagnostic(PreferMinimalCodeWriterOverloadAnalyzer.Rule.Id); + await Assert + .That(diagnostic.GetMessage(System.Globalization.CultureInfo.InvariantCulture)) + .Contains("Property(name, type, accessibility)"); + } + + [Test] + public async Task Field_WithBareOptions_ReportsDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Field(new FieldDeclarationOptions("_value", TypeReference.Create(), TypeDeclarationAccessibility.Private)); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasDiagnostics(1); + await Assert.That(result).HasDiagnostic(PreferMinimalCodeWriterOverloadAnalyzer.Rule.Id); + } + + [Test] + public async Task PartialMethod_WithBareOptions_ReportsDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.PartialMethod(new MethodDeclarationOptions("OnChanged", TypeReference.Create(), TypeDeclarationAccessibility.Public)); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + var diagnostic = await Assert.That(result).HasDiagnostic(PreferMinimalCodeWriterOverloadAnalyzer.Rule.Id); + await Assert + .That(diagnostic.GetMessage(System.Globalization.CultureInfo.InvariantCulture)) + .Contains("PartialMethod(name, returnType, accessibility)"); + } + + [Test] + public async Task ConstructorScope_WithBareOptions_ReportsDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.ConstructorScope(new ConstructorDeclarationOptions("C", TypeDeclarationAccessibility.Public)); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasDiagnostics(1); + await Assert.That(result).HasDiagnostic(PreferMinimalCodeWriterOverloadAnalyzer.Rule.Id); + } + + [Test] + public async Task EnumScope_WithBareOptions_ReportsDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.EnumScope(new TypeDeclarationOptions("Status", TypeDeclarationAccessibility.Public)); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasDiagnostics(1); + await Assert.That(result).HasDiagnostic(PreferMinimalCodeWriterOverloadAnalyzer.Rule.Id); + } + + [Test] + public async Task EnumField_WithBareOptions_ReportsDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.EnumField(new EnumFieldDeclarationOptions("Ready", 1)); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasDiagnostics(1); + await Assert.That(result).HasDiagnostic(PreferMinimalCodeWriterOverloadAnalyzer.Rule.Id); + } + + [Test] + public async Task Property_WithTargetTypedNew_ReportsDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Property(new("Name", TypeReference.Create(), TypeDeclarationAccessibility.Public)); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasDiagnostics(1); + await Assert.That(result).HasDiagnostic(PreferMinimalCodeWriterOverloadAnalyzer.Rule.Id); + } + + [Test] + public async Task Property_WithObjectInitializer_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Property(new PropertyDeclarationOptions("Name", TypeReference.Create(), TypeDeclarationAccessibility.Public) + { + HasSetter = true, + }); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasNoDiagnostics(); + } + + [Test] + public async Task Indexer_WithBareOptions_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Indexer(new IndexerDeclarationOptions(TypeReference.Create(), new("index", TypeReference.Create()))); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasNoDiagnostics(); + } + + [Test] + public async Task Property_OnOtherType_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + class Emitter + { + public void Emit() + { + var writer = new OtherWriter(); + writer.Property(new PropertyDeclarationOptions("Name", TypeReference.Create(), TypeDeclarationAccessibility.Public)); + } + } + + class OtherWriter + { + public void Property(PropertyDeclarationOptions declaration) { } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasNoDiagnostics(); + } +} diff --git a/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferNullableContextOverloadAnalyzerTests.cs b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferNullableContextOverloadAnalyzerTests.cs index 293a238..157e7b4 100644 --- a/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferNullableContextOverloadAnalyzerTests.cs +++ b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferNullableContextOverloadAnalyzerTests.cs @@ -17,7 +17,7 @@ class Emitter public void Emit(CodeWriter writer) { var nullable = TypeIdentity.Create().MakeNullable(); - writer.WriteType(nullable); + writer.Type(nullable); } } """; @@ -69,7 +69,7 @@ class Emitter public void Emit(CodeWriter writer) { var nullable = TypeIdentity.Create().MakeNullable(writer); - writer.WriteType(nullable); + writer.Type(nullable); } } """; diff --git a/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferPragmaDisableAnalyzerTests.cs b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferPragmaDisableAnalyzerTests.cs new file mode 100644 index 0000000..290d873 --- /dev/null +++ b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferPragmaDisableAnalyzerTests.cs @@ -0,0 +1,161 @@ +using Purview.SourceGeneratorFramework.Testing; +using Purview.SourceGeneratorFramework.Testing.TUnit; + +namespace Purview.SourceGeneratorFramework.Analyzers; + +public sealed class PreferPragmaDisableAnalyzerTests : TUnitDiagnosticAnalyzerTestBase +{ + static readonly AnalyzerTestOptions Options = new() + { + AdditionalAssemblyTypes = [typeof(CodeWriter), typeof(GenerationSettings)], + }; + + [Test] + public async Task Line_WithPragmaDisable_ReportsDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line("#pragma warning disable CS8625"); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasDiagnostics(1); + await Assert.That(result).HasDiagnostic(PreferPragmaDisableAnalyzer.Rule.Id); + } + + [Test] + public async Task Line_WithPragmaRestore_ReportsDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line("#pragma warning restore CS8625"); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasDiagnostics(1); + await Assert.That(result).HasDiagnostic(PreferPragmaDisableAnalyzer.Rule.Id); + } + + [Test] + public async Task Line_WithPragmaChecksum_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line("#pragma checksum \"file.cs\" \"{00000000-0000-0000-0000-000000000000}\" \"{AAAA}\""); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasNoDiagnostics(); + } + + [Test] + public async Task Line_WithStatement_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line("return value;"); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasNoDiagnostics(); + } + + [Test] + public async Task PragmaDisable_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.PragmaDisable("CS8625"); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasNoDiagnostics(); + } + + [Test] + public async Task Line_OnOtherType_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + class Emitter + { + public void Emit() + { + var writer = new OtherWriter(); + writer.Line("#pragma warning disable CS8625"); + } + } + + class OtherWriter + { + public void Line(string value) { } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasNoDiagnostics(); + } +} diff --git a/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferStructuredCodeWriterApiAnalyzerTests.cs b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferStructuredCodeWriterApiAnalyzerTests.cs index e4c7431..fed2c0a 100644 --- a/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferStructuredCodeWriterApiAnalyzerTests.cs +++ b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferStructuredCodeWriterApiAnalyzerTests.cs @@ -7,7 +7,7 @@ public sealed class PreferStructuredCodeWriterApiAnalyzerTests : TUnitDiagnosticAnalyzerTestBase { [Test] - public async Task WriteLine_WithClassDeclaration_ReportsDiagnostic(CancellationToken cancellationToken) + public async Task Line_WithClassDeclaration_ReportsDiagnostic(CancellationToken cancellationToken) { // Arrange const string source = """ @@ -18,7 +18,7 @@ class Emitter public void Emit() { var writer = new CodeWriter(new GenerationSettings("G")); - writer.WriteLine("public class C { }"); + writer.Line("public class C { }"); } } """; @@ -36,7 +36,7 @@ public void Emit() } [Test] - public async Task WriteLine_WithStatement_DoesNotReportDiagnostic(CancellationToken cancellationToken) + public async Task Line_WithStatement_DoesNotReportDiagnostic(CancellationToken cancellationToken) { // Arrange const string source = """ @@ -47,7 +47,7 @@ class Emitter public void Emit() { var writer = new CodeWriter(new GenerationSettings("G")); - writer.WriteLine("return value;"); + writer.Line("return value;"); } } """; @@ -64,7 +64,7 @@ public void Emit() } [Test] - public async Task WriteLine_WithUsingStatement_DoesNotReportDiagnostic(CancellationToken cancellationToken) + public async Task Line_WithUsingStatement_DoesNotReportDiagnostic(CancellationToken cancellationToken) { // Arrange const string source = """ @@ -75,7 +75,7 @@ class Emitter public void Emit() { var writer = new CodeWriter(new GenerationSettings("G")); - writer.WriteLine("using (var stream = Open())"); + writer.Line("using (var stream = Open())"); } } """; @@ -92,7 +92,7 @@ public void Emit() } [Test] - public async Task WriteMethodCall_DoesNotReportDiagnostic(CancellationToken cancellationToken) + public async Task MethodCall_DoesNotReportDiagnostic(CancellationToken cancellationToken) { // Arrange const string source = """ @@ -103,7 +103,153 @@ class Emitter public void Emit() { var writer = new CodeWriter(new GenerationSettings("G")); - writer.WriteMethodCall("Run", "value"); + writer.MethodCall("Run", "value"); + } + } + """; + + // Act + var result = await AnalyzeAsync( + source, + new AnalyzerTestOptions { AdditionalAssemblyTypes = [typeof(CodeWriter), typeof(GenerationSettings)] }, + cancellationToken + ); + + // Assert + await Assert.That(result).HasNoDiagnostics(); + } + + [Test] + public async Task Line_WithInterpolatedClassDeclaration_ReportsDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit(string name) + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line($"public class {name} {{ }}"); + } + } + """; + + // Act + var result = await AnalyzeAsync( + source, + new AnalyzerTestOptions { AdditionalAssemblyTypes = [typeof(CodeWriter), typeof(GenerationSettings)] }, + cancellationToken + ); + + // Assert + await Assert.That(result).HasDiagnostics(1); + await Assert.That(result).HasDiagnostic(PreferStructuredCodeWriterApiAnalyzer.Rule.Id); + } + + [Test] + public async Task Line_WithRawStringClassDeclaration_ReportsDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """" + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line("""public class C { }"""); + } + } + """"; + + // Act + var result = await AnalyzeAsync( + source, + new AnalyzerTestOptions { AdditionalAssemblyTypes = [typeof(CodeWriter), typeof(GenerationSettings)] }, + cancellationToken + ); + + // Assert + await Assert.That(result).HasDiagnostics(1); + await Assert.That(result).HasDiagnostic(PreferStructuredCodeWriterApiAnalyzer.Rule.Id); + } + + [Test] + public async Task Line_WithConstClassDeclaration_ReportsDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + const string Header = "public class C { }"; + + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line(Header); + } + } + """; + + // Act + var result = await AnalyzeAsync( + source, + new AnalyzerTestOptions { AdditionalAssemblyTypes = [typeof(CodeWriter), typeof(GenerationSettings)] }, + cancellationToken + ); + + // Assert + await Assert.That(result).HasDiagnostics(1); + await Assert.That(result).HasDiagnostic(PreferStructuredCodeWriterApiAnalyzer.Rule.Id); + } + + [Test] + public async Task Line_WithConcatenatedClassDeclaration_ReportsDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line("public " + "class C { }"); + } + } + """; + + // Act + var result = await AnalyzeAsync( + source, + new AnalyzerTestOptions { AdditionalAssemblyTypes = [typeof(CodeWriter), typeof(GenerationSettings)] }, + cancellationToken + ); + + // Assert + await Assert.That(result).HasDiagnostics(1); + await Assert.That(result).HasDiagnostic(PreferStructuredCodeWriterApiAnalyzer.Rule.Id); + } + + [Test] + public async Task Line_WithInterpolatedStatement_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit(string value) + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line($"return {value};"); } } """; diff --git a/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferStructuredCodeWriterStatementAnalyzerTests.cs b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferStructuredCodeWriterStatementAnalyzerTests.cs new file mode 100644 index 0000000..3727937 --- /dev/null +++ b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferStructuredCodeWriterStatementAnalyzerTests.cs @@ -0,0 +1,337 @@ +using Purview.SourceGeneratorFramework.Testing; +using Purview.SourceGeneratorFramework.Testing.TUnit; + +namespace Purview.SourceGeneratorFramework.Analyzers; + +public sealed class PreferStructuredCodeWriterStatementAnalyzerTests + : TUnitDiagnosticAnalyzerTestBase +{ + static readonly AnalyzerTestOptions Options = new() + { + AdditionalAssemblyTypes = [typeof(CodeWriter), typeof(GenerationSettings)], + }; + + [Test] + public async Task Line_WithReturnStatement_ReportsDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line("return value;"); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + var diagnostic = await Assert.That(result).HasDiagnostic(PreferStructuredCodeWriterStatementAnalyzer.Rule.Id); + await Assert.That(diagnostic.GetMessage(System.Globalization.CultureInfo.InvariantCulture)).Contains("Return"); + } + + [Test] + public async Task Line_WithThrowStatement_ReportsDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line("throw new global::System.InvalidOperationException(\"failed\");"); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasDiagnostics(1); + await Assert.That(result).HasDiagnostic(PreferStructuredCodeWriterStatementAnalyzer.Rule.Id); + } + + [Test] + public async Task Line_WithAwaitedMethodCall_ReportsDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line("await LoadAsync(token);"); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasDiagnostics(1); + await Assert.That(result).HasDiagnostic(PreferStructuredCodeWriterStatementAnalyzer.Rule.Id); + } + + [Test] + public async Task Line_WithMethodCall_ReportsDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line("Process(item);"); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + var diagnostic = await Assert.That(result).HasDiagnostic(PreferStructuredCodeWriterStatementAnalyzer.Rule.Id); + await Assert + .That(diagnostic.GetMessage(System.Globalization.CultureInfo.InvariantCulture)) + .Contains("MethodCall"); + } + + [Test] + public async Task Line_WithAssignment_ReportsDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line("value = 42;"); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasDiagnostics(1); + await Assert.That(result).HasDiagnostic(PreferStructuredCodeWriterStatementAnalyzer.Rule.Id); + } + + [Test] + public async Task Line_WithUsingDirective_ReportsDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line("using System;"); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasDiagnostics(1); + await Assert.That(result).HasDiagnostic(PreferStructuredCodeWriterStatementAnalyzer.Rule.Id); + } + + [Test] + public async Task Line_WithComment_ReportsDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line("// generated note"); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasDiagnostics(1); + await Assert.That(result).HasDiagnostic(PreferStructuredCodeWriterStatementAnalyzer.Rule.Id); + } + + [Test] + public async Task Line_WithBlankLine_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line(); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasNoDiagnostics(); + } + + [Test] + public async Task Line_WithDeclaration_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line("public class C { }"); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasNoDiagnostics(); + } + + [Test] + public async Task Line_WithUsingStatement_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line("using (var stream = Open())"); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasNoDiagnostics(); + } + + [Test] + public async Task Line_WithMultilineStatement_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line("return value\n\t+ other;"); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasNoDiagnostics(); + } + + [Test] + public async Task MethodCall_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.MethodCall("Run", "value"); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasNoDiagnostics(); + } + + [Test] + public async Task Line_OnOtherType_DoesNotReportDiagnostic(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + class Emitter + { + public void Emit() + { + var writer = new OtherWriter(); + writer.Line("return value;"); + } + } + + class OtherWriter + { + public void Line(string value) { } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + await Assert.That(result).HasNoDiagnostics(); + } +} diff --git a/src/tests/SourceGeneratorFramework.CodeFixers.UnitTests/PreferNullableContextOverloadCodeFixProviderTests.cs b/src/tests/SourceGeneratorFramework.CodeFixers.UnitTests/PreferNullableContextOverloadCodeFixProviderTests.cs index 177702f..1b3a205 100644 --- a/src/tests/SourceGeneratorFramework.CodeFixers.UnitTests/PreferNullableContextOverloadCodeFixProviderTests.cs +++ b/src/tests/SourceGeneratorFramework.CodeFixers.UnitTests/PreferNullableContextOverloadCodeFixProviderTests.cs @@ -18,7 +18,7 @@ class Emitter public void Emit(CodeWriter writer) { var nullable = TypeIdentity.Create().MakeNullable(); - writer.WriteType(nullable); + writer.Type(nullable); } } """; @@ -81,8 +81,8 @@ public void Emit(CodeWriter writer) { var one = TypeIdentity.Create().MakeNullable(); var two = TypeIdentity.Create().MakeNullable(); - writer.WriteType(one); - writer.WriteType(two); + writer.Type(one); + writer.Type(two); } } """, @@ -94,7 +94,7 @@ class EmitterB public void Emit(CodeWriter writer) { var three = TypeIdentity.Create().MakeNullable(); - writer.WriteType(three); + writer.Type(three); } } """, diff --git a/src/tests/SourceGeneratorFramework.ExampleGenerator.UnitTests/CodeWriterSampleGeneratorTests.cs b/src/tests/SourceGeneratorFramework.ExampleGenerator.UnitTests/CodeWriterSampleGeneratorTests.cs new file mode 100644 index 0000000..010d52d --- /dev/null +++ b/src/tests/SourceGeneratorFramework.ExampleGenerator.UnitTests/CodeWriterSampleGeneratorTests.cs @@ -0,0 +1,92 @@ +using Purview.SourceGeneratorFramework.Examples; +using Purview.SourceGeneratorFramework.Testing; +using Purview.SourceGeneratorFramework.Testing.TUnit; +using Purview.SourceGeneratorFramework.Testing.TUnit.Assertions; + +namespace Purview.SourceGeneratorFramework.ExampleGenerator; + +public class CodeWriterSampleGeneratorTests + : TUnitSourceGeneratorTestBase +{ + [Test] + public async Task GenerateSample_GeneratesDemonstrativeClass(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + [GenerateCodeWriterSample] + public class SampleTarget { } + """; + + // Act + var result = await GenerateAsync(source, cancellationToken); + + // Assert + await Assert.That(result).HasGeneratedClass("SampleTargetCodeWriterSample"); + await Assert.That(result).HasGeneratedField("_value"); + await Assert.That(result).HasGeneratedProperty("Value"); + await Assert.That(result).HasGeneratedProperty("DefaultAccessibility"); + await Assert.That(result).HasGeneratedMethod("Describe"); + await Assert.That(result).HasGeneratedMethod("Format"); + + var defaultAccessibility = await Assert.That(result).HasGeneratedProperty("DefaultAccessibility"); + await Assert.That(defaultAccessibility.Modifiers.ToString()).IsEqualTo("public"); + + var classText = ( + await result.Generated().GetSyntaxTree("SampleTarget.CodeWriterSample.g.cs").GetTextAsync(cancellationToken) + ).ToString(); + await Assert.That(classText).Contains("#if NET\n\t// This member is emitted only for .NET targets.\n#endif"); + await Assert.That(classText).Contains("#pragma warning disable CS8625"); + await Assert.That(classText).Contains("#pragma warning disable CS0618"); + await Assert.That(classText).Contains("#pragma warning restore CS0618"); + } + + [Test] + public async Task GenerateSample_UsesStructuredStatements(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + [GenerateCodeWriterSample] + public class SampleTarget { } + """; + + // Act + var result = await GenerateAsync(source, cancellationToken); + + // Assert + var describe = await Assert.That(result).HasGeneratedMethod("Describe"); + var describeText = describe.ToString(); + await Assert.That(describeText).Contains("global::System.Console.WriteLine(\"Describe\");"); + await Assert.That(describeText).Contains("return value.ToString();"); + + var constructor = result.Generated().GetConstructor("SampleTargetCodeWriterSample"); + await Assert.That(constructor.ToString()).Contains("_value = value;"); + } + + [Test] + public async Task GenerateSample_EmitsNetConditionalReturn(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + [GenerateCodeWriterSample] + public class SampleTarget { } + """; + + // Act + var result = await GenerateAsync(source, cancellationToken); + + // Assert + var format = await Assert.That(result).HasGeneratedMethod("Format"); + var formatText = format.ToString(); + await Assert.That(formatText).Contains("#if NET"); + await Assert + .That(formatText) + .Contains( + "return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $\"Value: {_value}\");" + ); + await Assert.That(formatText).Contains("#else"); + await Assert + .That(formatText) + .Contains("return global::System.FormattableString.Invariant($\"Value: {_value}\");"); + await Assert.That(formatText).Contains("#endif"); + } +} diff --git a/src/tests/SourceGeneratorFramework.ExampleGenerator.UnitTests/CodeWriterSampleTestOptions.cs b/src/tests/SourceGeneratorFramework.ExampleGenerator.UnitTests/CodeWriterSampleTestOptions.cs new file mode 100644 index 0000000..242ae28 --- /dev/null +++ b/src/tests/SourceGeneratorFramework.ExampleGenerator.UnitTests/CodeWriterSampleTestOptions.cs @@ -0,0 +1,11 @@ +using Purview.SourceGeneratorFramework.Testing; + +namespace Purview.SourceGeneratorFramework.ExampleGenerator; + +public record CodeWriterSampleTestOptions : SourceGeneratorTestOptions +{ + public CodeWriterSampleTestOptions() + { + AdditionalNamespaces = AdditionalNamespaces.Add("Purview.SourceGeneratorFramework.Examples"); + } +} diff --git a/src/tests/SourceGeneratorFramework.ExampleGenerator.UnitTests/ServiceRegistrationGeneratorTests.cs b/src/tests/SourceGeneratorFramework.ExampleGenerator.UnitTests/ServiceRegistrationGeneratorTests.cs index 2cd5600..0686833 100644 --- a/src/tests/SourceGeneratorFramework.ExampleGenerator.UnitTests/ServiceRegistrationGeneratorTests.cs +++ b/src/tests/SourceGeneratorFramework.ExampleGenerator.UnitTests/ServiceRegistrationGeneratorTests.cs @@ -69,13 +69,18 @@ public class OtherService { } await Assert.That(method.HasParameters(query, IServiceCollection)).IsTrue(); var methodText = method.ToString(); + var compact = methodText + .Replace("\r", "", StringComparison.Ordinal) + .Replace("\n", "", StringComparison.Ordinal) + .Replace("\t", "", StringComparison.Ordinal) + .Replace(" ", "", StringComparison.Ordinal); await Assert - .That(methodText) + .That(compact) .Contains( "global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services);" ); await Assert - .That(methodText) + .That(compact) .Contains( "global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddScoped(services);" ); diff --git a/src/tests/SourceGeneratorFramework.UnitTests/CodeQueryTests.cs b/src/tests/SourceGeneratorFramework.UnitTests/CodeQueryTests.cs index cec3bd4..8feaa71 100644 --- a/src/tests/SourceGeneratorFramework.UnitTests/CodeQueryTests.cs +++ b/src/tests/SourceGeneratorFramework.UnitTests/CodeQueryTests.cs @@ -285,6 +285,64 @@ public struct Money await Assert.That(query.HasAttribute(money, "Missing")).IsFalse(); } + [Test] + public async Task OperatorQueries_WithParameterTypes_MatchSignature() + { + // Arrange + const string source = """ + namespace Test; + + public struct Money + { + public static bool operator ==(Money left, Money right) => true; + public static bool operator !=(Money left, Money right) => false; + public static bool operator <(Money left, Money right) => true; + public static bool operator >(Money left, Money right) => false; + } + """; + var (compilation, _) = TestCompilation.CreateWithRoot(source); + var query = new CodeQuery([.. compilation.SyntaxTrees], compilation); + var moneyType = new TypeReference(new TypeIdentity("Money", "Test")); + var stringType = TypeReference.Create(); + + // Act / Assert + await Assert.That(query.HasOperator("==", moneyType, moneyType)).IsTrue(); + await Assert.That(query.HasOperator("==", moneyType, stringType)).IsFalse(); + await Assert.That(query.HasOperator("!=", moneyType, moneyType)).IsTrue(); + await Assert.That(query.HasOperator("+", moneyType, moneyType)).IsFalse(); + await Assert.That(query.HasOperator("<", stringType, moneyType)).IsFalse(); + await Assert.That(query.GetOperator("==", moneyType, moneyType).OperatorToken.ValueText).IsEqualTo("=="); + } + + [Test] + public async Task ConversionOperatorQueries_WithParameterType_MatchSignature() + { + // Arrange + const string source = """ + namespace Test; + + public struct Money + { + public static implicit operator int(Money value) => 0; + public static explicit operator string(Money value) => ""; + } + """; + var (compilation, _) = TestCompilation.CreateWithRoot(source); + var query = new CodeQuery([.. compilation.SyntaxTrees], compilation); + var moneyType = new TypeReference(new TypeIdentity("Money", "Test")); + var intType = TypeReference.Create(); + var stringType = TypeReference.Create(); + + // Act / Assert + await Assert.That(query.HasConversionOperator("implicit", moneyType)).IsTrue(); + await Assert.That(query.HasConversionOperator("implicit", intType)).IsFalse(); + await Assert.That(query.HasConversionOperator("explicit", moneyType)).IsTrue(); + await Assert.That(query.HasConversionOperator("explicit", stringType)).IsFalse(); + await Assert + .That(query.GetConversionOperator("implicit", moneyType).ImplicitOrExplicitKeyword.ValueText) + .IsEqualTo("implicit"); + } + [Test] public async Task StatementQueries_FindStatementsAndInvocations() { diff --git a/src/tests/SourceGeneratorShared.UnitTests/CodeWriterTests.cs b/src/tests/SourceGeneratorShared.UnitTests/CodeWriterTests.cs index 749b3ed..0b3eb62 100644 --- a/src/tests/SourceGeneratorShared.UnitTests/CodeWriterTests.cs +++ b/src/tests/SourceGeneratorShared.UnitTests/CodeWriterTests.cs @@ -42,23 +42,23 @@ public async Task EmptyTypeReference_IsIgnoredByMemberEmitters() var writer = CodeWriterFactory.ForTests(); // Act - writer.WriteField(new FieldDeclarationOptions("field", TypeReference.Empty)); - writer.WriteProperty(new PropertyDeclarationOptions("Property", TypeReference.Empty)); - writer.WriteMethodScope(new MethodDeclarationOptions("Method", TypeReference.Empty)).Dispose(); + writer.Field(new FieldDeclarationOptions("field", TypeReference.Empty)); + writer.Property(new PropertyDeclarationOptions("Property", TypeReference.Empty)); + writer.MethodScope(new MethodDeclarationOptions("Method", TypeReference.Empty)).Dispose(); // Assert await Assert.That(writer.ToString()).IsEmpty(); } [Test] - public async Task WriteLine_AppendsLineWithIndent() + public async Task Line_AppendsLineWithIndent() { var writer = CodeWriterFactory.ForTests(); - writer.WriteLine("public class C"); + writer.Line("public class C"); using (writer.OpenBlockScope()) { - writer.WriteLine("public int P { get; set; }"); + writer.Line("public int P { get; set; }"); } var result = writer.ToString(); @@ -97,7 +97,7 @@ public async Task Append_AliasForWrite() } [Test] - public async Task AppendLine_AliasForWriteLine() + public async Task AppendLine_AliasForLine() { var writer = CodeWriterFactory.ForTests(); @@ -107,41 +107,41 @@ public async Task AppendLine_AliasForWriteLine() } [Test] - public async Task WriteIf_True_WritesValue() + public async Task If_True_WritesValue() { var writer = CodeWriterFactory.ForTests(); - writer.WriteIf(true, "value"); + writer.If(true, "value"); await Assert.That(writer.ToString()).IsEqualTo("value"); } [Test] - public async Task WriteIf_False_DoesNotWrite() + public async Task If_False_DoesNotWrite() { var writer = CodeWriterFactory.ForTests(); - writer.WriteIf(false, "value"); + writer.If(false, "value"); await Assert.That(writer.ToString()).IsEmpty(); } [Test] - public async Task WriteLineIf_True_WritesLine() + public async Task LineIf_True_WritesLine() { var writer = CodeWriterFactory.ForTests(); - writer.WriteLineIf(true, "value"); + writer.LineIf(true, "value"); await Assert.That(writer.ToString()).Contains("value"); } [Test] - public async Task WriteLineIf_False_DoesNotWrite() + public async Task LineIf_False_DoesNotWrite() { var writer = CodeWriterFactory.ForTests(); - writer.WriteLineIf(false, "value"); + writer.LineIf(false, "value"); await Assert.That(writer.ToString()).IsEmpty(); } @@ -162,7 +162,7 @@ public async Task EnsureBlankLine_AddsSeparatorAfterCompletedLine() { var writer = CodeWriterFactory.ForTests(); - writer.WriteMethodCall("Run").EnsureBlankLine().Comment("Explains the next member."); + writer.MethodCall("Run").EnsureBlankLine().Comment("Explains the next member."); await Assert.That(writer.ToString()).IsEqualTo("Run();\n\n// Explains the next member.\n"); } @@ -188,22 +188,22 @@ public async Task EnsureBlankLine_OnEmptyWriterDoesNotWrite() } [Test] - public async Task WriteLines_WritesMultipleLines() + public async Task Lines_WritesMultipleLines() { var writer = CodeWriterFactory.ForTests(); - writer.WriteLines(["line1", "line2"]); + writer.Lines(["line1", "line2"]); await Assert.That(writer.ToString()).Contains("line1"); await Assert.That(writer.ToString()).Contains("line2"); } [Test] - public async Task WriteDelimited_WritesItemsWithDelimiter() + public async Task Delimited_WritesItemsWithDelimiter() { var writer = CodeWriterFactory.ForTests(); - writer.WriteDelimited(["a", "b", "c"], ", "); + writer.Delimited(["a", "b", "c"], ", "); await Assert.That(writer.ToString()).IsEqualTo("a, b, c"); } @@ -213,7 +213,7 @@ public async Task Block_WithBody_WritesBodyInsideBlock() { var writer = CodeWriterFactory.ForTests(); - writer.WriteBlock("public class C", w => w.WriteLine("public int P { get; set; }")); + writer.Block("public class C", w => w.Line("public int P { get; set; }")); var result = writer.ToString(); @@ -223,23 +223,23 @@ public async Task Block_WithBody_WritesBodyInsideBlock() } [Test] - public async Task WriteUsing_WritesUsingDirective() + public async Task Using_WritesUsingDirective() { var writer = CodeWriterFactory.ForTests(); - writer.WriteUsing("System"); + writer.Using("System"); await Assert.That(writer.ToString()).IsEqualTo("using System;\n"); } [Test] - public async Task WriteBlockNamespace_WritesNamespaceBlock() + public async Task BlockNamespace_WritesNamespaceBlock() { var writer = CodeWriterFactory.ForTests(); - using (writer.WriteBlockNamespaceScope("Test")) + using (writer.BlockNamespaceScope("Test")) { - writer.WriteLine("public class C { }"); + writer.Line("public class C { }"); } var result = writer.ToString(); @@ -250,12 +250,12 @@ public async Task WriteBlockNamespace_WritesNamespaceBlock() } [Test] - public async Task WriteBlockNamespaces_GivenMultipleNamespaces_InsertsBlankLineBetweenThem() + public async Task BlockNamespaces_GivenMultipleNamespaces_InsertsBlankLineBetweenThem() { var writer = CodeWriterFactory.ForTests(); - writer.WriteBlockNamespace("First", body => body.WriteLine("class A { }")); - writer.WriteBlockNamespace("Second", body => body.WriteLine("class B { }")); + writer.BlockNamespace("First", body => body.Line("class A { }")); + writer.BlockNamespace("Second", body => body.Line("class B { }")); await Assert .That(writer.ToString()) @@ -263,34 +263,34 @@ await Assert } [Test] - public async Task WriteBlockNamespaceAndTopLevelType_InsertsBlankLineBetweenDeclarations() + public async Task BlockNamespaceAndTopLevelType_InsertsBlankLineBetweenDeclarations() { var writer = CodeWriterFactory.ForTests(); var declaration = new TypeDeclarationOptions("TopLevel"); - writer.WriteBlockNamespace("First", body => body.WriteLine("class Nested { }")); - writer.WriteClass(declaration, static _ => { }); - writer.WriteBlockNamespace("Second", body => body.WriteLine("class Other { }")); + writer.BlockNamespace("First", body => body.Line("class Nested { }")); + writer.Class(declaration, static _ => { }); + writer.BlockNamespace("Second", body => body.Line("class Other { }")); await Assert .That(writer.ToString()) .IsEqualTo( "namespace First\n{\n\tclass Nested { }\n}\n\n" + GeneratedAttributes() - + "sealed partial class TopLevel\n{\n}\n\n" + + "public sealed partial class TopLevel\n{\n}\n\n" + "namespace Second\n{\n\tclass Other { }\n}\n" ); } [Test] - public async Task WriteBlockNamespace_TypeValueObject_WritesNamespaceBlock() + public async Task BlockNamespace_TypeValueObject_WritesNamespaceBlock() { var writer = CodeWriterFactory.ForTests(); var typeValue = new TypeIdentity("C", "Test"); - using (writer.WriteBlockNamespaceScope(typeValue)) + using (writer.BlockNamespaceScope(typeValue)) { - writer.WriteLine("public class C { }"); + writer.Line("public class C { }"); } var result = writer.ToString(); @@ -301,14 +301,14 @@ public async Task WriteBlockNamespace_TypeValueObject_WritesNamespaceBlock() } [Test] - public async Task WriteBlockNamespace_TypeValueObjectWithGlobalNamespace_ReturnsNoOpScope() + public async Task BlockNamespace_TypeValueObjectWithGlobalNamespace_ReturnsNoOpScope() { var writer = CodeWriterFactory.ForTests(); var typeValue = new TypeIdentity("C", null); - using (writer.WriteBlockNamespaceScope(typeValue)) + using (writer.BlockNamespaceScope(typeValue)) { - writer.WriteLine("public class C { }"); + writer.Line("public class C { }"); } var result = writer.ToString(); @@ -318,12 +318,12 @@ public async Task WriteBlockNamespace_TypeValueObjectWithGlobalNamespace_Returns } [Test] - public async Task WriteFileScopedNamespace_TypeValueObject_WritesNamespace() + public async Task FileScopedNamespace_TypeValueObject_WritesNamespace() { var writer = CodeWriterFactory.ForTests(); var typeValue = new TypeIdentity("C", "Test"); - writer.WriteFileScopedNamespace(typeValue); + writer.FileScopedNamespace(typeValue); var result = writer.ToString(); @@ -331,12 +331,12 @@ public async Task WriteFileScopedNamespace_TypeValueObject_WritesNamespace() } [Test] - public async Task WriteFileScopedNamespace_TypeValueObjectWithGlobalNamespace_WritesNothing() + public async Task FileScopedNamespace_TypeValueObjectWithGlobalNamespace_WritesNothing() { var writer = CodeWriterFactory.ForTests(); var typeValue = new TypeIdentity("C", null); - writer.WriteFileScopedNamespace(typeValue); + writer.FileScopedNamespace(typeValue); var result = writer.ToString(); @@ -344,12 +344,12 @@ public async Task WriteFileScopedNamespace_TypeValueObjectWithGlobalNamespace_Wr } [Test] - public async Task WriteClass_WritesClassBlock() + public async Task Class_WritesClassBlock() { var writer = CodeWriterFactory.ForTests(); using ( - writer.WriteClassScope( + writer.ClassScope( new TypeDeclarationOptions("C") { Accessibility = TypeDeclarationAccessibility.Public, @@ -359,7 +359,7 @@ public async Task WriteClass_WritesClassBlock() ) ) { - writer.WriteLine("public int P { get; set; }"); + writer.Line("public int P { get; set; }"); } var result = writer.ToString(); @@ -370,7 +370,7 @@ public async Task WriteClass_WritesClassBlock() } [Test] - public async Task WriteClass_WithOptions_WritesModifiersInheritanceAndConstraints() + public async Task Class_WithOptions_WritesModifiersInheritanceAndConstraints() { var writer = CodeWriterFactory.ForTests(); var declaration = new TypeDeclarationOptions("Repository") @@ -381,9 +381,9 @@ public async Task WriteClass_WithOptions_WritesModifiersInheritanceAndConstraint GenericTypes = [new GenericTypeParameterOptions("T") { Constraints = ["class", "new()"] }], }; - using (writer.WriteClassScope(declaration)) + using (writer.ClassScope(declaration)) { - writer.WriteLine("public T Value { get; } = new();"); + writer.Line("public T Value { get; } = new();"); } await Assert @@ -399,7 +399,7 @@ await Assert } [Test] - public async Task WriteRecordStruct_WithOptions_WritesReadonlyRecordStruct() + public async Task RecordStruct_WithOptions_WritesReadonlyRecordStruct() { var writer = CodeWriterFactory.ForTests(); var declaration = new TypeDeclarationOptions("Identifier") @@ -409,7 +409,7 @@ public async Task WriteRecordStruct_WithOptions_WritesReadonlyRecordStruct() Interfaces = [Type("IEquatable").Identity.MakeGeneric(Type("Identifier"))], }; - using (writer.WriteRecordStructScope(declaration)) + using (writer.RecordStructScope(declaration)) { // Here to prevent IDE0555 } @@ -425,12 +425,13 @@ await Assert } [Test] - public async Task WriteType_WithoutAccessibility_OmitsAccessibility() + public async Task Type_WithoutAccessibility_OmitsAccessibility() { var writer = CodeWriterFactory.ForTests(); + writer.DefaultTypeAccessibility = null; using ( - writer.WriteTypeScope( + writer.TypeScope( new TypeDeclarationOptions("State") { Kind = TypeDeclarationKind.RecordClass, @@ -447,7 +448,7 @@ public async Task WriteType_WithoutAccessibility_OmitsAccessibility() } [Test] - public async Task WriteClass_GivenStaticDeclaration_WritesStaticClass() + public async Task Class_GivenStaticDeclaration_WritesStaticClass() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -458,7 +459,7 @@ public async Task WriteClass_GivenStaticDeclaration_WritesStaticClass() }; // Act - using (writer.WriteClassScope(declaration)) + using (writer.ClassScope(declaration)) { // Intentionally empty. } @@ -470,7 +471,7 @@ await Assert } [Test] - public async Task WriteClass_GivenAbstractDeclaration_WritesAbstractInsteadOfDefaultSealed() + public async Task Class_GivenAbstractDeclaration_WritesAbstractInsteadOfDefaultSealed() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -481,7 +482,7 @@ public async Task WriteClass_GivenAbstractDeclaration_WritesAbstractInsteadOfDef }; // Act - using (writer.WriteClassScope(declaration)) + using (writer.ClassScope(declaration)) { // Intentionally empty. } @@ -493,44 +494,44 @@ await Assert } [Test] - public async Task WriteStruct_GivenAbstractDeclaration_ThrowsArgumentException() + public async Task Struct_GivenAbstractDeclaration_ThrowsArgumentException() { // Arrange var writer = CodeWriterFactory.ForTests(); var declaration = new TypeDeclarationOptions("Invalid") { IsAbstract = true }; // Act - CodeWriter.BlockScope Action() => writer.WriteStructScope(declaration); + CodeWriter.BlockScope Action() => writer.StructScope(declaration); // Assert await Assert.That(Action).Throws(); } [Test] - public async Task WriteType_GivenStaticStruct_ThrowsArgumentException() + public async Task Type_GivenStaticStruct_ThrowsArgumentException() { // Arrange var writer = CodeWriterFactory.ForTests(); var declaration = new TypeDeclarationOptions("Invalid") { Kind = TypeDeclarationKind.Struct, IsStatic = true }; // Act - CodeWriter.BlockScope Action() => writer.WriteTypeScope(declaration); + CodeWriter.BlockScope Action() => writer.TypeScope(declaration); // Assert await Assert.That(Action).Throws(); } [Test] - public async Task WriteStruct_WithBaseType_Throws() + public async Task Struct_WithBaseType_Throws() { var writer = CodeWriterFactory.ForTests(); var declaration = new TypeDeclarationOptions("Invalid") { BaseType = Type("BaseType") }; - await Assert.That(() => writer.WriteStructScope(declaration)).Throws(); + await Assert.That(() => writer.StructScope(declaration)).Throws(); } [Test] - public async Task WriteClass_WithPrimaryConstructor_WritesParametersBeforeBaseType() + public async Task Class_WithPrimaryConstructor_WritesParametersBeforeBaseType() { var writer = CodeWriterFactory.ForTests(); var declaration = new TypeDeclarationOptions("Repository") @@ -540,7 +541,7 @@ public async Task WriteClass_WithPrimaryConstructor_WritesParametersBeforeBaseTy BaseType = Type("RepositoryBase(connectionString)"), }; - using (writer.WriteClassScope(declaration)) + using (writer.ClassScope(declaration)) { // To stop IDE0055 } @@ -554,20 +555,20 @@ await Assert } [Test] - public async Task WriteClass_WithEmptyBaseType_DoesNotWriteBaseListColon() + public async Task Class_WithEmptyBaseType_DoesNotWriteBaseListColon() { var writer = CodeWriterFactory.ForTests(); var declaration = new TypeDeclarationOptions("ResourceKit") { BaseType = TypeReference.Empty }; - writer.WriteClass(declaration, static _ => { }); + writer.Class(declaration, static _ => { }); await Assert .That(writer.ToString()) - .IsEqualTo(GeneratedAttributes() + "sealed partial class ResourceKit\n{\n}\n"); + .IsEqualTo(GeneratedAttributes() + "public sealed partial class ResourceKit\n{\n}\n"); } [Test] - public async Task WriteClass_WithEmptyBaseAndInterfaces_WritesOnlyNonEmptyInterfaces() + public async Task Class_WithEmptyBaseAndInterfaces_WritesOnlyNonEmptyInterfaces() { var writer = CodeWriterFactory.ForTests(); var declaration = new TypeDeclarationOptions("ResourceKit") @@ -581,15 +582,15 @@ public async Task WriteClass_WithEmptyBaseAndInterfaces_WritesOnlyNonEmptyInterf ], }; - writer.WriteClass(declaration, static _ => { }); + writer.Class(declaration, static _ => { }); await Assert .That(writer.ToString()) - .IsEqualTo(GeneratedAttributes() + "sealed partial class ResourceKit : IResourceKit\n{\n}\n"); + .IsEqualTo(GeneratedAttributes() + "public sealed partial class ResourceKit : IResourceKit\n{\n}\n"); } [Test] - public async Task WriteConstructor_WritesParametersInitializerAndBody() + public async Task Constructor_WritesParametersInitializerAndBody() { var writer = CodeWriterFactory.ForTests(); var declaration = new ConstructorDeclarationOptions("Repository") @@ -599,9 +600,9 @@ public async Task WriteConstructor_WritesParametersInitializerAndBody() Initializer = "base(connectionString)", }; - using (writer.WriteConstructorScope(declaration)) + using (writer.ConstructorScope(declaration)) { - writer.WriteLine("_logger = logger;"); + writer.Line("_logger = logger;"); } await Assert @@ -617,11 +618,11 @@ await Assert } [Test] - public async Task WriteConstructor_StaticConstructor_WritesStaticConstructor() + public async Task Constructor_StaticConstructor_WritesStaticConstructor() { var writer = CodeWriterFactory.ForTests(); - using (writer.WriteConstructorScope(new ConstructorDeclarationOptions("Repository") { IsStatic = true })) + using (writer.ConstructorScope(new ConstructorDeclarationOptions("Repository") { IsStatic = true })) { // To stop IDE0055 } @@ -630,12 +631,12 @@ public async Task WriteConstructor_StaticConstructor_WritesStaticConstructor() } [Test] - public async Task WriteMethod_GivenShortParameters_WritesSingleLineDeclaration() + public async Task Method_GivenShortParameters_WritesSingleLineDeclaration() { var writer = CodeWriterFactory.ForTests(); using ( - writer.WriteMethodScope( + writer.MethodScope( new MethodDeclarationOptions("Execute", Type("void")) { Accessibility = TypeDeclarationAccessibility.Public, @@ -645,7 +646,7 @@ public async Task WriteMethod_GivenShortParameters_WritesSingleLineDeclaration() ) ) { - writer.WriteLine("Run(name, enabled);"); + writer.Line("Run(name, enabled);"); } await Assert @@ -660,7 +661,7 @@ await Assert } [Test] - public async Task WriteInterface_WithInheritanceAndConstraints_WritesInterface() + public async Task Interface_WithInheritanceAndConstraints_WritesInterface() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -672,7 +673,7 @@ public async Task WriteInterface_WithInheritanceAndConstraints_WritesInterface() }; // Act - writer.WriteInterface(declaration, body => body.WriteLine("T Get();")); + writer.Interface(declaration, body => body.Line("T Get();")); // Assert await Assert @@ -688,7 +689,7 @@ await Assert } [Test] - public async Task WriteEnum_WithUnderlyingType_WritesEnum() + public async Task Enum_WithUnderlyingType_WritesEnum() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -699,7 +700,7 @@ public async Task WriteEnum_WithUnderlyingType_WritesEnum() }; // Act - writer.WriteEnum(declaration, body => body.WriteLine("None = 0,").WriteLine("Ready = 1,")); + writer.Enum(declaration, body => body.Line("None = 0,").Line("Ready = 1,")); // Assert await Assert @@ -711,20 +712,20 @@ await Assert } [Test] - public async Task WriteAttributeClass_WithDefaults_WritesAttributeUsageAndSystemAttributeBase() + public async Task AttributeClass_WithDefaults_WritesAttributeUsageAndSystemAttributeBase() { // Arrange var writer = CodeWriterFactory.ForTests(); // Act - writer.WriteAttributeClass( + writer.AttributeClass( new TypeDeclarationOptions("RegistryAttribute") { Accessibility = TypeDeclarationAccessibility.Public, IsPartial = false, }, AttributeTargets.Class, - body => body.WriteLine("public string? Name { get; init; }") + body => body.Line("public string? Name { get; init; }") ); // Assert @@ -742,13 +743,13 @@ await Assert } [Test] - public async Task WriteAttributeClass_WithOptions_WritesCombinedTargetsFlagsAttributesAndCustomBase() + public async Task AttributeClass_WithOptions_WritesCombinedTargetsFlagsAttributesAndCustomBase() { // Arrange var writer = CodeWriterFactory.ForTests(); // Act - writer.WriteAttributeClass( + writer.AttributeClass( new TypeDeclarationOptions("KnownTypeAttribute") { Accessibility = TypeDeclarationAccessibility.Internal, @@ -777,11 +778,11 @@ await Assert } [Test] - public async Task WriteAttributeClass_WithEmbeddedAttributeDisabled_OmitsEmbeddedAttribute() + public async Task AttributeClass_WithEmbeddedAttributeDisabled_OmitsEmbeddedAttribute() { var writer = CodeWriterFactory.ForTests(); - writer.WriteAttributeClass( + writer.AttributeClass( new TypeDeclarationOptions("LocalAttribute") { IsPartial = false, IncludeEmbeddedAttribute = false }, AttributeTargets.Class, _ => { } @@ -791,25 +792,25 @@ public async Task WriteAttributeClass_WithEmbeddedAttributeDisabled_OmitsEmbedde } [Test] - public async Task WriteAttributeClass_GivenNoTargets_ThrowsWithoutWriting() + public async Task AttributeClass_GivenNoTargets_ThrowsWithoutWriting() { var writer = CodeWriterFactory.ForTests(); await Assert - .That(() => writer.WriteAttributeClass(new("InvalidAttribute"), 0, _ => { })) + .That(() => writer.AttributeClass(new("InvalidAttribute"), 0, _ => { })) .Throws(); await Assert.That(writer.ToString()).IsEmpty(); } [Test] - public async Task WriteEnum_WithStructuredFields_WritesSummariesAttributesAndValues() + public async Task Enum_WithStructuredFields_WritesSummariesAttributesAndValues() { // Arrange var writer = CodeWriterFactory.ForTests(); var declaration = new TypeDeclarationOptions("Status") { Accessibility = TypeDeclarationAccessibility.Public }; // Act - writer.WriteEnum( + writer.Enum( declaration, new EnumFieldDeclarationOptions("None", 0) { @@ -846,18 +847,18 @@ public async Task EnumFieldDeclarationOptions_GivenMissingName_Throws(string? fi } [Test] - public async Task WriteEnumField_GivenDefaultOptions_ThrowsWithoutWriting() + public async Task EnumField_GivenDefaultOptions_ThrowsWithoutWriting() { // Arrange var writer = CodeWriterFactory.ForTests(); // Act / Assert - await Assert.That(() => writer.WriteEnumField(default)).Throws(); + await Assert.That(() => writer.EnumField(default)).Throws(); await Assert.That(writer.ToString()).IsEmpty(); } [Test] - public async Task WriteDelegate_WithGenericConstraints_WritesCompleteDeclaration() + public async Task Delegate_WithGenericConstraints_WritesCompleteDeclaration() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -874,7 +875,7 @@ public async Task WriteDelegate_WithGenericConstraints_WritesCompleteDeclaration }; // Act - writer.WriteDelegate(declaration); + writer.Delegate(declaration); // Assert await Assert @@ -886,12 +887,12 @@ await Assert } [Test] - public async Task WriteMethod_GivenLongParameters_WritesOneParameterPerLine() + public async Task Method_GivenLongParameters_WritesOneParameterPerLine() { var writer = CodeWriterFactory.ForTests(); using ( - writer.WriteMethodScope( + writer.MethodScope( new MethodDeclarationOptions( "AddAspireResourceKit", Type("global::Aspire.Hosting.IDistributedApplicationBuilder") @@ -932,7 +933,7 @@ public async Task WriteMethod_GivenLongParameters_WritesOneParameterPerLine() ) ) { - writer.WriteLine("return builder;"); + writer.Line("return builder;"); } await Assert @@ -951,7 +952,7 @@ await Assert } [Test] - public async Task WriteMethod_GivenStructuredOptions_WritesModifiersGenericsAndBody() + public async Task Method_GivenStructuredOptions_WritesModifiersGenericsAndBody() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -965,7 +966,7 @@ public async Task WriteMethod_GivenStructuredOptions_WritesModifiersGenericsAndB }; // Act - writer.WriteMethod(declaration, body => body.WriteLine("return await SaveAsync(value);")); + writer.Method(declaration, body => body.Line("return await SaveAsync(value);")); // Assert await Assert @@ -981,61 +982,61 @@ await Assert } [Test] - public async Task WriteMethodExpression_GivenExpressionBody_WritesExpressionBodiedMethod() + public async Task MethodExpression_GivenExpressionBody_WritesExpressionBodiedMethod() { // Arrange var writer = CodeWriterFactory.ForTests(); var declaration = new MethodDeclarationOptions("Count", Type("int")) { ExpressionBody = "items.Count" }; // Act - writer.WriteMethodExpression(declaration); + writer.MethodExpression(declaration); // Assert - await Assert.That(writer).Generates(GeneratedAttributes() + "int Count() => items.Count;\n"); + await Assert.That(writer).Generates(GeneratedAttributes() + "public int Count() => items.Count;\n"); } [Test] [Arguments(null)] [Arguments("")] [Arguments(" ")] - public async Task WriteMethodExpression_GivenWhitespaceExpressionBody_ThrowsWithoutWriting(string? expressionBody) + public async Task MethodExpression_GivenWhitespaceExpressionBody_ThrowsWithoutWriting(string? expressionBody) { // Arrange var writer = CodeWriterFactory.ForTests(); var declaration = new MethodDeclarationOptions("Count", Type("int")) { ExpressionBody = expressionBody }; // Act / Assert - await Assert.That(() => writer.WriteMethodExpression(declaration)).Throws(); + await Assert.That(() => writer.MethodExpression(declaration)).Throws(); await Assert.That(writer.ToString()).IsEmpty(); } [Test] - public async Task WriteMethodExpression_GivenCallback_WritesExpressionBodiedMethod() + public async Task MethodExpression_GivenCallback_WritesExpressionBodiedMethod() { // Arrange var writer = CodeWriterFactory.ForTests(); var declaration = new MethodDeclarationOptions("Count", Type("int")); // Act - writer.WriteMethodExpression(declaration, expression => expression.Write("items.Count")); + writer.MethodExpression(declaration, expression => expression.Write("items.Count")); // Assert - await Assert.That(writer).Generates(GeneratedAttributes() + "int Count() => items.Count;\n"); + await Assert.That(writer).Generates(GeneratedAttributes() + "public int Count() => items.Count;\n"); } [Test] - public async Task WriteMethodExpression_GivenNullCallback_Throws() + public async Task MethodExpression_GivenNullCallback_Throws() { // Arrange var writer = CodeWriterFactory.ForTests(); var declaration = new MethodDeclarationOptions("Count", Type("int")); // Act / Assert - await Assert.That(() => writer.WriteMethodExpression(declaration, null!)).Throws(); + await Assert.That(() => writer.MethodExpression(declaration, null!)).Throws(); } [Test] - public async Task WriteMethodExpression_GivenExpressionBodyAndCallback_ThrowsWithoutWriting() + public async Task MethodExpression_GivenExpressionBodyAndCallback_ThrowsWithoutWriting() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -1043,13 +1044,13 @@ public async Task WriteMethodExpression_GivenExpressionBodyAndCallback_ThrowsWit // Act / Assert await Assert - .That(() => writer.WriteMethodExpression(declaration, expression => expression.Write("items.Count"))) + .That(() => writer.MethodExpression(declaration, expression => expression.Write("items.Count"))) .Throws(); await Assert.That(writer.ToString()).IsEmpty(); } [Test] - public async Task WriteMethodExpression_GivenPartialDeclaration_ThrowsWithoutWriting() + public async Task MethodExpression_GivenPartialDeclaration_ThrowsWithoutWriting() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -1060,12 +1061,12 @@ public async Task WriteMethodExpression_GivenPartialDeclaration_ThrowsWithoutWri }; // Act / Assert - await Assert.That(() => writer.WriteMethodExpression(declaration)).Throws(); + await Assert.That(() => writer.MethodExpression(declaration)).Throws(); await Assert.That(writer.ToString()).IsEmpty(); } [Test] - public async Task WriteMethodExpression_GivenPartialDeclarationAndCallback_ThrowsWithoutWriting() + public async Task MethodExpression_GivenPartialDeclarationAndCallback_ThrowsWithoutWriting() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -1073,25 +1074,25 @@ public async Task WriteMethodExpression_GivenPartialDeclarationAndCallback_Throw // Act / Assert await Assert - .That(() => writer.WriteMethodExpression(declaration, expression => expression.Write("items.Count"))) + .That(() => writer.MethodExpression(declaration, expression => expression.Write("items.Count"))) .Throws(); await Assert.That(writer.ToString()).IsEmpty(); } [Test] - public async Task WritePartialMethod_GivenExpressionBody_ThrowsWithoutWriting() + public async Task PartialMethod_GivenExpressionBody_ThrowsWithoutWriting() { // Arrange var writer = CodeWriterFactory.ForTests(); var declaration = new MethodDeclarationOptions("Count", Type("int")) { ExpressionBody = "items.Count" }; // Act / Assert - await Assert.That(() => writer.WritePartialMethod(declaration)).Throws(); + await Assert.That(() => writer.PartialMethod(declaration)).Throws(); await Assert.That(writer.ToString()).IsEmpty(); } [Test] - public async Task WriteMethod_GivenExpressionBody_ThrowsWithoutWriting() + public async Task Method_GivenExpressionBody_ThrowsWithoutWriting() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -1099,29 +1100,29 @@ public async Task WriteMethod_GivenExpressionBody_ThrowsWithoutWriting() // Act / Assert await Assert - .That(() => writer.WriteMethod(declaration, body => body.WriteLine("return items.Count;"))) + .That(() => writer.Method(declaration, body => body.Line("return items.Count;"))) .Throws(); await Assert.That(writer.ToString()).IsEmpty(); } [Test] - public async Task WriteMethod_GivenBodyAndNoExpressionBody_WritesBlockBody() + public async Task Method_GivenBodyAndNoExpressionBody_WritesBlockBody() { // Arrange var writer = CodeWriterFactory.ForTests(); var declaration = new MethodDeclarationOptions("Count", Type("int")); // Act - writer.WriteMethod(declaration, body => body.WriteLine("return items.Count;")); + writer.Method(declaration, body => body.Line("return items.Count;")); // Assert await Assert .That(writer) - .Generates(GeneratedAttributes() + "int Count()\n" + "{\n" + "\treturn items.Count;\n" + "}\n"); + .Generates(GeneratedAttributes() + "public int Count()\n" + "{\n" + "\treturn items.Count;\n" + "}\n"); } [Test] - public async Task WritePartialMethod_GivenPartialMethods_WritesDeclaration() + public async Task PartialMethod_GivenPartialMethods_WritesDeclaration() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -1135,7 +1136,7 @@ public async Task WritePartialMethod_GivenPartialMethods_WritesDeclaration() }; // Act - writer.WritePartialMethod(declaration); + writer.PartialMethod(declaration); // Assert await Assert @@ -1182,7 +1183,7 @@ public async Task StructuredDeclarations_GivenAttributes_WritesTypeMemberReturnA }; // Act - writer.WriteClass(type, body => body.WriteMethod(method, methodBody => methodBody.WriteLine("throw null;"))); + writer.Class(type, body => body.Method(method, methodBody => methodBody.Line("throw null;"))); // Assert await Assert @@ -1211,7 +1212,7 @@ public async Task AttributeDeclaration_RendersRetainedTypeReferenceSyntax() .MakeGeneric(TypeIdentity.Create().MakeNullable()) .AsTypeReference(); - writer.WriteClass(new("C") { Attributes = [new(attributeType)] }, _ => { }); + writer.Class(new("C") { Attributes = [new(attributeType)] }, _ => { }); await Assert.That(writer).ContainsGenerated("[global::Example.Marker]"); } @@ -1278,7 +1279,7 @@ public async Task PublicScopeReturningMethods_HaveCallbackCounterparts() } [Test] - public async Task WriteClass_GivenAttributeTypeValueObject_DoesNotDuplicateAttributeBrackets() + public async Task Class_GivenAttributeTypeValueObject_DoesNotDuplicateAttributeBrackets() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -1290,7 +1291,7 @@ public async Task WriteClass_GivenAttributeTypeValueObject_DoesNotDuplicateAttri }; // Act - writer.WriteClassScope(new TypeDeclarationOptions("Host") { Attributes = [attribute] }).Dispose(); + writer.ClassScope(new TypeDeclarationOptions("Host") { Attributes = [attribute] }).Dispose(); // Assert await Assert @@ -1298,7 +1299,7 @@ await Assert .IsEqualTo( GeneratedAttributes() + "[global::Purview.Aspire.ResourceKit.HostKit(GenerateOptions = true)]\n" - + "sealed partial class Host\n" + + "public sealed partial class Host\n" + "{\n" + "}\n" ); @@ -1312,14 +1313,14 @@ public async Task AttributeTypeValueObject_GivenDeclarationContexts_RendersUnder var writer = CodeWriterFactory.ForTests(); // Act - writer.WriteAttributeClass( + writer.AttributeClass( new TypeDeclarationOptions(attributeType) { IsPartial = false }, AttributeTargets.Class, body => { body.XmlSummary($"Creates a {CodeWriter.XmlSee(attributeType)} instance."); - body.WriteConstructor(new ConstructorDeclarationOptions(attributeType), _ => { }); - body.WriteProperty(new PropertyDeclarationOptions("Parent", attributeType)); + body.Constructor(new ConstructorDeclarationOptions(attributeType), _ => { }); + body.Property(new PropertyDeclarationOptions("Parent", attributeType)); } ); @@ -1357,21 +1358,21 @@ public async Task TypeReference_GivenNestedNullableGenericAndArray_RendersStruct }; // Act - writer.WriteMethodScope(method); + writer.MethodScope(method); // Assert await Assert .That(writer.ToString()) .IsEqualTo( GeneratedAttributes() - + "global::System.Collections.Generic.Dictionary[]? Load(\n" + + "public global::System.Collections.Generic.Dictionary[]? Load(\n" + "\tglobal::System.Collections.Generic.List? items\n" + ") => items.ToArray();\n" ); } [Test] - public async Task WriteMethod_GivenNullableParameterOption_WritesNullableTypeOnce() + public async Task Method_GivenNullableParameterOption_WritesNullableTypeOnce() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -1385,16 +1386,16 @@ public async Task WriteMethod_GivenNullableParameterOption_WritesNullableTypeOnc }; // Act - writer.WriteMethodScope(method); + writer.MethodScope(method); // Assert await Assert .That(writer.ToString()) - .IsEqualTo(GeneratedAttributes() + "void Use(Widget? value = null) => Consume(value);\n"); + .IsEqualTo(GeneratedAttributes() + "public void Use(Widget? value = null) => Consume(value);\n"); } [Test] - public async Task WriteProperty_GivenAutoAccessorsAndInitializer_WritesProperty() + public async Task Property_GivenAutoAccessorsAndInitializer_WritesProperty() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -1407,7 +1408,7 @@ public async Task WriteProperty_GivenAutoAccessorsAndInitializer_WritesProperty( }; // Act - writer.WriteProperty(declaration); + writer.Property(declaration); // Assert await Assert @@ -1416,7 +1417,7 @@ await Assert } [Test] - public async Task WriteProperty_GivenIsInitOnlyOnly_WritesInitAccessor() + public async Task Property_GivenIsInitOnlyOnly_WritesInitAccessor() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -1428,7 +1429,7 @@ public async Task WriteProperty_GivenIsInitOnlyOnly_WritesInitAccessor() }; // Act - writer.WriteProperty(declaration); + writer.Property(declaration); // Assert await Assert @@ -1437,7 +1438,7 @@ await Assert } [Test] - public async Task WriteProperty_GivenAccessorBodies_WritesScopedAccessors() + public async Task Property_GivenAccessorBodies_WritesScopedAccessors() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -1449,11 +1450,7 @@ public async Task WriteProperty_GivenAccessorBodies_WritesScopedAccessors() }; // Act - writer.WriteProperty( - declaration, - getter => getter.WriteLine("return _value;"), - setter => setter.WriteLine("_value = value;") - ); + writer.Property(declaration, getter => getter.Line("return _value;"), setter => setter.Line("_value = value;")); // Assert await Assert @@ -1469,7 +1466,7 @@ await Assert } [Test] - public async Task WriteProperty_GivenIsInitOnlyOnlyWithAccessorBodies_WritesInitAccessor() + public async Task Property_GivenIsInitOnlyOnlyWithAccessorBodies_WritesInitAccessor() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -1480,11 +1477,7 @@ public async Task WriteProperty_GivenIsInitOnlyOnlyWithAccessorBodies_WritesInit }; // Act - writer.WriteProperty( - declaration, - getter => getter.WriteLine("return _value;"), - setter => setter.WriteLine("_value = value;") - ); + writer.Property(declaration, getter => getter.Line("return _value;"), setter => setter.Line("_value = value;")); // Assert await Assert @@ -1500,7 +1493,7 @@ await Assert } [Test] - public async Task WriteProperty_GivenExpressionBody_WritesExpressionProperty() + public async Task Property_GivenExpressionBody_WritesExpressionProperty() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -1511,21 +1504,21 @@ public async Task WriteProperty_GivenExpressionBody_WritesExpressionProperty() }; // Act - writer.WriteProperty(declaration); + writer.Property(declaration); // Assert await Assert.That(writer.ToString()).IsEqualTo(GeneratedAttributes() + "public int Count => _items.Count;\n"); } [Test] - public async Task WriteRecordStruct_GivenIsInitOnlyProperty_WritesReadonlyCompatibleProperty() + public async Task RecordStruct_GivenIsInitOnlyProperty_WritesReadonlyCompatibleProperty() { // Arrange var writer = CodeWriterFactory.ForTests(); // Act using ( - writer.WriteRecordStructScope( + writer.RecordStructScope( new TypeDeclarationOptions("Sample") { Accessibility = TypeDeclarationAccessibility.Public, @@ -1534,7 +1527,7 @@ public async Task WriteRecordStruct_GivenIsInitOnlyProperty_WritesReadonlyCompat ) ) { - writer.WriteProperty( + writer.Property( new PropertyDeclarationOptions("Name", Type("string")) { Accessibility = TypeDeclarationAccessibility.Public, @@ -1552,7 +1545,7 @@ await Assert } [Test] - public async Task WriteField_GivenReadonlyStaticField_WritesField() + public async Task Field_GivenReadonlyStaticField_WritesField() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -1565,7 +1558,7 @@ public async Task WriteField_GivenReadonlyStaticField_WritesField() }; // Act - writer.WriteField(declaration); + writer.Field(declaration); // Assert await Assert @@ -1583,17 +1576,17 @@ public async Task StructuredMembers_GivenConsecutiveFields_DoesNotAddBlankLine() // Act writer - .WriteField(new FieldDeclarationOptions("_first", Type("int"))) - .WriteField(new FieldDeclarationOptions("_second", Type("int"))); + .Field(new FieldDeclarationOptions("_first", Type("int"))) + .Field(new FieldDeclarationOptions("_second", Type("int"))); // Assert await Assert .That(writer.ToString()) .IsEqualTo( GeneratedAttributes(includeCoverageExclusion: false) - + "int _first;\n" + + "private int _first;\n" + GeneratedAttributes(includeCoverageExclusion: false) - + "int _second;\n" + + "private int _second;\n" ); } @@ -1604,8 +1597,8 @@ public async Task StructuredMembers_GivenDifferentMemberKinds_AddsBlankLine() var writer = CodeWriterFactory.ForTests(); // Act - writer.WriteField(new FieldDeclarationOptions("_value", Type("int"))); - writer.WriteProperty( + writer.Field(new FieldDeclarationOptions("_value", Type("int"))); + writer.Property( new PropertyDeclarationOptions("Value", Type("int")) { Accessibility = TypeDeclarationAccessibility.Public } ); @@ -1614,7 +1607,7 @@ await Assert .That(writer.ToString()) .IsEqualTo( GeneratedAttributes(includeCoverageExclusion: false) - + "int _value;\n" + + "private int _value;\n" + "\n" + GeneratedAttributes() + "public int Value { get; }\n" @@ -1630,13 +1623,13 @@ public async Task StructuredMembers_GivenScopedMethods_AddsBlankLineAfterScopeCl var second = new MethodDeclarationOptions("Second", Type("void")); // Act - using (writer.WriteMethodScope(first)) + using (writer.MethodScope(first)) { - writer.WriteLine("Execute();"); + writer.Line("Execute();"); } - using (writer.WriteMethodScope(second)) + using (writer.MethodScope(second)) { - writer.WriteLine("Execute();"); + writer.Line("Execute();"); } // Assert @@ -1644,10 +1637,10 @@ await Assert .That(writer.ToString()) .IsEqualTo( GeneratedAttributes() - + "void First()\n{\n\tExecute();\n}\n" + + "public void First()\n{\n\tExecute();\n}\n" + "\n" + GeneratedAttributes() - + "void Second()\n{\n\tExecute();\n}\n" + + "public void Second()\n{\n\tExecute();\n}\n" ); } @@ -1658,20 +1651,20 @@ public async Task StructuredMembers_GivenDocumentationTrivia_InsertsSeparatorBef var writer = CodeWriterFactory.ForTests(); // Act - writer.WriteField(new("_value", Type("int"))); + writer.Field(new("_value", Type("int"))); writer.XmlSummary("Gets the value."); - writer.WriteProperty(new("Value", Type("int"))); + writer.Property(new("Value", Type("int"))); // Assert await Assert .That(writer.ToString()) .IsEqualTo( GeneratedAttributes(includeCoverageExclusion: false) - + "int _value;\n" + + "private int _value;\n" + "\n" + "/// Gets the value.\n" + GeneratedAttributes() - + "int Value { get; }\n" + + "public int Value { get; }\n" ); } @@ -1682,18 +1675,18 @@ public async Task StructuredMembers_GivenExistingBlankLine_DoesNotAddAnother() var writer = CodeWriterFactory.ForTests(); // Act - writer.WriteField(new FieldDeclarationOptions("_value", Type("int"))).NewLine(); - writer.WriteProperty(new PropertyDeclarationOptions("Value", Type("int"))); + writer.Field(new FieldDeclarationOptions("_value", Type("int"))).NewLine(); + writer.Property(new PropertyDeclarationOptions("Value", Type("int"))); // Assert await Assert .That(writer.ToString()) .IsEqualTo( GeneratedAttributes(includeCoverageExclusion: false) - + "int _value;\n" + + "private int _value;\n" + "\n" + GeneratedAttributes() - + "int Value { get; }\n" + + "public int Value { get; }\n" ); } @@ -1704,14 +1697,14 @@ public async Task Block_WithBodyAndCustomSeparators_WritesDelimitedBody() var writer = CodeWriterFactory.ForTests(); // Act - writer.WriteDelimitedBlock( + writer.DelimitedBlock( "Create", "(", ");", body => { - body.Quote("value").WriteLine(","); - body.WriteLine("EmptyPath"); + body.Quote("value").Line(","); + body.Line("EmptyPath"); } ); @@ -1726,38 +1719,38 @@ public async Task Block_WithBodyLast_WritesBodyInsideCustomSeparators() var writer = CodeWriterFactory.ForTests(); // Act - writer.WriteDelimitedBlock("Create", "(", ");", body => body.Quote("value").WriteLine()); + writer.DelimitedBlock("Create", "(", ");", body => body.Quote("value").Line()); // Assert await Assert.That(writer.ToString()).IsEqualTo("Create\n(\n\t\"value\"\n);\n"); } [Test] - public async Task WriteMethodCall_WritesSimpleInvocation() + public async Task MethodCall_WritesSimpleInvocation() { var writer = CodeWriterFactory.ForTests(); - writer.WriteMethodCall("Run", "value", "cancellationToken"); + writer.MethodCall("Run", "value", "cancellationToken"); await Assert.That(writer.ToString()).IsEqualTo("Run(value, cancellationToken);\n"); } [Test] - public async Task WriteAwaitedMethodCall_WritesAwaitPrefix() + public async Task AwaitedMethodCall_WritesAwaitPrefix() { var writer = CodeWriterFactory.ForTests(); - writer.WriteAwaitedMethodCall("LoadAsync", "cancellationToken"); + writer.AwaitedMethodCall("LoadAsync", "cancellationToken"); await Assert.That(writer.ToString()).IsEqualTo("await LoadAsync(cancellationToken);\n"); } [Test] - public async Task WriteAwaitedMethodCall_WithStructuredArguments_WritesReceiverAndModifiers() + public async Task AwaitedMethodCall_WithStructuredArguments_WritesReceiverAndModifiers() { var writer = CodeWriterFactory.ForTests(); - writer.WriteAwaitedMethodCall( + writer.AwaitedMethodCall( "LoadAsync", new MethodCallArgumentOptions[] { @@ -1772,11 +1765,11 @@ public async Task WriteAwaitedMethodCall_WithStructuredArguments_WritesReceiverA } [Test] - public async Task WriteMethodCall_WritesReceiverGenericArgumentsAndMultilineArguments() + public async Task MethodCall_WritesReceiverGenericArgumentsAndMultilineArguments() { var writer = CodeWriterFactory.ForTests(); - writer.WriteMethodCall( + writer.MethodCall( "Create", ["firstArgumentWithANameThatMakesTheCallLong", "secondArgumentWithANameThatMakesTheCallLong"], receiver: "factory", @@ -1794,11 +1787,11 @@ await Assert } [Test] - public async Task WriteMethodCall_WithStructuredArguments_WritesModifiersAndMultilineArguments() + public async Task MethodCall_WithStructuredArguments_WritesModifiersAndMultilineArguments() { var writer = CodeWriterFactory.ForTests(); - writer.WriteMethodCall( + writer.MethodCall( "AMethodCallWithLotsOfParams", new MethodCallArgumentOptions[] { @@ -1814,17 +1807,17 @@ await Assert } [Test] - public async Task WriteMethodCall_WithStructuredArgument_WritesNamedArgument() + public async Task MethodCall_WithStructuredArgument_WritesNamedArgument() { var writer = CodeWriterFactory.ForTests(); - writer.WriteMethodCall("Configure", new MethodCallArgumentOptions[] { new("value") { Name = "option" } }); + writer.MethodCall("Configure", new MethodCallArgumentOptions[] { new("value") { Name = "option" } }); await Assert.That(writer.ToString()).IsEqualTo("Configure(option: value);\n"); } [Test] - public async Task WriteAssignment_WithObjectCreationOptions_WritesOptionalVarAndMixedArguments() + public async Task Assignment_WithObjectCreationOptions_WritesOptionalVarAndMixedArguments() { var writer = CodeWriterFactory.ForTests(); var creation = new ObjectCreationOptions( @@ -1836,8 +1829,8 @@ public async Task WriteAssignment_WithObjectCreationOptions_WritesOptionalVarAnd WriteArgumentsOnSeparateLines = true, }; - writer.WriteAssignment("var", "@event", creation); - writer.WriteAssignment("existingEvent", creation); + writer.Assignment("var", "@event", creation); + writer.Assignment("existingEvent", creation); await Assert .That(writer.ToString()) @@ -1920,11 +1913,11 @@ await Assert } [Test] - public async Task WriteAutoGeneratedHeader_WritesHeader() + public async Task AutoGeneratedHeader_WritesHeader() { var writer = CodeWriterFactory.ForTests(); - writer.WriteAutoGeneratedHeader("TestGenerator", "1.0"); + writer.AutoGeneratedHeader("TestGenerator", "1.0"); var result = writer.ToString(); @@ -1935,17 +1928,17 @@ public async Task WriteAutoGeneratedHeader_WritesHeader() } [Test] - public async Task WriteAutoGeneratedHeader_GivenDefaultSettings_WritesNullableEnableDirective() + public async Task AutoGeneratedHeader_GivenDefaultSettings_WritesNullableEnableDirective() { var writer = CodeWriterFactory.ForTests(); - writer.WriteAutoGeneratedHeader(); + writer.AutoGeneratedHeader(); await Assert.That(writer.ToString()).Contains("#nullable enable"); } [Test] - public async Task WriteAutoGeneratedHeader_GivenNullableDirectiveDisable_OmitsDirective() + public async Task AutoGeneratedHeader_GivenNullableDirectiveDisable_OmitsDirective() { var writer = CodeWriterFactory.ForTests( settings: new GenerationSettings("TestGenerator", "1.0.0") @@ -1954,13 +1947,13 @@ public async Task WriteAutoGeneratedHeader_GivenNullableDirectiveDisable_OmitsDi } ); - writer.WriteAutoGeneratedHeader(); + writer.AutoGeneratedHeader(); await Assert.That(writer.ToString()).DoesNotContain("#nullable enable"); } [Test] - public async Task WriteAutoGeneratedHeader_GivenNullableDirectiveAlways_WritesDirective() + public async Task AutoGeneratedHeader_GivenNullableDirectiveAlways_WritesDirective() { var writer = CodeWriterFactory.ForTests( settings: new GenerationSettings("TestGenerator", "1.0.0") @@ -1969,37 +1962,37 @@ public async Task WriteAutoGeneratedHeader_GivenNullableDirectiveAlways_WritesDi } ); - writer.WriteAutoGeneratedHeader(); + writer.AutoGeneratedHeader(); await Assert.That(writer.ToString()).Contains("#nullable enable"); } [Test] - public async Task WriteAutoGeneratedHeader_GivenAutoAndNullableEnabled_WritesDirective() + public async Task AutoGeneratedHeader_GivenAutoAndNullableEnabled_WritesDirective() { var writer = CodeWriterFactory.ForTests( settings: new GenerationSettings("TestGenerator", "1.0.0") { IsNullableContextEnabled = true } ); - writer.WriteAutoGeneratedHeader(); + writer.AutoGeneratedHeader(); await Assert.That(writer.ToString()).Contains("#nullable enable"); } [Test] - public async Task WriteAutoGeneratedHeader_GivenAutoAndNullableDisabled_OmitsDirective() + public async Task AutoGeneratedHeader_GivenAutoAndNullableDisabled_OmitsDirective() { var writer = CodeWriterFactory.ForTests( settings: new GenerationSettings("TestGenerator", "1.0.0") { IsNullableContextEnabled = false } ); - writer.WriteAutoGeneratedHeader(); + writer.AutoGeneratedHeader(); await Assert.That(writer.ToString()).DoesNotContain("#nullable enable"); } [Test] - public async Task WriteAutoGeneratedHeader_GivenParameterOverride_OverridesSettings() + public async Task AutoGeneratedHeader_GivenParameterOverride_OverridesSettings() { var writer = CodeWriterFactory.ForTests( settings: new GenerationSettings("TestGenerator", "1.0.0") @@ -2008,13 +2001,13 @@ public async Task WriteAutoGeneratedHeader_GivenParameterOverride_OverridesSetti } ); - writer.WriteAutoGeneratedHeader(nullableDirective: NullableDirectiveMode.Always); + writer.AutoGeneratedHeader(nullableDirective: NullableDirectiveMode.Always); await Assert.That(writer.ToString()).Contains("#nullable enable"); } [Test] - public async Task WriteAutoGeneratedHeader_GivenDisabledDirective_WritesExactHeaderWithoutDirective() + public async Task AutoGeneratedHeader_GivenDisabledDirective_WritesExactHeaderWithoutDirective() { var writer = CodeWriterFactory.ForTests( settings: new GenerationSettings("TestGenerator", "1.0.0") @@ -2023,7 +2016,7 @@ public async Task WriteAutoGeneratedHeader_GivenDisabledDirective_WritesExactHea } ); - writer.WriteAutoGeneratedHeader("TestGenerator", "1.0"); + writer.AutoGeneratedHeader("TestGenerator", "1.0"); await Assert .That(writer.ToString()) @@ -2036,7 +2029,7 @@ await Assert } [Test] - public async Task WriteAutoGeneratedHeader_GivenEnabledDirective_WritesExactHeaderWithDirective() + public async Task AutoGeneratedHeader_GivenEnabledDirective_WritesExactHeaderWithDirective() { var writer = CodeWriterFactory.ForTests( settings: new GenerationSettings("TestGenerator", "1.0.0") @@ -2045,7 +2038,7 @@ public async Task WriteAutoGeneratedHeader_GivenEnabledDirective_WritesExactHead } ); - writer.WriteAutoGeneratedHeader("TestGenerator", "1.0"); + writer.AutoGeneratedHeader("TestGenerator", "1.0"); await Assert .That(writer.ToString()) @@ -2060,38 +2053,38 @@ await Assert } [Test] - public async Task WriteType_GivenNullableDisabledContext_StripsReferenceAnnotations() + public async Task Type_GivenNullableDisabledContext_StripsReferenceAnnotations() { var writer = new CodeWriter( new GenerationSettings("TestGenerator", "1.0.0") { IsNullableContextEnabled = false } ); - writer.WriteType(TypeIdentity.Create().MakeNullable()); + writer.Type(TypeIdentity.Create().MakeNullable()); writer.Write(" "); - writer.WriteType(TypeIdentity.Create().MakeNullable()); + writer.Type(TypeIdentity.Create().MakeNullable()); writer.Write(" "); - writer.WriteType(TypeIdentity.Create().MakeNullable().MakeArray()); + writer.Type(TypeIdentity.Create().MakeNullable().MakeArray()); await Assert.That(writer.ToString()).IsEqualTo("string int? string[]"); } [Test] - public async Task WriteType_GivenNullableEnabledOrUnknownContext_KeepsAnnotations() + public async Task Type_GivenNullableEnabledOrUnknownContext_KeepsAnnotations() { var enabled = new CodeWriter( new GenerationSettings("TestGenerator", "1.0.0") { IsNullableContextEnabled = true } ); var unknown = new CodeWriter(new GenerationSettings("TestGenerator", "1.0.0")); - enabled.WriteType(TypeIdentity.Create().MakeNullable()); - unknown.WriteType(TypeIdentity.Create().MakeNullable()); + enabled.Type(TypeIdentity.Create().MakeNullable()); + unknown.Type(TypeIdentity.Create().MakeNullable()); await Assert.That(enabled.ToString()).IsEqualTo("string?"); await Assert.That(unknown.ToString()).IsEqualTo("string?"); } [Test] - public async Task WriteType_GivenAlwaysModeAndDisabledContext_KeepsAnnotations() + public async Task Type_GivenAlwaysModeAndDisabledContext_KeepsAnnotations() { var writer = new CodeWriter( new GenerationSettings("TestGenerator", "1.0.0") @@ -2101,15 +2094,15 @@ public async Task WriteType_GivenAlwaysModeAndDisabledContext_KeepsAnnotations() } ); - writer.WriteType(TypeIdentity.Create().MakeNullable()); + writer.Type(TypeIdentity.Create().MakeNullable()); writer.Write(" "); - writer.WriteType(TypeIdentity.Create().MakeNullable()); + writer.Type(TypeIdentity.Create().MakeNullable()); await Assert.That(writer.ToString()).IsEqualTo("string? int?"); } [Test] - public async Task WriteType_GivenDisableModeAndEnabledContext_StripsReferenceAnnotations() + public async Task Type_GivenDisableModeAndEnabledContext_StripsReferenceAnnotations() { var writer = new CodeWriter( new GenerationSettings("TestGenerator", "1.0.0") @@ -2119,15 +2112,15 @@ public async Task WriteType_GivenDisableModeAndEnabledContext_StripsReferenceAnn } ); - writer.WriteType(TypeIdentity.Create().MakeNullable()); + writer.Type(TypeIdentity.Create().MakeNullable()); writer.Write(" "); - writer.WriteType(TypeIdentity.Create().MakeNullable()); + writer.Type(TypeIdentity.Create().MakeNullable()); await Assert.That(writer.ToString()).IsEqualTo("string int?"); } [Test] - public async Task WriteAutoGeneratedHeader_GivenAlwaysModeAndDisabledContext_WritesDirective() + public async Task AutoGeneratedHeader_GivenAlwaysModeAndDisabledContext_WritesDirective() { var writer = new CodeWriter( new GenerationSettings("TestGenerator", "1.0.0") @@ -2137,13 +2130,13 @@ public async Task WriteAutoGeneratedHeader_GivenAlwaysModeAndDisabledContext_Wri } ); - writer.WriteAutoGeneratedHeader(); + writer.AutoGeneratedHeader(); await Assert.That(writer.ToString()).Contains("#nullable enable"); } [Test] - public async Task WriteAutoGeneratedHeader_GivenDisableModeAndEnabledContext_OmitsDirective() + public async Task AutoGeneratedHeader_GivenDisableModeAndEnabledContext_OmitsDirective() { var writer = new CodeWriter( new GenerationSettings("TestGenerator", "1.0.0") @@ -2153,17 +2146,17 @@ public async Task WriteAutoGeneratedHeader_GivenDisableModeAndEnabledContext_Omi } ); - writer.WriteAutoGeneratedHeader(); + writer.AutoGeneratedHeader(); await Assert.That(writer.ToString()).DoesNotContain("#nullable enable"); } [Test] - public async Task WriteType_GivenNullReference_Throws() + public async Task Type_GivenNullReference_Throws() { var writer = CodeWriterFactory.ForTests(); - await Assert.That(() => writer.WriteType(null!)).Throws(); + await Assert.That(() => writer.Type(null!)).Throws(); } [Test] @@ -2171,10 +2164,10 @@ public async Task GeneratorIdentity_GivenNoHeaderArguments_UsesDefaultsAndDecora { var writer = new CodeWriter(new("HostKitGenerator", "2.3.4"), throwOnUnclosedScopes: false); - writer.WriteAutoGeneratedHeader(); - writer.WriteClass( + writer.AutoGeneratedHeader(); + writer.Class( new TypeDeclarationOptions("GeneratedType"), - body => body.WriteProperty(new PropertyDeclarationOptions("Value", TypeIdentity.Create())) + body => body.Property(new PropertyDeclarationOptions("Value", TypeIdentity.Create())) ); var result = writer.ToString(); @@ -2189,7 +2182,7 @@ await Assert } [Test] - public async Task WriteConstructor_WithMultilineParameters_WritesInitializerOnNewLine() + public async Task Constructor_WithMultilineParameters_WritesInitializerOnNewLine() { var writer = CodeWriterFactory.ForTests(); var declaration = new ConstructorDeclarationOptions("Repository") @@ -2200,7 +2193,7 @@ public async Task WriteConstructor_WithMultilineParameters_WritesInitializerOnNe Initializer = "this(connectionString, logger, true)", }; - writer.WriteConstructor(declaration, static _ => { }); + writer.Constructor(declaration, static _ => { }); await Assert .That(writer.ToString()) @@ -2221,7 +2214,7 @@ public async Task GeneratorIdentity_GivenConstField_DoesNotWriteInvalidCoverageA { var writer = new CodeWriter(new("HostKitGenerator", "2.3.4"), throwOnUnclosedScopes: false); - writer.WriteField( + writer.Field( new FieldDeclarationOptions("SectionName", TypeIdentity.Create()) { Accessibility = TypeDeclarationAccessibility.Public, @@ -2237,11 +2230,11 @@ public async Task GeneratorIdentity_GivenConstField_DoesNotWriteInvalidCoverageA } [Test] - public async Task WriteGeneratedCodeAttribute_WritesAttribute() + public async Task GeneratedCodeAttribute_WritesAttribute() { var writer = CodeWriterFactory.ForTests(); - writer.WriteGeneratedCodeAttribute("TestGenerator", "1.0.0.0"); + writer.GeneratedCodeAttribute("TestGenerator", "1.0.0.0"); var result = writer.ToString(); @@ -2257,16 +2250,16 @@ public async Task Determinism_SameInputProducesIdenticalOutputAndNoTimestamp() static string Generate() { var writer = CodeWriterFactory.ForTests(); - writer.WriteAutoGeneratedHeader(); - writer.WriteClass( + writer.AutoGeneratedHeader(); + writer.Class( new TypeDeclarationOptions("Sample") { Accessibility = TypeDeclarationAccessibility.Public }, body => - body.WriteMethod( + body.Method( new MethodDeclarationOptions("M", Type("void")) { Accessibility = TypeDeclarationAccessibility.Public, }, - methodBody => methodBody.WriteLine("return;") + methodBody => methodBody.Line("return;") ) ); return writer.ToString(); @@ -2302,12 +2295,12 @@ public async Task PragmaScope_EmitsDisableAndRestore() var writer = CodeWriterFactory.ForTests(); // Act - writer.WriteLine("// before"); + writer.Line("// before"); using (writer.OpenPragmasScope("CS0618", "CS1591")) { - writer.WriteLine("// inside"); + writer.Line("// inside"); } - writer.WriteLine("// after"); + writer.Line("// after"); // Assert await Assert @@ -2349,7 +2342,7 @@ public async Task MemberDeclarations_GivenIncludeGeneratedAttributesFalse_OmitsG var writer = CodeWriterFactory.ForTests(); // Act - writer.WriteClass( + writer.Class( new TypeDeclarationOptions("Sample") { Accessibility = TypeDeclarationAccessibility.Public, @@ -2357,19 +2350,17 @@ public async Task MemberDeclarations_GivenIncludeGeneratedAttributesFalse_OmitsG }, body => { - body.WriteField( - new FieldDeclarationOptions("_field", Type("int")) { IncludeGeneratedAttributes = false } - ); - body.WriteProperty( + body.Field(new FieldDeclarationOptions("_field", Type("int")) { IncludeGeneratedAttributes = false }); + body.Property( new PropertyDeclarationOptions("Property", Type("int")) { IncludeGeneratedAttributes = false } ); - body.WriteMethod( + body.Method( new MethodDeclarationOptions("Method", Type("void")) { IncludeGeneratedAttributes = false }, - methodBody => methodBody.WriteLine("return;") + methodBody => methodBody.Line("return;") ); - body.WriteConstructor( + body.Constructor( new ConstructorDeclarationOptions("Sample") { IncludeGeneratedAttributes = false }, - constructorBody => constructorBody.WriteLine("// ctor") + constructorBody => constructorBody.Line("// ctor") ); } ); @@ -2393,7 +2384,7 @@ public async Task ClassDeclaration_GivenDefaultCodeWriterAndNoOverride_EmitsGene var writer = CodeWriterFactory.ForTests(); // Act - writer.WriteClass(new("Sample", TypeDeclarationAccessibility.Public), body => body.Comment("Empty")); + writer.Class(new("Sample", TypeDeclarationAccessibility.Public), body => body.Comment("Empty")); // Assert var result = writer.ToString(); @@ -2411,7 +2402,7 @@ public async Task ClassDeclaration_GivenDefaultIncludeGeneratedAttributesFalseAn writer.DefaultIncludeGeneratedAttributes = false; // Act - writer.WriteClass( + writer.Class( new TypeDeclarationOptions("Sample") { Accessibility = TypeDeclarationAccessibility.Public }, body => body.Comment("Empty") ); @@ -2432,7 +2423,7 @@ public async Task ClassDeclaration_GivenDefaultIncludeGeneratedAttributesFalseAn writer.DefaultIncludeGeneratedAttributes = false; // Act - writer.WriteClass( + writer.Class( new TypeDeclarationOptions("Sample") { Accessibility = TypeDeclarationAccessibility.Public, @@ -2457,19 +2448,19 @@ public async Task MemberDeclarations_GivenDefaultIncludeGeneratedAttributesFalse writer.DefaultIncludeGeneratedAttributes = false; // Act - writer.WriteClass( + writer.Class( new TypeDeclarationOptions("Sample") { Accessibility = TypeDeclarationAccessibility.Public }, body => { - body.WriteField(new FieldDeclarationOptions("_field", Type("int"))); - body.WriteProperty(new PropertyDeclarationOptions("Property", Type("int"))); - body.WriteMethod( + body.Field(new FieldDeclarationOptions("_field", Type("int"))); + body.Property(new PropertyDeclarationOptions("Property", Type("int"))); + body.Method( new MethodDeclarationOptions("Method", Type("void")), - methodBody => methodBody.WriteLine("return;") + methodBody => methodBody.Line("return;") ); - body.WriteConstructor( + body.Constructor( new ConstructorDeclarationOptions("Sample"), - constructorBody => constructorBody.WriteLine("// ctor") + constructorBody => constructorBody.Line("// ctor") ); } ); @@ -2507,21 +2498,21 @@ public async Task CreateTestWriter_WithTrueParameters_SetsDefaultIncludeGenerate } [Test] - public async Task WriteIfBlock_WritesSingleLineConditionAndScopedBody() + public async Task IfBlock_WritesSingleLineConditionAndScopedBody() { var writer = CodeWriterFactory.ForTests(); - writer.WriteIfBlock("enabled", body => body.WriteReturn()); + writer.IfBlock("enabled", body => body.Return()); await Assert.That(writer.ToString()).IsEqualTo("if (enabled)\n{\n\treturn;\n}\n"); } [Test] - public async Task WriteIfBlock_WritesMultilineConditionWithContinuationIndent() + public async Task IfBlock_WritesMultilineConditionWithContinuationIndent() { var writer = CodeWriterFactory.ForTests(); - writer.WriteIfBlock("value != null\n&& value.IsValid", body => body.WriteReturn("value")); + writer.IfBlock("value != null\n&& value.IsValid", body => body.Return("value")); await Assert .That(writer.ToString()) @@ -2529,22 +2520,22 @@ await Assert } [Test] - public async Task WriteAssignment_WritesDeclarationAndMultilineInitializer() + public async Task Assignment_WritesDeclarationAndMultilineInitializer() { var writer = CodeWriterFactory.ForTests(); - writer.WriteAssignment( + writer.Assignment( "var value", value => { - value.WriteLine("new()"); + value.Line("new()"); value.OpenBlock( null, block => { - block.WriteLine("X = 1,"); - block.WriteLine("Y = 2,"); - block.WriteLine("Z = 3"); + block.Line("X = 1,"); + block.Line("Y = 2,"); + block.Line("Z = 3"); } ); } @@ -2556,12 +2547,12 @@ await Assert } [Test] - public async Task WriteReturnAndThrow_WritesStatements() + public async Task ReturnAndThrow_WritesStatements() { var writer = CodeWriterFactory.ForTests(); - writer.WriteReturn("value"); - writer.WriteThrow(throwExpression => throwExpression.WriteLine("new InvalidOperationException()")); + writer.Return("value"); + writer.Throw(throwExpression => throwExpression.Line("new InvalidOperationException()")); await Assert.That(writer.ToString()).IsEqualTo("return value;\nthrow new InvalidOperationException();\n"); } @@ -2571,36 +2562,36 @@ public async Task ExpressionMembers_WritesMultilineExpressions() { var writer = CodeWriterFactory.ForTests(); - writer.WriteMethodScope( + writer.MethodScope( new MethodDeclarationOptions("Load", Type("Value")) { ExpressionBody = "Create()\n.Configure()" } ); - writer.WritePropertyExpression( + writer.PropertyExpression( new PropertyDeclarationOptions("Current", Type("Value")), - property => property.WriteLine("GetCurrent()") + property => property.Line("GetCurrent()") ); await Assert .That(writer.ToString()) .IsEqualTo( GeneratedAttributes() - + "Value Load() => Create()\n\t.Configure();\n" + + "public Value Load() => Create()\n\t.Configure();\n" + "\n" + GeneratedAttributes() - + "Value Current => GetCurrent();\n" + + "public Value Current => GetCurrent();\n" ); } [Test] - public async Task WriteMethod_WithPartialDeclarationWithBody_WritesBodyOutsideMethod() + public async Task Method_WithPartialDeclarationWithBody_WritesBodyOutsideMethod() { // Arrange var writer = CodeWriterFactory.ForTests(); writer.DefaultIncludeGeneratedAttributes = false; // Act - writer.WriteClass( + writer.Class( new("Example") { IsPartial = true }, - body => body.WriteMethod(new("Apply") { IsPartial = true }, methodBody => methodBody.WriteReturn()) + body => body.Method(new("Apply") { IsPartial = true }, methodBody => methodBody.Return()) ); // Assert @@ -2625,7 +2616,7 @@ public async Task ModernizationOptions_AreValueTypes() } [Test] - public async Task WriteOperator_GivenBlockBody_WritesAccessibilityStaticTokenAndParameters() + public async Task Operator_GivenBlockBody_WritesAccessibilityStaticTokenAndParameters() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -2640,7 +2631,7 @@ public async Task WriteOperator_GivenBlockBody_WritesAccessibilityStaticTokenAnd }; // Act - writer.WriteOperator(declaration, body => body.WriteLine("return left.Equals(right);")); + writer.Operator(declaration, body => body.Line("return left.Equals(right);")); // Assert await Assert @@ -2655,7 +2646,7 @@ await Assert } [Test] - public async Task WriteOperator_GivenExpressionBody_WritesExpressionAndBalancesScope() + public async Task Operator_GivenExpressionBody_WritesExpressionAndBalancesScope() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -2671,7 +2662,7 @@ public async Task WriteOperator_GivenExpressionBody_WritesExpressionAndBalancesS }; // Act - using (writer.WriteOperatorScope(declaration)) + using (writer.OperatorScope(declaration)) { // Intentionally empty: an expression-bodied operator returns an empty scope. } @@ -2687,7 +2678,7 @@ await Assert } [Test] - public async Task WriteOperatorScope_GivenBlockBody_TracksOpenScopeCount() + public async Task OperatorScope_GivenBlockBody_TracksOpenScopeCount() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -2699,9 +2690,9 @@ public async Task WriteOperatorScope_GivenBlockBody_TracksOpenScopeCount() ); // Act - using (writer.WriteOperatorScope(declaration)) + using (writer.OperatorScope(declaration)) { - writer.WriteLine("return left.Equals(right);"); + writer.Line("return left.Equals(right);"); await Assert.That(writer.OpenScopeCount).IsEqualTo(1); } @@ -2710,7 +2701,7 @@ public async Task WriteOperatorScope_GivenBlockBody_TracksOpenScopeCount() } [Test] - public async Task WriteOperator_GivenNullBody_Throws() + public async Task Operator_GivenNullBody_Throws() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -2722,11 +2713,11 @@ public async Task WriteOperator_GivenNullBody_Throws() ); // Act / Assert - await Assert.That(() => writer.WriteOperator(declaration, null!)).Throws(); + await Assert.That(() => writer.Operator(declaration, null!)).Throws(); } [Test] - public async Task WriteOperator_GivenExpressionBodyAndCallback_ThrowsWithoutWriting() + public async Task Operator_GivenExpressionBodyAndCallback_ThrowsWithoutWriting() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -2741,12 +2732,12 @@ public async Task WriteOperator_GivenExpressionBodyAndCallback_ThrowsWithoutWrit }; // Act / Assert - await Assert.That(() => writer.WriteOperator(declaration, _ => { })).Throws(); + await Assert.That(() => writer.Operator(declaration, _ => { })).Throws(); await Assert.That(writer.ToString()).IsEmpty(); } [Test] - public async Task WritePartialMethod_GivenIsReadOnly_WritesReadonlyModifier() + public async Task PartialMethod_GivenIsReadOnly_WritesReadonlyModifier() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -2763,45 +2754,45 @@ public async Task WritePartialMethod_GivenIsReadOnly_WritesReadonlyModifier() }; // Act - writer.WritePartialMethod(declaration); + writer.PartialMethod(declaration); // Assert await Assert .That(writer.ToString()) .IsEqualTo( GeneratedAttributes() - + "readonly partial void OnValidate(global::System.Guid id, string? displayName, bool isActive);\n" + + "public readonly partial void OnValidate(global::System.Guid id, string? displayName, bool isActive);\n" ); } [Test] - public async Task WritePartialMethod_GivenIsReadOnlyFalse_OmitsReadonlyModifier() + public async Task PartialMethod_GivenIsReadOnlyFalse_OmitsReadonlyModifier() { // Arrange var writer = CodeWriterFactory.ForTests(); var declaration = new MethodDeclarationOptions("Apply", Type("void")) { IsPartial = true }; // Act - writer.WritePartialMethod(declaration); + writer.PartialMethod(declaration); // Assert - await Assert.That(writer.ToString()).IsEqualTo(GeneratedAttributes() + "partial void Apply();\n"); + await Assert.That(writer.ToString()).IsEqualTo(GeneratedAttributes() + "public partial void Apply();\n"); } [Test] - public async Task WriteMethod_GivenIsReadOnlyAndIsStatic_ThrowsWithoutWriting() + public async Task Method_GivenIsReadOnlyAndIsStatic_ThrowsWithoutWriting() { // Arrange var writer = CodeWriterFactory.ForTests(); var declaration = new MethodDeclarationOptions("Invalid", Type("void")) { IsReadOnly = true, IsStatic = true }; // Act / Assert - await Assert.That(() => writer.WriteMethodScope(declaration)).Throws(); + await Assert.That(() => writer.MethodScope(declaration)).Throws(); await Assert.That(writer.ToString()).IsEmpty(); } [Test] - public async Task WriteAssignment_WithObjectInitializerMembers_WritesBlockInitializer() + public async Task Assignment_WithObjectInitializerMembers_WritesBlockInitializer() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -2815,7 +2806,7 @@ public async Task WriteAssignment_WithObjectInitializerMembers_WritesBlockInitia }; // Act - writer.WriteAssignment("var", "aggregate", creation); + writer.Assignment("var", "aggregate", creation); // Assert await Assert @@ -2830,7 +2821,7 @@ await Assert } [Test] - public async Task WriteAssignment_WithObjectInitializerMembersAndConstructorArguments_WritesArgumentsBeforeInitializer() + public async Task Assignment_WithObjectInitializerMembersAndConstructorArguments_WritesArgumentsBeforeInitializer() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -2844,7 +2835,7 @@ public async Task WriteAssignment_WithObjectInitializerMembersAndConstructorArgu }; // Act - writer.WriteAssignment("var", "@event", creation); + writer.Assignment("var", "@event", creation); // Assert await Assert @@ -2859,7 +2850,7 @@ await Assert } [Test] - public async Task WriteAssignment_WithInlineInitializerMembers_WritesSingleLineInitializer() + public async Task Assignment_WithInlineInitializerMembers_WritesSingleLineInitializer() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -2870,42 +2861,42 @@ public async Task WriteAssignment_WithInlineInitializerMembers_WritesSingleLineI }; // Act - writer.WriteAssignment("var order", creation); + writer.Assignment("var order", creation); // Assert await Assert.That(writer.ToString()).IsEqualTo("var order = new Order { A = 1, B = 2, };\n"); } [Test] - public async Task WriteAssignment_WithEmptyInitializerMembers_RendersAsToday() + public async Task Assignment_WithEmptyInitializerMembers_RendersAsToday() { // Arrange var writer = CodeWriterFactory.ForTests(); var creation = new ObjectCreationOptions(Type("Order")); // Act - writer.WriteAssignment("var order", creation); + writer.Assignment("var order", creation); // Assert await Assert.That(writer.ToString()).IsEqualTo("var order = new Order();\n"); } [Test] - public async Task WriteAssignment_WithEmptyArgumentsAndForceNotNull_WritesBangAfterParentheses() + public async Task Assignment_WithEmptyArgumentsAndForceNotNull_WritesBangAfterParentheses() { // Arrange var writer = CodeWriterFactory.ForTests(); var creation = new ObjectCreationOptions(Type("Order")); // Act - writer.WriteAssignment("var order", creation, forceNotNull: true); + writer.Assignment("var order", creation, forceNotNull: true); // Assert await Assert.That(writer.ToString()).IsEqualTo("var order = new Order()!;\n"); } [Test] - public async Task WriteAssignment_WithInitializerMembersAndForceNotNull_WritesBangAfterBrace() + public async Task Assignment_WithInitializerMembersAndForceNotNull_WritesBangAfterBrace() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -2916,14 +2907,14 @@ public async Task WriteAssignment_WithInitializerMembersAndForceNotNull_WritesBa }; // Act - writer.WriteAssignment("var order", creation, forceNotNull: true); + writer.Assignment("var order", creation, forceNotNull: true); // Assert await Assert.That(writer.ToString()).IsEqualTo("var order = new Order { A = 1, }!;\n"); } [Test] - public async Task WriteAssignment_WithMultilineArgumentsAndInitializerMembers_WritesClosingBraceAndSemicolon() + public async Task Assignment_WithMultilineArgumentsAndInitializerMembers_WritesClosingBraceAndSemicolon() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -2934,7 +2925,7 @@ public async Task WriteAssignment_WithMultilineArgumentsAndInitializerMembers_Wr }; // Act - writer.WriteAssignment("var order", creation); + writer.Assignment("var order", creation); // Assert await Assert @@ -2950,13 +2941,109 @@ await Assert } [Test] - public async Task WriteThrow_GivenExceptionTypeAndMessage_WritesThrow() + public async Task Return_WithObjectInitializerMembers_WritesBlockInitializer() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + var creation = new ObjectCreationOptions(Type("global::Testing.OrderAggregateJsonModel")) + { + InitializerMembers = [new("Details", "Details"), new("CustomerId", "CustomerId")], + }; + + // Act + writer.Return(creation); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo( + "return new global::Testing.OrderAggregateJsonModel\n" + + "{\n" + + "\tDetails = Details,\n" + + "\tCustomerId = CustomerId,\n" + + "};\n" + ); + } + + [Test] + public async Task Return_WithConstructorArgumentsAndInitializerMembers_WritesArgumentsBeforeInitializer() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + var creation = new ObjectCreationOptions(Type("Order"), "customerId", "total") + { + InitializerMembers = [new("CustomerId", "customerId"), new("Total", "total")], + }; + + // Act + writer.Return(creation); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo( + "return new Order(customerId, total)\n" + + "{\n" + + "\tCustomerId = customerId,\n" + + "\tTotal = total,\n" + + "};\n" + ); + } + + [Test] + public async Task Return_WithInlineInitializerMembers_WritesSingleLineInitializer() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + var creation = new ObjectCreationOptions(Type("Order")) + { + InitializerMembers = [new("A", "1"), new("B", "2")], + WriteInitializerMembersOnSeparateLines = false, + }; + + // Act + writer.Return(creation); + + // Assert + await Assert.That(writer.ToString()).IsEqualTo("return new Order { A = 1, B = 2, };\n"); + } + + [Test] + public async Task Return_WithoutArgumentsOrInitializer_WritesEmptyConstruction() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + var creation = new ObjectCreationOptions(Type("Order")); + + // Act + writer.Return(creation); + + // Assert + await Assert.That(writer.ToString()).IsEqualTo("return new Order();\n"); + } + + [Test] + public async Task Return_WithForceNotNull_WritesBang() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + var creation = new ObjectCreationOptions(Type("Order")); + + // Act + writer.Return(creation, forceNotNull: true); + + // Assert + await Assert.That(writer.ToString()).IsEqualTo("return new Order()!;\n"); + } + + [Test] + public async Task Throw_GivenExceptionTypeAndMessage_WritesThrow() { // Arrange var writer = CodeWriterFactory.ForTests(); // Act - writer.WriteThrow( + writer.Throw( new TypeIdentity("InvalidOperationException", "System"), "Collection property 'Tags' cannot be null." ); @@ -2970,13 +3057,13 @@ await Assert } [Test] - public async Task WriteThrow_GivenMessageWithQuotesAndBackslashes_EscapesMessage() + public async Task Throw_GivenMessageWithQuotesAndBackslashes_EscapesMessage() { // Arrange var writer = CodeWriterFactory.ForTests(); // Act - writer.WriteThrow(new TypeIdentity("InvalidOperationException", "System"), "He said \"hi\" to C:\\temp\\file."); + writer.Throw(new TypeIdentity("InvalidOperationException", "System"), "He said \"hi\" to C:\\temp\\file."); // Assert await Assert @@ -2987,20 +3074,67 @@ await Assert } [Test] - public async Task WriteThrow_GivenNullMessage_WritesEmptyConstructor() + public async Task Throw_GivenNullMessage_WritesEmptyConstructor() { // Arrange var writer = CodeWriterFactory.ForTests(); // Act - writer.WriteThrow(new TypeIdentity("InvalidOperationException", "System")); + writer.Throw(new TypeIdentity("InvalidOperationException", "System")); // Assert await Assert.That(writer.ToString()).IsEqualTo("throw new global::System.InvalidOperationException();\n"); } [Test] - public async Task WriteOperator_GivenImplicitConversion_WritesConversionOperator() + public async Task Throw_WithMessageAndConstructorArgument_WritesEscapedMessageAndRawArgument() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Throw(TypeIdentity.Create(), "Value cannot be null.", "nameof(value)"); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo("throw new global::System.ArgumentNullException(\"Value cannot be null.\", nameof(value));\n"); + } + + [Test] + public async Task Throw_WithNullMessageAndConstructorArgument_WritesRawArgumentOnly() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Throw(TypeIdentity.Create(), null, "nameof(value)"); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo("throw new global::System.ArgumentNullException(nameof(value));\n"); + } + + [Test] + public async Task Throw_WithControlCharactersInMessage_EscapesNewlinesTabsAndCarriageReturns() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Throw(new TypeIdentity("InvalidOperationException", "System"), "Line 1\r\n\tTabbed \"C:\\path\".\nEnd"); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo( + "throw new global::System.InvalidOperationException(\"Line 1\\r\\n\\tTabbed \\\"C:\\\\path\\\".\\nEnd\");\n" + ); + } + + [Test] + public async Task Operator_GivenImplicitConversion_WritesConversionOperator() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -3016,7 +3150,7 @@ public async Task WriteOperator_GivenImplicitConversion_WritesConversionOperator }; // Act - writer.WriteOperatorScope(declaration); + writer.OperatorScope(declaration); // Assert await Assert.That(writer.OpenScopeCount).IsEqualTo(0); @@ -3029,7 +3163,7 @@ await Assert } [Test] - public async Task WriteOperator_GivenExplicitConversion_WritesExplicitKeyword() + public async Task Operator_GivenExplicitConversion_WritesExplicitKeyword() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -3044,10 +3178,7 @@ public async Task WriteOperator_GivenExplicitConversion_WritesExplicitKeyword() }; // Act - writer.WriteOperator( - declaration, - body => body.WriteLine("return new global::Testing.RawWidget(widget.Value);") - ); + writer.Operator(declaration, body => body.Line("return new global::Testing.RawWidget(widget.Value);")); // Assert await Assert @@ -3062,7 +3193,7 @@ await Assert } [Test] - public async Task WriteOperator_GivenUnaryOperator_WritesSingleOperand() + public async Task Operator_GivenUnaryOperator_WritesSingleOperand() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -3077,7 +3208,7 @@ public async Task WriteOperator_GivenUnaryOperator_WritesSingleOperand() }; // Act - writer.WriteOperator(declaration, body => body.WriteLine("return new global::Testing.Money(-value.Amount);")); + writer.Operator(declaration, body => body.Line("return new global::Testing.Money(-value.Amount);")); // Assert await Assert @@ -3092,7 +3223,7 @@ await Assert } [Test] - public async Task WriteProperty_GivenRequired_WritesRequiredModifier() + public async Task Property_GivenRequired_WritesRequiredModifier() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -3104,7 +3235,7 @@ public async Task WriteProperty_GivenRequired_WritesRequiredModifier() }; // Act - writer.WriteProperty(declaration); + writer.Property(declaration); // Assert await Assert @@ -3113,7 +3244,7 @@ await Assert } [Test] - public async Task WriteField_GivenRequired_WritesRequiredModifier() + public async Task Field_GivenRequired_WritesRequiredModifier() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -3124,7 +3255,7 @@ public async Task WriteField_GivenRequired_WritesRequiredModifier() }; // Act - writer.WriteField(declaration); + writer.Field(declaration); // Assert await Assert @@ -3133,7 +3264,7 @@ await Assert } [Test] - public async Task WriteIndexer_GivenAutoAccessors_WritesIndexer() + public async Task Indexer_GivenAutoAccessors_WritesIndexer() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -3144,7 +3275,7 @@ public async Task WriteIndexer_GivenAutoAccessors_WritesIndexer() }; // Act - writer.WriteIndexer(declaration); + writer.Indexer(declaration); // Assert await Assert @@ -3153,7 +3284,7 @@ await Assert } [Test] - public async Task WriteIndexer_GivenExpressionBody_WritesExpressionIndexer() + public async Task Indexer_GivenExpressionBody_WritesExpressionIndexer() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -3163,16 +3294,16 @@ public async Task WriteIndexer_GivenExpressionBody_WritesExpressionIndexer() }; // Act - writer.WriteIndexer(declaration); + writer.Indexer(declaration); // Assert await Assert .That(writer.ToString()) - .IsEqualTo(GeneratedAttributes() + "string this[int index] => _items[index];\n"); + .IsEqualTo(GeneratedAttributes() + "public string this[int index] => _items[index];\n"); } [Test] - public async Task WriteIndexer_GivenAccessorBodies_WritesScopedAccessors() + public async Task Indexer_GivenAccessorBodies_WritesScopedAccessors() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -3183,10 +3314,10 @@ public async Task WriteIndexer_GivenAccessorBodies_WritesScopedAccessors() }; // Act - writer.WriteIndexer( + writer.Indexer( declaration, - getter => getter.WriteLine("return _items[index];"), - setter => setter.WriteLine("_items[index] = value;") + getter => getter.Line("return _items[index];"), + setter => setter.Line("_items[index] = value;") ); // Assert @@ -3203,28 +3334,28 @@ await Assert } [Test] - public async Task WriteStatementFamily_WritesStructuredBlocks() + public async Task StatementFamily_WritesStructuredBlocks() { // Arrange var writer = CodeWriterFactory.ForTests(); // Act - writer.WriteTry(tryBody => - tryBody.WriteForeach( + writer.Try(tryBody => + tryBody.Foreach( "var item in items", foreachBody => - foreachBody.WriteIfElse( + foreachBody.IfElse( "item is null", - ifBody => ifBody.WriteThrow(TypeIdentity.Create(), "Null item"), - elseBody => elseBody.WriteMethodCall("Process", "item") + ifBody => ifBody.Throw(TypeIdentity.Create(), "Null item"), + elseBody => elseBody.MethodCall("Process", "item") ) ) ); - writer.WriteCatch(TypeIdentity.Create(), "ex", catchBody => catchBody.WriteMethodCall("Log", "ex")); - writer.WriteFinally(finallyBody => finallyBody.WriteMethodCall("Dispose")); - writer.WriteWhile("!finished", whileBody => whileBody.WriteMethodCall("Advance")); - writer.WriteUsingStatement("var stream = Open()", usingBody => usingBody.WriteMethodCall("Read", "stream")); - writer.WriteLockStatement("_gate", lockBody => lockBody.WriteMethodCall("Run")); + writer.Catch(TypeIdentity.Create(), "ex", catchBody => catchBody.MethodCall("Log", "ex")); + writer.Finally(finallyBody => finallyBody.MethodCall("Dispose")); + writer.While("!finished", whileBody => whileBody.MethodCall("Advance")); + writer.UsingStatement("var stream = Open()", usingBody => usingBody.MethodCall("Read", "stream")); + writer.LockStatement("_gate", lockBody => lockBody.MethodCall("Run")); // Assert var result = writer.ToString(); @@ -3242,13 +3373,13 @@ await Assert } [Test] - public async Task WriteDoWhile_GivenCondition_WritesTrailingCondition() + public async Task DoWhile_GivenCondition_WritesTrailingCondition() { // Arrange var writer = CodeWriterFactory.ForTests(); // Act - writer.WriteDoWhile("!finished", body => body.WriteMethodCall("Advance")); + writer.DoWhile("!finished", body => body.MethodCall("Advance")); // Assert await Assert.That(writer.ToString()).IsEqualTo("do\n{\n\tAdvance();\n} while (!finished);\n"); @@ -3261,7 +3392,7 @@ public async Task OpenRegion_GivenName_WritesRegionDirectives() var writer = CodeWriterFactory.ForTests(); // Act - writer.OpenRegion("Generated members", body => body.WriteLine("public int Value { get; }")); + writer.OpenRegion("Generated members", body => body.Line("public int Value { get; }")); // Assert await Assert @@ -3270,33 +3401,33 @@ await Assert } [Test] - public async Task WriteUsing_GivenGlobal_WritesGlobalUsing() + public async Task Using_GivenGlobal_WritesGlobalUsing() { // Arrange var writer = CodeWriterFactory.ForTests(); // Act - writer.WriteUsing("System.Linq", isGlobal: true); + writer.Using("System.Linq", isGlobal: true); // Assert await Assert.That(writer.ToString()).IsEqualTo("global using System.Linq;\n"); } [Test] - public async Task WriteUsingAlias_WritesAliasDirective() + public async Task UsingAlias_WritesAliasDirective() { // Arrange var writer = CodeWriterFactory.ForTests(); // Act - writer.WriteUsingAlias("Events", "global::Purview.Events"); + writer.UsingAlias("Events", "global::Purview.Events"); // Assert await Assert.That(writer.ToString()).IsEqualTo("using Events = global::Purview.Events;\n"); } [Test] - public async Task WriteMethod_GivenSpacesIndentation_UsesConfiguredSize() + public async Task Method_GivenSpacesIndentation_UsesConfiguredSize() { // Arrange var writer = CodeWriterFactory.ForTests( @@ -3309,12 +3440,12 @@ public async Task WriteMethod_GivenSpacesIndentation_UsesConfiguredSize() // Act using ( - writer.WriteMethodScope( + writer.MethodScope( new MethodDeclarationOptions("M", Type("void")) { Accessibility = TypeDeclarationAccessibility.Public } ) ) { - writer.WriteLine("return;"); + writer.Line("return;"); } // Assert @@ -3354,7 +3485,7 @@ public async Task GenerationSettings_GivenLanguageVersion_StoresIt() } [Test] - public async Task WriteProperty_GivenIsFieldBacked_WritesFieldKeywordAccessors() + public async Task Property_GivenIsFieldBacked_WritesFieldKeywordAccessors() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -3366,7 +3497,7 @@ public async Task WriteProperty_GivenIsFieldBacked_WritesFieldKeywordAccessors() }; // Act - writer.WriteProperty(declaration); + writer.Property(declaration); // Assert await Assert @@ -3375,7 +3506,7 @@ await Assert } [Test] - public async Task WriteProperty_GivenIsFieldBackedInitOnly_WritesFieldKeywordInit() + public async Task Property_GivenIsFieldBackedInitOnly_WritesFieldKeywordInit() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -3387,7 +3518,7 @@ public async Task WriteProperty_GivenIsFieldBackedInitOnly_WritesFieldKeywordIni }; // Act - writer.WriteProperty(declaration); + writer.Property(declaration); // Assert await Assert @@ -3396,7 +3527,7 @@ await Assert } [Test] - public async Task WriteProperty_GivenIsFieldBackedAndExpressionBody_ThrowsWithoutWriting() + public async Task Property_GivenIsFieldBackedAndExpressionBody_ThrowsWithoutWriting() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -3407,12 +3538,12 @@ public async Task WriteProperty_GivenIsFieldBackedAndExpressionBody_ThrowsWithou }; // Act / Assert - await Assert.That(() => writer.WriteProperty(declaration)).Throws(); + await Assert.That(() => writer.Property(declaration)).Throws(); await Assert.That(writer.ToString()).IsEmpty(); } [Test] - public async Task WriteStruct_GivenIsRefStruct_WritesRefStruct() + public async Task Struct_GivenIsRefStruct_WritesRefStruct() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -3425,7 +3556,7 @@ public async Task WriteStruct_GivenIsRefStruct_WritesRefStruct() }; // Act - using (writer.WriteStructScope(declaration)) + using (writer.StructScope(declaration)) { // Intentionally empty. } @@ -3435,7 +3566,7 @@ public async Task WriteStruct_GivenIsRefStruct_WritesRefStruct() } [Test] - public async Task WriteStruct_GivenIsRefStructOnRecordStruct_Throws() + public async Task Struct_GivenIsRefStructOnRecordStruct_Throws() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -3446,11 +3577,11 @@ public async Task WriteStruct_GivenIsRefStructOnRecordStruct_Throws() }; // Act / Assert - await Assert.That(() => writer.WriteRecordStructScope(declaration)).Throws(); + await Assert.That(() => writer.RecordStructScope(declaration)).Throws(); } [Test] - public async Task WriteField_GivenIsRefField_WritesRefField() + public async Task Field_GivenIsRefField_WritesRefField() { // Arrange var writer = CodeWriterFactory.ForTests(); @@ -3461,7 +3592,7 @@ public async Task WriteField_GivenIsRefField_WritesRefField() }; // Act - writer.WriteField(declaration); + writer.Field(declaration); // Assert await Assert @@ -3470,39 +3601,1117 @@ await Assert } [Test] - public async Task WriteField_GivenIsRefFieldAndInitializer_Throws() + public async Task Field_GivenIsRefFieldAndInitializer_Throws() { // Arrange var writer = CodeWriterFactory.ForTests(); var declaration = new FieldDeclarationOptions("_value", Type("int")) { IsRefField = true, Initializer = "0" }; // Act / Assert - await Assert.That(() => writer.WriteField(declaration)).Throws(); + await Assert.That(() => writer.Field(declaration)).Throws(); } [Test] - public async Task WriteCollectionExpression_GivenItems_WritesInlineExpression() + public async Task CollectionExpression_GivenItems_WritesInlineExpression() { // Arrange var writer = CodeWriterFactory.ForTests(); // Act - writer.WriteCollectionExpression(["first", "second", "..rest"]); + writer.CollectionExpression(["first", "second", "..rest"]); // Assert await Assert.That(writer.ToString()).IsEqualTo("[first, second, ..rest]"); } [Test] - public async Task WriteCollectionExpression_GivenSeparateLines_WritesMultilineExpression() + public async Task CollectionExpression_GivenSeparateLines_WritesMultilineExpression() { // Arrange var writer = CodeWriterFactory.ForTests(); // Act - writer.WriteCollectionExpression(["first", "second"], writeOnSeparateLines: true); + writer.CollectionExpression(["first", "second"], writeOnSeparateLines: true); // Assert await Assert.That(writer.ToString()).IsEqualTo("[\n\tfirst,\n\tsecond\n]"); } + + // --------------------------------------------------------------------------------------------- + // Declaration overloads + // --------------------------------------------------------------------------------------------- + + [Test] + public async Task MethodOverload_GivenMinimalProperties_WritesMethod() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Method("Run", Type("void"), TypeDeclarationAccessibility.Public, null, body => body.Line("return;")); + + // Assert + await Assert.That(writer.ToString()).IsEqualTo(GeneratedAttributes() + "public void Run()\n{\n\treturn;\n}\n"); + } + + [Test] + public async Task MethodOverload_WithConfigure_WritesConfiguredMethod() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Method( + "Run", + Type("void"), + TypeDeclarationAccessibility.Public, + options => options with { IsStatic = true }, + body => body.Line("Execute();") + ); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo(GeneratedAttributes() + "public static void Run()\n{\n\tExecute();\n}\n"); + } + + [Test] + public async Task MethodScopeOverload_GivenMinimalProperties_ReturnsBodyScope() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + using (writer.MethodScope("Run", Type("void"), TypeDeclarationAccessibility.Public)) + writer.Line("return;"); + + // Assert + await Assert.That(writer.ToString()).IsEqualTo(GeneratedAttributes() + "public void Run()\n{\n\treturn;\n}\n"); + } + + [Test] + public async Task PartialMethodOverload_GivenMinimalProperties_WritesPartialMethod() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.PartialMethod("OnChanged", Type("void")); + + // Assert + await Assert.That(writer.ToString()).IsEqualTo(GeneratedAttributes() + "public partial void OnChanged();\n"); + } + + [Test] + public async Task MethodExpressionOverload_GivenExpressionBody_WritesExpressionMethod() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.MethodExpression("Count", Type("int"), TypeDeclarationAccessibility.Public, "items.Count"); + + // Assert + await Assert.That(writer.ToString()).IsEqualTo(GeneratedAttributes() + "public int Count() => items.Count;\n"); + } + + [Test] + public async Task MethodExpressionOverload_GivenCallback_WritesExpressionMethod() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.MethodExpression( + "Count", + Type("int"), + TypeDeclarationAccessibility.Public, + expression => expression.Write("items.Count") + ); + + // Assert + await Assert.That(writer.ToString()).IsEqualTo(GeneratedAttributes() + "public int Count() => items.Count;\n"); + } + + [Test] + public async Task OperatorScopeOverload_GivenBinaryOperator_ReturnsBodyScope() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + var left = new ParameterDeclarationOptions("left", Type("global::Testing.Money")); + var right = new ParameterDeclarationOptions("right", Type("global::Testing.Money")); + + // Act + using (writer.OperatorScope("==", Type("bool"), left, right, TypeDeclarationAccessibility.Public)) + writer.Return("left.Equals(right)"); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo( + GeneratedAttributes() + + "public static bool operator ==(global::Testing.Money left, global::Testing.Money right)\n" + + "{\n" + + "\treturn left.Equals(right);\n" + + "}\n" + ); + } + + [Test] + public async Task OperatorOverload_WithConfigure_WritesConversionOperator() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + var source = new ParameterDeclarationOptions("source", Type("global::Testing.RawWidget")); + + // Act + writer.Operator( + "implicit", + Type("global::Testing.Widget"), + source, + default, + TypeDeclarationAccessibility.Public, + options => options with { Kind = OperatorDeclarationKind.ImplicitConversion }, + body => body.Line("return new global::Testing.Widget(source.Value);") + ); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo( + GeneratedAttributes() + + "public static implicit operator global::Testing.Widget(global::Testing.RawWidget source)\n" + + "{\n" + + "\treturn new global::Testing.Widget(source.Value);\n" + + "}\n" + ); + } + + [Test] + public async Task PropertyOverload_GivenMinimalProperties_WritesProperty() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Property("Name", Type("string"), TypeDeclarationAccessibility.Public); + + // Assert + await Assert.That(writer.ToString()).IsEqualTo(GeneratedAttributes() + "public string Name { get; }\n"); + } + + [Test] + public async Task PropertyOverload_WithConfigure_WritesConfiguredProperty() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Property( + "Name", + Type("string"), + TypeDeclarationAccessibility.Public, + options => options with { HasSetter = true, Initializer = "string.Empty" } + ); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo(GeneratedAttributes() + "public string Name { get; set; } = string.Empty;\n"); + } + + [Test] + public async Task PropertyOverload_GivenAccessorBodies_WritesScopedAccessors() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Property( + "Value", + Type("int"), + TypeDeclarationAccessibility.Public, + getter => getter.Line("return _value;"), + setter => setter.Line("_value = value;"), + options => options with { HasSetter = true } + ); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo( + GeneratedAttributes() + + "public int Value\n" + + "{\n" + + "\tget\n\t{\n\t\treturn _value;\n\t}\n" + + "\tset\n\t{\n\t\t_value = value;\n\t}\n" + + "}\n" + ); + } + + [Test] + public async Task PropertyExpressionOverload_WritesExpressionProperty() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.PropertyExpression( + "Count", + Type("int"), + TypeDeclarationAccessibility.Public, + expression => expression.Write("_items.Count") + ); + + // Assert + await Assert.That(writer.ToString()).IsEqualTo(GeneratedAttributes() + "public int Count => _items.Count;\n"); + } + + [Test] + public async Task IndexerOverload_GivenMinimalProperties_WritesIndexer() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Indexer( + Type("string"), + TypeDeclarationAccessibility.Public, + [new("index", Type("int"))], + options => options with { HasSetter = true } + ); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo(GeneratedAttributes() + "public string this[int index] { get; set; }\n"); + } + + [Test] + public async Task IndexerOverload_GivenAccessorBodies_WritesScopedAccessors() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Indexer( + Type("string"), + TypeDeclarationAccessibility.Public, + [new("index", Type("int"))], + getter => getter.Line("return _items[index];"), + setter => setter.Line("_items[index] = value;"), + options => options with { HasSetter = true } + ); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo( + GeneratedAttributes() + + "public string this[int index]\n" + + "{\n" + + "\tget\n\t{\n\t\treturn _items[index];\n\t}\n" + + "\tset\n\t{\n\t\t_items[index] = value;\n\t}\n" + + "}\n" + ); + } + + [Test] + public async Task FieldOverload_GivenMinimalProperties_WritesField() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Field("_value", Type("int"), TypeDeclarationAccessibility.Private); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo(GeneratedAttributes(includeCoverageExclusion: false) + "private int _value;\n"); + } + + [Test] + public async Task ConstructorScopeOverload_GivenMinimalProperties_ReturnsBodyScope() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + using (writer.ConstructorScope("Repository", TypeDeclarationAccessibility.Public)) + writer.Line("// body"); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo(GeneratedAttributes() + "public Repository()\n{\n\t// body\n}\n"); + } + + [Test] + public async Task ConstructorOverload_GivenMinimalProperties_WritesConstructor() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Constructor( + "Repository", + TypeDeclarationAccessibility.Public, + null, + body => body.Line("Connection = connection;") + ); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo(GeneratedAttributes() + "public Repository()\n{\n\tConnection = connection;\n}\n"); + } + + [Test] + public async Task ClassOverload_GivenMinimalProperties_WritesClass() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Class("Sample", TypeDeclarationAccessibility.Public, null, body => body.Comment("Empty")); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo(GeneratedAttributes() + "public sealed partial class Sample\n{\n\t// Empty\n}\n"); + } + + [Test] + public async Task ClassScopeOverload_GivenMinimalProperties_ReturnsBodyScope() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + using (writer.ClassScope("Sample", TypeDeclarationAccessibility.Public)) + writer.Line("// body"); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo(GeneratedAttributes() + "public sealed partial class Sample\n{\n\t// body\n}\n"); + } + + [Test] + public async Task StructOverload_GivenMinimalProperties_WritesStruct() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Struct("Value", TypeDeclarationAccessibility.Public, null, body => body.Line("// body")); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo(GeneratedAttributes() + "public partial struct Value\n{\n\t// body\n}\n"); + } + + [Test] + public async Task RecordClassOverload_GivenMinimalProperties_WritesRecordClass() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.RecordClass("Model", TypeDeclarationAccessibility.Public, null, body => body.Line("// body")); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo(GeneratedAttributes() + "public sealed partial record class Model\n{\n\t// body\n}\n"); + } + + [Test] + public async Task RecordStructOverload_GivenMinimalProperties_WritesRecordStruct() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.RecordStruct("Value", TypeDeclarationAccessibility.Public, null, body => body.Line("// body")); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo(GeneratedAttributes() + "public partial record struct Value\n{\n\t// body\n}\n"); + } + + [Test] + public async Task InterfaceOverload_GivenMinimalProperties_WritesInterface() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Interface("IService", TypeDeclarationAccessibility.Public, null, body => body.Line("// body")); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo( + GeneratedAttributes(includeCoverageExclusion: false) + + "public partial interface IService\n{\n\t// body\n}\n" + ); + } + + [Test] + public async Task EnumOverload_GivenBody_WritesEnum() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Enum("Status", TypeDeclarationAccessibility.Public, null, body => body.Line("Ready = 1,")); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo( + GeneratedAttributes(includeCoverageExclusion: false) + "public enum Status\n{\n\tReady = 1,\n}\n" + ); + } + + [Test] + public async Task EnumOverload_GivenFields_WritesEnumWithFields() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Enum("Status", TypeDeclarationAccessibility.Public, [new("Ready", 1), new("Processing", 2)]); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo( + GeneratedAttributes(includeCoverageExclusion: false) + + "public enum Status\n{\n\tReady = 1,\n\tProcessing = 2,\n}\n" + ); + } + + [Test] + public async Task TypeOverload_GivenKindAndBody_WritesType() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Type( + TypeDeclarationKind.RecordClass, + "Model", + TypeDeclarationAccessibility.Public, + null, + body => body.Line("// body") + ); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo(GeneratedAttributes() + "public sealed partial record class Model\n{\n\t// body\n}\n"); + } + + [Test] + public async Task AttributeClassOverload_GivenMinimalProperties_WritesAttributeClass() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.AttributeClass( + "RegistryAttribute", + TypeDeclarationAccessibility.Public, + AttributeTargets.Class, + body => body.Line("public string? Name { get; init; }"), + configure: options => options with { IsPartial = false } + ); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo( + "[global::Microsoft.CodeAnalysis.Embedded]\n" + + GeneratedAttributes() + + "[global::System.AttributeUsage(global::System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)]\n" + + "public sealed class RegistryAttribute : global::System.Attribute\n" + + "{\n" + + "\tpublic string? Name { get; init; }\n" + + "}\n" + ); + } + + [Test] + public async Task DelegateOverload_GivenMinimalProperties_WritesDelegate() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Delegate("Factory", Type("TResult"), TypeDeclarationAccessibility.Public, [new("value", Type("T"))]); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo( + GeneratedAttributes(includeCoverageExclusion: false) + "public delegate TResult Factory(T value);\n" + ); + } + + [Test] + public async Task EnumFieldOverload_GivenValue_WritesEnumField() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.EnumField("Ready", 1); + + // Assert + await Assert.That(writer.ToString()).IsEqualTo("Ready = 1,\n"); + } + + [Test] + public async Task NetConditionalReturn_WritesConditionalBlock() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.NetConditionalReturn("Argument '{value}' is required"); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo( + "#if NET\n" + + "return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $\"Argument '{value}' is required\");\n" + + "#else\n" + + "return global::System.FormattableString.Invariant($\"Argument '{value}' is required\");\n" + + "#endif\n" + ); + } + + [Test] + public async Task NetConditionalReturn_GivenCustomSymbol_WritesConditionalBlock() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.NetConditionalReturn("Value: {value}", "NET8_0_OR_GREATER"); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo( + "#if NET8_0_OR_GREATER\n" + + "return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $\"Value: {value}\");\n" + + "#else\n" + + "return global::System.FormattableString.Invariant($\"Value: {value}\");\n" + + "#endif\n" + ); + } + + [Test] + [Arguments(null)] + [Arguments("")] + [Arguments(" ")] + public async Task NetConditionalReturn_GivenWhitespaceMessage_Throws(string? message) + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act / Assert + await Assert.That(() => writer.NetConditionalReturn(message!)).Throws(); + await Assert.That(writer.ToString()).IsEmpty(); + } + + // --------------------------------------------------------------------------------------------- + // Default accessibility settings + // --------------------------------------------------------------------------------------------- + + [Test] + public async Task Type_WithoutAccessibility_UsesDefaultPublic() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + using (writer.ClassScope("Sample")) + { + // Intentionally empty. + } + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo(GeneratedAttributes() + "public sealed partial class Sample\n{\n}\n"); + } + + [Test] + public async Task Property_WithoutAccessibility_UsesDefaultPublic() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Property("Value", Type("int")); + + // Assert + await Assert.That(writer.ToString()).IsEqualTo(GeneratedAttributes() + "public int Value { get; }\n"); + } + + [Test] + public async Task Field_WithoutAccessibility_UsesDefaultPrivate() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Field("_value", Type("int")); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo(GeneratedAttributes(includeCoverageExclusion: false) + "private int _value;\n"); + } + + [Test] + public async Task Method_WithoutAccessibility_UsesDefaultPublic() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Method("Run", Type("void"), null, null, body => body.Return()); + + // Assert + await Assert.That(writer.ToString()).IsEqualTo(GeneratedAttributes() + "public void Run()\n{\n\treturn;\n}\n"); + } + + [Test] + public async Task Constructor_WithoutAccessibility_UsesDefaultPublic() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + using (writer.ConstructorScope("Repository")) + { + // Intentionally empty. + } + + // Assert + await Assert.That(writer.ToString()).IsEqualTo(GeneratedAttributes() + "public Repository()\n{\n}\n"); + } + + [Test] + public async Task Indexer_WithoutAccessibility_UsesDefaultPublic() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Indexer(Type("string"), parameters: [new("index", Type("int"))]); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo(GeneratedAttributes() + "public string this[int index] { get; }\n"); + } + + [Test] + public async Task Operator_WithoutAccessibility_UsesDefaultPublic() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + var left = new ParameterDeclarationOptions("left", Type("int")); + var right = new ParameterDeclarationOptions("right", Type("int")); + + // Act + using (writer.OperatorScope("+", Type("int"), left, right, null)) + { + // Intentionally empty. + } + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo(GeneratedAttributes() + "public static int operator +(int left, int right)\n{\n}\n"); + } + + [Test] + public async Task ExplicitAccessibility_OverridesDefault() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Property("Value", Type("int"), TypeDeclarationAccessibility.Internal); + + // Assert + await Assert.That(writer.ToString()).IsEqualTo(GeneratedAttributes() + "internal int Value { get; }\n"); + } + + [Test] + public async Task SettingDefaultToNull_OmitsAccessibility() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + writer.DefaultPropertyAccessibility = null; + + // Act + writer.Property("Value", Type("int")); + + // Assert + await Assert.That(writer.ToString()).IsEqualTo(GeneratedAttributes() + "int Value { get; }\n"); + } + + [Test] + public async Task PropertyAccessors_WithPublicDefaults_StayBare() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Property( + "Name", + Type("string"), + TypeDeclarationAccessibility.Public, + options => options with { HasSetter = true } + ); + + // Assert + await Assert.That(writer.ToString()).IsEqualTo(GeneratedAttributes() + "public string Name { get; set; }\n"); + } + + [Test] + public async Task PropertySetterDefault_MoreRestrictive_WritesModifier() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + writer.DefaultPropertySetterAccessibility = TypeDeclarationAccessibility.Private; + + // Act + writer.Property( + "Name", + Type("string"), + TypeDeclarationAccessibility.Public, + options => options with { HasSetter = true } + ); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo(GeneratedAttributes() + "public string Name { get; private set; }\n"); + } + + [Test] + public async Task PropertyAccessorDefault_MorePermissive_IsInherited() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + writer.DefaultPropertyAccessibility = TypeDeclarationAccessibility.Internal; + + // Act + writer.Property( + "Name", + Type("string"), + TypeDeclarationAccessibility.Internal, + options => options with { HasSetter = true } + ); + + // Assert + await Assert.That(writer.ToString()).IsEqualTo(GeneratedAttributes() + "internal string Name { get; set; }\n"); + } + + [Test] + public async Task GenerationSettings_DefaultAccessibility_FlowsIntoWriter() + { + // Arrange + var settings = new GenerationSettings("G") { DefaultFieldAccessibility = TypeDeclarationAccessibility.Public }; + var writer = new CodeWriter(settings); + + // Act + writer.Field("_value", Type("int")); + + // Assert + await Assert.That(writer.ToString()).Contains("public int _value;\n"); + await Assert.That(writer.ToString()).DoesNotContain("private int _value;"); + } + + [Test] + public async Task WriterDefaultAccessibility_CanBeOverridden() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + writer.DefaultTypeAccessibility = TypeDeclarationAccessibility.Internal; + + // Act + using (writer.ClassScope("Sample")) + { + // Intentionally empty. + } + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo(GeneratedAttributes() + "internal sealed partial class Sample\n{\n}\n"); + } + + // --------------------------------------------------------------------------------------------- + // HashDefines (conditional compilation) + // --------------------------------------------------------------------------------------------- + + [Test] + public async Task HashDefinesScope_GivenExpression_WritesDirectivesAtColumnZero() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + using (writer.HashDefinesScope("!EXCLUDE_PURVIEW_TELEMETRY_LOGGING")) + { + writer.Line("public const string Value = \"1\";"); + } + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo("#if !EXCLUDE_PURVIEW_TELEMETRY_LOGGING\npublic const string Value = \"1\";\n#endif\n\n"); + } + + [Test] + public async Task HashDefines_GivenBody_WritesConditionalBlock() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.HashDefines("NET", body => body.Line("// NET only")); + + // Assert + await Assert.That(writer.ToString()).IsEqualTo("#if NET\n// NET only\n#endif\n\n"); + } + + [Test] + public async Task HashDefines_InsideClass_WritesDirectivesAtColumnZero() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Class( + "Sample", + TypeDeclarationAccessibility.Public, + null, + body => body.HashDefines("NET", conditional => conditional.Property("Value", Type("int"))) + ); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo( + GeneratedAttributes() + + "public sealed partial class Sample\n" + + "{\n" + + "#if NET\n" + + "\t[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]\n" + + "\t[global::System.Runtime.CompilerServices.CompilerGenerated]\n" + + "\t[global::System.CodeDom.Compiler.GeneratedCode(\"TestGenerator\", \"1.0.0\")]\n" + + "\tpublic int Value { get; }\n" + + "#endif\n" + + "}\n" + ); + } + + [Test] + [Arguments(null)] + [Arguments("")] + [Arguments(" ")] + public async Task HashDefines_GivenWhitespaceExpression_Throws(string? expression) + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act / Assert + await Assert.That(() => writer.HashDefines(expression!, _ => { })).Throws(); + await Assert.That(writer.ToString()).IsEmpty(); + } + + // --------------------------------------------------------------------------------------------- + // PragmaDisable (warning suppression) + // --------------------------------------------------------------------------------------------- + + [Test] + public async Task PragmaDisable_GivenSingleCode_WritesDirective() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.PragmaDisable("CS8625"); + + // Assert + await Assert.That(writer.ToString()).IsEqualTo("#pragma warning disable CS8625\n\n"); + } + + [Test] + public async Task PragmaDisable_GivenMultipleCodes_WritesSingleDirective() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.PragmaDisable("CS8625", "CS0618"); + + // Assert + await Assert.That(writer.ToString()).IsEqualTo("#pragma warning disable CS8625 CS0618\n\n"); + } + + [Test] + public async Task PragmaDisable_GivenNoCodes_Throws() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act / Assert + await Assert.That(() => writer.PragmaDisable()).Throws(); + await Assert.That(writer.ToString()).IsEmpty(); + } + + // --------------------------------------------------------------------------------------------- + // HashElse and EmptyScope + // --------------------------------------------------------------------------------------------- + + [Test] + public async Task HashElse_InsideHashDefinesScope_WritesElseDirective() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + using (writer.HashDefinesScope("NET48_OR_GREATER || PURVIEW_TELEMETRY_NON_NULLABLE")) + { + writer.Property( + "name", + Type("string"), + TypeDeclarationAccessibility.Public, + options => options with { HasSetter = true, IncludeGeneratedAttributes = false } + ); + writer.HashElse(); + writer.Property( + "name", + Type("string").Nullable(), + TypeDeclarationAccessibility.Public, + options => options with { HasSetter = true, IncludeGeneratedAttributes = false } + ); + } + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo( + "#if NET48_OR_GREATER || PURVIEW_TELEMETRY_NON_NULLABLE\n" + + "public string name { get; set; }\n" + + "#else\n" + + "public string? name { get; set; }\n" + + "#endif\n\n" + ); + } + + [Test] + public async Task EmptyScope_GivenDisposal_WritesNothing() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + using (writer.EmptyScope()) + { + writer.Line("value"); + } + + // Assert + await Assert.That(writer.ToString()).IsEqualTo("value\n"); + } + + [Test] + public async Task EmptyScope_TernaryWithHashDefinesScope_WrapsConditionally() + { + // Arrange + var wrapped = CodeWriterFactory.ForTests(); + var guarded = CodeWriterFactory.ForTests(); + + // Act — the guard is on: EmptyScope writes nothing around the body. + using (var scope = true ? wrapped.EmptyScope() : wrapped.HashDefinesScope("NET")) + { + _ = scope; + wrapped.Line("value"); + } + + // Act — the guard is off: HashDefinesScope wraps the body. + using (var scope = false ? guarded.EmptyScope() : guarded.HashDefinesScope("NET")) + { + _ = scope; + guarded.Line("value"); + } + + // Assert + await Assert.That(wrapped.ToString()).IsEqualTo("value\n"); + await Assert.That(guarded.ToString()).IsEqualTo("#if NET\nvalue\n#endif\n\n"); + } + + [Test] + public async Task Empty_GivenBody_InvokesWithoutScope() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.Empty(body => body.Line("value")); + + // Assert + await Assert.That(writer.ToString()).IsEqualTo("value\n"); + } + + [Test] + public async Task FileLevelDirectives_AreSelfSpacingAndColumnZero() + { + // Arrange + var writer = CodeWriterFactory.ForTests(); + + // Act + writer.AutoGeneratedHeader(nullableDirective: NullableDirectiveMode.Disable); + writer.HashDefines( + "!NET48_OR_GREATER && !PURVIEW_TELEMETRY_NON_NULLABLE", + hashWriter => hashWriter.Line("#nullable enable") + ); + writer.PragmaDisable("CS8625"); + writer.FileScopedNamespace("Purview.Telemetry"); + + // Assert + await Assert + .That(writer.ToString()) + .IsEqualTo( + "// \n" + + "// This code was generated by TestGenerator (version 1.0.0).\n" + + "// Changes to this file will be lost when the source generator runs again.\n" + + "\n" + + "#if !NET48_OR_GREATER && !PURVIEW_TELEMETRY_NON_NULLABLE\n" + + "#nullable enable\n" + + "#endif\n" + + "\n" + + "#pragma warning disable CS8625\n" + + "\n" + + "namespace Purview.Telemetry;\n" + + "\n" + ); + } } diff --git a/src/tests/SourceGeneratorShared.UnitTests/Helpers/TypeHelpersTests.cs b/src/tests/SourceGeneratorShared.UnitTests/Helpers/TypeHelpersTests.cs index 13fb229..ca4cdb4 100644 --- a/src/tests/SourceGeneratorShared.UnitTests/Helpers/TypeHelpersTests.cs +++ b/src/tests/SourceGeneratorShared.UnitTests/Helpers/TypeHelpersTests.cs @@ -413,7 +413,7 @@ public static partial class Container // Act var declaration = TypeHelpers.CreatePartialTypeDeclarationOptions(symbol); var writer = CodeWriterFactory.ForTests(); - using (writer.WriteTypeScope(declaration)) + using (writer.TypeScope(declaration)) { // Intentionally empty. } @@ -474,7 +474,7 @@ public sealed partial class Container // Act var declaration = TypeHelpers.CreatePartialTypeDeclarationOptions(symbol, includeOptionalParts: false); var writer = CodeWriterFactory.ForTests(); - using (writer.WriteTypeScope(declaration)) + using (writer.TypeScope(declaration)) { // Intentionally empty. } @@ -483,7 +483,9 @@ public sealed partial class Container await Assert.That(declaration.Accessibility).IsNull(); await Assert.That(declaration.IsSealed).IsFalse(); await Assert.That(declaration.GenericTypes[0].Constraints).IsEmpty(); - await Assert.That(writer.ToString()).IsEqualTo(GeneratedAttributes() + "partial class Container\n{\n}\n"); + await Assert + .That(writer.ToString()) + .IsEqualTo(GeneratedAttributes() + "public partial class Container\n{\n}\n"); } [Test] diff --git a/src/tests/SourceGeneratorShared.UnitTests/TestGenerators/AlwaysNullableContextTestGenerator.cs b/src/tests/SourceGeneratorShared.UnitTests/TestGenerators/AlwaysNullableContextTestGenerator.cs index 139d03f..ce2d3a4 100644 --- a/src/tests/SourceGeneratorShared.UnitTests/TestGenerators/AlwaysNullableContextTestGenerator.cs +++ b/src/tests/SourceGeneratorShared.UnitTests/TestGenerators/AlwaysNullableContextTestGenerator.cs @@ -28,12 +28,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context) static (spc, ctx) => { var writer = ctx.CreateCodeWriter(); - writer.WriteAutoGeneratedHeader(); - writer.WriteFileScopedNamespace("Test"); - writer.WriteClass( + writer.AutoGeneratedHeader(); + writer.FileScopedNamespace("Test"); + writer.Class( new TypeDeclarationOptions("Sample") { Accessibility = TypeDeclarationAccessibility.Public }, body => - body.WriteProperty( + body.Property( new( "Name", TypeIdentity.Create().MakeNullable(writer), diff --git a/src/tests/SourceGeneratorShared.UnitTests/TestGenerators/DiagnosticTestGenerator.cs b/src/tests/SourceGeneratorShared.UnitTests/TestGenerators/DiagnosticTestGenerator.cs index 8b4e22f..bf02408 100644 --- a/src/tests/SourceGeneratorShared.UnitTests/TestGenerators/DiagnosticTestGenerator.cs +++ b/src/tests/SourceGeneratorShared.UnitTests/TestGenerators/DiagnosticTestGenerator.cs @@ -40,7 +40,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) static (spc, target, ctx) => { var writer = ctx.CreateCodeWriter(); - writer.WriteLine($"partial class {target.Name} {{ }}"); + writer.Line($"partial class {target.Name} {{ }}"); spc.AddSource($"{target.Name}.g.cs", writer.ToString()); } ); diff --git a/src/tests/SourceGeneratorShared.UnitTests/TestGenerators/ExplicitNullableContextTestGenerator.cs b/src/tests/SourceGeneratorShared.UnitTests/TestGenerators/ExplicitNullableContextTestGenerator.cs index 89166be..a7d5f4c 100644 --- a/src/tests/SourceGeneratorShared.UnitTests/TestGenerators/ExplicitNullableContextTestGenerator.cs +++ b/src/tests/SourceGeneratorShared.UnitTests/TestGenerators/ExplicitNullableContextTestGenerator.cs @@ -22,12 +22,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context) static (spc, ctx) => { var writer = ctx.CreateCodeWriter(); - writer.WriteAutoGeneratedHeader(); - writer.WriteFileScopedNamespace("Test"); - writer.WriteClass( + writer.AutoGeneratedHeader(); + writer.FileScopedNamespace("Test"); + writer.Class( new TypeDeclarationOptions("Sample") { Accessibility = TypeDeclarationAccessibility.Public }, body => - body.WriteProperty( + body.Property( new( "Name", TypeIdentity.Create().MakeNullable(writer), diff --git a/src/tests/SourceGeneratorShared.UnitTests/TestGenerators/NullableContextTestGenerator.cs b/src/tests/SourceGeneratorShared.UnitTests/TestGenerators/NullableContextTestGenerator.cs index bce1bd0..8caa1e4 100644 --- a/src/tests/SourceGeneratorShared.UnitTests/TestGenerators/NullableContextTestGenerator.cs +++ b/src/tests/SourceGeneratorShared.UnitTests/TestGenerators/NullableContextTestGenerator.cs @@ -21,12 +21,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context) static (spc, ctx) => { var writer = ctx.CreateCodeWriter(); - writer.WriteAutoGeneratedHeader(); - writer.WriteFileScopedNamespace("Test"); - writer.WriteClass( + writer.AutoGeneratedHeader(); + writer.FileScopedNamespace("Test"); + writer.Class( new TypeDeclarationOptions("Sample") { Accessibility = TypeDeclarationAccessibility.Public }, body => - body.WriteProperty( + body.Property( new( "Name", TypeIdentity.Create().MakeNullable(writer), From 713305c8756a2fd778c67c196ebea4f29b41f0eb Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Fri, 4 Sep 2026 13:04:45 +0100 Subject: [PATCH 2/2] feat: code writer supports more structured output --- docs/code-writer.md | 13 ++++- .../CodeWriterLiteralClassifier.cs | 15 ++++- ...PreferMinimalCodeWriterOverloadAnalyzer.cs | 4 +- .../CodeWriterSampleGenerator.cs | 1 + .../CodeWriter.DeclarationOverloads.cs | 6 +- src/src/SourceGeneratorShared/CodeWriter.cs | 25 +++++++++ ...ucturedCodeWriterStatementAnalyzerTests.cs | 56 +++++++++++++++++++ .../CodeWriterTests.cs | 20 +++++++ 8 files changed, 133 insertions(+), 7 deletions(-) diff --git a/docs/code-writer.md b/docs/code-writer.md index 015f739..24c3870 100644 --- a/docs/code-writer.md +++ b/docs/code-writer.md @@ -87,11 +87,22 @@ Emit executable statements through the structured statement methods rather than ```csharp writer.MethodCall("Process", "item"); // Process(item); writer.AwaitedMethodCall("SaveAsync", "cancellationToken"); // await SaveAsync(cancellationToken); +writer.MethodCallOn("variable", "Process", "item"); // variable.Process(item); +writer.AwaitedMethodCallOn("service", "LoadAsync", "token"); // await service.LoadAsync(token); writer.Return("value"); // return value; writer.Throw(TypeIdentity.Create(), "Failed."); // throw new ...; writer.Assignment("_total", "value"); // _total = value; writer.IfBlock("value is null", body => body.Return("null")); -writer.Foreach("var item in items", body => body.MethodCall("Process", "item")); +writer.Foreach("var item in items", body => body.MethodCallOn("item", "Process")); +``` + +`MethodCall`/`AwaitedMethodCall` write a call without a receiver — `Process(item);` or +`await SaveAsync(token);`. Use `MethodCallOn`/`AwaitedMethodCallOn` (or the `receiver` parameter on the +`IEnumerable` overloads) for a call on a variable, including generic arguments: + +```csharp +writer.MethodCall("Create", ["x"], receiver: "factory", genericArguments: [TypeReference.Create()]); +// factory.Create(x); ``` ### Conditional compilation blocks diff --git a/src/src/SourceGeneratorFramework.Analyzers/CodeWriterLiteralClassifier.cs b/src/src/SourceGeneratorFramework.Analyzers/CodeWriterLiteralClassifier.cs index 1d5930e..947b8fc 100644 --- a/src/src/SourceGeneratorFramework.Analyzers/CodeWriterLiteralClassifier.cs +++ b/src/src/SourceGeneratorFramework.Analyzers/CodeWriterLiteralClassifier.cs @@ -61,6 +61,7 @@ out string? text return true; case InterpolatedStringExpressionSyntax interpolated: +#pragma warning disable format { var builder = new StringBuilder(); foreach (var content in interpolated.Contents) @@ -72,6 +73,7 @@ out string? text text = builder.ToString(); return true; } +#pragma warning restore format default: break; @@ -152,6 +154,7 @@ public static bool IsPragmaWarningDirective(string value) if (StartsWithDeclaration(trimmed)) return null; + // The structured CodeWriter API does not yet support preprocessor directives other than #if/#else/#endif and return ClassifyExecutable(trimmed); } @@ -167,6 +170,7 @@ public static bool IsPragmaWarningDirective(string value) if (trimmed.StartsWith("using (", StringComparison.Ordinal) || !trimmed.EndsWith(";", StringComparison.Ordinal)) return null; + // "using alias = ..." is a using-alias directive, while "using ..." is a using-directive. return trimmed.Contains(" = ") ? "UsingAlias" : "Using"; } @@ -182,7 +186,7 @@ public static bool IsPragmaWarningDirective(string value) return "Throw"; if (trimmed.StartsWith("await ", StringComparison.Ordinal) && trimmed.EndsWith(";", StringComparison.Ordinal)) - return "AwaitedMethodCall"; + return HasReceiver(trimmed) ? "AwaitedMethodCallOn" : "AwaitedMethodCall"; if (trimmed.StartsWith("if (", StringComparison.Ordinal)) return "IfBlock"; @@ -203,8 +207,15 @@ public static bool IsPragmaWarningDirective(string value) return "Assignment"; if (trimmed.Contains("(") && trimmed.EndsWith(");", StringComparison.Ordinal)) - return "MethodCall"; + return HasReceiver(trimmed) ? "MethodCallOn" : "MethodCall"; + // The structured CodeWriter API does not yet support preprocessor directives other than #if/#else/#endif and return null; } + + static bool HasReceiver(string trimmed) + { + var openParen = trimmed.IndexOf('('); + return openParen > 0 && trimmed.LastIndexOf('.', openParen) >= 0; + } } diff --git a/src/src/SourceGeneratorFramework.Analyzers/PreferMinimalCodeWriterOverloadAnalyzer.cs b/src/src/SourceGeneratorFramework.Analyzers/PreferMinimalCodeWriterOverloadAnalyzer.cs index 3557163..7ec5eab 100644 --- a/src/src/SourceGeneratorFramework.Analyzers/PreferMinimalCodeWriterOverloadAnalyzer.cs +++ b/src/src/SourceGeneratorFramework.Analyzers/PreferMinimalCodeWriterOverloadAnalyzer.cs @@ -132,8 +132,8 @@ static bool IsConfigureParameter(IParameterSymbol parameter) => static bool HasObjectInitializer(SyntaxNode expression) => expression switch { - ObjectCreationExpressionSyntax { Initializer: { Expressions.Count: > 0 } } => true, - ImplicitObjectCreationExpressionSyntax { Initializer: { Expressions.Count: > 0 } } => true, + ObjectCreationExpressionSyntax { Initializer.Expressions.Count: > 0 } => true, + ImplicitObjectCreationExpressionSyntax { Initializer.Expressions.Count: > 0 } => true, _ => false, }; } diff --git a/src/src/SourceGeneratorFramework.ExampleGenerator/CodeWriterSampleGenerator.cs b/src/src/SourceGeneratorFramework.ExampleGenerator/CodeWriterSampleGenerator.cs index 51c9fe5..8b6a889 100644 --- a/src/src/SourceGeneratorFramework.ExampleGenerator/CodeWriterSampleGenerator.cs +++ b/src/src/SourceGeneratorFramework.ExampleGenerator/CodeWriterSampleGenerator.cs @@ -24,6 +24,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) if (ctx.SemanticModel.GetDeclaredSymbol(ctx.TargetNode, ct) is not INamedTypeSymbol symbol) return default; + // The generator will emit a sample class for every type annotated with the attribute, so the target is the return new CodeWriterSampleTarget( TypeName: symbol.Name, Namespace: symbol.ContainingNamespace is { IsGlobalNamespace: false } containingNamespace diff --git a/src/src/SourceGeneratorShared/CodeWriter.DeclarationOverloads.cs b/src/src/SourceGeneratorShared/CodeWriter.DeclarationOverloads.cs index b15b45f..6f3dd54 100644 --- a/src/src/SourceGeneratorShared/CodeWriter.DeclarationOverloads.cs +++ b/src/src/SourceGeneratorShared/CodeWriter.DeclarationOverloads.cs @@ -782,7 +782,8 @@ public CodeWriter Enum( if (fields is null) return Enum(declaration, static _ => { }); - return Enum(declaration, fields.ToArray()); + // The fields are passed as a list to avoid multiple enumerations of the enumerable. + return Enum(declaration, [.. fields]); } /// @@ -919,7 +920,8 @@ public CodeWriter EnumField(string fieldName, object fieldValue, params string[] if (fieldValue is null) throw new ArgumentNullException(nameof(fieldValue)); - return EnumField(new EnumFieldDeclarationOptions(fieldName, fieldValue, xmlSummary)); + // The field value is passed as an object to allow the caller to pass a string, int, or other type. The + return EnumField(new(fieldName, fieldValue, xmlSummary)); } // --------------------------------------------------------------------------------------------- diff --git a/src/src/SourceGeneratorShared/CodeWriter.cs b/src/src/SourceGeneratorShared/CodeWriter.cs index 7da9b75..59d17ca 100644 --- a/src/src/SourceGeneratorShared/CodeWriter.cs +++ b/src/src/SourceGeneratorShared/CodeWriter.cs @@ -1333,6 +1333,7 @@ public CodeWriter HashElse() /// /// A scope that does nothing when disposed. /// using var scope = wrapped ? writer.EmptyScope() : writer.HashDefinesScope("EXCLUDE_PURVIEW_TELEMETRY_LOGGING"); + [SuppressMessage("Performance", "CA1822:Mark members as static")] public BlockScope EmptyScope() => default; /// @@ -2139,6 +2140,28 @@ public CodeWriter MethodCall(string methodName, params string[] arguments) => public CodeWriter AwaitedMethodCall(string methodName, params string[] arguments) => MethodCallCore(methodName, arguments, receiver: null, genericArguments: null, false, true); + /// + /// Writes a method invocation on a receiver, such as variable.Method(arg). + /// + /// The receiver expression written before the method name. + /// The method name. + /// The argument expressions. + /// The current writer. + /// writer.MethodCallOn("service", "Add", "value"); // service.Add(value); + public CodeWriter MethodCallOn(string receiver, string methodName, params string[] arguments) => + MethodCallCore(methodName, arguments, receiver, genericArguments: null, false, false); + + /// + /// Writes an awaited method invocation on a receiver, such as await variable.MethodAsync(arg). + /// + /// The receiver expression written before the method name. + /// The method name. + /// The argument expressions. + /// The current writer. + /// writer.AwaitedMethodCallOn("service", "LoadAsync", "token"); // await service.LoadAsync(token); + public CodeWriter AwaitedMethodCallOn(string receiver, string methodName, params string[] arguments) => + MethodCallCore(methodName, arguments, receiver, genericArguments: null, false, true); + /// /// Writes a method invocation from structured argument declarations. /// @@ -2527,6 +2550,7 @@ public CodeWriter Return(ObjectCreationOptions value, bool forceNotNull = false) if (ObjectCreationExpression(value, forceNotNull)) return this; + // If the object creation was written inline, we can write the closing semicolon on the same line. return Line(";"); } @@ -3625,6 +3649,7 @@ CodeWriter Accessibility(TypeDeclarationAccessibility accessibility) return IsValidAccessorAccessibility(resolved.Value, propertyAccessibility.Value) ? resolved : null; } + [SuppressMessage("Style", "IDE0072:Add missing cases")] static bool IsValidAccessorAccessibility( TypeDeclarationAccessibility accessor, TypeDeclarationAccessibility property diff --git a/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferStructuredCodeWriterStatementAnalyzerTests.cs b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferStructuredCodeWriterStatementAnalyzerTests.cs index 3727937..84cdc8f 100644 --- a/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferStructuredCodeWriterStatementAnalyzerTests.cs +++ b/src/tests/SourceGeneratorFramework.Analyzers.UnitTests/PreferStructuredCodeWriterStatementAnalyzerTests.cs @@ -113,6 +113,62 @@ await Assert .Contains("MethodCall"); } + [Test] + public async Task Line_WithReceiverMethodCall_SuggestsMethodCallOn(CancellationToken cancellationToken) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line("variable.Process(item);"); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + var diagnostic = await Assert.That(result).HasDiagnostic(PreferStructuredCodeWriterStatementAnalyzer.Rule.Id); + await Assert + .That(diagnostic.GetMessage(System.Globalization.CultureInfo.InvariantCulture)) + .Contains("MethodCallOn"); + } + + [Test] + public async Task Line_WithAwaitedReceiverMethodCall_SuggestsAwaitedMethodCallOn( + CancellationToken cancellationToken + ) + { + // Arrange + const string source = """ + using Purview.SourceGeneratorFramework; + + class Emitter + { + public void Emit() + { + var writer = new CodeWriter(new GenerationSettings("G")); + writer.Line("await service.LoadAsync(token);"); + } + } + """; + + // Act + var result = await AnalyzeAsync(source, Options, cancellationToken); + + // Assert + var diagnostic = await Assert.That(result).HasDiagnostic(PreferStructuredCodeWriterStatementAnalyzer.Rule.Id); + await Assert + .That(diagnostic.GetMessage(System.Globalization.CultureInfo.InvariantCulture)) + .Contains("AwaitedMethodCallOn"); + } + [Test] public async Task Line_WithAssignment_ReportsDiagnostic(CancellationToken cancellationToken) { diff --git a/src/tests/SourceGeneratorShared.UnitTests/CodeWriterTests.cs b/src/tests/SourceGeneratorShared.UnitTests/CodeWriterTests.cs index 0b3eb62..d57d696 100644 --- a/src/tests/SourceGeneratorShared.UnitTests/CodeWriterTests.cs +++ b/src/tests/SourceGeneratorShared.UnitTests/CodeWriterTests.cs @@ -1745,6 +1745,26 @@ public async Task AwaitedMethodCall_WritesAwaitPrefix() await Assert.That(writer.ToString()).IsEqualTo("await LoadAsync(cancellationToken);\n"); } + [Test] + public async Task MethodCallOn_GivenReceiver_WritesReceiverDot() + { + var writer = CodeWriterFactory.ForTests(); + + writer.MethodCallOn("variable", "Process", "item"); + + await Assert.That(writer.ToString()).IsEqualTo("variable.Process(item);\n"); + } + + [Test] + public async Task AwaitedMethodCallOn_GivenReceiver_WritesAwaitAndDot() + { + var writer = CodeWriterFactory.ForTests(); + + writer.AwaitedMethodCallOn("service", "LoadAsync", "token"); + + await Assert.That(writer.ToString()).IsEqualTo("await service.LoadAsync(token);\n"); + } + [Test] public async Task AwaitedMethodCall_WithStructuredArguments_WritesReceiverAndModifiers() {