Skip to content

Commit 1147afb

Browse files
authored
Merge pull request #2703 from vweijsters/fix-2593
Made SA1130 codefix properly expand parameters when only delegate key…
2 parents 8e12234 + 761ba0b commit 1147afb

3 files changed

Lines changed: 273 additions & 29 deletions

File tree

StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/ReadabilityRules/SA1130CodeFixProvider.cs

Lines changed: 94 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
namespace StyleCop.Analyzers.ReadabilityRules
55
{
6+
using System;
67
using System.Collections.Generic;
78
using System.Collections.Immutable;
89
using System.Composition;
@@ -24,6 +25,8 @@ namespace StyleCop.Analyzers.ReadabilityRules
2425
[Shared]
2526
internal class SA1130CodeFixProvider : CodeFixProvider
2627
{
28+
private static readonly SyntaxToken ParameterListSeparator = SyntaxFactory.Token(SyntaxKind.CommaToken).WithTrailingTrivia(SyntaxFactory.Space);
29+
2730
/// <inheritdoc/>
2831
public override ImmutableArray<string> FixableDiagnosticIds { get; } =
2932
ImmutableArray.Create(SA1130UseLambdaSyntax.DiagnosticId);
@@ -50,14 +53,38 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context)
5053
return SpecializedTasks.CompletedTask;
5154
}
5255

