Skip to content
Closed
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
66 changes: 64 additions & 2 deletions src/libraries/Common/src/SourceGenerators/DiagnosticInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
using System;
using System.Linq;
using System.Numerics.Hashing;
using System.Reflection;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace SourceGenerators;

Expand All @@ -14,6 +16,9 @@ namespace SourceGenerators;
/// </summary>
internal readonly struct DiagnosticInfo : IEquatable<DiagnosticInfo>
{
private static readonly Lazy<CSharpSyntaxTree?> s_dummySyntaxTree = new Lazy<CSharpSyntaxTree?>(GetDummySyntaxTree);
private static readonly Lazy<FieldInfo?> s_sourceTreeBackingFieldInfo = new Lazy<FieldInfo?>(GetSourceTreeBackingFieldInfo);

public DiagnosticDescriptor Descriptor { get; private init; }
public object?[] MessageArgs { get; private init; }
public Location? Location { get; private init; }
Expand All @@ -30,8 +35,35 @@ public static DiagnosticInfo Create(DiagnosticDescriptor descriptor, Location? l
};

// 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);
static Location? GetTrimmedLocation(Location? sourceLocation)
{
if (sourceLocation is null)
{
return null;
}

Location trimmedLocation = Location.Create(sourceLocation.SourceTree?.FilePath ?? "", sourceLocation.SourceSpan, sourceLocation.GetLineSpan().Span);

if (sourceLocation.IsInSource &&
!trimmedLocation.IsInSource &&
Copy link
Member

Choose a reason for hiding this comment

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

What determines whether trimmedLocation.IsInSource is true?

s_sourceTreeBackingFieldInfo.Value is FieldInfo sourceTreeField &&
s_dummySyntaxTree.Value is CSharpSyntaxTree syntaxTree)
{
// Attempt to mark this as a source location, so that it is suppressible with #pragma.
try
{
sourceTreeField.SetValue(trimmedLocation, syntaxTree);

if (!trimmedLocation.IsInSource)
{
sourceTreeField.SetValue(trimmedLocation, null);
}
}
catch { }
}

return trimmedLocation;
}
}

public Diagnostic CreateDiagnostic()
Expand All @@ -57,4 +89,34 @@ public override readonly int GetHashCode()
hashCode = HashHelpers.Combine(hashCode, Location?.GetHashCode() ?? 0);
return hashCode;
}

