diff --git a/.editorconfig b/.editorconfig index 1d4c9ced5b..ab81a354fb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,6 +6,9 @@ root = true # Code files [*.{cs,csx}] +indent_style = space +indent_size = 4 + dotnet_sort_system_directives_first = true csharp_style_namespace_declarations = file_scoped:suggestion diff --git a/ChangeLog.md b/ChangeLog.md index bebc5db2a6..ac8853a0b1 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix [RCS1208](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1208) ([#1153](https://github.com/JosefPihrt/Roslynator/pull/1153)). - Fix [RCS1043](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1043) ([#1176](https://github.com/JosefPihrt/Roslynator/pull/1176)). - [CLI] Fix exit code of `spellcheck` command ([#1177](https://github.com/JosefPihrt/Roslynator/pull/1177)). +- Improve indentation analysis ([#1188](https://github.com/JosefPihrt/Roslynator/pull/1188)). ## [4.4.0] - 2023-08-01 diff --git a/src/Analyzers.xml b/src/Analyzers.xml index 136339d3b9..24a119220a 100644 --- a/src/Analyzers.xml +++ b/src/Analyzers.xml @@ -1446,6 +1446,7 @@ void M( Default maximal length is 140. diff --git a/src/Analyzers/CSharp/Analysis/RemoveEmptySyntaxAnalyzer.cs b/src/Analyzers/CSharp/Analysis/RemoveEmptySyntaxAnalyzer.cs index 03d5557cbc..f1318bb645 100644 --- a/src/Analyzers/CSharp/Analysis/RemoveEmptySyntaxAnalyzer.cs +++ b/src/Analyzers/CSharp/Analysis/RemoveEmptySyntaxAnalyzer.cs @@ -66,7 +66,7 @@ private static void AnalyzeElseClause(SyntaxNodeAnalysisContext context) IfStatementSyntax topmostIf = elseClause.GetTopmostIf(); if (topmostIf.Parent is IfStatementSyntax parentIf - && parentIf.Else != null) + && parentIf.Else is not null) { return; } diff --git a/src/Analyzers/CSharp/Analysis/RemovePartialModifierFromTypeWithSinglePartAnalyzer.cs b/src/Analyzers/CSharp/Analysis/RemovePartialModifierFromTypeWithSinglePartAnalyzer.cs index 37a784cdc8..4fcc5c2f96 100644 --- a/src/Analyzers/CSharp/Analysis/RemovePartialModifierFromTypeWithSinglePartAnalyzer.cs +++ b/src/Analyzers/CSharp/Analysis/RemovePartialModifierFromTypeWithSinglePartAnalyzer.cs @@ -14,7 +14,7 @@ public sealed class RemovePartialModifierFromTypeWithSinglePartAnalyzer : BaseDi { private static ImmutableArray _supportedDiagnostics; - private static readonly MetadataName[] _metadataNames = new MetadataName[] { + private static readonly MetadataName[] _metadataNames = new[] { // ASP.NET Core MetadataName.Parse("Microsoft.AspNetCore.Components.ComponentBase"), // WPF diff --git a/src/CodeFixes/CSharp/CodeFixes/AssignDefaultValueToOutParameterCodeFixProvider.cs b/src/CodeFixes/CSharp/CodeFixes/AssignDefaultValueToOutParameterCodeFixProvider.cs index fb85bc8b84..54a044d07c 100644 --- a/src/CodeFixes/CSharp/CodeFixes/AssignDefaultValueToOutParameterCodeFixProvider.cs +++ b/src/CodeFixes/CSharp/CodeFixes/AssignDefaultValueToOutParameterCodeFixProvider.cs @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; using Roslynator.CodeFixes; using Roslynator.CSharp.Refactorings; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; @@ -155,7 +156,9 @@ private static Task RefactorAsync( if (bodyOrExpressionBody is ArrowExpressionClauseSyntax expressionBody) { - newNode = ConvertExpressionBodyToBlockBodyRefactoring.Refactor(expressionBody, semanticModel, cancellationToken); + AnalyzerConfigOptions configOptions = document.GetConfigOptions(node.SyntaxTree); + + newNode = ConvertExpressionBodyToBlockBodyRefactoring.Refactor(expressionBody, configOptions, semanticModel, cancellationToken); newNode = InsertStatements(newNode, expressionStatements); } diff --git a/src/Common/CSharp/Extensions/CodeStyleExtensions.cs b/src/Common/CSharp/Extensions/CodeStyleExtensions.cs index e06730a91f..00fbc39fb7 100644 --- a/src/Common/CSharp/Extensions/CodeStyleExtensions.cs +++ b/src/Common/CSharp/Extensions/CodeStyleExtensions.cs @@ -1,6 +1,7 @@ // 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; +using System.Globalization; using Microsoft.CodeAnalysis.Diagnostics; using Roslynator.Configuration; using Roslynator.CSharp.CodeStyle; @@ -9,6 +10,42 @@ namespace Roslynator.CSharp; internal static class CodeStyleExtensions { + public static bool TryGetTabLength(this AnalyzerConfigOptions configOptions, out int tabLength) + { + if (configOptions.TryGetValue(ConfigOptionKeys.TabLength, out string tabLengthStr) + && int.TryParse(tabLengthStr, NumberStyles.None, CultureInfo.InvariantCulture, out tabLength)) + { + return true; + } + + tabLength = 0; + return false; + } + + public static bool TryGetIndentSize(this AnalyzerConfigOptions configOptions, out int indentSize) + { + if (configOptions.TryGetValue("indent_size", out string indentSizeStr) + && int.TryParse(indentSizeStr, NumberStyles.None, CultureInfo.InvariantCulture, out indentSize)) + { + return true; + } + + indentSize = 0; + return false; + } + + public static bool TryGetIndentStyle(this AnalyzerConfigOptions configOptions, out IndentStyle indentStyle) + { + if (configOptions.TryGetValue("indent_style", out string indentStyleStr) + && Enum.TryParse(indentStyleStr, ignoreCase: true, out indentStyle)) + { + return true; + } + + indentStyle = IndentStyle.Space; + return false; + } + public static bool GetPrefixFieldIdentifierWithUnderscore(this AnalyzerConfigOptions configOptions) { if (configOptions.TryGetValueAsBool(ConfigOptions.PrefixFieldIdentifierWithUnderscore, out bool value)) diff --git a/src/CSharp/CSharp/IndentationAnalysis.cs b/src/Common/CSharp/IndentationAnalysis.cs similarity index 83% rename from src/CSharp/CSharp/IndentationAnalysis.cs rename to src/Common/CSharp/IndentationAnalysis.cs index 0de4dda8cc..74b8bdb931 100644 --- a/src/CSharp/CSharp/IndentationAnalysis.cs +++ b/src/Common/CSharp/IndentationAnalysis.cs @@ -5,62 +5,86 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; namespace Roslynator.CSharp; [DebuggerDisplay("{DebuggerDisplay,nq}")] -internal readonly struct IndentationAnalysis +internal sealed class IndentationAnalysis { private readonly int? _indentSize; - private readonly SyntaxTrivia? _singleIndentation; + private readonly SyntaxTrivia? _indentStep; - private IndentationAnalysis(SyntaxTrivia indentation, int? indentSize, SyntaxTrivia? singleIndentation) + private IndentationAnalysis(SyntaxTrivia indentation, IndentStyle? indentStyle, int? indentSize, SyntaxTrivia? indentStep) { Indentation = indentation; + IndentStyle = indentStyle; _indentSize = indentSize; - _singleIndentation = singleIndentation; + _indentStep = indentStep; } public SyntaxTrivia Indentation { get; } - public int IndentSize => _indentSize ?? _singleIndentation?.Span.Length ?? 0; + public IndentStyle? IndentStyle { get; } - public int IndentationLength => Indentation.Span.Length; + public int IndentSize => _indentSize ?? _indentStep?.Span.Length ?? 0; - public int IncreasedIndentationLength => (IndentSize > 0) ? Indentation.Span.Length + IndentSize : 0; + public int IndentationLength => Indentation.Span.Length; - public bool IsDefault + public int IncreasedIndentationLength { get { - return Indentation.IsKind(SyntaxKind.None) - && _indentSize is null - && _singleIndentation is null; + if (IndentSize > 0) + { + if (IndentStyle == Roslynator.IndentStyle.Tab) + return IndentationLength + 1; + + return IndentationLength + IndentSize; + } + + if (_indentStep is not null) + return IndentationLength + 4; + + return IndentationLength; } } [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private string DebuggerDisplay => $"Length = {Indentation.Span.Length} {nameof(IndentSize)} = {IndentSize}"; + private string DebuggerDisplay => $"Length = {IndentationLength} {nameof(IndentSize)} = {IndentSize}"; - public static IndentationAnalysis Create(SyntaxNode node, CancellationToken cancellationToken = default) + public static IndentationAnalysis Create(SyntaxNode node, AnalyzerConfigOptions configOptions, CancellationToken cancellationToken = default) { SyntaxTrivia indentation = SyntaxTriviaAnalysis.DetermineIndentation(node, cancellationToken); + if (configOptions.TryGetIndentStyle(out IndentStyle indentStyle) + && indentStyle == Roslynator.IndentStyle.Tab) + { + if (!configOptions.TryGetTabLength(out int tabLength)) + tabLength = 4; + + return new IndentationAnalysis(indentation, indentStyle, tabLength, null); + } + else if (configOptions.TryGetIndentSize(out int indentSize)) + { + return new IndentationAnalysis(indentation, Roslynator.IndentStyle.Space, indentSize, null); + } + (SyntaxTrivia trivia1, SyntaxTrivia trivia2, bool isFromCompilationUnit) = DetermineSingleIndentation(node, cancellationToken); if (isFromCompilationUnit) { - return new IndentationAnalysis(indentation, trivia1.Span.Length - trivia2.Span.Length, null); + return new IndentationAnalysis(indentation, null, trivia1.Span.Length - trivia2.Span.Length, null); } else if (indentation.Span.Length > 0) { return (trivia1.Span.Length > 0) - ? new IndentationAnalysis(indentation, null, trivia1) - : new IndentationAnalysis(indentation, null, null); + ? new IndentationAnalysis(indentation, null, null, trivia1) + : new IndentationAnalysis(indentation, null, null, null); } else if (trivia1.Span.Length > 0) { - return new IndentationAnalysis(indentation, null, trivia1); + return new IndentationAnalysis(indentation, null, null, trivia1); } else { @@ -87,11 +111,14 @@ public SyntaxTriviaList GetIncreasedIndentationTriviaList() public string GetSingleIndentation() { - if (_singleIndentation is not null) - return _singleIndentation.ToString(); + if (_indentStep is not null) + return _indentStep.ToString(); + + if (IndentStyle == Roslynator.IndentStyle.Tab) + return "\t"; - if (_indentSize == -1) - return Indentation.ToString(); + if (IndentStyle == Roslynator.IndentStyle.Space) + return GetSpaces(); if (Indentation.Span.Length == 0) return ""; @@ -101,7 +128,18 @@ public string GetSingleIndentation() if (indentation[indentation.Length - 1] == '\t') return "\t"; - return new string(indentation[0], IndentSize); + return GetSpaces(); + + string GetSpaces() + { + return IndentSize switch + { + 2 => " ", + 4 => " ", + 8 => " ", + _ => new string(' ', IndentSize), + }; + } } private static (SyntaxTrivia, SyntaxTrivia, bool isFromCompilationUnit) DetermineSingleIndentation(SyntaxNode node, CancellationToken cancellationToken = default) diff --git a/src/CSharp/CSharp/SyntaxTriviaAnalysis.cs b/src/Common/CSharp/SyntaxTriviaAnalysis.cs similarity index 93% rename from src/CSharp/CSharp/SyntaxTriviaAnalysis.cs rename to src/Common/CSharp/SyntaxTriviaAnalysis.cs index d185efe922..2c8f052462 100644 --- a/src/CSharp/CSharp/SyntaxTriviaAnalysis.cs +++ b/src/Common/CSharp/SyntaxTriviaAnalysis.cs @@ -7,6 +7,7 @@ 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.Text; @@ -201,9 +202,9 @@ public static bool StartsWithOptionalWhitespaceThenEndOfLineTrivia(SyntaxTriviaL return en.Current.IsEndOfLineTrivia(); } - public static IndentationAnalysis AnalyzeIndentation(SyntaxNode node, CancellationToken cancellationToken = default) + public static IndentationAnalysis AnalyzeIndentation(SyntaxNode node, AnalyzerConfigOptions configOptions, CancellationToken cancellationToken = default) { - return IndentationAnalysis.Create(node, cancellationToken); + return IndentationAnalysis.Create(node, configOptions, cancellationToken); } public static SyntaxTrivia DetermineIndentation(SyntaxNodeOrToken nodeOrToken, CancellationToken cancellationToken = default) @@ -302,24 +303,19 @@ static bool IsMemberDeclarationOrStatementOrAccessorDeclaration(SyntaxNode node) } } - public static string GetIncreasedIndentation(SyntaxNode node, CancellationToken cancellationToken = default) + public static string GetIncreasedIndentation(SyntaxNode node, AnalyzerConfigOptions configOptions, CancellationToken cancellationToken = default) { - return AnalyzeIndentation(node, cancellationToken).GetIncreasedIndentation(); + return AnalyzeIndentation(node, configOptions, cancellationToken).GetIncreasedIndentation(); } - public static int GetIncreasedIndentationLength(SyntaxNode node, CancellationToken cancellationToken = default) + public static SyntaxTrivia GetIncreasedIndentationTrivia(SyntaxNode node, AnalyzerConfigOptions configOptions, CancellationToken cancellationToken = default) { - return AnalyzeIndentation(node, cancellationToken).IncreasedIndentationLength; + return AnalyzeIndentation(node, configOptions, cancellationToken).GetIncreasedIndentationTrivia(); } - public static SyntaxTrivia GetIncreasedIndentationTrivia(SyntaxNode node, CancellationToken cancellationToken = default) + public static SyntaxTriviaList GetIncreasedIndentationTriviaList(SyntaxNode node, AnalyzerConfigOptions configOptions, CancellationToken cancellationToken = default) { - return AnalyzeIndentation(node, cancellationToken).GetIncreasedIndentationTrivia(); - } - - public static SyntaxTriviaList GetIncreasedIndentationTriviaList(SyntaxNode node, CancellationToken cancellationToken = default) - { - return AnalyzeIndentation(node, cancellationToken).GetIncreasedIndentationTriviaList(); + return AnalyzeIndentation(node, configOptions, cancellationToken).GetIncreasedIndentationTriviaList(); } public static IEnumerable FindIndentations(SyntaxNode node) @@ -367,9 +363,10 @@ public static IEnumerable FindIndentations(SyntaxNode node, Tex public static TNode SetIndentation( TNode expression, SyntaxNode containingDeclaration, + AnalyzerConfigOptions configOptions, int increaseCount = 0) where TNode : SyntaxNode { - IndentationAnalysis analysis = AnalyzeIndentation(containingDeclaration); + IndentationAnalysis analysis = AnalyzeIndentation(containingDeclaration, configOptions); string replacement = (increaseCount > 0) ? string.Concat(Enumerable.Repeat(analysis.GetSingleIndentation(), increaseCount)) diff --git a/src/Common/ConfigOptionKeys.Generated.cs b/src/Common/ConfigOptionKeys.Generated.cs index 18b43bc31c..5c4844bfe2 100644 --- a/src/Common/ConfigOptionKeys.Generated.cs +++ b/src/Common/ConfigOptionKeys.Generated.cs @@ -35,6 +35,7 @@ internal static partial class ConfigOptionKeys public const string ObjectCreationTypeStyle = "roslynator_object_creation_type_style"; public const string PrefixFieldIdentifierWithUnderscore = "roslynator_prefix_field_identifier_with_underscore"; public const string SuppressUnityScriptMethods = "roslynator_suppress_unity_script_methods"; + public const string TabLength = "roslynator_tab_length"; public const string UseAnonymousFunctionOrMethodGroup = "roslynator_use_anonymous_function_or_method_group"; public const string UseBlockBodyWhenDeclarationSpansOverMultipleLines = "roslynator_use_block_body_when_declaration_spans_over_multiple_lines"; public const string UseBlockBodyWhenExpressionSpansOverMultipleLines = "roslynator_use_block_body_when_expression_spans_over_multiple_lines"; diff --git a/src/Common/ConfigOptions.Generated.cs b/src/Common/ConfigOptions.Generated.cs index 3a84f2d161..ca1648383a 100644 --- a/src/Common/ConfigOptions.Generated.cs +++ b/src/Common/ConfigOptions.Generated.cs @@ -182,6 +182,12 @@ public static partial class ConfigOptions defaultValuePlaceholder: "true|false", description: "Suppress Unity script methods"); + public static readonly ConfigOptionDescriptor TabLength = new( + key: ConfigOptionKeys.TabLength, + defaultValue: "4", + defaultValuePlaceholder: "", + description: "A length of a tab character."); + public static readonly ConfigOptionDescriptor UseAnonymousFunctionOrMethodGroup = new( key: ConfigOptionKeys.UseAnonymousFunctionOrMethodGroup, defaultValue: null, diff --git a/src/Common/IndentStyle.cs b/src/Common/IndentStyle.cs new file mode 100644 index 0000000000..47ff3c822c --- /dev/null +++ b/src/Common/IndentStyle.cs @@ -0,0 +1,9 @@ +// Copyright (c) Josef Pihrt and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Roslynator; + +public enum IndentStyle +{ + Space, + Tab, +} diff --git a/src/ConfigOptions.xml b/src/ConfigOptions.xml index 8c2f55806a..06fafe0530 100644 --- a/src/ConfigOptions.xml +++ b/src/ConfigOptions.xml @@ -124,6 +124,11 @@ <NUM> Max line length +