diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 8f55b3a..2399d51 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -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
\ No newline at end of file
+/app/ @MindWorkAI/net-experts
+
+# The source code rules must be reviewed by the release team:
+/app/SourceCodeRules/ @MindWorkAI/release @SommerEngineering
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Assistants/ERI/AllowedLLMProvidersExtensions.cs b/app/MindWork AI Studio/Assistants/ERI/AllowedLLMProvidersExtensions.cs
index 130859b..c9c6462 100644
--- a/app/MindWork AI Studio/Assistants/ERI/AllowedLLMProvidersExtensions.cs
+++ b/app/MindWork AI Studio/Assistants/ERI/AllowedLLMProvidersExtensions.cs
@@ -2,15 +2,12 @@ namespace AIStudio.Assistants.ERI;
public static class AllowedLLMProvidersExtensions
{
- public static string Description(this AllowedLLMProviders provider)
+ public static string Description(this AllowedLLMProviders provider) => provider switch
{
- return 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",
- AllowedLLMProviders.SELF_HOSTED => "Self-hosted LLM providers are allowed: users cannot choose any cloud-based provider",
-
- _ => "Unknown option was selected"
- };
- }
+ 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",
+ AllowedLLMProviders.SELF_HOSTED => "Self-hosted LLM providers are allowed: users cannot choose any cloud-based provider",
+
+ _ => "Unknown option was selected"
+ };
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/MudTextList.razor.cs b/app/MindWork AI Studio/Components/MudTextList.razor.cs
index 551878b..46cde41 100644
--- a/app/MindWork AI Studio/Components/MudTextList.razor.cs
+++ b/app/MindWork AI Studio/Components/MudTextList.razor.cs
@@ -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}";
}
diff --git a/app/MindWork AI Studio/Redirect.cs b/app/MindWork AI Studio/Redirect.cs
index 1b974a3..29c42bc 100644
--- a/app/MindWork AI Studio/Redirect.cs
+++ b/app/MindWork AI Studio/Redirect.cs
@@ -13,7 +13,6 @@ internal static class Redirect
await nextHandler();
return;
}
-
#if DEBUG
diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSourceTypeExtension.cs b/app/MindWork AI Studio/Settings/DataModel/DataSourceTypeExtension.cs
index a630a92..196eac7 100644
--- a/app/MindWork AI Studio/Settings/DataModel/DataSourceTypeExtension.cs
+++ b/app/MindWork AI Studio/Settings/DataModel/DataSourceTypeExtension.cs
@@ -10,15 +10,12 @@ public static class DataSourceTypeExtension
///
/// The data source type.
/// The display name of the data source type.
- public static string GetDisplayName(this DataSourceType type)
+ public static string GetDisplayName(this DataSourceType type) => type switch
{
- return type switch
- {
- DataSourceType.LOCAL_FILE => "Local File",
- DataSourceType.LOCAL_DIRECTORY => "Local Directory",
- DataSourceType.ERI_V1 => "External ERI Server (v1)",
-
- _ => "None",
- };
- }
+ DataSourceType.LOCAL_FILE => "Local File",
+ DataSourceType.LOCAL_DIRECTORY => "Local Directory",
+ DataSourceType.ERI_V1 => "External ERI Server (v1)",
+
+ _ => "None",
+ };
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/DataModel/ThemesExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/ThemesExtensions.cs
index 7942ab7..5d36a1b 100644
--- a/app/MindWork AI Studio/Settings/DataModel/ThemesExtensions.cs
+++ b/app/MindWork AI Studio/Settings/DataModel/ThemesExtensions.cs
@@ -2,15 +2,12 @@ namespace AIStudio.Settings.DataModel;
public static class ThemesExtensions
{
- public static string GetName(this Themes theme)
+ public static string GetName(this Themes theme) => theme switch
{
- return theme switch
- {
- Themes.SYSTEM => "Synchronized with the operating system settings",
- Themes.LIGHT => "Always use light theme",
- Themes.DARK => "Always use dark theme",
-
- _ => "Unknown setting",
- };
- }
+ Themes.SYSTEM => "Synchronized with the operating system settings",
+ Themes.LIGHT => "Always use light theme",
+ Themes.DARK => "Always use dark theme",
+
+ _ => "Unknown setting",
+ };
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/SettingsMigrations.cs b/app/MindWork AI Studio/Settings/SettingsMigrations.cs
index da2a5ee..98482ce 100644
--- a/app/MindWork AI Studio/Settings/SettingsMigrations.cs
+++ b/app/MindWork AI Studio/Settings/SettingsMigrations.cs
@@ -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,
diff --git a/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md b/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md
index 6b73c82..e6f97e7 100644
--- a/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md
+++ b/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md
@@ -2,6 +2,13 @@
### New Rules
- Rule ID | Category | Severity | Notes
------------|----------|----------|------------------------
- MWAIS0001 | Usage | Error | ProviderAccessAnalyzer
\ 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
+ MWAIS0007 | Usage | Error | EmptyStringAnalyzer
+ MWAIS0008 | Naming | Error | LocalConstantsAnalyzer
\ No newline at end of file
diff --git a/app/SourceCodeRules/SourceCodeRules/Identifier.cs b/app/SourceCodeRules/SourceCodeRules/Identifier.cs
new file mode 100644
index 0000000..aa782cf
--- /dev/null
+++ b/app/SourceCodeRules/SourceCodeRules/Identifier.cs
@@ -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";
+}
\ 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 0000000..d327313
--- /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 = 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 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/NamingAnalyzers/LocalConstantsAnalyzer.cs b/app/SourceCodeRules/SourceCodeRules/NamingAnalyzers/LocalConstantsAnalyzer.cs
new file mode 100644
index 0000000..da0e130
--- /dev/null
+++ b/app/SourceCodeRules/SourceCodeRules/NamingAnalyzers/LocalConstantsAnalyzer.cs
@@ -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 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);
+}
\ 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 0000000..6f35469
--- /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/ConvertToUpperCodeFixProvider.cs b/app/SourceCodeRules/SourceCodeRules/NamingCodeFixes/ConvertToUpperCodeFixProvider.cs
new file mode 100644
index 0000000..961e7e0
--- /dev/null
+++ b/app/SourceCodeRules/SourceCodeRules/NamingCodeFixes/ConvertToUpperCodeFixProvider.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(ConvertToUpperCodeFixProvider)), Shared]
+public sealed class ConvertToUpperCodeFixProvider : CodeFixProvider
+{
+ public override ImmutableArray 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().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 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
diff --git a/app/SourceCodeRules/SourceCodeRules/NamingCodeFixes/UnderscorePrefixCodeFixProvider.cs b/app/SourceCodeRules/SourceCodeRules/NamingCodeFixes/UnderscorePrefixCodeFixProvider.cs
new file mode 100644
index 0000000..6308e57
--- /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
diff --git a/app/SourceCodeRules/SourceCodeRules/StyleAnalyzers/SwitchExpressionMethodAnalyzer.cs b/app/SourceCodeRules/SourceCodeRules/StyleAnalyzers/SwitchExpressionMethodAnalyzer.cs
new file mode 100644
index 0000000..2bd7f80
--- /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 0000000..d89751f
--- /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
diff --git a/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/EmptyStringAnalyzer.cs b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/EmptyStringAnalyzer.cs
new file mode 100644
index 0000000..f6cc65b
--- /dev/null
+++ b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/EmptyStringAnalyzer.cs
@@ -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 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();
+ 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
+ };
+ }
+}
\ No newline at end of file
diff --git a/app/SourceCodeRules/SourceCodeRules/ProviderAccessAnalyzer.cs b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/ProviderAccessAnalyzer.cs
similarity index 85%
rename from app/SourceCodeRules/SourceCodeRules/ProviderAccessAnalyzer.cs
rename to app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/ProviderAccessAnalyzer.cs
index a28d7e0..a2db69d 100644
--- a/app/SourceCodeRules/SourceCodeRules/ProviderAccessAnalyzer.cs
+++ b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/ProviderAccessAnalyzer.cs
@@ -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 SupportedDiagnostics => ImmutableArray.Create(RULE);
+ public override ImmutableArray SupportedDiagnostics => [RULE];
public override void Initialize(AnalysisContext context)
{
@@ -37,15 +37,15 @@ public class ProviderAccessAnalyzer : DiagnosticAnalyzer
private void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context)
{
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());
diff --git a/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/RandomInstantiationAnalyzer.cs b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/RandomInstantiationAnalyzer.cs
new file mode 100644
index 0000000..8244aa8
--- /dev/null
+++ b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/RandomInstantiationAnalyzer.cs
@@ -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 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/ThisUsageAnalyzer.cs b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/ThisUsageAnalyzer.cs
new file mode 100644
index 0000000..f48c374
--- /dev/null
+++ b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/ThisUsageAnalyzer.cs
@@ -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 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;
+ }
+}
\ No newline at end of file
diff --git a/app/SourceCodeRules/SourceCodeRules/UsageCodeFixes/EmptyStringCodeFixProvider.cs b/app/SourceCodeRules/SourceCodeRules/UsageCodeFixes/EmptyStringCodeFixProvider.cs
new file mode 100644
index 0000000..fe04dad
--- /dev/null
+++ b/app/SourceCodeRules/SourceCodeRules/UsageCodeFixes/EmptyStringCodeFixProvider.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;
+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 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 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);
+ }
+}
\ No newline at end of file
diff --git a/app/SourceCodeRules/SourceCodeRules/UsageCodeFixes/ThisUsageCodeFixProvider.cs b/app/SourceCodeRules/SourceCodeRules/UsageCodeFixes/ThisUsageCodeFixProvider.cs
new file mode 100644
index 0000000..6f98772
--- /dev/null
+++ b/app/SourceCodeRules/SourceCodeRules/UsageCodeFixes/ThisUsageCodeFixProvider.cs
@@ -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 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 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);
+ }
+}
\ No newline at end of file