Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Analyzer: Add/remove blank line after file scoped namespace #993

Merged
merged 11 commits into from Nov 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .editorconfig
Expand Up @@ -35,6 +35,7 @@ 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

Expand Down Expand Up @@ -77,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
Expand Down
2 changes: 2 additions & 0 deletions ChangeLog.md
Expand Up @@ -10,6 +10,8 @@ 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/remove blank line after file scoped namespace declaration" [RCS0060](https://github.com/JosefPihrt/Roslynator/blob/main/docs/analyzers/RCS0060.md) ([#993](https://github.com/josefpihrt/roslynator/pull/993).
- Required option: `roslynator_blank_line_after_file_scoped_namespace_declaration = true|false`

### Changed

Expand Down
10 changes: 10 additions & 0 deletions src/Common/CSharp/Extensions/CodeStyleExtensions.cs
Expand Up @@ -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))
Expand Down
1 change: 1 addition & 0 deletions src/Common/ConfigOptionKeys.Generated.cs
Expand Up @@ -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";
Expand Down
7 changes: 7 additions & 0 deletions src/Common/ConfigOptions.Generated.cs
Expand Up @@ -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,
Expand Down Expand Up @@ -213,6 +219,7 @@ public static partial class ConfigOptions
yield return new KeyValuePair<string, string>("RCS0052", JoinOptionKeys(ConfigOptionKeys.EqualsTokenNewLine));
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>("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
4 changes: 4 additions & 0 deletions src/Common/ConfigOptions.xml
Expand Up @@ -195,6 +195,10 @@
</Values>
<Description>Use 'for'/'while' statement as an infinite loop</Description>
</Option>
<Option Id="BlankLineAfterFileScopedNamespaceDeclaration">
<ValuePlaceholder>true|false</ValuePlaceholder>
<Description>Add/remove blank line after file scoped namespace declaration</Description>
</Option>
<!--
<Option Id="">
<Key></Key>
Expand Down
@@ -0,0 +1,76 @@
// 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;
using Roslynator.CSharp.CodeStyle;
using System.Diagnostics;

namespace Roslynator.Formatting.CodeFixes.CSharp;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(FileScopedNamespaceDeclarationCodeFixProvider))]
[Shared]
public sealed class FileScopedNamespaceDeclarationCodeFixProvider : BaseCodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds
{
get { return ImmutableArray.Create(DiagnosticIdentifiers.BlankLineAfterFileScopedNamespaceDeclaration); }
}

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];
MemberDeclarationSyntax member = fileScopedNamespace.Members[0];
BlankLineStyle style = BlankLineAfterFileScopedNamespaceDeclarationAnalyzer.GetCurrentStyle(fileScopedNamespace, member);

if (style == BlankLineStyle.Add)
{
CodeAction codeAction = CodeAction.Create(
CodeFixTitles.AddBlankLine,
ct =>
{
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);
}
else if (style == BlankLineStyle.Remove)
{
CodeAction codeAction = CodeAction.Create(
CodeFixTitles.RemoveBlankLine,
ct => CodeFixHelpers.RemoveBlankLinesBeforeAsync(document, member.GetFirstToken(), ct),
GetEquivalenceKey(diagnostic));

context.RegisterCodeFix(codeAction, diagnostic);
}
else
{
Debug.Fail("");
}
}
}
@@ -0,0 +1,135 @@
// 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;
using Roslynator.CSharp.CodeStyle;

namespace Roslynator.Formatting.CSharp;

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

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

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 namespaceDeclaration = (FileScopedNamespaceDeclarationSyntax)context.Node;

MemberDeclarationSyntax memberDeclaration = namespaceDeclaration.Members.FirstOrDefault();

if (memberDeclaration is null)
return;

BlankLineStyle style = context.GetBlankLineAfterFileScopedNamespaceDeclaration();

if (style == BlankLineStyle.None)
return;

BlankLineStyle currentStyle = GetCurrentStyle(namespaceDeclaration, memberDeclaration);

if (style != currentStyle)
return;

context.ReportDiagnostic(
DiagnosticRules.BlankLineAfterFileScopedNamespaceDeclaration,
Location.Create(namespaceDeclaration.SyntaxTree, new TextSpan(memberDeclaration.FullSpan.Start, 0)),
(style == BlankLineStyle.Add) ? "Add" : "Remove");
}

