Skip to content

Commit

Permalink
Added source generator diagnostics tests and fixed bug while generati…
Browse files Browse the repository at this point in the history
…ng OneOfBase with interfaces (#123)

* added UserDefinedConversionsToOrFromAnInterfaceAreNotAllowed diagnostic description and its usage in OneOfGenerator

* bumped Microsoft.CodeAnalysis.CSharp to 4.2.0

* Added new analyzer tests project

* Changed UserDefinedConversionsToOrFromAnInterfaceAreNotAllowed id to custom

* rollbacked Microsoft.CodeAnalysis.CSharp to 3.9.0 due to appveyor error

* ProcessClass cleanup

Co-authored-by: Damian Romanowski <[email protected]>
  • Loading branch information
romfir and Damian Romanowski authored Jul 5, 2022
1 parent a1c3548 commit 12745ca
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 22 deletions.
207 changes: 207 additions & 0 deletions OneOf.SourceGenerator.AnalyzerTests/AnalyzerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;

namespace OneOf.SourceGenerator.AnalyzerTests
{
public class AnalyzerTests
{
[Fact]
public void GenerateOneOfAttribute()
{
const string expectedCode = @"// <auto-generated />
using System;
#pragma warning disable 1591
namespace OneOf
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
internal sealed class GenerateOneOfAttribute : Attribute
{
}
}
";
AssertCorrectSourceCodeIsGeneratedWithNoDiagnostics(string.Empty, expectedCode, "GenerateOneOfAttribute.g.cs", 2);
}

[Fact]
public void StringOrNumber()
{
const string input = @"
using OneOf;
namespace Foo
{
[GenerateOneOf]
public partial class StringOrNumber : OneOfBase<string, int, uint> { }
}
";

const string expectedCode = @"// <auto-generated />
#pragma warning disable 1591
namespace Foo
{
public partial class StringOrNumber
{
public StringOrNumber(OneOf.OneOf<string, int, uint> _) : base(_) { }
public static implicit operator StringOrNumber(string _) => new StringOrNumber(_);
public static explicit operator string(StringOrNumber _) => _.AsT0;
public static implicit operator StringOrNumber(int _) => new StringOrNumber(_);
public static explicit operator int(StringOrNumber _) => _.AsT1;
public static implicit operator StringOrNumber(uint _) => new StringOrNumber(_);
public static explicit operator uint(StringOrNumber _) => _.AsT2;
}
}";

AssertCorrectSourceCodeIsGeneratedWithNoDiagnostics(input, expectedCode, "Foo_StringOrNumber.g.cs");
}

[Fact]
public void Class_Must_Be_Top_Level()
{
const string input = @"
using OneOf;
namespace Foo
{
public static class A
{
[GenerateOneOf]
public partial class StringOrNumber : OneOfBase<string, int> { }
}
}
";

AssertDiagnosticErrorIsReturned(input, GeneratorDiagnosticDescriptors.TopLevelError.Id);
}

[Fact]
public void Class_Must_Be_Public()
{
const string input = @"
using OneOf;
namespace Foo
{
[GenerateOneOf]
private partial class StringOrNumber : OneOfBase<string, int> { }
}
";
AssertDiagnosticErrorIsReturned(input, GeneratorDiagnosticDescriptors.ClassIsNotPublic.Id);
}

[Fact]
public void Cannot_Use_Generator_With_Object_type()
{
const string input = @"
using OneOf;
namespace Foo
{
[GenerateOneOf]
public partial class ObjectOrNumber : OneOfBase<object, int> { }
}
";
AssertDiagnosticErrorIsReturned(input, GeneratorDiagnosticDescriptors.ObjectIsOneOfType.Id);
}

[Fact]
public void Class_Must_Be_Derived_From_OneOfBase()
{
const string input = @"
using OneOf;
namespace Foo
{
[GenerateOneOf]
public partial class ObjectOrNumber : MyClass { }
public class MyClass
{
}
}
";
AssertDiagnosticErrorIsReturned(input, GeneratorDiagnosticDescriptors.WrongBaseType.Id);
}

[Fact]
public void User_Defined_Conversions_To_Or_From_An_Interface_Are_Not_Allowed()
{
const string input = @"
using OneOf;
namespace Foo
{
[GenerateOneOf]
public partial class ObjectOrNumber : OneOfBase<IFoo, int> { }
public interface IFoo
{
}
}
";
AssertDiagnosticErrorIsReturned(input, GeneratorDiagnosticDescriptors.UserDefinedConversionsToOrFromAnInterfaceAreNotAllowed.Id);
}

private static void AssertCorrectSourceCodeIsGeneratedWithNoDiagnostics(string inputSource, string expectedCode, string generatedFileName, int expectedCompilationFileCount = 3)
{
var parsedAttribute = CSharpSyntaxTree.ParseText(expectedCode);

var inputCompilation = CreateCompilation(inputSource);

GeneratorDriver driver = CSharpGeneratorDriver.Create(new OneOfGenerator());

driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var outputCompilation, out var diagnostics);

Assert.True(diagnostics.IsEmpty);

Assert.Equal(expectedCompilationFileCount, outputCompilation.SyntaxTrees.Count());

Assert.Empty(outputCompilation.GetDiagnostics());

var compiledAttribute = outputCompilation.SyntaxTrees.Single(e => e.FilePath.Contains(generatedFileName));

Assert.True(parsedAttribute.IsEquivalentTo(compiledAttribute));

Assert.True(outputCompilation.GetDiagnostics().IsEmpty);
}

private static void AssertDiagnosticErrorIsReturned(string inputSource, string diagnosticId)
{
var inputCompilation = CreateCompilation(inputSource);

GeneratorDriver driver = CSharpGeneratorDriver.Create(new OneOfGenerator());

driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out _, out var diagnostics);

Assert.Contains(diagnostics, d => d.Id == diagnosticId && d.Severity == DiagnosticSeverity.Error);
}

