Skip to content

Commit c05d224

Browse files
committed
Merge pull request #1849 from sharwell/fix-436
Update SA1302 code fix to support conflict resolution
2 parents 32057ff + 78bcb26 commit c05d224

5 files changed

Lines changed: 230 additions & 8 deletions

File tree

StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/RenameHelper.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,43 @@ public static async Task<Solution> RenameSymbolAsync(Document document, SyntaxNo
2828
// TODO: return annotatedSolution instead of newSolution if newSolution contains any new errors (for any project)
2929
return newSolution;
3030
}
31+
32+
public static bool IsValidNewMemberName(SemanticModel semanticModel, ISymbol symbol, string name)
33+
{
34+
var members = (symbol as INamedTypeSymbol)?.GetMembers(name);
35+
if (members.HasValue && !members.Value.IsDefaultOrEmpty)
36+
{
37+
return false;
38+
}
39+
40+
var containingSymbol = symbol.ContainingSymbol as INamespaceOrTypeSymbol;
41+
if (containingSymbol == null)
42+
{
43+
return true;
44+
}
45+
46+
if (containingSymbol.Kind == SymbolKind.Namespace)
47+
{
48+
// Make sure to use the compilation namespace so interfaces in referenced assemblies are considered
49+
containingSymbol = semanticModel.Compilation.GetCompilationNamespace((INamespaceSymbol)containingSymbol);
50+
}
51+
else if (containingSymbol.Kind == SymbolKind.NamedType)
52+
{
53+
// The name can't be the same as the name of the containing type
54+
if (containingSymbol.Name == name)
55+
{
56+
return false;
57+
}
58+
}
59+
60+
// The name can't be the same as the name of an other member of the same type
61+
members = containingSymbol.GetMembers(name);
62+
if (!members.Value.IsDefaultOrEmpty)
63+
{
64+
return false;
65+
}
66+
67+
return true;
68+
}
3169
}
3270
}

StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/NamingRules/SA1302CodeFixProvider.cs

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ namespace StyleCop.Analyzers.NamingRules
55
{
66
using System.Collections.Immutable;
77
using System.Composition;
8+
using System.Linq;
9+
using System.Threading;
810
using System.Threading.Tasks;
911
using Helpers;
1012
using Microsoft.CodeAnalysis;
@@ -27,22 +29,38 @@ internal class SA1302CodeFixProvider : CodeFixProvider
2729
ImmutableArray.Create(SA1302InterfaceNamesMustBeginWithI.DiagnosticId);
2830

2931
/// <inheritdoc/>
30-
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
32+
public override Task RegisterCodeFixesAsync(CodeFixContext context)
3133
{
32-
var document = context.Document;
33-
var root = await document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
34-
3534
foreach (var diagnostic in context.Diagnostics)
3635
{
37-
var token = root.FindToken(diagnostic.Location.SourceSpan.Start);
38-
var newName = "I" + token.ValueText;
3936
context.RegisterCodeFix(
4037
CodeAction.Create(
41-
string.Format(NamingResources.RenameToCodeFix, newName),
42-
cancellationToken => RenameHelper.RenameSymbolAsync(document, root, token, newName, cancellationToken),
38+
NamingResources.SA1302CodeFix,
39+
cancellationToken => CreateChangedSolutionAsync(context.Document, diagnostic, cancellationToken),
4340
nameof(SA1302CodeFixProvider)),
4441
diagnostic);
4542
}
43+
44+
return SpecializedTasks.CompletedTask;
45+
}
46+
47+
private static async Task<Solution> CreateChangedSolutionAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
48+
{
49+
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
50+
var token = root.FindToken(diagnostic.Location.SourceSpan.Start);
51+
var baseName = "I" + token.ValueText;
52+
var index = 0;
53+
var newName = baseName;
54+
55+
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
56+
var declaredSymbol = semanticModel.GetDeclaredSymbol(token.Parent, cancellationToken);
57+
while (!RenameHelper.IsValidNewMemberName(semanticModel, declaredSymbol, newName))
58+
{
59+
index++;
60+
newName = baseName + index;
61+
}
62+
63+
return await RenameHelper.RenameSymbolAsync(document, root, token, newName, cancellationToken).ConfigureAwait(false);
4664
}
4765
}
4866
}