53-
private static SyntaxNode ReplaceWithLambda(AnonymousMethodExpressionSyntax anonymousMethod)
56+
private static SyntaxNode ReplaceWithLambda(SemanticModel semanticModel, AnonymousMethodExpressionSyntax anonymousMethod)
5457
{
5558
var parameterList = anonymousMethod.ParameterList;
5659
SyntaxNode lambdaExpression;
5760

5861
if (parameterList == null)
5962
{
60-
parameterList = SyntaxFactory.ParameterList()
63+
ImmutableArray<string> argumentList = default(ImmutableArray<string>);
64+
65+
switch (anonymousMethod.Parent.Kind())
66+
{
67+
case SyntaxKind.Argument:
68+
argumentList = GetMethodInvocationArgumentList(semanticModel, anonymousMethod);
69+
break;
70+
71+
case SyntaxKind.EqualsValueClause:
72+
argumentList = GetEqualsArgumentList(semanticModel, anonymousMethod);
73+
break;
74+
75+
case SyntaxKind.AddAssignmentExpression:
76+
case SyntaxKind.SubtractAssignmentExpression:
77+
argumentList = GetAssignmentArgumentList(semanticModel, anonymousMethod);
78+
break;
79+
}
80+
81+
List<ParameterSyntax> parameters = GenerateUniqueParameterNames(semanticModel, anonymousMethod, argumentList);
82+
83+
var newList = (parameters.Count > 0)
84+
? SyntaxFactory.SeparatedList(parameters, Enumerable.Repeat(ParameterListSeparator, parameters.Count - 1))
85+
: SyntaxFactory.SeparatedList<ParameterSyntax>();
86+
87+
parameterList = SyntaxFactory.ParameterList(newList)
6188
.WithLeadingTrivia(anonymousMethod.DelegateKeyword.LeadingTrivia)
6289
.WithTrailingTrivia(anonymousMethod.DelegateKeyword.TrailingTrivia);
6390
}
@@ -106,15 +133,74 @@ private static SyntaxNode ReplaceWithLambda(AnonymousMethodExpressionSyntax anon
106133
.WithAdditionalAnnotations(Formatter.Annotation);
107134
}
108135

136+
private static ImmutableArray<string> GetMethodInvocationArgumentList(SemanticModel semanticModel, AnonymousMethodExpressionSyntax anonymousMethod)
137+
{
138+
var argumentSyntax = (ArgumentSyntax)anonymousMethod.Parent;
139+
var argumentListSyntax = (ArgumentListSyntax)argumentSyntax.Parent;
140+
var originalInvocationExpression = (InvocationExpressionSyntax)argumentListSyntax.Parent;
141+
142+
var originalSymbolInfo = semanticModel.GetSymbolInfo(originalInvocationExpression);
143+
var argumentIndex = argumentListSyntax.Arguments.IndexOf(argumentSyntax);
144+
var parameterList = SA1130UseLambdaSyntax.GetDelegateParameterList((IMethodSymbol)originalSymbolInfo.Symbol, argumentIndex);
145+
return parameterList.Parameters.Select(p => p.Identifier.ToString()).ToImmutableArray();
146+
}
147+
148+
private static ImmutableArray<string> GetEqualsArgumentList(SemanticModel semanticModel, AnonymousMethodExpressionSyntax anonymousMethod)
149+
{
150+
var equalsValueClauseSyntax = (EqualsValueClauseSyntax)anonymousMethod.Parent;
151+
var variableDeclaration = (VariableDeclarationSyntax)equalsValueClauseSyntax.Parent.Parent;
152+
153+
var symbol = semanticModel.GetSymbolInfo(variableDeclaration.Type);
154+
var namedTypeSymbol = (INamedTypeSymbol)symbol.Symbol;
155+
return namedTypeSymbol.DelegateInvokeMethod.Parameters.Select(ps => ps.Name).ToImmutableArray();
156+
}
157+
158+
private static ImmutableArray<string> GetAssignmentArgumentList(SemanticModel semanticModel, AnonymousMethodExpressionSyntax anonymousMethod)
159+
{
160+
var assignmentExpressionSyntax = (AssignmentExpressionSyntax)anonymousMethod.Parent;
161+
162+
var symbol = semanticModel.GetSymbolInfo(assignmentExpressionSyntax.Left);
163+
var eventSymbol = (IEventSymbol)symbol.Symbol;
164+
var namedTypeSymbol = (INamedTypeSymbol)eventSymbol.Type;
165+
return namedTypeSymbol.DelegateInvokeMethod.Parameters.Select(ps => ps.Name).ToImmutableArray();
166+
}
167+
168+
private static List<ParameterSyntax> GenerateUniqueParameterNames(SemanticModel semanticModel, AnonymousMethodExpressionSyntax anonymousMethod, ImmutableArray<string> argumentNames)
169+
{
170+
var parameters = new List<ParameterSyntax>();
171+
172+
foreach (var argumentName in argumentNames)
173+
{
174+
var baseName = argumentName;
175+
var newName = baseName;
176+
var index = 0;
177+
178+
while (semanticModel.LookupSymbols(anonymousMethod.SpanStart, name: newName).Length > 0)
179+
{
180+
index++;
181+
newName = baseName + index;
182+
}
183+
184+
parameters.Add(SyntaxFactory.Parameter(SyntaxFactory.Identifier(newName)).WithType(null));
185+
}
186+
187+
return parameters;
188+
}
189+
109190
private static ParameterListSyntax RemoveType(ParameterListSyntax parameterList)
110191
{
111192
return parameterList.WithParameters(SyntaxFactory.SeparatedList(parameterList.Parameters.Select(x => RemoveType(x)), parameterList.Parameters.GetSeparators()));
112193
}
113194

114195
private static ParameterSyntax RemoveType(ParameterSyntax parameterSyntax)
115196
{
116-
var syntax = parameterSyntax.WithType(null)
117-
.WithLeadingTrivia(parameterSyntax.Type.GetLeadingTrivia().Concat(parameterSyntax.Type.GetTrailingTrivia()));
197+
var syntax = parameterSyntax.WithType(null);
198+
199+
if (parameterSyntax.Type != null)
200+
{
201+
syntax = syntax.WithLeadingTrivia(parameterSyntax.Type.GetLeadingTrivia().Concat(parameterSyntax.Type.GetTrailingTrivia()));
202+
}
203+
118204
return syntax.WithTrailingTrivia(syntax.GetTrailingTrivia().WithoutTrailingWhitespace())
119205
.WithLeadingTrivia(syntax.GetLeadingTrivia().WithoutWhitespace());
120206
}
@@ -131,10 +217,11 @@ private static bool IsValid(ParameterSyntax parameterSyntax)
131217
private static async Task<Document> GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
132218
{
133219
var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
220+
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
134221

135222
var anonymousMethod = (AnonymousMethodExpressionSyntax)syntaxRoot.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true);
136223

137-
var newSyntaxRoot = syntaxRoot.ReplaceNode(anonymousMethod, ReplaceWithLambda(anonymousMethod));
224+
var newSyntaxRoot = syntaxRoot.ReplaceNode(anonymousMethod, ReplaceWithLambda(semanticModel, anonymousMethod));
138225
var newDocument = document.WithSyntaxRoot(newSyntaxRoot.WithoutFormatting());
139226

140227
return newDocument;
@@ -150,17 +237,17 @@ private class FixAll : DocumentBasedFixAllProvider
150237
protected override async Task<SyntaxNode> FixAllInDocumentAsync(FixAllContext fixAllContext, Document document, ImmutableArray<Diagnostic> diagnostics)
151238
{
152239
var syntaxRoot = await document.GetSyntaxRootAsync(fixAllContext.CancellationToken).ConfigureAwait(false);
240+
var semanticModel = await document.GetSemanticModelAsync(fixAllContext.CancellationToken).ConfigureAwait(false);
153241

154242
var nodes = new List<AnonymousMethodExpressionSyntax>();
155243

156244
foreach (var diagnostic in diagnostics)
157245
{
158246
var node = (AnonymousMethodExpressionSyntax)syntaxRoot.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true);
159-
160247
nodes.Add(node);
161248
}
162249

163-
return syntaxRoot.ReplaceNodes(nodes, (originalNode, rewrittenNode) => ReplaceWithLambda(rewrittenNode));
250+
return syntaxRoot.ReplaceNodes(nodes, (originalNode, rewrittenNode) => ReplaceWithLambda(semanticModel, rewrittenNode));
164251
}
165252
}
166253
}

