File: Completion\CompletionProviders\PropertySubPatternCompletionProvider.cs
Web Access
Project: ..\..\..\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.Options;
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.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;
 
            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;
            }
        }
    }
}