/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Features/CSharp/Portable/Completion/CompletionProviders/PropertySubPatternCompletionProvider.cs
225 строк
10 KB
Ankita Khera
Fix property pattern completion filtering out member being edited (#83230)
20 апр 2026, 22:15
Не верифицирован
20 апр 2026, 22:15
371283a
Код
Авторство
О чём код?
// 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; namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; [ExportCompletionProvider(nameof(PropertySubpatternCompletionProvider), LanguageNames.CSharp)] [ExtensionOrder(After = nameof(InternalsVisibleToCompletionProvider))] [Shared] internal sealed 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 `{ ... }`, // - the member access before the last dot `Property.Property2` (or null), and // - the subpattern currently being edited (or null) so it can be excluded from the "already tested members" filter var (propertyPatternClause, memberAccess, editingSubpattern) = 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. // If we're currently editing an existing subpattern name (cursor at `IsCompleted$$:` in `{ IsCompleted: true }`), // exclude it so the member being edited still appears in the completion list. var alreadyTestedMembers = new HashSet<string>(propertyPatternClause.Subpatterns .Where(p => p != editingSubpattern) .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: [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 is IPropertySymbol { IsWriteOnly: false }) 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?, SubpatternSyntax? editingSubpattern) TryGetPropertyPatternClause(SyntaxTree tree, int position, CancellationToken cancellationToken) { if (tree.IsInNonUserCode(position, cancellationToken)) { return default; } var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken); var tokenBeforeAdjustment = token; token = token.GetPreviousTokenIfTouchingWord(position); // Check whether the original token was the name of a subpattern being edited // (e.g., `{ IsCompleted$$: true }`). We need to know this so the caller can exclude it from // the "already tested members" filter and keep showing the member in the completion list. SubpatternSyntax? editingSubpattern = null; if (tokenBeforeAdjustment.Parent is IdentifierNameSyntax { Parent: NameColonSyntax { Parent: SubpatternSyntax subpattern } }) { editingSubpattern = subpattern; } if (token.Kind() is SyntaxKind.CommaToken or SyntaxKind.OpenBraceToken) { return token.Parent is PropertyPatternClauseSyntax { Parent: PatternSyntax } propertyPatternClause ? (propertyPatternClause, null, editingSubpattern) : 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, editingSubpattern) : 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; } } }