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

Fix incremental source generator #22

Merged
merged 10 commits into from
Jan 17, 2024
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<PackageVersion Include="coverlet.collector" Version="6.0.0" />
<PackageVersion Include="Fody" Version="6.6.4" />
<PackageVersion Include="IsExternalInit" Version="1.0.3" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="1.1.1" />
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lsoft, soon. Most likely this week.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@GerardSmit, thanks for pointing that out. Will ensure it gets included. BTW, if any of the commits I made after your original PR don't make sense, let me know. We should have this merged soon.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything else look OK to me; using a ushort instead of a hacky wrapper struct is probably better. 😉

<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
Expand Down
8 changes: 8 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
coverage:
status:
project:
default:
threshold: 10%
patch:
default:
threshold: 10%
10 changes: 5 additions & 5 deletions src/Zomp.SyncMethodGenerator/AsyncToSyncRewriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ internal sealed class AsyncToSyncRewriter(SemanticModel semanticModel) : CSharpS
private readonly SemanticModel semanticModel = semanticModel;
private readonly HashSet<IParameterSymbol> removedParameters = [];
private readonly Dictionary<string, string> renamedLocalFunctions = [];
private readonly ImmutableArray<Diagnostic>.Builder diagnostics = ImmutableArray.CreateBuilder<Diagnostic>();
private readonly ImmutableArray<ReportedDiagnostic>.Builder diagnostics = ImmutableArray.CreateBuilder<ReportedDiagnostic>();

private enum SyncOnlyDirectiveType
{
Expand All @@ -77,7 +77,7 @@ private enum SpecialMethod
/// <summary>
/// Gets the diagnostics messages.
/// </summary>
public ImmutableArray<Diagnostic> Diagnostics => diagnostics.ToImmutable();
public ImmutableArray<ReportedDiagnostic> Diagnostics => diagnostics.ToImmutable();

/// <inheritdoc/>
public override SyntaxNode? VisitConditionalAccessExpression(ConditionalAccessExpressionSyntax node)
Expand Down Expand Up @@ -1307,7 +1307,7 @@ BinaryExpressionSyntax be

if (syncOnlyDirectiveType == SyncOnlyDirectiveType.Invalid)
{
var d = Diagnostic.Create(InvalidCondition, trivia.GetLocation(), trivia);
var d = ReportedDiagnostic.Create(InvalidCondition, trivia.GetLocation(), trivia.ToString());
diagnostics.Add(d);
return null;
}
Expand All @@ -1318,7 +1318,7 @@ BinaryExpressionSyntax be
{
if (isStackSyncOnly ^ syncOnlyDirectiveType == SyncOnlyDirectiveType.SyncOnly)
{
var d = Diagnostic.Create(InvalidNesting, trivia.GetLocation(), trivia);
var d = ReportedDiagnostic.Create(InvalidNesting, trivia.GetLocation(), trivia.ToString());
diagnostics.Add(d);
return null;
}
Expand Down Expand Up @@ -1378,7 +1378,7 @@ BinaryExpressionSyntax be
}
else
{
var d = Diagnostic.Create(InvalidElif, trivia.GetLocation(), trivia);
var d = ReportedDiagnostic.Create(InvalidElif, trivia.GetLocation(), trivia.ToString());
diagnostics.Add(d);
return null;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Zomp.SyncMethodGenerator/ClassDeclaration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
/// <param name="ClassName">Class name.</param>
/// <param name="Modifiers">A list of modifiers.</param>
/// <param name="TypeParameterListSyntax">A list of type parameters.</param>
internal sealed record ClassDeclaration(string ClassName, IEnumerable<SyntaxKind> Modifiers, TypeParameterListSyntax? TypeParameterListSyntax);
internal sealed record ClassDeclaration(string ClassName, EquatableArray<ushort> Modifiers, EquatableArray<string> TypeParameterListSyntax);
202 changes: 202 additions & 0 deletions src/Zomp.SyncMethodGenerator/Helpers/EquatableArray.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.Diagnostics.CodeAnalysis;

namespace Zomp.SyncMethodGenerator.Helpers;

/// <summary>
/// An immutable, equatable array. This is equivalent to <see cref="ImmutableArray{T}"/> but with value equality support.
/// </summary>
/// <typeparam name="T">The type of values in the array.</typeparam>
/// <remarks>
/// Modified from: https://github.com/dotnet/runtime/issues/77183#issuecomment-1284577055.
/// Remove this struct when the issue above is resolved.
/// </remarks>
[ExcludeFromCodeCoverage]
internal 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 <see cref="ImmutableArray{T}"/> to wrap.</param>
public EquatableArray(ImmutableArray<T> array)
{
this.array = Unsafe.As<ImmutableArray<T>, T[]?>(ref array);
}

/// <summary>
/// Gets a value indicating whether the current array is empty.
/// </summary>
public bool IsEmpty
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => AsImmutableArray().IsEmpty;
}

/// <summary>
/// Gets a reference to an item at a specified position within the array.
/// </summary>
/// <param name="index">The index of the item to retrieve a reference to.</param>
/// <returns>A reference to an item at a specified position within the array.</returns>
public ref readonly T this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => ref AsImmutableArray().ItemRef(index);
}

