diff --git a/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md b/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md index 6ce0a98e..50220653 100644 --- a/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md +++ b/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md @@ -2,10 +2,11 @@ ### New Rules - Rule ID | Category | Severity | Notes ------------|----------|----------|----------------------------- - MWAIS0001 | Usage | Error | ProviderAccessAnalyzer - MWAIS0002 | Naming | Error | ConstStaticAnalyzer - MWAIS0003 | Naming | Error | UnderscorePrefixAnalyzer - MWAIS0004 | Usage | Error | RandomInstantiationAnalyzer - MWAIS0005 | Usage | Error | ThisUsageAnalyzer \ No newline at end of file + Rule ID | Category | Severity | Notes +-----------|----------|----------|-------------------------------- + MWAIS0001 | Usage | Error | ProviderAccessAnalyzer + MWAIS0002 | Naming | Error | ConstStaticAnalyzer + MWAIS0003 | Naming | Error | UnderscorePrefixAnalyzer + MWAIS0004 | Usage | Error | RandomInstantiationAnalyzer + MWAIS0005 | Usage | Error | ThisUsageAnalyzer + MWAIS0006 | Style | Error | SwitchExpressionMethodAnalyzer \ No newline at end of file diff --git a/app/SourceCodeRules/SourceCodeRules/Identifier.cs b/app/SourceCodeRules/SourceCodeRules/Identifier.cs index 4e51b98b..dfd2fb05 100644 --- a/app/SourceCodeRules/SourceCodeRules/Identifier.cs +++ b/app/SourceCodeRules/SourceCodeRules/Identifier.cs @@ -7,4 +7,5 @@ public static class Identifier public const string UNDERSCORE_PREFIX_ANALYZER = $"{Tools.ID_PREFIX}0003"; public const string RANDOM_INSTANTIATION_ANALYZER = $"{Tools.ID_PREFIX}0004"; public const string THIS_USAGE_ANALYZER = $"{Tools.ID_PREFIX}0005"; + public const string SWITCH_EXPRESSION_METHOD_ANALYZER = $"{Tools.ID_PREFIX}0006"; } \ No newline at end of file diff --git a/app/SourceCodeRules/SourceCodeRules/StyleAnalyzers/SwitchExpressionMethodAnalyzer.cs b/app/SourceCodeRules/SourceCodeRules/StyleAnalyzers/SwitchExpressionMethodAnalyzer.cs new file mode 100644 index 00000000..2bd7f80e --- /dev/null +++ b/app/SourceCodeRules/SourceCodeRules/StyleAnalyzers/SwitchExpressionMethodAnalyzer.cs @@ -0,0 +1,96 @@ +using System.Collections.Immutable; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace SourceCodeRules.StyleAnalyzers; + +#pragma warning disable RS1038 +[DiagnosticAnalyzer(LanguageNames.CSharp)] +#pragma warning restore RS1038 +public class SwitchExpressionMethodAnalyzer : DiagnosticAnalyzer +{ + private const string DIAGNOSTIC_ID = Identifier.SWITCH_EXPRESSION_METHOD_ANALYZER; + + private static readonly string TITLE = "Method with switch expression should use inline expression body"; + + private static readonly string MESSAGE_FORMAT = "Method with a switch expression should use inline expression body syntax with the switch keyword on the same line"; + + private static readonly string DESCRIPTION = "Methods that only return a switch expression should use the expression body syntax (=>) with the switch keyword on the same line for better readability."; + + private const string CATEGORY = "Style"; + + private static readonly DiagnosticDescriptor RULE = new( + DIAGNOSTIC_ID, + TITLE, + MESSAGE_FORMAT, + CATEGORY, + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: DESCRIPTION); + + public override ImmutableArray SupportedDiagnostics => [RULE]; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeMethodDeclaration, SyntaxKind.MethodDeclaration); + } + + private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context) + { + var methodDeclaration = (MethodDeclarationSyntax)context.Node; + + // Fall 1: Methode hat Block-Body mit einem Return-Statement, das eine Switch-Expression ist + if (methodDeclaration is { Body: not null, ExpressionBody: null }) + { + var statements = methodDeclaration.Body.Statements; + if (statements.Count == 1 && statements[0] is ReturnStatementSyntax { Expression: SwitchExpressionSyntax }) + { + var diagnostic = Diagnostic.Create(RULE, methodDeclaration.Identifier.GetLocation()); + context.ReportDiagnostic(diagnostic); + return; + } + } + + // Fall 2: Methode hat Expression-Body, aber die Switch-Expression beginnt auf einer neuen Zeile + var expressionBody = methodDeclaration.ExpressionBody; + if (expressionBody?.Expression is SwitchExpressionSyntax switchExpr) + { + var arrowToken = expressionBody.ArrowToken; + var switchToken = switchExpr.SwitchKeyword; + bool hasNewLineBetweenArrowAndSwitch = false; + + foreach (var trivia in arrowToken.TrailingTrivia) + { + if (trivia.IsKind(SyntaxKind.EndOfLineTrivia)) + { + hasNewLineBetweenArrowAndSwitch = true; + break; + } + } + + // Prüfe Leading Trivia des Switch-Keywords, falls notwendig + if (!hasNewLineBetweenArrowAndSwitch) + { + foreach (var trivia in switchToken.LeadingTrivia) + { + if (trivia.IsKind(SyntaxKind.EndOfLineTrivia)) + { + hasNewLineBetweenArrowAndSwitch = true; + break; + } + } + } + + if (hasNewLineBetweenArrowAndSwitch) + { + var diagnostic = Diagnostic.Create(RULE, methodDeclaration.Identifier.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + } + } +} \ No newline at end of file diff --git a/app/SourceCodeRules/SourceCodeRules/StyleCodeFixes/SwitchExpressionMethodCodeFixProvider.cs b/app/SourceCodeRules/SourceCodeRules/StyleCodeFixes/SwitchExpressionMethodCodeFixProvider.cs new file mode 100644 index 00000000..d89751ff --- /dev/null +++ b/app/SourceCodeRules/SourceCodeRules/StyleCodeFixes/SwitchExpressionMethodCodeFixProvider.cs @@ -0,0 +1,162 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace SourceCodeRules.StyleCodeFixes; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SwitchExpressionMethodCodeFixProvider)), Shared] +public class SwitchExpressionMethodCodeFixProvider : CodeFixProvider +{ + public sealed override ImmutableArray FixableDiagnosticIds => [Identifier.SWITCH_EXPRESSION_METHOD_ANALYZER]; + + public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root == null) + return; + + var diagnostic = context.Diagnostics.First(); + var diagnosticSpan = diagnostic.Location.SourceSpan; + var methodDeclaration = root.FindToken(diagnosticSpan.Start) + .Parent?.AncestorsAndSelf() + .OfType() + .First(); + + if(methodDeclaration == null) + return; + + context.RegisterCodeFix( + CodeAction.Create( + title: "Use inline expression body for switch expression", + createChangedDocument: c => UseInlineExpressionBodyAsync(context.Document, methodDeclaration, c), + equivalenceKey: nameof(SwitchExpressionMethodCodeFixProvider)), + diagnostic); + } + + private static async Task UseInlineExpressionBodyAsync(Document document, MethodDeclarationSyntax methodDecl, CancellationToken cancellationToken) + { + var sourceText = await document.GetTextAsync(cancellationToken); + var parameterText = methodDecl.ParameterList.ToString(); + var methodStartLine = sourceText.Lines.GetLineFromPosition(methodDecl.SpanStart); + + SwitchExpressionSyntax? switchExpr = null; + ExpressionSyntax? governingExpression = null; + var switchBodyText = string.Empty; + + if (methodDecl.Body != null) + { + // Case: Block-Body with a Return-Statement that contains a Switch-Expression + var returnStmt = (ReturnStatementSyntax)methodDecl.Body.Statements[0]; + if (returnStmt.Expression is not SwitchExpressionSyntax matchingSwitchExpr) + return document; + + switchExpr = matchingSwitchExpr; + governingExpression = switchExpr.GoverningExpression; + + // Extract the switch body text: + var switchStart = switchExpr.SwitchKeyword.SpanStart; + var switchEnd = switchExpr.CloseBraceToken.Span.End; + switchBodyText = sourceText.ToString(TextSpan.FromBounds(switchStart, switchEnd)); + } + else if (methodDecl.ExpressionBody != null) + { + // Case 2: Expression-Body with a poorly formatted Switch-Expression + switchExpr = (SwitchExpressionSyntax)methodDecl.ExpressionBody.Expression; + governingExpression = switchExpr.GoverningExpression; + + // Extract the switch body text: + var switchStart = switchExpr.SwitchKeyword.SpanStart; + var switchEnd = switchExpr.CloseBraceToken.Span.End; + switchBodyText = sourceText.ToString(TextSpan.FromBounds(switchStart, switchEnd)); + } + + if (switchExpr is null || governingExpression is null) + return document; + + // Extract the governing expression and the switch body: + var govExprText = sourceText.ToString(governingExpression.Span); + + // Create the new method with inline expression body and correct formatting: + var returnTypeText = methodDecl.ReturnType.ToString(); + var modifiersText = string.Join(" ", methodDecl.Modifiers); + var methodNameText = methodDecl.Identifier.Text; + + // Determine the indentation of the method: + var methodIndentation = ""; + for (var i = methodStartLine.Start; i < methodDecl.SpanStart; i++) + { + if (char.IsWhiteSpace(sourceText[i])) + methodIndentation += sourceText[i]; + else + break; + } + + // Erstelle die neue Methode mit Expression-Body und korrekter Formatierung + var newMethodText = new StringBuilder(); + newMethodText.Append($"{modifiersText} {returnTypeText} {methodNameText}{parameterText} => {govExprText} switch"); + + // Formatiere die geschweiften Klammern und den Switch-Body + var switchBody = switchBodyText.Substring("switch".Length).Trim(); + + // Bestimme die Einrückung für die Switch-Cases (4 Spaces oder 1 Tab mehr als die Methode) + var caseIndentation = methodIndentation + " "; // 4 Spaces Einrückung + + // Verarbeite die Klammern und formatiere den Body + var formattedSwitchBody = FormatSwitchBody(switchBody, methodIndentation, caseIndentation); + newMethodText.Append(formattedSwitchBody); + + // Ersetze die alte Methoden-Deklaration mit dem neuen Text + var newText = sourceText.Replace(methodDecl.Span, newMethodText.ToString()); + return document.WithText(newText); + } + + private static string FormatSwitchBody(string switchBody, string methodIndentation, string caseIndentation) + { + var result = new StringBuilder(); + + // Remove braces from the switch body: + var bodyWithoutBraces = switchBody.Trim(); + if (bodyWithoutBraces.StartsWith("{")) + bodyWithoutBraces = bodyWithoutBraces.Substring(1); + if (bodyWithoutBraces.EndsWith("}")) + bodyWithoutBraces = bodyWithoutBraces.Substring(0, bodyWithoutBraces.Length - 1); + + bodyWithoutBraces = bodyWithoutBraces.Trim(); + + // Add braces with correct indentation: + result.AppendLine(); + result.Append($"{methodIndentation}{{"); + + // Process each line of the switch body: + var lines = bodyWithoutBraces.Split(["\r\n", "\n"], System.StringSplitOptions.None); + foreach (var line in lines) + { + result.AppendLine(); + + var trimmedLine = line.Trim(); + if (string.IsNullOrWhiteSpace(trimmedLine)) + continue; + + // Add correct indentation for each case: + result.Append(caseIndentation); + result.Append(trimmedLine); + } + + // Add the closing brace with correct indentation: + result.AppendLine(); + result.Append($"{methodIndentation}}};"); + + return result.ToString(); + } +} \ No newline at end of file