Skip to content

Commit 47250af

Browse files
committed
Update SA1003 for indices and ranges
1 parent b72b3f7 commit 47250af

File tree

3 files changed

+183
-17
lines changed

3 files changed

+183
-17
lines changed

StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp8/SpacingRules/SA1003CSharp8UnitTests.cs

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
22
// Licensed under the MIT License. See LICENSE in the project root for license information.
33

4-
#nullable disable
5-
64
namespace StyleCop.Analyzers.Test.CSharp8.SpacingRules
75
{
86
using System.Threading;
@@ -38,12 +36,11 @@ public class TestClass
3836
{
3937
public void TestMethod()
4038
{
41-
var test1 = .. {|#0:(|}int)1;
39+
var test1 = {|#0:..|} {|#1:(|}int)1;
4240
}
4341
}
4442
}
4543
";
46-
4744
var fixedCode = @"
4845
namespace TestNamespace
4946
{
@@ -62,7 +59,11 @@ public void TestMethod()
6259
{
6360
ReferenceAssemblies = ReferenceAssemblies.NetCore.NetCoreApp31,
6461
TestCode = testCode,
65-
ExpectedDiagnostics = { Diagnostic(DescriptorNotPrecededByWhitespace).WithLocation(0).WithArguments("(int)") },
62+
ExpectedDiagnostics =
63+
{
64+
Diagnostic(DescriptorNotFollowedByWhitespace).WithLocation(0).WithArguments(".."),
65+
Diagnostic(DescriptorNotPrecededByWhitespace).WithLocation(1).WithArguments("(int)"),
66+
},
6667
FixedCode = fixedCode,
6768
}.RunAsync(CancellationToken.None).ConfigureAwait(false);
6869
}
@@ -142,5 +143,94 @@ public string TestMethod(string? x)
142143
};
143144
await VerifyCSharpFixAsync(testCode, expected, fixedCode, CancellationToken.None).ConfigureAwait(false);
144145
}
146+
147+
[Fact]
148+
[WorkItem(3008, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3008")]
149+
public async Task TestIndexAndRangeExpressionsAsync()
150+
{
151+
var testCode = @"
152+
namespace TestNamespace
153+
{
154+
using System;
155+
156+
public class TestClass
157+
{
158+
public void TestMethod(int[] values)
159+
{
160+
var last = values[^1];
161+
var slice = values[2..^5];
162+
var slice2 = values[..];
163+
Index start = ^5;
164+
Range middle = 1..4;
165+
}
166+
}
167+
}
168+
";
169+
170+
await new CSharpTest()
171+
{
172+
ReferenceAssemblies = ReferenceAssemblies.NetCore.NetCoreApp31,
173+
TestCode = testCode,
174+
}.RunAsync(CancellationToken.None).ConfigureAwait(false);
175+
}
176+
177+
[Fact]
178+
[WorkItem(3008, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3008")]
179+
public async Task TestIndexAndRangeExpressionsSpacingViolationsAsync()
180+
{
181+
var testCode = @"
182+
namespace TestNamespace
183+
{
184+
using System;
185+
186+
public class TestClass
187+
{
188+
public void TestMethod(int[] values)
189+
{
190+
var last = values[ {|#0:^|}1];
191+
var compactSlice = values[1 {|#1:..|} ^2];
192+
var prefixRangeSpaceAfter = values[{|#2:..|} ^2];
193+
var suffixRangeSpaceBefore = values[1 {|#3:..|}];
194+
Range missingLeadingSpace = 1 {|#4:..|} 4;
195+
var missingTrailingSpace = 1{|#5:..|} ^2;
196+
}
197+
}
198+
}
199+
";
200+
201+
var fixedCode = @"
202+
namespace TestNamespace
203+
{
204+
using System;
205+
206+
public class TestClass
207+
{
208+
public void TestMethod(int[] values)
209+
{
210+
var last = values[^1];
211+
var compactSlice = values[1..^2];
212+
var prefixRangeSpaceAfter = values[..^2];
213+
var suffixRangeSpaceBefore = values[1..];
214+
Range missingLeadingSpace = 1..4;
215+
var missingTrailingSpace = 1..^2;
216+
}
217+
}
218+
}
219+
";
220+
221+
var expected = new[]
222+
{
223+
Diagnostic(DescriptorNotPrecededByWhitespace).WithLocation(0).WithArguments("^"),
224+
Diagnostic(DescriptorNotPrecededByWhitespace).WithLocation(1).WithArguments(".."),
225+
Diagnostic(DescriptorNotFollowedByWhitespace).WithLocation(1).WithArguments(".."),
226+
Diagnostic(DescriptorNotFollowedByWhitespace).WithLocation(2).WithArguments(".."),
227+
Diagnostic(DescriptorNotPrecededByWhitespace).WithLocation(3).WithArguments(".."),
228+
Diagnostic(DescriptorNotPrecededByWhitespace).WithLocation(4).WithArguments(".."),
229+
Diagnostic(DescriptorNotFollowedByWhitespace).WithLocation(4).WithArguments(".."),
230+
Diagnostic(DescriptorNotFollowedByWhitespace).WithLocation(5).WithArguments(".."),
231+
};
232+
233+
await VerifyCSharpFixAsync(testCode, expected, fixedCode, CancellationToken.None).ConfigureAwait(false);
234+
}
145235
}
146236
}

StyleCop.Analyzers/StyleCop.Analyzers/SpacingRules/SA1003SymbolsMustBeSpacedCorrectly.cs

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ internal class SA1003SymbolsMustBeSpacedCorrectly : DiagnosticAnalyzer
103103
SyntaxKind.LogicalNotExpression,
104104
SyntaxKind.PreIncrementExpression,
105105
SyntaxKind.PreDecrementExpression,
106-
SyntaxKind.AddressOfExpression);
106+
SyntaxKind.AddressOfExpression,
107+
SyntaxKindEx.IndexExpression);
107108

108109
private static readonly ImmutableArray<SyntaxKind> PostfixUnaryExpressionKinds =
109110
ImmutableArray.Create(
@@ -138,6 +139,7 @@ internal class SA1003SymbolsMustBeSpacedCorrectly : DiagnosticAnalyzer
138139
private static readonly Action<SyntaxNodeAnalysisContext> EqualsValueClauseAction = HandleEqualsValueClause;
139140
private static readonly Action<SyntaxNodeAnalysisContext> LambdaExpressionAction = HandleLambdaExpression;
140141
private static readonly Action<SyntaxNodeAnalysisContext> ArrowExpressionClauseAction = HandleArrowExpressionClause;
142+
private static readonly Action<SyntaxNodeAnalysisContext> RangeExpressionAction = HandleRangeExpression;
141143

142144
/// <summary>
143145
/// Gets the descriptor for prefix unary expression that may not be followed by a comment.
@@ -214,6 +216,7 @@ public override void Initialize(AnalysisContext context)
214216
context.RegisterSyntaxNodeAction(EqualsValueClauseAction, SyntaxKind.EqualsValueClause);
215217
context.RegisterSyntaxNodeAction(LambdaExpressionAction, SyntaxKinds.LambdaExpression);
216218
context.RegisterSyntaxNodeAction(ArrowExpressionClauseAction, SyntaxKind.ArrowExpressionClause);
219+
context.RegisterSyntaxNodeAction(RangeExpressionAction, SyntaxKindEx.RangeExpression);
217220
}
218221

219222
private static void HandleConstructorDeclaration(SyntaxNodeAnalysisContext context)
@@ -249,6 +252,41 @@ private static void HandleBinaryExpression(SyntaxNodeAnalysisContext context)
249252
CheckToken(context, binaryExpression.OperatorToken, true, true, true);
250253
}
251254

255+
private static void HandleRangeExpression(SyntaxNodeAnalysisContext context)
256+
{
257+
if (!RangeExpressionSyntaxWrapper.IsInstance(context.Node))
258+
{
259+
return;
260+
}
261+
262+
var rangeExpression = (RangeExpressionSyntaxWrapper)context.Node;
263+
var hasLeftOperand = rangeExpression.LeftOperand != null;
264+
var hasRightOperand = rangeExpression.RightOperand != null;
265+
266+
if (hasLeftOperand && hasRightOperand)
267+
{
268+
// Both operands present: no whitespace around the operator.
269+
CheckToken(context, rangeExpression.OperatorToken, withLeadingWhitespace: false, allowAtEndOfLine: true, withTrailingWhitespace: false);
270+
return;
271+
}
272+
273+
if (!hasLeftOperand && hasRightOperand)
274+
{
275+
// Left operand omitted: preceding spacing is governed by surrounding context; operator must be adjacent to right operand.
276+
CheckTokenTrailingWhitespace(context, rangeExpression.OperatorToken, allowAtEndOfLine: false, withTrailingWhitespace: false);
277+
return;
278+
}
279+
280+
if (hasLeftOperand && !hasRightOperand)
281+
{
282+
// Right operand omitted: operator must be adjacent to left operand; trailing spacing is governed by surrounding context.
283+
CheckTokenLeadingWhitespace(context, rangeExpression.OperatorToken, withLeadingWhitespace: false);
284+
return;
285+
}
286+
287+
// Both operands omitted: spacing governed by surrounding context.
288+
}
289+
252290
private static void HandlePrefixUnaryExpression(SyntaxNodeAnalysisContext context)
253291
{
254292
var unaryExpression = (PrefixUnaryExpressionSyntax)context.Node;
@@ -265,6 +303,7 @@ private static void HandlePrefixUnaryExpression(SyntaxNodeAnalysisContext contex
265303
&& !(unaryExpression.Parent is CastExpressionSyntax)
266304
&& !precedingToken.IsKind(SyntaxKind.OpenParenToken)
267305
&& !precedingToken.IsKind(SyntaxKind.OpenBracketToken)
306+
&& !precedingToken.IsKind(SyntaxKindEx.DotDotToken)
268307
&& !(precedingToken.IsKind(SyntaxKind.OpenBraceToken) && (precedingToken.Parent is InterpolationSyntax));
269308

270309
bool analyze;
@@ -291,7 +330,16 @@ private static void HandlePrefixUnaryExpression(SyntaxNodeAnalysisContext contex
291330
}
292331
else
293332
{
294-
CheckToken(context, unaryExpression.OperatorToken, mustHaveLeadingWhitespace, false, false);
333+
if (precedingToken.IsKind(SyntaxKindEx.DotDotToken))
334+
{
335+
// The preceding whitespace will be checked as part of the range operator '..' so only check
336+
// trailing whitespace for the current unary prefix operator.
337+
CheckTokenTrailingWhitespace(context, unaryExpression.OperatorToken, allowAtEndOfLine: false, false);
338+
}
339+
else
340+
{
341+
CheckToken(context, unaryExpression.OperatorToken, mustHaveLeadingWhitespace, allowAtEndOfLine: false, withTrailingWhitespace: false);
342+
}
295343
}
296344
}
297345
}
@@ -385,12 +433,17 @@ private static void CheckToken(SyntaxNodeAnalysisContext context, SyntaxToken to
385433
{
386434
tokenText = tokenText ?? token.Text;
387435

436+
CheckTokenLeadingWhitespace(context, token, withLeadingWhitespace, tokenText);
437+
CheckTokenTrailingWhitespace(context, token, allowAtEndOfLine, withTrailingWhitespace, tokenText);
438+
}
439+
440+
private static void CheckTokenLeadingWhitespace(SyntaxNodeAnalysisContext context, SyntaxToken token, bool withLeadingWhitespace, string tokenText = null)
441+
{
442+
tokenText = tokenText ?? token.Text;
443+
388444
var precedingToken = token.GetPreviousToken();
389445
var precedingTriviaList = TriviaHelper.MergeTriviaLists(precedingToken.TrailingTrivia, token.LeadingTrivia);
390446

391-
var followingToken = token.GetNextToken();
392-
var followingTriviaList = TriviaHelper.MergeTriviaLists(token.TrailingTrivia, followingToken.LeadingTrivia);
393-
394447
if (withLeadingWhitespace)
395448
{
396449
// Don't report missing leading whitespace when the token is the first token on a text line.
@@ -413,6 +466,14 @@ private static void CheckToken(SyntaxNodeAnalysisContext context, SyntaxToken to
413466
context.ReportDiagnostic(Diagnostic.Create(DescriptorNotPrecededByWhitespace, token.GetLocation(), properties, tokenText));
414467
}
415468
}
469+
}
470+
471+
private static void CheckTokenTrailingWhitespace(SyntaxNodeAnalysisContext context, SyntaxToken token, bool allowAtEndOfLine, bool withTrailingWhitespace, string tokenText = null)
472+
{
473+
tokenText = tokenText ?? token.Text;
474+
475+
var followingToken = token.GetNextToken();
476+
var followingTriviaList = TriviaHelper.MergeTriviaLists(token.TrailingTrivia, followingToken.LeadingTrivia);
416477

417478
if (!allowAtEndOfLine && token.TrailingTrivia.Any(SyntaxKind.EndOfLineTrivia))
418479
{
@@ -437,14 +498,11 @@ private static void CheckToken(SyntaxNodeAnalysisContext context, SyntaxToken to
437498
context.ReportDiagnostic(Diagnostic.Create(DescriptorFollowedByWhitespace, token.GetLocation(), properties, tokenText));
438499
}
439500
}
440-
else
501+
else if ((followingTriviaList.Count > 0) && followingTriviaList.First().IsKind(SyntaxKind.WhitespaceTrivia))
441502
{
442-
if ((followingTriviaList.Count > 0) && followingTriviaList.First().IsKind(SyntaxKind.WhitespaceTrivia))
443-
{
444-
var properties = ImmutableDictionary.Create<string, string>()
445-
.Add(CodeFixAction, RemoveAfterTag);
446-
context.ReportDiagnostic(Diagnostic.Create(DescriptorNotFollowedByWhitespace, token.GetLocation(), properties, tokenText));
447-
}
503+
var properties = ImmutableDictionary.Create<string, string>()
504+
.Add(CodeFixAction, RemoveAfterTag);
505+
context.ReportDiagnostic(Diagnostic.Create(DescriptorNotFollowedByWhitespace, token.GetLocation(), properties, tokenText));
448506
}
449507
}
450508
}

documentation/SA1003.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,24 @@ if (!value)
4646
}
4747
```
4848

49+
The C# 8 *index-from-end* operator (`^`) follows the same unary spacing requirements. It is written adjacent to its
50+
operand without a trailing space, and no additional space is inserted after `[` or `(` when it appears immediately after
51+
one of those tokens. Examples:
52+
53+
```csharp
54+
var last = values[^1];
55+
Index start = ^5;
56+
```
57+
58+
The C# 8 *range* operator (`..`) is treated as a binary operator which should not be surrounded by a single space on
59+
either side. Within indexers, this spacing is applied between operands while still omitting any space after `[` or
60+
before `]`. For example:
61+
62+
```csharp
63+
Range middle = 1..4;
64+
var slice = values[1..^2];
65+
```
66+
4967
## How to fix violations
5068

5169
To fix a violation of this rule, ensure that the spacing around the symbol follows the rule described above.

0 commit comments

Comments
 (0)