Skip to content

Commit

Permalink
Make config binding gen incremental (#89587)
Browse files Browse the repository at this point in the history
* Make config binding gen incremental

* Iterate on implementation

* Add incremental tests & driver

* Make incremental tests pass and revert functional regression

* Address failing tests

* Make tests pass

* Suppress diagnostic

* Address feedback on diag info creation

* Refactor member access expr parsing to indicate assumptions

* Address feedback & do misc clean up

* Adjust model to minimize baseline diff / misc clean up
  • Loading branch information
layomia committed Sep 27, 2023
1 parent da5bd2b commit 8e76039
Show file tree
Hide file tree
Showing 62 changed files with 2,674 additions and 1,390 deletions.
60 changes: 60 additions & 0 deletions src/libraries/Common/src/SourceGenerators/DiagnosticInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Linq;
using System.Numerics.Hashing;
using Microsoft.CodeAnalysis;

namespace SourceGenerators;

/// <summary>
/// Descriptor for diagnostic instances using structural equality comparison.
/// Provides a work-around for https://github.com/dotnet/roslyn/issues/68291.
/// </summary>
internal readonly struct DiagnosticInfo : IEquatable<DiagnosticInfo>
{
public DiagnosticDescriptor Descriptor { get; private init; }
public object?[] MessageArgs { get; private init; }
public Location? Location { get; private init; }

public static DiagnosticInfo Create(DiagnosticDescriptor descriptor, Location? location, object?[]? messageArgs)
{
Location? trimmedLocation = location is null ? null : GetTrimmedLocation(location);

return new DiagnosticInfo
{
Descriptor = descriptor,
Location = trimmedLocation,
MessageArgs = messageArgs ?? Array.Empty<object?>()
};

// Creates a copy of the Location instance that does not capture a reference to Compilation.
static Location GetTrimmedLocation(Location location)
=> Location.Create(location.SourceTree?.FilePath ?? "", location.SourceSpan, location.GetLineSpan().Span);
}

public Diagnostic CreateDiagnostic()
=> Diagnostic.Create(Descriptor, Location, MessageArgs);

public override readonly bool Equals(object? obj) => obj is DiagnosticInfo info && Equals(info);

public readonly bool Equals(DiagnosticInfo other)
{
return Descriptor.Equals(other.Descriptor) &&
MessageArgs.SequenceEqual(other.MessageArgs) &&
Location == other.Location;
}

public override readonly int GetHashCode()
{
int hashCode = Descriptor.GetHashCode();
foreach (object? messageArg in MessageArgs)
{
hashCode = HashHelpers.Combine(hashCode, messageArg?.GetHashCode() ?? 0);
}

hashCode = HashHelpers.Combine(hashCode, Location?.GetHashCode() ?? 0);
return hashCode;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Numerics.Hashing;

namespace System.Text.Json.SourceGeneration
namespace SourceGenerators
{
/// <summary>
/// Provides an immutable list implementation which implements sequence equality.
Expand Down Expand Up @@ -72,15 +73,9 @@ public bool MoveNext()
}
}

public static class ImmutableEquatableArray
internal static class ImmutableEquatableArray
{
public static ImmutableEquatableArray<T> Empty<T>() where T : IEquatable<T>
=> ImmutableEquatableArray<T>.Empty;

public static ImmutableEquatableArray<T> ToImmutableEquatableArray<T>(this IEnumerable<T> values) where T : IEquatable<T>
=> new(values);

public static ImmutableEquatableArray<T> Create<T>(params T[] values) where T : IEquatable<T>
=> values is { Length: > 0 } ? new(values) : ImmutableEquatableArray<T>.Empty;
}
}
4 changes: 4 additions & 0 deletions src/libraries/Common/src/SourceGenerators/TypeModelHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

using Microsoft.CodeAnalysis;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;

namespace SourceGenerators
{
Expand Down Expand Up @@ -32,5 +34,7 @@ void TraverseContainingTypes(INamedTypeSymbol current)
}
}
}

public static string GetFullyQualifiedName(this ITypeSymbol type) => type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;
using Microsoft.CodeAnalysis;

