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
30 changes: 28 additions & 2 deletions src/SourceGenerator.Foundations.Tests/DiagnosticAsserts.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.CodeAnalysis;
using SGF.Analyzer.Rules;
using System.Collections.Immutable;
using System.Text;

Expand All @@ -15,6 +16,31 @@ public static Action<ImmutableArray<Diagnostic>> NoErrors()
public static Action<ImmutableArray<Diagnostic>> NoErrorsOrWarnings()
=> NoneOfSeverity(DiagnosticSeverity.Warning, DiagnosticSeverity.Error);

/// <summary>
/// Asserts that the diagnostics contain a diagnostic for the specified rule.
/// </summary>
/// <typeparam name="TRule">The rule to check for</typeparam>
/// <returns></returns>
public static Action<ImmutableArray<Diagnostic>> TriggersRule<TRule>() where TRule : AnalyzerRule
{
string descriptorId = AnalyzerRule.GetDescriptorId<TRule>();

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<ImmutableArray<Diagnostic>> NoneOfSeverity(params DiagnosticSeverity[] severity)
{
HashSet<DiagnosticSeverity> set = new HashSet<DiagnosticSeverity>(severity);
Expand All @@ -25,11 +51,11 @@ public static Action<ImmutableArray<Diagnostic>> 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SourceGeneratorAnalyzer>();
}

[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<RequireDefaultConstructorRule>()
]);
}

[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()
]);
}
}
}
21 changes: 21 additions & 0 deletions src/SourceGenerator.Foundations/Analyzer/Rules/AnalyzerRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Type, string> s_ruleToDescriptorMap;


/// <summary>
/// Gets the descriptor that this rule creates
/// </summary>
Expand All @@ -17,9 +21,15 @@ internal abstract class AnalyzerRule
/// </summary>
protected SyntaxNodeAnalysisContext Context { get; private set; }

static AnalyzerRule()
{
s_ruleToDescriptorMap = new Dictionary<Type, string>();
}

public AnalyzerRule(DiagnosticDescriptor descriptor)
{
Descriptor = descriptor;
s_ruleToDescriptorMap[GetType()] = descriptor.Id;
}

/// <summary>
Expand All @@ -38,6 +48,17 @@ public void Invoke(SyntaxNodeAnalysisContext context, ClassDeclarationSyntax cla
}
}


/// <summary>
/// Gets the rule id for the given rule type
/// </summary>
/// <typeparam name="T">The type of the rule</typeparam>
/// <returns>The descriptor id for the given rule type</returns>
public static string GetDescriptorId<T>() where T : AnalyzerRule
{
return s_ruleToDescriptorMap[typeof(T)];
}

/// <summary>
/// Tells the rule to analyze and report and errors that it sees
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ protected override void Analyze(ClassDeclarationSyntax classDeclaration)
.OfType<ConstructorDeclarationSyntax>()
.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
Expand Down
Loading