From 494e48ddd3233c26188a5b17bb3a5c92fc8224d9 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 12 Aug 2026 20:40:09 +0200 Subject: [PATCH] Exempt the settings manager from the provider access rule --- .../Settings/SettingsManager.cs | 6 --- .../UsageAnalyzers/ProviderAccessAnalyzer.cs | 43 +++++++++++++++++-- 2 files changed, 40 insertions(+), 9 deletions(-) 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)