From bf90ccd11593522885c9cddccdd264c07748fea6 Mon Sep 17 00:00:00 2001 From: Josef Pihrt Date: Mon, 23 Dec 2024 11:18:25 +0100 Subject: [PATCH 1/4] impl --- .../WhitespaceTriviaCodeFixProvider.cs | 4 +- src/Analyzers.xml | 41 +- src/Analyzers/Analysis/WhitespaceAnalyzer.cs | 4 +- .../AnalyzerOptionIsObsoleteAnalyzer.cs | 2 +- .../RemoveUnnecessaryBlankLineAnalyzer.cs | 659 +---------------- .../Analysis/UnnecessaryBlankLineAnalysis.cs | 662 ++++++++++++++++++ src/Common/DiagnosticIdentifiers.Generated.cs | 3 +- src/Common/DiagnosticRules.Generated.cs | 22 +- .../RemoveUnnecessaryBlankLineFixProvider.cs | 44 ++ .../RemoveUnnecessaryBlankLineAnalyzer.cs | 103 +++ .../RCS1036RemoveUnnecessaryBlankLineTests.cs | 48 +- .../RCS0063RemoveUnnecessaryBlankLineTests.cs | 515 ++++++++++++++ .../src/configurationFiles.generated.ts | 10 +- 13 files changed, 1422 insertions(+), 695 deletions(-) create mode 100644 src/Common/CSharp/Analysis/UnnecessaryBlankLineAnalysis.cs create mode 100644 src/Formatting.Analyzers.CodeFixes/CSharp/RemoveUnnecessaryBlankLineFixProvider.cs create mode 100644 src/Formatting.Analyzers/CSharp/RemoveUnnecessaryBlankLineAnalyzer.cs create mode 100644 src/Tests/Formatting.Analyzers.Tests/RCS0063RemoveUnnecessaryBlankLineTests.cs diff --git a/src/Analyzers.CodeFixes/CodeFixes/WhitespaceTriviaCodeFixProvider.cs b/src/Analyzers.CodeFixes/CodeFixes/WhitespaceTriviaCodeFixProvider.cs index b5f8c0fc9d..ebe46946c2 100644 --- a/src/Analyzers.CodeFixes/CodeFixes/WhitespaceTriviaCodeFixProvider.cs +++ b/src/Analyzers.CodeFixes/CodeFixes/WhitespaceTriviaCodeFixProvider.cs @@ -23,7 +23,7 @@ public override ImmutableArray FixableDiagnosticIds { return ImmutableArray.Create( DiagnosticIdentifiers.RemoveTrailingWhitespace, - DiagnosticIdentifiers.RemoveUnnecessaryBlankLine); + DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine); } } @@ -54,7 +54,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) context.RegisterCodeFix(codeAction, diagnostic); break; } - case DiagnosticIdentifiers.RemoveUnnecessaryBlankLine: + case DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine: { CodeAction codeAction = CodeAction.Create( "Remove blank line", diff --git a/src/Analyzers.xml b/src/Analyzers.xml index 4e010d95f7..e8b7fee8bd 100644 --- a/src/Analyzers.xml +++ b/src/Analyzers.xml @@ -1629,6 +1629,43 @@ public class C + + RCS0063 + RemoveUnnecessaryBlankLine + Remove unnecessary blank line + Info + false + + + + + + + + + RCS1001 AddBracesWhenExpressionSpansOverMultipleLines @@ -2470,8 +2507,10 @@ if (f) RCS1036 - RemoveUnnecessaryBlankLine + Obsolete_RemoveUnnecessaryBlankLine Remove unnecessary blank line + Obsolete + Use RCS0063 instead Info true diff --git a/src/Analyzers/Analysis/WhitespaceAnalyzer.cs b/src/Analyzers/Analysis/WhitespaceAnalyzer.cs index b5a4275407..c56c8bc9f6 100644 --- a/src/Analyzers/Analysis/WhitespaceAnalyzer.cs +++ b/src/Analyzers/Analysis/WhitespaceAnalyzer.cs @@ -22,7 +22,7 @@ public override ImmutableArray SupportedDiagnostics Immutable.InterlockedInitialize( ref _supportedDiagnostics, DiagnosticRules.RemoveTrailingWhitespace, - DiagnosticRules.RemoveUnnecessaryBlankLine); + DiagnosticRules.Obsolete_RemoveUnnecessaryBlankLine); } return _supportedDiagnostics; @@ -83,7 +83,7 @@ private static void AnalyzeTrailingTrivia(SyntaxTreeAnalysisContext context) { DiagnosticHelpers.ReportDiagnostic( context, - DiagnosticRules.RemoveUnnecessaryBlankLine, + DiagnosticRules.Obsolete_RemoveUnnecessaryBlankLine, Location.Create(context.Tree, emptyLines)); } diff --git a/src/Analyzers/CSharp/Analysis/AnalyzerOptionIsObsoleteAnalyzer.cs b/src/Analyzers/CSharp/Analysis/AnalyzerOptionIsObsoleteAnalyzer.cs index daf2669f50..2085ff1e0f 100644 --- a/src/Analyzers/CSharp/Analysis/AnalyzerOptionIsObsoleteAnalyzer.cs +++ b/src/Analyzers/CSharp/Analysis/AnalyzerOptionIsObsoleteAnalyzer.cs @@ -49,7 +49,7 @@ public override void Initialize(AnalysisContext context) Validate(ref context, compilationOptions, options, Flags.ConvertMethodGroupToAnonymousFunction, ref flags, DiagnosticRules.UseAnonymousFunctionOrMethodGroup, ConfigOptions.UseAnonymousFunctionOrMethodGroup, LegacyConfigOptions.ConvertMethodGroupToAnonymousFunction, ConfigOptionValues.UseAnonymousFunctionOrMethodGroup_AnonymousFunction); Validate(ref context, compilationOptions, options, Flags.RemoveCallToConfigureAwait, ref flags, DiagnosticRules.ConfigureAwait, ConfigOptions.ConfigureAwait, LegacyConfigOptions.RemoveCallToConfigureAwait, "false"); Validate(ref context, compilationOptions, options, Flags.RemoveAccessibilityModifiers, ref flags, DiagnosticRules.AddOrRemoveAccessibilityModifiers, ConfigOptions.AccessibilityModifiers, LegacyConfigOptions.RemoveAccessibilityModifiers, ConfigOptionValues.AccessibilityModifiers_Implicit); - Validate(ref context, compilationOptions, options, Flags.RemoveEmptyLineBetweenClosingBraceAndSwitchSection, ref flags, DiagnosticRules.RemoveUnnecessaryBlankLine, ConfigOptions.BlankLineBetweenClosingBraceAndSwitchSection, LegacyConfigOptions.RemoveEmptyLineBetweenClosingBraceAndSwitchSection, "false"); + Validate(ref context, compilationOptions, options, Flags.RemoveEmptyLineBetweenClosingBraceAndSwitchSection, ref flags, DiagnosticRules.Obsolete_RemoveUnnecessaryBlankLine, ConfigOptions.BlankLineBetweenClosingBraceAndSwitchSection, LegacyConfigOptions.RemoveEmptyLineBetweenClosingBraceAndSwitchSection, "false"); Validate(ref context, compilationOptions, options, Flags.RemoveParenthesesFromConditionOfConditionalExpressionWhenExpressionIsSingleToken, ref flags, DiagnosticRules.AddOrRemoveParenthesesFromConditionInConditionalOperator, ConfigOptions.ConditionalOperatorConditionParenthesesStyle, LegacyConfigOptions.RemoveParenthesesFromConditionOfConditionalExpressionWhenExpressionIsSingleToken, ConfigOptionValues.ConditionalOperatorConditionParenthesesStyle_OmitWhenConditionIsSingleToken); Validate(ref context, compilationOptions, options, Flags.RemoveParenthesesWhenCreatingNewObject, ref flags, DiagnosticRules.UseImplicitOrExplicitObjectCreation, ConfigOptions.ObjectCreationParenthesesStyle, LegacyConfigOptions.RemoveParenthesesWhenCreatingNewObject, ConfigOptionValues.ObjectCreationParenthesesStyle_Omit); #pragma warning disable CS0618 // Type or member is obsolete diff --git a/src/Analyzers/CSharp/Analysis/RemoveUnnecessaryBlankLineAnalyzer.cs b/src/Analyzers/CSharp/Analysis/RemoveUnnecessaryBlankLineAnalyzer.cs index 19a17032bc..1a88d0689a 100644 --- a/src/Analyzers/CSharp/Analysis/RemoveUnnecessaryBlankLineAnalyzer.cs +++ b/src/Analyzers/CSharp/Analysis/RemoveUnnecessaryBlankLineAnalyzer.cs @@ -1,12 +1,8 @@ // Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Immutable; -using System.Diagnostics; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Text; namespace Roslynator.CSharp.Analysis; @@ -21,7 +17,7 @@ public override ImmutableArray SupportedDiagnostics { if (_supportedDiagnostics.IsDefault) { - Immutable.InterlockedInitialize(ref _supportedDiagnostics, DiagnosticRules.RemoveUnnecessaryBlankLine); + Immutable.InterlockedInitialize(ref _supportedDiagnostics, DiagnosticRules.Obsolete_RemoveUnnecessaryBlankLine); } return _supportedDiagnostics; @@ -32,658 +28,13 @@ public override void Initialize(AnalysisContext context) { base.Initialize(context); - context.RegisterSyntaxNodeAction(f => AnalyzeClassDeclaration(f), SyntaxKind.ClassDeclaration); - context.RegisterSyntaxNodeAction(f => AnalyzeStructDeclaration(f), SyntaxKind.StructDeclaration); -#if ROSLYN_4_0 - context.RegisterSyntaxNodeAction(f => AnalyzeRecordDeclaration(f), SyntaxKind.RecordDeclaration, SyntaxKind.RecordStructDeclaration); -#else - context.RegisterSyntaxNodeAction(f => AnalyzeRecordDeclaration(f), SyntaxKind.RecordDeclaration); -#endif - context.RegisterSyntaxNodeAction(f => AnalyzeInterfaceDeclaration(f), SyntaxKind.InterfaceDeclaration); - context.RegisterSyntaxNodeAction(f => AnalyzeEnumDeclaration(f), SyntaxKind.EnumDeclaration); - context.RegisterSyntaxNodeAction(f => AnalyzeNamespaceDeclaration(f), SyntaxKind.NamespaceDeclaration); - context.RegisterSyntaxNodeAction(f => AnalyzeTryStatement(f), SyntaxKind.TryStatement); - context.RegisterSyntaxNodeAction(f => AnalyzeElseClause(f), SyntaxKind.ElseClause); - - context.RegisterSyntaxNodeAction(f => AnalyzeIfStatement(f), SyntaxKind.IfStatement); - context.RegisterSyntaxNodeAction(f => AnalyzeCommonForEachStatement(f), SyntaxKind.ForEachStatement); - context.RegisterSyntaxNodeAction(f => AnalyzeCommonForEachStatement(f), SyntaxKind.ForEachVariableStatement); - context.RegisterSyntaxNodeAction(f => AnalyzeForStatement(f), SyntaxKind.ForStatement); - context.RegisterSyntaxNodeAction(f => AnalyzeUsingStatement(f), SyntaxKind.UsingStatement); - context.RegisterSyntaxNodeAction(f => AnalyzeWhileStatement(f), SyntaxKind.WhileStatement); - context.RegisterSyntaxNodeAction(f => AnalyzeDoStatement(f), SyntaxKind.DoStatement); - context.RegisterSyntaxNodeAction(f => AnalyzeLockStatement(f), SyntaxKind.LockStatement); - context.RegisterSyntaxNodeAction(f => AnalyzeFixedStatement(f), SyntaxKind.FixedStatement); - - context.RegisterSyntaxNodeAction(f => AnalyzeAccessorList(f), SyntaxKind.AccessorList); - context.RegisterSyntaxNodeAction(f => AnalyzeBlock(f), SyntaxKind.Block); - context.RegisterSyntaxNodeAction(f => AnalyzeSingleLineDocumentationCommentTrivia(f), SyntaxKind.SingleLineDocumentationCommentTrivia); - - context.RegisterSyntaxNodeAction(f => AnalyzeInitializer(f), SyntaxKind.ArrayInitializerExpression); - context.RegisterSyntaxNodeAction(f => AnalyzeInitializer(f), SyntaxKind.CollectionInitializerExpression); - context.RegisterSyntaxNodeAction(f => AnalyzeInitializer(f), SyntaxKind.ObjectInitializerExpression); - - context.RegisterSyntaxNodeAction(f => AnalyzeCompilationUnit(f), SyntaxKind.CompilationUnit); - } - - private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) - { - var classDeclaration = (ClassDeclarationSyntax)context.Node; - - AnalyzeDeclaration( - context, - classDeclaration.Members, - classDeclaration.OpenBraceToken, - classDeclaration.CloseBraceToken); - } - - private static void AnalyzeStructDeclaration(SyntaxNodeAnalysisContext context) - { - var structDeclaration = (StructDeclarationSyntax)context.Node; - - AnalyzeDeclaration( - context, - structDeclaration.Members, - structDeclaration.OpenBraceToken, - structDeclaration.CloseBraceToken); - } - - private static void AnalyzeRecordDeclaration(SyntaxNodeAnalysisContext context) - { - var recordDeclaration = (RecordDeclarationSyntax)context.Node; - - AnalyzeDeclaration( - context, - recordDeclaration.Members, - recordDeclaration.OpenBraceToken, - recordDeclaration.CloseBraceToken); - } - - private static void AnalyzeInterfaceDeclaration(SyntaxNodeAnalysisContext context) - { - var interfaceDeclaration = (InterfaceDeclarationSyntax)context.Node; - - AnalyzeDeclaration( - context, - interfaceDeclaration.Members, - interfaceDeclaration.OpenBraceToken, - interfaceDeclaration.CloseBraceToken); - } - - private static void AnalyzeEnumDeclaration(SyntaxNodeAnalysisContext context) - { - var enumDeclaration = (EnumDeclarationSyntax)context.Node; - - AnalyzeDeclaration( - context, - enumDeclaration.Members, - enumDeclaration.OpenBraceToken, - enumDeclaration.CloseBraceToken); - } - - private static void AnalyzeNamespaceDeclaration(SyntaxNodeAnalysisContext context) - { - var namespaceDeclaration = (NamespaceDeclarationSyntax)context.Node; - - SyntaxList members = namespaceDeclaration.Members; - SyntaxList externs = namespaceDeclaration.Externs; - - if (externs.Any()) - { - AnalyzeStart(context, externs[0], namespaceDeclaration.OpenBraceToken); - } - else - { - SyntaxList usings = namespaceDeclaration.Usings; - - if (usings.Any()) - { - AnalyzeStart(context, usings[0], namespaceDeclaration.OpenBraceToken); - } - else if (members.Any()) - { - AnalyzeStart(context, members[0], namespaceDeclaration.OpenBraceToken); - } - } - - if (members.Any()) - AnalyzeEnd(context, members.Last(), namespaceDeclaration.CloseBraceToken); - } - - private static void AnalyzeTryStatement(SyntaxNodeAnalysisContext context) - { - var tryStatement = (TryStatementSyntax)context.Node; - - BlockSyntax block = tryStatement.Block; - - if (block is not null) - { - SyntaxList catches = tryStatement.Catches; - - if (catches.Any()) - { - SyntaxNode previousNode = block; - - foreach (CatchClauseSyntax catchClause in catches) - { - Analyze(context, previousNode, catchClause); - - previousNode = catchClause; - } - - FinallyClauseSyntax finallyClause = tryStatement.Finally; - - if (finallyClause is not null) - Analyze(context, previousNode, finallyClause); - } - } - } - - private static void AnalyzeElseClause(SyntaxNodeAnalysisContext context) - { - var elseClause = (ElseClauseSyntax)context.Node; - - SyntaxNode parent = elseClause.Parent; - - if (parent is IfStatementSyntax ifStatement) - { - StatementSyntax statement = ifStatement.Statement; - - if (statement is not null) - Analyze(context, statement, elseClause); - - statement = elseClause.Statement; - - if (statement is not null) - Analyze(context, elseClause.ElseKeyword, statement); - } - } - - private static void AnalyzeIfStatement(SyntaxNodeAnalysisContext context) - { - var ifStatement = (IfStatementSyntax)context.Node; - - AnalyzeEmbeddedStatement(context, ifStatement.CloseParenToken, ifStatement.Statement); - } - - private static void AnalyzeCommonForEachStatement(SyntaxNodeAnalysisContext context) - { - var forEachStatement = (CommonForEachStatementSyntax)context.Node; - - AnalyzeEmbeddedStatement(context, forEachStatement.CloseParenToken, forEachStatement.Statement); - } - - private static void AnalyzeForStatement(SyntaxNodeAnalysisContext context) - { - var forStatement = (ForStatementSyntax)context.Node; - - AnalyzeEmbeddedStatement(context, forStatement.CloseParenToken, forStatement.Statement); - } - - private static void AnalyzeUsingStatement(SyntaxNodeAnalysisContext context) - { - var usingStatement = (UsingStatementSyntax)context.Node; - - AnalyzeEmbeddedStatement(context, usingStatement.CloseParenToken, usingStatement.Statement); - } - - private static void AnalyzeWhileStatement(SyntaxNodeAnalysisContext context) - { - var whileStatement = (WhileStatementSyntax)context.Node; - - AnalyzeEmbeddedStatement(context, whileStatement.CloseParenToken, whileStatement.Statement); - } - - private static void AnalyzeDoStatement(SyntaxNodeAnalysisContext context) - { - var doStatement = (DoStatementSyntax)context.Node; - - AnalyzeEmbeddedStatement(context, doStatement.DoKeyword, doStatement.Statement); - } - - private static void AnalyzeLockStatement(SyntaxNodeAnalysisContext context) - { - var lockStatement = (LockStatementSyntax)context.Node; - - AnalyzeEmbeddedStatement(context, lockStatement.CloseParenToken, lockStatement.Statement); - } - - private static void AnalyzeFixedStatement(SyntaxNodeAnalysisContext context) - { - var fixedStatement = (FixedStatementSyntax)context.Node; - - AnalyzeEmbeddedStatement(context, fixedStatement.CloseParenToken, fixedStatement.Statement); - } - - private static void AnalyzeEmbeddedStatement(SyntaxNodeAnalysisContext context, SyntaxToken token, StatementSyntax statement) - { - if (statement?.IsEmbedded() == true) - Analyze(context, token, statement); - } - - private static void Analyze( - SyntaxNodeAnalysisContext context, - SyntaxNode node1, - SyntaxNode node2) - { - SyntaxTriviaList trailingTrivia = node1.GetTrailingTrivia(); - SyntaxTriviaList leadingTrivia = node2.GetLeadingTrivia(); - - if (IsStandardTriviaBetweenLines(trailingTrivia, leadingTrivia)) - return; - - if (node1 - .SyntaxTree - .GetLineSpan(TextSpan.FromBounds(node1.Span.End, node2.SpanStart), context.CancellationToken) - .GetLineCount() != 3) - { - return; - } - - var trivia = default(SyntaxTrivia); - - foreach (SyntaxTrivia t in leadingTrivia) - { - if (!t.IsWhitespaceTrivia()) - { - trivia = t; - break; - } - } - - if (!trivia.IsEndOfLineTrivia()) - return; - - if (!trailingTrivia.IsEmptyOrWhitespace()) - return; - - if (!leadingTrivia.IsEmptyOrWhitespace()) - return; - - DiagnosticHelpers.ReportDiagnostic( - context, - DiagnosticRules.RemoveUnnecessaryBlankLine, - Location.Create(node1.SyntaxTree, TextSpan.FromBounds(node2.FullSpan.Start, trivia.Span.End))); - } - - private static void Analyze( - SyntaxNodeAnalysisContext context, - SyntaxToken token, - SyntaxNode node) - { - SyntaxTriviaList trailingTrivia = token.TrailingTrivia; - SyntaxTriviaList leadingTrivia = node.GetLeadingTrivia(); - - if (IsStandardTriviaBetweenLines(trailingTrivia, leadingTrivia)) - return; - - if (token - .SyntaxTree - .GetLineSpan(TextSpan.FromBounds(token.Span.End, node.SpanStart), context.CancellationToken) - .GetLineCount() != 3) - { - return; - } - - var trivia = default(SyntaxTrivia); - - foreach (SyntaxTrivia t in leadingTrivia) - { - if (!t.IsWhitespaceTrivia()) - { - trivia = t; - break; - } - } - - if (!trivia.IsEndOfLineTrivia()) - return; - - if (!trailingTrivia.IsEmptyOrWhitespace()) - return; - - if (!leadingTrivia.IsEmptyOrWhitespace()) - return; - - Location location = Location.Create(token.SyntaxTree, TextSpan.FromBounds(node.FullSpan.Start, trivia.Span.End)); - - DiagnosticHelpers.ReportDiagnostic(context, DiagnosticRules.RemoveUnnecessaryBlankLine, location); - } - - private static void AnalyzeDeclaration( - SyntaxNodeAnalysisContext context, - SyntaxList members, - SyntaxToken openBrace, - SyntaxToken closeBrace) where TMember : MemberDeclarationSyntax - { - if (members.Any()) - { - AnalyzeStart(context, members[0], openBrace); - AnalyzeEnd(context, members.Last(), closeBrace); - } - else - { - AnalyzeEmptyBraces(context, openBrace, closeBrace); - } - } - - private static void AnalyzeDeclaration( - SyntaxNodeAnalysisContext context, - SeparatedSyntaxList members, - SyntaxToken openBrace, - SyntaxToken closeBrace) where TMember : MemberDeclarationSyntax - { - if (members.Any()) - { - AnalyzeStart(context, members[0], openBrace); - - int count = members.SeparatorCount; - - SyntaxNodeOrToken nodeOrToken = (count == members.Count) - ? members.GetSeparator(count - 1) - : members.Last(); - - AnalyzeEnd(context, nodeOrToken, closeBrace); - } - else - { - AnalyzeEmptyBraces(context, openBrace, closeBrace); - } - } - - private static void AnalyzeBlock(SyntaxNodeAnalysisContext context) - { - var block = (BlockSyntax)context.Node; - - SyntaxList statements = block.Statements; - - if (statements.Any()) - { - AnalyzeStart(context, statements[0], block.OpenBraceToken); - AnalyzeEnd(context, statements.Last(), block.CloseBraceToken); - } - else - { - AnalyzeEmptyBraces(context, block.OpenBraceToken, block.CloseBraceToken); - } - } - - private static void AnalyzeSingleLineDocumentationCommentTrivia(SyntaxNodeAnalysisContext context) - { - var comment = (DocumentationCommentTriviaSyntax)context.Node; - - if (comment is IStructuredTriviaSyntax structuredTrivia) - { - SyntaxTrivia trivia = structuredTrivia.ParentTrivia; - SyntaxTriviaList leadingTrivia = trivia.Token.LeadingTrivia; - - int index = leadingTrivia.IndexOf(trivia); - - if (index >= 0 - && index < leadingTrivia.Count - 1 - && leadingTrivia[index + 1].IsEndOfLineTrivia()) - { - DiagnosticHelpers.ReportDiagnostic( - context, - DiagnosticRules.RemoveUnnecessaryBlankLine, - leadingTrivia[index + 1].GetLocation()); - } - } - } - - private static void AnalyzeInitializer(SyntaxNodeAnalysisContext context) - { - var initializer = (InitializerExpressionSyntax)context.Node; - - SeparatedSyntaxList expressions = initializer.Expressions; - - ExpressionSyntax first = expressions.FirstOrDefault(); - - if (first is null) - return; - - if (IsExpectedTrailingTrivia(initializer.OpenBraceToken.TrailingTrivia)) - { - SyntaxTriviaList leading = first.GetLeadingTrivia(); - - if (leading.Any()) - { - TextSpan? span = GetEmptyLineSpan(leading, isEnd: false); - - if (span is not null) - ReportDiagnostic(context, span.Value); - } - } - - if (IsExpectedTrailingTrivia(expressions.GetTrailingTrivia())) - { - SyntaxTriviaList leading = initializer.CloseBraceToken.LeadingTrivia; - - if (leading.Any()) - { - TextSpan? span = GetEmptyLineSpan(leading, isEnd: true); - - if (span is not null) - ReportDiagnostic(context, span.Value); - } - } - - static bool IsExpectedTrailingTrivia(SyntaxTriviaList triviaList) - { - foreach (SyntaxTrivia trivia in triviaList) - { - switch (trivia.Kind()) - { - case SyntaxKind.WhitespaceTrivia: - break; - case SyntaxKind.EndOfLineTrivia: - return true; - default: - return false; - } - } - - return false; - } + UnnecessaryBlankLineAnalysis.Instance.Initialize(context); } - private static void AnalyzeAccessorList(SyntaxNodeAnalysisContext context) + private class UnnecessaryBlankLineAnalysis : Roslynator.CSharp.Analysis.UnnecessaryBlankLineAnalysis { - var accessorList = (AccessorListSyntax)context.Node; + public static UnnecessaryBlankLineAnalysis Instance { get; } = new(); - SyntaxList accessors = accessorList.Accessors; - - if (accessors.Any()) - { - AnalyzeStart(context, accessors[0], accessorList.OpenBraceToken); - AnalyzeEnd(context, accessors.Last(), accessorList.CloseBraceToken); - } - } - - private static void AnalyzeCompilationUnit(SyntaxNodeAnalysisContext context) - { - var compilationUnit = (CompilationUnitSyntax)context.Node; - - SyntaxTriviaList leading = compilationUnit.EndOfFileToken.LeadingTrivia; - - SyntaxTriviaList.Reversed.Enumerator en = leading.Reverse().GetEnumerator(); - - if (en.MoveNext() - && en.Current.IsEndOfLineTrivia()) - { - int start = en.Current.SpanStart; - - if (en.MoveNext()) - { - if (!en.Current.IsWhitespaceTrivia() - || en.MoveNext()) - { - if (CSharpFacts.IsCommentTrivia(en.Current.Kind()) - || SyntaxFacts.IsPreprocessorDirective(en.Current.Kind())) - { - return; - } - - do - { - Debug.Assert(en.Current.IsWhitespaceOrEndOfLineTrivia(), en.Current.Kind().ToString()); - - if (en.Current.IsEndOfLineTrivia()) - { - start = en.Current.SpanStart; - } - else - { - break; - } - } - while (en.MoveNext()); - } - } - - ReportDiagnostic(context, TextSpan.FromBounds(start, leading.Span.End)); - } - } - - private static void AnalyzeStart( - SyntaxNodeAnalysisContext context, - SyntaxNode node, - SyntaxToken brace) - { - if (brace.IsMissing) - return; - - if ((node.GetSpanStartLine() - brace.GetSpanEndLine()) <= 1) - return; - - TextSpan? span = GetEmptyLineSpan(node.GetLeadingTrivia(), isEnd: false); - - if (span is null) - return; - - DiagnosticHelpers.ReportDiagnostic( - context, - DiagnosticRules.RemoveUnnecessaryBlankLine, - Location.Create(context.Node.SyntaxTree, span.Value)); - } - - private static void AnalyzeEnd( - SyntaxNodeAnalysisContext context, - SyntaxNodeOrToken nodeOrToken, - SyntaxToken brace) - { - if (brace.IsMissing) - return; - - int braceLine = brace.GetSpanStartLine(); - int nodeOrTokenLine = nodeOrToken.SyntaxTree.GetLineSpan(nodeOrToken.Span).EndLine(); - - if (braceLine - nodeOrTokenLine <= 1) - return; - - TextSpan? span = GetEmptyLineSpan(brace.LeadingTrivia, isEnd: true); - - if (span is null) - return; - - DiagnosticHelpers.ReportDiagnostic( - context, - DiagnosticRules.RemoveUnnecessaryBlankLine, - Location.Create(context.Node.SyntaxTree, span.Value)); - } - - private static void AnalyzeEmptyBraces( - SyntaxNodeAnalysisContext context, - SyntaxToken openBrace, - SyntaxToken closeBrace) - { - if (openBrace.IsMissing) - return; - - if (closeBrace.IsMissing) - return; - - SyntaxTree tree = context.Node.SyntaxTree; - - if (tree.GetLineCount(TextSpan.FromBounds(openBrace.SpanStart, closeBrace.Span.End)) <= 2) - return; - - TextSpan? span = GetEmptyLineSpan(closeBrace.LeadingTrivia, isEnd: true); - - if (span is null) - return; - - DiagnosticHelpers.ReportDiagnostic( - context, - DiagnosticRules.RemoveUnnecessaryBlankLine, - Location.Create(tree, span.Value)); - } - - private static TextSpan? GetEmptyLineSpan( - SyntaxTriviaList triviaList, - bool isEnd) - { - SyntaxTriviaList.Enumerator en = triviaList.GetEnumerator(); - while (en.MoveNext()) - { - switch (en.Current.Kind()) - { - case SyntaxKind.EndOfLineTrivia: - { - SyntaxTrivia endOfLine = en.Current; - - if (isEnd) - { - while (en.MoveNext()) - { - if (!en.Current.IsWhitespaceOrEndOfLineTrivia()) - return null; - } - } - - return TextSpan.FromBounds(triviaList.Span.Start, endOfLine.Span.End); - } - case SyntaxKind.WhitespaceTrivia: - { - break; - } - default: - { - return null; - } - } - } - - return null; - } - - private static bool IsStandardTriviaBetweenLines(SyntaxTriviaList trailingTrivia, SyntaxTriviaList leadingTrivia) - { - if (leadingTrivia.Any() - && leadingTrivia.All(f => f.IsWhitespaceTrivia())) - { - SyntaxTriviaList.Enumerator en = trailingTrivia.GetEnumerator(); - - while (en.MoveNext()) - { - SyntaxKind kind = en.Current.Kind(); - - if (kind == SyntaxKind.WhitespaceTrivia) - continue; - - return kind == SyntaxKind.EndOfLineTrivia - && !en.MoveNext(); - } - } - - return false; - } - - private static void ReportDiagnostic(SyntaxNodeAnalysisContext context, TextSpan span) - { - DiagnosticHelpers.ReportDiagnostic( - context, - DiagnosticRules.RemoveUnnecessaryBlankLine, - Location.Create(context.Node.SyntaxTree, span)); + protected override DiagnosticDescriptor Descriptor => DiagnosticRules.Obsolete_RemoveUnnecessaryBlankLine; } } diff --git a/src/Common/CSharp/Analysis/UnnecessaryBlankLineAnalysis.cs b/src/Common/CSharp/Analysis/UnnecessaryBlankLineAnalysis.cs new file mode 100644 index 0000000000..7af6fa4e44 --- /dev/null +++ b/src/Common/CSharp/Analysis/UnnecessaryBlankLineAnalysis.cs @@ -0,0 +1,662 @@ +using System.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; + +namespace Roslynator.CSharp.Analysis; + +internal abstract class UnnecessaryBlankLineAnalysis +{ + protected abstract DiagnosticDescriptor Descriptor { get; } + + public void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(f => AnalyzeClassDeclaration(f), SyntaxKind.ClassDeclaration); + context.RegisterSyntaxNodeAction(f => AnalyzeStructDeclaration(f), SyntaxKind.StructDeclaration); +#if ROSLYN_4_0 + context.RegisterSyntaxNodeAction(f => AnalyzeRecordDeclaration(f), SyntaxKind.RecordDeclaration, SyntaxKind.RecordStructDeclaration); +#else + context.RegisterSyntaxNodeAction(f => AnalyzeRecordDeclaration(f), SyntaxKind.RecordDeclaration); +#endif + context.RegisterSyntaxNodeAction(f => AnalyzeInterfaceDeclaration(f), SyntaxKind.InterfaceDeclaration); + context.RegisterSyntaxNodeAction(f => AnalyzeEnumDeclaration(f), SyntaxKind.EnumDeclaration); + context.RegisterSyntaxNodeAction(f => AnalyzeNamespaceDeclaration(f), SyntaxKind.NamespaceDeclaration); + context.RegisterSyntaxNodeAction(f => AnalyzeTryStatement(f), SyntaxKind.TryStatement); + context.RegisterSyntaxNodeAction(f => AnalyzeElseClause(f), SyntaxKind.ElseClause); + + context.RegisterSyntaxNodeAction(f => AnalyzeIfStatement(f), SyntaxKind.IfStatement); + context.RegisterSyntaxNodeAction(f => AnalyzeCommonForEachStatement(f), SyntaxKind.ForEachStatement); + context.RegisterSyntaxNodeAction(f => AnalyzeCommonForEachStatement(f), SyntaxKind.ForEachVariableStatement); + context.RegisterSyntaxNodeAction(f => AnalyzeForStatement(f), SyntaxKind.ForStatement); + context.RegisterSyntaxNodeAction(f => AnalyzeUsingStatement(f), SyntaxKind.UsingStatement); + context.RegisterSyntaxNodeAction(f => AnalyzeWhileStatement(f), SyntaxKind.WhileStatement); + context.RegisterSyntaxNodeAction(f => AnalyzeDoStatement(f), SyntaxKind.DoStatement); + context.RegisterSyntaxNodeAction(f => AnalyzeLockStatement(f), SyntaxKind.LockStatement); + context.RegisterSyntaxNodeAction(f => AnalyzeFixedStatement(f), SyntaxKind.FixedStatement); + + context.RegisterSyntaxNodeAction(f => AnalyzeAccessorList(f), SyntaxKind.AccessorList); + context.RegisterSyntaxNodeAction(f => AnalyzeBlock(f), SyntaxKind.Block); + context.RegisterSyntaxNodeAction(f => AnalyzeSingleLineDocumentationCommentTrivia(f), SyntaxKind.SingleLineDocumentationCommentTrivia); + + context.RegisterSyntaxNodeAction(f => AnalyzeInitializer(f), SyntaxKind.ArrayInitializerExpression); + context.RegisterSyntaxNodeAction(f => AnalyzeInitializer(f), SyntaxKind.CollectionInitializerExpression); + context.RegisterSyntaxNodeAction(f => AnalyzeInitializer(f), SyntaxKind.ObjectInitializerExpression); + + context.RegisterSyntaxNodeAction(f => AnalyzeCompilationUnit(f), SyntaxKind.CompilationUnit); + } + + private void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) + { + var classDeclaration = (ClassDeclarationSyntax)context.Node; + + AnalyzeDeclaration( + context, + classDeclaration.Members, + classDeclaration.OpenBraceToken, + classDeclaration.CloseBraceToken); + } + + private void AnalyzeStructDeclaration(SyntaxNodeAnalysisContext context) + { + var structDeclaration = (StructDeclarationSyntax)context.Node; + + AnalyzeDeclaration( + context, + structDeclaration.Members, + structDeclaration.OpenBraceToken, + structDeclaration.CloseBraceToken); + } + + private void AnalyzeRecordDeclaration(SyntaxNodeAnalysisContext context) + { + var recordDeclaration = (RecordDeclarationSyntax)context.Node; + + AnalyzeDeclaration( + context, + recordDeclaration.Members, + recordDeclaration.OpenBraceToken, + recordDeclaration.CloseBraceToken); + } + + private void AnalyzeInterfaceDeclaration(SyntaxNodeAnalysisContext context) + { + var interfaceDeclaration = (InterfaceDeclarationSyntax)context.Node; + + AnalyzeDeclaration( + context, + interfaceDeclaration.Members, + interfaceDeclaration.OpenBraceToken, + interfaceDeclaration.CloseBraceToken); + } + + private void AnalyzeEnumDeclaration(SyntaxNodeAnalysisContext context) + { + var enumDeclaration = (EnumDeclarationSyntax)context.Node; + + AnalyzeDeclaration( + context, + enumDeclaration.Members, + enumDeclaration.OpenBraceToken, + enumDeclaration.CloseBraceToken); + } + + private void AnalyzeNamespaceDeclaration(SyntaxNodeAnalysisContext context) + { + var namespaceDeclaration = (NamespaceDeclarationSyntax)context.Node; + + SyntaxList members = namespaceDeclaration.Members; + SyntaxList externs = namespaceDeclaration.Externs; + + if (externs.Any()) + { + AnalyzeStart(context, externs[0], namespaceDeclaration.OpenBraceToken); + } + else + { + SyntaxList usings = namespaceDeclaration.Usings; + + if (usings.Any()) + { + AnalyzeStart(context, usings[0], namespaceDeclaration.OpenBraceToken); + } + else if (members.Any()) + { + AnalyzeStart(context, members[0], namespaceDeclaration.OpenBraceToken); + } + } + + if (members.Any()) + AnalyzeEnd(context, members.Last(), namespaceDeclaration.CloseBraceToken); + } + + private void AnalyzeTryStatement(SyntaxNodeAnalysisContext context) + { + var tryStatement = (TryStatementSyntax)context.Node; + + BlockSyntax block = tryStatement.Block; + + if (block is not null) + { + SyntaxList catches = tryStatement.Catches; + + if (catches.Any()) + { + SyntaxNode previousNode = block; + + foreach (CatchClauseSyntax catchClause in catches) + { + Analyze(context, previousNode, catchClause); + + previousNode = catchClause; + } + + FinallyClauseSyntax finallyClause = tryStatement.Finally; + + if (finallyClause is not null) + Analyze(context, previousNode, finallyClause); + } + } + } + + private void AnalyzeElseClause(SyntaxNodeAnalysisContext context) + { + var elseClause = (ElseClauseSyntax)context.Node; + + SyntaxNode parent = elseClause.Parent; + + if (parent is IfStatementSyntax ifStatement) + { + StatementSyntax statement = ifStatement.Statement; + + if (statement is not null) + Analyze(context, statement, elseClause); + + statement = elseClause.Statement; + + if (statement is not null) + Analyze(context, elseClause.ElseKeyword, statement); + } + } + + private void AnalyzeIfStatement(SyntaxNodeAnalysisContext context) + { + var ifStatement = (IfStatementSyntax)context.Node; + + AnalyzeEmbeddedStatement(context, ifStatement.CloseParenToken, ifStatement.Statement); + } + + private void AnalyzeCommonForEachStatement(SyntaxNodeAnalysisContext context) + { + var forEachStatement = (CommonForEachStatementSyntax)context.Node; + + AnalyzeEmbeddedStatement(context, forEachStatement.CloseParenToken, forEachStatement.Statement); + } + + private void AnalyzeForStatement(SyntaxNodeAnalysisContext context) + { + var forStatement = (ForStatementSyntax)context.Node; + + AnalyzeEmbeddedStatement(context, forStatement.CloseParenToken, forStatement.Statement); + } + + private void AnalyzeUsingStatement(SyntaxNodeAnalysisContext context) + { + var usingStatement = (UsingStatementSyntax)context.Node; + + AnalyzeEmbeddedStatement(context, usingStatement.CloseParenToken, usingStatement.Statement); + } + + private void AnalyzeWhileStatement(SyntaxNodeAnalysisContext context) + { + var whileStatement = (WhileStatementSyntax)context.Node; + + AnalyzeEmbeddedStatement(context, whileStatement.CloseParenToken, whileStatement.Statement); + } + + private void AnalyzeDoStatement(SyntaxNodeAnalysisContext context) + { + var doStatement = (DoStatementSyntax)context.Node; + + AnalyzeEmbeddedStatement(context, doStatement.DoKeyword, doStatement.Statement); + } + + private void AnalyzeLockStatement(SyntaxNodeAnalysisContext context) + { + var lockStatement = (LockStatementSyntax)context.Node; + + AnalyzeEmbeddedStatement(context, lockStatement.CloseParenToken, lockStatement.Statement); + } + + private void AnalyzeFixedStatement(SyntaxNodeAnalysisContext context) + { + var fixedStatement = (FixedStatementSyntax)context.Node; + + AnalyzeEmbeddedStatement(context, fixedStatement.CloseParenToken, fixedStatement.Statement); + } + + private void AnalyzeEmbeddedStatement(SyntaxNodeAnalysisContext context, SyntaxToken token, StatementSyntax statement) + { + if (statement?.IsEmbedded() == true) + Analyze(context, token, statement); + } + + private void Analyze( + SyntaxNodeAnalysisContext context, + SyntaxNode node1, + SyntaxNode node2) + { + SyntaxTriviaList trailingTrivia = node1.GetTrailingTrivia(); + SyntaxTriviaList leadingTrivia = node2.GetLeadingTrivia(); + + if (IsStandardTriviaBetweenLines(trailingTrivia, leadingTrivia)) + return; + + if (node1 + .SyntaxTree + .GetLineSpan(TextSpan.FromBounds(node1.Span.End, node2.SpanStart), context.CancellationToken) + .GetLineCount() != 3) + { + return; + } + + var trivia = default(SyntaxTrivia); + + foreach (SyntaxTrivia t in leadingTrivia) + { + if (!t.IsWhitespaceTrivia()) + { + trivia = t; + break; + } + } + + if (!trivia.IsEndOfLineTrivia()) + return; + + if (!trailingTrivia.IsEmptyOrWhitespace()) + return; + + if (!leadingTrivia.IsEmptyOrWhitespace()) + return; + + ReportDiagnostic( + context, + TextSpan.FromBounds(node2.FullSpan.Start, trivia.Span.End)); + } + + private void Analyze( + SyntaxNodeAnalysisContext context, + SyntaxToken token, + SyntaxNode node) + { + SyntaxTriviaList trailingTrivia = token.TrailingTrivia; + SyntaxTriviaList leadingTrivia = node.GetLeadingTrivia(); + + if (IsStandardTriviaBetweenLines(trailingTrivia, leadingTrivia)) + return; + + if (token + .SyntaxTree + .GetLineSpan(TextSpan.FromBounds(token.Span.End, node.SpanStart), context.CancellationToken) + .GetLineCount() != 3) + { + return; + } + + var trivia = default(SyntaxTrivia); + + foreach (SyntaxTrivia t in leadingTrivia) + { + if (!t.IsWhitespaceTrivia()) + { + trivia = t; + break; + } + } + + if (!trivia.IsEndOfLineTrivia()) + return; + + if (!trailingTrivia.IsEmptyOrWhitespace()) + return; + + if (!leadingTrivia.IsEmptyOrWhitespace()) + return; + + Location location = Location.Create(token.SyntaxTree, TextSpan.FromBounds(node.FullSpan.Start, trivia.Span.End)); + + ReportDiagnostic(context, location); + } + + private void AnalyzeDeclaration( + SyntaxNodeAnalysisContext context, + SyntaxList members, + SyntaxToken openBrace, + SyntaxToken closeBrace) where TMember : MemberDeclarationSyntax + { + if (members.Any()) + { + AnalyzeStart(context, members[0], openBrace); + AnalyzeEnd(context, members.Last(), closeBrace); + } + else + { + AnalyzeEmptyBraces(context, openBrace, closeBrace); + } + } + + private void AnalyzeDeclaration( + SyntaxNodeAnalysisContext context, + SeparatedSyntaxList members, + SyntaxToken openBrace, + SyntaxToken closeBrace) where TMember : MemberDeclarationSyntax + { + if (members.Any()) + { + AnalyzeStart(context, members[0], openBrace); + + int count = members.SeparatorCount; + + SyntaxNodeOrToken nodeOrToken = (count == members.Count) + ? members.GetSeparator(count - 1) + : members.Last(); + + AnalyzeEnd(context, nodeOrToken, closeBrace); + } + else + { + AnalyzeEmptyBraces(context, openBrace, closeBrace); + } + } + + private void AnalyzeBlock(SyntaxNodeAnalysisContext context) + { + var block = (BlockSyntax)context.Node; + + SyntaxList statements = block.Statements; + + if (statements.Any()) + { + AnalyzeStart(context, statements[0], block.OpenBraceToken); + AnalyzeEnd(context, statements.Last(), block.CloseBraceToken); + } + else + { + AnalyzeEmptyBraces(context, block.OpenBraceToken, block.CloseBraceToken); + } + } + + private void AnalyzeSingleLineDocumentationCommentTrivia(SyntaxNodeAnalysisContext context) + { + var comment = (DocumentationCommentTriviaSyntax)context.Node; + + if (comment is IStructuredTriviaSyntax structuredTrivia) + { + SyntaxTrivia trivia = structuredTrivia.ParentTrivia; + SyntaxTriviaList leadingTrivia = trivia.Token.LeadingTrivia; + + int index = leadingTrivia.IndexOf(trivia); + + if (index >= 0 + && index < leadingTrivia.Count - 1 + && leadingTrivia[index + 1].IsEndOfLineTrivia()) + { + ReportDiagnostic(context, leadingTrivia[index + 1].GetLocation()); + } + } + } + + private void AnalyzeInitializer(SyntaxNodeAnalysisContext context) + { + var initializer = (InitializerExpressionSyntax)context.Node; + + SeparatedSyntaxList expressions = initializer.Expressions; + + ExpressionSyntax first = expressions.FirstOrDefault(); + + if (first is null) + return; + + if (IsExpectedTrailingTrivia(initializer.OpenBraceToken.TrailingTrivia)) + { + SyntaxTriviaList leading = first.GetLeadingTrivia(); + + if (leading.Any()) + { + TextSpan? span = GetEmptyLineSpan(leading, isEnd: false); + + if (span is not null) + ReportDiagnostic(context, span.Value); + } + } + + if (IsExpectedTrailingTrivia(expressions.GetTrailingTrivia())) + { + SyntaxTriviaList leading = initializer.CloseBraceToken.LeadingTrivia; + + if (leading.Any()) + { + TextSpan? span = GetEmptyLineSpan(leading, isEnd: true); + + if (span is not null) + ReportDiagnostic(context, span.Value); + } + } + + static bool IsExpectedTrailingTrivia(SyntaxTriviaList triviaList) + { + foreach (SyntaxTrivia trivia in triviaList) + { + switch (trivia.Kind()) + { + case SyntaxKind.WhitespaceTrivia: + break; + case SyntaxKind.EndOfLineTrivia: + return true; + default: + return false; + } + } + + return false; + } + } + + private void AnalyzeAccessorList(SyntaxNodeAnalysisContext context) + { + var accessorList = (AccessorListSyntax)context.Node; + + SyntaxList accessors = accessorList.Accessors; + + if (accessors.Any()) + { + AnalyzeStart(context, accessors[0], accessorList.OpenBraceToken); + AnalyzeEnd(context, accessors.Last(), accessorList.CloseBraceToken); + } + } + + private void AnalyzeCompilationUnit(SyntaxNodeAnalysisContext context) + { + var compilationUnit = (CompilationUnitSyntax)context.Node; + + SyntaxTriviaList leading = compilationUnit.EndOfFileToken.LeadingTrivia; + + SyntaxTriviaList.Reversed.Enumerator en = leading.Reverse().GetEnumerator(); + + if (en.MoveNext() + && en.Current.IsEndOfLineTrivia()) + { + int start = en.Current.SpanStart; + + if (en.MoveNext()) + { + if (!en.Current.IsWhitespaceTrivia() + || en.MoveNext()) + { + if (CSharpFacts.IsCommentTrivia(en.Current.Kind()) + || SyntaxFacts.IsPreprocessorDirective(en.Current.Kind())) + { + return; + } + + do + { + Debug.Assert(en.Current.IsWhitespaceOrEndOfLineTrivia(), en.Current.Kind().ToString()); + + if (en.Current.IsEndOfLineTrivia()) + { + start = en.Current.SpanStart; + } + else + { + break; + } + } + while (en.MoveNext()); + } + } + + ReportDiagnostic(context, TextSpan.FromBounds(start, leading.Span.End)); + } + } + + private void AnalyzeStart( + SyntaxNodeAnalysisContext context, + SyntaxNode node, + SyntaxToken brace) + { + if (brace.IsMissing) + return; + + if ((node.GetSpanStartLine() - brace.GetSpanEndLine()) <= 1) + return; + + TextSpan? span = GetEmptyLineSpan(node.GetLeadingTrivia(), isEnd: false); + + if (span is null) + return; + + ReportDiagnostic(context, span.Value); + } + + private void AnalyzeEnd( + SyntaxNodeAnalysisContext context, + SyntaxNodeOrToken nodeOrToken, + SyntaxToken brace) + { + if (brace.IsMissing) + return; + + int braceLine = brace.GetSpanStartLine(); + int nodeOrTokenLine = nodeOrToken.SyntaxTree.GetLineSpan(nodeOrToken.Span).EndLine(); + + if (braceLine - nodeOrTokenLine <= 1) + return; + + TextSpan? span = GetEmptyLineSpan(brace.LeadingTrivia, isEnd: true); + + if (span is null) + return; + + ReportDiagnostic(context, span.Value); + } + + private void AnalyzeEmptyBraces( + SyntaxNodeAnalysisContext context, + SyntaxToken openBrace, + SyntaxToken closeBrace) + { + if (openBrace.IsMissing) + return; + + if (closeBrace.IsMissing) + return; + + SyntaxTree tree = context.Node.SyntaxTree; + + if (tree.GetLineCount(TextSpan.FromBounds(openBrace.SpanStart, closeBrace.Span.End)) <= 2) + return; + + TextSpan? span = GetEmptyLineSpan(closeBrace.LeadingTrivia, isEnd: true); + + if (span is null) + return; + + ReportDiagnostic(context, span.Value); + } + + private TextSpan? GetEmptyLineSpan( + SyntaxTriviaList triviaList, + bool isEnd) + { + SyntaxTriviaList.Enumerator en = triviaList.GetEnumerator(); + while (en.MoveNext()) + { + switch (en.Current.Kind()) + { + case SyntaxKind.EndOfLineTrivia: + { + SyntaxTrivia endOfLine = en.Current; + + if (isEnd) + { + while (en.MoveNext()) + { + if (!en.Current.IsWhitespaceOrEndOfLineTrivia()) + return null; + } + } + + return TextSpan.FromBounds(triviaList.Span.Start, endOfLine.Span.End); + } + case SyntaxKind.WhitespaceTrivia: + { + break; + } + default: + { + return null; + } + } + } + + return null; + } + + private bool IsStandardTriviaBetweenLines(SyntaxTriviaList trailingTrivia, SyntaxTriviaList leadingTrivia) + { + if (leadingTrivia.Any() + && leadingTrivia.All(f => f.IsWhitespaceTrivia())) + { + SyntaxTriviaList.Enumerator en = trailingTrivia.GetEnumerator(); + + while (en.MoveNext()) + { + SyntaxKind kind = en.Current.Kind(); + + if (kind == SyntaxKind.WhitespaceTrivia) + continue; + + return kind == SyntaxKind.EndOfLineTrivia + && !en.MoveNext(); + } + } + + return false; + } + + private void ReportDiagnostic(SyntaxNodeAnalysisContext context, TextSpan span) + { + ReportDiagnostic(context, Location.Create(context.Node.SyntaxTree, span)); + } + + private void ReportDiagnostic(SyntaxNodeAnalysisContext context, Location location) + { + DiagnosticHelpers.ReportDiagnostic( + context, + Descriptor, + location); + } +} diff --git a/src/Common/DiagnosticIdentifiers.Generated.cs b/src/Common/DiagnosticIdentifiers.Generated.cs index 7cb40c987c..804da1b566 100644 --- a/src/Common/DiagnosticIdentifiers.Generated.cs +++ b/src/Common/DiagnosticIdentifiers.Generated.cs @@ -61,6 +61,7 @@ public static partial class DiagnosticIdentifiers public const string PlaceNewLineAfterOrBeforeNullConditionalOperator = "RCS0059"; public const string BlankLineAfterFileScopedNamespaceDeclaration = "RCS0060"; public const string BlankLineBetweenSwitchSections = "RCS0061"; + public const string RemoveUnnecessaryBlankLine = "RCS0063"; public const string AddBracesWhenExpressionSpansOverMultipleLines = "RCS1001"; public const string RemoveBraces = "RCS1002"; public const string AddBracesToIfElseWhenExpressionSpansOverMultipleLines = "RCS1003"; @@ -85,7 +86,7 @@ public static partial class DiagnosticIdentifiers public const string RemoveRedundantBooleanLiteral = "RCS1033"; public const string RemoveRedundantSealedModifier = "RCS1034"; public const string RemoveRedundantCommaInInitializer = "RCS1035"; - public const string RemoveUnnecessaryBlankLine = "RCS1036"; + public const string Obsolete_RemoveUnnecessaryBlankLine = "RCS1036"; public const string RemoveTrailingWhitespace = "RCS1037"; public const string RemoveEmptyStatement = "RCS1038"; public const string RemoveArgumentListFromAttribute = "RCS1039"; diff --git a/src/Common/DiagnosticRules.Generated.cs b/src/Common/DiagnosticRules.Generated.cs index 2768f9adc0..0b4e592221 100644 --- a/src/Common/DiagnosticRules.Generated.cs +++ b/src/Common/DiagnosticRules.Generated.cs @@ -645,6 +645,18 @@ public static partial class DiagnosticRules helpLinkUri: DiagnosticIdentifiers.BlankLineBetweenSwitchSections, customTags: []); + /// RCS0063 + public static readonly DiagnosticDescriptor RemoveUnnecessaryBlankLine = DiagnosticDescriptorFactory.Create( + id: DiagnosticIdentifiers.RemoveUnnecessaryBlankLine, + title: "Remove unnecessary blank line", + messageFormat: "Remove unnecessary blank line", + category: DiagnosticCategories.Roslynator, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: false, + description: null, + helpLinkUri: DiagnosticIdentifiers.RemoveUnnecessaryBlankLine, + customTags: []); + /// RCS1001 public static readonly DiagnosticDescriptor AddBracesWhenExpressionSpansOverMultipleLines = DiagnosticDescriptorFactory.Create( id: DiagnosticIdentifiers.AddBracesWhenExpressionSpansOverMultipleLines, @@ -950,15 +962,15 @@ public static partial class DiagnosticRules customTags: WellKnownDiagnosticTags.Unnecessary); /// RCS1036 - public static readonly DiagnosticDescriptor RemoveUnnecessaryBlankLine = DiagnosticDescriptorFactory.Create( - id: DiagnosticIdentifiers.RemoveUnnecessaryBlankLine, - title: "Remove unnecessary blank line", - messageFormat: "Remove unnecessary blank line", + public static readonly DiagnosticDescriptor Obsolete_RemoveUnnecessaryBlankLine = DiagnosticDescriptorFactory.Create( + id: DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine, + title: "[deprecated] Remove unnecessary blank line", + messageFormat: "([deprecated] Use RCS0063 instead) Remove unnecessary blank line", category: DiagnosticCategories.Roslynator, defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, description: null, - helpLinkUri: DiagnosticIdentifiers.RemoveUnnecessaryBlankLine, + helpLinkUri: DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine, customTags: []); /// RCS1037 diff --git a/src/Formatting.Analyzers.CodeFixes/CSharp/RemoveUnnecessaryBlankLineFixProvider.cs b/src/Formatting.Analyzers.CodeFixes/CSharp/RemoveUnnecessaryBlankLineFixProvider.cs new file mode 100644 index 0000000000..eef54459ae --- /dev/null +++ b/src/Formatting.Analyzers.CodeFixes/CSharp/RemoveUnnecessaryBlankLineFixProvider.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using System.Composition; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Text; +using Roslynator.CSharp; + +namespace Roslynator.Formatting.CodeFixes.CSharp; + +[ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, Name = nameof(RemoveUnnecessaryBlankLineFixProvider))] +[Shared] +public sealed class RemoveUnnecessaryBlankLineFixProvider : BaseCodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds + { + get { return ImmutableArray.Create(DiagnosticIdentifiers.RemoveUnnecessaryBlankLine); } + } + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + SyntaxNode root = await context.GetSyntaxRootAsync().ConfigureAwait(false); + TextSpan span = context.Span; + + if (!root.FindTrivia(span.Start).IsWhitespaceOrEndOfLineTrivia()) + { + Debug.Fail(""); + return; + } + + Diagnostic diagnostic = context.Diagnostics[0]; + + CodeAction codeAction = CodeAction.Create( + CodeFixTitles.RemoveBlankLine, + ct => context.Document.WithTextChangeAsync(span, "", ct), + GetEquivalenceKey(diagnostic)); + + context.RegisterCodeFix(codeAction, diagnostic); + } +} diff --git a/src/Formatting.Analyzers/CSharp/RemoveUnnecessaryBlankLineAnalyzer.cs b/src/Formatting.Analyzers/CSharp/RemoveUnnecessaryBlankLineAnalyzer.cs new file mode 100644 index 0000000000..0e246faad0 --- /dev/null +++ b/src/Formatting.Analyzers/CSharp/RemoveUnnecessaryBlankLineAnalyzer.cs @@ -0,0 +1,103 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; +using Roslynator.CSharp; + +namespace Roslynator.Formatting.CSharp; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class RemoveUnnecessaryBlankLineAnalyzer : BaseDiagnosticAnalyzer +{ + private static ImmutableArray _supportedDiagnostics; + + public override ImmutableArray SupportedDiagnostics + { + get + { + if (_supportedDiagnostics.IsDefault) + { + Immutable.InterlockedInitialize( + ref _supportedDiagnostics, + DiagnosticRules.RemoveUnnecessaryBlankLine); + } + + return _supportedDiagnostics; + } + } + + public override void Initialize(AnalysisContext context) + { + base.Initialize(context); + + context.RegisterSyntaxTreeAction(c => AnalyzeTrailingTrivia(c)); + + UnnecessaryBlankLineAnalysis.Instance.Initialize(context); + } + + private static void AnalyzeTrailingTrivia(SyntaxTreeAnalysisContext context) + { + if (!context.Tree.TryGetText(out SourceText sourceText)) + return; + + if (!context.Tree.TryGetRoot(out SyntaxNode root)) + return; + + var emptyLines = default(TextSpan); + var previousLineIsEmpty = false; + int i = 0; + + foreach (TextLine textLine in sourceText.Lines) + { + var lineIsEmpty = false; + + if (textLine.Span.Length == 0) + { + SyntaxTrivia endOfLine = root.FindTrivia(textLine.End); + + if (endOfLine.IsEndOfLineTrivia()) + { + lineIsEmpty = true; + + if (previousLineIsEmpty) + { + if (emptyLines.IsEmpty) + { + emptyLines = endOfLine.Span; + } + else + { + emptyLines = TextSpan.FromBounds(emptyLines.Start, endOfLine.Span.End); + } + } + } + else + { + emptyLines = default; + } + } + else + { + if (!emptyLines.IsEmpty) + { + DiagnosticHelpers.ReportDiagnostic( + context, + DiagnosticRules.RemoveUnnecessaryBlankLine, + Location.Create(context.Tree, emptyLines)); + } + + emptyLines = default; + } + + previousLineIsEmpty = lineIsEmpty; + i++; + } + } + + private class UnnecessaryBlankLineAnalysis : Roslynator.CSharp.Analysis.UnnecessaryBlankLineAnalysis + { + public static UnnecessaryBlankLineAnalysis Instance { get; } = new(); + + protected override DiagnosticDescriptor Descriptor => DiagnosticRules.RemoveUnnecessaryBlankLine; + } +} diff --git a/src/Tests/Analyzers.Tests/RCS1036RemoveUnnecessaryBlankLineTests.cs b/src/Tests/Analyzers.Tests/RCS1036RemoveUnnecessaryBlankLineTests.cs index 93cc914e34..7ba8ebbccc 100644 --- a/src/Tests/Analyzers.Tests/RCS1036RemoveUnnecessaryBlankLineTests.cs +++ b/src/Tests/Analyzers.Tests/RCS1036RemoveUnnecessaryBlankLineTests.cs @@ -10,9 +10,9 @@ namespace Roslynator.CSharp.Analysis.Tests; public class RCS1036RemoveUnnecessaryBlankLineTests : AbstractCSharpDiagnosticVerifier { - public override DiagnosticDescriptor Descriptor { get; } = DiagnosticRules.RemoveUnnecessaryBlankLine; + public override DiagnosticDescriptor Descriptor { get; } = DiagnosticRules.Obsolete_RemoveUnnecessaryBlankLine; - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine)] public async Task Test_ObjectInitializer() { await VerifyDiagnosticAndFixAsync(@" @@ -50,7 +50,7 @@ void M() "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine)] public async Task Test_ObjectInitializer_WithTrailingComma() { await VerifyDiagnosticAndFixAsync(@" @@ -88,7 +88,7 @@ void M() "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine)] public async Task Test_ArrayInitializer() { await VerifyDiagnosticAndFixAsync(@" @@ -120,7 +120,7 @@ void M() "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine)] public async Task Test_CollectionInitializer() { await VerifyDiagnosticAndFixAsync(@" @@ -156,7 +156,7 @@ void M() "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine)] public async Task Test_EmptyDeclaration() { await VerifyDiagnosticAndFixAsync(@" @@ -171,7 +171,7 @@ class C "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine)] public async Task Test_EmptyBlock() { await VerifyDiagnosticAndFixAsync(@" @@ -192,7 +192,7 @@ void M() "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine)] public async Task Test_EmptyLineAfterDocComment() { await VerifyDiagnosticAndFixAsync(@" @@ -215,7 +215,7 @@ void M() "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine)] public async Task Test_EndOfFile() { await VerifyDiagnosticAndFixAsync(@" @@ -230,7 +230,7 @@ class C ", options: Options.EnableConfigOption(ConfigOptionKeys.BlankLineBetweenClosingBraceAndSwitchSection)); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine)] public async Task Test_EndOfFile2() { await VerifyDiagnosticAndFixAsync(@" @@ -246,7 +246,7 @@ class C ", options: Options.EnableConfigOption(ConfigOptionKeys.BlankLineBetweenClosingBraceAndSwitchSection)); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine)] public async Task Test_LastEmptyLineInDoStatement() { await VerifyDiagnosticAndFixAsync(@" @@ -275,7 +275,7 @@ void M() "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine)] public async Task Test_EmptyLineAfterLastEnumMember_NoTrailingComma() { await VerifyDiagnosticAndFixAsync(@" @@ -300,7 +300,7 @@ enum E "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine)] public async Task Test_EmptyLineAfterLastEnumMember_TrailingComma() { await VerifyDiagnosticAndFixAsync(@" @@ -325,7 +325,7 @@ enum E "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine)] public async Task Test_EmptyLineBeforeFirstEnumMember() { await VerifyDiagnosticAndFixAsync(@" @@ -350,7 +350,7 @@ enum E "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine)] public async Task TestNoDiagnostic_ObjectInitializer() { await VerifyNoDiagnosticAsync(@" @@ -371,7 +371,7 @@ void M() "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine)] public async Task TestNoDiagnostic_ObjectInitializer_Singleline() { await VerifyNoDiagnosticAsync(@" @@ -388,7 +388,7 @@ void M() "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine)] public async Task TestNoDiagnostic_ObjectInitializer_Empty() { await VerifyNoDiagnosticAsync(@" @@ -402,7 +402,7 @@ void M() "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine)] public async Task TestNoDiagnostic_EmptyLineBetweenClosingBraceAndSwitchSection() { await VerifyNoDiagnosticAsync(""" @@ -428,7 +428,7 @@ void M() """); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine)] public async Task TestNoDiagnostic_EmptyLineAtEndOfFile() { await VerifyNoDiagnosticAsync(@" @@ -438,7 +438,7 @@ class C "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine)] public async Task TestNoDiagnostic_EmptyLineAtEndOfFileAfterMultiLineComment() { await VerifyNoDiagnosticAsync(@" @@ -449,7 +449,7 @@ class C "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine)] public async Task TestNoDiagnostic_EmptyLineAtEndOfFileWithWhitespace() { await VerifyNoDiagnosticAsync(@" @@ -459,7 +459,7 @@ class C "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine)] public async Task TestNoDiagnostic_EmptyLineAtEndOfFileAfterSingleLineComment() { await VerifyNoDiagnosticAsync(@" @@ -470,7 +470,7 @@ class C "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine)] public async Task TestNoDiagnostic_EmptyLineAtEndOfFileAfterPreprocessorDirective() { await VerifyNoDiagnosticAsync(@" @@ -482,7 +482,7 @@ class C "); } - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.Obsolete_RemoveUnnecessaryBlankLine)] public async Task Test_EmptyLineBetweenClosingBraceAndSwitchSection() { await VerifyNoDiagnosticAsync(""" diff --git a/src/Tests/Formatting.Analyzers.Tests/RCS0063RemoveUnnecessaryBlankLineTests.cs b/src/Tests/Formatting.Analyzers.Tests/RCS0063RemoveUnnecessaryBlankLineTests.cs new file mode 100644 index 0000000000..7cc1bc2681 --- /dev/null +++ b/src/Tests/Formatting.Analyzers.Tests/RCS0063RemoveUnnecessaryBlankLineTests.cs @@ -0,0 +1,515 @@ +// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Roslynator.Formatting.CodeFixes.CSharp; +using Roslynator.Testing.CSharp; +using Xunit; + +namespace Roslynator.Formatting.CSharp.Tests; + +public class RCS0063RemoveUnnecessaryBlankLineTests : AbstractCSharpDiagnosticVerifier +{ + public override DiagnosticDescriptor Descriptor { get; } = DiagnosticRules.RemoveUnnecessaryBlankLine; + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + public async Task Test_ObjectInitializer() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ + void M() + { + var x = new C() + { +[| +|] P1 = 1, + P2 = 2 +[| +|] }; + } + + public int P1 { get; set; } + public int P2 { get; set; } +} +", @" +class C +{ + void M() + { + var x = new C() + { + P1 = 1, + P2 = 2 + }; + } + + public int P1 { get; set; } + public int P2 { get; set; } +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + public async Task Test_ObjectInitializer_WithTrailingComma() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ + void M() + { + var x = new C() + { +[| +|] P1 = 1, + P2 = 2, +[| +|] }; + } + + public int P1 { get; set; } + public int P2 { get; set; } +} +", @" +class C +{ + void M() + { + var x = new C() + { + P1 = 1, + P2 = 2, + }; + } + + public int P1 { get; set; } + public int P2 { get; set; } +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + public async Task Test_ArrayInitializer() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ + void M() + { + var items = new object[] + { +[| +|] null, + null +[| +|] }; + } +} +", @" +class C +{ + void M() + { + var items = new object[] + { + null, + null + }; + } +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + public async Task Test_CollectionInitializer() + { + await VerifyDiagnosticAndFixAsync(@" +using System.Collections.Generic; + +class C +{ + void M() + { + var items = new List() + { +[| +|] null, + null +[| +|] }; + } +} +", @" +using System.Collections.Generic; + +class C +{ + void M() + { + var items = new List() + { + null, + null + }; + } +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + public async Task Test_EmptyDeclaration() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ +[| +|]} +", @" +class C +{ +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + public async Task Test_EmptyBlock() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ + void M() + { +[| +|] } +} +", @" +class C +{ + void M() + { + } +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + public async Task Test_EmptyLineAfterDocComment() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ + /// +[| +|] void M() + { + } +} +", @" +class C +{ + /// + void M() + { + } +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + public async Task Test_EndOfFile() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ +} +[| +|]", @" +class C +{ +} +", options: Options.EnableConfigOption(ConfigOptionKeys.BlankLineBetweenClosingBraceAndSwitchSection)); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + public async Task Test_EndOfFile2() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ +} +[| + +|]", @" +class C +{ +} +", options: Options.EnableConfigOption(ConfigOptionKeys.BlankLineBetweenClosingBraceAndSwitchSection)); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + public async Task Test_LastEmptyLineInDoStatement() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ + void M() + { + do + { + M(); +[| +|] } while (true); + } +} +", @" +class C +{ + void M() + { + do + { + M(); + } while (true); + } +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + public async Task Test_EmptyLineAfterLastEnumMember_NoTrailingComma() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ + enum E + { + A, + B +[| +|] } +} +", @" +class C +{ + enum E + { + A, + B + } +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + public async Task Test_EmptyLineAfterLastEnumMember_TrailingComma() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ + enum E + { + A, + B, +[| +|] } +} +", @" +class C +{ + enum E + { + A, + B, + } +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + public async Task Test_EmptyLineBeforeFirstEnumMember() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ + enum E + { +[| +|] A, + B, + } +} +", @" +class C +{ + enum E + { + A, + B, + } +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + public async Task TestNoDiagnostic_ObjectInitializer() + { + await VerifyNoDiagnosticAsync(@" +class C +{ + void M() + { + var x = new C() + { + P1 = 1, + P2 = 2 + }; + } + + public int P1 { get; set; } + public int P2 { get; set; } +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + public async Task TestNoDiagnostic_ObjectInitializer_Singleline() + { + await VerifyNoDiagnosticAsync(@" +class C +{ + void M() + { + var x = new C() { P1 = 1, P2 = 2 }; + } + + public int P1 { get; set; } + public int P2 { get; set; } +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + public async Task TestNoDiagnostic_ObjectInitializer_Empty() + { + await VerifyNoDiagnosticAsync(@" +class C +{ + void M() + { + var x = new C() { }; + } +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + public async Task TestNoDiagnostic_EmptyLineBetweenClosingBraceAndSwitchSection() + { + await VerifyNoDiagnosticAsync(""" +class C +{ + void M() + { + string x = null; + + switch (x) + { + case "a": + { + M(); + break; + } + + case "b": + break; + } + } +} +"""); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + public async Task TestNoDiagnostic_EmptyLineAtEndOfFile() + { + await VerifyNoDiagnosticAsync(@" +class C +{ +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + public async Task TestNoDiagnostic_EmptyLineAtEndOfFileAfterMultiLineComment() + { + await VerifyNoDiagnosticAsync(@" +class C +{ +} +/** **/ +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + public async Task TestNoDiagnostic_EmptyLineAtEndOfFileWithWhitespace() + { + await VerifyNoDiagnosticAsync(@" +class C +{ +} + "); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + public async Task TestNoDiagnostic_EmptyLineAtEndOfFileAfterSingleLineComment() + { + await VerifyNoDiagnosticAsync(@" +class C +{ +} +//x +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + public async Task TestNoDiagnostic_EmptyLineAtEndOfFileAfterPreprocessorDirective() + { + await VerifyNoDiagnosticAsync(@" +class C +{ +} +#if DEBUG +#endif +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBlankLine)] + public async Task Test_EmptyLineBetweenClosingBraceAndSwitchSection() + { + await VerifyNoDiagnosticAsync(""" +class C +{ + void M() + { + string x = null; + switch (x) + { + case "a": + { + M(); + break; + } + + case "b": + { + M(); + break; + } + + case "c": + break; + } + } +} +""", options: Options.AddConfigOption(ConfigOptionKeys.BlankLineBetweenClosingBraceAndSwitchSection, false)); + } +} diff --git a/src/VisualStudioCode/package/src/configurationFiles.generated.ts b/src/VisualStudioCode/package/src/configurationFiles.generated.ts index 4af6961de1..e04c1ea709 100644 --- a/src/VisualStudioCode/package/src/configurationFiles.generated.ts +++ b/src/VisualStudioCode/package/src/configurationFiles.generated.ts @@ -51,7 +51,7 @@ roslynator_analyzers.enabled_by_default = true|false # Applicable to: rcs0060 #roslynator_blank_line_between_closing_brace_and_switch_section = true|false -# Applicable to: rcs0014, rcs1036 +# Applicable to: rcs0014, rcs0063, rcs1036 #roslynator_blank_line_between_single_line_accessors = true|false # Applicable to: rcs0011 @@ -309,6 +309,10 @@ roslynator_analyzers.enabled_by_default = true|false #dotnet_diagnostic.rcs0061.severity = none # Options: roslynator_blank_line_between_switch_sections +# Remove unnecessary blank line +#dotnet_diagnostic.rcs0063.severity = none +# Options: roslynator_blank_line_between_closing_brace_and_switch_section + # Add braces (when expression spans over multiple lines) #dotnet_diagnostic.rcs1001.severity = suggestion @@ -369,10 +373,6 @@ roslynator_analyzers.enabled_by_default = true|false # Remove redundant 'sealed' modifier #dotnet_diagnostic.rcs1034.severity = silent -# Remove unnecessary blank line -#dotnet_diagnostic.rcs1036.severity = suggestion -# Options: roslynator_blank_line_between_closing_brace_and_switch_section - # Remove trailing white-space #dotnet_diagnostic.rcs1037.severity = suggestion From 204ce0d3b55cd5d7a9b7c58c97135e47aa03a38f Mon Sep 17 00:00:00 2001 From: Josef Pihrt Date: Mon, 23 Dec 2024 11:24:48 +0100 Subject: [PATCH 2/4] update --- src/Common/CSharp/Analysis/UnnecessaryBlankLineAnalysis.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Common/CSharp/Analysis/UnnecessaryBlankLineAnalysis.cs b/src/Common/CSharp/Analysis/UnnecessaryBlankLineAnalysis.cs index 7af6fa4e44..a5c8ad56ee 100644 --- a/src/Common/CSharp/Analysis/UnnecessaryBlankLineAnalysis.cs +++ b/src/Common/CSharp/Analysis/UnnecessaryBlankLineAnalysis.cs @@ -587,7 +587,7 @@ private void AnalyzeEmptyBraces( ReportDiagnostic(context, span.Value); } - private TextSpan? GetEmptyLineSpan( + private static TextSpan? GetEmptyLineSpan( SyntaxTriviaList triviaList, bool isEnd) { @@ -625,7 +625,7 @@ private void AnalyzeEmptyBraces( return null; } - private bool IsStandardTriviaBetweenLines(SyntaxTriviaList trailingTrivia, SyntaxTriviaList leadingTrivia) + private static bool IsStandardTriviaBetweenLines(SyntaxTriviaList trailingTrivia, SyntaxTriviaList leadingTrivia) { if (leadingTrivia.Any() && leadingTrivia.All(f => f.IsWhitespaceTrivia())) From 4129eb2148a14aa8a0cdd8508582ed5a9590a957 Mon Sep 17 00:00:00 2001 From: Josef Pihrt Date: Mon, 23 Dec 2024 19:54:32 +0100 Subject: [PATCH 3/4] changelog --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index a6cbc447fd..e6ab73d0e2 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Move `DiagnosticRules` and `DiagnosticIdentifiers` to `Roslynator.Common` ([PR](https://github.com/dotnet/roslynator/pull/1597)) +- Move analyzer [RCS1036](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1036) to Formatting.Analyzers as [RCS0063](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS0063) ([PR](https://github.com/dotnet/roslynator/pull/1600)) + - Old analyzer still works but is marked as obsolete. ## [4.12.10] - 2024-12-17 From 6544d0d8762b1c1be6ea2ddf558295bc35a8069e Mon Sep 17 00:00:00 2001 From: Josef Pihrt Date: Wed, 29 Jan 2025 22:35:53 +0100 Subject: [PATCH 4/4] changelog --- ChangeLog.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index aaada69266..2a88d5f469 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Move analyzer [RCS1036](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1036) to Formatting.Analyzers as [RCS0063](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS0063) ([PR](https://github.com/dotnet/roslynator/pull/1600)) + - Old analyzer still works but is marked as obsolete. + ## [4.12.11] - 2025-01-28 ### Added @@ -21,8 +26,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Move `DiagnosticRules` and `DiagnosticIdentifiers` to `Roslynator.Common` ([PR](https://github.com/dotnet/roslynator/pull/1597)) -- Move analyzer [RCS1036](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1036) to Formatting.Analyzers as [RCS0063](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS0063) ([PR](https://github.com/dotnet/roslynator/pull/1600)) - - Old analyzer still works but is marked as obsolete. ## [4.12.10] - 2024-12-17