Skip to content

Commit

Permalink
Implement a code fix and unit tests for SA1116
Browse files Browse the repository at this point in the history
Fixes #1228
  • Loading branch information
sharwell committed Aug 17, 2015
1 parent edc3d86 commit 6eed039
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,51 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using StyleCop.Analyzers.ReadabilityRules;
using TestHelper;
using Xunit;

public class SA1116UnitTests : DiagnosticVerifier
public class SA1116UnitTests : CodeFixVerifier
{
public static IEnumerable<object[]> GetTestDeclarations(string delimiter)
public static IEnumerable<object[]> GetTestDeclarations(string delimiter, string fixDelimiter)
{
yield return new object[] { $"public Foo(int a,{delimiter} string s) {{ }}", 16 };
yield return new object[] { $"public object Bar(int a,{delimiter} string s) => null;", 23 };
yield return new object[] { $"public static Foo operator + (Foo a,{delimiter} Foo b) => null;", 35 };
yield return new object[] { $"public object this[int a,{delimiter} string s] => null;", 24 };
yield return new object[] { $"public delegate void Bar(int a,{delimiter} string s);", 30 };
yield return new object[] { $"public Foo(int a,{delimiter} string s) {{ }}", $"public Foo({fixDelimiter}int a,{delimiter} string s) {{ }}", 16 };
yield return new object[] { $"public object Bar(int a,{delimiter} string s) => null;", $"public object Bar({fixDelimiter}int a,{delimiter} string s) => null;", 23 };
yield return new object[] { $"public static Foo operator + (Foo a,{delimiter} Foo b) => null;", $"public static Foo operator + ({fixDelimiter}Foo a,{delimiter} Foo b) => null;", 35 };
yield return new object[] { $"public object this[int a,{delimiter} string s] => null;", $"public object this[{fixDelimiter}int a,{delimiter} string s] => null;", 24 };
yield return new object[] { $"public delegate void Bar(int a,{delimiter} string s);", $"public delegate void Bar({fixDelimiter}int a,{delimiter} string s);", 30 };
}

public static IEnumerable<object[]> GetTestConstructorInitializers(string delimiter)
public static IEnumerable<object[]> GetTestConstructorInitializers(string delimiter, string fixDelimiter)
{
yield return new object[] { $"this(42,{delimiter} \"hello\")" };
yield return new object[] { $"base(42,{delimiter} \"hello\")" };
yield return new object[] { $"this(42,{delimiter} \"hello\")", $"this({fixDelimiter}42,{delimiter} \"hello\")" };
yield return new object[] { $"base(42,{delimiter} \"hello\")", $"base({fixDelimiter}42,{delimiter} \"hello\")" };
}

public static IEnumerable<object[]> GetTestExpressions(string delimiter)
public static IEnumerable<object[]> GetTestExpressions(string delimiter, string fixDelimiter)
{
yield return new object[] { $"Bar(1,{delimiter} 2)", 13 };
yield return new object[] { $"System.Action<int, int> func = (int x,{delimiter} int y) => Bar(x, y)", 41 };
yield return new object[] { $"System.Action<int, int> func = delegate(int x,{delimiter} int y) {{ Bar(x, y); }}", 49 };
yield return new object[] { $"new string('a',{delimiter} 2)", 20 };
yield return new object[] { $"var arr = new string[2,{delimiter} 2];", 30 };
yield return new object[] { $"char cc = (new char[3, 3])[2,{delimiter} 2];", 36 };
yield return new object[] { $"char? c = (new char[3, 3])?[2,{delimiter} 2];", 37 };
yield return new object[] { $"long ll = this[2,{delimiter} 2];", 24 };
yield return new object[] { $"Bar(1,{delimiter} 2)", $"Bar({fixDelimiter}1,{delimiter} 2)", 13 };
yield return new object[] { $"System.Action<int, int> func = (int x,{delimiter} int y) => Bar(x, y)", $"System.Action<int, int> func = ({fixDelimiter}int x,{delimiter} int y) => Bar(x, y)", 41 };
yield return new object[] { $"System.Action<int, int> func = delegate(int x,{delimiter} int y) {{ Bar(x, y); }}", $"System.Action<int, int> func = delegate({fixDelimiter}int x,{delimiter} int y) {{ Bar(x, y); }}", 49 };
yield return new object[] { $"new string('a',{delimiter} 2)", $"new string({fixDelimiter}'a',{delimiter} 2)", 20 };
yield return new object[] { $"var arr = new string[2,{delimiter} 2];", $"var arr = new string[{fixDelimiter}2,{delimiter} 2];", 30 };
yield return new object[] { $"char cc = (new char[3, 3])[2,{delimiter} 2];", $"char cc = (new char[3, 3])[{fixDelimiter}2,{delimiter} 2];", 36 };
yield return new object[] { $"char? c = (new char[3, 3])?[2,{delimiter} 2];", $"char? c = (new char[3, 3])?[{fixDelimiter}2,{delimiter} 2];", 37 };
yield return new object[] { $"long ll = this[2,{delimiter} 2];", $"long ll = this[{fixDelimiter}2,{delimiter} 2];", 24 };
}

public static IEnumerable<object[]> ValidTestExpressions()
{
yield return new object[] { $"System.Action func = () => Bar(0, 3)", 0 };
yield return new object[] { $"System.Action<int> func = x => Bar(x, 3)", 0 };
yield return new object[] { $"System.Action func = delegate {{ Bar(0, 0); }}", 0 };
yield return new object[] { $"System.Action func = () => Bar(0, 3)", null, 0 };
yield return new object[] { $"System.Action<int> func = x => Bar(x, 3)", null, 0 };
yield return new object[] { $"System.Action func = delegate {{ Bar(0, 0); }}", null, 0 };
}

[Theory]
[MemberData(nameof(GetTestDeclarations), "")]
public async Task TestValidDeclarationAsync(string declaration, int column)
[MemberData(nameof(GetTestDeclarations), "", "")]
public async Task TestValidDeclarationAsync(string declaration, string fixedDeclaration, int column)
{
var testCode = $@"
class Foo
Expand All @@ -57,21 +58,28 @@ class Foo
}

[Theory]
[MemberData(nameof(GetTestDeclarations), "\r\n")]
public async Task TestInvalidDeclarationAsync(string declaration, int column)
[MemberData(nameof(GetTestDeclarations), "\r\n", "\r\n ")]
public async Task TestInvalidDeclarationAsync(string declaration, string fixedDeclaration, int column)
{
var testCode = $@"
class Foo
{{
{declaration}
}}";
var fixedCode = $@"
class Foo
{{
{fixedDeclaration}
}}";
DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(4, column);
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
}

[Theory]
[MemberData(nameof(GetTestConstructorInitializers), "")]
public async Task TestValidConstructorInitializerAsync(string initializer)
[MemberData(nameof(GetTestConstructorInitializers), "", "")]
public async Task TestValidConstructorInitializerAsync(string initializer, string fixedInitializer)
{
var testCode = $@"
class Base
Expand All @@ -98,8 +106,8 @@ public Derived(int i, string z)
}

[Theory]
[MemberData(nameof(GetTestConstructorInitializers), "\r\n")]
public async Task TestInvalidConstructorInitializerAsync(string initializer)
[MemberData(nameof(GetTestConstructorInitializers), "\r\n", "\r\n ")]
public async Task TestInvalidConstructorInitializerAsync(string initializer, string fixedInitializer)
{
var testCode = $@"
class Base
Expand All @@ -121,14 +129,37 @@ public Derived(int i, string z)
{{
}}
}}";
var fixedCode = $@"
class Base
{{
public Base(int a, string s)
{{
}}
}}
class Derived : Base
{{
public Derived()
: {fixedInitializer}
{{
}}
public Derived(int i, string z)
: base(i, z)
{{
}}
}}";

DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(12, 16);
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
}

[Theory]
[MemberData(nameof(GetTestExpressions), "")]
[MemberData(nameof(GetTestExpressions), "", "")]
[MemberData(nameof(ValidTestExpressions))]
public async Task TestValidExpressionAsync(string expression, int column)
public async Task TestValidExpressionAsync(string expression, string fixedExpression, int column)
{
var testCode = $@"
class Foo
Expand All @@ -149,8 +180,8 @@ public void Baz()
}

[Theory]
[MemberData(nameof(GetTestExpressions), "\r\n")]
public async Task TestInvalidExpressionAsync(string expression, int column)
[MemberData(nameof(GetTestExpressions), "\r\n", "\r\n ")]
public async Task TestInvalidExpressionAsync(string expression, string fixedExpression, int column)
{
var testCode = $@"
class Foo
Expand All @@ -164,11 +195,27 @@ public void Baz()
{expression};
}}
public long this[int a, int s] => a + s;
}}";
var fixedCode = $@"
class Foo
{{
public void Bar(int i, int z)
{{
}}
public void Baz()
{{
{fixedExpression};
}}
public long this[int a, int s] => a + s;
}}";

DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(10, column);
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
}

[Fact]
Expand Down Expand Up @@ -228,15 +275,39 @@ public MyAttribute(int a, int b)
2)]
class Foo
{
}";
var fixedCode = @"
[System.AttributeUsage(System.AttributeTargets.Class)]
public class MyAttribute : System.Attribute
{
public MyAttribute(int a, int b)
{
}
}
[MyAttribute(
1,
2)]
class Foo
{
}";

DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(10, 14);
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
}

/// <inheritdoc/>
protected override IEnumerable<DiagnosticAnalyzer> GetCSharpDiagnosticAnalyzers()
{
yield return new SA1116SplitParametersMustStartOnLineAfterDeclaration();
}

/// <inheritdoc/>
protected override CodeFixProvider GetCSharpCodeFixProvider()
{
return new SA1116CodeFixProvider();
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,9 @@
<data name="SA1115Title" xml:space="preserve">
<value>Parameter must follow comma</value>
</data>
<data name="SA1116CodeFix" xml:space="preserve">
<value>Move first argument to next line</value>
</data>
<data name="SA1116Description" xml:space="preserve">
<value>The parameters to a C# method or indexer call or declaration span across multiple lines, but the first parameter does not start on the line after the opening bracket.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
namespace StyleCop.Analyzers.ReadabilityRules
{
using System.Collections.Immutable;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Helpers;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;

/// <summary>
/// Implements a code fix for <see cref="SA1116SplitParametersMustStartOnLineAfterDeclaration"/>.
/// </summary>
/// <remarks>
/// <para>To fix a violation of this rule, ensure that the first parameter starts on the line after the opening
/// bracket, or place all parameters on the same line if the parameters are not too long.</para>
/// </remarks>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SA1116CodeFixProvider))]
[Shared]
public class SA1116CodeFixProvider : CodeFixProvider
{
/// <inheritdoc/>
public override ImmutableArray<string> FixableDiagnosticIds { get; } =
ImmutableArray.Create(SA1116SplitParametersMustStartOnLineAfterDeclaration.DiagnosticId);

/// <inheritdoc/>
public override FixAllProvider GetFixAllProvider()
{
return CustomFixAllProviders.BatchFixer;
}

/// <inheritdoc/>
public override Task RegisterCodeFixesAsync(CodeFixContext context)
{
foreach (var diagnostic in context.Diagnostics)
{
if (!diagnostic.Id.Equals(SA1116SplitParametersMustStartOnLineAfterDeclaration.DiagnosticId))
{
continue;
}

context.RegisterCodeFix(
CodeAction.Create(
ReadabilityResources.SA1116CodeFix,
cancellationToken => GetTransformedDocumentAsync(context.Document, diagnostic, cancellationToken),
equivalenceKey: nameof(SA1116CodeFixProvider)),
diagnostic);
}

return SpecializedTasks.CompletedTask;
}

private static async Task<Document> GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
{
SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
SyntaxToken originalToken = root.FindToken(diagnostic.Location.SourceSpan.Start);

SyntaxTree tree = root.SyntaxTree;
SourceText sourceText = await tree.GetTextAsync(cancellationToken).ConfigureAwait(false);
TextLine sourceLine = sourceText.Lines.GetLineFromPosition(originalToken.SpanStart);

string lineText = sourceText.ToString(sourceLine.Span);
int indentLength;
for (indentLength = 0; indentLength < lineText.Length; indentLength++)
{
if (!char.IsWhiteSpace(lineText[indentLength]))
{
break;
}
}

IndentationOptions indentationOptions = IndentationOptions.FromDocument(document);
SyntaxTriviaList newTrivia =
SyntaxFactory.TriviaList(
SyntaxFactory.CarriageReturnLineFeed,
SyntaxFactory.Whitespace(lineText.Substring(0, indentLength) + IndentationHelper.GenerateIndentationString(indentationOptions, 1)));

SyntaxToken updatedToken = originalToken.WithLeadingTrivia(originalToken.LeadingTrivia.AddRange(newTrivia));
SyntaxNode updatedRoot = root.ReplaceToken(originalToken, updatedToken);
return document.WithSyntaxRoot(updatedRoot);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@
<Compile Include="ReadabilityRules\SA1113CommaMustBeOnSameLineAsPreviousParameter.cs" />
<Compile Include="ReadabilityRules\SA1114ParameterListMustFollowDeclaration.cs" />
<Compile Include="ReadabilityRules\SA1115ParameterMustFollowComma.cs" />
<Compile Include="ReadabilityRules\SA1116CodeFixProvider.cs" />
<Compile Include="ReadabilityRules\SA1116SplitParametersMustStartOnLineAfterDeclaration.cs" />
<Compile Include="ReadabilityRules\SA1117ParametersMustBeOnSameLineOrSeparateLines.cs" />
<Compile Include="ReadabilityRules\SA1118ParameterMustNotSpanMultipleLines.cs" />
Expand Down

0 comments on commit 6eed039

Please sign in to comment.