Skip to content
Open
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 @@ -244,7 +244,7 @@ land in the test project's compile references and collide with the in-box types
| Name | Type | Description |
|------|------|-------------|
| `MetadataFileNames` | `IReadOnlyList<string>` | The metadata files this generator reads, without their directories. Abstract. |
| `Diagnostics` | `DiagnosticCatalog` | The catalogue this generator's diagnostics are allocated from. Abstract. |
| `Diagnostics` | `DiagnosticCatalog` | The catalogue this generator's diagnostics are allocated from. `MetadataFileMissing` and `MetadataParseFailed` must both be allocated from it, or `Initialize` throws and the build reports CS8784. Abstract. |
| `MetadataFileMissing` | `DiagnosticDescriptor` | Reported when a declared file is not in the compilation. Takes the file name. Abstract. |
| `MetadataParseFailed` | `DiagnosticDescriptor` | Reported when a file cannot be parsed, or when several files of one declared name have different contents (the reason lists their paths, and none is used). Takes the file name and the reason. Abstract. |

Expand Down
21 changes: 21 additions & 0 deletions SourceGeneratorToolkit.Test/GeneratorBaseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,27 @@
public override SourceText GetText(System.Threading.CancellationToken cancellationToken = default) => SourceText.From(text);
}

[TestMethod]
public void ANullDiagnosticCatalogueIsRefused()
{
GeneratorRunResult result = Harness.Run(new NullCatalogGenerator());

Assert.IsInstanceOfType<InvalidOperationException>(result.Exception);
StringAssert.Contains(result.Exception.Message, "NullCatalogGenerator.Diagnostics returned null");

Check warning on line 129 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=AaG230O7WrHHQUT4ydx_&open=AaG230O7WrHHQUT4ydx_&pullRequest=70
Assert.AreEqual(0, result.GeneratedSources.Length);

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.IsEmpty' instead of 'Assert.AreEqual'

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

[TestMethod]
public void ADescriptorFromAnotherCatalogueIsRefused()
{
GeneratorRunResult result = Harness.Run(new MismatchedCatalogGenerator());

Assert.IsInstanceOfType<InvalidOperationException>(result.Exception);
StringAssert.Contains(result.Exception.Message, "MismatchedCatalogGenerator.MetadataFileMissing (TST001)");

Check warning on line 139 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=AaG230O7WrHHQUT4ydyB&open=AaG230O7WrHHQUT4ydyB&pullRequest=70
StringAssert.Contains(result.Exception.Message, "'Other' catalogue");

Check warning on line 140 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=AaG230O7WrHHQUT4ydyC&open=AaG230O7WrHHQUT4ydyC&pullRequest=70
Assert.AreEqual(0, result.GeneratedSources.Length);

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.IsEmpty' instead of 'Assert.AreEqual'

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

[TestMethod]
public void AMalformedMetadataFileIsReportedRatherThanSwallowed()
{
Expand Down
35 changes: 35 additions & 0 deletions SourceGeneratorToolkit.Test/TestGenerators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -421,3 +421,38 @@ internal static string WriteThingWithNullableAndNonNullableMembers()
return codeBlocker.ToString();
}
}

/// <summary>A second catalogue, so a generator can name one its descriptors weren't allocated from.</summary>
internal static class OtherDiagnostics
{
internal static DiagnosticCatalog Catalog { get; } = new("OTH", "Other");
}

/// <summary>Names a null catalogue, which <see cref="GeneratorBase"/> must refuse.</summary>
internal sealed class NullCatalogGenerator() : GeneratorBase<ThingsMetadata>("things.json")
{
protected override DiagnosticCatalog Diagnostics => null!;

protected override DiagnosticDescriptor MetadataFileMissing => TestDiagnostics.MetadataFileMissing;

protected override DiagnosticDescriptor MetadataParseFailed => TestDiagnostics.MetadataParseFailed;

protected override void Generate(SourceProductionContext context, ThingsMetadata metadata, CodeBlocker codeBlocker) =>
context.AddSource("Things.g.cs", "// generated");
}

/// <summary>
/// Names a catalogue its descriptors weren't allocated from: the prefix drift
/// <see cref="DiagnosticCatalog"/> exists to prevent.
/// </summary>
internal sealed class MismatchedCatalogGenerator() : GeneratorBase<ThingsMetadata>("things.json")
{
protected override DiagnosticCatalog Diagnostics => OtherDiagnostics.Catalog;

protected override DiagnosticDescriptor MetadataFileMissing => TestDiagnostics.MetadataFileMissing;

protected override DiagnosticDescriptor MetadataParseFailed => TestDiagnostics.MetadataParseFailed;

protected override void Generate(SourceProductionContext context, ThingsMetadata metadata, CodeBlocker codeBlocker) =>
context.AddSource("Things.g.cs", "// generated");
}
40 changes: 40 additions & 0 deletions SourceGeneratorToolkit/GeneratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ public abstract class GeneratorBase : IIncrementalGenerator
/// <summary>
/// Gets the catalogue this generator's diagnostics are allocated from.
/// </summary>
/// <remarks>
/// <see cref="MetadataFileMissing"/> and <see cref="MetadataParseFailed"/> must both be allocated
/// from it. <see cref="Initialize"/> throws otherwise, so the two diagnostics this base reports
/// cannot drift to a prefix other than the one the generator's own tests enumerate.
/// </remarks>
protected abstract DiagnosticCatalog Diagnostics { get; }

/// <summary>
Expand All @@ -57,8 +62,14 @@ public abstract class GeneratorBase : IIncrementalGenerator
protected abstract DiagnosticDescriptor MetadataParseFailed { get; }

/// <inheritdoc/>
/// <exception cref="InvalidOperationException">
/// <see cref="Diagnostics"/> is <see langword="null"/>, or <see cref="MetadataFileMissing"/> or
/// <see cref="MetadataParseFailed"/> was not allocated from it.
/// </exception>
public void Initialize(IncrementalGeneratorInitializationContext context)
{
EnsureCataloged();

IReadOnlyList<string> wanted = MetadataFileNames;

IncrementalValueProvider<ImmutableArray<MetadataFile>> metadataFiles = context.AdditionalTextsProvider
Expand Down Expand Up @@ -115,6 +126,35 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
});
}

