Skip to content

Commit ea9b758

Browse files
authored
FieldArgumentAnalyzer (#3755)
* FieldArgumentAnalyzer * cleanup * cleanup * Cleanup
1 parent cba0429 commit ea9b758

15 files changed

Lines changed: 838 additions & 8 deletions
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# GQL008: Don't use an obsolete 'Argument' method
2+
3+
| | Value |
4+
| ---------------------- | ------- |
5+
| **Rule ID** | GQL008 |
6+
| **Category** | Usage |
7+
| **Default severity** | Warning |
8+
| **Enabled by default** | Yes |
9+
| **Code fix provided** | Yes |
10+
11+
## Cause
12+
13+
This rule triggers when the obsolete `Argument<TArgumentGraphType, TArgumentType>()` method with two type parameters was used on the field builder.
14+
15+
## Rule description
16+
17+
The method overload `Argument<TArgumentGraphType, TArgumentType>(name, description, defaultValue, configure)` is obsolete and will be remove in future version.
18+
19+
## How to fix violations
20+
21+
Use `Argument<TArgumentGraphType>()` method overload with a single generic type parameter. Use `Action<QueryArgument>` parameter to configure default argument value.
22+
23+
## Example of a violation
24+
25+
```c#
26+
Field<StringGraphType>("Text").Argument<StringGraphType, string>(
27+
"arg",
28+
"description",
29+
"MyDefault");
30+
31+
Field<StringGraphType>("Text").Argument<StringGraphType, string>(
32+
"arg",
33+
"description",
34+
"MyDefault",
35+
argument => argument.DeprecationReason = "Deprecation Reason");
36+
```
37+
38+
## Example of how to fix
39+
40+
```c#
41+
Field<StringGraphType>("Text").Argument<StringGraphType>(
42+
"arg",
43+
"description",
44+
argument => argument.DefaultValue = "MyDefault");
45+
46+
Field<StringGraphType>("Text").Argument<StringGraphType>(
47+
"arg",
48+
"description",
49+
argument =>
50+
{
51+
argument.DeprecationReason = "Deprecation Reason";
52+
argument.DefaultValue = "MyDefault";
53+
});
54+
```
55+
56+
## Suppress a warning
57+
58+
If you just want to suppress a single violation, add preprocessor directives to your source file to disable and then re-enable the rule.
59+
60+
```csharp
61+
#pragma warning disable GQL008
62+
// The code that's violating the rule is on this line.
63+
#pragma warning restore GQL008
64+
```
65+
66+
To disable the rule for a file, folder, or project, set its severity to `none` in the [configuration file](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/configuration-files).
67+
68+
```ini
69+
[*.cs]
70+
dotnet_diagnostic.GQL008.severity = none
71+
```
72+
73+
For more information, see [How to suppress code analysis warnings](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/suppress-warnings).
74+
75+
## Related rules

docs2/site/docs/analyzers/Overview.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ The `ResolverAnalyzer` focuses on identifying incorrect usage of resolver method
5050

5151
The analyzer detects input graph type fields that can't be mapped to the source type during deserialization process.
5252

53+
### 5. FieldArgumentAnalyzer
54+
55+
The analyzer detects an obsolete `Argument` method usage and offers a code fix to automatically replace it with with another `Argument` overload.
56+
5357
## Configuration in .editorconfig
5458

5559
Certain analyzers and code fixes offer configuration options that control when the rule is applied and how the automatic code fix executes code adjustments. Refer to the specific documentation page for each analyzer to understand the available configuration options and their application methods.
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
using System.Collections.Immutable;
2+
using System.Composition;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CodeActions;
5+
using Microsoft.CodeAnalysis.CodeFixes;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
using Microsoft.CodeAnalysis.Editing;
9+
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
10+
11+
namespace GraphQL.Analyzers;
12+
13+
[Shared]
14+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(FieldArgumentCodeFixProvider))]
15+
public class FieldArgumentCodeFixProvider : CodeFixProvider
16+
{
17+
private const string LAMBDA_EXPRESSION_PARAMETER_NAME = "argument";
18+
19+
public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } =
20+
ImmutableArray.Create(FieldArgumentAnalyzer.DoNotUseObsoleteArgumentMethod.Id);
21+
22+
public sealed override FixAllProvider GetFixAllProvider() =>
23+
WellKnownFixAllProviders.BatchFixer;
24+
25+
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
26+
{
27+
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
28+
29+
foreach (var diagnostic in context.Diagnostics)
30+
{
31+
var diagnosticSpan = diagnostic.Location.SourceSpan;
32+
33+
var argumentInvocation = (InvocationExpressionSyntax)root!.FindNode(diagnosticSpan);
34+
35+
const string codeFixTitle = "Rewrite obsolete 'Argument' method";
36+
37+
context.RegisterCodeFix(
38+
CodeAction.Create(
39+
title: codeFixTitle,
40+
createChangedDocument: ct =>
41+
RewriteArgumentMethodAsync(context.Document, argumentInvocation, ct),
42+
equivalenceKey: codeFixTitle),
43+
diagnostic);
44+
}
45+
}
46+
47+
private static async Task<Document> RewriteArgumentMethodAsync(
48+
Document document,
49+
InvocationExpressionSyntax argumentInvocation,
50+
CancellationToken cancellationToken)
51+
{
52+
var docEditor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
53+
54+
var argumentMemberAccess = (MemberAccessExpressionSyntax)argumentInvocation.Expression;
55+
if (argumentMemberAccess.Name is not GenericNameSyntax genericName)
56+
{
57+
return document;
58+
}
59+
60+
if (genericName.TypeArgumentList.Arguments.Count != 2)
61+
{
62+
return document;
63+
}
64+
65+
var newMethodName = genericName
66+
.WithTypeArgumentList(
67+
RewriteTypeArgumentList(genericName));
68+
69+
docEditor.ReplaceNode(argumentMemberAccess.Name, newMethodName);
70+
71+
var semanticModel = (await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false))!;
72+
var defaultValueArg = argumentInvocation.GetMethodArgument(Constants.ArgumentNames.DefaultValue, semanticModel);
73+
74+
// .Argument<T>(x, y) or .Argument<T>(x, y, configure: arg => ...)
75+
if (defaultValueArg == null)
76+
{
77+
return docEditor.GetChangedDocument();
78+
}
79+
80+
var configureArg = argumentInvocation.GetMethodArgument(Constants.ArgumentNames.Configure, semanticModel);
81+
82+
// .Argument<T>(x, y, defaultValue)
83+
if (configureArg == null)
84+
{
85+
var configureLambdaArg = CreateConfigureLambdaArgument(defaultValueArg);
86+
docEditor.ReplaceNode(defaultValueArg, configureLambdaArg);
87+
return docEditor.GetChangedDocument();
88+
}
89+
90+
if (configureArg.Expression is not SimpleLambdaExpressionSyntax configureLambda)
91+
{
92+
return document;
93+
}
94+
95+
// .Argument<,>(x, y, defaultValue, arg => ...)
96+
// .Argument<,>(x, y, arg => ...)
97+
// .Argument<>(x, y, arg => ...)
98+
SimpleLambdaExpressionSyntax? newConfigureLambda;
99+
100+
// arg => { arg.Prop = val; }
101+
if (configureLambda.Block != null)
102+
{
103+
newConfigureLambda = AddDefaultValueToConfigureLambdaBlock(configureLambda);
104+
}
105+
// arg => arg.Prop = val
106+
else if (configureLambda.ExpressionBody != null)
107+
{
108+
newConfigureLambda = ConvertBodyConfigureLambdaToBlockLambda(configureLambda);
109+
}
110+
else
111+
{
112+
// theoretically should never happen
113+
return document;
114+
}
115+
116+
var newMethodArgumentList = argumentInvocation.ArgumentList
117+
.Arguments
118+
.Insert(2, Argument(newConfigureLambda))
119+
.RemoveAt(4)
120+
.RemoveAt(3);
121+
122+
var newArgumentsList = argumentInvocation.ArgumentList.WithArguments(newMethodArgumentList);
123+
docEditor.ReplaceNode(argumentInvocation.ArgumentList, newArgumentsList);
124+
125+
return docEditor.GetChangedDocument();
126+
127+
SimpleLambdaExpressionSyntax AddDefaultValueToConfigureLambdaBlock(SimpleLambdaExpressionSyntax lambda)
128+
{
129+
var (leadingTrivia, trailingTrivia) = GetBlockStatementTrivia(lambda.Block!);
130+
var defaultValueStatement = CreateDefaultValueStatement(leadingTrivia, defaultValueArg, trailingTrivia);
131+
132+
return lambda.WithBlock(
133+
lambda.Block!.AddStatements(defaultValueStatement));
134+
}
135+
136+
SimpleLambdaExpressionSyntax ConvertBodyConfigureLambdaToBlockLambda(SimpleLambdaExpressionSyntax lambda)
137+
{
138+
var whitespace = Whitespace(" ");
139+
var block = SyntaxFactory
140+
.Block(
141+
ExpressionStatement(lambda.ExpressionBody!.WithoutTrailingTrivia())
142+
.WithLeadingTrivia(whitespace),
143+
ExpressionStatement(
144+
CreateDefaultValuePropertyAssignment(lambda.Parameter.ToString(), defaultValueArg.Expression))
145+
.WithLeadingTrivia(whitespace)
146+
.WithTrailingTrivia(whitespace))
147+
.WithLeadingTrivia(lambda.ExpressionBody!.GetLeadingTrivia());
148+
149+
return SimpleLambdaExpression(lambda.Parameter, block)
150+
.WithArrowToken(lambda.ArrowToken);
151+
}
152+
}
153+
154+
private static (SyntaxTriviaList leadingTrivia, SyntaxTriviaList trailingTrivia) GetBlockStatementTrivia(BlockSyntax block)
155+
{
156+
var lastStatement = block.Statements.LastOrDefault();
157+
158+
// argument =>
159+
// {
160+
// statement;
161+
// }
162+
// or
163+
// argument => { statement; }
164+
if (lastStatement != null)
165+
{
166+
return (lastStatement.GetLeadingTrivia(), lastStatement.GetTrailingTrivia());
167+
}
168+
169+
// argument => { }
170+
if (block.OpenBraceToken.GetLocation().GetLineSpan().StartLinePosition.Line ==
171+
block.CloseBraceToken.GetLocation().GetLineSpan().StartLinePosition.Line)
172+
{
173+
return (SyntaxTriviaList.Empty, SyntaxTriviaList.Empty);
174+
}
175+
176+
// argument =>
177+
// {
178+
// }
179+
var leadingTrivia = block.GetLeadingTrivia().Add(Whitespace(" "));
180+
181+
return (leadingTrivia, new SyntaxTriviaList(EndOfLine(Environment.NewLine)));
182+
}
183+
184+
// {whitespaces}argument.DefaultValue = xxx;\n
185+
private static ExpressionStatementSyntax CreateDefaultValueStatement(
186+
SyntaxTriviaList leadingTrivia,
187+
ArgumentSyntax defaultValueArg,
188+
SyntaxTriviaList trailingTrivia) =>
189+
ExpressionStatement(CreateDefaultValuePropertyAssignment(defaultValueArg.Expression))
190+
.WithLeadingTrivia(leadingTrivia)
191+
.WithTrailingTrivia(trailingTrivia);
192+
193+
// argument => argument.DefaultValue = expression
194+
private static ArgumentSyntax CreateConfigureLambdaArgument(ArgumentSyntax defaultValueArg)
195+
{
196+
var configureLambda = ParseExpression(
197+
$"{LAMBDA_EXPRESSION_PARAMETER_NAME} => {CreateDefaultValuePropertyAssignment(defaultValueArg.Expression)}")
198+
.WithLeadingTrivia(defaultValueArg.GetLeadingTrivia());
199+
200+
return Argument(configureLambda);
201+
}
202+
203+
// argument.DefaultValue = expression
204+
private static ExpressionSyntax CreateDefaultValuePropertyAssignment(ExpressionSyntax expression) =>
205+
CreateDefaultValuePropertyAssignment(LAMBDA_EXPRESSION_PARAMETER_NAME, expression);
206+
207+
// parameterName.DefaultValue = expression
208+
private static ExpressionSyntax CreateDefaultValuePropertyAssignment(string parameterName, ExpressionSyntax expression) =>
209+
ParseExpression($"{parameterName}.{Constants.ObjectProperties.DefaultValue} = {expression}");
210+
211+
// from Argument<TArgumentGraphType, TArgumentType>
212+
// to Argument<TArgumentGraphType>
213+
private static TypeArgumentListSyntax RewriteTypeArgumentList(GenericNameSyntax genericName) =>
214+
genericName.TypeArgumentList
215+
.WithArguments(
216+
SingletonSeparatedList(
217+
genericName.TypeArgumentList.Arguments.First()));
218+
}

