diff --git a/src/EditorFeatures/CSharpTest/QuickInfo/SemanticQuickInfoSourceTests.cs b/src/EditorFeatures/CSharpTest/QuickInfo/SemanticQuickInfoSourceTests.cs index 57d114e2100ef..744f7cd5580eb 100644 --- a/src/EditorFeatures/CSharpTest/QuickInfo/SemanticQuickInfoSourceTests.cs +++ b/src/EditorFeatures/CSharpTest/QuickInfo/SemanticQuickInfoSourceTests.cs @@ -6997,6 +6997,40 @@ void N(string? s) MainDescription($"({FeaturesResources.parameter}) string? s"), NullabilityAnalysis(string.Format(FeaturesResources._0_may_be_null_here, "s"))); + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/42543")] + public Task NullableParameterThatIsMaybeNull_Suppressed1() + => TestWithOptionsAsync(TestOptions.Regular8, + """ + #nullable enable + + class X + { + void N(string? s) + { + string s2 = $$s!; + } + } + """, + MainDescription($"({FeaturesResources.parameter}) string? s"), + NullabilityAnalysis(string.Format(FeaturesResources._0_may_be_null_here, "s"))); + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/42543")] + public Task NullableParameterThatIsMaybeNull_Suppressed2() + => TestWithOptionsAsync(TestOptions.Regular8, + """ + #nullable enable + + class X + { + void N(string? s) + { + string s2 = $$s!!; + } + } + """, + MainDescription($"({FeaturesResources.parameter}) string? s"), + NullabilityAnalysis(string.Format(FeaturesResources._0_may_be_null_here, "s"))); + [Fact] public Task NullableParameterThatIsNotNull() => TestWithOptionsAsync(TestOptions.Regular8, diff --git a/src/EditorFeatures/Test2/Workspaces/SymbolDescriptionServiceTests.vb b/src/EditorFeatures/Test2/Workspaces/SymbolDescriptionServiceTests.vb index b1cf7a4bd2e86..30650712f1d44 100644 --- a/src/EditorFeatures/Test2/Workspaces/SymbolDescriptionServiceTests.vb +++ b/src/EditorFeatures/Test2/Workspaces/SymbolDescriptionServiceTests.vb @@ -9,10 +9,8 @@ Imports Microsoft.CodeAnalysis.Host Imports Microsoft.CodeAnalysis.LanguageService Namespace Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces - <[UseExportProvider]> Public Class SymbolDescriptionServiceTests - Private Shared Async Function TestAsync(languageServiceProvider As HostLanguageServices, workspace As EditorTestWorkspace, expectedDescription As String) As Task Dim solution = workspace.CurrentSolution diff --git a/src/Features/Core/Portable/LanguageServices/SymbolDisplayService/AbstractSymbolDisplayService.cs b/src/Features/Core/Portable/LanguageServices/SymbolDisplayService/AbstractSymbolDisplayService.cs index 647ea5618eee0..04c51af3abbf4 100644 --- a/src/Features/Core/Portable/LanguageServices/SymbolDisplayService/AbstractSymbolDisplayService.cs +++ b/src/Features/Core/Portable/LanguageServices/SymbolDisplayService/AbstractSymbolDisplayService.cs @@ -12,26 +12,12 @@ namespace Microsoft.CodeAnalysis.LanguageService; -internal abstract partial class AbstractSymbolDisplayService : ISymbolDisplayService +internal abstract partial class AbstractSymbolDisplayService(LanguageServices services) : ISymbolDisplayService { - protected readonly LanguageServices LanguageServices; - - protected AbstractSymbolDisplayService(LanguageServices services) - { - LanguageServices = services; - } + protected readonly LanguageServices LanguageServices = services; protected abstract AbstractSymbolDescriptionBuilder CreateDescriptionBuilder(SemanticModel semanticModel, int position, SymbolDescriptionOptions options, CancellationToken cancellationToken); - public Task ToDescriptionStringAsync(SemanticModel semanticModel, int position, ISymbol symbol, SymbolDescriptionOptions options, SymbolDescriptionGroups groups, CancellationToken cancellationToken) - => ToDescriptionStringAsync(semanticModel, position, [symbol], options, groups, cancellationToken); - - public async Task ToDescriptionStringAsync(SemanticModel semanticModel, int position, ImmutableArray symbols, SymbolDescriptionOptions options, SymbolDescriptionGroups groups, CancellationToken cancellationToken) - { - var parts = await ToDescriptionPartsAsync(semanticModel, position, symbols, options, groups, cancellationToken).ConfigureAwait(false); - return parts.ToDisplayString(); - } - public Task> ToDescriptionPartsAsync(SemanticModel semanticModel, int position, ImmutableArray symbols, SymbolDescriptionOptions options, SymbolDescriptionGroups groups, CancellationToken cancellationToken) { if (symbols.Length == 0) @@ -45,9 +31,7 @@ public async Task symbols, SymbolDescriptionOptions options, CancellationToken cancellationToken) { if (symbols.Length == 0) - { return SpecializedCollections.EmptyDictionary>(); - } var builder = CreateDescriptionBuilder(semanticModel, position, options, cancellationToken); return await builder.BuildDescriptionSectionsAsync(symbols).ConfigureAwait(false); diff --git a/src/Features/Core/Portable/LanguageServices/SymbolDisplayService/ISymbolDisplayService.cs b/src/Features/Core/Portable/LanguageServices/SymbolDisplayService/ISymbolDisplayService.cs index 977b8a461507f..1730e45680cfa 100644 --- a/src/Features/Core/Portable/LanguageServices/SymbolDisplayService/ISymbolDisplayService.cs +++ b/src/Features/Core/Portable/LanguageServices/SymbolDisplayService/ISymbolDisplayService.cs @@ -12,8 +12,21 @@ namespace Microsoft.CodeAnalysis.LanguageService; internal interface ISymbolDisplayService : ILanguageService { - Task ToDescriptionStringAsync(SemanticModel semanticModel, int position, ISymbol symbol, SymbolDescriptionOptions options, SymbolDescriptionGroups groups = SymbolDescriptionGroups.All, CancellationToken cancellationToken = default); - Task ToDescriptionStringAsync(SemanticModel semanticModel, int position, ImmutableArray symbols, SymbolDescriptionOptions options, SymbolDescriptionGroups groups = SymbolDescriptionGroups.All, CancellationToken cancellationToken = default); Task> ToDescriptionPartsAsync(SemanticModel semanticModel, int position, ImmutableArray symbols, SymbolDescriptionOptions options, SymbolDescriptionGroups groups = SymbolDescriptionGroups.All, CancellationToken cancellationToken = default); Task>> ToDescriptionGroupsAsync(SemanticModel semanticModel, int position, ImmutableArray symbols, SymbolDescriptionOptions options, CancellationToken cancellationToken = default); } + +internal static class ISymbolDisplayServiceExtensions +{ + extension(ISymbolDisplayService service) + { + public Task ToDescriptionStringAsync(SemanticModel semanticModel, int position, ISymbol symbol, SymbolDescriptionOptions options, SymbolDescriptionGroups groups = SymbolDescriptionGroups.All, CancellationToken cancellationToken = default) + => service.ToDescriptionStringAsync(semanticModel, position, [symbol], options, groups, cancellationToken); + + public async Task ToDescriptionStringAsync(SemanticModel semanticModel, int position, ImmutableArray symbols, SymbolDescriptionOptions options, SymbolDescriptionGroups groups = SymbolDescriptionGroups.All, CancellationToken cancellationToken = default) + { + var parts = await service.ToDescriptionPartsAsync(semanticModel, position, symbols, options, groups, cancellationToken).ConfigureAwait(false); + return parts.ToDisplayString(); + } + } +} diff --git a/src/Features/Core/Portable/QuickInfo/CommonSemanticQuickInfoProvider.cs b/src/Features/Core/Portable/QuickInfo/CommonSemanticQuickInfoProvider.cs index 7a257df0e8910..f793bfd20b019 100644 --- a/src/Features/Core/Portable/QuickInfo/CommonSemanticQuickInfoProvider.cs +++ b/src/Features/Core/Portable/QuickInfo/CommonSemanticQuickInfoProvider.cs @@ -8,10 +8,10 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.PooledObjects; -using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Utilities; using Roslyn.Utilities; @@ -20,6 +20,8 @@ namespace Microsoft.CodeAnalysis.QuickInfo; internal abstract partial class CommonSemanticQuickInfoProvider : CommonQuickInfoProvider { + private static readonly SyntaxAnnotation s_annotation = new(); + protected override async Task BuildQuickInfoAsync( QuickInfoContext context, SyntaxToken token) { @@ -188,7 +190,77 @@ protected static Task CreateContentAsync( protected virtual (NullableAnnotation, NullableFlowState) GetNullabilityAnalysis(SemanticModel semanticModel, ISymbol symbol, SyntaxNode node, CancellationToken cancellationToken) => default; - private TokenInformation BindToken( + private (NullableAnnotation, NullableFlowState) GetNullabilityAnalysis( + SolutionServices services, SemanticModel semanticModel, ISymbol symbol, SyntaxToken token, CancellationToken cancellationToken) + { + var languageServices = services.GetLanguageServices(semanticModel.Language); + var syntaxFacts = languageServices.GetRequiredService(); + + var bindableParent = syntaxFacts.TryGetBindableParent(token); + if (bindableParent is null) + return default; + + return TryGetNullabilityAnalysisForSuppressedExpression(out var analysis) + ? analysis + : GetNullabilityAnalysis(semanticModel, symbol, bindableParent, cancellationToken); + + bool TryGetNullabilityAnalysisForSuppressedExpression(out (NullableAnnotation, NullableFlowState) analysis) + { + analysis = default; + + // Look to see if we're inside a suppression (e.g. `expr!`). The suppression changes the nullability analysis, + // and we don't actually want that here as we want to show the original nullability prior to the suppression applying. + // + // In that case, actually fork the semantic model with the `!` removed and then re-bind the token, getting the + // analysis results from that. + var tokenParent = token.GetRequiredParent(); + var parentSuppression = GetOuterSuppression(tokenParent); + if (parentSuppression is null) + return false; + + var root = semanticModel.SyntaxTree.GetRoot(cancellationToken); + + var editor = new SyntaxEditor(root, services); + // First, mark the token, so we can find it later. + editor.ReplaceNode( + tokenParent, tokenParent.ReplaceToken(token, token.WithAdditionalAnnotations(s_annotation))); + + // Now walk upwards, removing all the suppressions until we hit the top of the suppression chain. + for (var currentSuppression = parentSuppression; + currentSuppression is not null; + currentSuppression = GetOuterSuppression(currentSuppression)) + { + editor.ReplaceNode( + currentSuppression, + (current, _) => syntaxFacts.GetOperandOfPostfixUnaryExpression(current)); + } + + // Now fork the semantic model with the new root that has the suppressions removed. + var newRoot = editor.GetChangedRoot(); + + var newTree = semanticModel.SyntaxTree.WithRootAndOptions(newRoot, semanticModel.SyntaxTree.Options); + var newToken = newTree.GetRoot(cancellationToken).GetAnnotatedTokens(s_annotation).Single(); + + var newBindableParent = syntaxFacts.TryGetBindableParent(newToken); + if (newBindableParent is null) + return false; + + var newCompilation = semanticModel.Compilation.ReplaceSyntaxTree(semanticModel.SyntaxTree, newTree); + semanticModel = newCompilation.GetSemanticModel(newTree); + + var symbols = BindSymbols(services, semanticModel, newToken, cancellationToken); + if (symbols.IsEmpty) + return false; + + analysis = GetNullabilityAnalysis(semanticModel, symbols[0], newBindableParent, cancellationToken); + return true; + + SyntaxNode? GetOuterSuppression(SyntaxNode node) + => node.Ancestors().FirstOrDefault(a => a.RawKind == syntaxFacts.SyntaxKinds.SuppressNullableWarningExpression); + } + } + + protected ImmutableArray BindSymbols( SolutionServices services, SemanticModel semanticModel, SyntaxToken token, CancellationToken cancellationToken) { var languageServices = services.GetLanguageServices(semanticModel.Language); @@ -203,14 +275,39 @@ private TokenInformation BindToken( AddSymbols(GetSymbolsFromToken(token, services, semanticModel, cancellationToken), checkAccessibility: true); AddSymbols(bindableParent != null ? semanticModel.GetMemberGroup(bindableParent, cancellationToken) : [], checkAccessibility: false); + return filteredSymbols.ToImmutableAndClear(); + + void AddSymbols(ImmutableArray symbols, bool checkAccessibility) + { + foreach (var symbol in symbols) + { + if (!IsOk(symbol)) + continue; + + if (checkAccessibility && !IsAccessible(symbol, enclosingType)) + continue; + + if (symbolSet.Add(symbol)) + filteredSymbols.Add(symbol); + } + } + } + + private TokenInformation BindToken( + SolutionServices services, SemanticModel semanticModel, SyntaxToken token, CancellationToken cancellationToken) + { + var filteredSymbols = BindSymbols(services, semanticModel, token, cancellationToken); + + var languageServices = services.GetLanguageServices(semanticModel.Language); + var syntaxFacts = languageServices.GetRequiredService(); + if (filteredSymbols is [var firstSymbol, ..]) { var isAwait = syntaxFacts.IsAwaitKeyword(token); - var nullabilityInfo = bindableParent != null - ? GetNullabilityAnalysis(semanticModel, firstSymbol, bindableParent, cancellationToken) - : default; + var nullabilityInfo = GetNullabilityAnalysis( + services, semanticModel, firstSymbol, token, cancellationToken); - return new TokenInformation(filteredSymbols.ToImmutableAndClear(), isAwait, nullabilityInfo); + return new TokenInformation(filteredSymbols, isAwait, nullabilityInfo); } // Couldn't bind the token to specific symbols. If it's an operator, see if we can at @@ -223,21 +320,6 @@ private TokenInformation BindToken( } return default; - - void AddSymbols(ImmutableArray symbols, bool checkAccessibility) - { - foreach (var symbol in symbols) - { - if (!IsOk(symbol)) - continue; - - if (checkAccessibility && !IsAccessible(symbol, enclosingType)) - continue; - - if (symbolSet.Add(symbol)) - filteredSymbols.Add(symbol); - } - } } private ImmutableArray GetSymbolsFromToken(SyntaxToken token, SolutionServices services, SemanticModel semanticModel, CancellationToken cancellationToken) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs index 9a163c7477c77..75f88b5726c43 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs @@ -1665,6 +1665,13 @@ public void GetPartsOfParenthesizedExpression( closeParen = parenthesizedExpression.CloseParenToken; } + public void GetPartsOfPostfixUnaryExpression(SyntaxNode node, out SyntaxNode operand, out SyntaxToken operatorToken) + { + var postfixUnaryExpression = (PostfixUnaryExpressionSyntax)node; + operand = postfixUnaryExpression.Operand; + operatorToken = postfixUnaryExpression.OperatorToken; + } + public void GetPartsOfPrefixUnaryExpression(SyntaxNode node, out SyntaxToken operatorToken, out SyntaxNode operand) { var prefixUnaryExpression = (PrefixUnaryExpressionSyntax)node; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs index 220d7208706c5..d4bfb48e6c1fa 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs @@ -34,7 +34,7 @@ namespace Microsoft.CodeAnalysis.LanguageService; /// /// /// 'GetXxxOfYYY' where 'XXX' matches the name of a property on a 'YYY' syntax construct that both C# and VB have. For -/// example 'GetExpressionOfMemberAccessExpression' corresponding to MemberAccessExpressionsyntax.Expression in both C# and +/// example 'GetExpressionOfMemberAccessExpression' corresponding to MemberAccessExpressionSyntax.Expression in both C# and /// VB. These functions should throw if passed a node that the corresponding 'IsYYY' did not return for. /// For nodes that only have a single child, these functions can stay here. For nodes with multiple children, these should migrate /// to and be built off of 'GetPartsOfXXX'. @@ -537,6 +537,7 @@ void GetPartsOfTupleExpression(SyntaxNode node, void GetPartsOfImplicitObjectCreationExpression(SyntaxNode node, out SyntaxToken keyword, out SyntaxNode argumentList, out SyntaxNode? initializer); void GetPartsOfParameter(SyntaxNode node, out SyntaxToken identifier, out SyntaxNode? @default); void GetPartsOfParenthesizedExpression(SyntaxNode node, out SyntaxToken openParen, out SyntaxNode expression, out SyntaxToken closeParen); + void GetPartsOfPostfixUnaryExpression(SyntaxNode node, out SyntaxNode operand, out SyntaxToken operatorToken); void GetPartsOfPrefixUnaryExpression(SyntaxNode node, out SyntaxToken operatorToken, out SyntaxNode operand); void GetPartsOfQualifiedName(SyntaxNode node, out SyntaxNode left, out SyntaxToken dotToken, out SyntaxNode right); void GetPartsOfUsingAliasDirective(SyntaxNode node, out SyntaxToken globalKeyword, out SyntaxToken alias, out SyntaxNode name); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFactsExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFactsExtensions.cs index f6a2d095ea2ce..fbb47a46606de 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFactsExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFactsExtensions.cs @@ -579,6 +579,12 @@ public static SyntaxNode GetNameOfMemberAccessExpression(this ISyntaxFacts synta return name; } + public static SyntaxNode GetOperandOfPostfixUnaryExpression(this ISyntaxFacts syntaxFacts, SyntaxNode node) + { + syntaxFacts.GetPartsOfPostfixUnaryExpression(node, out var operand, out _); + return operand; + } + public static SyntaxNode GetOperandOfPrefixUnaryExpression(this ISyntaxFacts syntaxFacts, SyntaxNode node) { syntaxFacts.GetPartsOfPrefixUnaryExpression(node, out _, out var operand); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb index 05044d486c833..5f3bf68ff6138 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb @@ -1866,6 +1866,10 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.LanguageService closeParen = parenthesizedExpression.CloseParenToken End Sub + Public Sub GetPartsOfPostfixUnaryExpression(node As SyntaxNode, ByRef operand As SyntaxNode, ByRef operatorToken As SyntaxToken) Implements ISyntaxFacts.GetPartsOfPostfixUnaryExpression + Throw New InvalidOperationException(DoesNotExistInVBErrorMessage) + End Sub + Public Sub GetPartsOfPrefixUnaryExpression(node As SyntaxNode, ByRef operatorToken As SyntaxToken, ByRef operand As SyntaxNode) Implements ISyntaxFacts.GetPartsOfPrefixUnaryExpression Dim unaryExpression = DirectCast(node, UnaryExpressionSyntax) operatorToken = unaryExpression.OperatorToken