Skip to content

Commit 2736f00

Browse files
Fixup quick info for suppressed nullable operations. (#79636)
2 parents 8fefbab + bbdb137 commit 2736f00

File tree

9 files changed

+173
-44
lines changed

9 files changed

+173
-44
lines changed

src/EditorFeatures/CSharpTest/QuickInfo/SemanticQuickInfoSourceTests.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6997,6 +6997,40 @@ void N(string? s)
69976997
MainDescription($"({FeaturesResources.parameter}) string? s"),
69986998
NullabilityAnalysis(string.Format(FeaturesResources._0_may_be_null_here, "s")));
69996999

7000+
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/42543")]
7001+
public Task NullableParameterThatIsMaybeNull_Suppressed1()
7002+
=> TestWithOptionsAsync(TestOptions.Regular8,
7003+
"""
7004+
#nullable enable
7005+
7006+
class X
7007+
{
7008+
void N(string? s)
7009+
{
7010+
string s2 = $$s!;
7011+
}
7012+
}
7013+
""",
7014+
MainDescription($"({FeaturesResources.parameter}) string? s"),
7015+
NullabilityAnalysis(string.Format(FeaturesResources._0_may_be_null_here, "s")));
7016+
7017+
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/42543")]
7018+
public Task NullableParameterThatIsMaybeNull_Suppressed2()
7019+
=> TestWithOptionsAsync(TestOptions.Regular8,
7020+
"""
7021+
#nullable enable
7022+
7023+
class X
7024+
{
7025+
void N(string? s)
7026+
{
7027+
string s2 = $$s!!;
7028+
}
7029+
}
7030+
""",
7031+
MainDescription($"({FeaturesResources.parameter}) string? s"),
7032+
NullabilityAnalysis(string.Format(FeaturesResources._0_may_be_null_here, "s")));
7033+
70007034
[Fact]
70017035
public Task NullableParameterThatIsNotNull()
70027036
=> TestWithOptionsAsync(TestOptions.Regular8,

src/EditorFeatures/Test2/Workspaces/SymbolDescriptionServiceTests.vb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,8 @@ Imports Microsoft.CodeAnalysis.Host
99
Imports Microsoft.CodeAnalysis.LanguageService
1010

1111
Namespace Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces
12-
1312
<[UseExportProvider]>
1413
Public Class SymbolDescriptionServiceTests
15-
1614
Private Shared Async Function TestAsync(languageServiceProvider As HostLanguageServices, workspace As EditorTestWorkspace, expectedDescription As String) As Task
1715

1816
Dim solution = workspace.CurrentSolution

src/Features/Core/Portable/LanguageServices/SymbolDisplayService/AbstractSymbolDisplayService.cs

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,12 @@
1212

1313
namespace Microsoft.CodeAnalysis.LanguageService;
1414

15-
internal abstract partial class AbstractSymbolDisplayService : ISymbolDisplayService
15+
internal abstract partial class AbstractSymbolDisplayService(LanguageServices services) : ISymbolDisplayService
1616
{
17-
protected readonly LanguageServices LanguageServices;
18-
19-
protected AbstractSymbolDisplayService(LanguageServices services)
20-
{
21-
LanguageServices = services;
22-
}
17+
protected readonly LanguageServices LanguageServices = services;
2318

2419
protected abstract AbstractSymbolDescriptionBuilder CreateDescriptionBuilder(SemanticModel semanticModel, int position, SymbolDescriptionOptions options, CancellationToken cancellationToken);
2520

26-
public Task<string> ToDescriptionStringAsync(SemanticModel semanticModel, int position, ISymbol symbol, SymbolDescriptionOptions options, SymbolDescriptionGroups groups, CancellationToken cancellationToken)
27-
=> ToDescriptionStringAsync(semanticModel, position, [symbol], options, groups, cancellationToken);
28-
29-
public async Task<string> ToDescriptionStringAsync(SemanticModel semanticModel, int position, ImmutableArray<ISymbol> symbols, SymbolDescriptionOptions options, SymbolDescriptionGroups groups, CancellationToken cancellationToken)
30-
{
31-
var parts = await ToDescriptionPartsAsync(semanticModel, position, symbols, options, groups, cancellationToken).ConfigureAwait(false);
32-
return parts.ToDisplayString();
33-
}
34-
3521
public Task<ImmutableArray<SymbolDisplayPart>> ToDescriptionPartsAsync(SemanticModel semanticModel, int position, ImmutableArray<ISymbol> symbols, SymbolDescriptionOptions options, SymbolDescriptionGroups groups, CancellationToken cancellationToken)
3622
{
3723
if (symbols.Length == 0)
@@ -45,9 +31,7 @@ public async Task<IDictionary<SymbolDescriptionGroups, ImmutableArray<TaggedText
4531
SemanticModel semanticModel, int position, ImmutableArray<ISymbol> symbols, SymbolDescriptionOptions options, CancellationToken cancellationToken)
4632
{
4733
if (symbols.Length == 0)
48-
{
4934
return SpecializedCollections.EmptyDictionary<SymbolDescriptionGroups, ImmutableArray<TaggedText>>();
50-
}
5135

5236
var builder = CreateDescriptionBuilder(semanticModel, position, options, cancellationToken);
5337
return await builder.BuildDescriptionSectionsAsync(symbols).ConfigureAwait(false);

src/Features/Core/Portable/LanguageServices/SymbolDisplayService/ISymbolDisplayService.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,21 @@ namespace Microsoft.CodeAnalysis.LanguageService;
1212

1313
internal interface ISymbolDisplayService : ILanguageService
1414
{
15-
Task<string> ToDescriptionStringAsync(SemanticModel semanticModel, int position, ISymbol symbol, SymbolDescriptionOptions options, SymbolDescriptionGroups groups = SymbolDescriptionGroups.All, CancellationToken cancellationToken = default);
16-
Task<string> ToDescriptionStringAsync(SemanticModel semanticModel, int position, ImmutableArray<ISymbol> symbols, SymbolDescriptionOptions options, SymbolDescriptionGroups groups = SymbolDescriptionGroups.All, CancellationToken cancellationToken = default);
1715
Task<ImmutableArray<SymbolDisplayPart>> ToDescriptionPartsAsync(SemanticModel semanticModel, int position, ImmutableArray<ISymbol> symbols, SymbolDescriptionOptions options, SymbolDescriptionGroups groups = SymbolDescriptionGroups.All, CancellationToken cancellationToken = default);
1816
Task<IDictionary<SymbolDescriptionGroups, ImmutableArray<TaggedText>>> ToDescriptionGroupsAsync(SemanticModel semanticModel, int position, ImmutableArray<ISymbol> symbols, SymbolDescriptionOptions options, CancellationToken cancellationToken = default);
1917
}
18+
19+
internal static class ISymbolDisplayServiceExtensions
20+
{
21+
extension(ISymbolDisplayService service)
22+
{
23+
public Task<string> ToDescriptionStringAsync(SemanticModel semanticModel, int position, ISymbol symbol, SymbolDescriptionOptions options, SymbolDescriptionGroups groups = SymbolDescriptionGroups.All, CancellationToken cancellationToken = default)
24+
=> service.ToDescriptionStringAsync(semanticModel, position, [symbol], options, groups, cancellationToken);
25+
26+
public async Task<string> ToDescriptionStringAsync(SemanticModel semanticModel, int position, ImmutableArray<ISymbol> symbols, SymbolDescriptionOptions options, SymbolDescriptionGroups groups = SymbolDescriptionGroups.All, CancellationToken cancellationToken = default)
27+
{
28+
var parts = await service.ToDescriptionPartsAsync(semanticModel, position, symbols, options, groups, cancellationToken).ConfigureAwait(false);
29+
return parts.ToDisplayString();
30+
}
31+
}
32+
}

src/Features/Core/Portable/QuickInfo/CommonSemanticQuickInfoProvider.cs

Lines changed: 103 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
using System.Linq;
99
using System.Threading;
1010
using System.Threading.Tasks;
11+
using Microsoft.CodeAnalysis.Editing;
1112
using Microsoft.CodeAnalysis.Host;
1213
using Microsoft.CodeAnalysis.LanguageService;
1314
using Microsoft.CodeAnalysis.PooledObjects;
14-
using Microsoft.CodeAnalysis.Shared.Collections;
1515
using Microsoft.CodeAnalysis.Shared.Extensions;
1616
using Microsoft.CodeAnalysis.Shared.Utilities;
1717
using Roslyn.Utilities;
@@ -20,6 +20,8 @@ namespace Microsoft.CodeAnalysis.QuickInfo;
2020

2121
internal abstract partial class CommonSemanticQuickInfoProvider : CommonQuickInfoProvider
2222
{
23+
private static readonly SyntaxAnnotation s_annotation = new();
24+
2325
protected override async Task<QuickInfoItem?> BuildQuickInfoAsync(
2426
QuickInfoContext context, SyntaxToken token)
2527
{
@@ -188,7 +190,77 @@ protected static Task<QuickInfoItem> CreateContentAsync(
188190

189191
protected virtual (NullableAnnotation, NullableFlowState) GetNullabilityAnalysis(SemanticModel semanticModel, ISymbol symbol, SyntaxNode node, CancellationToken cancellationToken) => default;
190192

191-
private TokenInformation BindToken(
193+
private (NullableAnnotation, NullableFlowState) GetNullabilityAnalysis(
194+
SolutionServices services, SemanticModel semanticModel, ISymbol symbol, SyntaxToken token, CancellationToken cancellationToken)
195+
{
196+
var languageServices = services.GetLanguageServices(semanticModel.Language);
197+
var syntaxFacts = languageServices.GetRequiredService<ISyntaxFactsService>();
198+
199+
var bindableParent = syntaxFacts.TryGetBindableParent(token);
200+
if (bindableParent is null)
201+
return default;
202+
203+
return TryGetNullabilityAnalysisForSuppressedExpression(out var analysis)
204+
? analysis
205+
: GetNullabilityAnalysis(semanticModel, symbol, bindableParent, cancellationToken);
206+
207+
bool TryGetNullabilityAnalysisForSuppressedExpression(out (NullableAnnotation, NullableFlowState) analysis)
208+
{
209+
analysis = default;
210+
211+
// Look to see if we're inside a suppression (e.g. `expr!`). The suppression changes the nullability analysis,
212+
// and we don't actually want that here as we want to show the original nullability prior to the suppression applying.
213+
//
214+
// In that case, actually fork the semantic model with the `!` removed and then re-bind the token, getting the
215+
// analysis results from that.
216+
var tokenParent = token.GetRequiredParent();
217+
var parentSuppression = GetOuterSuppression(tokenParent);
218+
if (parentSuppression is null)
219+
return false;
220+
221+
var root = semanticModel.SyntaxTree.GetRoot(cancellationToken);
222+
223+
var editor = new SyntaxEditor(root, services);
224+
// First, mark the token, so we can find it later.
225+
editor.ReplaceNode(
226+
tokenParent, tokenParent.ReplaceToken(token, token.WithAdditionalAnnotations(s_annotation)));
227+
228+
// Now walk upwards, removing all the suppressions until we hit the top of the suppression chain.
229+
for (var currentSuppression = parentSuppression;
230+
currentSuppression is not null;
231+
currentSuppression = GetOuterSuppression(currentSuppression))
232+
{
233+
editor.ReplaceNode(
234+
currentSuppression,
235+
(current, _) => syntaxFacts.GetOperandOfPostfixUnaryExpression(current));
236+
}
237+
238+
// Now fork the semantic model with the new root that has the suppressions removed.
239+
var newRoot = editor.GetChangedRoot();
240+
241+
var newTree = semanticModel.SyntaxTree.WithRootAndOptions(newRoot, semanticModel.SyntaxTree.Options);
242+
var newToken = newTree.GetRoot(cancellationToken).GetAnnotatedTokens(s_annotation).Single();
243+
244+
var newBindableParent = syntaxFacts.TryGetBindableParent(newToken);
245+
if (newBindableParent is null)
246+
return false;
247+
248+
var newCompilation = semanticModel.Compilation.ReplaceSyntaxTree(semanticModel.SyntaxTree, newTree);
249+
semanticModel = newCompilation.GetSemanticModel(newTree);
250+
251+
var symbols = BindSymbols(services, semanticModel, newToken, cancellationToken);
252+
if (symbols.IsEmpty)
253+
return false;
254+
255+
analysis = GetNullabilityAnalysis(semanticModel, symbols[0], newBindableParent, cancellationToken);
256+
return true;
257+
258+
SyntaxNode? GetOuterSuppression(SyntaxNode node)
259+
=> node.Ancestors().FirstOrDefault(a => a.RawKind == syntaxFacts.SyntaxKinds.SuppressNullableWarningExpression);
260+
}
261+
}
262+
263+
protected ImmutableArray<ISymbol> BindSymbols(
192264
SolutionServices services, SemanticModel semanticModel, SyntaxToken token, CancellationToken cancellationToken)
193265
{
194266
var languageServices = services.GetLanguageServices(semanticModel.Language);
@@ -203,14 +275,39 @@ private TokenInformation BindToken(
203275
AddSymbols(GetSymbolsFromToken(token, services, semanticModel, cancellationToken), checkAccessibility: true);
204276
AddSymbols(bindableParent != null ? semanticModel.GetMemberGroup(bindableParent, cancellationToken) : [], checkAccessibility: false);
205277

278+
return filteredSymbols.ToImmutableAndClear();
279+
280+
void AddSymbols(ImmutableArray<ISymbol> symbols, bool checkAccessibility)
281+
{
282+
foreach (var symbol in symbols)
283+
{
284+
if (!IsOk(symbol))
285+
continue;
286+
287+
if (checkAccessibility && !IsAccessible(symbol, enclosingType))
288+
continue;
289+
290+
if (symbolSet.Add(symbol))
291+
filteredSymbols.Add(symbol);
292+
}
293+
}
294+
}
295+
296+
private TokenInformation BindToken(
297+
SolutionServices services, SemanticModel semanticModel, SyntaxToken token, CancellationToken cancellationToken)
298+
{
299+
var filteredSymbols = BindSymbols(services, semanticModel, token, cancellationToken);
300+
301+
var languageServices = services.GetLanguageServices(semanticModel.Language);
302+
var syntaxFacts = languageServices.GetRequiredService<ISyntaxFactsService>();
303+
206304
if (filteredSymbols is [var firstSymbol, ..])
207305
{
208306
var isAwait = syntaxFacts.IsAwaitKeyword(token);
209-
var nullabilityInfo = bindableParent != null
210-
? GetNullabilityAnalysis(semanticModel, firstSymbol, bindableParent, cancellationToken)
211-
: default;
307+
var nullabilityInfo = GetNullabilityAnalysis(
308+
services, semanticModel, firstSymbol, token, cancellationToken);
212309

213-
return new TokenInformation(filteredSymbols.ToImmutableAndClear(), isAwait, nullabilityInfo);
310+
return new TokenInformation(filteredSymbols, isAwait, nullabilityInfo);
214311
}
215312

216313
// 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(
223320
}
224321

225322
return default;
226-
227-
void AddSymbols(ImmutableArray<ISymbol> symbols, bool checkAccessibility)
228-
{
229-
foreach (var symbol in symbols)
230-
{
231-
if (!IsOk(symbol))
232-
continue;
233-
234-
if (checkAccessibility && !IsAccessible(symbol, enclosingType))
235-
continue;
236-
237-
if (symbolSet.Add(symbol))
238-
filteredSymbols.Add(symbol);
239-
}
240-
}
241323
}
242324

243325
private ImmutableArray<ISymbol> GetSymbolsFromToken(SyntaxToken token, SolutionServices services, SemanticModel semanticModel, CancellationToken cancellationToken)

src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1665,6 +1665,13 @@ public void GetPartsOfParenthesizedExpression(
16651665
closeParen = parenthesizedExpression.CloseParenToken;
16661666
}
16671667

1668+
public void GetPartsOfPostfixUnaryExpression(SyntaxNode node, out SyntaxNode operand, out SyntaxToken operatorToken)
1669+
{
1670+
var postfixUnaryExpression = (PostfixUnaryExpressionSyntax)node;
1671+
operand = postfixUnaryExpression.Operand;
1672+
operatorToken = postfixUnaryExpression.OperatorToken;
1673+
}
1674+
16681675
public void GetPartsOfPrefixUnaryExpression(SyntaxNode node, out SyntaxToken operatorToken, out SyntaxNode operand)
16691676
{
16701677
var prefixUnaryExpression = (PrefixUnaryExpressionSyntax)node;

src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ namespace Microsoft.CodeAnalysis.LanguageService;
3434
/// </item>
3535
/// <item>
3636
/// 'GetXxxOfYYY' where 'XXX' matches the name of a property on a 'YYY' syntax construct that both C# and VB have. For
37-
/// example 'GetExpressionOfMemberAccessExpression' corresponding to MemberAccessExpressionsyntax.Expression in both C# and
37+
/// example 'GetExpressionOfMemberAccessExpression' corresponding to MemberAccessExpressionSyntax.Expression in both C# and
3838
/// VB. These functions should throw if passed a node that the corresponding 'IsYYY' did not return <see langword="true"/> for.
3939
/// For nodes that only have a single child, these functions can stay here. For nodes with multiple children, these should migrate
4040
/// to <see cref="ISyntaxFactsExtensions"/> and be built off of 'GetPartsOfXXX'.
@@ -537,6 +537,7 @@ void GetPartsOfTupleExpression<TArgumentSyntax>(SyntaxNode node,
537537
void GetPartsOfImplicitObjectCreationExpression(SyntaxNode node, out SyntaxToken keyword, out SyntaxNode argumentList, out SyntaxNode? initializer);
538538
void GetPartsOfParameter(SyntaxNode node, out SyntaxToken identifier, out SyntaxNode? @default);
539539
void GetPartsOfParenthesizedExpression(SyntaxNode node, out SyntaxToken openParen, out SyntaxNode expression, out SyntaxToken closeParen);
540+
void GetPartsOfPostfixUnaryExpression(SyntaxNode node, out SyntaxNode operand, out SyntaxToken operatorToken);
540541
void GetPartsOfPrefixUnaryExpression(SyntaxNode node, out SyntaxToken operatorToken, out SyntaxNode operand);
541542
void GetPartsOfQualifiedName(SyntaxNode node, out SyntaxNode left, out SyntaxToken dotToken, out SyntaxNode right);
542543
void GetPartsOfUsingAliasDirective(SyntaxNode node, out SyntaxToken globalKeyword, out SyntaxToken alias, out SyntaxNode name);

src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFactsExtensions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,12 @@ public static SyntaxNode GetNameOfMemberAccessExpression(this ISyntaxFacts synta
579579
return name;
580580
}
581581

582+
public static SyntaxNode GetOperandOfPostfixUnaryExpression(this ISyntaxFacts syntaxFacts, SyntaxNode node)
583+
{
584+
syntaxFacts.GetPartsOfPostfixUnaryExpression(node, out var operand, out _);
585+
return operand;
586+
}
587+
582588
public static SyntaxNode GetOperandOfPrefixUnaryExpression(this ISyntaxFacts syntaxFacts, SyntaxNode node)
583589
{
584590
syntaxFacts.GetPartsOfPrefixUnaryExpression(node, out _, out var operand);

src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1866,6 +1866,10 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.LanguageService
18661866
closeParen = parenthesizedExpression.CloseParenToken
18671867
End Sub
18681868

1869+
Public Sub GetPartsOfPostfixUnaryExpression(node As SyntaxNode, ByRef operand As SyntaxNode, ByRef operatorToken As SyntaxToken) Implements ISyntaxFacts.GetPartsOfPostfixUnaryExpression
1870+
Throw New InvalidOperationException(DoesNotExistInVBErrorMessage)
1871+
End Sub
1872+
18691873
Public Sub GetPartsOfPrefixUnaryExpression(node As SyntaxNode, ByRef operatorToken As SyntaxToken, ByRef operand As SyntaxNode) Implements ISyntaxFacts.GetPartsOfPrefixUnaryExpression
18701874
Dim unaryExpression = DirectCast(node, UnaryExpressionSyntax)
18711875
operatorToken = unaryExpression.OperatorToken

0 commit comments

Comments
 (0)