private static FieldInfo? GetSourceTreeBackingFieldInfo()
{
FieldInfo? info;

try
{
FieldInfo[] fields = typeof(Location).GetFields(BindingFlags.NonPublic);
info = typeof(Location).GetField("<SourceTree>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance);
Copy link
Member

Choose a reason for hiding this comment

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

I'm not comfortable taking a dependency on this type of implementation detail, particularly in product code. We should instead work with @dotnet/roslyn to come up with a better solution for storing Location instances in incremental values that doesn't encapsulate the Compilation instance.

cc @stephentoub

}
catch
{
info = null;
}

return info;
}

private static CSharpSyntaxTree? GetDummySyntaxTree()
{
try
{
Type? dummySyntaxTree = typeof(CSharpSyntaxTree).Assembly.GetType("Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.DummySyntaxTree", throwOnError: false);
return dummySyntaxTree is null ? null : (CSharpSyntaxTree?)Activator.CreateInstance(dummySyntaxTree);
}
catch
{
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -769,7 +769,7 @@ public class MyClassWithCustomCollections
public ICustomDictionary<string> ICustomDictionary { get; set; }
public ICustomSet<MyClassWithCustomCollections> ICustomCollection { get; set; }
public IReadOnlyList<int> IReadOnlyList { get; set; }
// Diagnostic warning because we don't know how to instantiate the property type.
// Built in collection: diagnostic warning because the key isn't supported for dictionary binding (only string-parsable types are).
public IReadOnlyDictionary<MyClassWithCustomCollections, int> UnsupportedIReadOnlyDictionaryUnsupported { get; set; }
public IReadOnlyDictionary<string, int> IReadOnlyDictionary { get; set; }
}
Expand Down Expand Up @@ -804,6 +804,60 @@ public interface ICustomSet<T> : ISet<T>
Assert.Equal(3, diagnostics.Where(diag => diag.Id == Diagnostics.PropertyNotSupported.Id).Count());
}

[Fact]
public async Task DiagnosticsAreSuppressibleWithPragma()
{
string source = $$"""
using System.Collections.Generic;
using Microsoft.Extensions.Configuration;

public class Program
{
public static void Main()
{
ConfigurationBuilder configurationBuilder = new();
IConfiguration config = configurationBuilder.Build();
IConfigurationSection section = config.GetSection("MySection");

#pragma warning disable {{Diagnostics.TypeNotSupported.Id}}
#pragma warning disable {{Diagnostics.PropertyNotSupported.Id}}
section.Get<MyClassWithCustomCollections>();
#pragma warning restore {{Diagnostics.TypeNotSupported.Id}}
#pragma warning restore {{Diagnostics.PropertyNotSupported.Id}}
}

// Diagnostic warning because we don't know how to instantiate two properties on this type.
public class MyClassWithCustomCollections
{
public string MyString { get; set; }
public ClassWithoutCtor ClassWithoutCtor { get; set; }
public ICustomSet<MyClassWithCustomCollections> ICustomCollection { get; set; }
// Built in collection: diagnostic warning because the key isn't supported for dictionary binding (only string-parsable types are).
public IReadOnlyDictionary<MyClassWithCustomCollections, int> UnsupportedDictionary { get; set; }
}

// Diagnostic warning because we don't know how to instantiate this type.
public interface ICustomSet<T> : ISet<T>
{
}

public abstract class ClassWithoutCtor { }

public interface InterfaceWithoutCtor { }
}
""";

ConfigBindingGenRunResult result = await VerifyAgainstBaselineUsingFile(
"Collections.generated.txt",
source,
expectedDiags: ExpectedDiagnostics.FromGeneratorOnly);

foreach (Diagnostic diagnostic in result.Diagnostics)
{
Assert.True(diagnostic.IsSuppressed);
}
}

[Fact]
public async Task MinimalGenerationIfNoBindableMembers()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<!-- Type not supported; property on type not supported -->
<NoWarn>$(NoWarn);SYSLIB1100,SYSLIB1101</NoWarn>
<!-- Logic not generated for unknown/unsupported type. -->
<NoWarn>$(NoWarn);SYSLIB1103,SYSLIB1104</NoWarn>
<Features>$(Features);InterceptorsPreview</Features>
<!-- TODO: Remove InterceptorsPreview feature after 8.0 RC2 SDK is used for build -->
<InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);Microsoft.Extensions.Configuration.Binder.SourceGeneration</InterceptorsPreviewNamespaces>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
<!-- TODO: Remove InterceptorsPreview feature after 8.0 RC2 SDK is used for build -->
<InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);Microsoft.Extensions.Configuration.Binder.SourceGeneration</InterceptorsPreviewNamespaces>
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
<!-- TODO: reinstate pragma suppressions for config binding diagnostics: https://github.com/dotnet/runtime/issues/92509. -->
<NoWarn>$(NoWarn);SYSLIB1100;SYSLIB1101</NoWarn>
<PackageDescription>Console logger provider implementation for Microsoft.Extensions.Logging.</PackageDescription>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -601,14 +601,20 @@ public partial class MyContext : JsonSerializerContext
}

[Fact]
public void JsonSerializableAttributeOnNonContextClass()
public void JsonSerializableAttributeOnNonContextClass() => JsonSerializableAttributeOnNonContextClassCore(suppressDiagnostics: false);

private JsonSourceGeneratorResult JsonSerializableAttributeOnNonContextClassCore(bool suppressDiagnostics)
{
Compilation compilation = CompilationHelper.CreateCompilation("""
string? GetSuppressionString(string action) => suppressDiagnostics ? $"#pragma warning {action} SYSLIB1224" : null;

Compilation compilation = CompilationHelper.CreateCompilation($$"""
using System.Text.Json.Serialization;

namespace Application
{
{{GetSuppressionString("disable")}}
[JsonSerializable(typeof(MyPoco))]
{{GetSuppressionString("restore")}}
public partial class MyContext : IDisposable
{
public void Dispose() { }
Expand All @@ -628,6 +634,25 @@ public class MyPoco { }
};

CompilationHelper.AssertEqualDiagnosticMessages(expectedDiagnostics, result.Diagnostics);

bool suppressionStateIsCorrect = suppressDiagnostics;
foreach (Diagnostic diagnostic in result.Diagnostics)
{
Assert.Equal(suppressionStateIsCorrect, diagnostic.IsSuppressed);
}

return result;
}

[Fact]
public void DiagnosticsAreSuppressibleWithPragma()
{
JsonSourceGeneratorResult result = JsonSerializableAttributeOnNonContextClassCore(suppressDiagnostics: true);

foreach (Diagnostic diagnostic in result.Diagnostics)
{
Assert.True(diagnostic.IsSuppressed);
}
}
}
}