private static Compilation CreateCompilation(string source)
{
var references = new List<MetadataReference>
{
MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location),
MetadataReference.CreateFromFile(typeof(OneOfBase<>).GetTypeInfo().Assembly.Location),
MetadataReference.CreateFromFile(AppDomain.CurrentDomain.GetAssemblies().Single(a => a.GetName().Name == "netstandard").Location)
};

//https://github.com/dotnet/roslyn/issues/49498#issuecomment-734452762
foreach (var assembly in Assembly.GetEntryAssembly()!.GetReferencedAssemblies())
{
references.Add(MetadataReference.CreateFromFile(Assembly.Load(assembly).Location));
}

return CSharpCompilation.Create("compilation", new[] { CSharpSyntaxTree.ParseText(source) }, references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.XUnit" Version="1.1.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\OneOf.SourceGenerator\OneOf.SourceGenerator.csproj" />
<ProjectReference Include="..\OneOf\OneOf.csproj" />
</ItemGroup>

</Project>
9 changes: 8 additions & 1 deletion OneOf.SourceGenerator/GeneratorDiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace OneOf
{
internal class GeneratorDiagnosticDescriptors
public class GeneratorDiagnosticDescriptors
{
public static readonly DiagnosticDescriptor TopLevelError = new(id: "ONEOFGEN001",
title: "Class must be top level",
Expand Down Expand Up @@ -31,5 +31,12 @@ internal class GeneratorDiagnosticDescriptors
category: "OneOfGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor UserDefinedConversionsToOrFromAnInterfaceAreNotAllowed = new(id: "ONEOFGEN005",
title: "user-defined conversions to or from an interface are not allowed",
messageFormat: "user-defined conversions to or from an interface are not allowed",
category: "OneOfGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);
}
}
38 changes: 21 additions & 17 deletions OneOf.SourceGenerator/OneOfGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ internal sealed class {AttributeName} : Attribute
{{
}}
}}
";
";

public void Execute(GeneratorExecutionContext context)
{
Expand Down Expand Up @@ -75,43 +75,49 @@ public void Execute(GeneratorExecutionContext context)
}
}

private static string? ProcessClass(INamedTypeSymbol classSymbol, GeneratorExecutionContext context,
Location? attributeLocation)
private static string? ProcessClass(INamedTypeSymbol classSymbol, GeneratorExecutionContext context, Location? attributeLocation)
{
attributeLocation ??= Location.None;

if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default))
{
context.ReportDiagnostic(Diagnostic.Create(GeneratorDiagnosticDescriptors.TopLevelError,
attributeLocation, classSymbol.Name, DiagnosticSeverity.Error));
CreateDiagnosticError(GeneratorDiagnosticDescriptors.TopLevelError);
return null;
}

if (classSymbol.BaseType is null || classSymbol.BaseType.Name != "OneOfBase" ||
classSymbol.BaseType.ContainingNamespace.ToString() != "OneOf")
if (classSymbol.BaseType is null || classSymbol.BaseType.Name != "OneOfBase" || classSymbol.BaseType.ContainingNamespace.ToString() != "OneOf")
{
context.ReportDiagnostic(Diagnostic.Create(GeneratorDiagnosticDescriptors.WrongBaseType,
attributeLocation, classSymbol.Name, DiagnosticSeverity.Error));
CreateDiagnosticError(GeneratorDiagnosticDescriptors.WrongBaseType);
return null;
}

