Skip to content

Commit

Permalink
Fixed source generators for use in Visual Studio
Browse files Browse the repository at this point in the history
  • Loading branch information
jezzsantos committed Jan 12, 2024
1 parent a1fa350 commit 9a22fb9
Show file tree
Hide file tree
Showing 9 changed files with 87 additions and 63 deletions.
8 changes: 6 additions & 2 deletions README_DERIVATIVE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@
You will need the following development tools to build, run, and test this project:

* Windows or MacOS.
* Jetbrains Rider or Visual Studio (There is most support for JetBrains dotUltimate)
* JetBrains Rider (recommended) or Visual Studio
* Note: Using JetBrains Rider is recommended since the codebase includes more comprehensive customized tooling (coding standards, live templates, etc.)
* Note: If using Visual Studio, you will need to install the additional component `.NET Compiler Platform SDK` in order to run the built-in Roslyn source generators.
* Note: if using Visual Studio, the built-in Roslyn analyzers will not work (due to .netstandard2.0 [restrictions between Visual Studio and Roslyn](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview))

* Install the .NET7.0 SDK (specifically version 7.0.14). Available for [download here](https://dotnet.microsoft.com/en-us/download/dotnet/7.0)

> We have ensured that you wont need any other infrastructure running on your local machine (i.e. SQLServer database), unless you want to run infrastructure specific integration tests.
> We have ensured that you won't need any other infrastructure running on your local machine (i.e., a Microsoft SQLServer database) unless you want to run infrastructure-specific integration tests.
# Setup Environment

Expand Down
14 changes: 10 additions & 4 deletions src/Infrastructure.Web.Api.Interfaces/RouteAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
using System.ComponentModel;
#if !NETSTANDARD2_0
using System.Diagnostics.CodeAnalysis;
#endif

namespace Infrastructure.Web.Api.Interfaces;

/// <summary>
/// Provides a declarative way to define a REST route and service operation
/// Provides a declarative way to define a REST route service operation, and configuration
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class RouteAttribute : Attribute
{
public RouteAttribute([StringSyntax("Route")] string routeTemplate, ServiceOperation operation,
public RouteAttribute(
#if !NETSTANDARD2_0
[StringSyntax("Route")]
#endif
string routeTemplate, ServiceOperation operation,
AccessType access = AccessType.Anonymous, bool isTestingOnly = false)
{
if (!Enum.IsDefined(typeof(ServiceOperation), operation))
Expand All @@ -23,10 +29,10 @@ public RouteAttribute([StringSyntax("Route")] string routeTemplate, ServiceOpera
IsTestingOnly = isTestingOnly;
}

public bool IsTestingOnly { get; }

public AccessType Access { get; }

public bool IsTestingOnly { get; }

public ServiceOperation Operation { get; }

public string RouteTemplate { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,47 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
using WebApi_MinimalApiMediatRGenerator = Generators::Tools.Generators.WebApi.MinimalApiMediatRGenerator;
using MinimalApiMediatRGenerator = Generators::Tools.Generators.WebApi.MinimalApiMediatRGenerator;

namespace Tools.Generators.WebApi.UnitTests;

[UsedImplicitly]
public class MinimalApiMediatRGeneratorSpec
{
private static readonly string[]
AdditionalCompilationAssemblies =
{ "System.Runtime.dll", "netstandard.dll" }; //HACK: required to analyze custom attributes

private static CSharpCompilation CreateCompilation(string sourceCode)
{
var assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location)!;

var references = new List<MetadataReference>
{
MetadataReference.CreateFromFile(typeof(MinimalApiMediatRGenerator).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location)
};
AdditionalCompilationAssemblies.ToList()
.ForEach(item => references.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, item))));
var compilation = CSharpCompilation.Create("compilation",
new[]
{
CSharpSyntaxTree.ParseText(sourceCode)
},
new[]
{
MetadataReference.CreateFromFile(typeof(WebApi_MinimalApiMediatRGenerator).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location),
MetadataReference.CreateFromFile(Path.Combine(assemblyPath,
"System.Runtime.dll")) //HACK: this is required to make custom attributes work
},
references,
new CSharpCompilationOptions(OutputKind.ConsoleApplication));

return compilation;
}

[Trait("Category", "Unit")]
public class GivenAServiceCLass
public class GivenAServiceClass
{
private GeneratorDriver _driver;

public GivenAServiceCLass()
public GivenAServiceClass()
{
var generator = new WebApi_MinimalApiMediatRGenerator();
var generator = new MinimalApiMediatRGenerator();
_driver = CSharpGeneratorDriver.Create(generator);
}

Expand Down
21 changes: 12 additions & 9 deletions src/Tools.Generators.WebApi.UnitTests/WebApiAssemblyVisitorSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,26 @@ namespace Tools.Generators.WebApi.UnitTests;
public class WebApiAssemblyVisitorSpec
{
private const string CompilationSourceCode = "";
private static readonly string[]
AdditionalCompilationAssemblies =
{ "System.Runtime.dll", "netstandard.dll" }; //HACK: required to analyze use custom attributes

private static CSharpCompilation CreateCompilation(string sourceCode)
{
var assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location)!;

var references = new List<MetadataReference>
{
MetadataReference.CreateFromFile(typeof(WebApiAssemblyVisitor).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location)
};
AdditionalCompilationAssemblies.ToList()
.ForEach(item => references.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, item))));
var compilation = CSharpCompilation.Create("compilation",
new[]
{
CSharpSyntaxTree.ParseText(sourceCode)
},
new[]
{
MetadataReference.CreateFromFile(typeof(WebApiAssemblyVisitor).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location),
MetadataReference.CreateFromFile(Path.Combine(assemblyPath,
"System.Runtime.dll")) //HACK: this is required to make custom attributes work
},
references,
new CSharpCompilationOptions(OutputKind.ConsoleApplication));

return compilation;
Expand Down Expand Up @@ -371,7 +374,7 @@ namespace ANamespace;
public class AResponse : IWebResponse
{
}
[Route("aroute", ServiceOperation.Get)]
[Infrastructure.Web.Api.Interfaces.RouteAttribute("aroute", ServiceOperation.Get)]
public class ARequest : IWebRequest<AResponse>
{
}
Expand Down
4 changes: 2 additions & 2 deletions src/Tools.Generators.WebApi/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ public static class StringExtensions
/// <summary>
/// Whether the string value contains no value: it is either: null, empty or only whitespaces
/// </summary>
public static bool HasNoValue(this string? value)
public static bool HasNoValue(this string value)
{
return string.IsNullOrEmpty(value) || string.IsNullOrWhiteSpace(value);
}

/// <summary>
/// Whether the string value contains any value except: null, empty or only whitespaces
/// </summary>
public static bool HasValue(this string? value)
public static bool HasValue(this string value)
{
return !string.IsNullOrEmpty(value) && !string.IsNullOrWhiteSpace(value);
}
Expand Down
7 changes: 3 additions & 4 deletions src/Tools.Generators.WebApi/Extensions/SymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ namespace Tools.Generators.WebApi.Extensions;

public static class SymbolExtensions
{
public static AttributeData? GetAttribute(this ISymbol symbol, INamedTypeSymbol attributeType)
public static AttributeData GetAttribute(this ISymbol symbol, INamedTypeSymbol attributeType)
{
return symbol.GetAttributes()
.FirstOrDefault(attribute => attribute.AttributeClass!.IsOfType(attributeType));
}

public static INamedTypeSymbol? GetBaseType(this ITypeSymbol symbol, INamedTypeSymbol baseType)
public static INamedTypeSymbol GetBaseType(this ITypeSymbol symbol, INamedTypeSymbol baseType)
{
return symbol.AllInterfaces.FirstOrDefault(@interface => @interface.IsOfType(baseType));
}
Expand Down Expand Up @@ -45,7 +45,7 @@ public static IEnumerable<string> GetUsingNamespaces(this INamedTypeSymbol symbo

return usingSyntaxes.Select(us => us.Name!.ToString())
.Distinct()
.OrderDescending()
.OrderByDescending(s => s)
.ToList();
}

Expand All @@ -61,7 +61,6 @@ public static bool IsConcreteInstanceClass(this INamedTypeSymbol symbol)

public static bool IsDerivedFrom(this ITypeSymbol symbol, INamedTypeSymbol baseType)
{
ArgumentNullException.ThrowIfNull(baseType);
return symbol.AllInterfaces.Any(@interface => @interface.IsOfType(baseType));
}

Expand Down
4 changes: 2 additions & 2 deletions src/Tools.Generators.WebApi/MinimalApiMediatRGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public void Execute(GeneratorExecutionContext context)

return;

static string BuildFile(string? assemblyNamespace, string allUsingNamespaces, string allEndpointRegistrations,
static string BuildFile(string assemblyNamespace, string allUsingNamespaces, string allEndpointRegistrations,
string allHandlerClasses)
{
return $@"// <auto-generated/>
Expand Down Expand Up @@ -221,7 +221,7 @@ private static void BuildHandlerClasses(
handlerClasses.AppendLine();
}

private static string BuildInjectorConstructorAndFields(string? handlerClassName,
private static string BuildInjectorConstructorAndFields(string handlerClassName,
List<WebApiAssemblyVisitor.Constructor> constructors)
{
var handlerClassConstructorAndFields = new StringBuilder();
Expand Down
4 changes: 3 additions & 1 deletion src/Tools.Generators.WebApi/Tools.Generators.WebApi.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>netstandard2.0</TargetFramework> <!-- Source Generators must be netstandard2.0 to work in Visual Studio -->
<LangVersion>latest</LangVersion>
<Nullable>disable</Nullable>
<IsPlatformProject>true</IsPlatformProject>
<IsRoslynComponent>true</IsRoslynComponent>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
Expand Down
61 changes: 33 additions & 28 deletions src/Tools.Generators.WebApi/WebApiAssemblyVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ namespace Tools.Generators.WebApi;
public class WebApiAssemblyVisitor : SymbolVisitor
{
internal static readonly string[] IgnoredNamespaces =
{ "System", "Microsoft", "MediatR", "MessagePack", "NerdBank*" };
["System", "Microsoft", "MediatR", "MessagePack", "NerdBank*"];

private readonly CancellationToken _cancellationToken;
private readonly INamedTypeSymbol _cancellationTokenSymbol;
Expand Down Expand Up @@ -205,24 +205,24 @@ TypeName GetServiceName()
return new TypeName(symbol.ContainingNamespace.ToDisplayString(), symbol.Name);
}

static ServiceOperation FromOperationVerb(string? operation)
static ServiceOperation FromOperationVerb(string operation)
{
if (operation is null)
{
return ServiceOperation.Get;
}

return Enum.Parse<ServiceOperation>(operation, true);
return (ServiceOperation)Enum.Parse(typeof(ServiceOperation), operation, true);
}

static AccessType FromAccessType(string? access)
static AccessType FromAccessType(string access)
{
if (access is null)
{
return AccessType.Anonymous;
}

return Enum.Parse<AccessType>(access, true);
return (AccessType)Enum.Parse(typeof(AccessType), access, true);
}

// We assume that the request type derives from IWebRequest<TResponse>
Expand Down Expand Up @@ -336,7 +336,7 @@ bool HasWrongSetOfParameters(IMethodSymbol method)
}

// We assume that the request DTO it is decorated with a RouteAttribute
bool HasRouteAttribute(IMethodSymbol method, out AttributeData? routeAttribute)
bool HasRouteAttribute(IMethodSymbol method, out AttributeData routeAttribute)
{
var parameters = method.Parameters;
if (parameters.Length == 0)
Expand All @@ -353,35 +353,33 @@ bool HasRouteAttribute(IMethodSymbol method, out AttributeData? routeAttribute)

public record ServiceOperationRegistration
{
public required ApiServiceClassRegistration Class { get; init; }
public ApiServiceClassRegistration Class { get; set; }

public required bool HasCancellationToken { get; init; }
public bool HasCancellationToken { get; set; }

public required bool IsAsync { get; init; }
public bool IsAsync { get; set; }

public required bool IsTestingOnly { get; init; }
public bool IsTestingOnly { get; set; }

public string? MethodBody { get; set; }
public string MethodBody { get; set; }

public required string MethodName { get; init; }
public string MethodName { get; set; }

public required AccessType OperationAccess { get; init; }
public AccessType OperationAccess { get; set; }

public required ServiceOperation OperationType { get; init; }
public ServiceOperation OperationType { get; set; }

public required TypeName RequestDtoType { get; init; }
public TypeName RequestDtoType { get; set; }

public required TypeName ResponseDtoType { get; init; }
public TypeName ResponseDtoType { get; set; }

public required string RoutePath { get; init; }
public string RoutePath { get; set; }
}

public record TypeName
{
public TypeName(string @namespace, string name)
{
ArgumentException.ThrowIfNullOrEmpty(@namespace);
ArgumentException.ThrowIfNullOrEmpty(name);
Namespace = @namespace;
Name = name;
}
Expand All @@ -392,7 +390,7 @@ public TypeName(string @namespace, string name)

public string Namespace { get; }

public virtual bool Equals(TypeName? other)
public virtual bool Equals(TypeName other)
{
if (ReferenceEquals(null, other))
{
Expand All @@ -409,32 +407,39 @@ public virtual bool Equals(TypeName? other)

public override int GetHashCode()
{
#if NETSTANDARD2_0
var hash = 17;
hash = hash * 23 + Namespace.GetHashCode();
hash = hash * 23 + Name.GetHashCode();
return hash;
#else
return HashCode.Combine(Namespace, Name);
#endif
}
}

public record ApiServiceClassRegistration
{
public IEnumerable<Constructor> Constructors { get; init; } = new List<Constructor>();
public IEnumerable<Constructor> Constructors { get; set; } = new List<Constructor>();

public required TypeName TypeName { get; init; }
public TypeName TypeName { get; set; }

public IEnumerable<string> UsingNamespaces { get; init; } = new List<string>();
public IEnumerable<string> UsingNamespaces { get; set; } = new List<string>();
}

public record Constructor
{
public IEnumerable<ConstructorParameter> CtorParameters { get; init; } = new List<ConstructorParameter>();
public IEnumerable<ConstructorParameter> CtorParameters { get; set; } = new List<ConstructorParameter>();

public required bool IsInjectionCtor { get; init; }
public bool IsInjectionCtor { get; set; }

public string? MethodBody { get; set; }
public string MethodBody { get; set; }
}

public record ConstructorParameter
{
public required TypeName TypeName { get; init; }
public TypeName TypeName { get; set; }

public required string VariableName { get; init; }
public string VariableName { get; set; }
}
}

0 comments on commit 9a22fb9

Please sign in to comment.