StyleCop.Analyzers/StyleCop.Analyzers.Test/ReadabilityRules/SA1130UnitTests.cs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,96 @@ public void Test()
294294
await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false);
295295
}
296296

297+
/// <summary>
298+
/// Verify that expansion of a delegate without parameters will generate a lambda with the necessary parameters.
299+
/// </summary>
300+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
301+
[Fact]
302+
[WorkItem(2593, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/2593")]
303+
public async Task VerifyThatDelegateExpansionWillNotGenerateInvalidCodeAsync()
304+
{
305+
var testCode = @"
306+
using System;
307+
308+
namespace StyleCopDemo
309+
{
310+
class Program
311+
{
312+
public delegate void TestDelegate(int arg1, double arg2);
313+
public static event EventHandler TestEvent;
314+
315+
static void Main(string[] args)
316+
{
317+
EventHandler anon = delegate { Console.WriteLine(""hello""); };
318+
TestEvent += delegate { Console.WriteLine(""hello""); };
319+
TestEvent -= delegate { Console.WriteLine(""hello""); };
320+
}
321+
322+
public static void TestMethod()
323+
{
324+
object sender = null;
325+
EventArgs e = EventArgs.Empty;
326+
327+
EventHandler anon = delegate { Console.WriteLine(""hello""); };
328+
329+
TestMethod2(delegate { Console.WriteLine(""hello""); });
330+
}
331+
332+
public static void TestMethod2(TestDelegate testDelegate)
333+
{
334+
}
335+
}
336+
}
337+
";
338+
339+
var fixedCode = @"
340+
using System;
341+
342+
namespace StyleCopDemo
343+
{
344+
class Program
345+
{
346+
public delegate void TestDelegate(int arg1, double arg2);
347+
public static event EventHandler TestEvent;
348+
349+
static void Main(string[] args)
350+
{
351+
EventHandler anon = (sender, e) => { Console.WriteLine(""hello""); };
352+
TestEvent += (sender, e) => { Console.WriteLine(""hello""); };
353+
TestEvent -= (sender, e) => { Console.WriteLine(""hello""); };
354+
}
355+
356+
public static void TestMethod()
357+
{
358+
object sender = null;
359+
EventArgs e = EventArgs.Empty;
360+
361+
EventHandler anon = (sender1, e1) => { Console.WriteLine(""hello""); };
362+
363+
TestMethod2((arg1, arg2) => { Console.WriteLine(""hello""); });
364+
}
365+
366+
public static void TestMethod2(TestDelegate testDelegate)
367+
{
368+
}
369+
}
370+
}
371+
";
372+
373+
DiagnosticResult[] expected =
374+
{
375+
this.CSharpDiagnostic().WithLocation(13, 33),
376+
this.CSharpDiagnostic().WithLocation(14, 26),
377+
this.CSharpDiagnostic().WithLocation(15, 26),
378+
this.CSharpDiagnostic().WithLocation(23, 33),
379+
this.CSharpDiagnostic().WithLocation(25, 25),
380+
};
381+
382+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
383+
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
384+
await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false);
385+
}
386+
297387
/// <inheritdoc/>
298388
protected override IEnumerable<DiagnosticAnalyzer> GetCSharpDiagnosticAnalyzers()
299389
{

0 commit comments

Comments
 (0)