/// <summary>
/// Implicitly converts an <see cref="ImmutableArray{T}"/> to <see cref="EquatableArray{T}"/>.
/// </summary>
/// <returns>An <see cref="EquatableArray{T}"/> instance from a given <see cref="ImmutableArray{T}"/>.</returns>
public static implicit operator EquatableArray<T>(ImmutableArray<T> array)
{
return FromImmutableArray(array);
}

/// <summary>
/// Implicitly converts an <see cref="EquatableArray{T}"/> to <see cref="ImmutableArray{T}"/>.
/// </summary>
/// <returns>An <see cref="ImmutableArray{T}"/> instance from a given <see cref="EquatableArray{T}"/>.</returns>
public static implicit operator ImmutableArray<T>(EquatableArray<T> array)
{
return array.AsImmutableArray();
}

/// <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)
{
return 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)
{
return !left.Equals(right);
}

/// <summary>
/// Creates an <see cref="EquatableArray{T}"/> instance from a given <see cref="ImmutableArray{T}"/>.
/// </summary>
/// <param name="array">The input <see cref="ImmutableArray{T}"/> instance.</param>
/// <returns>An <see cref="EquatableArray{T}"/> instance from a given <see cref="ImmutableArray{T}"/>.</returns>
public static EquatableArray<T> FromImmutableArray(ImmutableArray<T> array)
{
return new(array);
}

/// <inheritdoc/>
public bool Equals(EquatableArray<T> array)
{
return AsSpan().SequenceEqual(array.AsSpan());
}

/// <inheritdoc/>
public override bool Equals([NotNullWhen(true)] object? obj)
{
return obj is EquatableArray<T> array && Equals(this, array);
}

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

HashCode hashCode = default;

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

return hashCode.ToHashCode();
}

/// <summary>
/// Gets an <see cref="ImmutableArray{T}"/> instance from the current <see cref="EquatableArray{T}"/>.
/// </summary>
/// <returns>The <see cref="ImmutableArray{T}"/> from the current <see cref="EquatableArray{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ImmutableArray<T> AsImmutableArray()
{
return Unsafe.As<T[]?, ImmutableArray<T>>(ref Unsafe.AsRef(in array));
}

/// <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()
{
return AsImmutableArray().AsSpan();
}

/// <summary>
/// Copies the contents of this <see cref="EquatableArray{T}"/> instance to a mutable array.
/// </summary>
/// <returns>The newly instantiated array.</returns>
public T[] ToArray()
{
return AsImmutableArray().ToArray();
}

/// <summary>
/// Gets an <see cref="ImmutableArray{T}.Enumerator"/> value to traverse items in the current array.
/// </summary>
/// <returns>An <see cref="ImmutableArray{T}.Enumerator"/> value to traverse items in the current array.</returns>
public ImmutableArray<T>.Enumerator GetEnumerator()
{
return AsImmutableArray().GetEnumerator();
}

/// <inheritdoc/>
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return ((IEnumerable<T>)AsImmutableArray()).GetEnumerator();
}

/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)AsImmutableArray()).GetEnumerator();
}
}

