File: src\Workspaces\SharedUtilitiesAndExtensions\Compiler\Core\Extensions\ISymbolExtensions_Accessibility.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Diagnostics;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Shared.Extensions;
 
internal static partial class ISymbolExtensions
{
    /// <summary>
    /// Checks if 'symbol' is accessible from within 'within'.
    /// </summary>
    public static bool IsAccessibleWithin(
        this ISymbol symbol,
        ISymbol within,
        ITypeSymbol? throughType = null)
    {
        if (within is IAssemblySymbol assembly)
        {
            return symbol.IsAccessibleWithin(assembly, throughType);
        }
        else if (within is INamedTypeSymbol namedType)
        {
            return symbol.IsAccessibleWithin(namedType, throughType);
        }
        else
        {
            throw new ArgumentException();
        }
    }
 
    /// <summary>
    /// Checks if 'symbol' is accessible from within assembly 'within'.
    /// </summary>
    public static bool IsAccessibleWithin(
        this ISymbol symbol,
        IAssemblySymbol within,
        ITypeSymbol? throughType = null)
    {
        return IsSymbolAccessibleCore(symbol, within, throughType, out _);
    }
 
    /// <summary>
    /// Checks if 'symbol' is accessible from within name type 'within', with an optional
    /// qualifier of type "throughTypeOpt".
    /// </summary>
    public static bool IsAccessibleWithin(
        this ISymbol symbol,
        INamedTypeSymbol within,
        ITypeSymbol? throughType = null)
    {
        return IsSymbolAccessible(symbol, within, throughType, out _);
    }
 
    /// <summary>
    /// Checks if 'symbol' is accessible from within assembly 'within', with an qualifier of
    /// type "throughTypeOpt". Sets "failedThroughTypeCheck" to true if it failed the "through
    /// type" check.
    /// </summary>
    private static bool IsSymbolAccessible(
        ISymbol symbol,
        INamedTypeSymbol within,
        ITypeSymbol? throughType,
        out bool failedThroughTypeCheck)
    {
        return IsSymbolAccessibleCore(symbol, within, throughType, out failedThroughTypeCheck);
    }
 
