diff --git a/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md b/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md index 18a48e8d..bbbad53b 100644 --- a/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md +++ b/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md @@ -5,4 +5,5 @@ Rule ID | Category | Severity | Notes -----------|----------|----------|------------------------ MWAIS0001 | Usage | Error | ProviderAccessAnalyzer - MWAIS0002 | Naming | Error | ConstStaticAnalyzer \ No newline at end of file + MWAIS0002 | Naming | Error | ConstStaticAnalyzer + MWAIS0003 | Naming | Error | UnderscorePrefixAnalyzer \ No newline at end of file diff --git a/app/SourceCodeRules/SourceCodeRules/Identifier.cs b/app/SourceCodeRules/SourceCodeRules/Identifier.cs index acbc1d55..ab7408f9 100644 --- a/app/SourceCodeRules/SourceCodeRules/Identifier.cs +++ b/app/SourceCodeRules/SourceCodeRules/Identifier.cs @@ -4,4 +4,5 @@ public static class Identifier { public const string PROVIDER_ACCESS_ANALYZER = $"{Tools.ID_PREFIX}0001"; public const string CONST_STATIC_ANALYZER = $"{Tools.ID_PREFIX}0002"; + public const string UNDERSCORE_PREFIX_ANALYZER = $"{Tools.ID_PREFIX}0003"; } \ No newline at end of file diff --git a/app/SourceCodeRules/SourceCodeRules/NamingAnalyzers/UnderscorePrefixAnalyzer.cs b/app/SourceCodeRules/SourceCodeRules/NamingAnalyzers/UnderscorePrefixAnalyzer.cs new file mode 100644 index 00000000..6f35469c --- /dev/null +++ b/app/SourceCodeRules/SourceCodeRules/NamingAnalyzers/UnderscorePrefixAnalyzer.cs @@ -0,0 +1,46 @@ +using System.Collections.Immutable; + +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 UnderscorePrefixAnalyzer : DiagnosticAnalyzer +{ + private const string DIAGNOSTIC_ID = Identifier.UNDERSCORE_PREFIX_ANALYZER; + + private static readonly string TITLE = "Variable names cannot start with underscore"; + + private static readonly string MESSAGE_FORMAT = "The variable name '{0}' starts with an underscore which is not allowed"; + + private static readonly string DESCRIPTION = "Variable names cannot start with an underscore prefix."; + + 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(AnalyzeVariableDeclaration, SyntaxKind.VariableDeclarator); + } + + private static void AnalyzeVariableDeclaration(SyntaxNodeAnalysisContext context) + { + var variableDeclarator = (VariableDeclaratorSyntax)context.Node; + var variableName = variableDeclarator.Identifier.Text; + if (variableName.StartsWith("_")) + { + var diagnostic = Diagnostic.Create(RULE, variableDeclarator.Identifier.GetLocation(), variableName); + context.ReportDiagnostic(diagnostic); + } + } +} \ No newline at end of file diff --git a/app/SourceCodeRules/SourceCodeRules/NamingCodeFixes/UnderscorePrefixCodeFixProvider.cs b/app/SourceCodeRules/SourceCodeRules/NamingCodeFixes/UnderscorePrefixCodeFixProvider.cs new file mode 100644 index 00000000..6308e572 --- /dev/null +++ b/app/SourceCodeRules/SourceCodeRules/NamingCodeFixes/UnderscorePrefixCodeFixProvider.cs @@ -0,0 +1,54 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +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(UnderscorePrefixCodeFixProvider)), Shared] +public sealed class UnderscorePrefixCodeFixProvider : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => [Identifier.UNDERSCORE_PREFIX_ANALYZER]; + + 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: "Remove underscore prefix", + createChangedDocument: c => this.RemoveUnderscorePrefixAsync(context.Document, declaration, c), + equivalenceKey: nameof(UnderscorePrefixCodeFixProvider)), + diagnostic); + } + + private async Task RemoveUnderscorePrefixAsync(Document document, VariableDeclaratorSyntax declarator, CancellationToken cancellationToken) + { + var oldName = declarator.Identifier.Text; + var newName = oldName.TrimStart('_'); + + 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; + } +} \ No newline at end of file