Skip to content

Commit ebcbf1e

Browse files
authored
Merge pull request #2592 from vweijsters/pr1762
Implement SA1135 (UsingDirectivesMustBeQualified)
2 parents d8d53da + 9c2167d commit ebcbf1e

9 files changed

Lines changed: 575 additions & 0 deletions

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3+
4+
namespace StyleCop.Analyzers.ReadabilityRules
5+
{
6+
using System.Collections.Immutable;
7+
using System.Composition;
8+
using System.Linq;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using Microsoft.CodeAnalysis;
12+
using Microsoft.CodeAnalysis.CodeActions;
13+
using Microsoft.CodeAnalysis.CodeFixes;
14+
using Microsoft.CodeAnalysis.CSharp;
15+
using Microsoft.CodeAnalysis.CSharp.Syntax;
16+
using StyleCop.Analyzers.Helpers;
17+
18+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SA1135CodeFixProvider))]
19+
[Shared]
20+
internal class SA1135CodeFixProvider : CodeFixProvider
21+
{
22+
/// <inheritdoc/>
23+
public override ImmutableArray<string> FixableDiagnosticIds { get; } =
24+
ImmutableArray.Create(SA1135UsingDirectivesMustBeQualified.DiagnosticId);
25+
26+
/// <inheritdoc/>
27+
public override FixAllProvider GetFixAllProvider()
28+
{
29+
return FixAll.Instance;
30+
}
31+
32+
/// <inheritdoc/>
33+
public override Task RegisterCodeFixesAsync(CodeFixContext context)
34+
{
35+
foreach (var diagnostic in context.Diagnostics)
36+
{
37+
context.RegisterCodeFix(
38+
CodeAction.Create(
39+
ReadabilityResources.SA1135CodeFix,
40+
cancellationToken => GetTransformedDocumentAsync(context.Document, diagnostic, cancellationToken),
41+
nameof(SA1135CodeFixProvider)),
42+
diagnostic);
43+
}
44+
45+
return SpecializedTasks.CompletedTask;
46+
}
47+
48+
private static async Task<Document> GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
49+
{
50+
var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
51+
52+
var node = syntaxRoot.FindNode(diagnostic.Location.SourceSpan) as UsingDirectiveSyntax;
53+
if (node == null)
54+
{
55+
return document;
56+
}
57+
58+
SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
59+
var replacementNode = GetReplacementNode(semanticModel, node, cancellationToken);
60+
var newSyntaxRoot = syntaxRoot.ReplaceNode(node, replacementNode);
61+
return document.WithSyntaxRoot(newSyntaxRoot);
62+
}
63+
64+
private static SyntaxNode GetReplacementNode(SemanticModel semanticModel, UsingDirectiveSyntax node, CancellationToken cancellationToken)
65+
{
66+
SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(node.Name, cancellationToken);
67+
return node.WithName(SyntaxFactory.ParseName(symbolInfo.Symbol.ToString()));
68+
}
69+
70+
private class FixAll : DocumentBasedFixAllProvider
71+
{
72+
public static FixAllProvider Instance { get; } =
73+
new FixAll();
74+
75+
protected override string CodeActionTitle =>
76+
ReadabilityResources.SA1135CodeFix;
77+
78+
protected override async Task<SyntaxNode> FixAllInDocumentAsync(FixAllContext fixAllContext, Document document, ImmutableArray<Diagnostic> diagnostics)
79+
{
80+
if (diagnostics.IsEmpty)
81+
{
82+
return null;
83+
}
84+
85+
SyntaxNode syntaxRoot = await document.GetSyntaxRootAsync(fixAllContext.CancellationToken).ConfigureAwait(false);
86+
SemanticModel semanticModel = await document.GetSemanticModelAsync(fixAllContext.CancellationToken).ConfigureAwait(false);
87+
88+
var nodes = diagnostics.Select(diagnostic => syntaxRoot.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true).FirstAncestorOrSelf<UsingDirectiveSyntax>());
89+
90+
return syntaxRoot.ReplaceNodes(nodes, (originalNode, rewrittenNode) => GetReplacementNode(semanticModel, rewrittenNode, fixAllContext.CancellationToken));
91+
}
92+
}
93+
}
94+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3+
4+
namespace StyleCop.Analyzers.Test.CSharp7.ReadabilityRules
5+
{
6+
using StyleCop.Analyzers.Test.ReadabilityRules;
7+
8+
public class SA1135CSharp7UnitTests : SA1135UnitTests
9+
{
10+
}
11+
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3+
4+
namespace StyleCop.Analyzers.Test.ReadabilityRules
5+
{
6+
using System.Collections.Generic;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Analyzers.ReadabilityRules;
10+
using Microsoft.CodeAnalysis.CodeFixes;
11+
using Microsoft.CodeAnalysis.Diagnostics;
12+
using TestHelper;
13+
using Xunit;
14+
15+
public class SA1135UnitTests : CodeFixVerifier
16+
{
17+
[Fact]
18+
public async Task TestUnqualifiedUsingsAsync()
19+
{
20+
const string testCode = @"
21+
namespace System.Threading
22+
{
23+
using IO;
24+
using Tasks;
25+
}";
26+
const string fixedCode = @"
27+
namespace System.Threading
28+
{
29+
using System.IO;
30+
using System.Threading.Tasks;
31+
}";
32+
33+
DiagnosticResult[] expected =
34+
{
35+
this.CSharpDiagnostic(SA1135UsingDirectivesMustBeQualified.DescriptorNamespace).WithLocation(4, 5).WithArguments("System.IO"),
36+
this.CSharpDiagnostic(SA1135UsingDirectivesMustBeQualified.DescriptorNamespace).WithLocation(5, 5).WithArguments("System.Threading.Tasks"),
37+
};
38+
39+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
40+
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
41+
await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false);
42+
}
43+
44+
[Fact]
45+
public async Task TestUnqualifiedAliasedUsingsAsync()
46+
{
47+
const string testCode = @"
48+
namespace System.Threading
49+
{
50+
using NA = IO;
51+
using NB = Tasks;
52+
}";
53+
const string fixedCode = @"
54+
namespace System.Threading
55+
{
56+
using NA = System.IO;
57+
using NB = System.Threading.Tasks;
58+
}";
59+
60+
DiagnosticResult[] expected =
61+
{
62+
this.CSharpDiagnostic(SA1135UsingDirectivesMustBeQualified.DescriptorNamespace).WithLocation(4, 5).WithArguments("System.IO"),
63+
this.CSharpDiagnostic(SA1135UsingDirectivesMustBeQualified.DescriptorNamespace).WithLocation(5, 5).WithArguments("System.Threading.Tasks"),
64+
};
65+
66+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
67+
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
68+
await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false);
69+
}
70+
71+
[Fact]
72+
public async Task TestUnqualifiedAliasedUsingTypesAsync()
73+
{
74+
const string testCode = @"
75+
namespace System.Threading
76+
{
77+
using TP = IO.Path;
78+
using TT = Tasks.Task;
79+
}";
80+
const string fixedCode = @"
81+
namespace System.Threading
82+
{
83+
using TP = System.IO.Path;
84+
using TT = System.Threading.Tasks.Task;
85+
}";
86+
87+
DiagnosticResult[] expected =
88+
{
89+
this.CSharpDiagnostic(SA1135UsingDirectivesMustBeQualified.DescriptorType).WithLocation(4, 5).WithArguments("System.IO.Path"),
90+
this.CSharpDiagnostic(SA1135UsingDirectivesMustBeQualified.DescriptorType).WithLocation(5, 5).WithArguments("System.Threading.Tasks.Task"),
91+
};
92+
93+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
94+
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
95+
await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false);
96+
}
97+
98+
[Fact]
99+
public async Task TestGlobalUsingsAsync()
100+
{
101+
const string testCode = @"
102+
namespace System.Threading
103+
{
104+
using global::System.IO;
105+
using global::System.Threading.Tasks;
106+
}";
107+
108+
await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
109+
}
110+
111+
[Fact]
112+
public async Task TestStaticUsingsAsync()
113+
{
114+
const string testCode = @"
115+
namespace System.Threading
116+
{
117+
using static Console;
118+
}";
119+
120+
await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
121+
}
122+
123+
[Fact]
124+
public async Task TestFixAllAsync()
125+
{
126+
const string testCode = @"
127+
namespace System.Threading
128+
{
129+
using NA = IO;
130+
using NB = Tasks;
131+
}";
132+
const string fixedCode = @"
133+
namespace System.Threading
134+
{
135+
using NA = System.IO;
136+
using NB = System.Threading.Tasks;
137+
}";
138+
139+
DiagnosticResult[] expected =
140+
{
141+
this.CSharpDiagnostic(SA1135UsingDirectivesMustBeQualified.DescriptorNamespace).WithLocation(4, 5).WithArguments("System.IO"),
142+
this.CSharpDiagnostic(SA1135UsingDirectivesMustBeQualified.DescriptorNamespace).WithLocation(5, 5).WithArguments("System.Threading.Tasks"),
143+
};
144+
145+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
146+
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
147+
await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false);
148+
await this.VerifyCSharpFixAllFixAsync(testCode, fixedCode, numberOfIterations: 1, cancellationToken: CancellationToken.None).ConfigureAwait(false);
149+
}
150+
151+
[Fact]
152+
public async Task TestNoWarningsForAliasesAsync()
153+
{
154+
var testCode = @"
155+
using Tasks = System.Threading.Tasks;
156+
157+
namespace Namespace
158+
{
159+
using Task = Tasks.Task;
160+
}
161+
";
162+
163+
await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
164+
}
165+
166+
[Fact]
167+
public async Task TestAliasToGenericTypeAsync()
168+
{
169+
var testCode = @"
170+
namespace System.Collections
171+
{
172+
using Dictionary = Generic.Dictionary<int, string>;
173+
}
174+
";
175+
176+
var fixedCode = @"
177+
namespace System.Collections
178+
{
179+
using Dictionary = System.Collections.Generic.Dictionary<int, string>;
180+
}
181+
";
182+
183+
DiagnosticResult[] expected =
184+
{
185+
this.CSharpDiagnostic(SA1135UsingDirectivesMustBeQualified.DescriptorType).WithLocation(4, 5).WithArguments("System.Collections.Generic.Dictionary<int, string>"),
186+
};
187+
188+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
189+
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
190+
await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false);
191+
}
192+
193+
[Fact]
194+
public async Task TestAliasToTypesInSameNamespaceAsync()
195+
{
196+
var testCode = @"
197+
namespace Namespace
198+
{
199+
using Class2 = Class;
200+
201+
class Class { }
202+
}
203+
";
204+
205+
await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
206+
}
207+
208+
protected override CodeFixProvider GetCSharpCodeFixProvider()
209+
{
210+
return new SA1135CodeFixProvider();
211+
}
212+
213+
/// <inheritdoc/>
214+
protected override IEnumerable<DiagnosticAnalyzer> GetCSharpDiagnosticAnalyzers()
215+
{
216+
yield return new SA1135UsingDirectivesMustBeQualified();
217+
}
218+
}
219+
}

