File: ApiStabilityClassifier.cs
Web Access
Project: Microsoft.DotNet.ApiCompatibility.csproj (Microsoft.DotNet.ApiCompatibility)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis;
using Microsoft.DotNet.ApiSymbolExtensions.Filtering;

namespace Microsoft.DotNet.ApiCompatibility
{
    internal static class ApiStabilityClassifier
    {
        private const string ExperimentalAttributeName = "ExperimentalAttribute";

        private static readonly ConditionalWeakTable<ISymbol, CacheEntry> s_classifications = new();

        public static ApiStability Classify(ISymbol? symbol, ISymbolFilter attributeDataSymbolFilter)
        {
            if (symbol is null)
            {
                return ApiStability.Stable;
            }

            CacheEntry classification = s_classifications.GetValue(symbol, CreateCacheEntry);
            return classification.ExperimentalAttributeClasses.Any(
                attributeClass => IsIncluded(attributeClass, attributeDataSymbolFilter))
                ? ApiStability.Experimental
                : ApiStability.Stable;
        }

        private static CacheEntry CreateCacheEntry(ISymbol symbol)
        {
            List<INamedTypeSymbol> experimentalAttributeClasses = [];
            AddExperimentalAttributeClasses(symbol, experimentalAttributeClasses);

            if (symbol is IMethodSymbol { AssociatedSymbol: { } associatedSymbol })
            {
                AddExperimentalAttributeClasses(associatedSymbol, experimentalAttributeClasses);
            }

            if (symbol is not IModuleSymbol && symbol.ContainingModule is { } containingModule)
            {
                AddExperimentalAttributeClasses(containingModule, experimentalAttributeClasses);
            }

            if (symbol is not IAssemblySymbol && symbol.ContainingAssembly is { } containingAssembly)
            {
                AddExperimentalAttributeClasses(containingAssembly, experimentalAttributeClasses);
            }

            return new([.. experimentalAttributeClasses]);
        }

        private static void AddExperimentalAttributeClasses(ISymbol symbol, List<INamedTypeSymbol> experimentalAttributeClasses)
        {
            foreach (AttributeData attribute in symbol.GetAttributes())
            {
                if (IsExperimentalAttribute(attribute) && attribute.AttributeClass is { } attributeClass)
                {
                    experimentalAttributeClasses.Add(attributeClass);
                }
            }
        }

        public static bool IsExperimentalAttribute(AttributeData attribute) =>
            attribute.AttributeClass is
            {
                MetadataName: ExperimentalAttributeName,
                ContainingNamespace:
                {
                    Name: "CodeAnalysis",
                    ContainingNamespace:
                    {
                        Name: "Diagnostics",
                        ContainingNamespace:
                        {
                            Name: "System",
                            ContainingNamespace.IsGlobalNamespace: true
                        }
                    }
                }
            } &&
            HasExpectedConstructor(attribute);

        private static bool HasExpectedConstructor(AttributeData attribute)
        {
            // ApiCompat commonly loads metadata without assembly references, in which case Roslyn cannot bind the constructor.
            return attribute.AttributeConstructor is null ||
                attribute.AttributeConstructor.Parameters is [{ Type.SpecialType: SpecialType.System_String }];
        }

        private static bool IsIncluded(INamedTypeSymbol attributeClass, ISymbolFilter filter)
        {
            if (attributeClass is not IErrorTypeSymbol)
            {
                return filter.Include(attributeClass);
            }

            return filter switch
            {
                AccessibilitySymbolFilter => true,
                CompositeSymbolFilter { Mode: CompositeSymbolFilterMode.And } composite =>
                    composite.Filters.All(innerFilter => IsIncluded(attributeClass, innerFilter)),
                CompositeSymbolFilter { Mode: CompositeSymbolFilterMode.Or } composite =>
                    composite.Filters.Any(innerFilter => IsIncluded(attributeClass, innerFilter)),
                _ => filter.Include(attributeClass)
            };
        }

        private sealed class CacheEntry(INamedTypeSymbol[] experimentalAttributeClasses)
        {
            public INamedTypeSymbol[] ExperimentalAttributeClasses { get; } = experimentalAttributeClasses;
        }
    }
}