diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs
index 2c82b24e..8eb924bb 100644
--- a/app/MindWork AI Studio/Settings/SettingsManager.cs
+++ b/app/MindWork AI Studio/Settings/SettingsManager.cs
@@ -1,4 +1,3 @@
-using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Text.Json;
@@ -434,7 +433,6 @@ public sealed class SettingsManager
return localeTag[..separatorIndex];
}
- [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
public Provider GetPreselectedProvider(Tools.Components component, string? currentProviderId = null, bool usePreselectionBeforeCurrentProvider = false)
{
var minimumLevel = this.GetMinimumConfidenceLevel(component);
@@ -486,7 +484,6 @@ public sealed class SettingsManager
return this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.App.PreselectedProvider && x.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel) ?? Provider.NONE;
}
- [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
public Provider GetChatProviderForLoadedChat(string? chatProviderId = null)
{
var minimumLevel = this.GetMinimumConfidenceLevel(Tools.Components.CHAT);
@@ -548,7 +545,6 @@ public sealed class SettingsManager
///
///
/// All configured providers, unfiltered.
- [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
public IReadOnlyList GetAllProviders() => this.ConfigurationData.Providers;
///
@@ -563,7 +559,6 @@ public sealed class SettingsManager
///
/// The id of the provider to look up.
/// The provider, or when no provider with that id exists.
- [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
public Provider GetProviderById(string? providerId)
{
if (string.IsNullOrWhiteSpace(providerId))
@@ -611,7 +606,6 @@ public sealed class SettingsManager
/// The component for which the providers get filtered.
/// An explicit minimum level, which is applied when it is higher than the component's minimum.
/// All providers the component may use.
- [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
public IEnumerable GetConfidentProviders(Tools.Components component, ConfidenceLevel explicitMinimum = ConfidenceLevel.UNKNOWN)
{
var minimumLevel = this.GetEffectiveMinimumConfidenceLevel(component, explicitMinimum);
diff --git a/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/ProviderAccessAnalyzer.cs b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/ProviderAccessAnalyzer.cs
index a2db69df..a0fc03d1 100644
--- a/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/ProviderAccessAnalyzer.cs
+++ b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/ProviderAccessAnalyzer.cs
@@ -17,11 +17,16 @@ public sealed class ProviderAccessAnalyzer : DiagnosticAnalyzer
private static readonly string TITLE = "Direct access to `Providers` is not allowed";
- private static readonly string MESSAGE_FORMAT = "Direct access to `SettingsManager.ConfigurationData.Providers` is not allowed. Instead, use APIs like `SettingsManager.GetPreselectedProvider`, etc.";
+ private static readonly string MESSAGE_FORMAT = "Direct access to `SettingsManager.ConfigurationData.Providers` is not allowed. Instead, use APIs like `SettingsManager.GetAllProviders`, `GetProviderById`, `GetConfidentProviders`, `GetPreselectedProvider`, or `GetChatProviderForLoadedChat`.";
private static readonly string DESCRIPTION = MESSAGE_FORMAT;
private const string CATEGORY = "Usage";
+
+ ///
+ /// The one type which owns the provider list and is therefore allowed to access it directly.
+ ///
+ private const string OWNING_TYPE = "AIStudio.Settings.SettingsManager";
private static readonly DiagnosticDescriptor RULE = new(DIAGNOSTIC_ID, TITLE, MESSAGE_FORMAT, CATEGORY, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DESCRIPTION);
@@ -42,8 +47,17 @@ public sealed class ProviderAccessAnalyzer : DiagnosticAnalyzer
if (memberAccess.Name.Identifier.Text != "Providers")
return;
+ //
+ // The settings manager owns the provider list: it implements the very APIs which all other
+ // code is meant to use, so it must access `Providers` directly. Exempting it here keeps
+ // those implementations free of suppression attributes, which would otherwise read as if
+ // suppressing this rule was a normal thing to do:
+ //
+ if (IsOwningType(context.ContainingSymbol))
+ return;
+
// Get the full path of the member access:
- var fullPath = this.GetFullMemberAccessPath(memberAccess);
+ var fullPath = GetFullMemberAccessPath(memberAccess);
// Check for the forbidden pattern:
if (fullPath.EndsWith("ConfigurationData.Providers"))
@@ -53,7 +67,30 @@ public sealed class ProviderAccessAnalyzer : DiagnosticAnalyzer
}
}
- private string GetFullMemberAccessPath(ExpressionSyntax expression)
+ ///
+ /// Checks whether the analyzed node sits inside the type which owns the provider list.
+ ///
+ ///
+ /// The containing symbol is the member the node belongs to, e.g. a method or a property. We walk
+ /// the chain of containing types so that nested types of the owning type are covered as well.
+ ///
+ /// The symbol containing the analyzed node, which may be null.
+ /// True, when the node belongs to the owning type.
+ private static bool IsOwningType(ISymbol? containingSymbol)
+ {
+ var containingType = containingSymbol as INamedTypeSymbol ?? containingSymbol?.ContainingType;
+ while (containingType != null)
+ {
+ if (containingType.ToDisplayString() == OWNING_TYPE)
+ return true;
+
+ containingType = containingType.ContainingType;
+ }
+
+ return false;
+ }
+
+ private static string GetFullMemberAccessPath(ExpressionSyntax expression)
{
var parts = new List();
while (expression is MemberAccessExpressionSyntax memberAccess)