StyleCop.Analyzers/StyleCop.Analyzers/Helpers/UsingDirectiveSyntaxHelpers.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace StyleCop.Analyzers.Helpers
55
{
66
using System;
77
using System.Linq;
8+
using System.Threading;
89
using Microsoft.CodeAnalysis;
910
using Microsoft.CodeAnalysis.CSharp;
1011
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -84,6 +85,19 @@ internal static UsingGroup GetUsingGroupType(this UsingDirectiveSyntax usingDire
8485
return UsingGroup.Regular;
8586
}
8687

88+
/// <summary>
89+
/// Checks if the Name part of the given using directive starts with an alias.
90+
/// </summary>
91+
/// <param name="usingDirective">The <see cref="UsingDirectiveSyntax"/> that will be used.</param>
92+
/// <param name="semanticModel">The <see cref="SemanticModel"/> that will be used.</param>
93+
/// <param name="cancellationToken">The cancellation token that can be used to interrupt the operation.</param>
94+
/// <returns>True if the name part of the using directive starts with an alias.</returns>
95+
internal static bool StartsWithAlias(this UsingDirectiveSyntax usingDirective, SemanticModel semanticModel, CancellationToken cancellationToken)
96+
{
97+
var firstPart = usingDirective.Name.DescendantNodes().FirstOrDefault() ?? usingDirective.Name;
98+
return semanticModel.GetAliasInfo(firstPart, cancellationToken) != null;
99+
}
100+
87101
private static bool ExcludeGlobalKeyword(IdentifierNameSyntax token) => !token.Identifier.IsKind(SyntaxKind.GlobalKeyword);
88102

89103
private static SyntaxToken? GetFirstIdentifierInUsingDirective(UsingDirectiveSyntax usingDirective)

0 commit comments

Comments
 (0)