diff --git a/src/SourceGenerator.Foundations.Tests/DiagnosticAsserts.cs b/src/SourceGenerator.Foundations.Tests/DiagnosticAsserts.cs index 16893de..f912427 100644 --- a/src/SourceGenerator.Foundations.Tests/DiagnosticAsserts.cs +++ b/src/SourceGenerator.Foundations.Tests/DiagnosticAsserts.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using SGF.Analyzer.Rules; using System.Collections.Immutable; using System.Text; @@ -15,6 +16,31 @@ public static Action> NoErrors() public static Action> NoErrorsOrWarnings() => NoneOfSeverity(DiagnosticSeverity.Warning, DiagnosticSeverity.Error); + /// + /// Asserts that the diagnostics contain a diagnostic for the specified rule. + /// + /// The rule to check for + /// + public static Action> TriggersRule() where TRule : AnalyzerRule + { + string descriptorId = AnalyzerRule.GetDescriptorId(); + + return diagnostics => + { + if (diagnostics.Length == 0) + { + Assert.Fail($"Expected diagnostic {descriptorId} but no diagnostics were emitted"); + } + + if (diagnostics.Any(d => d.Descriptor.Id == descriptorId)) + { + return; + } + + Assert.Fail($"Expected diagnostic with ID '{descriptorId}' was not found in the emitted diagnostics. The emitted ones were ${string.Join(", ", diagnostics.Select(d => d.Descriptor.Id))}"); + }; + } + public static Action> NoneOfSeverity(params DiagnosticSeverity[] severity) { HashSet set = new HashSet(severity); @@ -25,11 +51,11 @@ public static Action> NoneOfSeverity(params Diagnosti .Where(d => set.Contains(d.Severity)) .ToList(); - if(filtered.Count > 0) + if (filtered.Count > 0) { StringBuilder errorBuilder = new StringBuilder(); - foreach(var dia in filtered) + foreach (var dia in filtered) { Location location = dia.Location; string? filePath = Path.GetFileName(location.SourceTree?.FilePath); diff --git a/src/SourceGenerator.Foundations.Tests/SourceGeneratorAnalyzerTests.cs b/src/SourceGenerator.Foundations.Tests/SourceGeneratorAnalyzerTests.cs new file mode 100644 index 0000000..d1fc893 --- /dev/null +++ b/src/SourceGenerator.Foundations.Tests/SourceGeneratorAnalyzerTests.cs @@ -0,0 +1,64 @@ +using SGF; +using SGF.Analyzer; +using SGF.Analyzer.Rules; +using Xunit.Abstractions; + +namespace SourceGenerator.Foundations.Tests +{ + public class SourceGeneratorAnalyzerTests : CompilationTestBase + { + public SourceGeneratorAnalyzerTests(ITestOutputHelper outputHelper) : base(outputHelper) + { + AddAnalyzer(); + } + + [Fact] + public async Task Generates_Without_Default_Constructor_Emit_SGF1003() + { + string source = """ + using SGF; + namespace Yellow + { + [SgfGenerator] + public class CustomGenerator : IncrementalGenerator + { + public CustomGenerator(string name) : base(name) + {} + public override void OnInitialize(SgfInitializationContext context) + {} + } + } + """; + await ComposeAsync([source], + assertDiagnostics: [ + DiagnosticAsserts.TriggersRule() + ]); + } + + [Fact] + public async Task Abstract_Generators_Dont_Trigger_SGF1003() + { + string source = """ + using SGF; + namespace Yellow + { + [SgfGenerator] + public abstract class CustomGenerator : IncrementalGenerator + { + public CustomGenerator(string name) : base(name) + { + } + + public override void OnInitialize(SgfInitializationContext context) + { + } + } + } + """; + await ComposeAsync([source], + assertDiagnostics: [ + DiagnosticAsserts.NoErrors() + ]); + } + } +} diff --git a/src/SourceGenerator.Foundations/Analyzer/Rules/AnalyzerRule.cs b/src/SourceGenerator.Foundations/Analyzer/Rules/AnalyzerRule.cs index aa25e20..9f15420 100644 --- a/src/SourceGenerator.Foundations/Analyzer/Rules/AnalyzerRule.cs +++ b/src/SourceGenerator.Foundations/Analyzer/Rules/AnalyzerRule.cs @@ -2,11 +2,15 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using System; +using System.Collections.Generic; namespace SGF.Analyzer.Rules { internal abstract class AnalyzerRule { + protected readonly static Dictionary s_ruleToDescriptorMap; + + /// /// Gets the descriptor that this rule creates /// @@ -17,9 +21,15 @@ internal abstract class AnalyzerRule /// protected SyntaxNodeAnalysisContext Context { get; private set; } + static AnalyzerRule() + { + s_ruleToDescriptorMap = new Dictionary(); + } + public AnalyzerRule(DiagnosticDescriptor descriptor) { Descriptor = descriptor; + s_ruleToDescriptorMap[GetType()] = descriptor.Id; } /// @@ -38,6 +48,17 @@ public void Invoke(SyntaxNodeAnalysisContext context, ClassDeclarationSyntax cla } } + + /// + /// Gets the rule id for the given rule type + /// + /// The type of the rule + /// The descriptor id for the given rule type + public static string GetDescriptorId() where T : AnalyzerRule + { + return s_ruleToDescriptorMap[typeof(T)]; + } + /// /// Tells the rule to analyze and report and errors that it sees /// diff --git a/src/SourceGenerator.Foundations/Analyzer/Rules/RequireDefaultConstructorRule.cs b/src/SourceGenerator.Foundations/Analyzer/Rules/RequireDefaultConstructorRule.cs index 3ac74c3..cd12d23 100644 --- a/src/SourceGenerator.Foundations/Analyzer/Rules/RequireDefaultConstructorRule.cs +++ b/src/SourceGenerator.Foundations/Analyzer/Rules/RequireDefaultConstructorRule.cs @@ -20,6 +20,13 @@ protected override void Analyze(ClassDeclarationSyntax classDeclaration) .OfType() .ToArray(); + if(classDeclaration.Modifiers.Any(m => m.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.AbstractKeyword))) + { + // Abstract classes do not need a default constructor, so we can skip this check + // Fix: https://github.com/ByronMayne/SourceGenerator.Foundations/issues/67 + return; + } + if(constructors.Length == 0) { // Already a compiler error since you need to call the base class constructor