From 6ba79674a6412ea502b140551602c600daf84233 Mon Sep 17 00:00:00 2001 From: Josef Pihrt Date: Fri, 18 Nov 2022 23:42:35 +0100 Subject: [PATCH 01/10] Analyzer: Add empty line after file scoped namespace --- ...opedNamespaceDeclarationCodeFixProvider.cs | 66 ++++++++++ ...ptyLineAfterFileScopedNamespaceAnalyzer.cs | 90 ++++++++++++++ .../CSharp/DiagnosticIdentifiers.Generated.cs | 1 + .../CSharp/DiagnosticRules.Generated.cs | 12 ++ ...s.AddEmptyLineAfterFileScopedNamespace.xml | 30 +++++ ...dEmptyLineAfterFileScopedNamespaceTests.cs | 115 ++++++++++++++++++ 6 files changed, 314 insertions(+) create mode 100644 src/Formatting.Analyzers.CodeFixes/CSharp/FileScopedNamespaceDeclarationCodeFixProvider.cs create mode 100644 src/Formatting.Analyzers/CSharp/AddEmptyLineAfterFileScopedNamespaceAnalyzer.cs create mode 100644 src/Formatting.Analyzers/Formatting.Analyzers.AddEmptyLineAfterFileScopedNamespace.xml create mode 100644 src/Tests/Formatting.Analyzers.Tests/RCS0060AddEmptyLineAfterFileScopedNamespaceTests.cs diff --git a/src/Formatting.Analyzers.CodeFixes/CSharp/FileScopedNamespaceDeclarationCodeFixProvider.cs b/src/Formatting.Analyzers.CodeFixes/CSharp/FileScopedNamespaceDeclarationCodeFixProvider.cs new file mode 100644 index 0000000000..5866dec354 --- /dev/null +++ b/src/Formatting.Analyzers.CodeFixes/CSharp/FileScopedNamespaceDeclarationCodeFixProvider.cs @@ -0,0 +1,66 @@ +// Copyright (c) Josef Pihrt 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.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Roslynator.CSharp; +using Roslynator.Formatting.CSharp; + +namespace Roslynator.Formatting.CodeFixes.CSharp +{ + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(FileScopedNamespaceDeclarationCodeFixProvider))] + [Shared] + public sealed class FileScopedNamespaceDeclarationCodeFixProvider : BaseCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds + { + get { return ImmutableArray.Create(DiagnosticIdentifiers.AddEmptyLineAfterFileScopedNamespace); } + } + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + SyntaxNode root = await context.GetSyntaxRootAsync().ConfigureAwait(false); + + if (!TryFindFirstAncestorOrSelf(root, context.Span, out FileScopedNamespaceDeclarationSyntax fileScopedNamespace)) + return; + + Document document = context.Document; + Diagnostic diagnostic = context.Diagnostics[0]; + + switch (diagnostic.Id) + { + case DiagnosticIdentifiers.AddEmptyLineAfterFileScopedNamespace: + { + CodeAction codeAction = CodeAction.Create( + "Add empty line", + ct => + { + MemberDeclarationSyntax member = fileScopedNamespace.Members[0]; + + MemberDeclarationSyntax newMember; + if (!fileScopedNamespace.SemicolonToken.TrailingTrivia.Contains(SyntaxKind.EndOfLineTrivia)) + { + newMember = member.PrependToLeadingTrivia(new SyntaxTrivia[] { CSharpFactory.NewLine(), CSharpFactory.NewLine() }); + } + else + { + newMember = member.PrependEndOfLineToLeadingTrivia(); + } + + return document.ReplaceNodeAsync(member, newMember, ct); + }, + GetEquivalenceKey(diagnostic)); + + context.RegisterCodeFix(codeAction, diagnostic); + break; + } + } + } + } +} diff --git a/src/Formatting.Analyzers/CSharp/AddEmptyLineAfterFileScopedNamespaceAnalyzer.cs b/src/Formatting.Analyzers/CSharp/AddEmptyLineAfterFileScopedNamespaceAnalyzer.cs new file mode 100644 index 0000000000..8e0928ed89 --- /dev/null +++ b/src/Formatting.Analyzers/CSharp/AddEmptyLineAfterFileScopedNamespaceAnalyzer.cs @@ -0,0 +1,90 @@ +// Copyright (c) Josef Pihrt 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 Microsoft.CodeAnalysis.Text; +using Roslynator.CSharp; + +namespace Roslynator.Formatting.CSharp +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class AddEmptyLineAfterFileScopedNamespaceAnalyzer : BaseDiagnosticAnalyzer + { + private static ImmutableArray _supportedDiagnostics; + + public override ImmutableArray SupportedDiagnostics + { + get + { + if (_supportedDiagnostics.IsDefault) + Immutable.InterlockedInitialize(ref _supportedDiagnostics, DiagnosticRules.AddEmptyLineAfterFileScopedNamespace); + + return _supportedDiagnostics; + } + } + + public override void Initialize(AnalysisContext context) + { + base.Initialize(context); + + context.RegisterSyntaxNodeAction(f => AnalyzeFileScopedNamespaceDeclaration(f), SyntaxKind.FileScopedNamespaceDeclaration); + } + + private static void AnalyzeFileScopedNamespaceDeclaration(SyntaxNodeAnalysisContext context) + { + var fileScopedNamespace = (FileScopedNamespaceDeclarationSyntax)context.Node; + + MemberDeclarationSyntax member = fileScopedNamespace.Members.FirstOrDefault(); + + if (member is not null + && AnalyzeTrailingTrivia() + && AnalyzeLeadingTrivia()) + { + context.ReportDiagnostic( + DiagnosticRules.AddEmptyLineAfterFileScopedNamespace, + Location.Create(fileScopedNamespace.SyntaxTree, new TextSpan(member.FullSpan.Start, 0))); + } + + bool AnalyzeTrailingTrivia() + { + SyntaxTriviaList.Enumerator en = fileScopedNamespace.SemicolonToken.TrailingTrivia.GetEnumerator(); + + if (!en.MoveNext()) + return true; + + if (en.Current.IsWhitespaceTrivia() + && !en.MoveNext()) + { + return true; + } + + if (en.Current.IsKind(SyntaxKind.SingleLineCommentTrivia) + && !en.MoveNext()) + { + return true; + } + + return en.Current.IsEndOfLineTrivia(); + } + + bool AnalyzeLeadingTrivia() + { + SyntaxTriviaList.Enumerator en = member.GetLeadingTrivia().GetEnumerator(); + + if (!en.MoveNext()) + return true; + + if (en.Current.IsWhitespaceTrivia() + && !en.MoveNext()) + { + return true; + } + + return en.Current.IsWhitespaceTrivia(); + } + } + } +} diff --git a/src/Formatting.Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs b/src/Formatting.Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs index e9858c2e95..aab4c734a0 100644 --- a/src/Formatting.Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs +++ b/src/Formatting.Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs @@ -59,5 +59,6 @@ public static partial class DiagnosticIdentifiers public const string NormalizeWhitespaceAtBeginningOfFile = "RCS0057"; public const string NormalizeWhitespaceAtEndOfFile = "RCS0058"; public const string PlaceNewLineAfterOrBeforeNullConditionalOperator = "RCS0059"; + public const string AddEmptyLineAfterFileScopedNamespace = "RCS0060"; } } \ 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 61e942bc3a..83f91fd77b 100644 --- a/src/Formatting.Analyzers/CSharp/DiagnosticRules.Generated.cs +++ b/src/Formatting.Analyzers/CSharp/DiagnosticRules.Generated.cs @@ -621,5 +621,17 @@ public static partial class DiagnosticRules helpLinkUri: DiagnosticIdentifiers.PlaceNewLineAfterOrBeforeNullConditionalOperator, customTags: Array.Empty()); + /// RCS0060 + public static readonly DiagnosticDescriptor AddEmptyLineAfterFileScopedNamespace = DiagnosticDescriptorFactory.Create( + id: DiagnosticIdentifiers.AddEmptyLineAfterFileScopedNamespace, + title: "Add empty line after file scoped namespace.", + messageFormat: "Add empty line after file scoped namespace.", + category: DiagnosticCategories.Roslynator, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: false, + description: null, + helpLinkUri: DiagnosticIdentifiers.AddEmptyLineAfterFileScopedNamespace, + customTags: Array.Empty()); + } } \ No newline at end of file diff --git a/src/Formatting.Analyzers/Formatting.Analyzers.AddEmptyLineAfterFileScopedNamespace.xml b/src/Formatting.Analyzers/Formatting.Analyzers.AddEmptyLineAfterFileScopedNamespace.xml new file mode 100644 index 0000000000..09d4c902e0 --- /dev/null +++ b/src/Formatting.Analyzers/Formatting.Analyzers.AddEmptyLineAfterFileScopedNamespace.xml @@ -0,0 +1,30 @@ + + + + RCS0060 + Add empty line after file scoped namespace. + Formatting + Info + false + 10.0 + + + + + + + + + https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/file-scoped-namespaces + File Scoped Namespaces + + + + \ No newline at end of file diff --git a/src/Tests/Formatting.Analyzers.Tests/RCS0060AddEmptyLineAfterFileScopedNamespaceTests.cs b/src/Tests/Formatting.Analyzers.Tests/RCS0060AddEmptyLineAfterFileScopedNamespaceTests.cs new file mode 100644 index 0000000000..bff90a8a94 --- /dev/null +++ b/src/Tests/Formatting.Analyzers.Tests/RCS0060AddEmptyLineAfterFileScopedNamespaceTests.cs @@ -0,0 +1,115 @@ +// Copyright (c) Josef Pihrt 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 RCS0060AddEmptyLineAfterFileScopedNamespaceTests : AbstractCSharpDiagnosticVerifier + { + public override DiagnosticDescriptor Descriptor { get; } = DiagnosticRules.AddEmptyLineAfterFileScopedNamespace; + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.AddEmptyLineAfterFileScopedNamespace)] + public async Task Test() + { + await VerifyDiagnosticAndFixAsync(@" +namespace A.B; +[||]class C +{ +} +", @" +namespace A.B; + +class C +{ +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.AddEmptyLineAfterFileScopedNamespace)] + public async Task Test2() + { + await VerifyDiagnosticAndFixAsync(@" +namespace A.B; +[||]class C +{ +} +", @" +namespace A.B; + +class C +{ +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.AddEmptyLineAfterFileScopedNamespace)] + public async Task Test3() + { + await VerifyDiagnosticAndFixAsync(@" +namespace A.B; //x +[||]class C +{ +} +", @" +namespace A.B; //x + +class C +{ +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.AddEmptyLineAfterFileScopedNamespace)] + public async Task Test4() + { + await VerifyDiagnosticAndFixAsync(@" +namespace A.B;//x +[||]class C +{ +} +", @" +namespace A.B;//x + +class C +{ +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.AddEmptyLineAfterFileScopedNamespace)] + public async Task Test5() + { + await VerifyDiagnosticAndFixAsync(@" +namespace A.B;[||]class C +{ +} +", @" +namespace A.B; + +class C +{ +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.AddEmptyLineAfterFileScopedNamespace)] + public async Task Test6() + { + await VerifyDiagnosticAndFixAsync(@" +namespace A.B; [||]class C +{ +} +", @" +namespace A.B; + +class C +{ +} +"); + } + } +} From bb818e3f9b0b059a64841a0e8a5f1ccdb5cc890e Mon Sep 17 00:00:00 2001 From: Josef Pihrt Date: Fri, 18 Nov 2022 23:44:35 +0100 Subject: [PATCH 02/10] changelog --- ChangeLog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ChangeLog.md b/ChangeLog.md index f469b88c58..eed34dd0ca 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add Arm64 VS 2022 extension support ([#990](https://github.com/JosefPihrt/Roslynator/pull/990) by @snickler). +- Add analyzer "Add emmpty line after file scoped namespace" [RCS0060](https://github.com/JosefPihrt/Roslynator/blob/main/docs/analyzers/RCS0060.md) ([#993](https://github.com/josefpihrt/roslynator/pull/993). ### Changed From 4d0759b85572be16d888dab20253b3af5f269759 Mon Sep 17 00:00:00 2001 From: Josef Pihrt Date: Sat, 19 Nov 2022 13:31:03 +0100 Subject: [PATCH 03/10] update --- .editorconfig | 4 + .../CSharp/Extensions/CodeStyleExtensions.cs | 10 + src/Common/ConfigOptionKeys.Generated.cs | 1 + src/Common/ConfigOptions.Generated.cs | 7 + src/Common/ConfigOptions.xml | 4 + ...opedNamespaceDeclarationCodeFixProvider.cs | 59 ++++-- ...ptyLineAfterFileScopedNamespaceAnalyzer.cs | 90 --------- ...rFileScopedNamespaceDeclarationAnalyzer.cs | 136 +++++++++++++ .../CSharp/DiagnosticIdentifiers.Generated.cs | 2 +- .../CSharp/DiagnosticRules.Generated.cs | 10 +- ...ers.BlankLineAfterFileScopedNamespace.xml} | 8 +- ...dEmptyLineAfterFileScopedNamespaceTests.cs | 185 ++++++++++++++++-- 12 files changed, 379 insertions(+), 137 deletions(-) delete mode 100644 src/Formatting.Analyzers/CSharp/AddEmptyLineAfterFileScopedNamespaceAnalyzer.cs create mode 100644 src/Formatting.Analyzers/CSharp/BlankLineAfterFileScopedNamespaceDeclarationAnalyzer.cs rename src/Formatting.Analyzers/{Formatting.Analyzers.AddEmptyLineAfterFileScopedNamespace.xml => Formatting.Analyzers.BlankLineAfterFileScopedNamespace.xml} (65%) diff --git a/.editorconfig b/.editorconfig index afcdd75fe1..034defa6af 100644 --- a/.editorconfig +++ b/.editorconfig @@ -35,6 +35,9 @@ roslynator_use_var_instead_of_implicit_object_creation = true roslynator_infinite_loop_style = while roslynator_doc_comment_summary_style = multi_line roslynator_enum_flag_value_style = shift_operator +roslynator_blank_line_after_file_scoped_namespace_declaration = true + +csharp_style_namespace_declarations = file_scoped:suggestion dotnet_diagnostic.RCS0001.severity = suggestion dotnet_diagnostic.RCS0003.severity = suggestion @@ -75,6 +78,7 @@ dotnet_diagnostic.RCS0055.severity = suggestion dotnet_diagnostic.RCS0057.severity = suggestion dotnet_diagnostic.RCS0058.severity = suggestion dotnet_diagnostic.RCS0059.severity = suggestion +dotnet_diagnostic.RCS0060.severity = suggestion dotnet_diagnostic.RCS1002.severity = silent dotnet_diagnostic.RCS1006.severity = suggestion dotnet_diagnostic.RCS1008.severity = suggestion diff --git a/src/Common/CSharp/Extensions/CodeStyleExtensions.cs b/src/Common/CSharp/Extensions/CodeStyleExtensions.cs index 9d821a1078..aaba4294f7 100644 --- a/src/Common/CSharp/Extensions/CodeStyleExtensions.cs +++ b/src/Common/CSharp/Extensions/CodeStyleExtensions.cs @@ -495,6 +495,16 @@ public static BodyStyle GetBodyStyle(this SyntaxNodeAnalysisContext context) return null; } + public static BlankLineStyle GetBlankLineAfterFileScopedNamespaceDeclaration(this SyntaxNodeAnalysisContext context) + { + if (ConfigOptions.TryGetValueAsBool(context.GetConfigOptions(), ConfigOptions.BlankLineAfterFileScopedNamespaceDeclaration, out bool value)) + { + return (value) ? BlankLineStyle.Add : BlankLineStyle.Remove; + } + + return BlankLineStyle.None; + } + public static bool? GetSuppressUnityScriptMethods(this SyntaxNodeAnalysisContext context) { if (ConfigOptions.TryGetValueAsBool(context.GetConfigOptions(), ConfigOptions.SuppressUnityScriptMethods, out bool value)) diff --git a/src/Common/ConfigOptionKeys.Generated.cs b/src/Common/ConfigOptionKeys.Generated.cs index 88def25b2f..18b43bc31c 100644 --- a/src/Common/ConfigOptionKeys.Generated.cs +++ b/src/Common/ConfigOptionKeys.Generated.cs @@ -11,6 +11,7 @@ internal static partial class ConfigOptionKeys public const string ArrayCreationTypeStyle = "roslynator_array_creation_type_style"; public const string ArrowTokenNewLine = "roslynator_arrow_token_new_line"; public const string BinaryOperatorNewLine = "roslynator_binary_operator_new_line"; + 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 BlankLineBetweenUsingDirectives = "roslynator_blank_line_between_using_directives"; diff --git a/src/Common/ConfigOptions.Generated.cs b/src/Common/ConfigOptions.Generated.cs index 4e8be78351..755d624575 100644 --- a/src/Common/ConfigOptions.Generated.cs +++ b/src/Common/ConfigOptions.Generated.cs @@ -38,6 +38,12 @@ public static partial class ConfigOptions defaultValuePlaceholder: "after|before", description: "Place new line after/before binary operator"); + public static readonly ConfigOptionDescriptor BlankLineAfterFileScopedNamespaceDeclaration = new( + key: ConfigOptionKeys.BlankLineAfterFileScopedNamespaceDeclaration, + defaultValue: null, + defaultValuePlaceholder: "true|false", + description: "Add/remove blank line after file scoped namespace declaration"); + public static readonly ConfigOptionDescriptor BlankLineBetweenClosingBraceAndSwitchSection = new( key: ConfigOptionKeys.BlankLineBetweenClosingBraceAndSwitchSection, defaultValue: null, @@ -213,6 +219,7 @@ public static partial class ConfigOptions yield return new KeyValuePair("RCS0052", JoinOptionKeys(ConfigOptionKeys.EqualsTokenNewLine)); yield return new KeyValuePair("RCS0058", JoinOptionKeys(ConfigOptionKeys.NewLineAtEndOfFile)); yield return new KeyValuePair("RCS0059", JoinOptionKeys(ConfigOptionKeys.NullConditionalOperatorNewLine)); + yield return new KeyValuePair("RCS0060", JoinOptionKeys(ConfigOptionKeys.BlankLineAfterFileScopedNamespaceDeclaration)); yield return new KeyValuePair("RCS1014", JoinOptionKeys(ConfigOptionKeys.ArrayCreationTypeStyle)); yield return new KeyValuePair("RCS1016", JoinOptionKeys(ConfigOptionKeys.BodyStyle, ConfigOptionKeys.UseBlockBodyWhenDeclarationSpansOverMultipleLines, ConfigOptionKeys.UseBlockBodyWhenExpressionSpansOverMultipleLines)); yield return new KeyValuePair("RCS1018", JoinOptionKeys(ConfigOptionKeys.AccessibilityModifiers)); diff --git a/src/Common/ConfigOptions.xml b/src/Common/ConfigOptions.xml index 303a6fa295..e571fe5d67 100644 --- a/src/Common/ConfigOptions.xml +++ b/src/Common/ConfigOptions.xml @@ -195,6 +195,10 @@ Use 'for'/'while' statement as an infinite loop +