File: Completion\CompletionProviders\PropertySubPatternCompletionProvider.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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Completion.Providers;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers;
 
[ExportCompletionProvider(nameof(PropertySubpatternCompletionProvider), LanguageNames.CSharp)]
[ExtensionOrder(After = nameof(InternalsVisibleToCompletionProvider))]
[Shared]
internal class PropertySubpatternCompletionProvider : LSPCompletionProvider
{
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public PropertySubpatternCompletionProvider()
    {
    }
 
    internal override string Language => LanguageNames.CSharp;
 
    // Examples:
    // is { $$
    // is { Property.$$
    // is { Property.Property2.$$
    public override async Task ProvideCompletionsAsync(CompletionContext context)
    {
        var document = context.Document;
        var position = context.Position;
        var cancellationToken = context.CancellationToken;
        var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
 
        // For `is { Property.Property2.$$`, we get:
        // - the property pattern clause `{ ... }` and
        // - the member access before the last dot `Property.Property2` (or null)
        var (propertyPatternClause, memberAccess) = TryGetPropertyPatternClause(tree, position, cancellationToken);
        if (propertyPatternClause is null)
        {
            return;
        }
 
        var semanticModel = await document.ReuseExistingSpeculativeModelAsync(position, cancellationToken).ConfigureAwait(false);
        var propertyPatternType = semanticModel.GetTypeInfo((PatternSyntax)propertyPatternClause.Parent!, cancellationToken).ConvertedType;
        // For simple property patterns, the type we want is the "input type" of the property pattern, ie the type of `c` in `c is { $$ }`.
        // For extended property patterns, we get the type by following the chain of members that we have so far, ie
        // the type of `c.Property` for `c is { Property.$$ }` and the type of `c.Property1.Property2` for `c is { Property1.Property2.$$ }`.
        var type = GetMemberAccessType(propertyPatternType, memberAccess, document, semanticModel, position);
 
        if (type is null)
        {
            return;
        }
 
        // Find the members that can be tested.
        var members = GetCandidatePropertiesAndFields(document, semanticModel, position, type);
        members = members.WhereAsArray(m => m.IsEditorBrowsable(context.CompletionOptions.MemberDisplayOptions.HideAdvancedMembers, semanticModel.Compilation));
 
        if (memberAccess is null)
        {
            // Filter out those members that have already been typed as simple (not extended) properties
            var alreadyTestedMembers = new HashSet<string>(propertyPatternClause.Subpatterns.Select(
                p => p.NameColon?.Name.Identifier.ValueText).Where(s => !string.IsNullOrEmpty(s))!);
 
            members = members.WhereAsArray(m => !alreadyTestedMembers.Contains(m.Name));
        }
 
        foreach (var member in members)
        {
            context.AddItem(SymbolCompletionItem.CreateWithSymbolId(
                displayText: member.Name.EscapeIdentifier(),
                displayTextSuffix: "",
                insertionText: null,
                symbols: ImmutableArray.Create(member),
                contextPosition: context.Position,
                rules: s_rules));
        }
 
        return;
 
        // We have to figure out the type of the extended property ourselves, because
        // the semantic model could not provide the answer we want in incomplete syntax:
        // `c is { X. }`
        static ITypeSymbol? GetMemberAccessType(ITypeSymbol? type, ExpressionSyntax? expression, Document document, SemanticModel semanticModel, int position)
        {
            if (expression is null)
            {
                return type;
            }
            else if (expression is MemberAccessExpressionSyntax memberAccess)
            {
                type = GetMemberAccessType(type, memberAccess.Expression, document, semanticModel, position);
                return GetMemberType(type, name: memberAccess.Name.Identifier.ValueText, document, semanticModel, position);
            }
            else if (expression is IdentifierNameSyntax identifier)
            {
                return GetMemberType(type, name: identifier.Identifier.ValueText, document, semanticModel, position);
            }
 
            throw ExceptionUtilities.Unreachable();
        }
 
        static ITypeSymbol? GetMemberType(ITypeSymbol? type, string name, Document document, SemanticModel semanticModel, int position)
        {
            var members = GetCandidatePropertiesAndFields(document, semanticModel, position, type);
            var matches = members.WhereAsArray(m => m.Name == name);
            if (matches.Length != 1)
            {
                return null;
            }
 
            return matches[0] switch
            {
                IPropertySymbol property => property.Type,
                IFieldSymbol field => field.Type,
                _ => throw ExceptionUtilities.Unreachable(),
            };
        }
 
        static ImmutableArray<ISymbol> GetCandidatePropertiesAndFields(Document document, SemanticModel semanticModel, int position, ITypeSymbol? type)
        {
            var members = semanticModel.LookupSymbols(position, type);
            return members.WhereAsArray(m => m.CanBeReferencedByName &&
                IsFieldOrReadableProperty(m) &&
                !m.IsImplicitlyDeclared &&
                !m.IsStatic);
        }
    }
 
    private static bool IsFieldOrReadableProperty(ISymbol symbol)
    {
        if (symbol.IsKind(SymbolKind.Field))
        {
            return true;
        }
 
        if (symbol.IsKind(SymbolKind.Property) && !((IPropertySymbol)symbol).IsWriteOnly)
        {
            return true;
        }
 
        return false;
    }
 
    internal override Task<CompletionDescription> GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken)
        => SymbolCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken);
 
    private static readonly CompletionItemRules s_rules = CompletionItemRules.Create(enterKeyRule: EnterKeyRule.Never);
 
    public override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options)
        => CompletionUtilities.IsTriggerCharacter(text, characterPosition, options) || text[characterPosition] == ' ';
 
    public override ImmutableHashSet<char> TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters.Add(' ');
 
    private static (PropertyPatternClauseSyntax?, ExpressionSyntax?) TryGetPropertyPatternClause(SyntaxTree tree, int position, CancellationToken cancellationToken)
    {
        if (tree.IsInNonUserCode(position, cancellationToken))
        {
            return default;
        }
 
        var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken);
        token = token.GetPreviousTokenIfTouchingWord(position);
 
        if (token.Kind() is SyntaxKind.CommaToken or SyntaxKind.OpenBraceToken)
        {
            return token.Parent is PropertyPatternClauseSyntax { Parent: PatternSyntax } propertyPatternClause
                ? (propertyPatternClause, null)
                : default;
        }
 
        if (token.IsKind(SyntaxKind.DotToken))
        {
            // is { Property1.$$ }
            // is { Property1.$$  Property1.Property2: ... } // typing before an existing pattern
            return token.Parent is MemberAccessExpressionSyntax memberAccess && IsExtendedPropertyPattern(memberAccess, out var propertyPatternClause)
                ? (propertyPatternClause, memberAccess.Expression)
                : default;
        }
 
        return default;
 
        static bool IsExtendedPropertyPattern(MemberAccessExpressionSyntax memberAccess, [NotNullWhen(true)] out PropertyPatternClauseSyntax? propertyPatternClause)
        {
            while (memberAccess.Parent.IsKind(SyntaxKind.SimpleMemberAccessExpression))
            {
                memberAccess = (MemberAccessExpressionSyntax)memberAccess.Parent;
            }
 
            if (memberAccess is { Parent.Parent: SubpatternSyntax { Parent: PropertyPatternClauseSyntax found } })
            {
                propertyPatternClause = found;
                return true;
            }
 
            propertyPatternClause = null;
            return false;
        }
    }
}