File: Completion\CompletionProviders\OverrideCompletionProvider.cs
Web Access
Project: src\src\Features\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.Features)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Completion.Providers;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers;
 
[ExportCompletionProvider(nameof(OverrideCompletionProvider), LanguageNames.CSharp), Shared]
[ExtensionOrder(After = nameof(PreprocessorCompletionProvider))]
[method: ImportingConstructor]
[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
internal sealed partial class OverrideCompletionProvider() : AbstractOverrideCompletionProvider
{
    internal override string Language => LanguageNames.CSharp;
 
    protected override SyntaxNode GetSyntax(SyntaxToken token)
    {
        return token.GetAncestor<EventFieldDeclarationSyntax>()
            ?? token.GetAncestor<EventDeclarationSyntax>()
            ?? token.GetAncestor<PropertyDeclarationSyntax>()
            ?? token.GetAncestor<IndexerDeclarationSyntax>()
            ?? (SyntaxNode?)token.GetAncestor<MethodDeclarationSyntax>()
            ?? throw ExceptionUtilities.UnexpectedValue(token);
    }
 
    public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options)
        => CompletionUtilities.IsTriggerAfterSpaceOrStartOfWordCharacter(text, characterPosition, options);
 
    public override ImmutableHashSet<char> TriggerCharacters { get; } = CompletionUtilities.SpaceTriggerCharacter;
 
    protected override SyntaxToken GetToken(CompletionItem completionItem, SyntaxTree tree, CancellationToken cancellationToken)
    {
        var tokenSpanEnd = MemberInsertionCompletionItem.GetTokenSpanEnd(completionItem);
        return tree.FindTokenOnLeftOfPosition(tokenSpanEnd, cancellationToken);
    }
 
    public override bool TryDetermineReturnType(SyntaxToken startToken, SemanticModel semanticModel, CancellationToken cancellationToken, out ITypeSymbol? returnType, out SyntaxToken nextToken)
    {
        nextToken = startToken;
        returnType = null;
        if (startToken.Parent is TypeSyntax typeSyntax)
        {
            // 'partial' is actually an identifier.  If we see it just bail.  This does mean
            // we won't handle overrides that actually return a type called 'partial'.  And
            // not a single tear was shed.
            if (typeSyntax is IdentifierNameSyntax identifierName &&
                identifierName.Identifier.IsKindOrHasMatchingText(SyntaxKind.PartialKeyword))
            {
                return false;
            }
 
            returnType = semanticModel.GetTypeInfo(typeSyntax, cancellationToken).Type;
            nextToken = typeSyntax.GetFirstToken().GetPreviousToken();
        }
 
        return true;
    }
 
    public override bool TryDetermineModifiers(
        SyntaxToken startToken,
        SourceText text,
        int startLine,
        out Accessibility seenAccessibility,
        out DeclarationModifiers modifiers)
    {
        var token = startToken;
        var parentMember = token.Parent;
        modifiers = default;
        seenAccessibility = Accessibility.NotApplicable;
 
        if (parentMember is null)
            return false;
 
        // Keep walking backwards as long as we're still within our parent member.
        while (token != default)
        {
            if (token.SpanStart < parentMember.SpanStart)
            {
                // moved before the start of the member we're in.  If previous member's token is on the same line,
                // we bail out as our replacement will delete the entire line we're on.
                if (IsOnStartLine(token.SpanStart, text, startLine))
                    return false;
 
                break;
            }
 
            // Ok to hit a `]` if it's the end of attributes on this member.
            if (token.Kind() == SyntaxKind.CloseBracketToken)
            {
                if (token.Parent is not AttributeListSyntax)
                    return false;
 
                break;
            }
 
            // We only accept tokens that precede us on the same line.  Splitting across multiple lines is too niche
            // to want to support, and more likely indicates a case of broken code that the user is in the middle of
            // fixing up.
            if (!IsOnStartLine(token.SpanStart, text, startLine))
                return false;
 
            switch (token.Kind())
            {
                // Standard modifier cases we accept.
 
                case SyntaxKind.AbstractKeyword:
                    modifiers = modifiers.WithIsAbstract(true);
                    break;
                case SyntaxKind.ExternKeyword:
                    modifiers = modifiers.WithIsExtern(true);
                    break;
                case SyntaxKind.OverrideKeyword:
                    modifiers = modifiers.WithIsOverride(true);
                    break;
                case SyntaxKind.RequiredKeyword:
                    modifiers = modifiers.WithIsRequired(true);
                    break;
                case SyntaxKind.SealedKeyword:
                    modifiers = modifiers.WithIsSealed(true);
                    break;
                case SyntaxKind.UnsafeKeyword:
                    modifiers = modifiers.WithIsUnsafe(true);
                    break;
 
                // Accessibility modifiers we accept.
 
                case SyntaxKind.PublicKeyword:
                    seenAccessibility = seenAccessibility == Accessibility.NotApplicable
                        ? Accessibility.Public
                        : seenAccessibility;
                    break;
                case SyntaxKind.PrivateKeyword:
                    seenAccessibility = seenAccessibility switch
                    {
                        Accessibility.NotApplicable => Accessibility.Private,
                        // If we see private AND protected, filter for private protected
                        Accessibility.Protected => Accessibility.ProtectedAndInternal,
                        _ => seenAccessibility,
                    };
                    break;
                case SyntaxKind.InternalKeyword:
                    // If we see internal AND protected, filter for protected internal
                    seenAccessibility = seenAccessibility switch
                    {
                        Accessibility.NotApplicable => Accessibility.Internal,
                        Accessibility.Protected => Accessibility.ProtectedOrInternal,
                        _ => seenAccessibility,
                    };
                    break;
                case SyntaxKind.ProtectedKeyword:
                    seenAccessibility = seenAccessibility switch
                    {
                        Accessibility.NotApplicable => Accessibility.Protected,
                        // If we see protected AND internal, filter for protected internal.
                        Accessibility.Internal => Accessibility.ProtectedOrInternal,
                        // Or if we see private AND protected, filter for private protected
                        Accessibility.Private => Accessibility.ProtectedAndInternal,
                        _ => seenAccessibility,
                    };
                    break;
                default:
                    // If we hit anything else, then this token is not valid for override completions, and we can just bail here.
                    return false;
            }
 
            token = token.GetPreviousToken();
        }
 
        // Have to at least found the override token for us to offer override-completion.
        return modifiers.IsOverride;
    }
 
    public override SyntaxToken FindStartingToken(SyntaxTree tree, int position, CancellationToken cancellationToken)
    {
        var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken);
        return token.GetPreviousTokenIfTouchingWord(position);
    }
 
    public override ImmutableArray<ISymbol> FilterOverrides(ImmutableArray<ISymbol> members, ITypeSymbol? returnType)
    {
        if (returnType == null)
        {
            return members;
        }
 
        var filteredMembers = members.WhereAsArray(m =>
            SymbolEquivalenceComparer.Instance.Equals(GetReturnType(m), returnType));
 
        // Don't filter by return type if we would then have nothing to show.
        // This way, the user gets completion even if they speculatively typed the wrong return type
        return filteredMembers.Length > 0 ? filteredMembers : members;
    }
 
    protected override int GetTargetCaretPosition(SyntaxNode caretTarget)
    {
        return CompletionUtilities.GetTargetCaretNodeForInsertedMember(caretTarget).GetLocation().SourceSpan.End;
    }
}