Skip to content

Commit b4f9499

Browse files
committed
REFL022 Use fully qualified name.
1 parent 10068ba commit b4f9499

File tree

9 files changed

+294
-6
lines changed

9 files changed

+294
-6
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ Analyzers checking System.Reflection
8080
<td><a href="https://github.com/DotNetAnalyzers/ReflectionAnalyzers/tree/master/documentation/REFL020.md">REFL020</a></td>
8181
<td>More than one interface is matching the name.</td>
8282
</tr>
83+
<tr>
84+
<td><a href="https://github.com/DotNetAnalyzers/ReflectionAnalyzers/tree/master/documentation/REFL022.md">REFL022</a></td>
85+
<td>Use fully qualified name.</td>
86+
</tr>
8387
<tr>
8488
<td><a href="https://github.com/DotNetAnalyzers/ReflectionAnalyzers/tree/master/documentation/REFL023.md">REFL023</a></td>
8589
<td>The type does not implement the interface.</td>

ReflectionAnalyzers.Tests/Documentation/Tests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ private DescriptorInfo(DiagnosticAnalyzer analyzer, DiagnosticDescriptor descrip
207207

208208
public static string GetCodeFileUri(DiagnosticAnalyzer analyzer)
209209
{
210-
string fileName = Directory.EnumerateFiles(SolutionDirectory.FullName, analyzer.GetType().Name + ".cs", SearchOption.AllDirectories)
210+
var fileName = Directory.EnumerateFiles(SolutionDirectory.FullName, analyzer.GetType().Name + ".cs", SearchOption.AllDirectories)
211211
.FirstOrDefault();
212212
return fileName != null
213213
? "https://github.com/DotNetAnalyzers/ReflectionAnalyzers/blob/master" +
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
namespace ReflectionAnalyzers.Tests.REFL022UseFullyQualifiedNameTests
2+
{
3+
using Gu.Roslyn.Asserts;
4+
using Microsoft.CodeAnalysis.CodeFixes;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
using NUnit.Framework;
7+
using ReflectionAnalyzers.Codefixes;
8+
9+
public class CodeFix
10+
{
11+
private static readonly DiagnosticAnalyzer Analyzer = new GetInterfaceAnalyzer();
12+
private static readonly CodeFixProvider Fix = new UseFullyQualifiedFix();
13+
private static readonly ExpectedDiagnostic ExpectedDiagnostic = ExpectedDiagnostic.Create(REFL022UseFullyQualifiedName.Descriptor);
14+
15+
[TestCase("GetInterface(↓\"IEnumerable`1\")", "GetInterface(\"System.Collections.Generic.IEnumerable`1\")")]
16+
[TestCase("GetInterface(typeof(IEnumerable<>).↓Name)", "GetInterface(typeof(IEnumerable<>).FullName)")]
17+
[TestCase("GetInterface(↓\"IEnumerable\")", "GetInterface(\"System.Collections.IEnumerable\")")]
18+
[TestCase("GetInterface(typeof(IEnumerable).↓Name)", "GetInterface(typeof(IEnumerable).FullName)")]
19+
public void GetInterface(string call, string expected)
20+
{
21+
var code = @"
22+
namespace RoslynSandbox
23+
{
24+
using System;
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(↓typeof(IEnumerable<>).Name);
35+
}
36+
37+
public IEnumerator<int> GetEnumerator() => this.ints.GetEnumerator();
38+
39+
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
40+
}
41+
}".AssertReplace("GetInterface(↓typeof(IEnumerable<>).Name)", call);
42+
43+
var fixedCode = @"
44+
namespace RoslynSandbox
45+
{
46+
using System;
47+
using System.Collections;
48+
using System.Collections.Generic;
49+
50+
public class Foo : IEnumerable<int>
51+
{
52+
private readonly List<int> ints = new List<int>();
53+
54+
public Foo()
55+
{
56+
var type = typeof(Foo).GetInterface(typeof(IEnumerable<>).FullName);
57+
}
58+
59+
public IEnumerator<int> GetEnumerator() => this.ints.GetEnumerator();
60+
61+
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
62+
}
63+
}".AssertReplace("GetInterface(typeof(IEnumerable<>).FullName)", expected);
64+
65+
AnalyzerAssert.CodeFix(Analyzer, Fix, ExpectedDiagnostic, code, fixedCode);
66+
}
67+
}
68+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
namespace ReflectionAnalyzers.Tests.REFL022UseFullyQualifiedNameTests
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(REFL022UseFullyQualifiedName.Descriptor);
11+
12+
[TestCase("GetInterface(\"System.Collections.Generic.IEnumerable`1\")")]
13+
[TestCase("GetInterface(typeof(IEnumerable).FullName)")]
14+
[TestCase("GetInterface(typeof(IEnumerable<>).FullName)")]
15+
[TestCase("GetInterface(\"System.Collections.IEnumerable\")")]
16+
public void GetInterface(string call)
17+
{
18+
var code = @"
19+
namespace RoslynSandbox
20+
{
21+
using System.Collections;
22+
using System.Collections.Generic;
23+
24+
public class Foo : IEnumerable<int>
25+
{
26+
private readonly List<int> ints = new List<int>();
27+
28+
public Foo()
29+
{
30+
var type = typeof(Foo).GetInterface(""System.Collections.Generic.IEnumerable`1"");
31+
}
32+
33+
public IEnumerator<int> GetEnumerator() => this.ints.GetEnumerator();
34+
35+
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
36+
}
37+
}".AssertReplace("GetInterface(\"System.Collections.Generic.IEnumerable`1\")", call);
38+
39+
AnalyzerAssert.Valid(Analyzer, ExpectedDiagnostic, code);
40+
}
41+
}
42+
}

ReflectionAnalyzers.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".docs", ".docs", "{1C271AF2
5050
documentation\REFL018.md = documentation\REFL018.md
5151
documentation\REFL019.md = documentation\REFL019.md
5252
documentation\REFL020.md = documentation\REFL020.md
53+
documentation\REFL022.md = documentation\REFL022.md
5354
documentation\REFL023.md = documentation\REFL023.md
5455
RELEASE_NOTES.md = RELEASE_NOTES.md
5556
EndProjectSection
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
namespace ReflectionAnalyzers.Codefixes
2+
{
3+
using System.Collections.Immutable;
4+
using System.Composition;
5+
using System.Threading.Tasks;
6+
using Gu.Roslyn.CodeFixExtensions;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.CodeFixes;
9+
using Microsoft.CodeAnalysis.CSharp;
10+
using Microsoft.CodeAnalysis.CSharp.Syntax;
11+
12+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UseFullyQualifiedFix))]
13+
[Shared]
14+
internal class UseFullyQualifiedFix : DocumentEditorCodeFixProvider
15+
{
16+
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(
17+
REFL022UseFullyQualifiedName.DiagnosticId);
18+
19+
protected override async Task RegisterCodeFixesAsync(DocumentEditorCodeFixContext context)
20+
{
21+
var syntaxRoot = await context.Document.GetSyntaxRootAsync(context.CancellationToken)
22+
.ConfigureAwait(false);
23+
foreach (var diagnostic in context.Diagnostics)
24+
{
25+
if (syntaxRoot.FindNode(diagnostic.Location.SourceSpan, findInsideTrivia: false, getInnermostNodeForTie: true) is LiteralExpressionSyntax literal &&
26+
diagnostic.Properties.TryGetValue(nameof(SyntaxKind.StringLiteralExpression), out var text))
27+
{
28+
context.RegisterCodeFix(
29+
$"Use fully qualified name: {text}.",
30+
(editor, _) => editor.ReplaceNode(
31+
literal,
32+
x => x.WithToken(SyntaxFactory.Literal(text))),
33+
nameof(UseFullyQualifiedFix),
34+
diagnostic);
35+
}
36+
37+
if (syntaxRoot.FindNode(diagnostic.Location.SourceSpan, findInsideTrivia: false, getInnermostNodeForTie: true) is IdentifierNameSyntax identifierName &&
38+
diagnostic.Properties.TryGetValue(nameof(SimpleNameSyntax), out var name))
39+
{
40+
context.RegisterCodeFix(
41+
$"Use fully qualified name: {name}.",
42+
(editor, _) => editor.ReplaceNode(
43+
identifierName,
44+
x => x.WithIdentifier(SyntaxFactory.Identifier(name))),
45+
nameof(UseFullyQualifiedFix),
46+
diagnostic);
47+
}
48+
}
49+
}
50+
}
51+
}

