diff --git a/src/Equatable.SourceGenerator/DiagnosticDescriptors.cs b/src/Equatable.SourceGenerator/DiagnosticDescriptors.cs index 3cb0c72..f1c5043 100644 --- a/src/Equatable.SourceGenerator/DiagnosticDescriptors.cs +++ b/src/Equatable.SourceGenerator/DiagnosticDescriptors.cs @@ -4,10 +4,28 @@ namespace Equatable.SourceGenerator; internal static class DiagnosticDescriptors { + public static DiagnosticDescriptor MissingDictionaryEqualityAttribute => new( + id: "EQ0001", + title: "Missing DictionaryEquality Attribute", + messageFormat: "Property '{0}' type implements IDictionary but does not have the [DictionaryEquality] attribute", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public static DiagnosticDescriptor MissingSequenceEqualityAttribute => new( + id: "EQ0002", + title: "Missing SequenceEquality or HashSetEquality Attribute", + messageFormat: "Property '{0}' type implements IEnumerable but does not have the [SequenceEquality] or [HashSetEquality] attribute", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + public static DiagnosticDescriptor InvalidStringEqualityAttributeUsage => new( id: "EQ0010", title: "Invalid StringEquality Attribute Usage", - messageFormat: "Invalid StringEquality attribute usage for property {0}. Property type is not a string", + messageFormat: "Invalid StringEquality attribute usage for property '{0}'. Property type is not a string", category: "Usage", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true @@ -16,7 +34,7 @@ internal static class DiagnosticDescriptors public static DiagnosticDescriptor InvalidDictionaryEqualityAttributeUsage => new( id: "EQ0011", title: "Invalid DictionaryEquality Attribute Usage", - messageFormat: "Invalid DictionaryEquality attribute usage for property {0}. Property type does not implement IDictionary", + messageFormat: "Invalid DictionaryEquality attribute usage for property '{0}'. Property type does not implement IDictionary", category: "Usage", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true @@ -25,7 +43,7 @@ internal static class DiagnosticDescriptors public static DiagnosticDescriptor InvalidHashSetEqualityAttributeUsage => new( id: "EQ0012", title: "Invalid HashSetEquality Attribute Usage", - messageFormat: "Invalid HashSetEquality attribute usage for property {0}. Property type does not implement IEnumerable", + messageFormat: "Invalid HashSetEquality attribute usage for property '{0}'. Property type does not implement IEnumerable", category: "Usage", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true @@ -34,7 +52,7 @@ internal static class DiagnosticDescriptors public static DiagnosticDescriptor InvalidSequenceEqualityAttributeUsage => new( id: "EQ0013", title: "Invalid SequenceEquality Attribute Usage", - messageFormat: "Invalid SequenceEquality attribute usage for property {0}. Property type does not implement IEnumerable", + messageFormat: "Invalid SequenceEquality attribute usage for property '{0}'. Property type does not implement IEnumerable", category: "Usage", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true diff --git a/src/Equatable.SourceGenerator/EquatableAnalyzer.cs b/src/Equatable.SourceGenerator/EquatableAnalyzer.cs new file mode 100644 index 0000000..35a3298 --- /dev/null +++ b/src/Equatable.SourceGenerator/EquatableAnalyzer.cs @@ -0,0 +1,253 @@ +using System.Collections.Generic; +using System.Collections.Immutable; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Equatable.SourceGenerator; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class EquatableAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create( + DiagnosticDescriptors.MissingDictionaryEqualityAttribute, + DiagnosticDescriptors.MissingSequenceEqualityAttribute, + DiagnosticDescriptors.InvalidStringEqualityAttributeUsage, + DiagnosticDescriptors.InvalidDictionaryEqualityAttributeUsage, + DiagnosticDescriptors.InvalidHashSetEqualityAttributeUsage, + DiagnosticDescriptors.InvalidSequenceEqualityAttributeUsage + ); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterSymbolAction(AnalyzeNamedType, SymbolKind.NamedType); + } + + private static void AnalyzeNamedType(SymbolAnalysisContext context) + { + var typeSymbol = (INamedTypeSymbol)context.Symbol; + + // Only analyze types with [Equatable] attribute + if (!HasEquatableAttribute(typeSymbol)) + return; + + foreach (var property in GetAnalyzableProperties(typeSymbol)) + AnalyzeProperty(context, property); + } + + private static IEnumerable GetAnalyzableProperties(INamedTypeSymbol typeSymbol) + { + // De-duplicate by name (same as the generator's GetProperties) so that + // a derived-class property shadows any same-named base-class property. + var seenPropertyNames = new HashSet(StringComparer.Ordinal); + + for (var currentSymbol = typeSymbol; currentSymbol != null; currentSymbol = currentSymbol.BaseType) + { + // Stop at system base types + if (IsSystemBaseType(currentSymbol)) + break; + + // If a base type (not the target itself) has [Equatable], stop: it will be analyzed separately + if (!SymbolEqualityComparer.Default.Equals(currentSymbol, typeSymbol) && HasEquatableAttribute(currentSymbol)) + break; + + foreach (var property in currentSymbol + .GetMembers() + .OfType() + .Where(p => !p.IsIndexer + && p.DeclaredAccessibility == Accessibility.Public + && !IsIgnored(p))) + { + if (seenPropertyNames.Add(property.Name)) + yield return property; + } + } + } + + private static void AnalyzeProperty(SymbolAnalysisContext context, IPropertySymbol property) + { + var attributes = property.GetAttributes(); + var hasEqualityAttribute = false; + + foreach (var attribute in attributes) + { + if (!IsKnownAttribute(attribute)) + continue; + + hasEqualityAttribute = true; + + var className = attribute.AttributeClass?.Name; + var attributeLocation = attribute.ApplicationSyntaxReference + ?.GetSyntax(context.CancellationToken).GetLocation() + ?? property.Locations.FirstOrDefault(); + + if (className == "StringEqualityAttribute" && !IsString(property.Type)) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.InvalidStringEqualityAttributeUsage, + attributeLocation, + property.Name)); + } + else if (className == "DictionaryEqualityAttribute" + && !ImplementsDictionary(property.Type)) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.InvalidDictionaryEqualityAttributeUsage, + attributeLocation, + property.Name)); + } + else if (className == "HashSetEqualityAttribute" + && !ImplementsEnumerable(property.Type)) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.InvalidHashSetEqualityAttributeUsage, + attributeLocation, + property.Name)); + } + else if (className == "SequenceEqualityAttribute" + && !ImplementsEnumerable(property.Type)) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.InvalidSequenceEqualityAttributeUsage, + attributeLocation, + property.Name)); + } + } + + // Warn when a collection/dictionary property has no equality attribute + if (!hasEqualityAttribute) + { + var propertyLocation = property.Locations.FirstOrDefault(); + + if (ImplementsDictionary(property.Type)) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.MissingDictionaryEqualityAttribute, + propertyLocation, + property.Name)); + } + else if (!IsString(property.Type) && ImplementsEnumerable(property.Type)) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.MissingSequenceEqualityAttribute, + propertyLocation, + property.Name)); + } + } + } + + private static bool HasEquatableAttribute(INamedTypeSymbol typeSymbol) + { + return typeSymbol.GetAttributes().Any( + a => IsKnownAttribute(a) && a.AttributeClass?.Name == "EquatableAttribute"); + } + + private static bool IsIgnored(IPropertySymbol propertySymbol) + { + return propertySymbol.GetAttributes().Any( + a => IsKnownAttribute(a) && a.AttributeClass?.Name == "IgnoreEqualityAttribute"); + } + + private static bool IsKnownAttribute(AttributeData? attribute) + { + if (attribute == null) + return false; + + return attribute.AttributeClass is + { + ContainingNamespace: + { + Name: "Attributes", + ContainingNamespace.Name: "Equatable" + } + }; + } + + private static bool IsSystemBaseType(INamedTypeSymbol symbol) + { + return symbol is + { + Name: "Object" or "ValueType", + ContainingNamespace.Name: "System" + }; + } + + private static bool IsString(ITypeSymbol targetSymbol) + { + return targetSymbol is + { + Name: "String", + ContainingNamespace.Name: "System" + }; + } + + /// + /// Returns true when the type either IS IDictionary<TKey, TValue> + /// or implements it, covering both interface-typed and concrete-typed properties. + /// + private static bool ImplementsDictionary(ITypeSymbol type) + { + return (type is INamedTypeSymbol named && IsDictionary(named)) + || type.AllInterfaces.Any(IsDictionary); + } + + /// + /// Returns true when the type either IS IEnumerable<T> + /// or implements it, covering both interface-typed and concrete-typed properties. + /// + private static bool ImplementsEnumerable(ITypeSymbol type) + { + return (type is INamedTypeSymbol named && IsEnumerable(named)) + || type.AllInterfaces.Any(IsEnumerable); + } + + private static bool IsEnumerable(INamedTypeSymbol targetSymbol) + { + return targetSymbol is + { + Name: "IEnumerable", + IsGenericType: true, + TypeArguments.Length: 1, + TypeParameters.Length: 1, + ContainingNamespace: + { + Name: "Generic", + ContainingNamespace: + { + Name: "Collections", + ContainingNamespace: + { + Name: "System" + } + } + } + }; + } + + private static bool IsDictionary(INamedTypeSymbol targetSymbol) + { + return targetSymbol is + { + Name: "IDictionary", + IsGenericType: true, + TypeArguments.Length: 2, + TypeParameters.Length: 2, + ContainingNamespace: + { + Name: "Generic", + ContainingNamespace: + { + Name: "Collections", + ContainingNamespace: + { + Name: "System" + } + } + } + }; + } +} diff --git a/test/Equatable.Generator.Tests/EquatableAnalyzerTest.cs b/test/Equatable.Generator.Tests/EquatableAnalyzerTest.cs new file mode 100644 index 0000000..a04ab1d --- /dev/null +++ b/test/Equatable.Generator.Tests/EquatableAnalyzerTest.cs @@ -0,0 +1,599 @@ +using System.Collections.Immutable; + +using Equatable.Attributes; +using Equatable.SourceGenerator; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Equatable.Generator.Tests; + +public class EquatableAnalyzerTest +{ + [Fact] + public async Task AnalyzeValidUsage() + { + var source = @" +using System; +using System.Collections.Generic; +using Equatable.Attributes; + +namespace Equatable.Entities; + +[Equatable] +public partial class UserImport +{ + [StringEquality(StringComparison.OrdinalIgnoreCase)] + public string EmailAddress { get; set; } = null!; + + public string? FirstName { get; set; } + + [HashSetEquality] + public HashSet? Roles { get; set; } + + [DictionaryEquality] + public Dictionary? Permissions { get; set; } + + [SequenceEquality] + public List? History { get; set; } +} +"; + + var diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task AnalyzeIgnoreEqualityExcluded() + { + var source = @" +using System; +using System.Collections.Generic; +using Equatable.Attributes; + +namespace Equatable.Entities; + +[Equatable] +public partial class UserImport +{ + public string? FirstName { get; set; } + + [IgnoreEquality] + public List? Ignored { get; set; } +} +"; + + var diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task AnalyzeReferenceEqualityValid() + { + var source = @" +using Equatable.Attributes; + +namespace Equatable.Entities; + +[Equatable] +public partial class Audit +{ + public int Id { get; set; } + + [ReferenceEquality] + public object? Lock { get; set; } +} +"; + + var diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task AnalyzeCustomComparerValid() + { + var source = @" +using System.Collections.Generic; +using Equatable.Attributes; + +namespace Equatable.Entities; + +[Equatable] +public partial class CustomComparer +{ + public int Id { get; set; } + + [EqualityComparer(typeof(LengthEqualityComparer))] + public string? Key { get; set; } +} + +public class LengthEqualityComparer : IEqualityComparer +{ + public static readonly LengthEqualityComparer Default = new(); + + public bool Equals(string? x, string? y) => x?.Length == y?.Length; + + public int GetHashCode(string? obj) => obj?.Length.GetHashCode() ?? 0; +} +"; + + var diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task AnalyzeNotEquatableNoWarnings() + { + var source = @" +using System.Collections.Generic; + +namespace Equatable.Entities; + +public class NotEquatable +{ + public List? Items { get; set; } + public Dictionary? Map { get; set; } +} +"; + + var diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task AnalyzeInvalidStringEquality() + { + var source = @" +using System; +using Equatable.Attributes; + +namespace Equatable.Entities; + +[Equatable] +public partial class UserImport +{ + public string EmailAddress { get; set; } = null!; + + [StringEquality(StringComparison.OrdinalIgnoreCase)] + public DateTimeOffset? LockoutEnd { get; set; } +} +"; + + var diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + var diagnostic = Assert.Single(diagnostics); + Assert.Equal("EQ0010", diagnostic.Id); + Assert.Contains("LockoutEnd", diagnostic.GetMessage()); + } + + [Fact] + public async Task AnalyzeInvalidDictionaryEquality() + { + var source = @" +using System; +using Equatable.Attributes; + +namespace Equatable.Entities; + +[Equatable] +public partial class UserImport +{ + public string EmailAddress { get; set; } = null!; + + [DictionaryEquality] + public DateTimeOffset? LastLogin { get; set; } +} +"; + + var diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + var diagnostic = Assert.Single(diagnostics); + Assert.Equal("EQ0011", diagnostic.Id); + Assert.Contains("LastLogin", diagnostic.GetMessage()); + } + + [Fact] + public async Task AnalyzeInvalidHashSetEquality() + { + var source = @" +using System; +using Equatable.Attributes; + +namespace Equatable.Entities; + +[Equatable] +public partial class UserImport +{ + public string EmailAddress { get; set; } = null!; + + [HashSetEquality] + public DateTimeOffset? LastLogin { get; set; } +} +"; + + var diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + var diagnostic = Assert.Single(diagnostics); + Assert.Equal("EQ0012", diagnostic.Id); + Assert.Contains("LastLogin", diagnostic.GetMessage()); + } + + [Fact] + public async Task AnalyzeInvalidSequenceEquality() + { + var source = @" +using System; +using Equatable.Attributes; + +namespace Equatable.Entities; + +[Equatable] +public partial class UserImport +{ + public string EmailAddress { get; set; } = null!; + + [SequenceEquality] + public DateTimeOffset? LastLogin { get; set; } +} +"; + + var diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + var diagnostic = Assert.Single(diagnostics); + Assert.Equal("EQ0013", diagnostic.Id); + Assert.Contains("LastLogin", diagnostic.GetMessage()); + } + + [Fact] + public async Task AnalyzeMissingDictionaryAttribute() + { + var source = @" +using System.Collections.Generic; +using Equatable.Attributes; + +namespace Equatable.Entities; + +[Equatable] +public partial class UserImport +{ + public string EmailAddress { get; set; } = null!; + + public Dictionary? Permissions { get; set; } +} +"; + + var diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + var diagnostic = Assert.Single(diagnostics); + Assert.Equal("EQ0001", diagnostic.Id); + Assert.Contains("Permissions", diagnostic.GetMessage()); + } + + [Fact] + public async Task AnalyzeMissingSequenceAttribute() + { + var source = @" +using System.Collections.Generic; +using Equatable.Attributes; + +namespace Equatable.Entities; + +[Equatable] +public partial class UserImport +{ + public string EmailAddress { get; set; } = null!; + + public List? Items { get; set; } +} +"; + + var diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + var diagnostic = Assert.Single(diagnostics); + Assert.Equal("EQ0002", diagnostic.Id); + Assert.Contains("Items", diagnostic.GetMessage()); + } + + [Fact] + public async Task AnalyzeMissingHashSetAttribute() + { + var source = @" +using System.Collections.Generic; +using Equatable.Attributes; + +namespace Equatable.Entities; + +[Equatable] +public partial class UserImport +{ + public string EmailAddress { get; set; } = null!; + + public HashSet? Roles { get; set; } +} +"; + + var diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + var diagnostic = Assert.Single(diagnostics); + Assert.Equal("EQ0002", diagnostic.Id); + Assert.Contains("Roles", diagnostic.GetMessage()); + } + + [Fact] + public async Task AnalyzeStringNoMissingWarning() + { + // string implements IEnumerable but should NOT trigger EQ0002 + var source = @" +using Equatable.Attributes; + +namespace Equatable.Entities; + +[Equatable] +public partial class UserImport +{ + public string? Name { get; set; } +} +"; + + var diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task AnalyzeDictionaryWithMissingAttributeEmitsDictDiagnostic() + { + // Dictionary implements both IDictionary and IEnumerable, should emit EQ0001 (not EQ0002) + var source = @" +using System.Collections.Generic; +using Equatable.Attributes; + +namespace Equatable.Entities; + +[Equatable] +public partial class UserImport +{ + public Dictionary? Data { get; set; } +} +"; + + var diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + var diagnostic = Assert.Single(diagnostics); + Assert.Equal("EQ0001", diagnostic.Id); + } + + [Fact] + public async Task AnalyzeMultipleMissingAttributes() + { + var source = @" +using System.Collections.Generic; +using Equatable.Attributes; + +namespace Equatable.Entities; + +[Equatable] +public partial class UserImport +{ + public Dictionary? Permissions { get; set; } + + public List? Items { get; set; } +} +"; + + var diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + Assert.Equal(2, diagnostics.Length); + Assert.Contains(diagnostics, d => d.Id == "EQ0001" && d.GetMessage().Contains("Permissions")); + Assert.Contains(diagnostics, d => d.Id == "EQ0002" && d.GetMessage().Contains("Items")); + } + + [Fact] + public async Task AnalyzeMissingDictionaryAttributeForIDictionary() + { + var source = @" +using System.Collections.Generic; +using Equatable.Attributes; + +namespace Equatable.Entities; + +[Equatable] +public partial class UserImport +{ + public string EmailAddress { get; set; } = null!; + + public IDictionary? Permissions { get; set; } +} +"; + + var diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + var diagnostic = Assert.Single(diagnostics); + Assert.Equal("EQ0001", diagnostic.Id); + Assert.Contains("Permissions", diagnostic.GetMessage()); + } + + [Fact] + public async Task AnalyzeMissingSequenceAttributeForIEnumerable() + { + var source = @" +using System.Collections.Generic; +using Equatable.Attributes; + +namespace Equatable.Entities; + +[Equatable] +public partial class UserImport +{ + public string EmailAddress { get; set; } = null!; + + public IEnumerable? Items { get; set; } +} +"; + + var diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + var diagnostic = Assert.Single(diagnostics); + Assert.Equal("EQ0002", diagnostic.Id); + Assert.Contains("Items", diagnostic.GetMessage()); + } + + [Fact] + public async Task AnalyzeMissingSequenceAttributeForIReadOnlyCollection() + { + var source = @" +using System.Collections.Generic; +using Equatable.Attributes; + +namespace Equatable.Entities; + +[Equatable] +public partial class UserImport +{ + public string EmailAddress { get; set; } = null!; + + public IReadOnlyCollection? Items { get; set; } +} +"; + + var diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + var diagnostic = Assert.Single(diagnostics); + Assert.Equal("EQ0002", diagnostic.Id); + Assert.Contains("Items", diagnostic.GetMessage()); + } + + [Fact] + public async Task AnalyzeDictionaryEqualityOnIDictionaryIsValid() + { + // [DictionaryEquality] on an IDictionary<,>-typed property must NOT emit EQ0011 + var source = @" +using System.Collections.Generic; +using Equatable.Attributes; + +namespace Equatable.Entities; + +[Equatable] +public partial class UserImport +{ + [DictionaryEquality] + public IDictionary? Permissions { get; set; } +} +"; + + var diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task AnalyzeSequenceEqualityOnIEnumerableIsValid() + { + // [SequenceEquality] on an IEnumerable-typed property must NOT emit EQ0013 + var source = @" +using System.Collections.Generic; +using Equatable.Attributes; + +namespace Equatable.Entities; + +[Equatable] +public partial class UserImport +{ + [SequenceEquality] + public IEnumerable? Items { get; set; } +} +"; + + var diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task AnalyzeBaseTypePropertiesIncludedWhenNoEquatableBase() + { + // Properties from a non-[Equatable] base class should be analyzed + var source = @" +using System.Collections.Generic; +using Equatable.Attributes; + +namespace Equatable.Entities; + +public abstract class ModelBase +{ + public List? Tags { get; set; } +} + +[Equatable] +public partial class Priority : ModelBase +{ + public string Name { get; set; } = null!; +} +"; + + var diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + var diagnostic = Assert.Single(diagnostics); + Assert.Equal("EQ0002", diagnostic.Id); + Assert.Contains("Tags", diagnostic.GetMessage()); + } + + [Fact] + public async Task AnalyzeBaseTypePropertiesExcludedWhenEquatableBase() + { + // Properties from a [Equatable] base class must NOT cause duplicate diagnostics on the derived type + var source = @" +using System.Collections.Generic; +using Equatable.Attributes; + +namespace Equatable.Entities; + +[Equatable] +public abstract partial class ModelBase +{ + [SequenceEquality] + public List? Tags { get; set; } +} + +[Equatable] +public partial class Priority : ModelBase +{ + public string Name { get; set; } = null!; +} +"; + + var diagnostics = await GetAnalyzerDiagnosticsAsync(source); + + Assert.Empty(diagnostics); + } + + private static async Task> GetAnalyzerDiagnosticsAsync(string source) + { + var syntaxTree = CSharpSyntaxTree.ParseText(source); + var references = AppDomain.CurrentDomain.GetAssemblies() + .Where(assembly => !assembly.IsDynamic && !string.IsNullOrWhiteSpace(assembly.Location)) + .Select(assembly => MetadataReference.CreateFromFile(assembly.Location)) + .Concat( + [ + MetadataReference.CreateFromFile(typeof(EquatableAttribute).Assembly.Location), + ]); + + var compilation = CSharpCompilation.Create( + "Test.Analyzer", + [syntaxTree], + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var analyzer = new EquatableAnalyzer(); + var compilationWithAnalyzers = compilation.WithAnalyzers( + ImmutableArray.Create(analyzer)); + + return await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync(); + } +}