Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor code to be more performant and cleaner. #169

Merged
merged 9 commits into from
Jan 18, 2024
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
4 changes: 2 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/bin/${input:rememberConfig}/net7.0/BuilderDemo.dll",
"program": "${workspaceFolder}/bin/${input:rememberConfig}/net8.0/BuilderDemo.dll",
"args": [],
"cwd": "${workspaceFolder}/src/BuilderDemo",
"console": "internalConsole",
Expand All @@ -21,7 +21,7 @@
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/bin/${input:rememberConfig}/net7.0/BuilderGenerator.Tests.dll",
"program": "${workspaceFolder}/bin/${input:rememberConfig}/net8.0/BuilderGenerator.Tests.dll",
"args": [],
"cwd": "${workspaceFolder}/tests/BuilderGenerator.Tests",
"console": "internalConsole",
Expand Down
10 changes: 9 additions & 1 deletion src/BuilderDemo/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,16 @@
"resolved": "0.3.1",
"contentHash": "Q7N3nziye+vGsDPcrKrPFjlaFhrM+1adtJcpjFeq4ufBB+uKc4kRezSK03382xpE8iNQbiRHUl+z31NW0W18FQ=="
},
"Microsoft.Bcl.HashCode": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "MalY0Y/uM/LjXtHfX/26l2VtN4LDNZ2OE3aumNOHDLsT4fNYy2hiHXI4CXCqKpNUNm7iJ2brrc4J89UdaL56FA=="
},
"buildergenerator": {
"type": "Project"
"type": "Project",
"dependencies": {
"Microsoft.Bcl.HashCode": "[1.1.1, )"
}
}
}
}
Expand Down
103 changes: 29 additions & 74 deletions src/BuilderGenerator/BuilderGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;

using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -43,98 +40,56 @@
context.RegisterPostInitializationOutput(ctx => ctx.AddSource(
"BuildableAttribute.g.cs", SourceText.From($"{Header}{AttributeText}", Encoding.UTF8)));

IncrementalValuesProvider<ClassDeclarationSyntax> classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
IncrementalValuesProvider<ClassToGenerate? > classesToGenerate = context.SyntaxProvider
.ForAttributeWithMetadataName(
BuildableAttribute,
// 👇 Runs for _every_ syntax node, on _every_ key press!
predicate: static (s, _) => IsSyntaxTargetForGeneration(s),
transform: static (ctx, _) => GetSemanticTargetForGeneration(ctx))
.Where(static m => m is not null)!;
// 👇 Runs for _every_ node selected by the predicate, on _every_ key press!
transform: static (ctx, _) => GetClassToGenerate(ctx.SemanticModel, ctx.TargetNode));

IncrementalValueProvider<(Compilation Left, ImmutableArray<ClassDeclarationSyntax> Right)> incValueProvider = context.CompilationProvider.Combine(classDeclarations.Collect());
context.RegisterSourceOutput(incValueProvider,
static (spc, source) => Execute(source.Left, source.Right, spc));

// 👇 Runs for every _new_ value returned by the syntax provider
context.RegisterImplementationSourceOutput(classesToGenerate,
static (spc, source) => Execute(source, spc));
}

public static bool IsSyntaxTargetForGeneration(SyntaxNode node)
=> node is ClassDeclarationSyntax { AttributeLists.Count: > 0 };

public static ClassDeclarationSyntax? GetSemanticTargetForGeneration(in GeneratorSyntaxContext context)
{
// we know the node is a cds thanks to IsSyntaxTargetForGeneration
var cds = (ClassDeclarationSyntax)context.Node;

// loop through all the attributes on the method
foreach (AttributeListSyntax attributeListSyntax in cds.AttributeLists)
{
foreach (AttributeSyntax attributeSyntax in attributeListSyntax.Attributes)
{
if (context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol is not IMethodSymbol attributeSymbol)
continue;

INamedTypeSymbol attributeContainingTypeSymbol = attributeSymbol.ContainingType;

// Is the attribute the attribute we are interested in?
if (attributeContainingTypeSymbol.ToDisplayString() == BuildableAttribute)
return cds;
}
}

return null;
}

public static void Execute(Compilation compilation, IEnumerable<ClassDeclarationSyntax> classes, in SourceProductionContext context)
private static ClassToGenerate? GetClassToGenerate(SemanticModel semanticModel, SyntaxNode classDeclarationSyntax)
{
if (!(classes?.Any() ?? false))
{
// nothing to do yet
return;
}
INamedTypeSymbol buildableSymbol = compilation.GetTypeByMetadataName(BuildableAttribute)!;

foreach (ClassDeclarationSyntax @class in classes)
{
if (context.CancellationToken.IsCancellationRequested)
return;

SemanticModel model = compilation.GetSemanticModel(@class.SyntaxTree, true);
if (model.GetDeclaredSymbol(@class) is not INamedTypeSymbol typeSymbol)
continue;

if (HasAttribute(typeSymbol, buildableSymbol))
Execute(context, typeSymbol);
}
return semanticModel.GetDeclaredSymbol(classDeclarationSyntax) is not INamedTypeSymbol typeSymbol
? null
: (ClassToGenerate?)new ClassToGenerate(typeSymbol);
}