src/GraphQL.Analyzers.CodeFixes/FieldBuilderCodeFixProvider.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
3131
foreach (var diagnostic in context.Diagnostics)
3232
{
3333
var diagnosticSpan = diagnostic.Location.SourceSpan;
34-
bool isAsyncField = diagnostic.Properties.ContainsKey(Constants.Properties.IsAsync);
35-
bool isDelegate = diagnostic.Properties.ContainsKey(Constants.Properties.IsDelegate);
34+
bool isAsyncField = diagnostic.Properties.ContainsKey(Constants.AnalyzerProperties.IsAsync);
35+
bool isDelegate = diagnostic.Properties.ContainsKey(Constants.AnalyzerProperties.IsDelegate);
3636

3737
var fieldInvocationExpression = (InvocationExpressionSyntax)root!.FindNode(diagnosticSpan);
3838

src/GraphQL.Analyzers.CodeFixes/FieldNameCodeFixProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
2828
foreach (var diagnostic in context.Diagnostics)
2929
{
3030
var diagnosticSpan = diagnostic.Location.SourceSpan;
31-
string builderMethodName = diagnostic.Properties[Constants.Properties.BuilderMethodName]!;
31+
string builderMethodName = diagnostic.Properties[Constants.AnalyzerProperties.BuilderMethodName]!;
3232

3333
var nameInvocationExpression = (InvocationExpressionSyntax)root!.FindNode(diagnosticSpan);
3434
var nameMemberAccess = (MemberAccessExpressionSyntax)nameInvocationExpression.Expression;

0 commit comments

Comments
 (0)