if (classSymbol.DeclaredAccessibility != Accessibility.Public)
{
context.ReportDiagnostic(Diagnostic.Create(GeneratorDiagnosticDescriptors.ClassIsNotPublic,
attributeLocation, classSymbol.Name, DiagnosticSeverity.Error));
CreateDiagnosticError(GeneratorDiagnosticDescriptors.ClassIsNotPublic);
return null;
}

ImmutableArray<ITypeSymbol> typeArguments = classSymbol.BaseType.TypeArguments;

if (typeArguments.Any(x => x.Name == nameof(Object)))
foreach (ITypeSymbol typeSymbol in typeArguments)
{
context.ReportDiagnostic(Diagnostic.Create(GeneratorDiagnosticDescriptors.ObjectIsOneOfType,
attributeLocation, classSymbol.Name, DiagnosticSeverity.Error));
return null;
if (typeSymbol.Name == nameof(Object))
{
CreateDiagnosticError(GeneratorDiagnosticDescriptors.ObjectIsOneOfType);
return null;
}

if (typeSymbol.TypeKind == TypeKind.Interface)
{
CreateDiagnosticError(GeneratorDiagnosticDescriptors.UserDefinedConversionsToOrFromAnInterfaceAreNotAllowed);
return null;
}
}

return GenerateClassSource(classSymbol, classSymbol.BaseType.TypeParameters, typeArguments);

void CreateDiagnosticError(DiagnosticDescriptor descriptor)
=> context.ReportDiagnostic(Diagnostic.Create(descriptor, attributeLocation, classSymbol.Name, DiagnosticSeverity.Error));
}

private static string GenerateClassSource(INamedTypeSymbol classSymbol,
Expand All @@ -125,8 +131,6 @@ private static string GenerateClassSource(INamedTypeSymbol classSymbol,
string classNameWithGenericTypes = $"{classSymbol.Name}{GetOpenGenericPart(classSymbol)}";

StringBuilder source = new($@"// <auto-generated />
using System;
#pragma warning disable 1591
namespace {classSymbol.ContainingNamespace.ToDisplayString()}
Expand Down
14 changes: 10 additions & 4 deletions OneOf.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30907.101
# Visual Studio Version 17
VisualStudioVersion = 17.3.32611.2
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OneOf.Tests", "OneOf.Tests\OneOf.Tests.csproj", "{82023ED0-3E20-43CF-ACA1-1EA547E70903}"
ProjectSection(ProjectDependencies) = postProject
Expand All @@ -16,9 +16,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OneOf.Extended", "OneOf.Ext
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Generator", "Generator\Generator.csproj", "{508CDAF6-E780-459E-BD8F-776A5EE2C2FF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OneOf.SourceGenerator", "OneOf.SourceGenerator\OneOf.SourceGenerator.csproj", "{AC54E93D-1DB2-4143-A1AF-3CE2492EAC83}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OneOf.SourceGenerator", "OneOf.SourceGenerator\OneOf.SourceGenerator.csproj", "{AC54E93D-1DB2-4143-A1AF-3CE2492EAC83}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OneOf.SourceGenerator.Tests", "OneOf.SourceGenerator.Tests\OneOf.SourceGenerator.Tests.csproj", "{A7D18F0E-8966-4685-8146-34F507356F5D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OneOf.SourceGenerator.Tests", "OneOf.SourceGenerator.Tests\OneOf.SourceGenerator.Tests.csproj", "{A7D18F0E-8966-4685-8146-34F507356F5D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OneOf.SourceGenerator.AnalyzerTests", "OneOf.SourceGenerator.AnalyzerTests\OneOf.SourceGenerator.AnalyzerTests.csproj", "{C08F270E-157A-48B9-A7B6-C948FCFC5494}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -54,6 +56,10 @@ Global
{A7D18F0E-8966-4685-8146-34F507356F5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A7D18F0E-8966-4685-8146-34F507356F5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A7D18F0E-8966-4685-8146-34F507356F5D}.Release|Any CPU.Build.0 = Release|Any CPU
{C08F270E-157A-48B9-A7B6-C948FCFC5494}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C08F270E-157A-48B9-A7B6-C948FCFC5494}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C08F270E-157A-48B9-A7B6-C948FCFC5494}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C08F270E-157A-48B9-A7B6-C948FCFC5494}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down

0 comments on commit 12745ca

Please sign in to comment.