Skip to content

Commit c549534

Browse files
committed
Implemented SA1649 (incl. tests + codefix)
1 parent 057193d commit c549534

14 files changed

Lines changed: 580 additions & 127 deletions
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
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.DocumentationRules
5+
{
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Collections.Immutable;
9+
using System.IO;
10+
using System.Linq;
11+
using System.Threading;
12+
using System.Threading.Tasks;
13+
using Microsoft.CodeAnalysis;
14+
using Microsoft.CodeAnalysis.CodeActions;
15+
using Microsoft.CodeAnalysis.CodeFixes;
16+
using Microsoft.CodeAnalysis.Diagnostics;
17+
using StyleCop.Analyzers.DocumentationRules;
18+
using TestHelper;
19+
using Xunit;
20+
21+
/// <summary>
22+
/// Unit tests for the SA1649 diagnostic.
23+
/// </summary>
24+
public class SA1649UnitTests : CodeFixVerifier
25+
{
26+
private const string MetadataSettings = @"
27+
{
28+
""settings"": {
29+
""documentationRules"": {
30+
""fileNamingConvention"": ""metadata""
31+
}
32+
}
33+
}
34+
";
35+
36+
private const string StyleCopSettings = @"
37+
{
38+
""settings"": {
39+
""documentationRules"": {
40+
""fileNamingConvention"": ""stylecop""
41+
}
42+
}
43+
}
44+
";
45+
46+
private bool useMetadataSettings;
47+
48+
public static IEnumerable<object[]> TypeKeywords
49+
{
50+
get
51+
{
52+
yield return new object[] { "class" };
53+
yield return new object[] { "struct" };
54+
yield return new object[] { "interface" };
55+
}
56+
}
57+
58+
/// <summary>
59+
/// Verifies that a wrong file name is correctly reported
60+
/// </summary>
61+
/// <param name="typeKeyword">The type keyword to use during the test.</param>
62+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
63+
[Theory]
64+
[MemberData(nameof(TypeKeywords))]
65+
public async Task VerifyWrongFileNameAsync(string typeKeyword)
66+
{
67+
var testCode = $@"namespace TestNameSpace
68+
{{
69+
public {typeKeyword} TestType
70+
{{
71+
}}
72+
}}
73+
";
74+
75+
var fixedCode = $@"namespace TestNamespace
76+
{{
77+
public {typeKeyword} TestType
78+
{{
79+
}}
80+
}}
81+
";
82+
83+
var expectedDiagnostic = this.CSharpDiagnostic().WithLocation("WrongFileName.cs", 3, 13 + typeKeyword.Length);
84+
await this.VerifyCSharpDiagnosticAsync(testCode, expectedDiagnostic, CancellationToken.None, "WrongFileName.cs").ConfigureAwait(false);
85+
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None, "TestType.cs").ConfigureAwait(false);
86+
await this.VerifyRenameAsync(testCode, "TestType.cs", CancellationToken.None).ConfigureAwait(false);
87+
}
88+
89+
/// <summary>
90+
/// Verifies that the file name is not case sensitive.
91+
/// </summary>
92+
/// <param name="typeKeyword">The type keyword to use during the test.</param>
93+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
94+
[Theory]
95+
[MemberData(nameof(TypeKeywords))]
96+
public async Task VerifyCaseInsensitivityAsync(string typeKeyword)
97+
{
98+
var testCode = $@"namespace TestNameSpace
99+
{{
100+
public {typeKeyword} TestType
101+
{{
102+
}}
103+
}}
104+
";
105+
106+
await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None, "testtype.cs").ConfigureAwait(false);
107+
}
108+
109+
/// <summary>
110+
/// Verifies that the file name is based on the first type.
111+
/// </summary>
112+
/// <param name="typeKeyword">The type keyword to use during the test.</param>
113+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
114+
[Theory]
115+
[MemberData(nameof(TypeKeywords))]
116+
public async Task VerifyFirstTypeIsUsedAsync(string typeKeyword)
117+
{
118+
var testCode = $@"namespace TestNameSpace
119+
{{
120+
public {typeKeyword} TestType
121+
{{
122+
}}
123+
124+
public {typeKeyword} TestType2
125+
{{
126+
}}
127+
}}
128+
";
129+
130+
await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None, "TestType.cs").ConfigureAwait(false);
131+
}
132+
133+
/// <summary>
134+
/// Verifies that partial types are ignored.
135+
/// </summary>
136+
/// <param name="typeKeyword">The type keyword to use during the test.</param>
137+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
138+
[Theory]
139+
[MemberData(nameof(TypeKeywords))]
140+
public async Task VerifyThatPartialTypesAreIgnoredAsync(string typeKeyword)
141+
{
142+
var testCode = $@"namespace TestNameSpace
143+
{{
144+
public partial {typeKeyword} TestType
145+
{{
146+
}}
147+
}}
148+
";
149+
150+
await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None, "WrongFileName.cs").ConfigureAwait(false);
151+
}
152+
153+
/// <summary>
154+
/// Verifies that the StyleCop file name convention for a generic type is handled correctly.
155+
/// </summary>
156+
/// <param name="typeKeyword">The type keyword to use during the test.</param>
157+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
158+
[Theory]
159+
[MemberData(nameof(TypeKeywords))]
160+
public async Task VerifyStyleCopNamingConventionForGenericTypeAsync(string typeKeyword)
161+
{
162+
this.useMetadataSettings = false;
163+
164+
var testCode = $@"namespace TestNameSpace
165+
{{
166+
public {typeKeyword} TestType<T1, T2, T3>
167+
{{
168+
}}
169+
}}
170+
";
171+
172+
var expectedDiagnostic = this.CSharpDiagnostic().WithLocation("TestType`3.cs", 3, 13 + typeKeyword.Length);
173+
await this.VerifyCSharpDiagnosticAsync(testCode, expectedDiagnostic, CancellationToken.None, "TestType`3.cs").ConfigureAwait(false);
174+
await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None, "TestType{T1,T2,T3}.cs").ConfigureAwait(false);
175+
await this.VerifyRenameAsync(testCode, "TestType{T1,T2,T3}.cs", CancellationToken.None).ConfigureAwait(false);
176+
}
177+
178+
/// <summary>
179+
/// Verifies that the metadata file name convention for a generic type is handled correctly.
180+
/// </summary>
181+
/// <param name="typeKeyword">The type keyword to use during the test.</param>
182+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
183+
[Theory]
184+
[MemberData(nameof(TypeKeywords))]
185+
public async Task VerifyMetadataNamingConventionForGenericTypeAsync(string typeKeyword)
186+
{
187+
this.useMetadataSettings = true;
188+
189+
var testCode = $@"namespace TestNameSpace
190+
{{
191+
public {typeKeyword} TestType<T1, T2, T3>
192+
{{
193+
}}
194+
}}
195+
";
196+
197+
var expectedDiagnostic = this.CSharpDiagnostic().WithLocation("TestType{T1,T2,T3}.cs", 3, 13 + typeKeyword.Length);
198+
await this.VerifyCSharpDiagnosticAsync(testCode, expectedDiagnostic, CancellationToken.None, "TestType{T1,T2,T3}.cs").ConfigureAwait(false);
199+
await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None, "TestType`3.cs").ConfigureAwait(false);
200+
await this.VerifyRenameAsync(testCode, "TestType`3.cs", CancellationToken.None).ConfigureAwait(false);
201+
}
202+
203+
/// <summary>
204+
/// Verifies that no diagnostic is generated if there is no first type.
205+
/// </summary>
206+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
207+
[Fact]
208+
public async Task VerifyWithoutFirstTypeAsync()
209+
{
210+
var testCode = @"namespace TestNameSpace
211+
{
212+
}
213+
";
214+
215+
await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
216+
}
217+
218+
/// <inheritdoc/>
219+
protected override IEnumerable<DiagnosticAnalyzer> GetCSharpDiagnosticAnalyzers()
220+
{
221+
yield return new SA1649FileNameMustMatchTypeName();
222+
}
223+
224+
/// <inheritdoc/>
225+
protected override CodeFixProvider GetCSharpCodeFixProvider()
226+
{
227+
return new SA1649CodeFixProvider();
228+
}
229+
230+
/// <inheritdoc/>
231+
protected override string GetSettings()
232+
{
233+
return this.useMetadataSettings ? MetadataSettings : StyleCopSettings;
234+
}
235+
236+
private async Task VerifyRenameAsync(string source, string expectedFileName, CancellationToken cancellationToken)
237+
{
238+
var analyzers = this.GetCSharpDiagnosticAnalyzers().ToImmutableArray();
239+
var document = this.CreateDocument(source, LanguageNames.CSharp);
240+
var analyzerDiagnostics = await GetSortedDiagnosticsFromDocumentsAsync(analyzers, new[] { document }, cancellationToken).ConfigureAwait(false);
241+
242+
Assert.Equal(1, analyzerDiagnostics.Length);
243+
244+
var actions = new List<CodeAction>();
245+
var context = new CodeFixContext(document, analyzerDiagnostics[0], (a, d) => actions.Add(a), cancellationToken);
246+
await this.GetCSharpCodeFixProvider().RegisterCodeFixesAsync(context).ConfigureAwait(false);
247+
248+
Assert.Equal(1, actions.Count);
249+
250+
var operations = await actions[0].GetOperationsAsync(cancellationToken).ConfigureAwait(false);
251+
252+
var changedSolution = operations.OfType<ApplyChangesOperation>().Single().ChangedSolution;
253+
254+
var solutionChanges = changedSolution.GetChanges(document.Project.Solution);
255+
var projectChanges = solutionChanges.GetProjectChanges().ToArray();
256+
257+
Assert.Equal(1, projectChanges.Length);
258+
259+
var removedDocuments = projectChanges[0].GetRemovedDocuments().ToArray();
260+
Assert.Equal(1, removedDocuments.Length);
261+
Assert.Equal(document.Id, removedDocuments[0]);
262+
263+
var addedDocuments = projectChanges[0].GetAddedDocuments().ToArray();
264+
Assert.Equal(1, addedDocuments.Length);
265+
266+
var newDocument = changedSolution.GetDocument(addedDocuments[0]);
267+
Assert.Equal(expectedFileName, newDocument.Name);
268+
}
269+
}
270+
}

