| File: ConfigurationBindingGenerator.Suppressor.cs | Web Access |
| Project: src\runtime\src\libraries\Microsoft.Extensions.Configuration.Binder\gen\Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj (Microsoft.Extensions.Configuration.Binder.SourceGeneration) |
// 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.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.Text; namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { public sealed partial class ConfigurationBindingGenerator { /// <summary> /// Suppresses false-positive diagnostics emitted by the linker /// when analyzing binding invocations that we have intercepted. /// Workaround for https://github.com/dotnet/roslyn/issues/68669. /// </summary> [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class Suppressor : DiagnosticSuppressor { private const string Justification = "The target method has been intercepted by a generated static variant."; /// <summary> /// Suppression descriptor for IL2026: Members attributed with RequiresUnreferencedCode may break when trimming. /// </summary> private static readonly SuppressionDescriptor RUCDiagnostic = new(id: "SYSLIBSUPPRESS0002", suppressedDiagnosticId: "IL2026", Justification); /// <summary> /// Suppression descriptor for IL3050: Avoid calling members annotated with 'RequiresDynamicCodeAttribute' when publishing as native AOT. /// </summary> private static readonly SuppressionDescriptor RDCDiagnostic = new(id: "SYSLIBSUPPRESS0003", suppressedDiagnosticId: "IL3050", Justification); public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions => ImmutableArray.Create(RUCDiagnostic, RDCDiagnostic); public override void ReportSuppressions(SuppressionAnalysisContext context) { // Lazily built set of locations that were actually intercepted by the generator. HashSet<string>? interceptedLocationKeys = null; foreach (Diagnostic diagnostic in context.ReportedDiagnostics) { string diagnosticId = diagnostic.Id; if (diagnosticId != RDCDiagnostic.SuppressedDiagnosticId && diagnosticId != RUCDiagnostic.SuppressedDiagnosticId) { continue; } Location location = diagnostic.AdditionalLocations.Count > 0 ? diagnostic.AdditionalLocations[0] : diagnostic.Location; // The trim analyzer changed from warning on the InvocationExpression to the MemberAccessExpression in https://github.com/dotnet/runtime/pull/110086 // In other words, the warning location went from from `{|Method1(arg1, arg2)|}` to `{|Method1|}(arg1, arg2)` // To account for this, we need to check if the location is an InvocationExpression or a child of an InvocationExpression. // Use getInnermostNodeForTie to handle the case where the binding call is an // argument to another method (e.g. Some.Method(config.Get<T>())). In that case, // ArgumentSyntax and the inner InvocationExpressionSyntax can share the same span. if (location.SourceTree is not SyntaxTree sourceTree) { continue; } SyntaxNode syntaxNode = sourceTree.GetRoot(context.CancellationToken).FindNode(location.SourceSpan, getInnermostNodeForTie: true); if ((syntaxNode as InvocationExpressionSyntax ?? syntaxNode.Parent as InvocationExpressionSyntax) is not InvocationExpressionSyntax invocation) { continue; } if (!BinderInvocation.IsCandidateSyntaxNode(invocation)) { continue; } SemanticModel semanticModel = context.GetSemanticModel(sourceTree); if (semanticModel.GetOperation(invocation, context.CancellationToken) is not IInvocationOperation operation || !BinderInvocation.IsBindingOperation(operation)) { continue; } // Only suppress if the generator actually intercepted this call site. // The generator may skip interception for unsupported types (https://github.com/dotnet/runtime/issues/96643). interceptedLocationKeys ??= CollectInterceptedLocationKeys(context.Compilation, context.CancellationToken); string? locationKey = GetInvocationLocationKey(invocation, semanticModel, context.CancellationToken); if (locationKey is null || !interceptedLocationKeys.Contains(locationKey)) { continue; } SuppressionDescriptor targetSuppression = diagnosticId == RUCDiagnostic.SuppressedDiagnosticId ? RUCDiagnostic : RDCDiagnostic; context.ReportSuppression(Suppression.Create(targetSuppression, diagnostic)); } } /// <summary> /// Scans the generated source trees for [InterceptsLocation] attributes and collects /// all intercepted locations. For v0, locations are (filePath, line, column) tuples. /// For v1, locations are the encoded data strings from the attribute. /// </summary> private static HashSet<string> CollectInterceptedLocationKeys(Compilation compilation, CancellationToken cancellationToken) { var keys = new HashSet<string>(); foreach (SyntaxTree tree in compilation.SyntaxTrees) { if (!tree.FilePath.EndsWith("BindingExtensions.g.cs", System.StringComparison.Ordinal)) { continue; } SyntaxNode root = tree.GetRoot(cancellationToken); foreach (AttributeSyntax attr in root.DescendantNodes().OfType<AttributeSyntax>()) { // Matching the name like this is somewhat brittle, but it's okay as long as we match what the generator emits. if (attr.Name.ToString() != "InterceptsLocation") { continue; } AttributeArgumentListSyntax? argList = attr.ArgumentList; if (argList is null) { continue; } SeparatedSyntaxList<AttributeArgumentSyntax> args = argList.Arguments; if (InterceptorVersion == 0) { // v0 format: [InterceptsLocation("filePath", line, column)] if (args.Count == 3 && args[0].Expression is LiteralExpressionSyntax filePathLiteral && filePathLiteral.IsKind(SyntaxKind.StringLiteralExpression) && args[1].Expression is LiteralExpressionSyntax lineLiteral && lineLiteral.IsKind(SyntaxKind.NumericLiteralExpression) && args[2].Expression is LiteralExpressionSyntax colLiteral && colLiteral.IsKind(SyntaxKind.NumericLiteralExpression)) { keys.Add(MakeV0Key(filePathLiteral.Token.ValueText, (int)lineLiteral.Token.Value!, (int)colLiteral.Token.Value!)); } } else { // v1 format: [InterceptsLocation(version, "data")] if (args.Count == 2 && args[1].Expression is LiteralExpressionSyntax dataLiteral && dataLiteral.IsKind(SyntaxKind.StringLiteralExpression)) { keys.Add(MakeV1Key(dataLiteral.Token.ValueText)); } } } } return keys; } private static string MakeV0Key(string filePath, int line, int column) => $"{filePath}|{line}|{column}"; private static string MakeV1Key(string data) => data; private static string? GetInvocationLocationKey(InvocationExpressionSyntax invocation, SemanticModel semanticModel, CancellationToken cancellationToken) { if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) { return null; } if (InterceptorVersion == 0) { SyntaxTree syntaxTree = memberAccess.SyntaxTree; TextSpan nameSpan = memberAccess.Name.Span; FileLinePositionSpan lineSpan = syntaxTree.GetLineSpan(nameSpan, cancellationToken); string filePath; SourceReferenceResolver? resolver = semanticModel.Compilation.Options.SourceReferenceResolver; filePath = resolver?.NormalizePath(syntaxTree.FilePath, baseFilePath: null) ?? syntaxTree.FilePath; return MakeV0Key(filePath, lineSpan.StartLinePosition.Line + 1, lineSpan.StartLinePosition.Character + 1); } else { object? interceptableLocation = GetInterceptableLocationFunc?.Invoke(semanticModel, invocation, cancellationToken); if (interceptableLocation is null) { return null; } string data = (string)InterceptableLocationDataGetter!.Invoke(interceptableLocation, parameters: null)!; return MakeV1Key(data); } } } } }