| File: MatchPartialSafetyModifierCodeFixProvider.cs | Web Access |
| Project: src\runtime\src\tools\illink\src\ILLink.CodeFix\ILLink.CodeFixProvider.csproj (ILLink.CodeFixProvider) |
// Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. #if DEBUG using System.Collections.Immutable; using System.Composition; using System.Linq; using System.Threading; using System.Threading.Tasks; using ILLink.CodeFixProvider; using ILLink.RoslynAnalyzer; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace ILLink.CodeFix { /// <summary> /// Fixes compiler diagnostics <c>CS0764</c> and <c>CS9390</c> by copying the safety modifier from the partial /// declaration that has it onto the one that does not. /// </summary> /// <remarks> /// The modifier is always propagated rather than removed. For <c>unsafe</c> that preserves the caller /// obligation, and for <c>safe</c> removing it would reintroduce <c>CS9389</c> on an <c>extern</c> member. /// </remarks> [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MatchPartialSafetyModifierCodeFixProvider)), Shared] public sealed class MatchPartialSafetyModifierCodeFixProvider : Microsoft.CodeAnalysis.CodeFixes.CodeFixProvider { public const string PartialMemberUnsafeDifferenceDiagnosticId = "CS0764"; public const string PartialMemberSafeDifferenceDiagnosticId = "CS9390"; private static LocalizableString CodeFixTitle => new LocalizableResourceString( nameof(Resources.MatchPartialSafetyModifierCodeFixTitle), Resources.ResourceManager, typeof(Resources)); public override ImmutableArray<string> FixableDiagnosticIds => [PartialMemberUnsafeDifferenceDiagnosticId, PartialMemberSafeDifferenceDiagnosticId]; public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; public override async Task RegisterCodeFixesAsync(CodeFixContext context) { Diagnostic diagnostic = context.Diagnostics[0]; SyntaxKind modifier = diagnostic.Id == PartialMemberUnsafeDifferenceDiagnosticId ? SyntaxKind.UnsafeKeyword : UnsafeMigrationSyntaxHelpers.SafeKeywordKind; if (modifier == SyntaxKind.None) return; if (await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false) is not { } root || await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false) is not { } semanticModel) { return; } SyntaxNode targetNode = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); if (FindPartialDeclaration(targetNode) is not { } declaration || semanticModel.GetDeclaredSymbol(declaration, context.CancellationToken) is not { } symbol || GetOtherPart(symbol) is not { } otherPart) { return; } // Determine which declaration is missing the modifier. The diagnostic is reported on the // implementing part regardless of which part carries it. bool declarationHasModifier = HasModifier(declaration, modifier); (Document Document, SyntaxNode Declaration)? target = declarationHasModifier ? await GetDeclarationToEditAsync(context.Document.Project.Solution, otherPart, modifier, context.CancellationToken).ConfigureAwait(false) : (context.Document, declaration); // When the parts disagree in opposite directions the compiler reports both CS0764 and CS9390, and // adding the missing modifier would put 'safe' and 'unsafe' on the same declaration, which is an // error. Resolving that requires deciding which contract is correct, so no fix is offered. if (target is not { } edit || HasModifier(edit.Declaration, GetOppositeModifier(modifier))) return; string title = CodeFixTitle.ToString(); context.RegisterCodeFix( CodeAction.Create( title, async cancellationToken => { Document updated = await UnsafeModifierCodeFixHelpers .AddModifierAsync(edit.Document, edit.Declaration, modifier, cancellationToken) .ConfigureAwait(false); return updated.Project.Solution; }, title), diagnostic); } private static async Task<(Document, SyntaxNode)?> GetDeclarationToEditAsync( Solution solution, ISymbol otherPart, SyntaxKind modifier, CancellationToken cancellationToken) { foreach (SyntaxReference reference in otherPart.DeclaringSyntaxReferences) { // A part emitted by a source generator has no editable document. if (solution.GetDocument(reference.SyntaxTree) is not { } document) continue; SyntaxNode declaration = reference.GetSyntax(cancellationToken); // A field-like event's reference points at the variable declarator, but the modifier lives on the // declaration that contains it. if (declaration is VariableDeclaratorSyntax variable && variable.Parent?.Parent is BaseFieldDeclarationSyntax field) declaration = field; if (HasModifier(declaration, modifier)) continue; // Re-resolve against the document's current tree so the edit applies to a live node. if (await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false) is not { } root) continue; SyntaxNode candidate = root.FindNode(declaration.Span, getInnermostNodeForTie: true); if (candidate.AncestorsAndSelf().FirstOrDefault(node => node.IsKind(declaration.Kind())) is { } current) return (document, current); } return null; } private static bool HasModifier(SyntaxNode declaration, SyntaxKind modifier) => modifier == SyntaxKind.UnsafeKeyword ? UnsafeMigrationSyntaxHelpers.HasModifier(declaration, SyntaxKind.UnsafeKeyword) : UnsafeMigrationSyntaxHelpers.HasSafeModifier(declaration); private static SyntaxKind GetOppositeModifier(SyntaxKind modifier) => modifier == SyntaxKind.UnsafeKeyword ? UnsafeMigrationSyntaxHelpers.SafeKeywordKind : SyntaxKind.UnsafeKeyword; private static ISymbol? GetOtherPart(ISymbol symbol) => symbol switch { IMethodSymbol method => method.PartialDefinitionPart ?? method.PartialImplementationPart, IPropertySymbol property => property.PartialDefinitionPart ?? property.PartialImplementationPart, IEventSymbol @event => @event.PartialDefinitionPart ?? @event.PartialImplementationPart, _ => null, }; private static SyntaxNode? FindPartialDeclaration(SyntaxNode node) => // BaseMethodDeclarationSyntax covers partial methods and constructors, BasePropertyDeclarationSyntax // covers partial properties, indexers and events, and a field-like event is its own declaration kind. node.AncestorsAndSelf().FirstOrDefault(static ancestor => ancestor is BaseMethodDeclarationSyntax or BasePropertyDeclarationSyntax or EventFieldDeclarationSyntax); } } #endif