StyleCop.Analyzers/StyleCop.Analyzers.Test/NamingRules/SA1302UnitTests.cs

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,160 @@ public interface FileOpenDialog111
185185
await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
186186
}
187187

188+
[Fact]
189+
public async Task TestInterfaceDeclarationDoesNotStartWithIWithConflictAsync()
190+
{
191+
string testCode = @"
192+
public interface Foo
193+
{
194+
}
195+
196+
public interface IFoo { }";
197+
string fixedCode = @"
198+
public interface IFoo1
199+
{
200+
}
201+
202+
public interface IFoo { }";
203+
204+
DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(2, 18);
205+
206+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
207+
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
208+
await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
209+
}
210+
211+
[Fact]
212+
public async Task TestInterfaceDeclarationDoesNotStartWithIWithMemberConflictAsync()
213+
{
214+
string testCode = @"
215+
public interface Foo
216+
{
217+
int IFoo { get; }
218+
}";
219+
string fixedCode = @"
220+
public interface IFoo1
221+
{
222+
int IFoo { get; }
223+
}";
224+
225+
DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(2, 18);
226+
227+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
228+
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
229+
await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
230+
}
231+
232+
[Fact]
233+
public async Task TestNestedInterfaceDeclarationDoesNotStartWithIWithConflictAsync()
234+
{
235+
string testCode = @"
236+
public class Outer
237+
{
238+
public interface Foo
239+
{
240+
}
241+
242+
public interface IFoo { }
243+
}";
244+
string fixedCode = @"
245+
public class Outer
246+
{
247+
public interface IFoo1
248+
{
249+
}
250+
251+
public interface IFoo { }
252+
}";
253+
254+
DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(4, 22);
255+
256+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
257+
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
258+
await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
259+
}
260+
261+
[Fact]
262+
public async Task TestNestedInterfaceDeclarationDoesNotStartWithIWithContainingTypeConflictAsync()
263+
{
264+
string testCode = @"
265+
public class IFoo
266+
{
267+
public interface Foo
268+
{
269+
}
270+
}";
271+
string fixedCode = @"
272+
public class IFoo
273+
{
274+
public interface IFoo1
275+
{
276+
}
277+
}";
278+
279+
DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(4, 22);
280+
281+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
282+
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
283+
await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
284+
}
285+
286+
[Fact]
287+
public async Task TestNestedInterfaceDeclarationDoesNotStartWithIWithNonInterfaceConflictAsync()
288+
{
289+
string testCode = @"
290+
public class Outer
291+
{
292+
public interface Foo
293+
{
294+
}
295+
296+
private int IFoo => 0;
297+
}";
298+
string fixedCode = @"
299+
public class Outer
300+
{
301+
public interface IFoo1
302+
{
303+
}
304+
305+
private int IFoo => 0;
306+
}";
307+
308+
DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(4, 22);
309+
310+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
311+
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
312+
await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
313+
}
314+
315+
[Fact]
316+
public async Task TestInterfaceDeclarationDoesNotStartWithIWithConflictInAnotherAssemblyAsync()
317+
{
318+
string testCode = @"
319+
namespace System
320+
{
321+
public interface Disposable
322+
{
323+
}
324+
}
325+
";
326+
string fixedCode = @"
327+
namespace System
328+
{
329+
public interface IDisposable1
330+
{
331+
}
332+
}
333+
";
334+
335+
DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(4, 22);
336+
337+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
338+
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
339+
await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
340+
}
341+
188342
protected override IEnumerable<DiagnosticAnalyzer> GetCSharpDiagnosticAnalyzers()
189343
{
190344
yield return new SA1302InterfaceNamesMustBeginWithI();

StyleCop.Analyzers/StyleCop.Analyzers/NamingRules/NamingResources.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

StyleCop.Analyzers/StyleCop.Analyzers/NamingRules/NamingResources.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@
120120
<data name="RenameToCodeFix" xml:space="preserve">
121121
<value>Rename To '{0}'</value>
122122
</data>
123+
<data name="SA1302CodeFix" xml:space="preserve">
124+
<value>Prefix interface name with 'I'</value>
125+
</data>
123126
<data name="SA1312Description" xml:space="preserve">
124127
<value>The name of a variable in C# does not begin with a lower-case letter.</value>
125128
</data>

0 commit comments

Comments
 (0)