mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2025-03-12 09:09:07 +00:00
Extended code analyzers (#293)
This commit is contained in:
parent
99dac520cb
commit
5e3b632eee
8
.github/CODEOWNERS
vendored
8
.github/CODEOWNERS
vendored
@ -2,13 +2,13 @@
|
||||
* @MindWorkAI/maintainer
|
||||
|
||||
# The release team is responsible for anything inside the .github directory, such as workflows, actions, and issue templates:
|
||||
/.github/ @MindWorkAI/release
|
||||
|
||||
# The release team is responsible for the update directory:
|
||||
/.updates/ @MindWorkAI/release
|
||||
/.github/ @MindWorkAI/release @SommerEngineering
|
||||
|
||||
# Our Rust experts are responsible for the Rust codebase:
|
||||
/runtime/ @MindWorkAI/rust-experts
|
||||
|
||||
# Our .NET experts are responsible for the .NET codebase:
|
||||
/app/ @MindWorkAI/net-experts
|
||||
|
||||
# The source code rules must be reviewed by the release team:
|
||||
/app/SourceCodeRules/ @MindWorkAI/release @SommerEngineering
|
@ -2,9 +2,7 @@ namespace AIStudio.Assistants.ERI;
|
||||
|
||||
public static class AllowedLLMProvidersExtensions
|
||||
{
|
||||
public static string Description(this AllowedLLMProviders provider)
|
||||
{
|
||||
return provider switch
|
||||
public static string Description(this AllowedLLMProviders provider) => provider switch
|
||||
{
|
||||
AllowedLLMProviders.NONE => "Please select what kind of LLM provider are allowed for this data source",
|
||||
AllowedLLMProviders.ANY => "Any LLM provider is allowed: users might choose a cloud-based or a self-hosted provider",
|
||||
@ -12,5 +10,4 @@ public static class AllowedLLMProvidersExtensions
|
||||
|
||||
_ => "Unknown option was selected"
|
||||
};
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ public partial class MudTextList : ComponentBase
|
||||
public string Icon { get; set; } = Icons.Material.Filled.CheckCircle;
|
||||
|
||||
[Parameter]
|
||||
public string Class { get; set; } = "";
|
||||
public string Class { get; set; } = string.Empty;
|
||||
|
||||
private string Classes => $"mud-text-list {this.Class}";
|
||||
}
|
||||
|
@ -14,7 +14,6 @@ internal static class Redirect
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
#if DEBUG
|
||||
|
||||
if (path.StartsWith(SYSTEM, StringComparison.InvariantCulture))
|
||||
|
@ -10,9 +10,7 @@ public static class DataSourceTypeExtension
|
||||
/// </summary>
|
||||
/// <param name="type">The data source type.</param>
|
||||
/// <returns>The display name of the data source type.</returns>
|
||||
public static string GetDisplayName(this DataSourceType type)
|
||||
{
|
||||
return type switch
|
||||
public static string GetDisplayName(this DataSourceType type) => type switch
|
||||
{
|
||||
DataSourceType.LOCAL_FILE => "Local File",
|
||||
DataSourceType.LOCAL_DIRECTORY => "Local Directory",
|
||||
@ -20,5 +18,4 @@ public static class DataSourceTypeExtension
|
||||
|
||||
_ => "None",
|
||||
};
|
||||
}
|
||||
}
|
@ -2,9 +2,7 @@ namespace AIStudio.Settings.DataModel;
|
||||
|
||||
public static class ThemesExtensions
|
||||
{
|
||||
public static string GetName(this Themes theme)
|
||||
{
|
||||
return theme switch
|
||||
public static string GetName(this Themes theme) => theme switch
|
||||
{
|
||||
Themes.SYSTEM => "Synchronized with the operating system settings",
|
||||
Themes.LIGHT => "Always use light theme",
|
||||
@ -12,5 +10,4 @@ public static class ThemesExtensions
|
||||
|
||||
_ => "Unknown setting",
|
||||
};
|
||||
}
|
||||
}
|
@ -84,7 +84,7 @@ public static class SettingsMigrations
|
||||
{
|
||||
Version = Version.V2,
|
||||
|
||||
Providers = previousData.Providers.Select(provider => provider with { IsSelfHosted = false, Hostname = "" }).ToList(),
|
||||
Providers = previousData.Providers.Select(provider => provider with { IsSelfHosted = false, Hostname = string.Empty }).ToList(),
|
||||
|
||||
EnableSpellchecking = previousData.EnableSpellchecking,
|
||||
IsSavingEnergy = previousData.IsSavingEnergy,
|
||||
|
@ -3,5 +3,12 @@
|
||||
### 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
|
||||
MWAIS0006 | Style | Error | SwitchExpressionMethodAnalyzer
|
||||
MWAIS0007 | Usage | Error | EmptyStringAnalyzer
|
||||
MWAIS0008 | Naming | Error | LocalConstantsAnalyzer
|
13
app/SourceCodeRules/SourceCodeRules/Identifier.cs
Normal file
13
app/SourceCodeRules/SourceCodeRules/Identifier.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace SourceCodeRules;
|
||||
|
||||
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";
|
||||
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";
|
||||
public const string EMPTY_STRING_ANALYZER = $"{Tools.ID_PREFIX}0007";
|
||||
public const string LOCAL_CONSTANTS_ANALYZER = $"{Tools.ID_PREFIX}0008";
|
||||
}
|
@ -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 = Identifier.CONST_STATIC_ANALYZER;
|
||||
|
||||
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<DiagnosticDescriptor> 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 == '_');
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
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 LocalConstantsAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string DIAGNOSTIC_ID = Identifier.LOCAL_CONSTANTS_ANALYZER;
|
||||
|
||||
private static readonly string TITLE = "Local constant variables must be in UPPER_CASE";
|
||||
|
||||
private static readonly string MESSAGE_FORMAT = "Local constant variable '{0}' must be in UPPER_CASE";
|
||||
|
||||
private static readonly string DESCRIPTION = "All local constant variables should be named using UPPER_CASE with words separated by underscores.";
|
||||
|
||||
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<DiagnosticDescriptor> SupportedDiagnostics => [RULE];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterSyntaxNodeAction(AnalyzeLocalDeclaration, SyntaxKind.LocalDeclarationStatement);
|
||||
}
|
||||
|
||||
private static void AnalyzeLocalDeclaration(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var localDeclaration = (LocalDeclarationStatementSyntax)context.Node;
|
||||
if (!localDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.ConstKeyword)))
|
||||
return;
|
||||
|
||||
foreach (var variable in localDeclaration.Declaration.Variables)
|
||||
{
|
||||
var variableName = variable.Identifier.Text;
|
||||
if (!IsUpperCase(variableName))
|
||||
{
|
||||
var diagnostic = Diagnostic.Create(RULE, variable.Identifier.GetLocation(), variableName);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsUpperCase(string name) => name.All(c => char.IsUpper(c) || char.IsDigit(c) || c == '_') &&
|
||||
!string.IsNullOrEmpty(name) && name.Any(char.IsLetter);
|
||||
}
|
@ -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<DiagnosticDescriptor> 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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(ConvertToUpperCodeFixProvider)), Shared]
|
||||
public sealed class ConvertToUpperCodeFixProvider : CodeFixProvider
|
||||
{
|
||||
public override ImmutableArray<string> FixableDiagnosticIds => [Identifier.CONST_STATIC_ANALYZER, Identifier.LOCAL_CONSTANTS_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<VariableDeclaratorSyntax>().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(ConvertToUpperCodeFixProvider)), diagnostic);
|
||||
}
|
||||
|
||||
private async Task<Document> 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();
|
||||
}
|
||||
}
|
@ -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<string> 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<VariableDeclaratorSyntax>().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<Document> 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;
|
||||
}
|
||||
}
|
@ -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<DiagnosticDescriptor> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<string> 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<MethodDeclarationSyntax>()
|
||||
.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<Document> 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();
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
namespace SourceCodeRules.UsageAnalyzers;
|
||||
|
||||
#pragma warning disable RS1038
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
#pragma warning restore RS1038
|
||||
public sealed class EmptyStringAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string DIAGNOSTIC_ID = Identifier.EMPTY_STRING_ANALYZER;
|
||||
|
||||
private static readonly string TITLE = """
|
||||
Use string.Empty instead of ""
|
||||
""";
|
||||
|
||||
private static readonly string MESSAGE_FORMAT = """
|
||||
Use string.Empty instead of ""
|
||||
""";
|
||||
|
||||
private static readonly string DESCRIPTION = """Empty string literals ("") should be replaced with string.Empty for better code consistency and readability except in const contexts.""";
|
||||
|
||||
private const string CATEGORY = "Usage";
|
||||
|
||||
private static readonly DiagnosticDescriptor RULE = new(DIAGNOSTIC_ID, TITLE, MESSAGE_FORMAT, CATEGORY, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DESCRIPTION);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [RULE];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterSyntaxNodeAction(AnalyzeEmptyStringLiteral, SyntaxKind.StringLiteralExpression);
|
||||
}
|
||||
|
||||
private static void AnalyzeEmptyStringLiteral(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var stringLiteral = (LiteralExpressionSyntax)context.Node;
|
||||
if (stringLiteral.Token.ValueText != string.Empty)
|
||||
return;
|
||||
|
||||
if (IsInConstContext(stringLiteral))
|
||||
return;
|
||||
|
||||
var diagnostic = Diagnostic.Create(RULE, stringLiteral.GetLocation());
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
|
||||
private static bool IsInConstContext(LiteralExpressionSyntax stringLiteral)
|
||||
{
|
||||
var variableDeclarator = stringLiteral.FirstAncestorOrSelf<VariableDeclaratorSyntax>();
|
||||
if (variableDeclarator is null)
|
||||
return false;
|
||||
|
||||
var declaration = variableDeclarator.Parent?.Parent;
|
||||
return declaration switch
|
||||
{
|
||||
FieldDeclarationSyntax fieldDeclaration => fieldDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword),
|
||||
LocalDeclarationStatementSyntax localDeclaration => localDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword),
|
||||
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
}
|
@ -6,14 +6,14 @@ using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
namespace SourceCodeRules;
|
||||
namespace SourceCodeRules.UsageAnalyzers;
|
||||
|
||||
#pragma warning disable RS1038
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
#pragma warning restore RS1038
|
||||
public class ProviderAccessAnalyzer : DiagnosticAnalyzer
|
||||
public sealed class ProviderAccessAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string DIAGNOSTIC_ID = $"{Tools.ID_PREFIX}0001";
|
||||
private const string DIAGNOSTIC_ID = Identifier.PROVIDER_ACCESS_ANALYZER;
|
||||
|
||||
private static readonly string TITLE = "Direct access to `Providers` is not allowed";
|
||||
|
||||
@ -25,7 +25,7 @@ public class ProviderAccessAnalyzer : DiagnosticAnalyzer
|
||||
|
||||
private static readonly DiagnosticDescriptor RULE = new(DIAGNOSTIC_ID, TITLE, MESSAGE_FORMAT, CATEGORY, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DESCRIPTION);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(RULE);
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [RULE];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
@ -38,14 +38,14 @@ public class ProviderAccessAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
var memberAccess = (MemberAccessExpressionSyntax)context.Node;
|
||||
|
||||
// Prüfen, ob wir eine Kette von Zugriffen haben, die auf "Providers" endet
|
||||
// Check if the member access is not on the `Providers` property:
|
||||
if (memberAccess.Name.Identifier.Text != "Providers")
|
||||
return;
|
||||
|
||||
// Den kompletten Zugriffspfad aufbauen
|
||||
// Get the full path of the member access:
|
||||
var fullPath = this.GetFullMemberAccessPath(memberAccess);
|
||||
|
||||
// Prüfen, ob der Pfad unserem verbotenen Muster entspricht
|
||||
// Check for the forbidden pattern:
|
||||
if (fullPath.EndsWith("ConfigurationData.Providers"))
|
||||
{
|
||||
var diagnostic = Diagnostic.Create(RULE, memberAccess.GetLocation());
|
@ -0,0 +1,55 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
namespace SourceCodeRules.UsageAnalyzers;
|
||||
|
||||
#pragma warning disable RS1038
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
#pragma warning restore RS1038
|
||||
public class RandomInstantiationAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string DIAGNOSTIC_ID = Identifier.RANDOM_INSTANTIATION_ANALYZER;
|
||||
|
||||
private static readonly string TITLE = "Direct instantiation of Random is not allowed";
|
||||
|
||||
private static readonly string MESSAGE_FORMAT = "Do not use 'new Random()'. Instead, inject and use the ThreadSafeRandom service from the DI container.";
|
||||
|
||||
private static readonly string DESCRIPTION = "Using 'new Random()' can lead to issues in multi-threaded scenarios. Use the ThreadSafeRandom service instead.";
|
||||
|
||||
private const string CATEGORY = "Usage";
|
||||
|
||||
private static readonly DiagnosticDescriptor RULE = new(
|
||||
DIAGNOSTIC_ID,
|
||||
TITLE,
|
||||
MESSAGE_FORMAT,
|
||||
CATEGORY,
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description: DESCRIPTION);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [RULE];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterSyntaxNodeAction(this.AnalyzeObjectCreation, SyntaxKind.ObjectCreationExpression);
|
||||
}
|
||||
|
||||
private void AnalyzeObjectCreation(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var objectCreation = (ObjectCreationExpressionSyntax)context.Node;
|
||||
if (context.SemanticModel.GetSymbolInfo(objectCreation.Type).Symbol is not ITypeSymbol typeSymbol)
|
||||
return;
|
||||
|
||||
if (typeSymbol.ToString() == "System.Random" || typeSymbol is { Name: "Random", ContainingNamespace.Name: "System" })
|
||||
{
|
||||
var diagnostic = Diagnostic.Create(RULE, objectCreation.GetLocation());
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,236 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
namespace SourceCodeRules.UsageAnalyzers;
|
||||
#pragma warning disable RS1038
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
#pragma warning restore RS1038
|
||||
public sealed class ThisUsageAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string DIAGNOSTIC_ID = Identifier.THIS_USAGE_ANALYZER;
|
||||
|
||||
private static readonly string TITLE = "`this.` must be used";
|
||||
|
||||
private static readonly string MESSAGE_FORMAT = "`this.` must be used to access variables, methods, and properties";
|
||||
|
||||
private static readonly string DESCRIPTION = MESSAGE_FORMAT;
|
||||
|
||||
private const string CATEGORY = "Usage";
|
||||
|
||||
private static readonly DiagnosticDescriptor RULE = new(DIAGNOSTIC_ID, TITLE, MESSAGE_FORMAT, CATEGORY, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DESCRIPTION);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [RULE];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterSyntaxNodeAction(this.AnalyzeIdentifier, SyntaxKind.IdentifierName);
|
||||
context.RegisterSyntaxNodeAction(this.AnalyzeGenericName, SyntaxKind.GenericName);
|
||||
}
|
||||
|
||||
private void AnalyzeGenericName(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var genericNameSyntax = (GenericNameSyntax)context.Node;
|
||||
|
||||
// Skip if already part of a 'this' expression
|
||||
if (IsAccessedThroughThis(genericNameSyntax))
|
||||
return;
|
||||
|
||||
if (IsWithinInitializer(genericNameSyntax))
|
||||
return;
|
||||
|
||||
if (IsPartOfMemberAccess(genericNameSyntax))
|
||||
return;
|
||||
|
||||
// Get symbol info for the generic name
|
||||
var symbolInfo = context.SemanticModel.GetSymbolInfo(genericNameSyntax);
|
||||
var symbol = symbolInfo.Symbol;
|
||||
|
||||
if (symbol == null)
|
||||
return;
|
||||
|
||||
// Skip static methods
|
||||
if (symbol.IsStatic)
|
||||
return;
|
||||
|
||||
// Skip local functions
|
||||
if (symbol is IMethodSymbol methodSymbol && IsLocalFunction(methodSymbol))
|
||||
return;
|
||||
|
||||
// Get the containing type of the current context
|
||||
var containingSymbol = context.ContainingSymbol;
|
||||
var currentType = containingSymbol?.ContainingType;
|
||||
|
||||
// If we're in a static context, allow accessing members without this
|
||||
if (IsInStaticContext(containingSymbol))
|
||||
return;
|
||||
|
||||
if (symbol is IMethodSymbol)
|
||||
{
|
||||
var containingType = symbol.ContainingType;
|
||||
|
||||
// If the symbol is a member of the current type or a base type, then require this
|
||||
if (currentType != null && (SymbolEqualityComparer.Default.Equals(containingType, currentType) ||
|
||||
IsBaseTypeOf(containingType, currentType)))
|
||||
{
|
||||
var diagnostic = Diagnostic.Create(RULE, genericNameSyntax.Identifier.GetLocation());
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AnalyzeIdentifier(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var identifierNameSyntax = (IdentifierNameSyntax)context.Node;
|
||||
|
||||
// Skip if this identifier is part of a generic name - we'll handle that separately
|
||||
if (identifierNameSyntax.Parent is GenericNameSyntax)
|
||||
return;
|
||||
|
||||
// Skip if already part of a 'this' expression
|
||||
if (IsAccessedThroughThis(identifierNameSyntax))
|
||||
return;
|
||||
|
||||
if (IsWithinInitializer(identifierNameSyntax))
|
||||
return;
|
||||
|
||||
if (IsPartOfMemberAccess(identifierNameSyntax))
|
||||
return;
|
||||
|
||||
// Also skip if it's part of static import statements
|
||||
if (IsPartOfUsingStaticDirective(identifierNameSyntax))
|
||||
return;
|
||||
|
||||
// Skip if it's part of a namespace or type name
|
||||
if (IsPartOfNamespaceOrTypeName(identifierNameSyntax))
|
||||
return;
|
||||
|
||||
// Get symbol info
|
||||
var symbolInfo = context.SemanticModel.GetSymbolInfo(identifierNameSyntax);
|
||||
var symbol = symbolInfo.Symbol;
|
||||
|
||||
if (symbol == null)
|
||||
return;
|
||||
|
||||
// Skip local variables, parameters, and range variables
|
||||
if (symbol.Kind is SymbolKind.Local or SymbolKind.Parameter or SymbolKind.RangeVariable or SymbolKind.TypeParameter)
|
||||
return;
|
||||
|
||||
// Skip types and namespaces
|
||||
if (symbol.Kind is SymbolKind.NamedType or SymbolKind.Namespace)
|
||||
return;
|
||||
|
||||
// Explicitly check if this is a local function
|
||||
if (symbol is IMethodSymbol methodSymbol && IsLocalFunction(methodSymbol))
|
||||
return;
|
||||
|
||||
// Get the containing type of the current context
|
||||
var containingSymbol = context.ContainingSymbol;
|
||||
var currentType = containingSymbol?.ContainingType;
|
||||
|
||||
// If we're in a static context, allow accessing members without this
|
||||
if (IsInStaticContext(containingSymbol))
|
||||
return;
|
||||
|
||||
// Now check if the symbol is an instance member of the current class
|
||||
if (symbol is IFieldSymbol or IPropertySymbol or IMethodSymbol or IEventSymbol)
|
||||
{
|
||||
// Skip static members
|
||||
if (symbol.IsStatic)
|
||||
return;
|
||||
|
||||
// Skip constants
|
||||
if (symbol is IFieldSymbol { IsConst: true })
|
||||
return;
|
||||
|
||||
var containingType = symbol.ContainingType;
|
||||
|
||||
// If the symbol is a member of the current type or a base type, then require this
|
||||
if (currentType != null && (SymbolEqualityComparer.Default.Equals(containingType, currentType) ||
|
||||
IsBaseTypeOf(containingType, currentType)))
|
||||
{
|
||||
var diagnostic = Diagnostic.Create(RULE, identifierNameSyntax.GetLocation());
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsLocalFunction(IMethodSymbol methodSymbol) => methodSymbol.MethodKind is MethodKind.LocalFunction;
|
||||
|
||||
private static bool IsBaseTypeOf(INamedTypeSymbol baseType, INamedTypeSymbol derivedType)
|
||||
{
|
||||
var currentType = derivedType.BaseType;
|
||||
while (currentType != null)
|
||||
{
|
||||
if (SymbolEqualityComparer.Default.Equals(currentType, baseType))
|
||||
return true;
|
||||
|
||||
currentType = currentType.BaseType;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsInStaticContext(ISymbol? containingSymbol) => containingSymbol?.IsStatic is true;
|
||||
|
||||
private static bool IsAccessedThroughThis(SyntaxNode node)
|
||||
{
|
||||
if (node.Parent is MemberAccessExpressionSyntax memberAccess)
|
||||
if (memberAccess.Expression is ThisExpressionSyntax && memberAccess.Name == node)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsWithinInitializer(SyntaxNode node)
|
||||
{
|
||||
for (var current = node.Parent; current != null; current = current.Parent)
|
||||
if (current is InitializerExpressionSyntax)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsPartOfMemberAccess(SyntaxNode node)
|
||||
{
|
||||
// Check if the node is part of a member access expression where the expression is not 'this':
|
||||
if (node.Parent is MemberAccessExpressionSyntax memberAccess)
|
||||
{
|
||||
// If the member access expression is 'this', it's allowed:
|
||||
if (memberAccess.Expression is ThisExpressionSyntax)
|
||||
return false;
|
||||
|
||||
// If the member access expression is something else (e.g., instance.Member), skip:
|
||||
if (memberAccess.Name == node)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Also check for conditional access expressions (e.g., instance?.Member):
|
||||
if (node.Parent is ConditionalAccessExpressionSyntax)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsPartOfUsingStaticDirective(SyntaxNode node)
|
||||
{
|
||||
for (var current = node.Parent; current != null; current = current.Parent)
|
||||
if (current is UsingDirectiveSyntax)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsPartOfNamespaceOrTypeName(SyntaxNode node)
|
||||
{
|
||||
// Check if a node is part of a namespace, class, or type declaration:
|
||||
if (node.Parent is NameSyntax && node.Parent is not MemberAccessExpressionSyntax)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -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;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Formatting;
|
||||
|
||||
namespace SourceCodeRules.UsageCodeFixes;
|
||||
|
||||
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(EmptyStringCodeFixProvider)), Shared]
|
||||
public class EmptyStringCodeFixProvider : CodeFixProvider
|
||||
{
|
||||
private const string TITLE = """Replace "" with string.Empty""";
|
||||
|
||||
public sealed override ImmutableArray<string> FixableDiagnosticIds => [Identifier.EMPTY_STRING_ANALYZER];
|
||||
|
||||
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
|
||||
|
||||
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
|
||||
{
|
||||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
|
||||
if(root is null)
|
||||
return;
|
||||
|
||||
var diagnostic = context.Diagnostics.First();
|
||||
var diagnosticSpan = diagnostic.Location.SourceSpan;
|
||||
|
||||
if (root.FindToken(diagnosticSpan.Start).Parent is not LiteralExpressionSyntax emptyStringLiteral)
|
||||
return;
|
||||
|
||||
context.RegisterCodeFix(
|
||||
CodeAction.Create(
|
||||
title: TITLE,
|
||||
createChangedDocument: c => ReplaceWithStringEmpty(context.Document, emptyStringLiteral, c),
|
||||
equivalenceKey: TITLE),
|
||||
diagnostic);
|
||||
}
|
||||
private static async Task<Document> ReplaceWithStringEmpty(Document document, LiteralExpressionSyntax emptyStringLiteral, CancellationToken cancellationToken)
|
||||
{
|
||||
var stringEmptyExpression = SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, SyntaxFactory.IdentifierName("string"), SyntaxFactory.IdentifierName("Empty")).WithAdditionalAnnotations(Formatter.Annotation);
|
||||
var root = await document.GetSyntaxRootAsync(cancellationToken);
|
||||
if (root is null)
|
||||
return document;
|
||||
|
||||
var newRoot = root.ReplaceNode(emptyStringLiteral, stringEmptyExpression);
|
||||
return document.WithSyntaxRoot(newRoot);
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
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;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace SourceCodeRules.UsageCodeFixes;
|
||||
|
||||
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ThisUsageCodeFixProvider)), Shared]
|
||||
public class ThisUsageCodeFixProvider : CodeFixProvider
|
||||
{
|
||||
private const string TITLE = "Add 'this.' prefix";
|
||||
|
||||
public sealed override ImmutableArray<string> FixableDiagnosticIds => [Identifier.THIS_USAGE_ANALYZER];
|
||||
|
||||
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
|
||||
|
||||
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
|
||||
{
|
||||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
|
||||
if (root == null)
|
||||
return;
|
||||
|
||||
var diagnostic = context.Diagnostics.First();
|
||||
var diagnosticSpan = diagnostic.Location.SourceSpan;
|
||||
var node = root.FindNode(diagnosticSpan);
|
||||
|
||||
if (node is IdentifierNameSyntax identifierNode)
|
||||
{
|
||||
context.RegisterCodeFix(
|
||||
CodeAction.Create(
|
||||
title: TITLE,
|
||||
createChangedDocument: c => AddThisPrefixAsync(context.Document, identifierNode, c),
|
||||
equivalenceKey: nameof(ThisUsageCodeFixProvider)),
|
||||
diagnostic);
|
||||
}
|
||||
else if (node is GenericNameSyntax genericNameNode)
|
||||
{
|
||||
context.RegisterCodeFix(
|
||||
CodeAction.Create(
|
||||
title: TITLE,
|
||||
createChangedDocument: c => AddThisPrefixAsync(context.Document, genericNameNode, c),
|
||||
equivalenceKey: nameof(ThisUsageCodeFixProvider)),
|
||||
diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<Document> AddThisPrefixAsync(Document document, SyntaxNode node, CancellationToken cancellationToken)
|
||||
{
|
||||
var root = await document.GetSyntaxRootAsync(cancellationToken);
|
||||
if (root == null)
|
||||
return document;
|
||||
|
||||
var thisExpression = SyntaxFactory.ThisExpression();
|
||||
var leadingTrivia = node.GetLeadingTrivia();
|
||||
var memberAccessExpression = SyntaxFactory.MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
thisExpression.WithLeadingTrivia(leadingTrivia),
|
||||
((SimpleNameSyntax)node).WithLeadingTrivia(SyntaxTriviaList.Empty))
|
||||
.WithTrailingTrivia(node.GetTrailingTrivia());
|
||||
|
||||
var newRoot = root.ReplaceNode(node, memberAccessExpression);
|
||||
return document.WithSyntaxRoot(newRoot);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user