    /// <summary>
    /// Checks if 'symbol' is accessible from within 'within', which must be a INamedTypeSymbol
    /// or an IAssemblySymbol.  If 'symbol' is accessed off of an expression then
    /// 'throughTypeOpt' is the type of that expression. This is needed to properly do protected
    /// access checks. Sets "failedThroughTypeCheck" to true if this protected check failed.
    /// </summary>
    //// NOTE(cyrusn): I expect this function to be called a lot.  As such, I do not do any memory
    //// allocations in the function itself (including not making any iterators).  This does mean
    //// that certain helper functions that we'd like to call are inlined in this method to
    //// prevent the overhead of returning collections or enumerators.  
    private static bool IsSymbolAccessibleCore(
        ISymbol symbol,
        ISymbol within,  // must be assembly or named type symbol
        ITypeSymbol? throughType,
        out bool failedThroughTypeCheck)
    {
        Contract.ThrowIfNull(symbol);
        Contract.ThrowIfNull(within);
        Debug.Assert(within is INamedTypeSymbol or IAssemblySymbol);
 
        failedThroughTypeCheck = false;
        switch (symbol.Kind)
        {
            case SymbolKind.Alias:
                return IsSymbolAccessibleCore(((IAliasSymbol)symbol).Target, within, throughType, out failedThroughTypeCheck);
 
            case SymbolKind.ArrayType:
                return IsSymbolAccessibleCore(((IArrayTypeSymbol)symbol).ElementType, within, null, out failedThroughTypeCheck);
 
            case SymbolKind.PointerType:
                return IsSymbolAccessibleCore(((IPointerTypeSymbol)symbol).PointedAtType, within, null, out failedThroughTypeCheck);
 
            case SymbolKind.FunctionPointerType:
                var funcPtrSignature = ((IFunctionPointerTypeSymbol)symbol).Signature;
                if (!IsSymbolAccessibleCore(funcPtrSignature.ReturnType, within, null, out failedThroughTypeCheck))
                {
                    return false;
                }
 
                foreach (var param in funcPtrSignature.Parameters)
                {
                    if (!IsSymbolAccessibleCore(param.Type, within, null, out failedThroughTypeCheck))
                    {
                        return false;
                    }
                }
 
                return true;
 
            case SymbolKind.NamedType:
                return IsNamedTypeAccessible((INamedTypeSymbol)symbol, within);
 
            case SymbolKind.ErrorType:
            case SymbolKind.Discard:
                return true;
 
            case SymbolKind.TypeParameter:
            case SymbolKind.Parameter:
            case SymbolKind.Local:
            case SymbolKind.Label:
            case SymbolKind.Namespace:
            case SymbolKind.DynamicType:
            case SymbolKind.Assembly:
            case SymbolKind.NetModule:
            case SymbolKind.RangeVariable:
                // These types of symbols are always accessible (if visible).
                return true;
 
            case SymbolKind.Method:
            case SymbolKind.Property:
            case SymbolKind.Field:
            case SymbolKind.Event:
                if (symbol.IsStatic)
                {
                    // static members aren't accessed "through" an "instance" of any type.  So we
                    // null out the "through" instance here.  This ensures that we'll understand
                    // accessing protected statics properly.
                    throughType = null;
                }
 
                // If this is a synthesized operator of dynamic, it's always accessible.
                if (symbol is IMethodSymbol { MethodKind: MethodKind.BuiltinOperator })
                {
                    if (symbol.ContainingSymbol.IsKind(SymbolKind.DynamicType))
                        return true;
 
                    // If it's a synthesized operator on a pointer, use the pointer's PointedAtType.
                    // Note: there are currently no synthesized operators on function pointer types. If that
                    // ever changes, updated the below assert and fix the code
                    if (symbol.ContainingSymbol is IPointerTypeSymbol pointerType)
                        return IsSymbolAccessibleCore(pointerType.PointedAtType, within, null, out failedThroughTypeCheck);
                }
 
                return IsMemberAccessible(symbol.ContainingType, symbol.DeclaredAccessibility, within, throughType, out failedThroughTypeCheck);
 
            default:
                throw ExceptionUtilities.UnexpectedValue(symbol.Kind);
        }
    }
 
    // Is the named type "type" accessible from within "within", which must be a named type or
    // an assembly.
    private static bool IsNamedTypeAccessible(INamedTypeSymbol type, ISymbol within)
    {
        Debug.Assert(within is INamedTypeSymbol or IAssemblySymbol);
        Contract.ThrowIfNull(type);
 
        if (type.IsErrorType())
        {
            // Always assume that error types are accessible.
            return true;
        }
 
        if (!type.IsDefinition)
        {
            // All type argument must be accessible.
            foreach (var typeArg in type.TypeArguments)
            {
                // type parameters are always accessible, so don't check those (so common it's
                // worth optimizing this).
                if (typeArg.Kind != SymbolKind.TypeParameter &&
                    typeArg.TypeKind != TypeKind.Error &&
                    !IsSymbolAccessibleCore(typeArg, within, null, out _))
                {
                    return false;
                }
            }
        }
 
        var containingType = type.ContainingType;
        return containingType == null
            ? IsNonNestedTypeAccessible(type.ContainingAssembly, type.DeclaredAccessibility, within)
            : IsMemberAccessible(type.ContainingType, type.DeclaredAccessibility, within, null, out _);
    }
 