ReflectionAnalyzers/NodeAnalzers/GetInterfaceAnalyzer.cs

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ internal class GetInterfaceAnalyzer : DiagnosticAnalyzer
1414
/// <inheritdoc/>
1515
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(
1616
REFL020AmbiguousMatchInterface.Descriptor,
17+
REFL022UseFullyQualifiedName.Descriptor,
1718
REFL023TypeDoesNotImplementInterface.Descriptor);
1819

1920
/// <inheritdoc/>
@@ -31,20 +32,71 @@ context.Node is InvocationExpressionSyntax invocation &&
3132
invocation.TryGetTarget(KnownSymbol.Type.GetInterface, context.SemanticModel, context.CancellationToken, out var getInterface) &&
3233
getInterface.TryFindParameter(KnownSymbol.String, out var nameParameter) &&
3334
invocation.TryFindArgument(nameParameter, out var nameArg) &&
34-
TryGetName(nameArg, context, out var name) &&
35+
TryGetName(nameArg, context, out var maybeNameSyntax, out var name) &&
3536
GetX.TryGetType(invocation, context, out var type, out _))
3637
{
37-
var count = type.AllInterfaces.Count(x => IsMatch(x));
38+
var count = CountInterfaces(type, name, out var match);
39+
3840
if (count > 1)
3941
{
4042
context.ReportDiagnostic(Diagnostic.Create(REFL020AmbiguousMatchInterface.Descriptor, nameArg.GetLocation()));
4143
}
4244

45+
if (count == 1 && match.MetadataName == name)
46+
{
47+
switch (nameArg.Expression)
48+
{
49+
case LiteralExpressionSyntax literal when literal.IsKind(SyntaxKind.StringLiteralExpression):
50+
context.ReportDiagnostic(
51+
Diagnostic.Create(
52+
REFL022UseFullyQualifiedName.Descriptor,
53+
literal.GetLocation(),
54+
ImmutableDictionary<string, string>.Empty.Add(
55+
nameof(SyntaxKind.StringLiteralExpression),
56+
$"{match.ContainingNamespace}.{match.MetadataName}")));
57+
break;
58+
default:
59+
if (maybeNameSyntax.HasValue &&
60+
maybeNameSyntax.Value.Identifier.ValueText == "Name")
61+
{
62+
context.ReportDiagnostic(
63+
Diagnostic.Create(
64+
REFL022UseFullyQualifiedName.Descriptor,
65+
maybeNameSyntax.Value.Identifier.GetLocation(),
66+
ImmutableDictionary<string, string>.Empty.Add(
67+
nameof(SimpleNameSyntax),
68+
"FullName")));
69+
}
70+
else
71+
{
72+
context.ReportDiagnostic(Diagnostic.Create(REFL022UseFullyQualifiedName.Descriptor, nameArg.GetLocation()));
73+
}
74+
75+
break;
76+
}
77+
}
78+
4379
if (count == 0)
4480
{
4581
context.ReportDiagnostic(Diagnostic.Create(REFL023TypeDoesNotImplementInterface.Descriptor, nameArg.GetLocation()));
4682
}
4783
}
84+
}
85+
86+
private static int CountInterfaces(ITypeSymbol type, string name, out ITypeSymbol match)
87+
{
88+
var count = 0;
89+
match = null;
90+
foreach (var candidate in type.AllInterfaces)
91+
{
92+
if (IsMatch(candidate))
93+
{
94+
count++;
95+
match = candidate;
96+
}
97+
}
98+
99+
return count;
48100

49101
bool IsMatch(ITypeSymbol candidate)
50102
{
@@ -57,8 +109,9 @@ bool IsMatch(ITypeSymbol candidate)
57109
}
58110
}
59111

