diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/ReadabilityRules/SA1116UnitTests.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/ReadabilityRules/SA1116UnitTests.cs index 3f58d0381..a61ecd2b1 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/ReadabilityRules/SA1116UnitTests.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/ReadabilityRules/SA1116UnitTests.cs @@ -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 GetTestDeclarations(string delimiter) + public static IEnumerable 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 GetTestConstructorInitializers(string delimiter) + public static IEnumerable 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 GetTestExpressions(string delimiter) + public static IEnumerable GetTestExpressions(string delimiter, string fixDelimiter) { - yield return new object[] { $"Bar(1,{delimiter} 2)", 13 }; - yield return new object[] { $"System.Action func = (int x,{delimiter} int y) => Bar(x, y)", 41 }; - yield return new object[] { $"System.Action 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 func = (int x,{delimiter} int y) => Bar(x, y)", $"System.Action func = ({fixDelimiter}int x,{delimiter} int y) => Bar(x, y)", 41 }; + yield return new object[] { $"System.Action func = delegate(int x,{delimiter} int y) {{ Bar(x, y); }}", $"System.Action 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 ValidTestExpressions() { - yield return new object[] { $"System.Action func = () => Bar(0, 3)", 0 }; - yield return new object[] { $"System.Action 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 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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] @@ -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); } + /// protected override IEnumerable GetCSharpDiagnosticAnalyzers() { yield return new SA1116SplitParametersMustStartOnLineAfterDeclaration(); } + + /// + protected override CodeFixProvider GetCSharpCodeFixProvider() + { + return new SA1116CodeFixProvider(); + } } } diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/ReadabilityRules/ReadabilityResources.Designer.cs b/StyleCop.Analyzers/StyleCop.Analyzers/ReadabilityRules/ReadabilityResources.Designer.cs index 95b03166e..94513a158 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/ReadabilityRules/ReadabilityResources.Designer.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers/ReadabilityRules/ReadabilityResources.Designer.cs @@ -556,6 +556,15 @@ internal static string SA1115Title { } } + /// + /// Looks up a localized string similar to Move first argument to next line. + /// + internal static string SA1116CodeFix { + get { + return ResourceManager.GetString("SA1116CodeFix", resourceCulture); + } + } + /// /// Looks up a localized string similar to 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.. /// diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/ReadabilityRules/ReadabilityResources.resx b/StyleCop.Analyzers/StyleCop.Analyzers/ReadabilityRules/ReadabilityResources.resx index 3058938d2..376c1a9d4 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/ReadabilityRules/ReadabilityResources.resx +++ b/StyleCop.Analyzers/StyleCop.Analyzers/ReadabilityRules/ReadabilityResources.resx @@ -282,6 +282,9 @@ Parameter must follow comma + + Move first argument to next line + 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. diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/ReadabilityRules/SA1116CodeFixProvider.cs b/StyleCop.Analyzers/StyleCop.Analyzers/ReadabilityRules/SA1116CodeFixProvider.cs new file mode 100644 index 000000000..e5069ed0b --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers/ReadabilityRules/SA1116CodeFixProvider.cs @@ -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; + + /// + /// Implements a code fix for . + /// + /// + /// 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. + /// + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SA1116CodeFixProvider))] + [Shared] + public class SA1116CodeFixProvider : CodeFixProvider + { + /// + public override ImmutableArray FixableDiagnosticIds { get; } = + ImmutableArray.Create(SA1116SplitParametersMustStartOnLineAfterDeclaration.DiagnosticId); + + /// + public override FixAllProvider GetFixAllProvider() + { + return CustomFixAllProviders.BatchFixer; + } + + /// + 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 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); + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/StyleCop.Analyzers.csproj b/StyleCop.Analyzers/StyleCop.Analyzers/StyleCop.Analyzers.csproj index cc75b79d2..60968c846 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/StyleCop.Analyzers.csproj +++ b/StyleCop.Analyzers/StyleCop.Analyzers/StyleCop.Analyzers.csproj @@ -297,6 +297,7 @@ +