// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp
{
///
/// Contains the code for determining C# accessibility rules.
///
internal static class AccessCheck
{
///
/// Checks if 'symbol' is accessible from within assembly 'within'.
///
public static bool IsSymbolAccessible(
Symbol symbol,
AssemblySymbol within,
ref HashSet useSiteDiagnostics)
{
bool failedThroughTypeCheck;
return IsSymbolAccessibleCore(symbol, within, null, out failedThroughTypeCheck, within.DeclaringCompilation, ref useSiteDiagnostics);
}
///
/// Checks if 'symbol' is accessible from within type 'within', with
/// an optional qualifier of type "throughTypeOpt".
///
public static bool IsSymbolAccessible(
Symbol symbol,
NamedTypeSymbol within,
ref HashSet useSiteDiagnostics,
TypeSymbol throughTypeOpt = null)
{
bool failedThroughTypeCheck;
return IsSymbolAccessibleCore(symbol, within, throughTypeOpt, out failedThroughTypeCheck, within.DeclaringCompilation, ref useSiteDiagnostics);
}
///
/// Checks if 'symbol' is accessible from within type 'within', with
/// an qualifier of type "throughTypeOpt". Sets "failedThroughTypeCheck" to true
/// if it failed the "through type" check.
///
public static bool IsSymbolAccessible(
Symbol symbol,
NamedTypeSymbol within,
TypeSymbol throughTypeOpt,
out bool failedThroughTypeCheck,
ref HashSet useSiteDiagnostics,
ConsList basesBeingResolved = null)
{
return IsSymbolAccessibleCore(symbol, within, throughTypeOpt, out failedThroughTypeCheck, within.DeclaringCompilation, ref useSiteDiagnostics, basesBeingResolved);
}
///
/// Checks if 'symbol' is accessible from within 'within', which must be a NamedTypeSymbol
/// or an AssemblySymbol.
///
/// Note that NamedTypeSymbol, if available, is the type that is associated with the binder
/// that found the 'symbol', not the inner-most type that contains the access to the
/// 'symbol'.
///
/// 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.
///
/// 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(
Symbol symbol,
Symbol within, // must be assembly or named type symbol
TypeSymbol throughTypeOpt,
out bool failedThroughTypeCheck,
CSharpCompilation compilation,
ref HashSet useSiteDiagnostics,
ConsList basesBeingResolved = null)
{
Debug.Assert((object)symbol != null);
Debug.Assert((object)within != null);
Debug.Assert(within.IsDefinition);
Debug.Assert(within is NamedTypeSymbol || within is AssemblySymbol);
failedThroughTypeCheck = false;
switch (symbol.Kind)
{
case SymbolKind.ArrayType:
return IsSymbolAccessibleCore(((ArrayTypeSymbol)symbol).ElementType, within, null, out failedThroughTypeCheck, compilation, ref useSiteDiagnostics);
case SymbolKind.PointerType:
return IsSymbolAccessibleCore(((PointerTypeSymbol)symbol).PointedAtType, within, null, out failedThroughTypeCheck, compilation, ref useSiteDiagnostics);
case SymbolKind.NamedType:
return IsNamedTypeAccessible((NamedTypeSymbol)symbol, within, ref useSiteDiagnostics, basesBeingResolved);
case SymbolKind.ErrorType:
// Always assume that error types are accessible.
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.Event:
case SymbolKind.Field:
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.
throughTypeOpt = null;
}
return IsMemberAccessible(symbol.ContainingType, symbol.DeclaredAccessibility, within, throughTypeOpt, out failedThroughTypeCheck, compilation, ref useSiteDiagnostics);
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(NamedTypeSymbol type, Symbol within, ref HashSet useSiteDiagnostics, ConsList basesBeingResolved = null)
{
Debug.Assert(within is NamedTypeSymbol || within is AssemblySymbol);
Debug.Assert((object)type != null);
var compilation = within.DeclaringCompilation;
bool unused;
if (!type.IsDefinition)
{
// All type argument must be accessible.
var typeArgs = type.TypeArgumentsWithDefinitionUseSiteDiagnostics(ref useSiteDiagnostics);
for (int i = 0; i < typeArgs.Length; ++i)
{
// type parameters are always accessible, so don't check those (so common it's
// worth optimizing this).
if (typeArgs[i].Kind != SymbolKind.TypeParameter && !IsSymbolAccessibleCore(typeArgs[i], within, null, out unused, compilation, ref useSiteDiagnostics))
{
return false;
}
}
}
var containingType = type.ContainingType;
return (object)containingType == null
? IsNonNestedTypeAccessible(type.ContainingAssembly, type.DeclaredAccessibility, within)
: IsMemberAccessible(containingType, type.DeclaredAccessibility, within, null, out unused, compilation, ref useSiteDiagnostics, basesBeingResolved);
}
// 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(
AssemblySymbol assembly,
Accessibility declaredAccessibility,
Symbol within)
{
Debug.Assert(within is NamedTypeSymbol || within is AssemblySymbol);
Debug.Assert((object)assembly != null);
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:
// within is typically a type
var withinType = within as NamedTypeSymbol;
var withinAssembly = (object)withinType != null ? withinType.ContainingAssembly : (AssemblySymbol)within;
// 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 (object)withinAssembly == (object)assembly || withinAssembly.HasInternalAccessTo(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(
NamedTypeSymbol containingType, // the symbol's containing type
Accessibility declaredAccessibility,
Symbol within,
TypeSymbol throughTypeOpt,
out bool failedThroughTypeCheck,
CSharpCompilation compilation,
ref HashSet useSiteDiagnostics,
ConsList basesBeingResolved = null)
{
Debug.Assert(within is NamedTypeSymbol || within is AssemblySymbol);
Debug.Assert((object)containingType != null);
failedThroughTypeCheck = false;
// easy case - members of containing type are accessible.
if ((object)containingType == (object)within)
{
return true;
}
// A nested symbol is only accessible to us if its container is accessible as well.
if (!IsNamedTypeAccessible(containingType, within, ref useSiteDiagnostics, basesBeingResolved))
{
return false;
}
// public in accessible type is accessible
if (declaredAccessibility == Accessibility.Public)
{
return true;
}
return IsNonPublicMemberAccessible(
containingType,
declaredAccessibility,
within,
throughTypeOpt,
out failedThroughTypeCheck,
compilation,
ref useSiteDiagnostics,
basesBeingResolved);
}
private static bool IsNonPublicMemberAccessible(
NamedTypeSymbol containingType, // the symbol's containing type
Accessibility declaredAccessibility,
Symbol within,
TypeSymbol throughTypeOpt,
out bool failedThroughTypeCheck,
CSharpCompilation compilation,
ref HashSet useSiteDiagnostics,
ConsList basesBeingResolved = null)
{
failedThroughTypeCheck = false;
var originalContainingType = containingType.OriginalDefinition;
var withinType = within as NamedTypeSymbol;
var withinAssembly = (object)withinType != null ? withinType.ContainingAssembly : (AssemblySymbol)within;
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.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 (containingType.TypeKind == TypeKind.Submission)
{
return true;
}
// private members never accessible from outside a type.
return (object)withinType != null && IsPrivateSymbolAccessible(withinType, 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.HasInternalAccessTo(containingType.ContainingAssembly);
case Accessibility.ProtectedAndInternal:
if (!withinAssembly.HasInternalAccessTo(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(withinType, throughTypeOpt, originalContainingType, out failedThroughTypeCheck, compilation, ref useSiteDiagnostics, basesBeingResolved);
case Accessibility.ProtectedOrInternal:
if (withinAssembly.HasInternalAccessTo(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(withinType, throughTypeOpt, originalContainingType, out failedThroughTypeCheck, compilation, ref useSiteDiagnostics, basesBeingResolved);
case Accessibility.Protected:
return IsProtectedSymbolAccessible(withinType, throughTypeOpt, originalContainingType, out failedThroughTypeCheck, compilation, ref useSiteDiagnostics, basesBeingResolved);
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(
NamedTypeSymbol withinType,
TypeSymbol throughTypeOpt,
NamedTypeSymbol originalContainingType,
out bool failedThroughTypeCheck,
CSharpCompilation compilation,
ref HashSet useSiteDiagnostics,
ConsList basesBeingResolved = null)
{
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 (originalContainingType.TypeKind == TypeKind.Submission)
{
return true;
}
if ((object)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 think about '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 originalThroughTypeOpt = (object)throughTypeOpt == null ? null : throughTypeOpt.OriginalDefinition as TypeSymbol;
while ((object)current != null)
{
Debug.Assert(current.IsDefinition);
if (current.InheritsFromIgnoringConstruction(originalContainingType, compilation, ref useSiteDiagnostics, basesBeingResolved))
{
// 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 ((object)originalThroughTypeOpt == null ||
originalThroughTypeOpt.InheritsFromIgnoringConstruction(current, compilation, ref useSiteDiagnostics))
{
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(
Symbol within,
NamedTypeSymbol originalContainingType)
{
Debug.Assert(within is NamedTypeSymbol || within is AssemblySymbol);
var withinType = within as NamedTypeSymbol;
if ((object)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(
NamedTypeSymbol withinType,
NamedTypeSymbol originalContainingType)
{
Debug.Assert((object)withinType != null);
Debug.Assert((object)originalContainingType != null);
// 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 ((object)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;
}
// Determine if "type" inherits from "baseType", ignoring constructed types, and dealing
// only with original types.
private static bool InheritsFromIgnoringConstruction(
this TypeSymbol type,
NamedTypeSymbol baseType,
CSharpCompilation compilation,
ref HashSet useSiteDiagnostics,
ConsList basesBeingResolved = null)
{
Debug.Assert(type.IsDefinition);
Debug.Assert(baseType.IsDefinition);
PooledHashSet visited = null;
var current = type;
bool result = false;
while ((object)current != null)
{
if (current.Equals(baseType))
{
result = true;
break;
}
// NOTE(cyrusn): The base type of an 'original' type may not be 'original'. i.e.
// "class Foo : IBar". We must map it back to the 'original' when as we walk up
// the base type hierarchy.
var next = current.GetNextBaseTypeNoUseSiteDiagnostics(basesBeingResolved, compilation, ref visited);
if ((object)next == null)
{
current = null;
}
else
{
current = (TypeSymbol)next.OriginalDefinition;
current.AddUseSiteDiagnostics(ref useSiteDiagnostics);
}
}
visited?.Free();
return result;
}
// Does the assembly has internal accessibility to "toAssembly"?
internal static bool HasInternalAccessTo(this AssemblySymbol assembly, AssemblySymbol toAssembly)
{
if (Equals(assembly, toAssembly))
{
return true;
}
if (assembly.AreInternalsVisibleToThisAssembly(toAssembly))
{
return true;
}
// all interactive assemblies are friends of each other:
if (assembly.IsInteractive && toAssembly.IsInteractive)
{
return true;
}
return false;
}
internal static ErrorCode GetProtectedMemberInSealedTypeError(NamedTypeSymbol containingType)
{
return containingType.TypeKind == TypeKind.Struct ? ErrorCode.ERR_ProtectedInStruct : ErrorCode.WRN_ProtectedInSealed;
}
}
}