60-
private static bool TryGetName(ArgumentSyntax nameArg, SyntaxNodeAnalysisContext context, out string name)
112+
private static bool TryGetName(ArgumentSyntax nameArg, SyntaxNodeAnalysisContext context, out Optional<SimpleNameSyntax> nameSyntax, out string name)
61113
{
114+
nameSyntax = default(Optional<SimpleNameSyntax>);
62115
switch (nameArg.Expression)
63116
{
64117
case MemberAccessExpressionSyntax memberAccess:
@@ -67,12 +120,14 @@ private static bool TryGetName(ArgumentSyntax nameArg, SyntaxNodeAnalysisContext
67120
{
68121
if (memberAccess.Name.Identifier.ValueText == "Name")
69122
{
123+
nameSyntax = memberAccess.Name;
70124
name = type.MetadataName;
71125
return true;
72126
}
73127

74128
if (memberAccess.Name.Identifier.ValueText == "FullName")
75129
{
130+
nameSyntax = memberAccess.Name;
76131
name = $"{type.ContainingNamespace}.{type.MetadataName}";
77132
return true;
78133
}

ReflectionAnalyzers/REFL022UseFullyQualifiedName.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ internal static class REFL022UseFullyQualifiedName
99
internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor(
1010
id: DiagnosticId,
1111
title: "Use fully qualified name.",
12-
messageFormat: "Use fully qualified name {0}.",
12+
messageFormat: "Use fully qualified name.",
1313
category: AnalyzerCategory.SystemReflection,
1414
defaultSeverity: DiagnosticSeverity.Warning,
1515
isEnabledByDefault: true,
1616
description: "Use fully qualified name.",
1717
helpLinkUri: HelpLink.ForId(DiagnosticId));
1818
}
19-
}
19+
}

documentation/REFL022.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# REFL022
2+
## Use fully qualified name.
3+
4+
<!-- start generated table -->
5+
<table>
6+
<tr>
7+
<td>CheckId</td>
8+
<td>REFL022</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+
Use fully qualified 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 REFL022 // Use fully qualified name.
51+
Code violating the rule here
52+
#pragma warning restore REFL022 // Use fully qualified name.
53+
```
54+
55+
Or put this at the top of the file to disable all instances.
56+
```C#
57+
#pragma warning disable REFL022 // Use fully qualified name.
58+
```
59+
60+
### Via attribute `[SuppressMessage]`.
61+
62+
```C#
63+
[System.Diagnostics.CodeAnalysis.SuppressMessage("ReflectionAnalyzers.SystemReflection",
64+
"REFL022:Use fully qualified name.",
65+
Justification = "Reason...")]
66+
```
67+
<!-- end generated config severity -->

0 commit comments

Comments
 (0)