Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ land in the test project's compile references and collide with the in-box types
| `Initialize(IncrementalGeneratorInitializationContext)` | `void` | Finds the declared files and registers the source output. Sealed behaviour; do not reimplement. |
| `Generate(SourceProductionContext, MetadataSet)` | `void` | Emits this generator's sources. Abstract. |
| `CreateCodeBlocker()` | `CodeBlocker` | A `CodeBlocker` pinned to LF, so output is byte-identical on every platform. Static. |
| `WriteFileHeader(CodeBlocker, string?)` | `void` | Writes the copyright line and the `<auto-generated />` marker. A null or empty copyright writes only the marker. Static. |
| `WriteFileHeader(CodeBlocker, string?)` | `void` | Writes the copyright line, the `<auto-generated />` marker and `#nullable enable` (generated code otherwise compiles with nullable disabled). A null or empty copyright writes no copyright lines. Static. |
| `WriteSourceFile(CodeBlocker, SourceFileTemplate, string?)` | `void` | Writes a whole source file, header included. Static. |

### `GeneratorBase<T>`
Expand Down
43 changes: 42 additions & 1 deletion SourceGeneratorToolkit.Test/GeneratorBaseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -293,10 +294,50 @@
{
string source = HeaderProbe.Write($"Copyright (c) Example.{lineBreak}{lineBreak}All rights reserved.");

Assert.AreEqual("// Copyright (c) Example.\n//\n// All rights reserved.\n// <auto-generated />\n\nnamespace Generated { }\n", source);
Assert.AreEqual("// Copyright (c) Example.\n//\n// All rights reserved.\n// <auto-generated />\n#nullable enable\n\nnamespace Generated { }\n", source);
Assert.AreEqual(0, CSharpSyntaxTree.ParseText(source).GetDiagnostics().Count(), source);
}

[TestMethod]
public void TheHeaderEnablesNullableAfterTheGeneratedMarker() =>
StringAssert.Contains(HeaderProbe.Write(null), "// <auto-generated />\n#nullable enable\n");

Check warning on line 303 in SourceGeneratorToolkit.Test/GeneratorBaseTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'StringAssert.Contains'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_SourceGeneratorToolkit&issues=AaGKpwoojMEuUl6imjsF&open=AaGKpwoojMEuUl6imjsF&pullRequest=57

[TestMethod]
public void AnnotatedGeneratedMembersCompileWithoutCS8669()
{
ImmutableArray<Diagnostic> diagnostics = CompileWithConsumer("int n = t.Description?.Length ?? 0;");

Assert.IsFalse(diagnostics.Any(d => d.Id == "CS8669"), string.Join("\n", diagnostics));
}

[TestMethod]
public void UnannotatedGeneratedMembersAreNotNullOblivious()
{
ImmutableArray<Diagnostic> diagnostics = CompileWithConsumer("t.Name = null;");

Assert.IsTrue(diagnostics.Any(d => d.Id == "CS8625"), string.Join("\n", diagnostics));
}

/// <summary>
/// Compiles a <see cref="GeneratorBase.WriteSourceFile"/> class holding a <c>string?</c> and a
/// <c>string</c> member, next to a consumer statement, the way a nullable-enabled project would.
/// </summary>
private static ImmutableArray<Diagnostic> CompileWithConsumer(string statement)
{
string generated = SourceFileProbe.WriteThingWithNullableAndNonNullableMembers();
string consumer = $"namespace Consumer {{ public static class User {{ public static void Use(Generated.Thing t) {{ {statement} }} }} }}";

CSharpCompilation compilation = CSharpCompilation.Create(
assemblyName: "NullableConsumer",
syntaxTrees: [CSharpSyntaxTree.ParseText(generated, path: "Thing.g.cs"), CSharpSyntaxTree.ParseText(consumer, path: "User.cs")],
references: [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)],
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable));

ImmutableArray<Diagnostic> diagnostics = compilation.GetDiagnostics();
Assert.IsFalse(diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error), generated + "\n" + string.Join("\n", diagnostics));
return diagnostics;
}

[TestMethod]
public void TheGeneratorReusesItsOutputWhenNothingChanged()
{
Expand Down
29 changes: 29 additions & 0 deletions SourceGeneratorToolkit.Test/TestGenerators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -340,3 +340,32 @@ internal static string Write(string? copyright)
return codeBlocker.ToString();
}
}

/// <summary>
/// Exposes the protected whole-file writer, so its output can be compiled alongside a consumer.
/// </summary>
internal abstract class SourceFileProbe : GeneratorBase
{
/// <summary>
/// Writes <c>Generated.Thing</c> with a <c>string?</c> field and a <c>string</c> field.
/// </summary>
internal static string WriteThingWithNullableAndNonNullableMembers()
{
SourceFileTemplate file = new() { Namespace = "Generated" };
file.Classes.Add(new ClassTemplate
{
Name = "Thing",
Kind = ktsu.CodeBlocker.Templates.TypeKind.Class,
Keywords = { CSharpKeywords.Public },
Members =
{
new FieldTemplate { Name = "Description", Type = "string?", Keywords = { CSharpKeywords.Public } },
new FieldTemplate { Name = "Name", Type = "string", DefaultValue = "\"\"", Keywords = { CSharpKeywords.Public } },
},
});

using CodeBlocker codeBlocker = CreateCodeBlocker();
WriteSourceFile(codeBlocker, file, TestDiagnostics.Copyright);
return codeBlocker.ToString();
}
}
7 changes: 7 additions & 0 deletions SourceGeneratorToolkit/GeneratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@ protected static CodeBlocker CreateCodeBlocker() =>
/// A parameter rather than a hard-coded literal, so a consuming repository can keep the header in
/// step with its own file header template from one place, and so nothing about any one
/// repository leaks into this package.
/// <para>
/// The header ends with <c>#nullable enable</c>. Roslyn compiles a file carrying the
/// generated-file marker with the nullable context disabled, whatever the consuming project's
/// <c>Nullable</c> setting, so without the directive every <c>?</c> annotation in the output
/// raises CS8669 and every unannotated reference member is null-oblivious to its callers.
/// </para>
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="codeBlocker"/> is <see langword="null"/>.</exception>
protected static void WriteFileHeader(CodeBlocker codeBlocker, string? copyright)
Expand All @@ -143,6 +149,7 @@ protected static void WriteFileHeader(CodeBlocker codeBlocker, string? copyright
}

codeBlocker.WriteLine("// <auto-generated />");
codeBlocker.WriteLine("#nullable enable");
codeBlocker.NewLine();
}

Expand Down
Loading