diff --git a/ChangeLog.md b/ChangeLog.md index f5245273a2..7010f94c22 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix [RCS1084](https://github.com/JosefPihrt/Roslynator/blob/main/docs/analyzers/RCS1084.md) ([#1006](https://github.com/josefpihrt/roslynator/pull/1006)). - Fix [RCS1244](https://github.com/JosefPihrt/Roslynator/blob/main/docs/analyzers/RCS1244.md) ([#1007](https://github.com/josefpihrt/roslynator/pull/1007)). - [CLI] Add nullable reference type modifier when creating a list of symbols (`list-symbols` command) ([#1013](https://github.com/josefpihrt/roslynator/pull/1013)). +- Add/remove blank line after file scoped namespace declaration ([RCS0060](https://github.com/JosefPihrt/Roslynator/blob/main/docs/analyzers/RCS0060.md)) ([#1014](https://github.com/josefpihrt/roslynator/pull/1014)). ## [4.2.0] - 2022-11-27 diff --git a/src/Formatting.Analyzers.CodeFixes/CSharp/FileScopedNamespaceDeclarationCodeFixProvider.cs b/src/Formatting.Analyzers.CodeFixes/CSharp/FileScopedNamespaceDeclarationCodeFixProvider.cs index 603ef37ebf..adf7e59b61 100644 --- a/src/Formatting.Analyzers.CodeFixes/CSharp/FileScopedNamespaceDeclarationCodeFixProvider.cs +++ b/src/Formatting.Analyzers.CodeFixes/CSharp/FileScopedNamespaceDeclarationCodeFixProvider.cs @@ -34,8 +34,8 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) Document document = context.Document; Diagnostic diagnostic = context.Diagnostics[0]; - MemberDeclarationSyntax member = fileScopedNamespace.Members[0]; - BlankLineStyle style = BlankLineAfterFileScopedNamespaceDeclarationAnalyzer.GetCurrentStyle(fileScopedNamespace, member); + SyntaxNode node = BlankLineAfterFileScopedNamespaceDeclarationAnalyzer.GetNodeAfterNamespaceDeclaration(fileScopedNamespace); + BlankLineStyle style = BlankLineAfterFileScopedNamespaceDeclarationAnalyzer.GetCurrentStyle(fileScopedNamespace, node); if (style == BlankLineStyle.Add) { @@ -43,17 +43,17 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) CodeFixTitles.AddBlankLine, ct => { - MemberDeclarationSyntax newMember; + SyntaxNode newNode; if (!fileScopedNamespace.SemicolonToken.TrailingTrivia.Contains(SyntaxKind.EndOfLineTrivia)) { - newMember = member.PrependToLeadingTrivia(new SyntaxTrivia[] { CSharpFactory.NewLine(), CSharpFactory.NewLine() }); + newNode = node.PrependToLeadingTrivia(new SyntaxTrivia[] { CSharpFactory.NewLine(), CSharpFactory.NewLine() }); } else { - newMember = member.PrependEndOfLineToLeadingTrivia(); + newNode = node.PrependEndOfLineToLeadingTrivia(); } - return document.ReplaceNodeAsync(member, newMember, ct); + return document.ReplaceNodeAsync(node, newNode, ct); }, GetEquivalenceKey(diagnostic)); @@ -63,7 +63,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) { CodeAction codeAction = CodeAction.Create( CodeFixTitles.RemoveBlankLine, - ct => CodeFixHelpers.RemoveBlankLinesBeforeAsync(document, member.GetFirstToken(), ct), + ct => CodeFixHelpers.RemoveBlankLinesBeforeAsync(document, node.GetFirstToken(), ct), GetEquivalenceKey(diagnostic)); context.RegisterCodeFix(codeAction, diagnostic); diff --git a/src/Formatting.Analyzers/CSharp/BlankLineAfterFileScopedNamespaceDeclarationAnalyzer.cs b/src/Formatting.Analyzers/CSharp/BlankLineAfterFileScopedNamespaceDeclarationAnalyzer.cs index 83bc137444..6d032cc65b 100644 --- a/src/Formatting.Analyzers/CSharp/BlankLineAfterFileScopedNamespaceDeclarationAnalyzer.cs +++ b/src/Formatting.Analyzers/CSharp/BlankLineAfterFileScopedNamespaceDeclarationAnalyzer.cs @@ -38,30 +38,27 @@ private static void AnalyzeFileScopedNamespaceDeclaration(SyntaxNodeAnalysisCont { var namespaceDeclaration = (FileScopedNamespaceDeclarationSyntax)context.Node; - MemberDeclarationSyntax memberDeclaration = namespaceDeclaration.Members.FirstOrDefault(); - - if (memberDeclaration is null) - return; + SyntaxNode node = GetNodeAfterNamespaceDeclaration(namespaceDeclaration); BlankLineStyle style = context.GetBlankLineAfterFileScopedNamespaceDeclaration(); if (style == BlankLineStyle.None) return; - BlankLineStyle currentStyle = GetCurrentStyle(namespaceDeclaration, memberDeclaration); + BlankLineStyle currentStyle = GetCurrentStyle(namespaceDeclaration, node); if (style != currentStyle) return; context.ReportDiagnostic( DiagnosticRules.BlankLineAfterFileScopedNamespaceDeclaration, - Location.Create(namespaceDeclaration.SyntaxTree, new TextSpan(memberDeclaration.FullSpan.Start, 0)), + Location.Create(namespaceDeclaration.SyntaxTree, new TextSpan(node.FullSpan.Start, 0)), (style == BlankLineStyle.Add) ? "Add" : "Remove"); } internal static BlankLineStyle GetCurrentStyle( FileScopedNamespaceDeclarationSyntax namespaceDeclaration, - MemberDeclarationSyntax memberDeclaration) + SyntaxNode node) { (bool add, bool remove) = AnalyzeTrailingTrivia(); @@ -109,7 +106,7 @@ internal static BlankLineStyle GetCurrentStyle( BlankLineStyle AnalyzeLeadingTrivia() { - SyntaxTriviaList.Enumerator en = memberDeclaration.GetLeadingTrivia().GetEnumerator(); + SyntaxTriviaList.Enumerator en = node.GetLeadingTrivia().GetEnumerator(); if (!en.MoveNext()) return BlankLineStyle.Add; @@ -132,4 +129,14 @@ BlankLineStyle AnalyzeLeadingTrivia() return BlankLineStyle.None; } } + + internal static SyntaxNode GetNodeAfterNamespaceDeclaration(FileScopedNamespaceDeclarationSyntax namespaceDeclaration) + { + MemberDeclarationSyntax memberDeclaration = namespaceDeclaration.Members.FirstOrDefault(); + UsingDirectiveSyntax usingDirective = namespaceDeclaration.Usings.FirstOrDefault(); + + return (usingDirective?.SpanStart > namespaceDeclaration.SpanStart) + ? usingDirective + : memberDeclaration; + } } diff --git a/src/Tests/Formatting.Analyzers.Tests/RCS0060AddEmptyLineAfterFileScopedNamespaceTests.cs b/src/Tests/Formatting.Analyzers.Tests/RCS0060AddEmptyLineAfterFileScopedNamespaceTests.cs index 7465958b65..bbc1fa2be2 100644 --- a/src/Tests/Formatting.Analyzers.Tests/RCS0060AddEmptyLineAfterFileScopedNamespaceTests.cs +++ b/src/Tests/Formatting.Analyzers.Tests/RCS0060AddEmptyLineAfterFileScopedNamespaceTests.cs @@ -171,6 +171,27 @@ class C ", options: Options.AddConfigOption(ConfigOptionKeys.BlankLineAfterFileScopedNamespaceDeclaration, true)); } + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.BlankLineAfterFileScopedNamespaceDeclaration)] + public async Task Test_RemoveEmptyLine_UsingAfter() + { + await VerifyDiagnosticAndFixAsync(@" +namespace N; +[||]using System; + +public class C +{ +} +", @" +namespace N; + +using System; + +public class C +{ +} +", options: Options.AddConfigOption(ConfigOptionKeys.BlankLineAfterFileScopedNamespaceDeclaration, true)); + } + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.BlankLineAfterFileScopedNamespaceDeclaration)] public async Task Test_RemoveEmptyLine() {