Skip to content

Commit af735b2

Browse files
committed
REFL020 More than one interface is matching the name. Fix #23.
1 parent 8ba637b commit af735b2

File tree

10 files changed

+269
-3
lines changed

10 files changed

+269
-3
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,9 @@ Analyzers checking System.Reflection
7676
<td><a href="https://github.com/DotNetAnalyzers/ReflectionAnalyzers/tree/master/documentation/REFL019.md">REFL019</a></td>
7777
<td>No member matches the types.</td>
7878
</tr>
79+
<tr>
80+
<td><a href="https://github.com/DotNetAnalyzers/ReflectionAnalyzers/tree/master/documentation/REFL020.md">REFL020</a></td>
81+
<td>More than one interface is matching the name.</td>
82+
</tr>
7983
<table>
8084
<!-- end generated table -->
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
namespace ReflectionAnalyzers.Tests.REFL020AmbiguousMatchInterfaceTests
2+
{
3+
using Gu.Roslyn.Asserts;
4+
using Microsoft.CodeAnalysis.Diagnostics;
5+
using NUnit.Framework;
6+
7+
public class Diagnostics
8+
{
9+
private static readonly DiagnosticAnalyzer Analyzer = new GetInterfaceAnalyzer();
10+
private static readonly ExpectedDiagnostic ExpectedDiagnostic = ExpectedDiagnostic.Create(REFL020AmbiguousMatchInterface.Descriptor);
11+
12+
[TestCase("GetInterface(↓\"System.Collections.Generic.IEnumerable`1\")")]
13+
[TestCase("GetInterface(↓\"IEnumerable`1\")")]
14+
[TestCase("GetInterface(typeof(IEnumerable<>).FullName)")]
15+
public void GetInterface(string call)
16+
{
17+
var code = @"
18+
namespace RoslynSandbox
19+
{
20+
using System;
21+
using System.Collections;
22+
using System.Collections.Generic;
23+
24+
public class Foo : IEnumerable<int>, IEnumerable<double>
25+
{
26+
private readonly List<int> ints = new List<int>();
27+
private readonly List<double> doubles = new List<double>();
28+
29+
public Foo()
30+
{
31+
var type = typeof(Foo).GetInterface(↓""System.Collections.Generic.IEnumerable`1"");
32+
}
33+
34+
IEnumerator<int> IEnumerable<int>.GetEnumerator() => this.ints.GetEnumerator();
35+
36+
IEnumerator<double> IEnumerable<double>.GetEnumerator() => this.doubles.GetEnumerator();
37+
38+
IEnumerator IEnumerable.GetEnumerator() => throw new NotSupportedException();
39+
}
40+
}".AssertReplace("GetInterface(↓\"System.Collections.Generic.IEnumerable`1\")", call);
41+
42+
AnalyzerAssert.Diagnostics(Analyzer, ExpectedDiagnostic, code);
43+
}
44+
}
45+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
namespace ReflectionAnalyzers.Tests.REFL020AmbiguousMatchInterfaceTests
2+
{
3+
using Gu.Roslyn.Asserts;
4+
using Microsoft.CodeAnalysis.Diagnostics;
5+
using NUnit.Framework;
6+
7+
public class ValidCode
8+
{
9+
private static readonly DiagnosticAnalyzer Analyzer = new GetInterfaceAnalyzer();
10+
private static readonly ExpectedDiagnostic ExpectedDiagnostic = ExpectedDiagnostic.Create(REFL020AmbiguousMatchInterface.Descriptor);
11+
12+
[TestCase("GetInterface(\"System.Collections.Generic.IEnumerable`1\")")]
13+
[TestCase("GetInterface(\"IEnumerable`1\")")]
14+
[TestCase("GetInterface(typeof(IEnumerable).FullName)")]
15+
[TestCase("GetInterface(typeof(IEnumerable).Name)")]
16+
[TestCase("GetInterface(typeof(IEnumerable<>).FullName)")]
17+
[TestCase("GetInterface(typeof(IEnumerable<>).Name)")]
18+
[TestCase("GetInterface(\"IEnumerable\")")]
19+
[TestCase("GetInterface(\"System.Collections.IEnumerable\")")]
20+
public void GetInterface(string call)
21+
{
22+
var code = @"
23+
namespace RoslynSandbox
24+
{
25+
using System.Collections;
26+
using System.Collections.Generic;
27+
28+
public class Foo : IEnumerable<int>
29+
{
30+
private readonly List<int> ints = new List<int>();
31+
32+
public Foo()
33+
{
34+
var type = typeof(Foo).GetInterface(""System.Collections.Generic.IEnumerable`1"");
35+
}
36+
37+
public IEnumerator<int> GetEnumerator() => this.ints.GetEnumerator();
38+
39+
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
40+
}
41+
}".AssertReplace("GetInterface(\"System.Collections.Generic.IEnumerable`1\")", call);
42+
43+
AnalyzerAssert.Valid(Analyzer, ExpectedDiagnostic, code);
44+
}
45+
}
46+
}

ReflectionAnalyzers.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".docs", ".docs", "{1C271AF2
4949
documentation\REFL017.md = documentation\REFL017.md
5050
documentation\REFL018.md = documentation\REFL018.md
5151
documentation\REFL019.md = documentation\REFL019.md
52+
documentation\REFL020.md = documentation\REFL020.md
5253
RELEASE_NOTES.md = RELEASE_NOTES.md
5354
EndProjectSection
5455
EndProject

ReflectionAnalyzers/Helpers/KnownSymbols/TypeType.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ internal class TypeType : QualifiedType
88
internal readonly QualifiedMethod GetConstructor;
99
internal readonly QualifiedMethod GetEvent;
1010
internal readonly QualifiedMethod GetField;
11+
internal readonly QualifiedMethod GetInterface;
1112
internal readonly QualifiedMethod GetMember;
1213
internal readonly QualifiedMethod GetMethod;
1314
internal readonly QualifiedMethod GetNestedType;
@@ -20,6 +21,7 @@ internal TypeType()
2021
this.GetConstructor = new QualifiedMethod(this, nameof(this.GetConstructor));
2122
this.GetEvent = new QualifiedMethod(this, nameof(this.GetEvent));
2223
this.GetField = new QualifiedMethod(this, nameof(this.GetField));
24+
this.GetInterface = new QualifiedMethod(this, nameof(this.GetInterface));
2325
this.GetMember = new QualifiedMethod(this, nameof(this.GetMember));
2426
this.GetNestedType = new QualifiedMethod(this, nameof(this.GetNestedType));
2527
this.GetMethod = new QualifiedMethod(this, nameof(this.GetMethod));
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
namespace ReflectionAnalyzers
2+
{
3+
using System.Collections.Immutable;
4+
using System.Linq;
5+
using Gu.Roslyn.AnalyzerExtensions;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.CSharp;
8+
using Microsoft.CodeAnalysis.CSharp.Syntax;
9+
using Microsoft.CodeAnalysis.Diagnostics;
10+
11+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
12+
internal class GetInterfaceAnalyzer : DiagnosticAnalyzer
13+
{
14+
/// <inheritdoc/>
15+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(
16+
REFL020AmbiguousMatchInterface.Descriptor);
17+
18+
/// <inheritdoc/>
19+
public override void Initialize(AnalysisContext context)
20+
{
21+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
22+
context.EnableConcurrentExecution();
23+
context.RegisterSyntaxNodeAction(Handle, SyntaxKind.InvocationExpression);
24+
}
25+
26+
private static void Handle(SyntaxNodeAnalysisContext context)
27+
{
28+
if (!context.IsExcludedFromAnalysis() &&
29+
context.Node is InvocationExpressionSyntax invocation &&
30+
invocation.TryGetTarget(KnownSymbol.Type.GetInterface, context.SemanticModel, context.CancellationToken, out var getInterface) &&
31+
getInterface.TryFindParameter(KnownSymbol.String, out var nameParameter) &&
32+
invocation.TryFindArgument(nameParameter, out var nameArg) &&
33+
TryGetName(nameArg, context, out var name) &&
34+
GetX.TryGetType(invocation, context, out var type, out _))
35+
{
36+
var count = type.AllInterfaces.Count(x => IsMatch(x));
37+
if (count > 1)
38+
{
39+
context.ReportDiagnostic(Diagnostic.Create(REFL020AmbiguousMatchInterface.Descriptor, nameArg.GetLocation()));
40+
}
41+
}
42+
43+
bool IsMatch(ITypeSymbol candidate)
44+
{
45+
if (candidate.MetadataName == name)
46+
{
47+
return true;
48+
}
49+
50+
return name.IsParts(candidate.ContainingNamespace.ToString(), ".", candidate.MetadataName);
51+
}
52+
}
53+
54+
private static bool TryGetName(ArgumentSyntax nameArg, SyntaxNodeAnalysisContext context, out string name)
55+
{
56+
switch (nameArg.Expression)
57+
{
58+
case MemberAccessExpressionSyntax memberAccess:
59+
if (memberAccess.Expression is TypeOfExpressionSyntax typeOf &&
60+
context.SemanticModel.TryGetType(typeOf.Type, context.CancellationToken, out var type))
61+
{
62+
if (memberAccess.Name.Identifier.ValueText == "Name")
63+
{
64+
name = type.MetadataName;
65+
return true;
66+
}
67+
68+
if (memberAccess.Name.Identifier.ValueText == "FullName")
69+
{
70+
name = $"{type.ContainingNamespace}.{type.MetadataName}";
71+
return true;
72+
}
73+
}
74+
75+
name = null;
76+
return false;
77+
default:
78+
return context.SemanticModel.TryGetConstantValue(nameArg.Expression, context.CancellationToken, out name);
79+
}
80+
}
81+
}
82+
}

ReflectionAnalyzers/NodeAnalzers/GetXAnalyzer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ internal class GetXAnalyzer : DiagnosticAnalyzer
1717
/// <inheritdoc/>
1818
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(
1919
REFL003MemberDoesNotExist.Descriptor,
20-
REFL004AmbiguousMatch.Descriptor,
20+
REFL004AmbiguousMatchMember.Descriptor,
2121
REFL005WrongBindingFlags.Descriptor,
2222
REFL006RedundantBindingFlags.Descriptor,
2323
REFL008MissingBindingFlags.Descriptor,
@@ -48,7 +48,7 @@ context.Node is InvocationExpressionSyntax invocation &&
4848
break;
4949

5050
case GetXResult.Ambiguous:
51-
context.ReportDiagnostic(Diagnostic.Create(REFL004AmbiguousMatch.Descriptor, argumentList.GetLocation()));
51+
context.ReportDiagnostic(Diagnostic.Create(REFL004AmbiguousMatchMember.Descriptor, argumentList.GetLocation()));
5252
break;
5353

5454
case GetXResult.WrongFlags when TryGetExpectedFlags(member, type, out var correctFlags):

ReflectionAnalyzers/REFL004AmbiguousMatch.cs renamed to ReflectionAnalyzers/REFL004AmbiguousMatchMember.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ namespace ReflectionAnalyzers
22
{
33
using Microsoft.CodeAnalysis;
44

5-
internal static class REFL004AmbiguousMatch
5+
internal static class REFL004AmbiguousMatchMember
66
{
77
public const string DiagnosticId = "REFL004";
88

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace ReflectionAnalyzers
2+
{
3+
using Microsoft.CodeAnalysis;
4+
5+
internal static class REFL020AmbiguousMatchInterface
6+
{
7+
public const string DiagnosticId = "REFL020";
8+
9+
internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor(
10+
id: DiagnosticId,
11+
title: "More than one interface is matching the name.",
12+
messageFormat: "More than one interface is matching the name.",
13+
category: AnalyzerCategory.SystemReflection,
14+
defaultSeverity: DiagnosticSeverity.Warning,
15+
isEnabledByDefault: true,
16+
description: "More than one interface is matching the name.",
17+
helpLinkUri: HelpLink.ForId(DiagnosticId));
18+
}
19+
}

documentation/REFL020.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# REFL020
2+
## More than one interface is matching the name.
3+
4+
<!-- start generated table -->
5+
<table>
6+
<tr>
7+
<td>CheckId</td>
8+
<td>REFL020</td>
9+
</tr>
10+
<tr>
11+
<td>Severity</td>
12+
<td>Warning</td>
13+
</tr>
14+
<tr>
15+
<td>Enabled</td>
16+
<td>true</td>
17+
</tr>
18+
<tr>
19+
<td>Category</td>
20+
<td>ReflectionAnalyzers.SystemReflection</td>
21+
</tr>
22+
<tr>
23+
<td>Code</td>
24+
<td><a href="https://github.com/DotNetAnalyzers/ReflectionAnalyzers/blob/master/ReflectionAnalyzers/NodeAnalzers/GetInterfaceAnalyzer.cs">GetInterfaceAnalyzer</a></td>
25+
</tr>
26+
</table>
27+
<!-- end generated table -->
28+
29+
## Description
30+
31+
More than one interface is matching the name.
32+
33+
## Motivation
34+
35+
ADD MOTIVATION HERE
36+
37+
## How to fix violations
38+
39+
ADD HOW TO FIX VIOLATIONS HERE
40+
41+
<!-- start generated config severity -->
42+
## Configure severity
43+
44+
### Via ruleset file.
45+
46+
Configure the severity per project, for more info see [MSDN](https://msdn.microsoft.com/en-us/library/dd264949.aspx).
47+
48+
### Via #pragma directive.
49+
```C#
50+
#pragma warning disable REFL020 // More than one interface is matching the name.
51+
Code violating the rule here
52+
#pragma warning restore REFL020 // More than one interface is matching the name.
53+
```
54+
55+
Or put this at the top of the file to disable all instances.
56+
```C#
57+
#pragma warning disable REFL020 // More than one interface is matching the name.
58+
```
59+
60+
### Via attribute `[SuppressMessage]`.
61+
62+
```C#
63+
[System.Diagnostics.CodeAnalysis.SuppressMessage("ReflectionAnalyzers.SystemReflection",
64+
"REFL020:More than one interface is matching the name.",
65+
Justification = "Reason...")]
66+
```
67+
<!-- end generated config severity -->

0 commit comments

Comments
 (0)