// A required catalogue that nothing reads looks enforced and isn't: a generator could report its
// two built-in diagnostics under a prefix other than the one it releases and tests, which is the
// drift DiagnosticCatalog exists to prevent. Thrown from Initialize, the failure reaches the
// consumer's build as the driver's CS8784 warning naming this message, and the generator emits
// nothing.
private void EnsureCataloged()
{
DiagnosticCatalog catalog = Diagnostics
?? throw new InvalidOperationException($"{GetType().Name}.Diagnostics returned null. A generator must name the catalogue its diagnostics are allocated from.");

EnsureCataloged(catalog, MetadataFileMissing, nameof(MetadataFileMissing));
EnsureCataloged(catalog, MetadataParseFailed, nameof(MetadataParseFailed));
}

private void EnsureCataloged(DiagnosticCatalog catalog, DiagnosticDescriptor? descriptor, string member)
{
if (descriptor is not null && catalog.Descriptors.Any(cataloged => cataloged.Id == descriptor.Id))
{
return;
}

string cataloged = catalog.Descriptors.Count == 0
? "none"
: string.Join(", ", catalog.Descriptors.Select(entry => entry.Id));
throw new InvalidOperationException(
$"{GetType().Name}.{member} ({descriptor?.Id ?? "null"}) was not allocated from {GetType().Name}.Diagnostics, "
+ $"the '{catalog.Category}' catalogue (descriptors: {cataloged}). Allocate it from that catalogue.");
}

/// <summary>
/// Runs an action with the consumer's language version available to <see cref="WriteFileHeader(CodeBlocker, string?)"/>.
/// </summary>
Expand Down
Loading