From ba17fd0cfee6704da00b3e05587a5910db8d629b Mon Sep 17 00:00:00 2001 From: vweijsters Date: Sat, 30 May 2015 21:36:26 +0200 Subject: [PATCH] Implemented SA1508 (incl. codefix + test) --- .../LayoutRules/SA1508UnitTests.cs | 846 ++++++++++++++++++ .../StyleCop.Analyzers.Test.csproj | 1 + .../LayoutRules/LayoutResources.Designer.cs | 9 + .../LayoutRules/LayoutResources.resx | 3 + .../LayoutRules/SA1505CodeFixProvider.cs | 20 - ...rlyBracketsMustNotBePrecededByBlankLine.cs | 108 ++- .../LayoutRules/SA1508CodeFixProvider.cs | 91 ++ .../StyleCop.Analyzers.csproj | 1 + 8 files changed, 1051 insertions(+), 28 deletions(-) create mode 100644 StyleCop.Analyzers/StyleCop.Analyzers.Test/LayoutRules/SA1508UnitTests.cs create mode 100644 StyleCop.Analyzers/StyleCop.Analyzers/LayoutRules/SA1508CodeFixProvider.cs diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/LayoutRules/SA1508UnitTests.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/LayoutRules/SA1508UnitTests.cs new file mode 100644 index 000000000..1ea36856a --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/LayoutRules/SA1508UnitTests.cs @@ -0,0 +1,846 @@ +namespace StyleCop.Analyzers.Test.LayoutRules +{ + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.CodeAnalysis.CodeFixes; + using Microsoft.CodeAnalysis.Diagnostics; + using StyleCop.Analyzers.LayoutRules; + using TestHelper; + using Xunit; + + /// + /// Unit tests for the class. + /// + public class SA1508UnitTests : CodeFixVerifier + { + public static IEnumerable TypeTestData + { + get + { + yield return new object[] { "class", "public " }; + yield return new object[] { "struct", "public " }; + yield return new object[] { "interface", string.Empty }; + } + } + + /// + /// Verifies that the analyzer will properly handle an empty source. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestEmptySourceAsync() + { + var testCode = string.Empty; + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Verifies that a valid block will not produce any diagnostics. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestValidBlockAsync() + { + var testCode = @"namespace TestNamespace +{ + public class TestClass + { + private int testField; + + public void TestMethod() + { + if (this.testField < 0) + { + this.testField = -this.testField; + } + } + } +} +"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Verifies that an invalid block will produce the expected diagnostics. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestInvalidBlockAsync() + { + var testCode = @"namespace TestNamespace +{ + public class TestClass + { + private int testField; + + public void TestMethod() + { + if (this.testField < 0) + { + this.testField = -this.testField; + + } + } + } +} +"; + + var fixedTestCode = @"namespace TestNamespace +{ + public class TestClass + { + private int testField; + + public void TestMethod() + { + if (this.testField < 0) + { + this.testField = -this.testField; + } + } + } +} +"; + + var expectedDiagnostic = this.CSharpDiagnostic().WithLocation(13, 13); + + await this.VerifyCSharpDiagnosticAsync(testCode, expectedDiagnostic, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedTestCode).ConfigureAwait(false); + } + + /// + /// Verifies that valid object initializers will not produce any diagnostics. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestValidObjectInitializerAsync() + { + var testCode = @"namespace TestNamespace +{ + public class TestClass + { + private struct Helper + { + public int A; + public int B; + } + + public void TestMethod() + { + var v1 = new Helper { A = 10, B = 5 }; + var v2 = new Helper + { + A = 5, + B = 10 + }; + } + } +} +"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Verifies that invalid object initializers will produce the expected diagnostics. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestInvalidObjectInitializerAsync() + { + var testCode = @"namespace TestNamespace +{ + public class TestClass + { + private struct Helper + { + public int A; + public int B; + } + + public void TestMethod() + { + var v2 = new Helper + { + A = 5, + B = 10 + + }; + } + } +} +"; + + var fixedTestCode = @"namespace TestNamespace +{ + public class TestClass + { + private struct Helper + { + public int A; + public int B; + } + + public void TestMethod() + { + var v2 = new Helper + { + A = 5, + B = 10 + }; + } + } +} +"; + + var expectedDiagnostic = this.CSharpDiagnostic().WithLocation(18, 13); + + await this.VerifyCSharpDiagnosticAsync(testCode, expectedDiagnostic, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedTestCode).ConfigureAwait(false); + } + + /// + /// Verifies that valid collection initializers will not produce any diagnostics. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestValidCollectionInitializerAsync() + { + var testCode = @"namespace TestNamespace +{ + using System.Collections.Generic; + + public class TestClass + { + public void TestMethod() + { + var v1 = new List { 1, 2, 3 }; + var v2 = new List + { + 1, + 2, + 4 + }; + } + } +} +"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Verifies that invalid collection initializers will produce the expected diagnostics. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestInvalidCollectionInitializerAsync() + { + var testCode = @"namespace TestNamespace +{ + using System.Collections.Generic; + + public class TestClass + { + public void TestMethod() + { + var v1 = new List + { + 1, + 2, + 3 + + }; + } + } +} +"; + + var fixedTestCode = @"namespace TestNamespace +{ + using System.Collections.Generic; + + public class TestClass + { + public void TestMethod() + { + var v1 = new List + { + 1, + 2, + 3 + }; + } + } +} +"; + + var expectedDiagnostic = this.CSharpDiagnostic().WithLocation(15, 13); + + await this.VerifyCSharpDiagnosticAsync(testCode, expectedDiagnostic, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedTestCode).ConfigureAwait(false); + } + + /// + /// Verifies that valid array initializers will not produce any diagnostics. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestValidArrayInitializerAsync() + { + var testCode = @"namespace TestNamespace +{ + public class TestClass + { + public void TestMethod() + { + int[] v1 = { 1, 2, 3 }; + int[] v2 = + { + 1, + 2, + 4 + }; + } + } +} +"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Verifies that invalid array initializers will produce the expected diagnostics. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestInvalidArrayInitializerAsync() + { + var testCode = @"namespace TestNamespace +{ + public class TestClass + { + public void TestMethod() + { + int[] v1 = + { + 1, + 2, + 3 + + }; + } + } +} +"; + + var fixedTestCode = @"namespace TestNamespace +{ + public class TestClass + { + public void TestMethod() + { + int[] v1 = + { + 1, + 2, + 3 + }; + } + } +} +"; + + var expectedDiagnostic = this.CSharpDiagnostic().WithLocation(13, 13); + + await this.VerifyCSharpDiagnosticAsync(testCode, expectedDiagnostic, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedTestCode).ConfigureAwait(false); + } + + /// + /// Verifies that valid complex element initializers will not produce any diagnostics. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestValidComplexElementInitializerAsync() + { + var testCode = @"namespace TestNamespace +{ + using System.Collections.Generic; + + public class TestClass + { + public void TestMethod() + { + var v1 = new Dictionary + { + { 1, ""Test"" } + }; + + var v2 = new Dictionary + { + { + 1, + ""Test"" + } + }; + } + } +} +"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Verifies that invalid complex element initializers will produce the expected diagnostics. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestInvalidComplexElementInitializerAsync() + { + var testCode = @"namespace TestNamespace +{ + using System.Collections.Generic; + + public class TestClass + { + public void TestMethod() + { + var v1 = new Dictionary + { + { + 1, + ""Test"" + + } + }; + } + } +} +"; + + var fixedTestCode = @"namespace TestNamespace +{ + using System.Collections.Generic; + + public class TestClass + { + public void TestMethod() + { + var v1 = new Dictionary + { + { + 1, + ""Test"" + } + }; + } + } +} +"; + + var expectedDiagnostic = this.CSharpDiagnostic().WithLocation(15, 17); + + await this.VerifyCSharpDiagnosticAsync(testCode, expectedDiagnostic, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedTestCode).ConfigureAwait(false); + } + + /// + /// Verifies that valid anonymous object creation statements will not produce any diagnostics. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestValidAnonymousObjectCreationAsync() + { + var testCode = @"namespace TestNamespace +{ + public class TestClass + { + public void TestMethod() + { + var v1 = new { x = 10, y = 5 }; + var v2 = new + { + x = 5, + y = 10 + }; + } + } +} +"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Verifies that invalid anonymous object creation statements will produce the expected diagnostics. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestInvalidAnonymousObjectCreationAsync() + { + var testCode = @"namespace TestNamespace +{ + public class TestClass + { + public void TestMethod() + { + var v1 = new + { + x = 5, + y = 10 + + }; + } + } +} +"; + + var fixedTestCode = @"namespace TestNamespace +{ + public class TestClass + { + public void TestMethod() + { + var v1 = new + { + x = 5, + y = 10 + }; + } + } +} +"; + + var expectedDiagnostic = this.CSharpDiagnostic().WithLocation(12, 13); + + await this.VerifyCSharpDiagnosticAsync(testCode, expectedDiagnostic, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedTestCode).ConfigureAwait(false); + } + + /// + /// Verifies that valid switch statements will not produce any diagnostics. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestValidSwitchStatementAsync() + { + var testCode = @"namespace TestNamespace +{ + public class TestClass + { + public int TestMethod(int x) + { + switch (x) + { + case 1: + return 2; + default: + return x; + } + } + } +} +"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Verifies that invalid switch statements will produce the expected diagnostics. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestInvalidSwitchStatementAsync() + { + var testCode = @"namespace TestNamespace +{ + public class TestClass + { + public int TestMethod(int x) + { + switch (x) + { + case 1: + return 2; + default: + return x; + + } + } + } +} +"; + + var fixedTestCode = @"namespace TestNamespace +{ + public class TestClass + { + public int TestMethod(int x) + { + switch (x) + { + case 1: + return 2; + default: + return x; + } + } + } +} +"; + + var expectedDiagnostic = this.CSharpDiagnostic().WithLocation(14, 13); + + await this.VerifyCSharpDiagnosticAsync(testCode, expectedDiagnostic, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedTestCode).ConfigureAwait(false); + } + + /// + /// Verifies that valid namespace declarations will not produce any diagnostics. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestValidNamespaceDeclarationAsync() + { + var testCode = @"namespace TestNamespace +{ + public class TestClass + { + } +} +"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Verifies that invalid namespace declarations will produce the expected diagnostics. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestInvalidNamespaceDeclarationAsync() + { + var testCode = @"namespace TestNamespace +{ + public class TestClass + { + } + +} +"; + + var fixedTestCode = @"namespace TestNamespace +{ + public class TestClass + { + } +} +"; + + var expectedDiagnostic = this.CSharpDiagnostic().WithLocation(7, 1); + + await this.VerifyCSharpDiagnosticAsync(testCode, expectedDiagnostic, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedTestCode).ConfigureAwait(false); + } + + /// + /// Verifies that valid type declarations will not produce any diagnostics. + /// + /// The type keyword to use. + /// The access modifier to use. + /// A representing the asynchronous unit test. + [Theory] + [MemberData(nameof(TypeTestData))] + public async Task TestValidTypeDeclarationAsync(string typeKeyword, string accessModifier) + { + var testCode = $@"namespace TestNamespace +{{ + public {typeKeyword} TestType + {{ + {accessModifier}int TestProperty {{ get; set; }} + }} +}} +"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Verifies that invalid type declarations will produce the expected diagnostics. + /// + /// The type keyword to use. + /// The access modifier to use. + /// A representing the asynchronous unit test. + [Theory] + [MemberData(nameof(TypeTestData))] + public async Task TestInvalidTypeDeclarationAsync(string typeKeyword, string accessModifier) + { + var testCode = $@"namespace TestNamespace +{{ + public {typeKeyword} TestType + {{ + {accessModifier}int TestProperty {{ get; set; }} + + }} +}} +"; + + var fixedTestCode = $@"namespace TestNamespace +{{ + public {typeKeyword} TestType + {{ + {accessModifier}int TestProperty {{ get; set; }} + }} +}} +"; + + var expectedDiagnostic = this.CSharpDiagnostic().WithLocation(7, 5); + + await this.VerifyCSharpDiagnosticAsync(testCode, expectedDiagnostic, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedTestCode).ConfigureAwait(false); + } + + /// + /// Verifies that valid enum declarations will not produce any diagnostics. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestValidEnumDeclarationAsync() + { + var testCode = @"namespace TestNamespace +{ + public enum TestEnum + { + TestValue1, + TestValue2 + } +} +"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Verifies that invalid enum declarations will produce the expected diagnostics. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestInvalidEnumDeclarationAsync() + { + var testCode = @"namespace TestNamespace +{ + public enum TestEnum + { + TestValue1, + TestValue2 + + } +} +"; + + var fixedTestCode = @"namespace TestNamespace +{ + public enum TestEnum + { + TestValue1, + TestValue2 + } +} +"; + + var expectedDiagnostic = this.CSharpDiagnostic().WithLocation(8, 5); + + await this.VerifyCSharpDiagnosticAsync(testCode, expectedDiagnostic, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedTestCode).ConfigureAwait(false); + } + + /// + /// Verifies that valid accessor lists will not produce any diagnostics. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestValidAccessorListAsync() + { + var testCode = @"namespace TestNamespace +{ + public class TestClass + { + public int TestProperty1 { get; set; } + + public int TestProperty2 + { + get; set; + } + } +} +"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Verifies that invalid accessor lists will produce the expected diagnostics. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task TestInvalidAccessorListAsync() + { + var testCode = @"namespace TestNamespace +{ + public class TestClass + { + public int TestProperty + { + get; set; + + } + } +} +"; + + var fixedTestCode = @"namespace TestNamespace +{ + public class TestClass + { + public int TestProperty + { + get; set; + } + } +} +"; + + var expectedDiagnostic = this.CSharpDiagnostic().WithLocation(9, 9); + + await this.VerifyCSharpDiagnosticAsync(testCode, expectedDiagnostic, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedTestCode).ConfigureAwait(false); + } + + /// + protected override IEnumerable GetCSharpDiagnosticAnalyzers() + { + yield return new SA1508ClosingCurlyBracketsMustNotBePrecededByBlankLine(); + } + + /// + protected override CodeFixProvider GetCSharpCodeFixProvider() + { + return new SA1508CodeFixProvider(); + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/StyleCop.Analyzers.Test.csproj b/StyleCop.Analyzers/StyleCop.Analyzers.Test/StyleCop.Analyzers.Test.csproj index abe461107..e741c8e32 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/StyleCop.Analyzers.Test.csproj +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/StyleCop.Analyzers.Test.csproj @@ -196,6 +196,7 @@ + diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/LayoutRules/LayoutResources.Designer.cs b/StyleCop.Analyzers/StyleCop.Analyzers/LayoutRules/LayoutResources.Designer.cs index 7df3d4f6f..4c8460c8b 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/LayoutRules/LayoutResources.Designer.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers/LayoutRules/LayoutResources.Designer.cs @@ -115,6 +115,15 @@ internal static string SA1507CodeFix { } } + /// + /// Looks up a localized string similar to Remove blank lines preceding this curly bracket. + /// + internal static string SA1508CodeFix { + get { + return ResourceManager.GetString("SA1508CodeFix", resourceCulture); + } + } + /// /// Looks up a localized string similar to Remove blank lines preceding this curly bracket. /// diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/LayoutRules/LayoutResources.resx b/StyleCop.Analyzers/StyleCop.Analyzers/LayoutRules/LayoutResources.resx index 8539f390d..847d0062b 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/LayoutRules/LayoutResources.resx +++ b/StyleCop.Analyzers/StyleCop.Analyzers/LayoutRules/LayoutResources.resx @@ -135,6 +135,9 @@ Remove multiple blank lines + + Remove blank lines preceding this curly bracket + Remove blank lines preceding this curly bracket diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/LayoutRules/SA1505CodeFixProvider.cs b/StyleCop.Analyzers/StyleCop.Analyzers/LayoutRules/SA1505CodeFixProvider.cs index ba555c206..e68a4d244 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/LayoutRules/SA1505CodeFixProvider.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers/LayoutRules/SA1505CodeFixProvider.cs @@ -78,25 +78,5 @@ private static async Task GetTransformedDocumentAsync(Document documen var newSyntaxRoot = syntaxRoot.ReplaceTokens(replaceMap.Keys, (t1, t2) => replaceMap[t1]); return document.WithSyntaxRoot(newSyntaxRoot); } - - private class TriviaRemover : CSharpSyntaxRewriter - { - private List triviaToRemove; - - internal TriviaRemover(List triviaToRemove) - { - this.triviaToRemove = triviaToRemove; - } - - public override SyntaxTrivia VisitTrivia(SyntaxTrivia trivia) - { - if (this.triviaToRemove.Contains(trivia)) - { - return default(SyntaxTrivia); - } - - return base.VisitTrivia(trivia); - } - } } } diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/LayoutRules/SA1508ClosingCurlyBracketsMustNotBePrecededByBlankLine.cs b/StyleCop.Analyzers/StyleCop.Analyzers/LayoutRules/SA1508ClosingCurlyBracketsMustNotBePrecededByBlankLine.cs index 9203ef0d6..c452f9c7b 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/LayoutRules/SA1508ClosingCurlyBracketsMustNotBePrecededByBlankLine.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers/LayoutRules/SA1508ClosingCurlyBracketsMustNotBePrecededByBlankLine.cs @@ -2,6 +2,8 @@ { using System.Collections.Immutable; using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; /// @@ -39,30 +41,120 @@ public class SA1508ClosingCurlyBracketsMustNotBePrecededByBlankLine : Diagnostic /// public const string DiagnosticId = "SA1508"; private const string Title = "Closing curly brackets must not be preceded by blank line"; - private const string MessageFormat = "TODO: Message format"; + private const string MessageFormat = "A closing curly bracket must not be preceded by a blank line."; private const string Category = "StyleCop.CSharp.LayoutRules"; private const string Description = "A closing curly bracket within a C# element, statement, or expression is preceded by a blank line."; private const string HelpLink = "http://www.stylecop.com/docs/SA1508.html"; private static readonly DiagnosticDescriptor Descriptor = - new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, AnalyzerConstants.DisabledNoTests, Description, HelpLink); + new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, AnalyzerConstants.EnabledByDefault, Description, HelpLink); private static readonly ImmutableArray SupportedDiagnosticsValue = ImmutableArray.Create(Descriptor); /// - public override ImmutableArray SupportedDiagnostics + public override ImmutableArray SupportedDiagnostics => SupportedDiagnosticsValue; + + /// + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeActionHonorExclusions(this.HandleBlock, SyntaxKind.Block); + context.RegisterSyntaxNodeActionHonorExclusions(this.HandleInitializers, SyntaxKind.ObjectInitializerExpression, SyntaxKind.CollectionInitializerExpression, SyntaxKind.ArrayInitializerExpression, SyntaxKind.ComplexElementInitializerExpression); + context.RegisterSyntaxNodeActionHonorExclusions(this.HandleAnonymousObjectCreation, SyntaxKind.AnonymousObjectCreationExpression); + context.RegisterSyntaxNodeActionHonorExclusions(this.HandleSwitchStatement, SyntaxKind.SwitchStatement); + context.RegisterSyntaxNodeActionHonorExclusions(this.HandleNamespaceDeclaration, SyntaxKind.NamespaceDeclaration); + context.RegisterSyntaxNodeActionHonorExclusions(this.HandleTypeDeclaration, SyntaxKind.ClassDeclaration, SyntaxKind.StructDeclaration, SyntaxKind.InterfaceDeclaration, SyntaxKind.EnumDeclaration); + context.RegisterSyntaxNodeActionHonorExclusions(this.HandleAccessorList, SyntaxKind.AccessorList); + } + + private void HandleBlock(SyntaxNodeAnalysisContext context) + { + var block = (BlockSyntax)context.Node; + this.AnalyzeCloseBrace(context, block.CloseBraceToken); + } + + private void HandleInitializers(SyntaxNodeAnalysisContext context) + { + var expression = (InitializerExpressionSyntax)context.Node; + this.AnalyzeCloseBrace(context, expression.CloseBraceToken); + } + + private void HandleAnonymousObjectCreation(SyntaxNodeAnalysisContext context) + { + var expression = (AnonymousObjectCreationExpressionSyntax)context.Node; + this.AnalyzeCloseBrace(context, expression.CloseBraceToken); + } + + private void HandleSwitchStatement(SyntaxNodeAnalysisContext context) + { + var switchStatement = (SwitchStatementSyntax)context.Node; + this.AnalyzeCloseBrace(context, switchStatement.CloseBraceToken); + } + + private void HandleNamespaceDeclaration(SyntaxNodeAnalysisContext context) + { + var namespaceDeclaration = (NamespaceDeclarationSyntax)context.Node; + this.AnalyzeCloseBrace(context, namespaceDeclaration.CloseBraceToken); + } + + private void HandleTypeDeclaration(SyntaxNodeAnalysisContext context) + { + var typeDeclaration = (BaseTypeDeclarationSyntax)context.Node; + this.AnalyzeCloseBrace(context, typeDeclaration.CloseBraceToken); + } + + private void HandleAccessorList(SyntaxNodeAnalysisContext context) + { + var accessorList = (AccessorListSyntax)context.Node; + this.AnalyzeCloseBrace(context, accessorList.CloseBraceToken); + } + + private void AnalyzeCloseBrace(SyntaxNodeAnalysisContext context, SyntaxToken closeBraceToken) { - get + var previousToken = closeBraceToken.GetPreviousToken(); + if ((GetLine(closeBraceToken) - GetLine(previousToken)) < 2) + { + // there will be no blank lines when the closing brace and the preceding token are not at least two lines apart. + return; + } + + var separatingTrivia = previousToken.TrailingTrivia.AddRange(closeBraceToken.LeadingTrivia); + + // skip all leading whitespace for the close brace + var index = separatingTrivia.Count - 1; + while (separatingTrivia[index].IsKind(SyntaxKind.WhitespaceTrivia)) + { + index--; + } + + var done = false; + var eolCount = 0; + while (!done && index >= 0) + { + switch (separatingTrivia[index].Kind()) + { + case SyntaxKind.WhitespaceTrivia: + break; + case SyntaxKind.EndOfLineTrivia: + eolCount++; + break; + default: + done = true; + break; + } + + index--; + } + + if (eolCount > 1) { - return SupportedDiagnosticsValue; + context.ReportDiagnostic(Diagnostic.Create(Descriptor, closeBraceToken.GetLocation())); } } - /// - public override void Initialize(AnalysisContext context) + private static int GetLine(SyntaxToken token) { - // TODO: Implement analysis + return token.GetLocation().GetLineSpan().StartLinePosition.Line; } } } diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/LayoutRules/SA1508CodeFixProvider.cs b/StyleCop.Analyzers/StyleCop.Analyzers/LayoutRules/SA1508CodeFixProvider.cs new file mode 100644 index 000000000..98eba5524 --- /dev/null +++ b/StyleCop.Analyzers/StyleCop.Analyzers/LayoutRules/SA1508CodeFixProvider.cs @@ -0,0 +1,91 @@ +namespace StyleCop.Analyzers.LayoutRules +{ + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Composition; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CodeActions; + using Microsoft.CodeAnalysis.CodeFixes; + using Microsoft.CodeAnalysis.CSharp; + + /// + /// Implements a code fix for . + /// + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SA1508CodeFixProvider))] + [Shared] + public class SA1508CodeFixProvider : CodeFixProvider + { + private static readonly ImmutableArray FixableDiagnostics = + ImmutableArray.Create(SA1508ClosingCurlyBracketsMustNotBePrecededByBlankLine.DiagnosticId); + + /// + public override ImmutableArray FixableDiagnosticIds => FixableDiagnostics; + + /// + public override FixAllProvider GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + /// + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + foreach (var diagnostic in context.Diagnostics.Where(d => FixableDiagnostics.Contains(d.Id))) + { + context.RegisterCodeFix(CodeAction.Create(LayoutResources.SA1508CodeFix, token => GetTransformedDocumentAsync(context.Document, diagnostic, token)), diagnostic); + } + + return Task.FromResult(true); + } + + private static async Task GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + { + var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + var closeBraceToken = syntaxRoot.FindToken(diagnostic.Location.SourceSpan.Start); + var previousToken = closeBraceToken.GetPreviousToken(); + + var triviaList = previousToken.TrailingTrivia.AddRange(closeBraceToken.LeadingTrivia); + + // skip all leading whitespace for the close brace + var index = triviaList.Count - 1; + while (triviaList[index].IsKind(SyntaxKind.WhitespaceTrivia)) + { + index--; + } + + var firstLeadingWhitespace = index + 1; + + var done = false; + var lastEndOfLineIndex = -1; + while (!done && index >= 0) + { + switch (triviaList[index].Kind()) + { + case SyntaxKind.WhitespaceTrivia: + break; + case SyntaxKind.EndOfLineTrivia: + lastEndOfLineIndex = index; + break; + default: + done = true; + break; + } + + index--; + } + + var replaceMap = new Dictionary() + { + [previousToken] = previousToken.WithTrailingTrivia(triviaList.Take(lastEndOfLineIndex + 1)), + [closeBraceToken] = closeBraceToken.WithLeadingTrivia(triviaList.Skip(firstLeadingWhitespace)) + }; + + var newSyntaxRoot = syntaxRoot.ReplaceTokens(replaceMap.Keys, (t1, t2) => replaceMap[t1]); + return document.WithSyntaxRoot(newSyntaxRoot); + } + } +} diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/StyleCop.Analyzers.csproj b/StyleCop.Analyzers/StyleCop.Analyzers/StyleCop.Analyzers.csproj index bf91c451c..ae0d22808 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/StyleCop.Analyzers.csproj +++ b/StyleCop.Analyzers/StyleCop.Analyzers/StyleCop.Analyzers.csproj @@ -145,6 +145,7 @@ +