Skip to content

Commit

Permalink
Add analyzer 'Add/remove blank line between switch sections' (#1302)
Browse files Browse the repository at this point in the history
  • Loading branch information
josefpihrt committed Dec 5, 2023
1 parent 65425bc commit 1039445
Show file tree
Hide file tree
Showing 24 changed files with 650 additions and 110 deletions.
9 changes: 9 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add analyzer "Add/remove blank line between switch sections" ([RCS0061](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS0061)) ([PR](https://github.com/dotnet/roslynator/pull/1302))
- Option (required): `roslynator_blank_line_between_switch_sections = include|omit|omit_after_block`
- Make analyzer [RCS0014](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS0014) obsolete

### Changed

- Replace type declaration's empty braces with semicolon ([RCS1251](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1251) ([PR](https://github.com/dotnet/roslynator/pull/1323))
Expand All @@ -18,6 +24,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add analyzer "Dispose resource asynchronously" ([RCS1261](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1261)) ([PR](https://github.com/dotnet/roslynator/pull/1285))
- Add analyzer "Unnecessary raw string literal" ([RCS1262](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1262)) ([PR](https://github.com/dotnet/roslynator/pull/1293))
- Add analyzer "Invalid reference in a documentation comment" ([RCS1263](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1263)) ([PR](https://github.com/dotnet/roslynator/pull/1295))
- Add analyzer "Add/remove blank line between switch sections" ([RCS0061](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS0061)) ([PR](https://github.com/dotnet/roslynator/pull/1302))
- Option (required): `roslynator_blank_line_between_switch_sections = include|omit|omit_after_block`
- Make analyzer [RCS0014](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS0014) obsolete

### Changed

Expand Down
99 changes: 99 additions & 0 deletions src/Analyzers.xml
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,8 @@ object this[int index] { get; }]]></After>
<Analyzer>
<Id>RCS0014</Id>
<Identifier>AddBlankLineBetweenSwitchSections</Identifier>
<Status>Obsolete</Status>
<ObsoleteMessage>Use RCS0061 instead</ObsoleteMessage>
<Title>Add blank line between switch sections</Title>
<DefaultSeverity>Info</DefaultSeverity>
<IsEnabledByDefault>false</IsEnabledByDefault>
Expand Down Expand Up @@ -1530,6 +1532,103 @@ public class C
</Link>
</Links>
</Analyzer>
<Analyzer>
<Id>RCS0061</Id>
<Identifier>BlankLineBetweenSwitchSections</Identifier>
<Title>Add/remove blank line between switch sections</Title>
<MessageFormat>{0} blank line between switch sections</MessageFormat>
<DefaultSeverity>Info</DefaultSeverity>
<IsEnabledByDefault>false</IsEnabledByDefault>
<ConfigOptions>
<Option Key="blank_line_between_switch_sections" IsRequired="true" />
</ConfigOptions>
<Samples>
<Sample>
<ConfigOptions>
<Option Key="blank_line_between_switch_sections" Value="include" />
</ConfigOptions>
<Before><![CDATA[switch (x)
{
case "foo":
return true;
case "bar":
return false;
default:
throw new InvalidOperationException();
}
]]></Before>
<After><![CDATA[switch (x)
{
case "foo":
return true;
case "bar":
return false;
default:
throw new InvalidOperationException();
}
]]></After>
</Sample>
<Sample>
<ConfigOptions>
<Option Key="blank_line_between_switch_sections" Value="omit" />
</ConfigOptions>
<Before><![CDATA[switch (x)
{
case "foo":
return true;
case "bar":
return false;
default:
throw new InvalidOperationException();
}
]]></Before>
<After><![CDATA[switch (x)
{
case "foo":
return true;
case "bar":
return false;
default:
throw new InvalidOperationException();
}
]]></After>
</Sample>
<Sample>
<ConfigOptions>
<Option Key="blank_line_between_switch_sections" Value="omit_after_block" />
</ConfigOptions>
<Before><![CDATA[switch (x)
{
case "foo":
{
return true;
}
case "bar":
{
return false;
}
}
]]></Before>
<After><![CDATA[switch (x)
{
case "foo":
{
return true;
}
case "bar":
{
return false;
}
}
]]></After>
</Sample>
</Samples>
</Analyzer>
<Analyzer>
<Id>RCS1001</Id>
<Identifier>AddBracesWhenExpressionSpansOverMultipleLines</Identifier>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ public override void Initialize(AnalysisContext context)
context.RegisterSyntaxNodeAction(f => AnalyzeInterfaceDeclaration(f), SyntaxKind.InterfaceDeclaration);
context.RegisterSyntaxNodeAction(f => AnalyzeEnumDeclaration(f), SyntaxKind.EnumDeclaration);
context.RegisterSyntaxNodeAction(f => AnalyzeNamespaceDeclaration(f), SyntaxKind.NamespaceDeclaration);
context.RegisterSyntaxNodeAction(f => AnalyzeSwitchStatement(f), SyntaxKind.SwitchStatement);
context.RegisterSyntaxNodeAction(f => AnalyzeTryStatement(f), SyntaxKind.TryStatement);
context.RegisterSyntaxNodeAction(f => AnalyzeElseClause(f), SyntaxKind.ElseClause);

Expand Down Expand Up @@ -147,35 +146,6 @@ private static void AnalyzeNamespaceDeclaration(SyntaxNodeAnalysisContext contex
AnalyzeEnd(context, members.Last(), namespaceDeclaration.CloseBraceToken);
}

private static void AnalyzeSwitchStatement(SyntaxNodeAnalysisContext context)
{
var switchStatement = (SwitchStatementSyntax)context.Node;

SyntaxList<SwitchSectionSyntax> sections = switchStatement.Sections;

if (sections.Any())
{
AnalyzeStart(context, sections[0], switchStatement.OpenBraceToken);
AnalyzeEnd(context, sections.Last(), switchStatement.CloseBraceToken);

if (sections.Count > 1
&& context.GetBlankLineBetweenClosingBraceAndSwitchSection() == false)
{
SwitchSectionSyntax prevSection = sections[0];

for (int i = 1; i < sections.Count; i++)
{
SwitchSectionSyntax section = sections[i];

if (prevSection.Statements.LastOrDefault() is BlockSyntax block)
Analyze(context, block.CloseBraceToken, section);

prevSection = section;
}
}
}
}

private static void AnalyzeTryStatement(SyntaxNodeAnalysisContext context)
{
var tryStatement = (TryStatementSyntax)context.Node;
Expand Down
11 changes: 11 additions & 0 deletions src/Common/CSharp/CodeStyle/BlankLineBetweenSwitchSections.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

namespace Roslynator.CSharp.CodeStyle;

internal enum BlankLineBetweenSwitchSections
{
None,
Include,
Omit,
OmitAfterBlock,
}
17 changes: 17 additions & 0 deletions src/Common/CSharp/Extensions/CodeStyleExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,23 @@ public static BlankLineStyle GetBlankLineAfterFileScopedNamespaceDeclaration(thi
return null;
}

public static BlankLineBetweenSwitchSections GetBlankLineBetweenSwitchSections(this SyntaxNodeAnalysisContext context)
{
if (ConfigOptions.TryGetValue(context.GetConfigOptions(), ConfigOptions.BlankLineBetweenSwitchSections, out string rawValue))
{
if (string.Equals(rawValue, ConfigOptionValues.BlankLineBetweenSwitchSections_Include, StringComparison.OrdinalIgnoreCase))
return BlankLineBetweenSwitchSections.Include;

if (string.Equals(rawValue, ConfigOptionValues.BlankLineBetweenSwitchSections_Omit, StringComparison.OrdinalIgnoreCase))
return BlankLineBetweenSwitchSections.Omit;

if (string.Equals(rawValue, ConfigOptionValues.BlankLineBetweenSwitchSections_OmitAfterBlock, StringComparison.OrdinalIgnoreCase))
return BlankLineBetweenSwitchSections.OmitAfterBlock;
}

return BlankLineBetweenSwitchSections.None;
}

public static NewLinePosition GetEqualsSignNewLinePosition(this SyntaxNodeAnalysisContext context)
{
return context.GetConfigOptions().GetEqualsTokenNewLinePosition();
Expand Down
1 change: 1 addition & 0 deletions src/Common/ConfigOptionKeys.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ internal static partial class ConfigOptionKeys
public const string BlankLineAfterFileScopedNamespaceDeclaration = "roslynator_blank_line_after_file_scoped_namespace_declaration";
public const string BlankLineBetweenClosingBraceAndSwitchSection = "roslynator_blank_line_between_closing_brace_and_switch_section";
public const string BlankLineBetweenSingleLineAccessors = "roslynator_blank_line_between_single_line_accessors";
public const string BlankLineBetweenSwitchSections = "roslynator_blank_line_between_switch_sections";
public const string BlankLineBetweenUsingDirectives = "roslynator_blank_line_between_using_directives";
public const string BlockBracesStyle = "roslynator_block_braces_style";
public const string BodyStyle = "roslynator_body_style";
Expand Down
3 changes: 3 additions & 0 deletions src/Common/ConfigOptionValues.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ internal static partial class ConfigOptionValues
public const string ArrowTokenNewLine_Before = "before";
public const string BinaryOperatorNewLine_After = "after";
public const string BinaryOperatorNewLine_Before = "before";
public const string BlankLineBetweenSwitchSections_Include = "include";
public const string BlankLineBetweenSwitchSections_Omit = "omit";
public const string BlankLineBetweenSwitchSections_OmitAfterBlock = "omit_after_block";
public const string BlankLineBetweenUsingDirectives_Never = "never";
public const string BlankLineBetweenUsingDirectives_SeparateGroups = "separate_groups";
public const string BlockBracesStyle_MultiLine = "multi_line";
Expand Down
7 changes: 7 additions & 0 deletions src/Common/ConfigOptions.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ public static partial class ConfigOptions
defaultValuePlaceholder: "true|false",
description: "Add/remove blank line between single-line accessors");

public static readonly ConfigOptionDescriptor BlankLineBetweenSwitchSections = new(
key: ConfigOptionKeys.BlankLineBetweenSwitchSections,
defaultValue: null,
defaultValuePlaceholder: "include|omit|omit_after_block",
description: "Include/omit blank line between switch sections");

public static readonly ConfigOptionDescriptor BlankLineBetweenUsingDirectives = new(
key: ConfigOptionKeys.BlankLineBetweenUsingDirectives,
defaultValue: null,
Expand Down Expand Up @@ -240,6 +246,7 @@ public static partial class ConfigOptions
yield return new KeyValuePair<string, string>("RCS0058", JoinOptionKeys(ConfigOptionKeys.NewLineAtEndOfFile));
yield return new KeyValuePair<string, string>("RCS0059", JoinOptionKeys(ConfigOptionKeys.NullConditionalOperatorNewLine));
yield return new KeyValuePair<string, string>("RCS0060", JoinOptionKeys(ConfigOptionKeys.BlankLineAfterFileScopedNamespaceDeclaration));
yield return new KeyValuePair<string, string>("RCS0061", JoinOptionKeys(ConfigOptionKeys.BlankLineBetweenSwitchSections));
yield return new KeyValuePair<string, string>("RCS1014", JoinOptionKeys(ConfigOptionKeys.ArrayCreationTypeStyle));
yield return new KeyValuePair<string, string>("RCS1016", JoinOptionKeys(ConfigOptionKeys.BodyStyle, ConfigOptionKeys.UseBlockBodyWhenDeclarationSpansOverMultipleLines, ConfigOptionKeys.UseBlockBodyWhenExpressionSpansOverMultipleLines));
yield return new KeyValuePair<string, string>("RCS1018", JoinOptionKeys(ConfigOptionKeys.AccessibilityModifiers));
Expand Down
8 changes: 8 additions & 0 deletions src/ConfigOptions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,14 @@
<Value>omit_when_single_line</Value>
</Values>
</Option>
<Option Id="BlankLineBetweenSwitchSections">
<Description>Include/omit blank line between switch sections</Description>
<Values>
<Value>include</Value>
<Value>omit</Value>
<Value>omit_after_block</Value>
</Values>
</Option>
<!--
<Option Id="">
<Key></Key>
Expand Down
4 changes: 0 additions & 4 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,4 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All"/>
</ItemGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<NoWarn>$(NoWarn);1573</NoWarn>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ public sealed class SwitchSectionCodeFixProvider : BaseCodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds
{
get { return ImmutableArray.Create(DiagnosticIdentifiers.AddBlankLineBetweenSwitchSections); }
get
{
return ImmutableArray.Create(
DiagnosticIdentifiers.AddBlankLineBetweenSwitchSections,
DiagnosticIdentifiers.BlankLineBetweenSwitchSections);
}
}

public override Task RegisterCodeFixesAsync(CodeFixContext context)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// 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 Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Roslynator.CSharp;
using Roslynator.CSharp.CodeStyle;

namespace Roslynator.Formatting.CSharp;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class BlankLineBetweenSwitchSectionsAnalyzer : BaseDiagnosticAnalyzer
{
private static ImmutableArray<DiagnosticDescriptor> _supportedDiagnostics;

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
{
get
{
if (_supportedDiagnostics.IsDefault)
Immutable.InterlockedInitialize(ref _supportedDiagnostics, DiagnosticRules.BlankLineBetweenSwitchSections);

return _supportedDiagnostics;
}
}

public override void Initialize(AnalysisContext context)
{
base.Initialize(context);

context.RegisterSyntaxNodeAction(c => AnalyzeSwitchStatement(c), SyntaxKind.SwitchStatement);
}

private static void AnalyzeSwitchStatement(SyntaxNodeAnalysisContext context)
{
var switchStatement = (SwitchStatementSyntax)context.Node;

BlankLineBetweenSwitchSections option = context.GetBlankLineBetweenSwitchSections();

if (option == BlankLineBetweenSwitchSections.None)
return;

SyntaxList<SwitchSectionSyntax> sections = switchStatement.Sections;
SyntaxList<SwitchSectionSyntax>.Enumerator en = sections.GetEnumerator();

if (!en.MoveNext())
return;

SwitchSectionSyntax previousSection = en.Current;
StatementSyntax previousLastStatement = previousSection.Statements.LastOrDefault();

while (en.MoveNext())
{
TriviaBlockAnalysis analysis = TriviaBlockAnalysis.FromBetween(previousSection, en.Current);

if (!analysis.Success)
continue;

if (analysis.Kind == TriviaBlockKind.BlankLine)
{
if (option == BlankLineBetweenSwitchSections.Omit)
{
ReportDiagnostic(context, analysis, "Remove");
}
else if (option == BlankLineBetweenSwitchSections.OmitAfterBlock
&& previousLastStatement.IsKind(SyntaxKind.Block))
{
ReportDiagnostic(context, analysis, "Remove");
}
}
else if (option == BlankLineBetweenSwitchSections.Include)
{
ReportDiagnostic(context, analysis, "Add");
}
else if (option == BlankLineBetweenSwitchSections.OmitAfterBlock
&& !previousLastStatement.IsKind(SyntaxKind.Block))
{
ReportDiagnostic(context, analysis, "Add");
}

previousSection = en.Current;
previousLastStatement = previousSection.Statements.LastOrDefault();
}
}

private static void ReportDiagnostic(SyntaxNodeAnalysisContext context, TriviaBlockAnalysis analysis, string messageArg)
{
context.ReportDiagnostic(
DiagnosticRules.BlankLineBetweenSwitchSections,
analysis.GetLocation(),
messageArg);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,6 @@ public static partial class DiagnosticIdentifiers
public const string NormalizeWhitespaceAtEndOfFile = "RCS0058";
public const string PlaceNewLineAfterOrBeforeNullConditionalOperator = "RCS0059";
public const string BlankLineAfterFileScopedNamespaceDeclaration = "RCS0060";
public const string BlankLineBetweenSwitchSections = "RCS0061";
}
}

0 comments on commit 1039445

Please sign in to comment.