Skip to content

Commit fa65373

Browse files
Make Implement with Copilot light up on member name (#77769)
Every time a developer uses the 💡 `Generate Method` feature in VS, then with a ctrl+dot the cursor gets placed on the name of the newly generated method which by default contains `throw new NotImplementedException()`: https://github.com/user-attachments/assets/5aa69115-6660-43c4-b711-eaa0f4772d8d To help the discoverability and usability of `Implement with Copilot`, especially for the developers frequently using the `Generate Method` feature, I am making the analyzer diagnostic for `Implement with Copilot` also light up on the method or property name containing the `NotImplementedException` throw nodes.
2 parents 209d492 + 67c82fc commit fa65373

5 files changed

+415
-128
lines changed

src/Features/CSharp/Portable/Copilot/CSharpImplementNotImplementedExceptionDiagnosticAnalyzer.cs

Lines changed: 78 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,30 +28,92 @@ protected override void InitializeWorker(AnalysisContext context)
2828
context.RegisterCompilationStartAction(context =>
2929
{
3030
var notImplementedExceptionType = context.Compilation.GetTypeByMetadataName(typeof(NotImplementedException).FullName!);
31-
if (notImplementedExceptionType != null)
32-
context.RegisterOperationAction(context => AnalyzeThrow(context, notImplementedExceptionType), OperationKind.Throw);
31+
if (notImplementedExceptionType is null)
32+
return;
33+
34+
context.RegisterOperationBlockAction(context => AnalyzeOperationBlock(context, notImplementedExceptionType));
3335
});
3436
}
3537

36-
private void AnalyzeThrow(OperationAnalysisContext context, INamedTypeSymbol notImplementedExceptionType)
38+
private void AnalyzeOperationBlock(
39+
OperationBlockAnalysisContext context,
40+
INamedTypeSymbol notImplementedExceptionType)
3741
{
38-
var throwOperation = (IThrowOperation)context.Operation;
39-
if (throwOperation is
42+
foreach (var block in context.OperationBlocks)
43+
AnalyzeBlock(block);
44+
45+
void AnalyzeBlock(IOperation block)
46+
{
47+
foreach (var operation in block.DescendantsAndSelf())
4048
{
41-
Exception: IConversionOperation
49+
if (operation is IThrowOperation
50+
{
51+
Exception: IConversionOperation
52+
{
53+
Operand: IObjectCreationOperation
54+
{
55+
Constructor.ContainingType: INamedTypeSymbol constructedType,
56+
},
57+
},
58+
Syntax: var throwSyntax
59+
} throwOperation &&
60+
notImplementedExceptionType.Equals(constructedType))
4261
{
43-
Operand: IObjectCreationOperation
62+
// Report diagnostic for each throw operation
63+
context.ReportDiagnostic(Diagnostic.Create(
64+
Descriptor,
65+
throwOperation.Syntax.GetLocation()));
66+
67+
// If the throw is the top-level operation in the containing symbol, report a diagnostic on the
68+
// symbol as well. Note: consider reporting on the entire symbol, instead of just the name. And in
69+
// this case, do not report directly on the throw as well.
70+
if (ShouldReportAsTopLevel(block, operation))
4471
{
45-
Constructor.ContainingType: INamedTypeSymbol constructedType,
46-
},
47-
},
48-
Syntax: ThrowExpressionSyntax or ThrowStatementSyntax,
49-
} &&
50-
notImplementedExceptionType.Equals(constructedType))
72+
foreach (var location in context.OwningSymbol.Locations)
73+
{
74+
if (location.SourceTree == context.FilterTree)
75+
{
76+
context.ReportDiagnostic(Diagnostic.Create(
77+
Descriptor,
78+
location));
79+
}
80+
}
81+
}
82+
}
83+
}
84+
}
85+
}
86+
87+
private static bool ShouldReportAsTopLevel(IOperation block, IOperation operation)
88+
{
89+
if (block is IBlockOperation { Operations: [var child] })
5190
{
52-
context.ReportDiagnostic(Diagnostic.Create(
53-
Descriptor,
54-
throwOperation.Syntax.GetLocation()));
91+
// Handle: { throw new NotImplementedException(); }
92+
if (child == operation)
93+
return true;
94+
95+
// Handle: => throw new NotImplementedException();
96+
if (child is IReturnOperation
97+
{
98+
ReturnedValue: IConversionOperation
99+
{
100+
Operand: var operand
101+
}
102+
} && operand == operation &&
103+
// Excluding property declarations with expression bodies
104+
// Because their location is an exact match with the throw operation
105+
// and we don't want to report the same location twice
106+
operation.Syntax.Parent is not ArrowExpressionClauseSyntax { Parent: PropertyDeclarationSyntax })
107+
{
108+
// Include expression-bodied methods and get accessors
109+
return true;
110+
}
111+
112+
// Handle expression-bodied set accessors
113+
if (child is IExpressionStatementOperation { Operation: var throwOperation } && throwOperation == operation)
114+
return true;
55115
}
116+
117+
return false;
56118
}
57119
}

src/Features/CSharp/Portable/Copilot/CSharpImplementNotImplementedExceptionFixProvider.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,11 @@ await copilotService.IsImplementNotImplementedExceptionsAvailableAsync(cancellat
5555
return;
5656
}
5757

58-
var throwNode = context.Diagnostics[0].Location.FindNode(getInnermostNodeForTie: true, cancellationToken);
58+
var diagnosticNode = context.Diagnostics[0].Location.FindNode(getInnermostNodeForTie: true, cancellationToken);
5959

6060
// Preliminary analysis before registering fix
61-
var methodOrProperty = throwNode.FirstAncestorOrSelf<MemberDeclarationSyntax>();
61+
var methodOrProperty = diagnosticNode.FirstAncestorOrSelf<MemberDeclarationSyntax>();
62+
6263
if (methodOrProperty is BasePropertyDeclarationSyntax or BaseMethodDeclarationSyntax)
6364
{
6465
// Pull out the computation into a lazy computation here. That way if we compute (and thus cache) the
@@ -84,8 +85,9 @@ protected override async Task FixAllAsync(
8485

8586
foreach (var diagnostic in diagnostics)
8687
{
87-
var throwNode = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken);
88-
var methodOrProperty = throwNode.FirstAncestorOrSelf<MemberDeclarationSyntax>();
88+
var diagnosticNode = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken);
89+
var methodOrProperty = diagnosticNode.FirstAncestorOrSelf<MemberDeclarationSyntax>();
90+
8991
Contract.ThrowIfFalse(methodOrProperty is BasePropertyDeclarationSyntax or BaseMethodDeclarationSyntax);
9092

9193
if (!memberReferencesBuilder.ContainsKey(methodOrProperty))

0 commit comments

Comments
 (0)