StyleCop.Analyzers/StyleCop.Analyzers.Test/StyleCop.Analyzers.Test.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@
160160
<Compile Include="DocumentationRules\SA1646UnitTests.cs" />
161161
<Compile Include="DocumentationRules\SA1647UnitTests.cs" />
162162
<Compile Include="DocumentationRules\SA1648UnitTests.cs" />
163+
<Compile Include="DocumentationRules\SA1649UnitTests.cs" />
163164
<Compile Include="DocumentationRules\SA1650UnitTests.cs" />
164165
<Compile Include="DocumentationRules\SA1651UnitTests.cs" />
165166
<Compile Include="DocumentationRules\SA1652UnitTests.cs" />

StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/DocumentationResources.Designer.cs

Lines changed: 36 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/DocumentationRules/DocumentationResources.resx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,18 @@
222222
<data name="SA1642SA1643CodeFix" xml:space="preserve">
223223
<value>Add standard text</value>
224224
</data>
225+
<data name="SA1649CodeFix" xml:space="preserve">
226+
<value>Rename file to match first type name</value>
227+
</data>
228+
<data name="SA1649Description" xml:space="preserve">
229+
<value>The file name of a C# code file does not match the first type declared in the file.</value>
230+
</data>
231+
<data name="SA1649MessageFormat" xml:space="preserve">
232+
<value>File name must match first type name.</value>
233+
</data>
234+
<data name="SA1649Title" xml:space="preserve">
235+
<value>File name must match first type name</value>
236+
</data>
225237
<data name="SA1651CodeFix" xml:space="preserve">
226238
<value>Finalize placeholder text</value>
227239
</data>

0 commit comments

Comments
 (0)