namespace System.Text.Json.SourceGeneration
namespace SourceGenerators
{
/// <summary>
/// An equatable value representing type identity.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Xunit;

namespace SourceGenerators.Tests
{
internal static class GeneratorTestHelpers
{
/// <summary>
/// Asserts for structural equality, returning a path to the mismatching data when not equal.
/// </summary>
public static void AssertStructurallyEqual<T>(T expected, T actual)
{
CheckAreEqualCore(expected, actual, new());
static void CheckAreEqualCore(object expected, object actual, Stack<string> path)
{
if (expected is null || actual is null)
{
if (expected is not null || actual is not null)
{
FailNotEqual();
}

return;
}

Type type = expected.GetType();
if (type != actual.GetType())
{
FailNotEqual();
return;
}

if (expected is IEnumerable leftCollection)
{
if (actual is not IEnumerable rightCollection)
{
FailNotEqual();
return;
}

object?[] expectedValues = leftCollection.Cast<object?>().ToArray();
object?[] actualValues = rightCollection.Cast<object?>().ToArray();

for (int i = 0; i < Math.Max(expectedValues.Length, actualValues.Length); i++)
{
object? expectedElement = i < expectedValues.Length ? expectedValues[i] : "<end of collection>";
object? actualElement = i < actualValues.Length ? actualValues[i] : "<end of collection>";

path.Push($"[{i}]");
CheckAreEqualCore(expectedElement, actualElement, path);
path.Pop();
}
}

if (type.GetProperty("EqualityContract", BindingFlags.Instance | BindingFlags.NonPublic, null, returnType: typeof(Type), types: Array.Empty<Type>(), null) != null)
{
// Type is a C# record, run pointwise equality comparison.
foreach (PropertyInfo property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
path.Push("." + property.Name);
CheckAreEqualCore(property.GetValue(expected), property.GetValue(actual), path);
path.Pop();
}

return;
}

if (!expected.Equals(actual))
{
FailNotEqual();
}

void FailNotEqual() => Assert.Fail($"Value not equal in ${string.Join("", path.Reverse())}: expected {expected}, but was {actual}.");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// 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.Immutable;
using Microsoft.CodeAnalysis;
using SourceGenerators;

Expand All @@ -11,19 +10,22 @@ public sealed partial class ConfigurationBindingGenerator : IIncrementalGenerato
{
private sealed partial class Emitter
{
private readonly SourceProductionContext _context;
private readonly SourceGenerationSpec _sourceGenSpec;
private readonly InterceptorInfo _interceptorInfo;
private readonly BindingHelperInfo _bindingHelperInfo;
private readonly TypeIndex _typeIndex;

private readonly SourceWriter _writer = new();

public Emitter(SourceProductionContext context, SourceGenerationSpec sourceGenSpec)
public Emitter(SourceGenerationSpec sourceGenSpec)
{
_context = context;
_sourceGenSpec = sourceGenSpec;
_interceptorInfo = sourceGenSpec.InterceptorInfo;
_bindingHelperInfo = sourceGenSpec.BindingHelperInfo;
_typeIndex = new TypeIndex(sourceGenSpec.ConfigTypes);
}

public void Emit()
public void Emit(SourceProductionContext context)
{
if (!ShouldEmitBindingExtensions())
if (!ShouldEmitMethods(MethodsToGen.Any))
{
return;
}
Expand Down Expand Up @@ -52,7 +54,7 @@ public void Emit()

EmitEndBlock(); // Binding namespace.

_context.AddSource($"{Identifier.BindingExtensions}.g.cs", _writer.ToSourceText());
context.AddSource($"{Identifier.BindingExtensions}.g.cs", _writer.ToSourceText());
}

private void EmitInterceptsLocationAttrDecl()
Expand All @@ -79,7 +81,7 @@ public InterceptsLocationAttribute(string filePath, int line, int column)

private void EmitUsingStatements()
{
foreach (string @namespace in _sourceGenSpec.Namespaces.ToImmutableSortedSet())
foreach (string @namespace in _bindingHelperInfo.Namespaces)
{
_writer.WriteLine($"using {@namespace};");
}
Expand Down
Loading

0 comments on commit 8e76039

Please sign in to comment.