Skip to content

Commit 8076780

Browse files
committed
Update SA1027 to support the UseTabs setting
Fixes #2035 Fixes #2037
1 parent 6f52625 commit 8076780

5 files changed

Lines changed: 349 additions & 49 deletions

File tree

StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/SpacingRules/SA1027CodeFixProvider.cs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,34 +62,61 @@ private static TextChange FixDiagnostic(IndentationSettings indentationSettings,
6262
TextSpan span = diagnostic.Location.SourceSpan;
6363

6464
TextLine startLine = sourceText.Lines.GetLineFromPosition(span.Start);
65+
bool useTabs = indentationSettings.UseTabs && span.Start == startLine.Start;
6566
string text = sourceText.ToString(TextSpan.FromBounds(startLine.Start, span.End));
6667
StringBuilder replacement = StringBuilderPool.Allocate();
68+
int spaceCount = 0;
69+
bool encounteredNonWhitespace = false;
6770
int column = 0;
6871
for (int i = 0; i < text.Length; i++)
6972
{
7073
char c = text[i];
7174
if (c == '\t')
7275
{
7376
var offsetWithinTabColumn = column % indentationSettings.TabSize;
74-
var spaceCount = indentationSettings.TabSize - offsetWithinTabColumn;
77+
var tabWidth = indentationSettings.TabSize - offsetWithinTabColumn;
7578

76-
if (i >= span.Start - startLine.Start)
79+
if (useTabs && !encounteredNonWhitespace)
80+
{
81+
// We already know indentation started at the beginning of the line
82+
replacement.Length = replacement.Length - spaceCount;
83+
replacement.Append('\t');
84+
spaceCount = 0;
85+
}
86+
else if (i >= span.Start - startLine.Start)
7787
{
78-
replacement.Append(' ', spaceCount);
88+
replacement.Append(' ', tabWidth);
7989
}
8090

81-
column += spaceCount;
91+
column += tabWidth;
8292
}
8393
else
8494
{
8595
if (i >= span.Start - startLine.Start)
8696
{
8797
replacement.Append(c);
98+
if (c == ' ')
99+
{
100+
spaceCount++;
101+
if (useTabs && !encounteredNonWhitespace && spaceCount == indentationSettings.TabSize)
102+
{
103+
replacement.Length = replacement.Length - spaceCount;
104+
replacement.Append('\t');
105+
spaceCount = 0;
106+
}
107+
}
108+
else
109+
{
110+
spaceCount = 0;
111+
encounteredNonWhitespace = true;
112+
}
88113
}
89114

90115
if (c == '\n')
91116
{
92117
column = 0;
118+
spaceCount = 0;
119+
encounteredNonWhitespace = false;
93120
}
94121
else
95122
{

StyleCop.Analyzers/StyleCop.Analyzers.Test/SpacingRules/SA1027UnitTests.cs

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ namespace StyleCop.Analyzers.Test.SpacingRules
1717
/// </summary>
1818
public class SA1027UnitTests : CodeFixVerifier
1919
{
20-
private string settings;
21-
2220
/// <summary>
2321
/// Verifies that tabs used inside string and char literals are not producing diagnostics.
2422
/// </summary>
@@ -37,7 +35,7 @@ public async Task TestValidTabsAsync()
3735
}
3836

3937
/// <summary>
40-
/// Verifies that tabs used inside disabled code is not producing diagnostics.
38+
/// Verifies that tabs used inside disabled code are not producing diagnostics.
4139
/// </summary>
4240
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
4341
[Fact]
@@ -232,38 +230,6 @@ Comment 2
232230
await this.VerifyCSharpFixAsync(testCode, fixedTestCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
233231
}
234232

235-
[Fact]
236-
public async Task TestUseTabsSettingAsync()
237-
{
238-
this.settings = @"
239-
{
240-
""settings"": {
241-
""indentation"": {
242-
""useTabs"": true
243-
}
244-
}
245-
}
246-
";
247-
248-
var testCode =
249-
"using\tSystem.Diagnostics;\r\n" +
250-
"\r\n" +
251-
"public\tclass\tFoo\r\n" +
252-
"{\r\n" +
253-
"\tpublic void Bar()\r\n" +
254-
"\t{\r\n" +
255-
"\t \t// Comment\r\n" +
256-
"\t \tDebug.Indent();\r\n" +
257-
" \t}\r\n" +
258-
"}\r\n";
259-
260-
await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
261-
}
262-
263-
/// <inheritdoc/>
264-
protected override string GetSettings() =>
265-
this.settings;
266-
267233
/// <inheritdoc/>
268234
protected override IEnumerable<DiagnosticAnalyzer> GetCSharpDiagnosticAnalyzers()
269235
{
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
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.SpacingRules
5+
{
6+
using System.Collections.Generic;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Microsoft.CodeAnalysis.CodeFixes;
10+
using Microsoft.CodeAnalysis.Diagnostics;
11+
using StyleCop.Analyzers.Settings.ObjectModel;
12+
using StyleCop.Analyzers.SpacingRules;
13+
using TestHelper;
14+
using Xunit;
15+
16+
/// <summary>
17+
/// Unit tests for <see cref="SA1027TabsMustNotBeUsed"/> when <see cref="IndentationSettings.UseTabs"/> is
18+
/// <see langword="true"/>.
19+
/// </summary>
20+
public class SA1027UseTabsUnitTests : CodeFixVerifier
21+
{
22+
/// <summary>
23+
/// Verifies that tabs used inside string and char literals are not producing diagnostics.
24+
/// </summary>
25+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
26+
[Fact]
27+
public async Task TestValidTabsAsync()
28+
{
29+
var testCode =
30+
"public class Foo\r\n" +
31+
"{\r\n" +
32+
"\tpublic const string ValidTestString = \"\tText\";\r\n" +
33+
"\tpublic const char ValidTestChar = '\t';\r\n" +
34+
"}\r\n";
35+
36+
await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
37+
}
38+
39+
/// <summary>
40+
/// Verifies that spaces used inside disabled code are not producing diagnostics.
41+
/// </summary>
42+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
43+
[Fact]
44+
public async Task TestDisabledCodeAsync()
45+
{
46+
var testCode =
47+
"public class Foo\r\n" +
48+
"{\r\n" +
49+
"#if false\r\n" +
50+
" public const string ValidTestString = \"Text\";\r\n" +
51+
" public const char ValidTestChar = 'c';\r\n" +
52+
"#endif\r\n" +
53+
"}\r\n";
54+
55+
await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
56+
}
57+
58+
[Fact]
59+
public async Task TestInvalidSpacesAsync()
60+
{
61+
var testCode =
62+
"using\tSystem.Diagnostics;\r\n" +
63+
"\r\n" +
64+
"public\tclass\tFoo\r\n" +
65+
"{\r\n" +
66+
" public void Bar()\r\n" +
67+
" {\r\n" +
68+
" \t \t// Comment\r\n" +
69+
"\t \tDebug.Indent();\r\n" +
70+
" \t}\r\n" +
71+
"}\r\n";
72+
73+
var fixedTestCode =
74+
"using System.Diagnostics;\r\n" +
75+
"\r\n" +
76+
"public class Foo\r\n" +
77+
"{\r\n" +
78+
"\tpublic void Bar()\r\n" +
79+
"\t{\r\n" +
80+
"\t\t// Comment\r\n" +
81+
"\t\tDebug.Indent();\r\n" +
82+
"\t}\r\n" +
83+
"}\r\n";
84+
85+
DiagnosticResult[] expected =
86+
{
87+
this.CSharpDiagnostic().WithLocation(1, 6),
88+
this.CSharpDiagnostic().WithLocation(3, 7),
89+
this.CSharpDiagnostic().WithLocation(3, 13),
90+
this.CSharpDiagnostic().WithLocation(5, 1),
91+
this.CSharpDiagnostic().WithLocation(6, 1),
92+
this.CSharpDiagnostic().WithLocation(7, 1),
93+
this.CSharpDiagnostic().WithLocation(8, 1),
94+
this.CSharpDiagnostic().WithLocation(9, 1)
95+
};
96+
97+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
98+
await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
99+
await this.VerifyCSharpFixAsync(testCode, fixedTestCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
100+
}
101+
102+
[Fact]
103+
public async Task TestInvalidTabsInDocumentationCommentsAsync()
104+
{
105+
var testCode =
106+
" ///\t<summary>\r\n" +
107+
" /// foo\tbar\r\n" +
108+
" ///\t</summary>\r\n" +
109+
" public class Foo\r\n" +
110+
" {\r\n" +
111+
" \t/// <MyElement>\tValue </MyElement>\r\n" +
112+
" \t/**\t \t<MyElement> Value </MyElement>\t*/\r\n" +
113+
" }\r\n";
114+
115+
var fixedTestCode =
116+
"\t/// <summary>\r\n" +
117+
"\t/// foo bar\r\n" +
118+
"\t/// </summary>\r\n" +
119+
"\tpublic class Foo\r\n" +
120+
"\t{\r\n" +
121+
"\t\t/// <MyElement> Value </MyElement>\r\n" +
122+
"\t\t/** <MyElement> Value </MyElement> */\r\n" +
123+
"\t}\r\n";
124+
125+
DiagnosticResult[] expected =
126+
{
127+
this.CSharpDiagnostic().WithLocation(1, 1),
128+
this.CSharpDiagnostic().WithLocation(1, 8),
129+
this.CSharpDiagnostic().WithLocation(2, 1),
130+
this.CSharpDiagnostic().WithLocation(2, 8),
131+
this.CSharpDiagnostic().WithLocation(3, 1),
132+
this.CSharpDiagnostic().WithLocation(3, 8),
133+
this.CSharpDiagnostic().WithLocation(4, 1),
134+
this.CSharpDiagnostic().WithLocation(5, 1),
135+
this.CSharpDiagnostic().WithLocation(6, 1),
136+
this.CSharpDiagnostic().WithLocation(6, 22),
137+
this.CSharpDiagnostic().WithLocation(7, 1),
138+
this.CSharpDiagnostic().WithLocation(7, 9),
139+
this.CSharpDiagnostic().WithLocation(7, 42),
140+
this.CSharpDiagnostic().WithLocation(8, 1),
141+
};
142+
143+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
144+
await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
145+
await this.VerifyCSharpFixAsync(testCode, fixedTestCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
146+
}
147+
148+
[Fact]
149+
public async Task TestInvalidTabsInCommentsAsync()
150+
{
151+
var testCode =
152+
" public class Foo\r\n" +
153+
" {\r\n" +
154+
" public void Bar()\r\n" +
155+
" {\r\n" +
156+
" \t//\tComment\t\t1\r\n" +
157+
"\t\t\t////\tCommented Code\t\t1\r\n" +
158+
" \t \t// Comment 2\r\n" +
159+
" }\r\n" +
160+
" }\r\n";
161+
162+
var fixedTestCode =
163+
"\tpublic class Foo\r\n" +
164+
"\t{\r\n" +
165+
"\t\tpublic void Bar()\r\n" +
166+
"\t\t{\r\n" +
167+
"\t\t\t// Comment 1\r\n" +
168+
"\t\t\t////\tCommented Code\t\t1\r\n" +
169+
"\t\t\t// Comment 2\r\n" +
170+
"\t\t}\r\n" +
171+
"\t}\r\n";
172+
173+
DiagnosticResult[] expected =
174+
{
175+
this.CSharpDiagnostic().WithLocation(1, 1),
176+
this.CSharpDiagnostic().WithLocation(2, 1),
177+
this.CSharpDiagnostic().WithLocation(3, 1),
178+
this.CSharpDiagnostic().WithLocation(4, 1),
179+
this.CSharpDiagnostic().WithLocation(5, 1),
180+
this.CSharpDiagnostic().WithLocation(5, 11),
181+
this.CSharpDiagnostic().WithLocation(7, 1),
182+
this.CSharpDiagnostic().WithLocation(8, 1),
183+
this.CSharpDiagnostic().WithLocation(9, 1),
184+
};
185+
186+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
187+
await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
188+
await this.VerifyCSharpFixAsync(testCode, fixedTestCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
189+
}
190+
191+
[Fact]
192+
public async Task TestInvalidTabsInMultiLineCommentsAsync()
193+
{
194+
var testCode =
195+
" public class Foo\r\n" +
196+
" {\r\n" +
197+
" public void Bar()\r\n" +
198+
" {\r\n" +
199+
" \t/*\r\n" +
200+
" \tComment\t\t1\r\n" +
201+
" \t\tComment 2\r\n" +
202+
"\t \t */\r\n" +
203+
" }\r\n" +
204+
" }\r\n";
205+
206+
var fixedTestCode =
207+
"\tpublic class Foo\r\n" +
208+
"\t{\r\n" +
209+
"\t\tpublic void Bar()\r\n" +
210+
"\t\t{\r\n" +
211+
"\t\t\t/*\r\n" +
212+
" Comment 1\r\n" +
213+
" Comment 2\r\n" +
214+
" */\r\n" +
215+
"\t\t}\r\n" +
216+
"\t}\r\n";
217+
218+
DiagnosticResult[] expected =
219+
{
220+
this.CSharpDiagnostic().WithLocation(1, 1),
221+
this.CSharpDiagnostic().WithLocation(2, 1),
222+
this.CSharpDiagnostic().WithLocation(3, 1),
223+
this.CSharpDiagnostic().WithLocation(4, 1),
224+
this.CSharpDiagnostic().WithLocation(5, 1),
225+
this.CSharpDiagnostic().WithLocation(5, 11),
226+
this.CSharpDiagnostic().WithLocation(9, 1),
227+
this.CSharpDiagnostic().WithLocation(10, 1),
228+
};
229+
230+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
231+
await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
232+
await this.VerifyCSharpFixAsync(testCode, fixedTestCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
233+
}
234+
235+
/// <inheritdoc/>
236+
protected override string GetSettings() =>
237+
@"
238+
{
239+
""settings"": {
240+
""indentation"": {
241+
""useTabs"": true
242+
}
243+
}
244+
}
245+
";
246+
247+
/// <inheritdoc/>
248+
protected override IEnumerable<DiagnosticAnalyzer> GetCSharpDiagnosticAnalyzers()
249+
{
250+
yield return new SA1027TabsMustNotBeUsed();
251+
}
252+
253+
/// <inheritdoc/>
254+
protected override CodeFixProvider GetCSharpCodeFixProvider()
255+
{
256+
return new SA1027CodeFixProvider();
257+
}
258+
}
259+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@
358358
<Compile Include="SpacingRules\SA1025UnitTests.cs" />
359359
<Compile Include="SpacingRules\SA1026UnitTests.cs" />
360360
<Compile Include="SpacingRules\SA1027UnitTests.cs" />
361+
<Compile Include="SpacingRules\SA1027UseTabsUnitTests.cs" />
361362
<Compile Include="SpacingRules\SA1028UnitTests.cs" />
362363
<Compile Include="SpecialRules\SA0001UnitTests.cs" />
363364
<Compile Include="SpecialRules\SA0002UnitTests.cs" />

0 commit comments

Comments
 (0)