/// <summary>
/// Extensions for <see cref="EquatableArray{T}"/>.
/// </summary>
[ExcludeFromCodeCoverage]
internal static class EquatableArray
{
/// <summary>
/// Creates an <see cref="EquatableArray{T}"/> instance from a given <see cref="ImmutableArray{T}"/>.
/// </summary>
/// <typeparam name="T">The type of items in the input array.</typeparam>
/// <param name="array">The input <see cref="ImmutableArray{T}"/> instance.</param>
/// <returns>An <see cref="EquatableArray{T}"/> instance from a given <see cref="ImmutableArray{T}"/>.</returns>
public static EquatableArray<T> AsEquatableArray<T>(this ImmutableArray<T> array)
where T : IEquatable<T>
{
return new(array);
}
}
12 changes: 9 additions & 3 deletions src/Zomp.SyncMethodGenerator/MethodToGenerate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,22 @@
/// <summary>
/// Represents a sync method to generate from its async version.
/// </summary>
/// <param name="Index">Index of the method in the source file.</param>
/// <param name="Namespaces">List of namespaces this method is under.</param>
/// <param name="IsNamespaceFileScoped">True if namespace is file scoped.</param>
/// <param name="Classes">List of classes this method belongs to starting from the outer-most class.</param>
/// <param name="MethodName">Name of the method.</param>
/// <param name="Implementation">Implementation.</param>
/// <param name="DisableNullable">Disables nullable for the method.</param>
/// <param name="Diagnostics">Diagnostics.</param>
/// <param name="HasErrors">True if there are errors in <see cref="Diagnostics"/>.</param>
internal sealed record MethodToGenerate(
IEnumerable<string> Namespaces,
int Index,
EquatableArray<string> Namespaces,
bool IsNamespaceFileScoped,
IEnumerable<ClassDeclaration> Classes,
EquatableArray<ClassDeclaration> Classes,
string MethodName,
string Implementation,
bool DisableNullable);
bool DisableNullable,
EquatableArray<ReportedDiagnostic> Diagnostics,
bool HasErrors);
37 changes: 37 additions & 0 deletions src/Zomp.SyncMethodGenerator/Models/ReportedDiagnostic.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace Zomp.SyncMethodGenerator.Models;

/// <summary>
/// Basic diagnostic description for reporting diagnostic inside the incremental pipeline.
/// </summary>
/// <param name="Descriptor">Diagnostic descriptor.</param>
/// <param name="FilePath">File path.</param>
/// <param name="TextSpan">Text span.</param>
/// <param name="LineSpan">Line span.</param>
/// <param name="Trivia">Trivia.</param>
/// <see href="https://github.com/dotnet/roslyn/issues/62269#issuecomment-1170760367" />
internal sealed record ReportedDiagnostic(DiagnosticDescriptor Descriptor, string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan, string Trivia)
{
/// <summary>
/// Implicitly converts <see cref="ReportedDiagnostic"/> to <see cref="Diagnostic"/>.
/// </summary>
/// <param name="diagnostic">Diagnostic to convert.</param>
public static implicit operator Diagnostic(ReportedDiagnostic diagnostic)
{
return Diagnostic.Create(
descriptor: diagnostic.Descriptor,
location: Location.Create(diagnostic.FilePath, diagnostic.TextSpan, diagnostic.LineSpan),
messageArgs: new object[] { diagnostic.Trivia });
}

/// <summary>
/// Creates a new <see cref="ReportedDiagnostic"/> from <see cref="DiagnosticDescriptor"/> and <see cref="Location"/>.
/// </summary>
/// <param name="descriptor">Descriptor.</param>
/// <param name="location">Location.</param>
/// <param name="trivia">Trivia.</param>
/// <returns>A new <see cref="ReportedDiagnostic"/>.</returns>
public static ReportedDiagnostic Create(DiagnosticDescriptor descriptor, Location location, string trivia)
{
return new(descriptor, location.SourceTree?.FilePath ?? string.Empty, location.SourceSpan, location.GetLineSpan().Span, trivia);
}
}
6 changes: 3 additions & 3 deletions src/Zomp.SyncMethodGenerator/SourceGenerationHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ internal static string GenerateExtensionClass(MethodToGenerate methodToGenerate)
{
var indent = new string(' ', 4 * i);

var modifiers = string.Join(string.Empty, @class.Modifiers.Select(z => GetKeyword(z) + " "));
var classDeclarationLine = $"{modifiers}partial class {@class.ClassName}{(@class.TypeParameterListSyntax is null ? string.Empty
: "<" + string.Join(", ", @class.TypeParameterListSyntax.Parameters.Select(z => z.ToString())) + ">")}";
var modifiers = string.Join(string.Empty, @class.Modifiers.Select(z => GetKeyword((SyntaxKind)z) + " "));
var classDeclarationLine = $"{modifiers}partial class {@class.ClassName}{(@class.TypeParameterListSyntax.IsEmpty ? string.Empty
: "<" + string.Join(", ", @class.TypeParameterListSyntax) + ">")}";

sbBegin.Append($$"""
{{indent}}{{classDeclarationLine}}
Expand Down
Loading