Skip to content

Commit fdb043f

Browse files
committed
Merge pull request #2045 from sharwell/fix-2037
Update SA1027 to support the UseTabs setting
2 parents 6f52625 + 694fc7f commit fdb043f

11 files changed

Lines changed: 894 additions & 196 deletions

File tree

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

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ namespace StyleCop.Analyzers.SpacingRules
1818
using Settings.ObjectModel;
1919

2020
/// <summary>
21-
/// Implements a code fix for <see cref="SA1027TabsMustNotBeUsed"/>.
21+
/// Implements a code fix for <see cref="SA1027UseTabsCorrectly"/>.
2222
/// </summary>
2323
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SA1027CodeFixProvider))]
2424
[Shared]
2525
internal class SA1027CodeFixProvider : CodeFixProvider
2626
{
2727
/// <inheritdoc/>
2828
public override ImmutableArray<string> FixableDiagnosticIds { get; } =
29-
ImmutableArray.Create(SA1027TabsMustNotBeUsed.DiagnosticId);
29+
ImmutableArray.Create(SA1027UseTabsCorrectly.DiagnosticId);
3030

3131
/// <inheritdoc/>
3232
public override FixAllProvider GetFixAllProvider()
@@ -62,34 +62,75 @@ private static TextChange FixDiagnostic(IndentationSettings indentationSettings,
6262
TextSpan span = diagnostic.Location.SourceSpan;
6363

6464
TextLine startLine = sourceText.Lines.GetLineFromPosition(span.Start);
65+
66+
bool useTabs = false;
67+
string behavior;
68+
if (diagnostic.Properties.TryGetValue(SA1027UseTabsCorrectly.BehaviorKey, out behavior))
69+
{
70+
useTabs = behavior == SA1027UseTabsCorrectly.ConvertToTabsBehavior;
71+
}
72+
6573
string text = sourceText.ToString(TextSpan.FromBounds(startLine.Start, span.End));
6674
StringBuilder replacement = StringBuilderPool.Allocate();
75+
int spaceCount = 0;
6776
int column = 0;
6877
for (int i = 0; i < text.Length; i++)
6978
{
7079
char c = text[i];
7180
if (c == '\t')
7281
{
7382
var offsetWithinTabColumn = column % indentationSettings.TabSize;
74-
var spaceCount = indentationSettings.TabSize - offsetWithinTabColumn;
83+
var tabWidth = indentationSettings.TabSize - offsetWithinTabColumn;
7584

7685
if (i >= span.Start - startLine.Start)
7786
{
78-
replacement.Append(' ', spaceCount);
87+
if (useTabs)
88+
{
89+
replacement.Length = replacement.Length - spaceCount;
90+
replacement.Append('\t');
91+
spaceCount = 0;
92+
}
93+
else
94+
{
95+
replacement.Append(' ', tabWidth);
96+
}
7997
}
8098

81-
column += spaceCount;
99+
column += tabWidth;
82100
}
83101
else
84102
{
85103
if (i >= span.Start - startLine.Start)
86104
{
87105
replacement.Append(c);
106+
if (c == ' ')
107+
{
108+
spaceCount++;
109+
if (useTabs)
110+
{
111+
// Note that we account for column not yet being incremented
112+
var offsetWithinTabColumn = (column + 1) % indentationSettings.TabSize;
113+
if (offsetWithinTabColumn == 0)
114+
{
115+
// We reached a tab stop.
116+
replacement.Length = replacement.Length - spaceCount;
117+
replacement.Append('\t');
118+
spaceCount = 0;
119+
}
120+
}
121+
}
122+
else
123+
{
124+
spaceCount = 0;
125+
}
88126
}
89127

90-
if (c == '\n')
128+
if (c == '\r' || c == '\n')
91129
{
130+
// Handle newlines. We can ignore CR/LF/CRLF issues because we are only tracking column position
131+
// in a line, and not the line numbers themselves.
92132
column = 0;
133+
spaceCount = 0;
93134
}
94135
else
95136
{
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
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="SA1027UseTabsCorrectly"/> when <see cref="IndentationSettings.UseTabs"/> is
18+
/// <see langword="true"/> and <see cref="IndentationSettings.TabSize"/> is set to a non-default value.
19+
/// </summary>
20+
public class SA1027AlternateIndentationUnitTests : 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+
"\t public void Bar()\r\n" +
79+
"\t {\r\n" +
80+
"\t\t// Comment\r\n" +
81+
"\t\tDebug.Indent();\r\n" +
82+
"\t\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+
"\t public 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, 12),
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+
"\t public class Foo\r\n" +
164+
"\t {\r\n" +
165+
"\t\t public void Bar()\r\n" +
166+
"\t\t {\r\n" +
167+
"\t\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, 13),
181+
this.CSharpDiagnostic().WithLocation(5, 21),
182+
this.CSharpDiagnostic().WithLocation(7, 1),
183+
this.CSharpDiagnostic().WithLocation(8, 1),
184+
this.CSharpDiagnostic().WithLocation(9, 1),
185+
};
186+
187+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
188+
await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
189+
await this.VerifyCSharpFixAsync(testCode, fixedTestCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
190+
}
191+
192+
[Fact]
193+
public async Task TestInvalidTabsInMultiLineCommentsAsync()
194+
{
195+
var testCode =
196+
" public class Foo\r\n" +
197+
" {\r\n" +
198+
" public void Bar()\r\n" +
199+
" {\r\n" +
200+
" \t/*\r\n" +
201+
" \tComment\t\t1\r\n" +
202+
" \t\tComment 2\r\n" +
203+
"\t \t */\r\n" +
204+
" }\r\n" +
205+
" }\r\n";
206+
207+
var fixedTestCode =
208+
"\t public class Foo\r\n" +
209+
"\t {\r\n" +
210+
"\t\t public void Bar()\r\n" +
211+
"\t\t {\r\n" +
212+
"\t\t\t\t/*\r\n" +
213+
"\t\t\tComment 1\r\n" +
214+
"\t\t\t\tComment 2\r\n" +
215+
"\t\t\t */\r\n" +
216+
"\t\t }\r\n" +
217+
"\t }\r\n";
218+
219+
DiagnosticResult[] expected =
220+
{
221+
this.CSharpDiagnostic().WithLocation(1, 1),
222+
this.CSharpDiagnostic().WithLocation(2, 1),
223+
this.CSharpDiagnostic().WithLocation(3, 1),
224+
this.CSharpDiagnostic().WithLocation(4, 1),
225+
this.CSharpDiagnostic().WithLocation(5, 1),
226+
this.CSharpDiagnostic().WithLocation(6, 1),
227+
this.CSharpDiagnostic().WithLocation(6, 17),
228+
this.CSharpDiagnostic().WithLocation(7, 1),
229+
this.CSharpDiagnostic().WithLocation(8, 1),
230+
this.CSharpDiagnostic().WithLocation(9, 1),
231+
this.CSharpDiagnostic().WithLocation(10, 1),
232+
};
233+
234+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
235+
await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
236+
await this.VerifyCSharpFixAsync(testCode, fixedTestCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
237+
}
238+
239+
/// <inheritdoc/>
240+
protected override string GetSettings() =>
241+
@"
242+
{
243+
""settings"": {
244+
""indentation"": {
245+
""useTabs"": true,
246+
""tabSize"": 3
247+
}
248+
}
249+
}
250+
";
251+
252+
/// <inheritdoc/>
253+
protected override IEnumerable<DiagnosticAnalyzer> GetCSharpDiagnosticAnalyzers()
254+
{
255+
yield return new SA1027UseTabsCorrectly();
256+
}
257+
258+
/// <inheritdoc/>
259+
protected override CodeFixProvider GetCSharpCodeFixProvider()
260+
{
261+
return new SA1027CodeFixProvider();
262+
}
263+
}
264+
}

0 commit comments

Comments
 (0)