private static bool HasAttribute(INamedTypeSymbol typeSymbol, INamedTypeSymbol attributeSymbol)
private static void Execute(ClassToGenerate? classToGenerate, SourceProductionContext context)
{
foreach (AttributeData attribute in typeSymbol.GetAttributes())
{
if (attribute.AttributeClass?.Equals(attributeSymbol, SymbolEqualityComparer.Default) == true)
return true;
}
return false;
}
if (context.CancellationToken.IsCancellationRequested)
return;

private static void Execute(in SourceProductionContext context, INamedTypeSymbol typeSymbol)
{
try
if (classToGenerate is { } value)
{
var source = TypeBuilderWriter.Write(typeSymbol);
var sourceText = SourceText.From(source, Encoding.UTF8);
context.ReportDiagnostic(Diagnostic.Create(s_successfullyGeneratedBuilderSource, Location.None, typeSymbol.Name));
var name = typeSymbol.Name;
if (typeSymbol.IsGenericType)
var typeName = value.TypeName;
try
{
var source = TypeBuilderWriter.Write(value);
var sourceText = SourceText.From(source, Encoding.UTF8);
context.ReportDiagnostic(Diagnostic.Create(s_successfullyGeneratedBuilderSource, Location.None, typeName));
var name = value.FullTypeName;
var idx = name.IndexOf('<');
if (idx > -1)
{
name = name.Substring(0, idx);
}
context.AddSource($"{name}Builder.g.cs", sourceText);
}
catch (Exception ex)

Check warning on line 89 in src/BuilderGenerator/BuilderGenerator.cs

View workflow job for this annotation

GitHub Actions / test-ubuntu-latest

Modify 'Execute' to catch a more specific allowed exception type, or rethrow the exception (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1031)

Check warning on line 89 in src/BuilderGenerator/BuilderGenerator.cs

View workflow job for this annotation

GitHub Actions / test-ubuntu-latest

Modify 'Execute' to catch a more specific allowed exception type, or rethrow the exception (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1031)

Check warning on line 89 in src/BuilderGenerator/BuilderGenerator.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Modify 'Execute' to catch a more specific allowed exception type, or rethrow the exception (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1031)

Check warning on line 89 in src/BuilderGenerator/BuilderGenerator.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Modify 'Execute' to catch a more specific allowed exception type, or rethrow the exception (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1031)

Check warning on line 89 in src/BuilderGenerator/BuilderGenerator.cs

View workflow job for this annotation

GitHub Actions / test-ubuntu-latest

Modify 'Execute' to catch a more specific allowed exception type, or rethrow the exception (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1031)

Check warning on line 89 in src/BuilderGenerator/BuilderGenerator.cs

View workflow job for this annotation

GitHub Actions / test-ubuntu-latest

Modify 'Execute' to catch a more specific allowed exception type, or rethrow the exception (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1031)
{
context.ReportDiagnostic(Diagnostic.Create(s_errorGeneratingBuilderSource, Location.None, typeName, ex.Message));
}
context.AddSource($"{name}Builder.g.cs", sourceText);
}
catch (Exception ex)
{
context.ReportDiagnostic(Diagnostic.Create(s_errorGeneratingBuilderSource, Location.None, typeSymbol.Name, ex.Message));
}
}
}
39 changes: 20 additions & 19 deletions src/BuilderGenerator/BuilderGenerator.csproj
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Label="Build">
<TargetFramework>netstandard2.0</TargetFramework>
<RoslynVersion>4.6.0</RoslynVersion>
<IsRoslynComponent>true</IsRoslynComponent>
<IncludeBuildOutput>false</IncludeBuildOutput>
<PropertyGroup Label="Build">
<TargetFramework>netstandard2.0</TargetFramework>
<RoslynVersion>4.6.0</RoslynVersion>
<IsRoslynComponent>true</IsRoslynComponent>
<IncludeBuildOutput>false</IncludeBuildOutput>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="IDisposableAnalyzers" Version="4.0.7">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Bcl.HashCode" Version="1.1.1" />
<PackageReference Include="IDisposableAnalyzers" Version="4.0.7">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
<PackageReference Include="ReflectionAnalyzers" Version="0.3.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<!-- This ensures the library will be packaged as a source generator when we use `dotnet pack` -->
<ItemGroup>
<PackageReference Include="ReflectionAnalyzers" Version="0.3.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<!-- This ensures the library will be packaged as a source generator when we use `dotnet pack` -->
<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="AnalyzerReleases.Shipped.md" />
<AdditionalFiles Include="AnalyzerReleases.Unshipped.md" />
Expand Down
60 changes: 60 additions & 0 deletions src/BuilderGenerator/ClassToGenerate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System.Collections.Generic;
using System.Collections.Immutable;

using Microsoft.CodeAnalysis;

