diff --git a/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md b/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md index 6b73c826..18a48e8d 100644 --- a/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md +++ b/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md @@ -4,4 +4,5 @@ Rule ID | Category | Severity | Notes -----------|----------|----------|------------------------ - MWAIS0001 | Usage | Error | ProviderAccessAnalyzer \ No newline at end of file + MWAIS0001 | Usage | Error | ProviderAccessAnalyzer + MWAIS0002 | Naming | Error | ConstStaticAnalyzer \ No newline at end of file diff --git a/app/SourceCodeRules/SourceCodeRules/NamingAnalyzers/ConstStaticAnalyzer.cs b/app/SourceCodeRules/SourceCodeRules/NamingAnalyzers/ConstStaticAnalyzer.cs new file mode 100644 index 00000000..89cfec69 --- /dev/null +++ b/app/SourceCodeRules/SourceCodeRules/NamingAnalyzers/ConstStaticAnalyzer.cs @@ -0,0 +1,67 @@ +using System.Collections.Immutable; +using System.Linq; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace SourceCodeRules.NamingAnalyzers; + +#pragma warning disable RS1038 +[DiagnosticAnalyzer(LanguageNames.CSharp)] +#pragma warning restore RS1038 +public sealed class ConstStaticAnalyzer : DiagnosticAnalyzer +{ + private const string DIAGNOSTIC_ID = $"{Tools.ID_PREFIX}0002"; + + private static readonly string TITLE = "Constant and static fields must be in UPPER_CASE"; + + private static readonly string MESSAGE_FORMAT = "Field '{0}' must be in UPPER_CASE"; + + private static readonly string DESCRIPTION = "All constant and static fields should be named using UPPER_CASE."; + + private const string CATEGORY = "Naming"; + + 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(this.AnalyzeField, SyntaxKind.FieldDeclaration); + } + + private void AnalyzeField(SyntaxNodeAnalysisContext context) + { + var fieldDeclaration = (FieldDeclarationSyntax)context.Node; + + // Prüfen ob das Feld static oder const ist + if (!fieldDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword) || m.IsKind(SyntaxKind.ConstKeyword))) + return; + + foreach (var variable in fieldDeclaration.Declaration.Variables) + { + var fieldName = variable.Identifier.Text; + + // Prüfen ob der Name bereits in UPPER_CASE ist + if (!IsUpperCase(fieldName)) + { + var diagnostic = Diagnostic.Create( + RULE, + variable.Identifier.GetLocation(), + fieldName); + + context.ReportDiagnostic(diagnostic); + } + } + } + + private static bool IsUpperCase(string name) + { + // Erlaubt: Nur Großbuchstaben, Zahlen und Unterstriche + return name.All(c => char.IsUpper(c) || char.IsDigit(c) || c == '_'); + } +} \ No newline at end of file diff --git a/app/SourceCodeRules/SourceCodeRules/NamingCodeFixes/ConstStaticCodeFixProvider.cs b/app/SourceCodeRules/SourceCodeRules/NamingCodeFixes/ConstStaticCodeFixProvider.cs new file mode 100644 index 00000000..89a4f313 --- /dev/null +++ b/app/SourceCodeRules/SourceCodeRules/NamingCodeFixes/ConstStaticCodeFixProvider.cs @@ -0,0 +1,67 @@ +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.Rename; + +namespace SourceCodeRules.NamingCodeFixes; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ConstStaticCodeFixProvider)), Shared] +public sealed class ConstStaticCodeFixProvider : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => [$"{Tools.ID_PREFIX}0002"]; + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken); + var diagnostic = context.Diagnostics.First(); + var diagnosticSpan = diagnostic.Location.SourceSpan; + var declaration = root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType().First(); + if (declaration is null) + return; + + context.RegisterCodeFix(CodeAction.Create(title: "Convert to UPPER_CASE", createChangedDocument: c => this.ConvertToUpperCaseAsync(context.Document, declaration, c), equivalenceKey: nameof(ConstStaticCodeFixProvider)), diagnostic); + } + + private async Task ConvertToUpperCaseAsync(Document document, VariableDeclaratorSyntax declarator, CancellationToken cancellationToken) + { + var oldName = declarator.Identifier.Text; + var newName = ConvertToUpperCase(oldName); + + var semanticModel = await document.GetSemanticModelAsync(cancellationToken); + var symbol = semanticModel?.GetDeclaredSymbol(declarator, cancellationToken); + if (symbol is null) + return document; + + var solution = document.Project.Solution; + var newSolution = await Renamer.RenameSymbolAsync(solution, symbol, new SymbolRenameOptions(), newName, cancellationToken); + + return newSolution.GetDocument(document.Id) ?? document; + } + + private static string ConvertToUpperCase(string name) + { + var result = new StringBuilder(); + for (var i = 0; i < name.Length; i++) + { + var current = name[i]; + + // Insert an underscore before each uppercase letter, except the first one: + if (i > 0 && char.IsUpper(current) && !char.IsUpper(name[i - 1])) + result.Append('_'); + + result.Append(char.ToUpper(current)); + } + + return result.ToString(); + } +} \ No newline at end of file