internal static BlankLineStyle GetCurrentStyle(
FileScopedNamespaceDeclarationSyntax namespaceDeclaration,
MemberDeclarationSyntax memberDeclaration)
{
(bool add, bool remove) = AnalyzeTrailingTrivia();

if (add || remove)
{
BlankLineStyle style = AnalyzeLeadingTrivia();

if (style == BlankLineStyle.Add)
{
if (add)
return style;
}
else if (style == BlankLineStyle.Remove)
{
if (remove)
return style;
}
}

return BlankLineStyle.None;

(bool add, bool remove) AnalyzeTrailingTrivia()
{
SyntaxTriviaList.Enumerator en = namespaceDeclaration.SemicolonToken.TrailingTrivia.GetEnumerator();

if (!en.MoveNext())
return (true, false);

if (en.Current.IsWhitespaceTrivia()
&& !en.MoveNext())
{
return (true, false);
}

if (en.Current.IsKind(SyntaxKind.SingleLineCommentTrivia)
&& !en.MoveNext())
{
return (true, false);
}

return (en.Current.IsEndOfLineTrivia())
? (true, true)
: (false, false);
}

BlankLineStyle AnalyzeLeadingTrivia()
{
SyntaxTriviaList.Enumerator en = memberDeclaration.GetLeadingTrivia().GetEnumerator();

if (!en.MoveNext())
return BlankLineStyle.Add;

if (en.Current.IsWhitespaceTrivia()
&& !en.MoveNext())
{
return BlankLineStyle.Add;
}

switch (en.Current.Kind())
{
case SyntaxKind.SingleLineCommentTrivia:
case SyntaxKind.SingleLineDocumentationCommentTrivia:
return BlankLineStyle.Add;
case SyntaxKind.EndOfLineTrivia:
return BlankLineStyle.Remove;
}

return BlankLineStyle.None;
}
}
}
Expand Up @@ -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 BlankLineAfterFileScopedNamespaceDeclaration = "RCS0060";
}
}
12 changes: 12 additions & 0 deletions src/Formatting.Analyzers/CSharp/DiagnosticRules.Generated.cs
Expand Up @@ -621,5 +621,17 @@ public static partial class DiagnosticRules
helpLinkUri: DiagnosticIdentifiers.PlaceNewLineAfterOrBeforeNullConditionalOperator,
customTags: Array.Empty<string>());

/// <summary>RCS0060</summary>
public static readonly DiagnosticDescriptor BlankLineAfterFileScopedNamespaceDeclaration = DiagnosticDescriptorFactory.Create(
id: DiagnosticIdentifiers.BlankLineAfterFileScopedNamespaceDeclaration,
title: "Add/remove line after file scoped namespace declaration.",
messageFormat: "{0} line after file scoped namespace declaration.",
category: DiagnosticCategories.Roslynator,
defaultSeverity: DiagnosticSeverity.Info,
isEnabledByDefault: false,
description: null,
helpLinkUri: DiagnosticIdentifiers.BlankLineAfterFileScopedNamespaceDeclaration,
customTags: Array.Empty<string>());

}
}
31 changes: 31 additions & 0 deletions src/Formatting.Analyzers/Formatting.Analyzers.xml
Expand Up @@ -1481,4 +1481,35 @@ Default maximal length is 140.</Summary>
<Option Key="null_conditional_operator_new_line" IsRequired="true" />
</ConfigOptions>
</Analyzer>
<Analyzer Identifier="BlankLineAfterFileScopedNamespaceDeclaration">
<Id>RCS0060</Id>
<Title>Add/remove line after file scoped namespace declaration.</Title>
<MessageFormat>{0} line after file scoped namespace declaration.</MessageFormat>
<Category>Formatting</Category>
<DefaultSeverity>Info</DefaultSeverity>
<IsEnabledByDefault>false</IsEnabledByDefault>
<MinLanguageVersion>10.0</MinLanguageVersion>
<Samples>
<Sample>
<Before><![CDATA[namespace A.B // [|Id|]
public class C
{
}]]></Before>
<After><![CDATA[namespace A.B
public class C
{
}]]></After>
</Sample>
</Samples>
<ConfigOptions>
<Option Key="blank_line_after_file_scoped_namespace_declaration" IsRequired="true" />
</ConfigOptions>
<Links>
<Link>
<Url>https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/file-scoped-namespaces</Url>
<Text>File Scoped Namespaces</Text>
</Link>
</Links>
</Analyzer>
</Analyzers>