namespace BuilderGenerator;
public readonly record struct ClassToGenerate
{
#pragma warning disable CA1051 // Do not declare visible instance fields
public readonly string FullTypeName;
public readonly string TypeName;
public readonly string ContainingNameSpace;

public readonly string BuilderClassName;
public readonly EquatableArray<PropertyInfo> Properties;
#pragma warning restore CA1051 // Do not declare visible instance fields

public ClassToGenerate(INamedTypeSymbol type)
{
TypeName = type.Name;
ContainingNameSpace = type.ContainingNamespace.ToDisplayString();
FullTypeName = GetTypeName(type, false);
BuilderClassName = GetTypeName(type, true);

List<PropertyInfo> propInfo = [];
foreach (ISymbol member in type.GetMembers())
{
if (member is IPropertySymbol propSymbol)
propInfo.Add(new PropertyInfo(propSymbol.Name, propSymbol.Type.ToString()));
}
Properties = new([.. propInfo]);
}

private static string GetTypeName(INamedTypeSymbol type, bool isBuilder)
{
var typeName = type.Name;
ImmutableArray<SymbolDisplayPart> displayParts = type.ToDisplayParts();
var numParts = displayParts.Length;

if (numParts == 0 || !type.IsGenericType || type.IsUnboundGenericType)
return isBuilder ? typeName + "Builder" : typeName;

var parts = new List<string>(numParts);
var capture = false;
for (var i = 0; i < numParts; i++)
{
var part = displayParts[i].ToString();
if (!capture && part == typeName && i + 2 < numParts && displayParts[i + 1].ToString() == "<")
{
capture = true;
if (isBuilder)
part += "Builder";
}
if (capture)
parts.Add(part);
}
return string.Concat(parts);
}

}
92 changes: 92 additions & 0 deletions src/BuilderGenerator/EquatableArray.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// <copyright file="EquatableArray.cs" company="Datadog">
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
// </copyright>

using System;
using System.Collections;
using System.Collections.Generic;

namespace BuilderGenerator;

/// <summary>
/// An immutable, equatable array. This is equivalent to <see cref="Array"/> but with value equality support.
/// </summary>
/// <typeparam name="T">The type of values in the array.</typeparam>
public readonly struct EquatableArray<T> : IEquatable<EquatableArray<T>>, IEnumerable<T>
where T : IEquatable<T>
{
/// <summary>
/// The underlying <typeparamref name="T"/> array.
/// </summary>
private readonly T[]? _array;

/// <summary>
/// Initializes a new instance of the <see cref="EquatableArray{T}"/> struct.
/// </summary>
/// <param name="array">The input array to wrap.</param>
public EquatableArray(T[] array) => _array = array;

/// <summary>
/// Gets the length of the array, or 0 if the array is null
/// </summary>
public int Count => _array?.Length ?? 0;

/// <summary>
/// Checks whether two <see cref="EquatableArray{T}"/> values are the same.
/// </summary>
/// <param name="left">The first <see cref="EquatableArray{T}"/> value.</param>
/// <param name="right">The second <see cref="EquatableArray{T}"/> value.</param>
/// <returns>Whether <paramref name="left"/> and <paramref name="right"/> are equal.</returns>
public static bool operator ==(EquatableArray<T> left, EquatableArray<T> right) => left.Equals(right);

/// <summary>
/// Checks whether two <see cref="EquatableArray{T}"/> values are not the same.
/// </summary>
/// <param name="left">The first <see cref="EquatableArray{T}"/> value.</param>
/// <param name="right">The second <see cref="EquatableArray{T}"/> value.</param>
/// <returns>Whether <paramref name="left"/> and <paramref name="right"/> are not equal.</returns>
public static bool operator !=(EquatableArray<T> left, EquatableArray<T> right) => !left.Equals(right);

/// <inheritdoc/>
public bool Equals(EquatableArray<T> other) => AsSpan().SequenceEqual(other.AsSpan());

/// <inheritdoc/>
public override bool Equals(object? obj) => obj is EquatableArray<T> array && Equals(this, array);

/// <inheritdoc/>
public override int GetHashCode()
{
if (_array is not T[] array)
{
return 0;
}

HashCode hashCode = default;

foreach (T item in array)
{
hashCode.Add(item);
}

return hashCode.ToHashCode();
}

/// <summary>
/// Returns a <see cref="ReadOnlySpan{T}"/> wrapping the current items.
/// </summary>
/// <returns>A <see cref="ReadOnlySpan{T}"/> wrapping the current items.</returns>
public ReadOnlySpan<T> AsSpan() => _array.AsSpan();

/// <summary>
/// Returns the underlying wrapped array.
/// </summary>
/// <returns>Returns the underlying array.</returns>
public T[]? AsArray() => _array;

/// <inheritdoc/>
IEnumerator<T> IEnumerable<T>.GetEnumerator() => ((IEnumerable<T>)(_array ?? [])).GetEnumerator();

/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<T>)(_array ?? [])).GetEnumerator();
}
10 changes: 10 additions & 0 deletions src/BuilderGenerator/PropertyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace BuilderGenerator;

public record PropertyInfo
{
public string PropertyName { get; }

public string PropertyType { get; }

public PropertyInfo(string name, string type) => (PropertyName, PropertyType) = (name, type);
}
Loading
Loading