    // Is a top-level type with accessibility "declaredAccessibility" inside assembly "assembly"
    // accessible from "within", which must be a named type of an assembly.
    private static bool IsNonNestedTypeAccessible(
        IAssemblySymbol assembly,
        Accessibility declaredAccessibility,
        ISymbol within)
    {
        Debug.Assert(within is INamedTypeSymbol or IAssemblySymbol);
        Contract.ThrowIfNull(assembly);
        var withinAssembly = (within as IAssemblySymbol) ?? ((INamedTypeSymbol)within).ContainingAssembly;
 
        switch (declaredAccessibility)
        {
            case Accessibility.NotApplicable:
            case Accessibility.Public:
                // Public symbols are always accessible from any context
                return true;
 
            case Accessibility.Private:
            case Accessibility.Protected:
            case Accessibility.ProtectedAndInternal:
                // Shouldn't happen except in error cases.
                return false;
 
            case Accessibility.Internal:
            case Accessibility.ProtectedOrInternal:
                // An internal type is accessible if we're in the same assembly or we have
                // friend access to the assembly it was defined in.
                return withinAssembly.IsSameAssemblyOrHasFriendAccessTo(assembly);
 
            default:
                throw ExceptionUtilities.UnexpectedValue(declaredAccessibility);
        }
    }
 
    // Is a member with declared accessibility "declaredAccessibility" accessible from within
    // "within", which must be a named type or an assembly.
    private static bool IsMemberAccessible(
        INamedTypeSymbol containingType,
        Accessibility declaredAccessibility,
        ISymbol within,
        ITypeSymbol? throughType,
        out bool failedThroughTypeCheck)
    {
        Debug.Assert(within is INamedTypeSymbol or IAssemblySymbol);
        Contract.ThrowIfNull(containingType);
 
        failedThroughTypeCheck = false;
 
        var originalContainingType = containingType.OriginalDefinition;
        var withinNamedType = within as INamedTypeSymbol;
        var withinAssembly = (within as IAssemblySymbol) ?? ((INamedTypeSymbol)within).ContainingAssembly;
 
        // A nested symbol is only accessible to us if its container is accessible as well.
        if (!IsNamedTypeAccessible(containingType, within))
        {
            return false;
        }
 
        switch (declaredAccessibility)
        {
            case Accessibility.NotApplicable:
                // TODO(cyrusn): Is this the right thing to do here?  Should the caller ever be
                // asking about the accessibility of a symbol that has "NotApplicable" as its
                // value?  For now, I'm preserving the behavior of the existing code.  But perhaps
                // we should fail here and require the caller to not do this?
                return true;
 
            case Accessibility.Public:
                // Public symbols are always accessible from any context
                return true;
 
            case Accessibility.Private:
                // All expressions in the current submission (top-level or nested in a method or
                // type) can access previous submission's private top-level members. Previous
                // submissions are treated like outer classes for the current submission - the
                // inner class can access private members of the outer class.
                if (withinAssembly.IsInteractive && containingType.IsScriptClass)
                {
                    return true;
                }
 
                // private members never accessible from outside a type.
                return withinNamedType != null && IsPrivateSymbolAccessible(withinNamedType, originalContainingType);
 
            case Accessibility.Internal:
                // An internal type is accessible if we're in the same assembly or we have
                // friend access to the assembly it was defined in.
                return withinAssembly.IsSameAssemblyOrHasFriendAccessTo(containingType.ContainingAssembly);
 
            case Accessibility.ProtectedAndInternal:
                if (!withinAssembly.IsSameAssemblyOrHasFriendAccessTo(containingType.ContainingAssembly))
                {
                    // We require internal access.  If we don't have it, then this symbol is
                    // definitely not accessible to us.
                    return false;
                }
 
                // We had internal access.  Also have to make sure we have protected access.
                return IsProtectedSymbolAccessible(withinNamedType, withinAssembly, throughType, originalContainingType, out failedThroughTypeCheck);
 
            case Accessibility.ProtectedOrInternal:
                if (withinAssembly.IsSameAssemblyOrHasFriendAccessTo(containingType.ContainingAssembly))
                {
                    // If we have internal access to this symbol, then that's sufficient.  no
                    // need to do the complicated protected case.
                    return true;
                }
 
                // We don't have internal access.  But if we have protected access then that's
                // sufficient.
                return IsProtectedSymbolAccessible(withinNamedType, withinAssembly, throughType, originalContainingType, out failedThroughTypeCheck);
 
            case Accessibility.Protected:
                return IsProtectedSymbolAccessible(withinNamedType, withinAssembly, throughType, originalContainingType, out failedThroughTypeCheck);
 
            default:
                throw ExceptionUtilities.UnexpectedValue(declaredAccessibility);
        }
    }
 
