Moved all extension methods into extension blocks (#1155)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-11-04 10:46:18 +01:00
1 parent acc7d09fa2
commit c0a75f39ac
138 files changed
+5884 -4954

No files matched your search

@@ -19,3 +19,4 @@
MWAIS0013 | Usage | Error | ModelPatternLiteralAnalyzer
MWAIS0014 | Usage | Error | MissingInterpolationAnalyzer
MWAIS0015 | Usage | Error | JsonSourceGenerationAnalyzer
MWAIS0016 | Style | Error | ClassicExtensionMethodAnalyzer
@@ -17,4 +17,5 @@ public static class Identifier
public const string MODEL_PATTERN_LITERAL_ANALYZER = $"{Tools.ID_PREFIX}0013";
public const string MISSING_INTERPOLATION_ANALYZER = $"{Tools.ID_PREFIX}0014";
public const string JSON_SOURCE_GENERATION_ANALYZER = $"{Tools.ID_PREFIX}0015";
public const string CLASSIC_EXTENSION_METHOD_ANALYZER = $"{Tools.ID_PREFIX}0016";
}
@@ -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.StyleAnalyzers;
/// <summary>
/// Refuses extension methods which are declared with a this parameter instead of in an extension block.
/// </summary>
/// <remarks>
/// AI Studio declares its extension members in extension blocks, one per receiver, so that all members of a
/// receiver read alike and extension properties can join them. A method with a params parameter stays a
/// classic one for now: for a member of an extension block, the compiler of the .NET 10.0.1xx SDK warns about
/// every argument passed one by one to a params parameter (CS8620, dotnet/roslyn#80024). Drop that exception
/// once the SDK in metadata.txt carries the fix.
/// </remarks>
#pragma warning disable RS1038
[DiagnosticAnalyzer(LanguageNames.CSharp)]
#pragma warning restore RS1038
public sealed class ClassicExtensionMethodAnalyzer : DiagnosticAnalyzer
{
private const string DIAGNOSTIC_ID = Identifier.CLASSIC_EXTENSION_METHOD_ANALYZER;
private const string TITLE = "Extension methods belong in extension blocks";
private const string MESSAGE_FORMAT = "Declare '{0}' as a member of an extension block for its receiver instead of with a this parameter";
private const string DESCRIPTION = "AI Studio declares its extension members in extension blocks, one per receiver. Only a method with a params parameter stays a classic extension method, until the SDK fixes dotnet/roslyn#80024.";
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(AnalyzeMethod, SyntaxKind.MethodDeclaration);
}
private static void AnalyzeMethod(SyntaxNodeAnalysisContext context)
{
var method = (MethodDeclarationSyntax)context.Node;
var parameters = method.ParameterList.Parameters;
if (parameters.Count == 0)
return;
var thisKeyword = parameters[0].Modifiers.FirstOrDefault(modifier => modifier.IsKind(SyntaxKind.ThisKeyword));
if (thisKeyword.IsKind(SyntaxKind.None))
return;
if (parameters.Any(parameter => parameter.Modifiers.Any(SyntaxKind.ParamsKeyword)))
return;
context.ReportDiagnostic(Diagnostic.Create(RULE, thisKeyword.GetLocation(), method.Identifier.Text));
}
}
@@ -42,7 +42,7 @@ public sealed class DirectI18NGetTextAnalyzer : DiagnosticAnalyzer
return;
var targetMethod = method.ReducedFrom ?? method;
if (targetMethod.Name != "GetText" || targetMethod.ContainingType.Name != "ILangExtensions" || targetMethod.ContainingNamespace.ToDisplayString() != "AIStudio.Tools.PluginSystem")
if (targetMethod.Name != "GetText" || !IsLangExtensions(targetMethod.ContainingType))
return;
if (context.SemanticModel.GetOperation(invocation) is IInvocationOperation operation
@@ -52,6 +52,19 @@ public sealed class DirectI18NGetTextAnalyzer : DiagnosticAnalyzer
context.ReportDiagnostic(Diagnostic.Create(RULE, invocation.GetLocation()));
}
/// <summary>
/// Whether a method of this type is the GetText of the language plugins.
/// </summary>
/// <remarks>
/// A classic extension method belongs to its static class directly. A member of an extension block
/// belongs to the block, and only the block belongs to the static class.
/// </remarks>
private static bool IsLangExtensions(INamedTypeSymbol? type)
{
var staticClass = type is { IsExtension: true } ? type.ContainingType : type;
return staticClass?.Name == "ILangExtensions" && staticClass.ContainingNamespace.ToDisplayString() == "AIStudio.Tools.PluginSystem";
}
private static bool IsCollectorCompatibleWrapper(IMethodSymbol? containingMethod, IInvocationOperation invocation)
{
if (containingMethod?.Name is not ("T" or "TB")