File: AspireExportAnalyzer.cs
Web Access
Project: src\src\Aspire.Hosting.Integration.Analyzers\Aspire.Hosting.Integration.Analyzers.csproj (Aspire.Hosting.Integration.Analyzers)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using Aspire.Hosting.Analyzers.Infrastructure;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
 
namespace Aspire.Hosting.Analyzers;
 
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal partial class AspireExportAnalyzer : DiagnosticAnalyzer
{
    private const string RunSyncOnBackgroundThreadPropertyName = "RunSyncOnBackgroundThread";
    private const string ExposeMethodsPropertyName = "ExposeMethods";
    private const string ExposePropertiesPropertyName = "ExposeProperties";
    private const string MethodNamePropertyName = "MethodName";
    private const string DescriptionPropertyName = "Description";
 
    // Matches: valid method name (camelCase identifier, may contain dots for namespacing)
    // Examples: addRedis, addContainer, Dictionary.set
    private static readonly Regex s_exportIdPattern = new(
        @"^[a-zA-Z][a-zA-Z0-9.]*$",
        RegexOptions.Compiled);
 
    private readonly struct CapabilityExport : IEquatable<CapabilityExport>
    {
        public CapabilityExport(string source, Location location)
        {
            Source = source;
            Location = location;
        }
 
        public string Source { get; }
 
        public Location Location { get; }
 
        public bool Equals(CapabilityExport other)
        {
            return Source == other.Source && Location.Equals(other.Location);
        }
 
        public override bool Equals(object? obj)
        {
            return obj is CapabilityExport other && Equals(other);
        }
 
        public override int GetHashCode()
        {
            return StringComparer.Ordinal.GetHashCode(Source) ^ Location.GetHashCode();
        }
    }
 
    private readonly struct GeneratedMethodNameExport : IEquatable<GeneratedMethodNameExport>
    {
        public GeneratedMethodNameExport(string source, Location location, string effectiveExportId)
        {
            Source = source;
            Location = location;
            EffectiveExportId = effectiveExportId;
        }
 
        public string Source { get; }
 
        public Location Location { get; }
 
        public string EffectiveExportId { get; }
 
        public bool Equals(GeneratedMethodNameExport other)
        {
            return Source == other.Source &&
                Location.Equals(other.Location) &&
                EffectiveExportId == other.EffectiveExportId;
        }
 
        public override bool Equals(object? obj)
        {
            return obj is GeneratedMethodNameExport other && Equals(other);
        }
 
        public override int GetHashCode()
        {
            return StringComparer.Ordinal.GetHashCode(Source) ^
                Location.GetHashCode() ^
                StringComparer.Ordinal.GetHashCode(EffectiveExportId);
        }
    }
 
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => Diagnostics.SupportedDiagnostics;
 
    public override void Initialize(AnalysisContext context)
    {
        context.EnableConcurrentExecution();
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.RegisterCompilationStartAction(AnalyzeCompilationStart);
    }
 
    private void AnalyzeCompilationStart(CompilationStartAnalysisContext context)
    {
        var wellKnownTypes = WellKnownTypes.GetOrCreate(context.Compilation);
 
        // Try to get the AspireExportAttribute type - if it doesn't exist, nothing to analyze
        INamedTypeSymbol? aspireExportAttribute;
        try
        {
            aspireExportAttribute = wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Aspire_Hosting_AspireExportAttribute);
        }
        catch (InvalidOperationException)
        {
            // Type not found in compilation, nothing to analyze
            return;
        }
 
        var currentAssemblyExportedTypes = GetAssemblyExportedTypes(context.Compilation.Assembly, aspireExportAttribute);
 
        // Try to get AspireExportIgnoreAttribute for ASPIREEXPORT008
        INamedTypeSymbol? aspireExportIgnoreAttribute = null;
        try
        {
            aspireExportIgnoreAttribute = wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Aspire_Hosting_AspireExportIgnoreAttribute);
        }
        catch (InvalidOperationException)
        {
            // Type not found, missing attribute check won't run
        }
 
        // Try to get AspireUnionAttribute for ASPIREEXPORT005/006 validation
        INamedTypeSymbol? aspireUnionAttribute = null;
        try
        {
            aspireUnionAttribute = wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Aspire_Hosting_AspireUnionAttribute);
        }
        catch (InvalidOperationException)
        {
            // Type not found, union validation won't run
        }
 
        // Collection for ASPIREEXPORT007: track export IDs to detect duplicates
        // Key: (exportId, targetTypeFullName), Value: list of (method, location)
        var exportsByKey = new ConcurrentDictionary<(string ExportId, string TargetType), ConcurrentBag<(IMethodSymbol Method, Location Location)>>();
 
        // Collection for ASPIREEXPORT013: track generated capability IDs across the assembly.
        var capabilityIds = new ConcurrentDictionary<string, ConcurrentBag<CapabilityExport>>();
 
        // Collection for ASPIREEXPORT014: track generated member names per generated target type.
        var generatedMethodNames = new ConcurrentDictionary<(string MethodName, string TargetType), ConcurrentBag<GeneratedMethodNameExport>>();
        AnalyzeAssemblyExportedTypes(context.Compilation, aspireExportAttribute, aspireExportIgnoreAttribute, capabilityIds, generatedMethodNames, context.CancellationToken);
 
        // ASPIREEXPORT017: track whether this assembly has any [AspireExport] coverage so the
        // compilation-end action can require the polyglot opt-in. Seeded from assembly-level and
        // type-level exports; member-level exports flip it from the symbol actions below.
        // Writing 'true' from concurrent symbol actions is safe because the value is monotonic.
        var assemblyHasAspireExport = new StrongBox<bool>(
            currentAssemblyExportedTypes.Count > 0 || HasAnyAspireExportAttribute(context.Compilation.Assembly, aspireExportAttribute));
 
        context.RegisterSymbolAction(
            c => AnalyzeMethod(c, wellKnownTypes, aspireExportAttribute, aspireExportIgnoreAttribute, aspireUnionAttribute, currentAssemblyExportedTypes, exportsByKey, capabilityIds, generatedMethodNames, assemblyHasAspireExport),
            SymbolKind.Method);
 
        context.RegisterSymbolAction(
            c => AnalyzeNamedType(c, wellKnownTypes, aspireExportAttribute, aspireExportIgnoreAttribute, capabilityIds, generatedMethodNames, assemblyHasAspireExport),
            SymbolKind.NamedType);
 
        context.RegisterSymbolAction(
            c => AnalyzeProperty(c, aspireExportAttribute, assemblyHasAspireExport),
            SymbolKind.Property);
 
        context.RegisterCompilationEndAction(c => ReportAssemblyExportDescriptions(c, aspireExportAttribute));
 
        // At the end of compilation, report duplicate export IDs
        context.RegisterCompilationEndAction(c => ReportDuplicateExports(c, exportsByKey));
        context.RegisterCompilationEndAction(c => ReportDuplicateCapabilityIds(c, capabilityIds));
        context.RegisterCompilationEndAction(c => ReportDuplicateGeneratedMethodNames(c, generatedMethodNames));
 
        // ASPIREEXPORT017: a project that runs the export analyzer is treated as a polyglot integration
        // by default. If it has no [AspireExport] coverage it must either add some or acknowledge it is not
        // a polyglot integration via <IsAspirePolyglotCompatible>false</IsAspirePolyglotCompatible>.
        context.RegisterCompilationEndAction(c => ReportMissingPolyglotCompatibleMarker(c, assemblyHasAspireExport));
 
        // Warn when exported builder methods invoke synchronous callback delegates inline. Deferred callbacks
        // that are stored for later execution are fine, and exports that opt into background-thread dispatch
        // are handled safely by the runtime.
        // NOTE: This check only covers classic static `this`-parameter extension methods. C# 14 extension
        // block instance members (method.IsExtensionMethod == false) are not analyzed here yet; missing the
        // warning is non-blocking and never produces a false positive.
        var inlineDelegateInvocationCache = new ConcurrentDictionary<ISymbol, ImmutableHashSet<int>>(SymbolEqualityComparer.Default);
        context.RegisterOperationBlockStartAction(c =>
        {
            if (c.OwningSymbol is not IMethodSymbol method ||
                !method.IsExtensionMethod ||
                method.Parameters.Length == 0 ||
                !IsBuilderType(method.Parameters[0].Type, wellKnownTypes) ||
                !TryGetEffectiveAspireExportAttribute(method, aspireExportAttribute, out var exportAttribute, out var containingTypeExportAttribute) ||
                IsRunSyncOnBackgroundThreadEnabled(exportAttribute) ||
                IsRunSyncOnBackgroundThreadEnabled(containingTypeExportAttribute))
            {
                return;
            }
 
            var synchronousDelegateParameters = method.Parameters
                .Skip(1)
                .Where(IsSynchronousDelegateParameter)
                .ToDictionary(p => p.Name, p => p, StringComparer.Ordinal);
 
            if (synchronousDelegateParameters.Count == 0)
            {
                return;
            }
 
            var reportedParameters = new ConcurrentDictionary<string, byte>(StringComparer.Ordinal);
            c.RegisterOperationAction(
                oc => AnalyzeInlineSynchronousDelegateInvocation(
                    oc,
                    context.Compilation.Assembly,
                    method,
                    synchronousDelegateParameters,
                    reportedParameters,
                    inlineDelegateInvocationCache),
                OperationKind.Invocation);
        });
    }
 
    private static void AnalyzeMethod(
        SymbolAnalysisContext context,
        WellKnownTypes wellKnownTypes,
        INamedTypeSymbol aspireExportAttribute,
        INamedTypeSymbol? aspireExportIgnoreAttribute,
        INamedTypeSymbol? aspireUnionAttribute,
        HashSet<ITypeSymbol> currentAssemblyExportedTypes,
        ConcurrentDictionary<(string ExportId, string TargetType), ConcurrentBag<(IMethodSymbol Method, Location Location)>> exportsByKey,
        ConcurrentDictionary<string, ConcurrentBag<CapabilityExport>> capabilityIds,
        ConcurrentDictionary<(string MethodName, string TargetType), ConcurrentBag<GeneratedMethodNameExport>> generatedMethodNames,
        StrongBox<bool> assemblyHasAspireExport)
    {
        var method = (IMethodSymbol)context.Symbol;
 
        // Find AspireExportAttribute on the method
        AttributeData? exportAttribute = null;
        var hasExportIgnore = false;
        var isObsolete = false;
        foreach (var attr in method.GetAttributes())
        {
            if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, aspireExportAttribute))
            {
                exportAttribute = attr;
            }
            else if (aspireExportIgnoreAttribute is not null &&
                     SymbolEqualityComparer.Default.Equals(attr.AttributeClass, aspireExportIgnoreAttribute))
            {
                hasExportIgnore = true;
            }
            else if (attr.AttributeClass?.Name == "ObsoleteAttribute")
            {
                isObsolete = true;
            }
        }
 
        var containingTypeHasExportIgnore = false;
        if (!hasExportIgnore && aspireExportIgnoreAttribute is not null)
        {
            foreach (var attr in method.ContainingType.GetAttributes())
            {
                if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, aspireExportIgnoreAttribute))
                {
                    containingTypeHasExportIgnore = true;
                    break;
                }
            }
        }
 
        // ASPIREEXPORT008: Check for missing export attributes on builder extension methods
        if (exportAttribute is null && !hasExportIgnore && !containingTypeHasExportIgnore && !isObsolete)
        {
            AnalyzeMissingExportAttribute(context, method, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes);
        }
 
        if (exportAttribute is null)
        {
            return;
        }
 
        // ASPIREEXPORT017: this assembly has at least one exported member.
        assemblyHasAspireExport.Value = true;
 
        var attributeSyntax = exportAttribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken);
        var location = attributeSyntax?.GetLocation() ?? method.Locations.FirstOrDefault() ?? Location.None;
        AnalyzeExportDescription(context, exportAttribute, location);
 
        var containingTypeExportAttribute = GetContainingTypeAspireExportAttribute(method.ContainingType, aspireExportAttribute);
 
        // Rule 1: Method must be static.
        // C# 14 extension block members (declared inside `extension(receiver) { ... }`) are surfaced
        // by Roslyn as non-static instance members even though the compiler lowers them to static
        // extension methods in IL. They are valid [AspireExport] targets, so they must not trip this rule.
        if (!method.IsStatic && !IsExtensionBlockMember(method) && containingTypeExportAttribute is null)
        {
            context.ReportDiagnostic(Diagnostic.Create(
                Diagnostics.s_exportMethodMustBeStatic,
                location,
                method.Name));
        }
 
        // Rule 2: Validate export ID format
        var exportId = GetExportId(exportAttribute);
        var isExportIdFormatValid = exportId is not null && s_exportIdPattern.IsMatch(exportId);
        if (exportId is not null && !isExportIdFormatValid)
        {
            context.ReportDiagnostic(Diagnostic.Create(
                Diagnostics.s_invalidExportIdFormat,
                location,
                exportId));
        }
 
        // Compute the effective export ID: either from the explicit attribute or auto-derived from method name (camelCase)
        // Normalize empty/invalid exportId to null so the fallback applies
        var derivedExportId = GetDerivedExportId(method, containingTypeExportAttribute);
        var normalizedExportId = isExportIdFormatValid ? exportId : null;
        var effectiveExportId = normalizedExportId ?? derivedExportId;
 
        // Build the effective parameter list used by the receiver/target-type rules below. For classic
        // `this`-parameter extension methods (and ordinary methods) this is method.Parameters. For C# 14
        // extension block instance members the receiver lives on the extension container rather than in
        // Parameters, so GetEffectiveExportParameters prepends it to produce the same [receiver, ...args]
        // shape, letting the rules treat both extension syntaxes identically.
        var effectiveParameters = GetEffectiveExportParameters(method);
 
        // Track the runtime capability ID for static exports. Instance exports are tracked from
        // their containing type so ExposeMethods/ExposeProperties semantics match the scanner.
        // Extension block instance members are static-equivalent exports, so they are tracked here too.
        if (IsStaticForExport(method) && effectiveExportId is not null)
        {
            AddCapabilityExport(
                capabilityIds,
                $"{context.Compilation.Assembly.Identity.Name}/{effectiveExportId}",
                GetMethodDisplayString(method),
                location);
 
            var generatedMethodName = GetNamedStringArgument(exportAttribute, MethodNamePropertyName) ?? effectiveExportId;
            var generatedTargetType = GetGeneratedTargetTypeName(method);
 
            AddGeneratedMethodNameExport(
                generatedMethodNames,
                generatedMethodName,
                generatedTargetType,
                effectiveExportId,
                GetMethodDisplayString(method),
                location);
        }
 
        // Rule 2b (ASPIREEXPORT011): Warn when explicit id matches the convention-derived name.
        // Suppressed when Rule 7 (ASPIREEXPORT009) also fires — the two give contradictory advice
        // (ASPIREEXPORT011 says "remove the id"; ASPIREEXPORT009 says "make the id more specific"),
        // so only the actionable ASPIREEXPORT009 should appear.
        if (isExportIdFormatValid &&
            string.Equals(exportId, derivedExportId, StringComparison.Ordinal) &&
            !HasConcreteResourceBuilderTargetParameter(method, effectiveParameters, wellKnownTypes))
        {
            context.ReportDiagnostic(Diagnostic.Create(
                Diagnostics.s_redundantExportId,
                location,
                exportId,
                method.Name));
        }
 
        // Rule 3: Validate return type is ATS-compatible
        if (!IsAtsCompatibleType(method.ReturnType, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes))
        {
            context.ReportDiagnostic(Diagnostic.Create(
                Diagnostics.s_returnTypeMustBeAtsCompatible,
                location,
                method.Name,
                method.ReturnType.ToDisplayString()));
        }
 
        // Rule 4: Validate parameter types are ATS-compatible
        foreach (var parameter in method.Parameters)
        {
            if (!IsAtsCompatibleParameter(parameter, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes))
            {
                context.ReportDiagnostic(Diagnostic.Create(
                    Diagnostics.s_parameterTypeMustBeAtsCompatible,
                    location,
                    parameter.Name,
                    parameter.Type.ToDisplayString(),
                    method.Name));
            }
 
            // Rule 5 (ASPIREEXPORT005/006): Validate [AspireUnion] on parameters
            if (aspireUnionAttribute is not null)
            {
                AnalyzeUnionAttribute(context, parameter.GetAttributes(), aspireUnionAttribute, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes);
            }
        }
 
        // Rule 6 (ASPIREEXPORT007): Track export for duplicate detection
        if (effectiveExportId is not null && IsExtensionLike(method) && effectiveParameters.Length > 0)
        {
            var targetType = effectiveParameters[0].Type;
            var targetTypeName = targetType.ToDisplayString();
            var key = (effectiveExportId, targetTypeName);
            var bag = exportsByKey.GetOrAdd(key, _ => new ConcurrentBag<(IMethodSymbol, Location)>());
            bag.Add((method, location));
        }
 
        // Rule 7 (ASPIREEXPORT009): Warn when export name may collide across integrations
        if (effectiveExportId is not null && IsExtensionLike(method) && effectiveParameters.Length > 0)
        {
            AnalyzeExportNameUniqueness(context, method, effectiveParameters, effectiveExportId, wellKnownTypes, location);
        }
 
        // Rule 8 (ASPIREEXPORT012): Check that callback parameter types (Action<T>/Func<T>) have [AspireExport]
        AnalyzeCallbackContextTypes(context, method, aspireExportAttribute, location);
    }
 
    private static void AnalyzeNamedType(
        SymbolAnalysisContext context,
        WellKnownTypes wellKnownTypes,
        INamedTypeSymbol aspireExportAttribute,
        INamedTypeSymbol? aspireExportIgnoreAttribute,
        ConcurrentDictionary<string, ConcurrentBag<CapabilityExport>> capabilityIds,
        ConcurrentDictionary<(string MethodName, string TargetType), ConcurrentBag<GeneratedMethodNameExport>> generatedMethodNames,
        StrongBox<bool> assemblyHasAspireExport)
    {
        var type = (INamedTypeSymbol)context.Symbol;
        AnalyzeDtoType(type, wellKnownTypes, aspireExportIgnoreAttribute, context);
 
        var typeExportAttribute = GetContainingTypeAspireExportAttribute(type, aspireExportAttribute);
        if (typeExportAttribute is not null)
        {
            // ASPIREEXPORT017: a type-level [AspireExport] is export coverage too.
            assemblyHasAspireExport.Value = true;
 
            var location = GetAttributeLocation(typeExportAttribute, context.CancellationToken) ?? type.Locations.FirstOrDefault() ?? Location.None;
            AnalyzeExportDescription(context, typeExportAttribute, location);
        }
 
        AnalyzeContextType(type, typeExportAttribute, context.Compilation.Assembly.Identity.Name, aspireExportAttribute, aspireExportIgnoreAttribute, capabilityIds, generatedMethodNames, context.CancellationToken);
    }
 
    private static void AnalyzeProperty(SymbolAnalysisContext context, INamedTypeSymbol aspireExportAttribute, StrongBox<bool> assemblyHasAspireExport)
    {
        var property = (IPropertySymbol)context.Symbol;
        var propertyExportAttribute = GetAspireExportAttribute(property, aspireExportAttribute);
        if (propertyExportAttribute is null)
        {
            return;
        }
 
        // ASPIREEXPORT017: a property-level [AspireExport] is export coverage too.
        assemblyHasAspireExport.Value = true;
 
        var location = GetAttributeLocation(propertyExportAttribute, context.CancellationToken) ?? property.Locations.FirstOrDefault() ?? Location.None;
        AnalyzeExportDescription(context, propertyExportAttribute, location);
    }
 
    private static void AnalyzeDtoType(
        INamedTypeSymbol type,
        WellKnownTypes wellKnownTypes,
        INamedTypeSymbol? aspireExportIgnoreAttribute,
        SymbolAnalysisContext context)
    {
        if (!HasAspireDtoAttribute(type))
        {
            return;
        }
 
        foreach (var property in GetInstanceProperties(type))
        {
            if (IsMutableCollectionType(property.Type, wellKnownTypes) &&
                property.SetMethod is null &&
                !property.IsStatic &&
                property.GetMethod?.DeclaredAccessibility == Accessibility.Public &&
                !HasAspireExportIgnoreAttribute(property, aspireExportIgnoreAttribute))
            {
                context.ReportDiagnostic(Diagnostic.Create(
                    Diagnostics.s_dtoMutableCollectionPropertyMustBeInitSettable,
                    property.Locations.FirstOrDefault() ?? type.Locations.FirstOrDefault() ?? Location.None,
                    $"{type.Name}.{property.Name}"));
            }
        }
    }
 
    private static void ReportAssemblyExportDescriptions(CompilationAnalysisContext context, INamedTypeSymbol aspireExportAttribute)
    {
        foreach (var attribute in context.Compilation.Assembly.GetAttributes())
        {
            if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, aspireExportAttribute))
            {
                continue;
            }
 
            var location = GetAttributeLocation(attribute, context.CancellationToken) ?? Location.None;
            if (HasNamedArgument(attribute, DescriptionPropertyName))
            {
                context.ReportDiagnostic(Diagnostic.Create(Diagnostics.s_descriptionShouldUseXmlDocs, location));
            }
        }
    }
 
    // ASPIREEXPORT017: a project that runs the export analyzer is a polyglot integration by default and
    // gets the 'polyglot' NuGet tag so `aspire add` can surface it to non-C# AppHosts. When such a project
    // has no [AspireExport] surface at all it is almost certainly either missing its exports or is really
    // infrastructure that should not be discovered, so the build fails unless the author acknowledges that
    // with <IsAspirePolyglotCompatible>false</IsAspirePolyglotCompatible> (which also omits the tag).
    private static void ReportMissingPolyglotCompatibleMarker(CompilationAnalysisContext context, StrongBox<bool> assemblyHasAspireExport)
    {
        if (assemblyHasAspireExport.Value)
        {
            return;
        }
 
        if (IsPolyglotCompatibilityOptedOut(context.Options.AnalyzerConfigOptionsProvider.GlobalOptions))
        {
            return;
        }
 
        context.ReportDiagnostic(Diagnostic.Create(
            Diagnostics.s_missingPolyglotCompatibleMarker,
            Location.None,
            context.Compilation.Assembly.Identity.Name));
    }
 
    private static bool IsPolyglotCompatibilityOptedOut(Microsoft.CodeAnalysis.Diagnostics.AnalyzerConfigOptions options)
    {
        // Exposed via <CompilerVisibleProperty Include="IsAspirePolyglotCompatible" /> in the build, surfaced
        // to analyzers as the 'build_property.<name>' key. Only an explicit 'false' opts out; any other value
        // (or the property being absent) leaves the project polyglot-compatible by default.
        return options.TryGetValue("build_property.IsAspirePolyglotCompatible", out var value)
            && bool.TryParse(value, out var enabled)
            && !enabled;
    }
 
    private static bool HasAnyAspireExportAttribute(IAssemblySymbol assembly, INamedTypeSymbol aspireExportAttribute)
    {
        foreach (var attribute in assembly.GetAttributes())
        {
            if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, aspireExportAttribute))
            {
                return true;
            }
        }
 
        return false;
    }
 
    private static void AnalyzeAssemblyExportedTypes(
        Compilation compilation,
        INamedTypeSymbol aspireExportAttribute,
        INamedTypeSymbol? aspireExportIgnoreAttribute,
        ConcurrentDictionary<string, ConcurrentBag<CapabilityExport>> capabilityIds,
        ConcurrentDictionary<(string MethodName, string TargetType), ConcurrentBag<GeneratedMethodNameExport>> generatedMethodNames,
        CancellationToken cancellationToken)
    {
        foreach (var attribute in compilation.Assembly.GetAttributes())
        {
            if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, aspireExportAttribute) ||
                !TryGetAssemblyExportedType(attribute, out var exportedType) ||
                exportedType is not INamedTypeSymbol namedType ||
                (!IsBooleanNamedArgumentEnabled(attribute, ExposePropertiesPropertyName) &&
                 !IsBooleanNamedArgumentEnabled(attribute, ExposeMethodsPropertyName)))
            {
                continue;
            }
 
            AnalyzeContextType(namedType, attribute, compilation.Assembly.Identity.Name, aspireExportAttribute, aspireExportIgnoreAttribute, capabilityIds, generatedMethodNames, cancellationToken);
        }
    }
 
    private static void AnalyzeContextType(
        INamedTypeSymbol type,
        AttributeData? typeExportAttribute,
        string assemblyName,
        INamedTypeSymbol aspireExportAttribute,
        INamedTypeSymbol? aspireExportIgnoreAttribute,
        ConcurrentDictionary<string, ConcurrentBag<CapabilityExport>> capabilityIds,
        ConcurrentDictionary<(string MethodName, string TargetType), ConcurrentBag<GeneratedMethodNameExport>> generatedMethodNames,
        CancellationToken cancellationToken)
    {
        if (typeExportAttribute is null)
        {
            return;
        }
 
        if (HasAspireExportIgnoreAttribute(type, aspireExportIgnoreAttribute))
        {
            return;
        }
 
        var exposeProperties = IsBooleanNamedArgumentEnabled(typeExportAttribute, ExposePropertiesPropertyName);
        var exposeMethods = IsBooleanNamedArgumentEnabled(typeExportAttribute, ExposeMethodsPropertyName);
 
        if (!exposeProperties && !exposeMethods)
        {
            var hasExportedMember = type.GetMembers()
                .Any(member => member is IMethodSymbol or IPropertySymbol &&
                    GetAspireExportAttribute(member, aspireExportAttribute) is not null);
 
            if (!hasExportedMember)
            {
                return;
            }
        }
 
        var package = GetCapabilityPackage(type, assemblyName);
        var typeName = GetRuntimeTypeName(type);
        var typeId = $"{package}/{typeName}";
        var generatedTargetType = GetGeneratedTargetTypeName(type);
 
        foreach (var property in GetInstanceProperties(type))
        {
            if (property.IsStatic ||
                HasAspireExportIgnoreAttribute(property, aspireExportIgnoreAttribute))
            {
                continue;
            }
 
            var memberExportAttribute = GetAspireExportAttribute(property, aspireExportAttribute);
            var isPublicGetter = property.GetMethod?.DeclaredAccessibility == Accessibility.Public;
            if (!ShouldExportMember(isPublicGetter, exposeProperties, memberExportAttribute))
            {
                continue;
            }
 
            var location = GetAttributeLocation(memberExportAttribute, cancellationToken) ??
                property.Locations.FirstOrDefault() ??
                Location.None;
            var customMethodName = memberExportAttribute is null ? null : GetExportId(memberExportAttribute);
            var methodNameOverride = GetNamedStringArgument(memberExportAttribute, MethodNamePropertyName);
            var getterMethodName = methodNameOverride ?? ToCamelCase(property.Name);
 
            if (property.GetMethod is not null)
            {
                var getMethodName = customMethodName ?? $"{typeName}.{getterMethodName}";
                AddCapabilityExport(
                    capabilityIds,
                    $"{package}/{getMethodName}",
                    property.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
                    location);
 
                AddGeneratedMethodNameExport(
                    generatedMethodNames,
                    getterMethodName,
                    generatedTargetType,
                    getMethodName,
                    property.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
                    location);
            }
 
            if (property.SetMethod is { IsInitOnly: false })
            {
                var setterMethodNameSuffix = methodNameOverride is { Length: > 0 }
                    ? char.ToUpperInvariant(methodNameOverride[0]) + methodNameOverride.Substring(1)
                    : property.Name;
                var setterMethodName = $"set{setterMethodNameSuffix}";
                AddCapabilityExport(
                    capabilityIds,
                    $"{typeId}.{setterMethodName}",
                    property.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
                    location);
 
                AddGeneratedMethodNameExport(
                    generatedMethodNames,
                    setterMethodName,
                    generatedTargetType,
                    $"{typeId}.{setterMethodName}",
                    property.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
                    location);
            }
        }
 
        foreach (var method in type.GetMembers().OfType<IMethodSymbol>())
        {
            if (method.IsStatic ||
                method.MethodKind != MethodKind.Ordinary ||
                IsSpecialRuntimeMethod(method) ||
                method.IsGenericMethod ||
                HasAspireExportIgnoreAttribute(method, aspireExportIgnoreAttribute))
            {
                continue;
            }
 
            var memberExportAttribute = GetAspireExportAttribute(method, aspireExportAttribute);
            if (!ShouldExportMember(method.DeclaredAccessibility == Accessibility.Public, exposeMethods, memberExportAttribute))
            {
                continue;
            }
 
            var customMethodName = memberExportAttribute is null ? null : GetExportId(memberExportAttribute);
            var methodCapabilityName = customMethodName ?? (exposeMethods
                ? $"{typeName}.{ToCamelCase(method.Name)}"
                : ToCamelCase(method.Name));
            var location = GetAttributeLocation(memberExportAttribute, cancellationToken) ??
                method.Locations.FirstOrDefault() ??
                Location.None;
 
            AddCapabilityExport(
                capabilityIds,
                $"{package}/{methodCapabilityName}",
                GetMethodDisplayString(method),
                location);
        }
    }
 
    private static IEnumerable<IPropertySymbol> GetInstanceProperties(INamedTypeSymbol type)
    {
        var seenPropertyNames = new HashSet<string>(StringComparer.Ordinal);
        for (INamedTypeSymbol? current = type; current is not null; current = current.BaseType)
        {
            foreach (var property in current.GetMembers().OfType<IPropertySymbol>())
            {
                if (property.IsStatic || !seenPropertyNames.Add(property.Name))
                {
                    continue;
                }
 
                yield return property;
            }
        }
    }
 
    private static void AnalyzeMissingExportAttribute(
        SymbolAnalysisContext context,
        IMethodSymbol method,
        WellKnownTypes wellKnownTypes,
        INamedTypeSymbol aspireExportAttribute,
        HashSet<ITypeSymbol> currentAssemblyExportedTypes)
    {
        // Only check public static extension methods.
        // C# 14 extension block instance members are intentionally not covered here yet: they surface as
        // non-static (IsStatic == false) and IsExtensionMethod == false, so the guard below skips them.
        // ASPIREEXPORT008 only nudges authors to add a missing [AspireExport], so the gap is non-blocking
        // (it can cause a missing suggestion, never a false positive). This parallels the ASPIREEXPORT010
        // limitation; expand coverage here only if extension-block authors need the same nudge.
        if (!method.IsStatic || !method.IsExtensionMethod || method.DeclaredAccessibility != Accessibility.Public)
        {
            return;
        }
 
        if (method.Parameters.Length == 0)
        {
            return;
        }
 
        // Only check methods extending exported handle types that participate in ATS.
        var firstParamType = method.Parameters[0].Type;
        if (!RequiresExplicitExportCoverage(firstParamType, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes))
        {
            return;
        }
 
        // Determine the incompatibility reason (if any) to include in the warning
        var reason = GetIncompatibilityReason(method, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes);
        var location = method.Locations.FirstOrDefault() ?? Location.None;
 
        context.ReportDiagnostic(Diagnostic.Create(
            Diagnostics.s_missingExportAttribute,
            location,
            method.Name,
            reason ?? "Add [AspireExport] if ATS-compatible, or [AspireExportIgnore] with a reason."));
    }
 
    /// <summary>
    /// Returns true when the method satisfies the ASPIREEXPORT009 preconditions (open-generic
    /// <c>IResourceBuilder&lt;T&gt;</c> first parameter plus at least one concrete
    /// <c>IResourceBuilder&lt;ConcreteType&gt;</c> parameter), independently of what the export ID is.
    /// Used to suppress ASPIREEXPORT011 when ASPIREEXPORT009 would fire for the same method.
    /// </summary>
    private static bool HasConcreteResourceBuilderTargetParameter(IMethodSymbol method, ImmutableArray<IParameterSymbol> effectiveParameters, WellKnownTypes wellKnownTypes)
    {
        if (!IsExtensionLike(method) || effectiveParameters.Length < 2)
        {
            return false;
        }
 
        if (!IsOpenGenericResourceBuilder(effectiveParameters[0].Type, wellKnownTypes))
        {
            return false;
        }
 
        for (var i = 1; i < effectiveParameters.Length; i++)
        {
            if (GetConcreteResourceBuilderTypeName(effectiveParameters[i].Type, wellKnownTypes) is not null)
            {
                return true;
            }
        }
 
        return false;
    }
 
    private static void AnalyzeExportNameUniqueness(
        SymbolAnalysisContext context,
        IMethodSymbol method,
        ImmutableArray<IParameterSymbol> effectiveParameters,
        string exportId,
        WellKnownTypes wellKnownTypes,
        Location location)
    {
        // Only applies to extension methods where the first param is IResourceBuilder<T>
        // with T being an open generic type parameter (constrained to IResource)
        var firstParamType = effectiveParameters[0].Type;
        if (!IsOpenGenericResourceBuilder(firstParamType, wellKnownTypes))
        {
            return;
        }
 
        // Look for a parameter (beyond the first) that is IResourceBuilder<ConcreteType>
        // where ConcreteType is a specific resource type (not a type parameter)
        string? concreteTargetTypeName = null;
        for (var i = 1; i < effectiveParameters.Length; i++)
        {
            concreteTargetTypeName = GetConcreteResourceBuilderTypeName(effectiveParameters[i].Type, wellKnownTypes);
            if (concreteTargetTypeName is not null)
            {
                break;
            }
        }
 
        if (concreteTargetTypeName is null)
        {
            return;
        }
 
        // Check if the export ID matches the method name (camelCase), suggesting it wasn't made unique
        var expectedDefault = char.ToLowerInvariant(method.Name[0]) + method.Name.Substring(1);
        if (!string.Equals(exportId, expectedDefault, StringComparison.Ordinal))
        {
            // Export name was explicitly customized, assume the author made it unique
            return;
        }
 
        // Strip the "Resource" suffix from the concrete type name to build a suggested unique name
        var shortName = concreteTargetTypeName;
        if (shortName.EndsWith("Resource", StringComparison.Ordinal))
        {
            shortName = shortName.Substring(0, shortName.Length - "Resource".Length);
        }
 
        // Remove common prefixes like "Azure" to keep the suggestion concise
        if (shortName.StartsWith("Azure", StringComparison.Ordinal))
        {
            shortName = shortName.Substring("Azure".Length);
        }
 
        var suggestedName = $"with{shortName}{method.Name.Substring(4)}"; // e.g., "withSearchRoleAssignments"
        if (method.Name.Length <= 4)
        {
            suggestedName = $"{exportId}{shortName}";
        }
 
        context.ReportDiagnostic(Diagnostic.Create(
            Diagnostics.s_exportNameShouldBeUnique,
            location,
            exportId,
            method.Name,
            concreteTargetTypeName,
            suggestedName));
    }
 
    /// <summary>
    /// Checks that callback parameter types (e.g., Action&lt;HelmChartOptions&gt;) in exported methods
    /// have [AspireExport] so their members are visible to TypeScript.
    /// </summary>
    private static void AnalyzeCallbackContextTypes(
        SymbolAnalysisContext context,
        IMethodSymbol method,
        INamedTypeSymbol aspireExportAttribute,
        Location location)
    {
        foreach (var parameter in method.Parameters)
        {
            var paramType = parameter.Type;
 
            // Unwrap nullable (Action<T>? → Action<T>)
            if (paramType is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } nullable)
            {
                paramType = nullable.TypeArguments[0];
            }
 
            // Check if this is a delegate type (Action<T>, Func<T, ...>, custom delegate)
            if (paramType is not INamedTypeSymbol namedType || !IsDelegateType(namedType))
            {
                continue;
            }
 
            // Extract the type arguments of the delegate (Action<T> → T, Func<T, R> → T)
            var invokeMethod = namedType.DelegateInvokeMethod;
            if (invokeMethod is null)
            {
                continue;
            }
 
            foreach (var delegateParam in invokeMethod.Parameters)
            {
                var contextType = delegateParam.Type;
                if (contextType is not INamedTypeSymbol contextNamedType)
                {
                    continue;
                }
 
                // Skip primitive types, string, well-known framework types
                if (contextNamedType.SpecialType != SpecialType.None ||
                    contextNamedType.TypeKind is TypeKind.Enum or TypeKind.Interface)
                {
                    continue;
                }
 
                // Skip types with no public instance members worth exporting
                var hasPublicMembers = contextNamedType.GetMembers()
                    .Any(m => m.DeclaredAccessibility == Accessibility.Public &&
                              !m.IsStatic &&
                              m is IMethodSymbol { MethodKind: MethodKind.Ordinary } or IPropertySymbol);
                if (!hasPublicMembers)
                {
                    continue;
                }
 
                // Check if the type has [AspireExport] or [AspireDto]
                var hasExport = false;
                foreach (var attr in contextNamedType.GetAttributes())
                {
                    if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, aspireExportAttribute))
                    {
                        hasExport = true;
                        break;
                    }
 
                    // [AspireDto] types are already exported for serialization
                    if (attr.AttributeClass?.Name == "AspireDtoAttribute")
                    {
                        hasExport = true;
                        break;
                    }
                }
 
                if (!hasExport)
                {
                    // Only warn for types defined in the same compilation (same assembly).
                    // We can't add attributes to types from external packages.
                    if (!SymbolEqualityComparer.Default.Equals(contextNamedType.ContainingAssembly, method.ContainingAssembly))
                    {
                        continue;
                    }
 
                    context.ReportDiagnostic(Diagnostic.Create(
                        Diagnostics.s_callbackContextTypeMissingExport,
                        location,
                        contextNamedType.Name,
                        method.Name));
                }
            }
        }
    }
 
    private static bool IsDelegateType(INamedTypeSymbol type)
    {
        return type.TypeKind == TypeKind.Delegate ||
               type.BaseType?.SpecialType == SpecialType.System_MulticastDelegate;
    }
 
    private static void AnalyzeInlineSynchronousDelegateInvocation(
        OperationAnalysisContext context,
        IAssemblySymbol assembly,
        IMethodSymbol method,
        IReadOnlyDictionary<string, IParameterSymbol> synchronousDelegateParameters,
        ConcurrentDictionary<string, byte> reportedParameters,
        ConcurrentDictionary<ISymbol, ImmutableHashSet<int>> inlineDelegateInvocationCache)
    {
        var invocation = (IInvocationOperation)context.Operation;
 
        if (invocation.Syntax is not InvocationExpressionSyntax invocationSyntax ||
            IsInsideNestedCallback(invocationSyntax))
        {
            return;
        }
 
        if (invocation.TargetMethod.MethodKind == MethodKind.DelegateInvoke)
        {
            var parameterName = GetInvokedDelegateParameterName(invocationSyntax);
            if (parameterName is null || !synchronousDelegateParameters.TryGetValue(parameterName, out var parameter))
            {
                return;
            }
 
            ReportInlineSynchronousDelegateInvocation(context, method, reportedParameters, invocationSyntax.GetLocation(), parameter);
            return;
        }
 
        foreach (var argument in invocation.Arguments)
        {
            if (argument.Parameter is null ||
                GetReferencedParameter(argument.Value) is not { } referencedParameter ||
                !synchronousDelegateParameters.TryGetValue(referencedParameter.Name, out var callbackParameter))
            {
                continue;
            }
 
            var targetInvokedParameters = GetInlineInvokedDelegateParameterOrdinals(
                invocation.TargetMethod,
                assembly,
                inlineDelegateInvocationCache,
                new HashSet<ISymbol>(SymbolEqualityComparer.Default),
                context.CancellationToken);
 
            if (!targetInvokedParameters.Contains(argument.Parameter.Ordinal))
            {
                continue;
            }
 
            ReportInlineSynchronousDelegateInvocation(context, method, reportedParameters, invocationSyntax.GetLocation(), callbackParameter);
        }
    }
 
    private static void ReportInlineSynchronousDelegateInvocation(
        OperationAnalysisContext context,
        IMethodSymbol method,
        ConcurrentDictionary<string, byte> reportedParameters,
        Location location,
        IParameterSymbol parameter)
    {
        if (reportedParameters.TryAdd(parameter.Name, default))
        {
            context.ReportDiagnostic(Diagnostic.Create(
                Diagnostics.s_exportedSyncDelegateInvokedInline,
                location,
                method.Name,
                parameter.Name));
        }
    }
 
    private static ImmutableHashSet<int> GetInlineInvokedDelegateParameterOrdinals(
        IMethodSymbol method,
        IAssemblySymbol assembly,
        ConcurrentDictionary<ISymbol, ImmutableHashSet<int>> cache,
        HashSet<ISymbol> visiting,
        CancellationToken cancellationToken)
    {
        if (cache.TryGetValue(method, out var cached))
        {
            return cached;
        }
 
        if (!SymbolEqualityComparer.Default.Equals(method.ContainingAssembly, assembly) ||
            method.DeclaringSyntaxReferences.Length == 0 ||
            !visiting.Add(method))
        {
            return ImmutableHashSet<int>.Empty;
        }
 
        try
        {
            var result = ComputeInlineInvokedDelegateParameterOrdinals(method, assembly, cache, visiting, cancellationToken);
            cache.TryAdd(method, result);
            return result;
        }
        finally
        {
            visiting.Remove(method);
        }
    }
 
    private static ImmutableHashSet<int> ComputeInlineInvokedDelegateParameterOrdinals(
        IMethodSymbol method,
        IAssemblySymbol assembly,
        ConcurrentDictionary<ISymbol, ImmutableHashSet<int>> cache,
        HashSet<ISymbol> visiting,
        CancellationToken cancellationToken)
    {
        var synchronousDelegateParameterOrdinals = method.Parameters
            .Where(IsSynchronousDelegateParameter)
            .Select(static p => p.Ordinal)
            .ToImmutableHashSet();
 
        if (synchronousDelegateParameterOrdinals.Count == 0)
        {
            return ImmutableHashSet<int>.Empty;
        }
 
        var syntax = method.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken);
        var result = ImmutableHashSet.CreateBuilder<int>();
 
        foreach (var invocationSyntax in syntax.DescendantNodes().OfType<InvocationExpressionSyntax>())
        {
            if (IsInsideNestedCallback(invocationSyntax))
            {
                continue;
            }
 
            var parameterName = GetInvokedDelegateParameterName(invocationSyntax);
            var parameter = parameterName is null
                ? null
                : method.Parameters.FirstOrDefault(p => p.Name == parameterName);
            if (parameter is not null && synchronousDelegateParameterOrdinals.Contains(parameter.Ordinal))
            {
                result.Add(parameter.Ordinal);
                continue;
            }
 
            foreach (var targetMethod in ResolveSameTypeMethodInvocations(method, invocationSyntax))
            {
                foreach (var argument in invocationSyntax.ArgumentList.Arguments)
                {
                    if (GetReferencedParameterName(argument.Expression) is not { } referencedParameterName)
                    {
                        continue;
                    }
 
                    var referencedParameter = method.Parameters.FirstOrDefault(p => p.Name == referencedParameterName);
                    var targetParameterOrdinal = GetTargetParameterOrdinal(targetMethod, argument, invocationSyntax.ArgumentList);
 
                    if (referencedParameter is null ||
                        targetParameterOrdinal is null ||
                        !synchronousDelegateParameterOrdinals.Contains(referencedParameter.Ordinal))
                    {
                        continue;
                    }
 
                    var targetInvokedParameters = GetInlineInvokedDelegateParameterOrdinals(
                        targetMethod,
                        assembly,
                        cache,
                        visiting,
                        cancellationToken);
 
                    if (targetInvokedParameters.Contains(targetParameterOrdinal.Value))
                    {
                        result.Add(referencedParameter.Ordinal);
                    }
                }
            }
        }
 
        return result.ToImmutable();
    }
 
    private static IParameterSymbol? GetReferencedParameter(IOperation operation)
    {
        while (operation is IConversionOperation conversion)
        {
            operation = conversion.Operand;
        }
 
        return operation is IParameterReferenceOperation parameterReference
            ? parameterReference.Parameter
            : null;
    }
 
    private static IEnumerable<IMethodSymbol> ResolveSameTypeMethodInvocations(IMethodSymbol containingMethod, InvocationExpressionSyntax invocation)
    {
        // Analyzer rules prohibit fetching a SemanticModel for arbitrary helper syntax here, so helper
        // summaries use the exact operation symbol at the export boundary and bounded syntax matching
        // for subsequent calls inside the same helper type.
        var methodName = invocation.Expression switch
        {
            IdentifierNameSyntax identifier => identifier.Identifier.ValueText,
            MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.ValueText,
            _ => null
        };
 
        if (methodName is null || containingMethod.ContainingType is null)
        {
            yield break;
        }
 
        foreach (var member in containingMethod.ContainingType.GetMembers(methodName).OfType<IMethodSymbol>())
        {
            if (member.DeclaringSyntaxReferences.Length == 0 ||
                !CouldAcceptArguments(member, invocation.ArgumentList))
            {
                continue;
            }
 
            yield return member;
        }
    }
 
    private static bool CouldAcceptArguments(IMethodSymbol targetMethod, ArgumentListSyntax argumentList)
    {
        var positionalArgumentCount = 0;
        foreach (var argument in argumentList.Arguments)
        {
            if (argument.NameColon is not null)
            {
                if (!targetMethod.Parameters.Any(p => p.Name == argument.NameColon.Name.Identifier.ValueText))
                {
                    return false;
                }
 
                continue;
            }
 
            positionalArgumentCount++;
        }
 
        if (positionalArgumentCount > targetMethod.Parameters.Length)
        {
            return false;
        }
 
        var suppliedNames = new HashSet<string>(argumentList.Arguments
            .Where(static a => a.NameColon is not null)
            .Select(static a => a.NameColon!.Name.Identifier.ValueText),
            StringComparer.Ordinal);
 
        return targetMethod.Parameters
            .Where(static p => !p.IsOptional)
            .All(p => p.Ordinal < positionalArgumentCount || suppliedNames.Contains(p.Name));
    }
 
    private static int? GetTargetParameterOrdinal(IMethodSymbol targetMethod, ArgumentSyntax argument, ArgumentListSyntax argumentList)
    {
        if (argument.NameColon is not null)
        {
            var parameter = targetMethod.Parameters.FirstOrDefault(p => p.Name == argument.NameColon.Name.Identifier.ValueText);
            return parameter?.Ordinal;
        }
 
        var ordinal = argumentList.Arguments.IndexOf(argument);
        return ordinal >= 0 && ordinal < targetMethod.Parameters.Length
            ? ordinal
            : null;
    }
 
    private static string? GetReferencedParameterName(ExpressionSyntax expression)
    {
        while (expression is ParenthesizedExpressionSyntax parenthesized)
        {
            expression = parenthesized.Expression;
        }
 
        return expression is IdentifierNameSyntax identifier
            ? identifier.Identifier.ValueText
            : null;
    }
 
    private static bool IsInsideNestedCallback(InvocationExpressionSyntax invocation)
    {
        foreach (var ancestor in invocation.Ancestors())
        {
            switch (ancestor)
            {
                case AnonymousFunctionExpressionSyntax anonymousFunction:
                    return !IsImmediatelyInvokedAnonymousFunction(anonymousFunction);
                case LocalFunctionStatementSyntax localFunction:
                    return !IsImmediatelyInvokedLocalFunction(localFunction);
            }
        }
 
        return false;
    }
 
    private static bool IsImmediatelyInvokedAnonymousFunction(AnonymousFunctionExpressionSyntax anonymousFunction)
    {
        SyntaxNode current = anonymousFunction;
 
        while (current.Parent is ParenthesizedExpressionSyntax or CastExpressionSyntax)
        {
            current = current.Parent;
        }
 
        return current.Parent is InvocationExpressionSyntax invocation &&
            invocation.Expression == current;
    }
 
    private static bool IsImmediatelyInvokedLocalFunction(LocalFunctionStatementSyntax localFunction)
    {
        if (localFunction.Parent is null)
        {
            return false;
        }
 
        var localFunctionName = localFunction.Identifier.ValueText;
 
        foreach (var invocation in localFunction.Parent.DescendantNodes().OfType<InvocationExpressionSyntax>())
        {
            if (localFunction.Span.Contains(invocation.Span))
            {
                continue;
            }
 
            if (invocation.Expression is IdentifierNameSyntax identifier &&
                identifier.Identifier.ValueText == localFunctionName)
            {
                return true;
            }
        }
 
        return false;
    }
 
    private static string? GetInvokedDelegateParameterName(InvocationExpressionSyntax invocation)
    {
        return invocation.Expression switch
        {
            IdentifierNameSyntax identifier => identifier.Identifier.ValueText,
            MemberAccessExpressionSyntax
            {
                Name.Identifier.ValueText: "Invoke",
                Expression: IdentifierNameSyntax identifier
            } => identifier.Identifier.ValueText,
            MemberBindingExpressionSyntax
            {
                Name.Identifier.ValueText: "Invoke"
            } when invocation.Parent is ConditionalAccessExpressionSyntax
            {
                Expression: IdentifierNameSyntax identifier
            } => identifier.Identifier.ValueText,
            _ => null
        };
    }
 
    private static bool IsSynchronousDelegateParameter(IParameterSymbol parameter)
    {
        if (parameter.Type is not INamedTypeSymbol namedType || !IsDelegateType(namedType))
        {
            return false;
        }
 
        var invokeMethod = namedType.DelegateInvokeMethod;
        if (invokeMethod is null)
        {
            return false;
        }
 
        return !IsTaskReturnType(invokeMethod.ReturnType);
    }
 
    private static bool IsTaskReturnType(ITypeSymbol type)
    {
        return type is INamedTypeSymbol namedType
            && namedType.Name == "Task"
            && namedType.ContainingNamespace.ToDisplayString() == "System.Threading.Tasks";
    }
 
    /// <summary>
    /// Checks if the type is IResourceBuilder&lt;T&gt; where T is a type parameter (open generic).
    /// </summary>
    private static bool IsOpenGenericResourceBuilder(ITypeSymbol type, WellKnownTypes wellKnownTypes)
    {
        if (type is not INamedTypeSymbol namedType || !namedType.IsGenericType)
        {
            return false;
        }
 
        try
        {
            var iResourceBuilderType = wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Aspire_Hosting_ApplicationModel_IResourceBuilder_1);
            if (!SymbolEqualityComparer.Default.Equals(namedType.OriginalDefinition, iResourceBuilderType))
            {
                return false;
            }
 
            // Check that the type argument is a type parameter (open generic), not a concrete type
            return namedType.TypeArguments.Length == 1 && namedType.TypeArguments[0] is ITypeParameterSymbol;
        }
        catch (InvalidOperationException)
        {
            return false;
        }
    }
 
    private static string GetGeneratedTargetTypeName(IMethodSymbol method)
    {
        var effectiveParameters = GetEffectiveExportParameters(method);
        if (!IsExtensionLike(method) || effectiveParameters.Length == 0)
        {
            return "<global>";
        }
 
        var targetType = effectiveParameters[0].Type;
        var targetTypeName = targetType.ToDisplayString();
        if (targetType is not INamedTypeSymbol { IsGenericType: true } namedTargetType)
        {
            return targetTypeName;
        }
 
        var typeParameterConstraints = namedTargetType.TypeArguments
            .OfType<ITypeParameterSymbol>()
            .Select(typeParameter =>
            {
                var constraints = string.Join("&", typeParameter.ConstraintTypes.Select(static t => t.ToDisplayString()));
                return constraints.Length > 0
                    ? $"{typeParameter.Name}:{constraints}"
                    : typeParameter.Name;
            })
            .ToArray();
 
        if (typeParameterConstraints.Length == 0)
        {
            return targetTypeName;
        }
 
        return $"{targetTypeName} where {string.Join(",", typeParameterConstraints)}";
    }
 
    private static string GetGeneratedTargetTypeName(INamedTypeSymbol resourceType)
    {
        return $"Aspire.Hosting.ApplicationModel.IResourceBuilder<{resourceType.ToDisplayString()}>";
    }
 
    /// <summary>
    /// If the type is IResourceBuilder&lt;ConcreteType&gt; (not open generic), returns the ConcreteType name; otherwise null.
    /// </summary>
    private static string? GetConcreteResourceBuilderTypeName(ITypeSymbol type, WellKnownTypes wellKnownTypes)
    {
        if (type is not INamedTypeSymbol namedType || !namedType.IsGenericType)
        {
            return null;
        }
 
        try
        {
            var iResourceBuilderType = wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Aspire_Hosting_ApplicationModel_IResourceBuilder_1);
            if (!SymbolEqualityComparer.Default.Equals(namedType.OriginalDefinition, iResourceBuilderType))
            {
                return null;
            }
 
            // Check that the type argument is a concrete resource type, not a type parameter or interface.
            if (namedType.TypeArguments.Length == 1 &&
                namedType.TypeArguments[0] is not ITypeParameterSymbol &&
                namedType.TypeArguments[0].TypeKind is not TypeKind.Interface)
            {
                return namedType.TypeArguments[0].Name;
            }
        }
        catch (InvalidOperationException)
        {
            // Type not found
        }
 
        return null;
    }
 
    private static bool IsBuilderType(ITypeSymbol type, WellKnownTypes wellKnownTypes)
    {
        // Check IDistributedApplicationBuilder
        try
        {
            var distributedAppBuilder = wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Aspire_Hosting_IDistributedApplicationBuilder);
            if (SymbolEqualityComparer.Default.Equals(type, distributedAppBuilder) ||
                WellKnownTypes.Implements(type, distributedAppBuilder))
            {
                return true;
            }
        }
        catch (InvalidOperationException)
        {
            // Type not found
        }
 
        // Check IResourceBuilder<T>
        if (IsResourceBuilderType(type, wellKnownTypes))
        {
            return true;
        }
 
        return false;
    }
 
    private static bool RequiresExplicitExportCoverage(
        ITypeSymbol type,
        WellKnownTypes wellKnownTypes,
        INamedTypeSymbol aspireExportAttribute,
        HashSet<ITypeSymbol> currentAssemblyExportedTypes)
    {
        return IsBuilderType(type, wellKnownTypes) ||
               IsResourceType(type, wellKnownTypes) ||
               HasAspireExportAttribute(type, aspireExportAttribute, currentAssemblyExportedTypes);
    }
 
    private static string? GetIncompatibilityReason(
        IMethodSymbol method,
        WellKnownTypes wellKnownTypes,
        INamedTypeSymbol aspireExportAttribute,
        HashSet<ITypeSymbol> currentAssemblyExportedTypes)
    {
        var reasons = new List<string>();
 
        // Check for out parameters
        foreach (var param in method.Parameters)
        {
            if (param.RefKind == RefKind.Out)
            {
                reasons.Add($"'out' parameter '{param.Name}' is not ATS-compatible");
            }
        }
 
        // Check for open generic type parameters on the method itself (not constrained to IResource)
        if (method.TypeParameters.Length > 0)
        {
            foreach (var tp in method.TypeParameters)
            {
                var hasResourceConstraint = false;
                foreach (var constraint in tp.ConstraintTypes)
                {
                    if (IsResourceType(constraint, wellKnownTypes) || IsResourceBuilderType(constraint, wellKnownTypes))
                    {
                        hasResourceConstraint = true;
                        break;
                    }
                }
                if (!hasResourceConstraint)
                {
                    reasons.Add($"open generic type parameter '{tp.Name}' is not ATS-compatible");
                }
            }
        }
 
        // Check parameters (skip 'this' first parameter)
        for (var i = 1; i < method.Parameters.Length; i++)
        {
            var param = method.Parameters[i];
            var paramType = param.Type;
 
            // Skip params arrays if element type is compatible
            if (param.IsParams && paramType is IArrayTypeSymbol paramsArray)
            {
                if (!IsAtsCompatibleValueType(paramsArray.ElementType, wellKnownTypes, aspireExportAttribute))
                {
                    reasons.Add($"parameter '{param.Name}' uses '{paramsArray.ElementType.ToDisplayString()}[]' which is not ATS-compatible");
                }
                continue;
            }
 
            // Check delegate types more carefully
            if (IsDelegateType(paramType))
            {
                var reason = GetDelegateIncompatibilityReason(param, paramType, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes);
                if (reason is not null)
                {
                    reasons.Add(reason);
                }
                continue;
            }
 
            if (!IsAtsCompatibleValueType(paramType, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes))
            {
                reasons.Add($"parameter '{param.Name}' of type '{paramType.ToDisplayString()}' is not ATS-compatible");
            }
        }
 
        // Check return type
        if (!IsAtsCompatibleType(method.ReturnType, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes))
        {
            reasons.Add($"return type '{method.ReturnType.ToDisplayString()}' is not ATS-compatible");
        }
 
        if (reasons.Count == 0)
        {
            return null;
        }
 
        return string.Join("; ", reasons) + ".";
    }
 
    private static string? GetDelegateIncompatibilityReason(
        IParameterSymbol param,
        ITypeSymbol delegateType,
        WellKnownTypes wellKnownTypes,
        INamedTypeSymbol aspireExportAttribute,
        HashSet<ITypeSymbol> currentAssemblyExportedTypes)
    {
        if (delegateType is not INamedTypeSymbol namedDelegate)
        {
            return $"parameter '{param.Name}' uses delegate type which is not ATS-compatible";
        }
 
        // Find the Invoke method to get delegate signature
        var invokeMethod = namedDelegate.DelegateInvokeMethod;
        if (invokeMethod is null)
        {
            return null;
        }
 
        // Check delegate parameter types for known incompatible patterns
        foreach (var delegateParam in invokeMethod.Parameters)
        {
            var dpType = delegateParam.Type;
            if (!IsAtsCompatibleValueType(dpType, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes))
            {
                return $"parameter '{param.Name}' uses delegate with '{dpType.Name}' which is not ATS-compatible";
            }
 
            // Check for IResource as a raw parameter (not wrapped in IResourceBuilder<T>)
            if (IsRawResourceInterface(dpType, wellKnownTypes))
            {
                return $"parameter '{param.Name}' uses delegate with raw '{dpType.Name}' interface which is not ATS-compatible";
            }
        }
 
        return null;
    }
 
    private static bool IsRawResourceInterface(ITypeSymbol type, WellKnownTypes wellKnownTypes)
    {
        try
        {
            var iResourceType = wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Aspire_Hosting_ApplicationModel_IResource);
            return SymbolEqualityComparer.Default.Equals(type, iResourceType);
        }
        catch (InvalidOperationException)
        {
            return false;
        }
    }
 
    private static void AnalyzeUnionAttribute(
        SymbolAnalysisContext context,
        ImmutableArray<AttributeData> attributes,
        INamedTypeSymbol aspireUnionAttribute,
        WellKnownTypes wellKnownTypes,
        INamedTypeSymbol aspireExportAttribute,
        HashSet<ITypeSymbol> currentAssemblyExportedTypes)
    {
        foreach (var attr in attributes)
        {
            if (!SymbolEqualityComparer.Default.Equals(attr.AttributeClass, aspireUnionAttribute))
            {
                continue;
            }
 
            var attrSyntax = attr.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken);
            var attrLocation = attrSyntax?.GetLocation() ?? Location.None;
 
            // Get the types from the constructor argument (params Type[] types)
            if (attr.ConstructorArguments.Length == 0)
            {
                // No arguments - report ASPIREEXPORT005
                context.ReportDiagnostic(Diagnostic.Create(
                    Diagnostics.s_unionRequiresAtLeastTwoTypes,
                    attrLocation,
                    0));
                continue;
            }
 
            var typesArg = attr.ConstructorArguments[0];
            if (typesArg.Kind != TypedConstantKind.Array)
            {
                continue;
            }
 
            var types = typesArg.Values;
 
            // ASPIREEXPORT005: Check that we have at least 2 types
            if (types.Length < 2)
            {
                context.ReportDiagnostic(Diagnostic.Create(
                    Diagnostics.s_unionRequiresAtLeastTwoTypes,
                    attrLocation,
                    types.Length));
            }
 
            // ASPIREEXPORT006: Check that each type is ATS-compatible
            foreach (var typeConstant in types)
            {
                if (typeConstant.Value is INamedTypeSymbol typeSymbol)
                {
                    if (!IsAtsCompatibleValueType(typeSymbol, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes))
                    {
                        context.ReportDiagnostic(Diagnostic.Create(
                            Diagnostics.s_unionTypeMustBeAtsCompatible,
                            attrLocation,
                            typeSymbol.ToDisplayString()));
                    }
                }
            }
        }
    }
 
    private static void ReportDuplicateExports(
        CompilationAnalysisContext context,
        ConcurrentDictionary<(string ExportId, string TargetType), ConcurrentBag<(IMethodSymbol Method, Location Location)>> exportsByKey)
    {
        foreach (var kvp in exportsByKey)
        {
            var methods = kvp.Value.ToArray();
            if (methods.Length > 1)
            {
                // Report on all methods that share the same export ID and target type
                foreach (var (_, location) in methods)
                {
                    context.ReportDiagnostic(Diagnostic.Create(
                        Diagnostics.s_duplicateExportId,
                        location,
                        kvp.Key.ExportId,
                        kvp.Key.TargetType));
                }
            }
        }
    }
 
    private static void ReportDuplicateCapabilityIds(
        CompilationAnalysisContext context,
        ConcurrentDictionary<string, ConcurrentBag<CapabilityExport>> capabilityIds)
    {
        foreach (var kvp in capabilityIds.OrderBy(kvp => kvp.Key, StringComparer.Ordinal))
        {
            var exports = kvp.Value
                .Distinct()
                .OrderBy(static e => e.Location.SourceSpan.Start)
                .ThenBy(static e => e.Source, StringComparer.Ordinal)
                .ToArray();
 
            if (exports.Length <= 1)
            {
                continue;
            }
 
            var sources = string.Join(", ", exports.Select(static e => e.Source).Distinct(StringComparer.Ordinal));
            foreach (var export in exports)
            {
                context.ReportDiagnostic(Diagnostic.Create(
                    Diagnostics.s_duplicatePolyglotCapabilityId,
                    export.Location,
                    kvp.Key,
                    sources));
            }
        }
    }
 
    private static void ReportDuplicateGeneratedMethodNames(
        CompilationAnalysisContext context,
        ConcurrentDictionary<(string MethodName, string TargetType), ConcurrentBag<GeneratedMethodNameExport>> generatedMethodNames)
    {
        foreach (var kvp in generatedMethodNames.OrderBy(kvp => kvp.Key.TargetType, StringComparer.Ordinal).ThenBy(kvp => kvp.Key.MethodName, StringComparer.Ordinal))
        {
            var exports = kvp.Value
                .Distinct()
                .OrderBy(static e => e.Location.SourceSpan.Start)
                .ThenBy(static e => e.Source, StringComparer.Ordinal)
                .ToArray();
 
            if (exports.Length <= 1 ||
                exports.Select(static e => e.EffectiveExportId).Distinct(StringComparer.Ordinal).Count() <= 1)
            {
                continue;
            }
 
            var sources = string.Join(", ", exports.Select(static e => e.Source).Distinct(StringComparer.Ordinal));
            foreach (var export in exports)
            {
                context.ReportDiagnostic(Diagnostic.Create(
                    Diagnostics.s_duplicateGeneratedMethodName,
                    export.Location,
                    kvp.Key.MethodName,
                    kvp.Key.TargetType,
                    sources));
            }
        }
    }
 
    private static void AddCapabilityExport(
        ConcurrentDictionary<string, ConcurrentBag<CapabilityExport>> capabilityIds,
        string capabilityId,
        string source,
        Location location)
    {
        var bag = capabilityIds.GetOrAdd(capabilityId, _ => []);
        bag.Add(new CapabilityExport(source, location));
    }
 
    private static void AddGeneratedMethodNameExport(
        ConcurrentDictionary<(string MethodName, string TargetType), ConcurrentBag<GeneratedMethodNameExport>> generatedMethodNames,
        string methodName,
        string targetType,
        string effectiveExportId,
        string source,
        Location location)
    {
        var bag = generatedMethodNames.GetOrAdd((methodName, targetType), _ => []);
        bag.Add(new GeneratedMethodNameExport(source, location, effectiveExportId));
    }
 
    private static string? GetExportId(AttributeData attribute)
    {
        if (attribute.ConstructorArguments.Length > 0 &&
            attribute.ConstructorArguments[0].Value is string id)
        {
            return id;
        }
        return null;
    }
 
    private static string? GetDerivedExportId(IMethodSymbol method, AttributeData? containingTypeExportAttribute)
    {
        if (string.IsNullOrEmpty(method.Name))
        {
            return null;
        }
 
        var camelCaseName = char.ToLowerInvariant(method.Name[0]) + method.Name.Substring(1);
 
        // Non-static methods auto-exposed via ExposeMethods=true use TypeName.methodName to avoid collisions
        if (!method.IsStatic && IsExposeMethodsEnabled(containingTypeExportAttribute))
        {
            return $"{GetRuntimeTypeName(method.ContainingType)}.{camelCaseName}";
        }
 
        return camelCaseName;
    }
 
    private static string GetRuntimeTypeName(INamedTypeSymbol type)
    {
        return type.MetadataName;
    }
 
    private static bool IsSpecialRuntimeMethod(IMethodSymbol method)
    {
        return method.MethodKind != MethodKind.Ordinary ||
            method.Name is "GetType" or "ToString" or "Equals" or "GetHashCode";
    }
 
    private static string ToCamelCase(string name)
    {
        return string.IsNullOrEmpty(name)
            ? name
            : char.ToLowerInvariant(name[0]) + name.Substring(1);
    }
 
    private static bool IsExposeMethodsEnabled(AttributeData? exportAttribute)
    {
        return IsBooleanNamedArgumentEnabled(exportAttribute, ExposeMethodsPropertyName);
    }
 
    private static bool IsBooleanNamedArgumentEnabled(AttributeData? exportAttribute, string argumentName)
    {
        if (exportAttribute is null)
        {
            return false;
        }
 
        foreach (var namedArgument in exportAttribute.NamedArguments)
        {
            if (namedArgument.Key == argumentName &&
                namedArgument.Value.Value is bool enabled)
            {
                return enabled;
            }
        }
 
        return false;
    }
 
    private static string? GetNamedStringArgument(AttributeData? exportAttribute, string argumentName)
    {
        if (exportAttribute is null)
        {
            return null;
        }
 
        foreach (var namedArgument in exportAttribute.NamedArguments)
        {
            if (namedArgument.Key == argumentName &&
                namedArgument.Value.Value is string value)
            {
                return value;
            }
        }
 
        return null;
    }
 
    private static void AnalyzeExportDescription(SymbolAnalysisContext context, AttributeData exportAttribute, Location location)
    {
        if (HasNamedArgument(exportAttribute, DescriptionPropertyName))
        {
            context.ReportDiagnostic(Diagnostic.Create(Diagnostics.s_descriptionShouldUseXmlDocs, location));
        }
    }
 
    private static bool HasNamedArgument(AttributeData exportAttribute, string argumentName)
    {
        foreach (var namedArgument in exportAttribute.NamedArguments)
        {
            if (namedArgument.Key == argumentName)
            {
                return true;
            }
        }
 
        return false;
    }
 
    private static AttributeData? GetAspireExportAttribute(ISymbol symbol, INamedTypeSymbol aspireExportAttribute)
    {
        foreach (var attr in symbol.GetAttributes())
        {
            if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, aspireExportAttribute))
            {
                return attr;
            }
        }
 
        return null;
    }
 
    private static bool HasAspireExportIgnoreAttribute(ISymbol symbol, INamedTypeSymbol? aspireExportIgnoreAttribute)
    {
        if (aspireExportIgnoreAttribute is null)
        {
            return false;
        }
 
        foreach (var attr in symbol.GetAttributes())
        {
            if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, aspireExportIgnoreAttribute))
            {
                return true;
            }
        }
 
        return false;
    }
 
    private static bool ShouldExportMember(bool isPublic, bool exposeAll, AttributeData? exportAttribute)
    {
        return exportAttribute is not null || (exposeAll && isPublic);
    }
 
    private static string GetCapabilityPackage(INamedTypeSymbol type, string assemblyName)
    {
        return type.ContainingNamespace.IsGlobalNamespace
            ? assemblyName
            : type.ContainingNamespace.ToDisplayString();
    }
 
    // C# 14 introduced "extension blocks" (extension members). A method declared inside an
    // `extension(receiver) { ... }` block is surfaced by Roslyn as a non-static instance member whose
    // ContainingType has IsExtension == true and exposes the receiver via ExtensionParameter. These
    // members are semantically equivalent to classic static `this`-parameter extension methods (the
    // compiler lowers them to exactly that in IL), so [AspireExport] is valid on them and
    // ASPIREEXPORT001 ("must be static") must not fire.
    //
    // This analyzer compiles against Roslyn 4.8, which predates extension blocks, so
    // INamedTypeSymbol.IsExtension / INamedTypeSymbol.ExtensionParameter are not available at compile
    // time. They DO exist at runtime when the analyzer is loaded into a newer Roslyn host
    // (.NET 10 SDK / VS 17.14+) that is able to compile this syntax in the first place. We therefore
    // probe for them via reflection and treat their absence as "not an extension" (older hosts can
    // never produce extension-block members).
    // See: https://learn.microsoft.com/dotnet/csharp/whats-new/csharp-14#extension-members
    //
    // The cached members are MethodInfo getters (not delegates closing over INamedTypeSymbol) so the
    // analyzer does not store per-compilation symbol data in its fields (RS1008).
    private static readonly MethodInfo? s_isExtensionGetter = typeof(INamedTypeSymbol).GetProperty("IsExtension")?.GetGetMethod();
    private static readonly MethodInfo? s_extensionParameterGetter = typeof(INamedTypeSymbol).GetProperty("ExtensionParameter")?.GetGetMethod();
 
    private static bool IsExtensionContainer(INamedTypeSymbol type)
    {
        if (s_isExtensionGetter is null)
        {
            return false;
        }
 
        try
        {
            return s_isExtensionGetter.Invoke(type, null) is true;
        }
        catch (TargetInvocationException)
        {
            return false;
        }
    }
 
    private static IParameterSymbol? GetExtensionParameter(INamedTypeSymbol type)
    {
        if (s_extensionParameterGetter is null)
        {
            return null;
        }
 
        try
        {
            return s_extensionParameterGetter.Invoke(type, null) as IParameterSymbol;
        }
        catch (TargetInvocationException)
        {
            return null;
        }
    }
 
    /// <summary>
    /// Returns true when the method is an instance member declared inside a C# 14 extension block
    /// (e.g. <c>extension(IResourceBuilder&lt;T&gt; builder) { public ... Method(...) { } }</c>).
    /// Such members are equivalent to classic static extension methods and are valid AspireExport targets.
    /// </summary>
    private static bool IsExtensionBlockMember(IMethodSymbol method)
    {
        return !method.IsStatic &&
            method.ContainingType is { } containingType &&
            IsExtensionContainer(containingType) &&
            GetExtensionParameter(containingType) is not null;
    }
 
    /// <summary>
    /// True when the method participates in ATS as an extension-style export: either a classic static
    /// <c>this</c>-parameter extension method, or a C# 14 extension block instance member.
    /// </summary>
    private static bool IsExtensionLike(IMethodSymbol method)
    {
        return method.IsExtensionMethod || IsExtensionBlockMember(method);
    }
 
    /// <summary>
    /// True when the method is treated as a static export for capability/generated-name tracking.
    /// Extension block instance members are static-equivalent (lowered to static extension methods in IL).
    /// </summary>
    private static bool IsStaticForExport(IMethodSymbol method)
    {
        return method.IsStatic || IsExtensionBlockMember(method);
    }
 
    /// <summary>
    /// Returns the parameter list used by the receiver/target-type export rules. For classic extension
    /// methods and ordinary methods this is <see cref="IMethodSymbol.Parameters"/> (the receiver, if any,
    /// is the first parameter). For C# 14 extension block instance members the receiver lives on the
    /// extension container rather than in <c>Parameters</c>, so it is prepended to produce the same
    /// <c>[receiver, ...declaredParameters]</c> shape as a classic extension method.
    /// </summary>
    private static ImmutableArray<IParameterSymbol> GetEffectiveExportParameters(IMethodSymbol method)
    {
        if (GetExtensionBlockReceiver(method) is { } receiver)
        {
            return method.Parameters.Insert(0, receiver);
        }
 
        return method.Parameters;
    }
 
    private static IParameterSymbol? GetExtensionBlockReceiver(IMethodSymbol method)
    {
        if (method.IsStatic || method.ContainingType is not { } containingType || !IsExtensionContainer(containingType))
        {
            return null;
        }
 
        return GetExtensionParameter(containingType);
    }
 
    private static string GetMethodDisplayString(IMethodSymbol method)
    {
        return method.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
    }
 
    private static Location? GetAttributeLocation(AttributeData? attribute, CancellationToken cancellationToken)
    {
        return attribute?.ApplicationSyntaxReference?.GetSyntax(cancellationToken).GetLocation();
    }
 
    private static bool TryGetEffectiveAspireExportAttribute(IMethodSymbol method, INamedTypeSymbol aspireExportAttribute, out AttributeData? exportAttribute, out AttributeData? containingTypeExportAttribute)
    {
        foreach (var attr in method.GetAttributes())
        {
            if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, aspireExportAttribute))
            {
                exportAttribute = attr;
                containingTypeExportAttribute = GetContainingTypeAspireExportAttribute(method.ContainingType, aspireExportAttribute);
                return true;
            }
        }
 
        containingTypeExportAttribute = GetContainingTypeAspireExportAttribute(method.ContainingType, aspireExportAttribute);
        if (containingTypeExportAttribute is not null)
        {
            exportAttribute = containingTypeExportAttribute;
            return true;
        }
 
        exportAttribute = null;
        containingTypeExportAttribute = null;
        return false;
    }
 
    private static AttributeData? GetContainingTypeAspireExportAttribute(INamedTypeSymbol? type, INamedTypeSymbol aspireExportAttribute)
    {
        if (type is null)
        {
            return null;
        }
 
        foreach (var attr in type.GetAttributes())
        {
            if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, aspireExportAttribute))
            {
                return attr;
            }
        }
 
        return null;
    }
 
    private static bool IsRunSyncOnBackgroundThreadEnabled(AttributeData? exportAttribute)
    {
        if (exportAttribute is null)
        {
            return false;
        }
 
        foreach (var namedArgument in exportAttribute.NamedArguments)
        {
            if (namedArgument.Key == RunSyncOnBackgroundThreadPropertyName &&
                namedArgument.Value.Value is bool enabled)
            {
                return enabled;
            }
        }
 
        return false;
    }
 
    private static bool IsAtsCompatibleType(
        ITypeSymbol type,
        WellKnownTypes wellKnownTypes,
        INamedTypeSymbol aspireExportAttribute,
        HashSet<ITypeSymbol> currentAssemblyExportedTypes)
    {
        // void is allowed
        if (type.SpecialType == SpecialType.System_Void)
        {
            return true;
        }
 
        // Task, Task<T>, ValueTask, and ValueTask<T> are allowed (for async methods)
        if (IsAsyncResultType(type, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes))
        {
            return true;
        }
 
        return IsAtsCompatibleValueType(type, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes);
    }
 
    private static bool IsAsyncResultType(
        ITypeSymbol type,
        WellKnownTypes wellKnownTypes,
        INamedTypeSymbol aspireExportAttribute,
        HashSet<ITypeSymbol> currentAssemblyExportedTypes)
    {
        // Check for Task / ValueTask
        try
        {
            var taskType = wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Threading_Tasks_Task);
            if (SymbolEqualityComparer.Default.Equals(type, taskType))
            {
                return true;
            }
        }
        catch (InvalidOperationException)
        {
            // Type not found
        }
 
        try
        {
            var valueTaskType = wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Threading_Tasks_ValueTask);
            if (SymbolEqualityComparer.Default.Equals(type, valueTaskType))
            {
                return true;
            }
        }
        catch (InvalidOperationException)
        {
            // Type not found
        }
 
        // Check for Task<T> / ValueTask<T>
        if (type is INamedTypeSymbol namedType && namedType.IsGenericType)
        {
            try
            {
                var taskOfTType = wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Threading_Tasks_Task_1);
                if (SymbolEqualityComparer.Default.Equals(namedType.OriginalDefinition, taskOfTType))
                {
                    // Validate the T in Task<T> is also ATS-compatible
                    return namedType.TypeArguments.Length == 1 &&
                           IsAtsCompatibleValueType(namedType.TypeArguments[0], wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes);
                }
            }
            catch (InvalidOperationException)
            {
                // Type not found
            }
 
            try
            {
                var valueTaskOfTType = wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Threading_Tasks_ValueTask_1);
                if (SymbolEqualityComparer.Default.Equals(namedType.OriginalDefinition, valueTaskOfTType))
                {
                    return namedType.TypeArguments.Length == 1 &&
                           IsAtsCompatibleValueType(namedType.TypeArguments[0], wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes);
                }
            }
            catch (InvalidOperationException)
            {
                // Type not found
            }
        }
 
        return false;
    }
 
    private static bool IsAtsCompatibleValueType(
        ITypeSymbol type,
        WellKnownTypes wellKnownTypes,
        INamedTypeSymbol? aspireExportAttribute = null,
        HashSet<ITypeSymbol>? currentAssemblyExportedTypes = null)
    {
        // Handle nullable types
        if (type is INamedTypeSymbol namedType &&
            namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T &&
            namedType.TypeArguments.Length == 1)
        {
            type = namedType.TypeArguments[0];
        }
 
        // Simple/primitive types (includes System.Object)
        if (IsSimpleType(type, wellKnownTypes))
        {
            return true;
        }
 
        // Enums
        if (type.TypeKind == TypeKind.Enum)
        {
            return true;
        }
 
        // Arrays of ATS-compatible types
        if (type is IArrayTypeSymbol arrayType)
        {
            return IsAtsCompatibleValueType(arrayType.ElementType, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes);
        }
 
        // Collection types (Dictionary, List, IReadOnlyList, etc.)
        if (IsAtsCompatibleCollectionType(type, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes))
        {
            return true;
        }
 
        // IResource types
        if (IsResourceType(type, wellKnownTypes))
        {
            return true;
        }
 
        // IResourceBuilder<T> types
        if (IsResourceBuilderType(type, wellKnownTypes))
        {
            return true;
        }
 
        // Types with [AspireExport] or [AspireDto] attribute
        if (aspireExportAttribute != null && HasAspireExportAttribute(type, aspireExportAttribute, currentAssemblyExportedTypes))
        {
            return true;
        }
 
        // Types with [AspireDto] attribute
        if (HasAspireDtoAttribute(type))
        {
            return true;
        }
 
        return false;
    }
 
    private static bool IsSimpleType(ITypeSymbol type, WellKnownTypes wellKnownTypes)
    {
        // Primitives via SpecialType
        if (type.SpecialType switch
        {
            SpecialType.System_Boolean => true,
            SpecialType.System_Byte => true,
            SpecialType.System_SByte => true,
            SpecialType.System_Int16 => true,
            SpecialType.System_UInt16 => true,
            SpecialType.System_Int32 => true,
            SpecialType.System_UInt32 => true,
            SpecialType.System_Int64 => true,
            SpecialType.System_UInt64 => true,
            SpecialType.System_Single => true,
            SpecialType.System_Double => true,
            SpecialType.System_Decimal => true,
            SpecialType.System_Char => true,
            SpecialType.System_String => true,
            SpecialType.System_DateTime => true,
            SpecialType.System_Object => true, // Maps to 'any' in ATS
            _ => false
        })
        {
            return true;
        }
 
        // Well-known scalar types using symbol comparison
        return IsWellKnownScalarType(type, wellKnownTypes);
    }
 
    private static bool IsWellKnownScalarType(ITypeSymbol type, WellKnownTypes wellKnownTypes)
    {
        // Date/time types
        if (TryMatchType(type, wellKnownTypes, WellKnownTypeData.WellKnownType.System_DateTimeOffset) ||
            TryMatchType(type, wellKnownTypes, WellKnownTypeData.WellKnownType.System_TimeSpan) ||
            TryMatchType(type, wellKnownTypes, WellKnownTypeData.WellKnownType.System_DateOnly) ||
            TryMatchType(type, wellKnownTypes, WellKnownTypeData.WellKnownType.System_TimeOnly))
        {
            return true;
        }
 
        // Other scalar types
        if (TryMatchType(type, wellKnownTypes, WellKnownTypeData.WellKnownType.System_Guid) ||
            TryMatchType(type, wellKnownTypes, WellKnownTypeData.WellKnownType.System_Uri))
        {
            return true;
        }
 
        return false;
    }
 
    private static bool TryMatchType(ITypeSymbol type, WellKnownTypes wellKnownTypes, WellKnownTypeData.WellKnownType wellKnownType)
    {
        try
        {
            var knownType = wellKnownTypes.Get(wellKnownType);
            return SymbolEqualityComparer.Default.Equals(type, knownType);
        }
        catch (InvalidOperationException)
        {
            // Type not found in compilation
            return false;
        }
    }
 
    private static bool TryMatchGenericType(ITypeSymbol type, WellKnownTypes wellKnownTypes, WellKnownTypeData.WellKnownType wellKnownType)
    {
        if (type is not INamedTypeSymbol namedType || !namedType.IsGenericType)
        {
            return false;
        }
 
        try
        {
            var knownType = wellKnownTypes.Get(wellKnownType);
            return SymbolEqualityComparer.Default.Equals(namedType.OriginalDefinition, knownType);
        }
        catch (InvalidOperationException)
        {
            // Type not found in compilation
            return false;
        }
    }
 
    private static bool IsAtsCompatibleCollectionType(
        ITypeSymbol type,
        WellKnownTypes wellKnownTypes,
        INamedTypeSymbol? aspireExportAttribute,
        HashSet<ITypeSymbol>? currentAssemblyExportedTypes)
    {
        if (type is not INamedTypeSymbol namedType)
        {
            return false;
        }
 
        if (!namedType.IsGenericType)
        {
            return namedType is
            {
                ContainingNamespace.Name: "Collections",
                ContainingNamespace.ContainingNamespace.Name: "System",
                Name: "IDictionary" or "IList"
            };
        }
 
        // Dictionary<K,V> and IDictionary<K,V>
        if (TryMatchGenericType(type, wellKnownTypes, WellKnownTypeData.WellKnownType.System_Collections_Generic_Dictionary_2) ||
            TryMatchGenericType(type, wellKnownTypes, WellKnownTypeData.WellKnownType.System_Collections_Generic_IDictionary_2))
        {
            // Validate key and value types are ATS-compatible
            return namedType.TypeArguments.Length == 2 &&
                   IsAtsCompatibleValueType(namedType.TypeArguments[0], wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes) &&
                   IsAtsCompatibleValueType(namedType.TypeArguments[1], wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes);
        }
 
        // List<T> and IList<T>
        if (TryMatchGenericType(type, wellKnownTypes, WellKnownTypeData.WellKnownType.System_Collections_Generic_List_1) ||
            TryMatchGenericType(type, wellKnownTypes, WellKnownTypeData.WellKnownType.System_Collections_Generic_IList_1))
        {
            return namedType.TypeArguments.Length == 1 &&
                   IsAtsCompatibleValueType(namedType.TypeArguments[0], wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes);
        }
 
        // IReadOnlyList<T> and IReadOnlyCollection<T>
        if (TryMatchGenericType(type, wellKnownTypes, WellKnownTypeData.WellKnownType.System_Collections_Generic_IReadOnlyList_1) ||
            TryMatchGenericType(type, wellKnownTypes, WellKnownTypeData.WellKnownType.System_Collections_Generic_IReadOnlyCollection_1) ||
            TryMatchGenericType(type, wellKnownTypes, WellKnownTypeData.WellKnownType.System_Collections_Generic_IEnumerable_1))
        {
            return namedType.TypeArguments.Length == 1 &&
                   IsAtsCompatibleValueType(namedType.TypeArguments[0], wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes);
        }
 
        // IReadOnlyDictionary<K,V>
        if (TryMatchGenericType(type, wellKnownTypes, WellKnownTypeData.WellKnownType.System_Collections_Generic_IReadOnlyDictionary_2))
        {
            return namedType.TypeArguments.Length == 2 &&
                   IsAtsCompatibleValueType(namedType.TypeArguments[0], wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes) &&
                   IsAtsCompatibleValueType(namedType.TypeArguments[1], wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes);
        }
 
        return false;
    }
 
    private static bool HasAspireExportAttribute(ITypeSymbol type, INamedTypeSymbol aspireExportAttribute, HashSet<ITypeSymbol>? currentAssemblyExportedTypes)
    {
        // Check direct attributes on the type
        foreach (var attr in type.GetAttributes())
        {
            if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, aspireExportAttribute))
            {
                return true;
            }
        }
 
        if (currentAssemblyExportedTypes?.Contains(type) == true)
        {
            return true;
        }
 
        var containingAssembly = type.ContainingAssembly;
        if (containingAssembly is null)
        {
            return false;
        }
 
        foreach (var attr in containingAssembly.GetAttributes())
        {
            if (!SymbolEqualityComparer.Default.Equals(attr.AttributeClass, aspireExportAttribute))
            {
                continue;
            }
 
            if (TryGetAssemblyExportedType(attr, out var exportedType) &&
                SymbolEqualityComparer.Default.Equals(type, exportedType))
            {
                return true;
            }
        }
 
        return false;
    }
 
    private static HashSet<ITypeSymbol> GetAssemblyExportedTypes(IAssemblySymbol assembly, INamedTypeSymbol aspireExportAttribute)
    {
        var exportedTypes = new HashSet<ITypeSymbol>(SymbolEqualityComparer.Default);
 
        foreach (var attr in assembly.GetAttributes())
        {
            if (!SymbolEqualityComparer.Default.Equals(attr.AttributeClass, aspireExportAttribute))
            {
                continue;
            }
 
            if (TryGetAssemblyExportedType(attr, out var exportedType) && exportedType is not null)
            {
                exportedTypes.Add(exportedType);
            }
        }
 
        return exportedTypes;
    }
 
    private static bool TryGetAssemblyExportedType(AttributeData attribute, out ITypeSymbol? exportedType)
    {
        exportedType = null;
 
        if (attribute.ConstructorArguments.Length > 0 &&
            attribute.ConstructorArguments[0].Value is ITypeSymbol constructorType)
        {
            exportedType = constructorType;
            return true;
        }
 
        foreach (var namedArgument in attribute.NamedArguments)
        {
            if (namedArgument.Key == "Type" &&
                namedArgument.Value.Value is ITypeSymbol namedType)
            {
                exportedType = namedType;
                return true;
            }
        }
 
        return false;
    }
 
    private static bool HasAspireDtoAttribute(ITypeSymbol type)
    {
        // Check for [AspireDto] attribute by name (simpler than adding to WellKnownTypes dependency)
        foreach (var attr in type.GetAttributes())
        {
            if (attr.AttributeClass?.Name == "AspireDtoAttribute" &&
                attr.AttributeClass.ContainingNamespace?.ToDisplayString() == "Aspire.Hosting")
            {
                return true;
            }
        }
 
        return false;
    }
 
    private static bool IsMutableCollectionType(ITypeSymbol type, WellKnownTypes wellKnownTypes)
    {
        return TryMatchGenericType(type, wellKnownTypes, WellKnownTypeData.WellKnownType.System_Collections_Generic_Dictionary_2) ||
            TryMatchGenericType(type, wellKnownTypes, WellKnownTypeData.WellKnownType.System_Collections_Generic_IDictionary_2) ||
            TryMatchGenericType(type, wellKnownTypes, WellKnownTypeData.WellKnownType.System_Collections_Generic_List_1) ||
            TryMatchGenericType(type, wellKnownTypes, WellKnownTypeData.WellKnownType.System_Collections_Generic_IList_1);
    }
 
    private static bool IsResourceType(ITypeSymbol type, WellKnownTypes wellKnownTypes)
    {
        try
        {
            var iResourceType = wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Aspire_Hosting_ApplicationModel_IResource);
            return WellKnownTypes.Implements(type, iResourceType) ||
                   SymbolEqualityComparer.Default.Equals(type, iResourceType);
        }
        catch (InvalidOperationException)
        {
            return false;
        }
    }
 
    private static bool IsResourceBuilderType(ITypeSymbol type, WellKnownTypes wellKnownTypes)
    {
        if (type is not INamedTypeSymbol namedType)
        {
            return false;
        }
 
        try
        {
            var iResourceBuilderType = wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Aspire_Hosting_ApplicationModel_IResourceBuilder_1);
 
            // Check if type itself is IResourceBuilder<T>
            if (namedType.IsGenericType &&
                SymbolEqualityComparer.Default.Equals(namedType.OriginalDefinition, iResourceBuilderType))
            {
                return true;
            }
 
            // Check interfaces for IResourceBuilder<T>
            foreach (var iface in namedType.AllInterfaces)
            {
                if (iface.IsGenericType &&
                    SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, iResourceBuilderType))
                {
                    return true;
                }
            }
        }
        catch (InvalidOperationException)
        {
            // Type not found
        }
 
        return false;
    }
 
    private static bool IsAtsCompatibleParameter(
        IParameterSymbol parameter,
        WellKnownTypes wellKnownTypes,
        INamedTypeSymbol aspireExportAttribute,
        HashSet<ITypeSymbol> currentAssemblyExportedTypes)
    {
        var type = parameter.Type;
 
        // Delegate types (Func<>, Action<>, custom delegates) are allowed as callbacks
        if (IsDelegateType(type))
        {
            return true;
        }
 
        // params arrays are allowed if element type is compatible
        if (parameter.IsParams && type is IArrayTypeSymbol arrayType)
        {
            return IsAtsCompatibleValueType(arrayType.ElementType, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes);
        }
 
        return IsAtsCompatibleValueType(type, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes);
    }
 
    private static bool IsDelegateType(ITypeSymbol type)
    {
        if (type is INamedTypeSymbol namedType)
        {
            return namedType.TypeKind == TypeKind.Delegate;
        }
        return false;
    }
}