Skip to content

Commit

Permalink
refactor: Use .NET 5 Source Generators (#122)
Browse files Browse the repository at this point in the history
Replace CodeGeneration.Roslyn with Source Generators (Roslyn built-in feature). Thanks for all the 🐟.

* build: Upgrade to .NET 5 RC1 SDK

* wip: Create first SourceGenerator

* refactor: Replace CG.R with .NET SourceGenerator

* refactor: Cleanup CG.R references

* fix: Add missing nuget source

* tests: Run tests on .NET 5 RC1 (target net5)

* fix: Nullability suppressions in tests
  • Loading branch information
amis92 committed Sep 28, 2020
1 parent f67fb02 commit f8cd264
Show file tree
Hide file tree
Showing 23 changed files with 281 additions and 355 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
@@ -1,7 +1,7 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

<PropertyGroup>
<LangVersion>8</LangVersion>
<LangVersion>9</LangVersion>
<Nullable>enable</Nullable>
<Authors>Amadeusz Sadowski</Authors>
<Company>WarHub</Company>
Expand Down
9 changes: 4 additions & 5 deletions Packages.props
Expand Up @@ -3,9 +3,8 @@

<ItemGroup>
<PackageReference Update="Amadevus.RecordGenerator" Version="[0.6.0]" PrivateAssets="all" />
<PackageReference Update="CodeGeneration.Roslyn" Version="[0.7.63]" />
<PackageReference Update="CodeGeneration.Roslyn.Attributes" Version="[0.7.63]" PrivateAssets="all" ExcludeAssets="runtime" />
<PackageReference Update="CodeGeneration.Roslyn.Tool" Version="[0.7.63]" PrivateAssets="all" />
<PackageReference Update="Microsoft.CodeAnalysis.CSharp" Version="3.8.0-3.final" PrivateAssets="all" />
<PackageReference Update="Microsoft.CodeAnalysis.Analyzers" Version="3.0.0" PrivateAssets="all" />
<PackageReference Update="morelinq" Version="3.2.0" />
<PackageReference Update="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Update="Optional" Version="4.0.0" />
Expand All @@ -17,7 +16,7 @@

<ItemGroup Label="Tests">
<PackageReference Update="FluentAssertions" Version="[5.7.0]" />
<PackageReference Update="Microsoft.NET.Test.Sdk" Version="[16.2.0]" />
<PackageReference Update="Microsoft.NET.Test.Sdk" Version="[16.7.1]" />
<PackageReference Update="NSubstitute" Version="[3.1.0]" />
<PackageReference Update="XmlDiffPatch.Core" Version="[1.0.1]" />
<PackageReference Update="xunit" Version="[2.4.1]" />
Expand All @@ -31,7 +30,7 @@
Version="1.0.0"
Condition=" !$(IsTestingOnlyProject) " />
<GlobalPackageReference Include="Nerdbank.GitVersioning"
Version="3.0.26" />
Version="3.2.31" />
<GlobalPackageReference Include="Nullable" Version="1.2.1" IncludeAssets="contentFiles" />
</ItemGroup>

Expand Down
2 changes: 1 addition & 1 deletion global.json
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "3.1.302",
"version": "5.0.100-rc.1.20452.10",
"rollForward": "feature"
},
"msbuild-sdks": {
Expand Down
Expand Up @@ -7,8 +7,4 @@
<RootNamespace>WarHub.ArmouryModel.Source</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CodeGeneration.Roslyn.Attributes" />
</ItemGroup>

</Project>
@@ -1,11 +1,9 @@
using System;
using System.Diagnostics;
using CodeGeneration.Roslyn;

namespace WarHub.ArmouryModel.Source
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
[CodeGenerationAttribute("WarHub.ArmouryModel.Source.CodeGeneration.WhamNodeGenerator, WarHub.ArmouryModel.Source.CodeGeneration")]
[Conditional("CodeGeneration")]
public sealed class WhamNodeCoreAttribute : Attribute
{
Expand Down
150 changes: 8 additions & 142 deletions src/WarHub.ArmouryModel.Source.CodeGeneration/CoreDescriptorBuilder.cs
@@ -1,10 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using CodeGeneration.Roslyn;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand All @@ -13,107 +9,22 @@ namespace WarHub.ArmouryModel.Source.CodeGeneration
{
internal class CoreDescriptorBuilder
{
private const string ImmutableArrayMetadataName = "System.Collections.Immutable.ImmutableArray`1";
private const string WhamNodeCoreAttributeMetadataName = "WarHub.ArmouryModel.Source.WhamNodeCoreAttribute";

private static SymbolCache<INamedTypeSymbol> immutableArraySymbolCache = new SymbolCache<INamedTypeSymbol>(ImmutableArrayMetadataName);
private static SymbolCache<INamedTypeSymbol> whamNodeCoreAttributeSymbolCache = new SymbolCache<INamedTypeSymbol>(WhamNodeCoreAttributeMetadataName);

public CoreDescriptorBuilder(TransformationContext context, CancellationToken cancellationToken)
{
SemanticModel = context.SemanticModel;
Compilation = context.Compilation;
TypeDeclaration = (ClassDeclarationSyntax)context.ProcessingNode;
CancellationToken = cancellationToken;
UpdateNamedTypeSymbolCache(ref immutableArraySymbolCache, Compilation);
UpdateNamedTypeSymbolCache(ref whamNodeCoreAttributeSymbolCache, Compilation);
TypeSymbol = GetNamedTypeSymbol(TypeDeclaration) ?? throw new ArgumentException("Type is not a named type.");
}

private SemanticModel SemanticModel { get; }
private CSharpCompilation Compilation { get; }
private ClassDeclarationSyntax TypeDeclaration { get; }
private CancellationToken CancellationToken { get; }
public static INamedTypeSymbol ImmutableArraySymbol => immutableArraySymbolCache.Symbol!;
public INamedTypeSymbol TypeSymbol { get; }

public CoreDescriptor CreateDescriptor()
{
var firstDeclaration =
NodeFromLocation(TypeSymbol.Locations[0])
.FirstAncestorOrSelf<ClassDeclarationSyntax>();

var attributeLists = GetClassAttributeLists().ToImmutableArray();

var properties =
GetCustomBaseTypesAndSelf(TypeSymbol)
.Reverse()
.SelectMany(GetAutoReadonlyPropertySymbols)
.Select(p =>
{
var x = p.DeclaringSyntaxReferences.FirstOrDefault();
var syntax = (PropertyDeclarationSyntax?)x?.GetSyntax();
var auto = syntax?.AccessorList?.Accessors.All(accessor => accessor.Body == null && accessor.ExpressionBody == null) ?? false;
return auto ? new { syntax = syntax!, symbol = p } : null;
})
.Where(x => x != null)
.Select(x => CreateRecordEntry(x!.symbol, x.syntax))
.ToImmutableArray();

return new CoreDescriptor(
TypeSymbol,
firstDeclaration.GetTypeSyntax().WithoutTrivia(),
firstDeclaration.Identifier.WithoutTrivia(),
properties,
attributeLists);
}

private static IEnumerable<INamedTypeSymbol> GetCustomBaseTypesAndSelf(INamedTypeSymbol self)
{
yield return self;
while (self.BaseType?.SpecialType == SpecialType.None)
{
self = self.BaseType;
yield return self;
}
}

private static IEnumerable<IPropertySymbol> GetAutoReadonlyPropertySymbols(INamedTypeSymbol namedTypeSymbol)
{
return namedTypeSymbol
.GetMembers()
.OfType<IPropertySymbol>()
.Where(p => p.IsReadOnly && !p.IsStatic && !p.IsIndexer && p.DeclaredAccessibility == Accessibility.Public);
}

private static SyntaxNode NodeFromLocation(Location l)
{
return l.SourceTree.GetRoot().FindNode(l.SourceSpan);
}

private IEnumerable<AttributeListSyntax> GetClassAttributeLists()
{
var xmlAttributeNames = new[] { Names.XmlType, Names.XmlRoot };
var attributes = TypeSymbol.Locations
.SelectMany(l => NodeFromLocation(l).FirstAncestorOrSelf<ClassDeclarationSyntax>().AttributeLists)
.SelectMany(list => list.Attributes)
.ToImmutableArray();
var xmlAttributes = attributes.Where(att => xmlAttributeNames.Any(name => att.IsNamed(name)));
return xmlAttributes.Select(att => SyntaxFactory.AttributeList().AddAttributes(att));
}

private static CoreDescriptor.Entry CreateRecordEntry(IPropertySymbol symbol, PropertyDeclarationSyntax syntax)
public static CoreDescriptor.Entry CreateRecordEntry(
IPropertySymbol symbol,
PropertyDeclarationSyntax syntax,
INamedTypeSymbol immutableArraySymbol,
INamedTypeSymbol attributeSymbol)
{
var typeSyntax = syntax.Type;
var typeIdentifier = syntax.Identifier.WithoutTrivia();
var attributes = GetPropertyAttributeLists(syntax).ToImmutableArray();
if (symbol.Type is INamedTypeSymbol namedType && namedType.SpecialType == SpecialType.None)
{
if (namedType.Arity == 1 && ImmutableArraySymbol.Equals(namedType.OriginalDefinition, SymbolEqualityComparer.IncludeNullability))
if (namedType.Arity == 1 && immutableArraySymbol.Equals(namedType.OriginalDefinition, SymbolEqualityComparer.IncludeNullability))
{
return new CoreDescriptor.CollectionEntry(symbol, typeIdentifier, (NameSyntax)typeSyntax, attributes);
}
if (namedType.GetAttributes().Any(a => a.AttributeClass.Equals(whamNodeCoreAttributeSymbolCache.Symbol, SymbolEqualityComparer.Default)))
if (namedType.GetAttributes().Any(a => a.AttributeClass?.Equals(attributeSymbol, SymbolEqualityComparer.Default) == true))
{
return new CoreDescriptor.ComplexEntry(symbol, typeIdentifier, typeSyntax, attributes);
}
Expand All @@ -128,50 +39,5 @@ private static IEnumerable<AttributeListSyntax> GetPropertyAttributeLists(Proper
var xmlAttributes = attributes.Where(att => xmlAttributeNames.Any(name => att.IsNamed(name)));
return xmlAttributes.Select(att => SyntaxFactory.AttributeList().AddAttributes(att));
}

private INamedTypeSymbol? GetNamedTypeSymbol(SyntaxNode node)
{
var namedTypeSymbol = SemanticModel.GetDeclaredSymbol(node, CancellationToken) as INamedTypeSymbol;
return namedTypeSymbol is IErrorTypeSymbol ? null : namedTypeSymbol;
}

private static void UpdateNamedTypeSymbolCache(ref SymbolCache<INamedTypeSymbol> cache, CSharpCompilation compilation)
{
if (cache.Compilation != compilation)
{
cache = new SymbolCache<INamedTypeSymbol>(
compilation.GetTypeByMetadataName(cache.FullMetadataName),
compilation,
cache.FullMetadataName);
}
}

private readonly struct SymbolCache<T>
{
public SymbolCache(string fullMetadataName)
{
FullMetadataName = fullMetadataName;
Symbol = default;
Compilation = null;
}

public SymbolCache(T symbol, CSharpCompilation compilation, string fullMetadataName)
{
FullMetadataName = fullMetadataName;
Symbol = symbol;
Compilation = compilation;
}

[AllowNull, MaybeNull]
public T Symbol { get; }
public CSharpCompilation? Compilation { get; }
public string FullMetadataName { get; }

public void Deconstruct([MaybeNull] out T symbol, out CSharpCompilation? compilation)
{
symbol = Symbol;
compilation = Compilation;
}
}
}
}

0 comments on commit f8cd264

Please sign in to comment.