    // Is a protected symbol inside "originalContainingType" accessible from within "within",
    // which much be a named type or an assembly.
    private static bool IsProtectedSymbolAccessible(
        INamedTypeSymbol? withinType,
        IAssemblySymbol withinAssembly,
        ITypeSymbol? throughType,
        INamedTypeSymbol originalContainingType,
        out bool failedThroughTypeCheck)
    {
        failedThroughTypeCheck = false;
 
        // It is not an error to define protected member in a sealed Script class, 
        // it's just a warning. The member behaves like a private one - it is visible 
        // in all subsequent submissions.
        if (withinAssembly.IsInteractive && originalContainingType.IsScriptClass)
        {
            return true;
        }
 
        if (withinType == null)
        {
            // If we're not within a type, we can't access a protected symbol
            return false;
        }
 
        // A protected symbol is accessible if we're (optionally nested) inside the type that it
        // was defined in. 
 
        // NOTE(ericli): It is helpful to consider 'protected' as *increasing* the
        // accessibility domain of a private member, rather than *decreasing* that of a public
        // member. Members are naturally private; the protected, internal and public access
        // modifiers all increase the accessibility domain. Since private members are accessible
        // to nested types, so are protected members.
 
        // NOTE(cyrusn): We do this check up front as it is very fast and easy to do.
        if (IsNestedWithinOriginalContainingType(withinType, originalContainingType))
        {
            return true;
        }
 
        // Protected is really confusing.  Check out 3.5.3 of the language spec "protected access
        // for instance members" to see how it works.  I actually got the code for this from
        // LangCompiler::CheckAccessCore
        {
            var current = withinType.OriginalDefinition;
            var originalThroughType = throughType?.OriginalDefinition;
            while (current != null)
            {
                Debug.Assert(current.IsDefinition);
 
                if (current.InheritsFromOrImplementsOrEqualsIgnoringConstruction(originalContainingType))
                {
                    // NOTE(cyrusn): We're continually walking up the 'throughType's inheritance
                    // chain.  We could compute it up front and cache it in a set.  However, i
                    // don't want to allocate memory in this function.  Also, in practice
                    // inheritance chains should be very short.  As such, it might actually be
                    // slower to create and check inside the set versus just walking the
                    // inheritance chain.
                    if (originalThroughType == null ||
                        originalThroughType.InheritsFromOrImplementsOrEqualsIgnoringConstruction(current))
                    {
                        return true;
                    }
                    else
                    {
                        failedThroughTypeCheck = true;
                    }
                }
 
                // NOTE(cyrusn): The container of an original type is always original.
                current = current.ContainingType;
            }
        }
 
        return false;
    }
 
    // Is a private symbol access
    private static bool IsPrivateSymbolAccessible(
        ISymbol within,
        INamedTypeSymbol originalContainingType)
    {
        Debug.Assert(within is INamedTypeSymbol or IAssemblySymbol);
 
        var withinType = within as INamedTypeSymbol;
        if (withinType == null)
        {
            // If we're not within a type, we can't access a private symbol
            return false;
        }
 
        // A private symbol is accessible if we're (optionally nested) inside the type that it
        // was defined in.
        return IsNestedWithinOriginalContainingType(withinType, originalContainingType);
    }
 
    // Is the type "withinType" nested within the original type "originalContainingType".
    private static bool IsNestedWithinOriginalContainingType(
        INamedTypeSymbol withinType,
        INamedTypeSymbol originalContainingType)
    {
        Contract.ThrowIfNull(withinType);
        Contract.ThrowIfNull(originalContainingType);
 
        // Walk up my parent chain and see if I eventually hit the owner.  If so then I'm a
        // nested type of that owner and I'm allowed access to everything inside of it.
        var current = withinType.OriginalDefinition;
        while (current != null)
        {
            Debug.Assert(current.IsDefinition);
            if (current.Equals(originalContainingType))
            {
                return true;
            }
 
            // NOTE(cyrusn): The container of an 'original' type is always original. 
            current = current.ContainingType;
        }
 
        return false;
    }
}