diff --git a/src/Analyzers.xml b/src/Analyzers.xml index 3668c8bb60..f9cb4d3e1a 100644 --- a/src/Analyzers.xml +++ b/src/Analyzers.xml @@ -1629,6 +1629,23 @@ public class C + + RCS0062 + PutExpressionBodyOnItsOwnLine + Put expression body on its own line + Info + false + + + null;]]> + null;]]> + + + + + RCS1001 AddBracesWhenExpressionSpansOverMultipleLines @@ -7973,4 +7990,4 @@ class FooCodeFixProvider : CodeFixProvider - + \ No newline at end of file diff --git a/src/Formatting.Analyzers.CodeFixes/CSharp/SyntaxTokenCodeFixProvider.cs b/src/Formatting.Analyzers.CodeFixes/CSharp/SyntaxTokenCodeFixProvider.cs index 6d6330cd05..c51dd4a5a6 100644 --- a/src/Formatting.Analyzers.CodeFixes/CSharp/SyntaxTokenCodeFixProvider.cs +++ b/src/Formatting.Analyzers.CodeFixes/CSharp/SyntaxTokenCodeFixProvider.cs @@ -25,7 +25,8 @@ public override ImmutableArray FixableDiagnosticIds DiagnosticIdentifiers.PlaceNewLineAfterOrBeforeArrowToken, DiagnosticIdentifiers.PlaceNewLineAfterOrBeforeEqualsToken, DiagnosticIdentifiers.PutAttributeListOnItsOwnLine, - DiagnosticIdentifiers.AddOrRemoveNewLineBeforeWhileInDoStatement); + DiagnosticIdentifiers.AddOrRemoveNewLineBeforeWhileInDoStatement, + DiagnosticIdentifiers.PutExpressionBodyOnItsOwnLine); } } @@ -61,6 +62,11 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) await CodeActionFactory.RegisterCodeActionForNewLineAsync(context).ConfigureAwait(false); break; } + case DiagnosticIdentifiers.PutExpressionBodyOnItsOwnLine: + { + await CodeActionFactory.RegisterCodeActionForNewLineAsync(context, increaseIndentation: true).ConfigureAwait(false); + break; + } } } } diff --git a/src/Formatting.Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs b/src/Formatting.Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs index 937b9df58b..d99ce3aab4 100644 --- a/src/Formatting.Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs +++ b/src/Formatting.Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs @@ -61,5 +61,6 @@ public static partial class DiagnosticIdentifiers public const string PlaceNewLineAfterOrBeforeNullConditionalOperator = "RCS0059"; public const string BlankLineAfterFileScopedNamespaceDeclaration = "RCS0060"; public const string BlankLineBetweenSwitchSections = "RCS0061"; + public const string PutExpressionBodyOnItsOwnLine = "RCS0062"; } } \ No newline at end of file diff --git a/src/Formatting.Analyzers/CSharp/DiagnosticRules.Generated.cs b/src/Formatting.Analyzers/CSharp/DiagnosticRules.Generated.cs index bb553cf2b9..26d94d82d7 100644 --- a/src/Formatting.Analyzers/CSharp/DiagnosticRules.Generated.cs +++ b/src/Formatting.Analyzers/CSharp/DiagnosticRules.Generated.cs @@ -645,5 +645,17 @@ public static partial class DiagnosticRules helpLinkUri: DiagnosticIdentifiers.BlankLineBetweenSwitchSections, customTags: Array.Empty()); + /// RCS0062 + public static readonly DiagnosticDescriptor PutExpressionBodyOnItsOwnLine = DiagnosticDescriptorFactory.Create( + id: DiagnosticIdentifiers.PutExpressionBodyOnItsOwnLine, + title: "Put expression body on its own line", + messageFormat: "Put expression body on its own line", + category: DiagnosticCategories.Roslynator, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: false, + description: null, + helpLinkUri: DiagnosticIdentifiers.PutExpressionBodyOnItsOwnLine, + customTags: Array.Empty()); + } } \ No newline at end of file diff --git a/src/Formatting.Analyzers/CSharp/PutExpressionBodyOnItsOwnLineAnalyzer.cs b/src/Formatting.Analyzers/CSharp/PutExpressionBodyOnItsOwnLineAnalyzer.cs new file mode 100644 index 0000000000..2ccb453d5c --- /dev/null +++ b/src/Formatting.Analyzers/CSharp/PutExpressionBodyOnItsOwnLineAnalyzer.cs @@ -0,0 +1,81 @@ +// 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 PutExpressionBodyOnItsOwnLineAnalyzer : BaseDiagnosticAnalyzer +{ + private static ImmutableArray _supportedDiagnostics; + + public override ImmutableArray SupportedDiagnostics + { + get + { + if (_supportedDiagnostics.IsDefault) + Immutable.InterlockedInitialize(ref _supportedDiagnostics, DiagnosticRules.PutExpressionBodyOnItsOwnLine); + + return _supportedDiagnostics; + } + } + + public override void Initialize(AnalysisContext context) + { + base.Initialize(context); + + context.RegisterSyntaxNodeAction(f => AnalyzeArrowExpressionClause(f), SyntaxKind.ArrowExpressionClause); + } + + private static void AnalyzeArrowExpressionClause(SyntaxNodeAnalysisContext context) + { + var arrowExpressionClause = (ArrowExpressionClauseSyntax)context.Node; + + switch (arrowExpressionClause.Parent.Kind()) + { + case SyntaxKind.MethodDeclaration: + case SyntaxKind.ConstructorDeclaration: + case SyntaxKind.DestructorDeclaration: + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.IndexerDeclaration: + case SyntaxKind.OperatorDeclaration: + case SyntaxKind.ConversionOperatorDeclaration: + AnalyzeArrowExpressionClause(arrowExpressionClause.ArrowToken, context); + break; + } + } + + private static void AnalyzeArrowExpressionClause(SyntaxToken arrowToken, SyntaxNodeAnalysisContext context) + { + NewLinePosition newLinePosition = context.GetArrowTokenNewLinePosition(); + + SyntaxToken first; + SyntaxToken second; + if (newLinePosition == NewLinePosition.After) + { + first = arrowToken; + second = arrowToken.GetNextToken(); + } + else + { + first = arrowToken.GetPreviousToken(); + second = arrowToken; + } + + TriviaBlock block = TriviaBlock.FromBetween(first, second); + + if (block.Kind == TriviaBlockKind.NoNewLine) + { + DiagnosticHelpers.ReportDiagnostic( + context, + DiagnosticRules.PutExpressionBodyOnItsOwnLine, + block.GetLocation()); + } + } +} diff --git a/src/Tests/Formatting.Analyzers.Tests/RCS0062PutExpressionBodyOnItsOwnLineTests.cs b/src/Tests/Formatting.Analyzers.Tests/RCS0062PutExpressionBodyOnItsOwnLineTests.cs new file mode 100644 index 0000000000..be60edeae7 --- /dev/null +++ b/src/Tests/Formatting.Analyzers.Tests/RCS0062PutExpressionBodyOnItsOwnLineTests.cs @@ -0,0 +1,276 @@ +// 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 RCS0062PutExpressionBodyOnItsOwnLineTests : AbstractCSharpDiagnosticVerifier +{ + public override DiagnosticDescriptor Descriptor { get; } = DiagnosticRules.PutExpressionBodyOnItsOwnLine; + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.PutExpressionBodyOnItsOwnLine)] + public async Task Test_Constructor_BreakAfterArrow() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ + public C() =>[||] M(); + + void M() { } +} +", @" +class C +{ + public C() => + M(); + + void M() { } +} +", options: Options.AddConfigOption(ConfigOptionKeys.ArrowTokenNewLine, ConfigOptionValues.ArrowTokenNewLine_After)); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.PutExpressionBodyOnItsOwnLine)] + public async Task Test_Constructor_BreakBeforeArrow() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ + public C()[||] => M(); + + void M() { } +} +", @" +class C +{ + public C() + => M(); + + void M() { } +} +", options: Options.AddConfigOption(ConfigOptionKeys.ArrowTokenNewLine, ConfigOptionValues.ArrowTokenNewLine_Before)); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.PutExpressionBodyOnItsOwnLine)] + public async Task Test_Destructor_BreakAfterArrow() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ + ~C() =>[||] M(); + + void M() { } +} +", @" +class C +{ + ~C() => + M(); + + void M() { } +} +", options: Options.AddConfigOption(ConfigOptionKeys.ArrowTokenNewLine, ConfigOptionValues.ArrowTokenNewLine_After)); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.PutExpressionBodyOnItsOwnLine)] + public async Task Test_Destructor_BreakBeforeArrow() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ + ~C()[||] => M(); + + void M() { } +} +", @" +class C +{ + ~C() + => M(); + + void M() { } +} +", options: Options.AddConfigOption(ConfigOptionKeys.ArrowTokenNewLine, ConfigOptionValues.ArrowTokenNewLine_Before)); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.PutExpressionBodyOnItsOwnLine)] + public async Task Test_Method_BreakAfterArrow() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ + string M() =>[||] null; +} +", @" +class C +{ + string M() => + null; +} +", options: Options.AddConfigOption(ConfigOptionKeys.ArrowTokenNewLine, ConfigOptionValues.ArrowTokenNewLine_After)); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.PutExpressionBodyOnItsOwnLine)] + public async Task Test_Method_BreakBeforeArrow() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ + string M()[||] => null; +} +", @" +class C +{ + string M() + => null; +} +", options: Options.AddConfigOption(ConfigOptionKeys.ArrowTokenNewLine, ConfigOptionValues.ArrowTokenNewLine_Before)); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.PutExpressionBodyOnItsOwnLine)] + public async Task Test_Operator_BreakAfterArrow() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ + public static C operator !(C value) =>[||] value; +} +", @" +class C +{ + public static C operator !(C value) => + value; +} +", options: Options.AddConfigOption(ConfigOptionKeys.ArrowTokenNewLine, ConfigOptionValues.ArrowTokenNewLine_After)); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.PutExpressionBodyOnItsOwnLine)] + public async Task Test_Operator_BreakBeforeArrow() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ + public static C operator !(C value)[||] => value; +} +", @" +class C +{ + public static C operator !(C value) + => value; +} +", options: Options.AddConfigOption(ConfigOptionKeys.ArrowTokenNewLine, ConfigOptionValues.ArrowTokenNewLine_Before)); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.PutExpressionBodyOnItsOwnLine)] + public async Task Test_ConversionOperator_BreakAfterArrow() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ + public static explicit operator C(string value) =>[||] new C(); +} +", @" +class C +{ + public static explicit operator C(string value) => + new C(); +} +", options: Options.AddConfigOption(ConfigOptionKeys.ArrowTokenNewLine, ConfigOptionValues.ArrowTokenNewLine_After)); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.PutExpressionBodyOnItsOwnLine)] + public async Task Test_ConversionOperator_BreakBeforeArrow() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ + public static explicit operator C(string value)[||] => new C(); +} +", @" +class C +{ + public static explicit operator C(string value) + => new C(); +} +", options: Options.AddConfigOption(ConfigOptionKeys.ArrowTokenNewLine, ConfigOptionValues.ArrowTokenNewLine_Before)); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.PutExpressionBodyOnItsOwnLine)] + public async Task Test_PropertyWithoutAccessor_BreakAfterArrow() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ + string P =>[||] null; +} +", @" +class C +{ + string P => + null; +} +", options: Options.AddConfigOption(ConfigOptionKeys.ArrowTokenNewLine, ConfigOptionValues.ArrowTokenNewLine_After)); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.PutExpressionBodyOnItsOwnLine)] + public async Task Test_PropertyWithoutAccessor_BreakBeforeArrow() + { + await VerifyDiagnosticAndFixAsync(@" +class C +{ + string P[||] => null; +} +", @" +class C +{ + string P + => null; +} +", options: Options.AddConfigOption(ConfigOptionKeys.ArrowTokenNewLine, ConfigOptionValues.ArrowTokenNewLine_Before)); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.PutExpressionBodyOnItsOwnLine)] + public async Task TestNoDiagnostic_LocalFunction() + { + await VerifyNoDiagnosticAsync(@" +class C +{ + void M() + { + string LF() => null; + } +} +", options: Options.AddConfigOption(ConfigOptionKeys.ArrowTokenNewLine, ConfigOptionValues.ArrowTokenNewLine_Before)); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.PutExpressionBodyOnItsOwnLine)] + public async Task TestNoDiagnostic_PropertyWithGetter() + { + await VerifyNoDiagnosticAsync(@" +class C +{ + string P + { + get => null; + } +} +", options: Options.AddConfigOption(ConfigOptionKeys.ArrowTokenNewLine, ConfigOptionValues.ArrowTokenNewLine_Before)); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.PutExpressionBodyOnItsOwnLine)] + public async Task TestNoDiagnostic_IndexerWithGetter() + { + await VerifyNoDiagnosticAsync(@" +class C +{ + string this[int index] + { + get => null; + } +} +", options: Options.AddConfigOption(ConfigOptionKeys.ArrowTokenNewLine, ConfigOptionValues.ArrowTokenNewLine_Before)); + } +} diff --git a/src/VisualStudioCode/package/src/configurationFiles.generated.ts b/src/VisualStudioCode/package/src/configurationFiles.generated.ts index 4af6961de1..6a94390b6f 100644 --- a/src/VisualStudioCode/package/src/configurationFiles.generated.ts +++ b/src/VisualStudioCode/package/src/configurationFiles.generated.ts @@ -42,7 +42,7 @@ roslynator_analyzers.enabled_by_default = true|false # Applicable to: rcs1014 #roslynator_arrow_token_new_line = after|before -# Applicable to: rcs0032 +# Applicable to: rcs0032, rcs0062 #roslynator_binary_operator_new_line = after|before # Applicable to: rcs0027 @@ -309,6 +309,10 @@ roslynator_analyzers.enabled_by_default = true|false #dotnet_diagnostic.rcs0061.severity = none # Options: roslynator_blank_line_between_switch_sections +# Put expression body on its own line +#dotnet_diagnostic.rcs0062.severity = none +# Options: roslynator_arrow_token_new_line + # Add braces (when expression spans over multiple lines) #dotnet_diagnostic.rcs1001.severity = suggestion