File: Binder\Semantics\Operators\UnaryOperatorOverloadResolution.cs
Web Access
Project: src\src\Compilers\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.csproj (Microsoft.CodeAnalysis.CSharp)
// 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.
 
#nullable disable
 
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp
{
    internal sealed partial class OverloadResolution
    {
        private NamedTypeSymbol MakeNullable(TypeSymbol type)
        {
            return Compilation.GetSpecialType(SpecialType.System_Nullable_T).Construct(type);
        }
 
        public void UnaryOperatorOverloadResolution(
            UnaryOperatorKind kind,
            bool isChecked,
            string name1,
            string name2Opt,
            BoundExpression operand,
            UnaryOperatorOverloadResolutionResult result,
            ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo)
        {
            Debug.Assert(operand != null);
            Debug.Assert(result.Results.Count == 0);
 
            // We can do a table lookup for well-known problems in overload resolution.
            UnaryOperatorEasyOut(kind, operand, result);
            if (result.Results.Count > 0)
            {
                return;
            }
 
            // SPEC: An operation of the form op x or x op, where op is an overloadable unary operator,
            // SPEC: and x is an expression of type X, is processed as follows:
 
            // SPEC: The set of candidate user-defined operators provided by X for the operation operator 
            // SPEC: op(x) is determined using the rules of 7.3.5.
 
            bool hadUserDefinedCandidate = GetUserDefinedOperators(kind, isChecked, name1, name2Opt, operand, result.Results, ref useSiteInfo);
 
            // SPEC: If the set of candidate user-defined operators is not empty, then this becomes the 
            // SPEC: set of candidate operators for the operation. Otherwise, the predefined unary operator 
            // SPEC: implementations, including their lifted forms, become the set of candidate operators 
            // SPEC: for the operation. 
 
            if (!hadUserDefinedCandidate)
            {
                result.Results.Clear();
                GetAllBuiltInOperators(kind, isChecked, operand, result.Results, ref useSiteInfo);
            }
 
            // SPEC: The overload resolution rules of 7.5.3 are applied to the set of candidate operators 
            // SPEC: to select the best operator with respect to the argument list (x), and this operator 
            // SPEC: becomes the result of the overload resolution process. If overload resolution fails 
            // SPEC: to select a single best operator, a binding-time error occurs.
 
            UnaryOperatorOverloadResolution(operand, result, ref useSiteInfo);
        }
 
#nullable enable 
 
        public bool UnaryOperatorExtensionOverloadResolutionInSingleScope(
            ArrayBuilder<NamedTypeSymbol> extensionDeclarationsInSingleScope,
            UnaryOperatorKind kind,
            bool isChecked,
            string name1,
            string? name2Opt,
            BoundExpression operand,
            UnaryOperatorOverloadResolutionResult result,
            ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo)
        {
            Debug.Assert(isChecked || name2Opt is null);
            Debug.Assert(operand.Type is not null);
 
            var operators = ArrayBuilder<UnaryOperatorSignature>.GetInstance();
 
            getDeclaredUserDefinedUnaryOperatorsInScope(extensionDeclarationsInSingleScope, kind, name1, name2Opt, operators);
 
            if (operand.Type.IsNullableType()) // Wouldn't be applicable to the receiver type otherwise
            {
                AddLiftedUserDefinedUnaryOperators(constrainedToTypeOpt: null, kind, operators);
            }
 
            inferTypeArgumentsAndRemoveInapplicableToReceiverType(kind, operand, operators, ref useSiteInfo);
 
            bool hadApplicableCandidates = false;
 
            if (!operators.IsEmpty)
            {
                var results = result.Results;
                results.Clear();
                if (CandidateOperators(isChecked, operators, operand, results, ref useSiteInfo))
                {
                    UnaryOperatorOverloadResolution(operand, result, ref useSiteInfo);
                    hadApplicableCandidates = true;
                }
            }
 
            operators.Free();
 
            return hadApplicableCandidates;
 
            static void getDeclaredUserDefinedUnaryOperatorsInScope(ArrayBuilder<NamedTypeSymbol> extensionDeclarationsInSingleScope, UnaryOperatorKind kind, string name1, string? name2Opt, ArrayBuilder<UnaryOperatorSignature> operators)
            {
                getDeclaredUserDefinedUnaryOperators(extensionDeclarationsInSingleScope, kind, name1, operators);
 
                if (name2Opt is not null)
                {
                    if (!operators.IsEmpty)
                    {
                        var existing = new HashSet<MethodSymbol>(PairedExtensionOperatorSignatureComparer.Instance);
                        existing.AddRange(operators.Select(static (op) => op.Method));
 
                        var operators2 = ArrayBuilder<UnaryOperatorSignature>.GetInstance();
                        getDeclaredUserDefinedUnaryOperators(extensionDeclarationsInSingleScope, kind, name2Opt, operators2);
 
                        foreach (var op in operators2)
                        {
                            if (!existing.Contains(op.Method))
                            {
                                operators.Add(op);
                            }
                        }
 
                        operators2.Free();
                    }
                    else
                    {
                        getDeclaredUserDefinedUnaryOperators(extensionDeclarationsInSingleScope, kind, name2Opt, operators);
                    }
                }
            }
 
            static void getDeclaredUserDefinedUnaryOperators(ArrayBuilder<NamedTypeSymbol> extensionDeclarationsInSingleScope, UnaryOperatorKind kind, string name, ArrayBuilder<UnaryOperatorSignature> operators)
            {
                foreach (NamedTypeSymbol extensionDeclaration in extensionDeclarationsInSingleScope)
                {
                    Debug.Assert(extensionDeclaration.IsExtension);
 
                    if (extensionDeclaration.ExtensionParameter is null)
                    {
                        continue;
                    }
 
                    GetDeclaredUserDefinedUnaryOperators(constrainedToTypeOpt: null, extensionDeclaration, kind, name, operators);
                }
            }
 
            void inferTypeArgumentsAndRemoveInapplicableToReceiverType(UnaryOperatorKind kind, BoundExpression operand, ArrayBuilder<UnaryOperatorSignature> operators, ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo)
            {
                for (int i = operators.Count - 1; i >= 0; i--)
                {
                    var candidate = operators[i];
                    MethodSymbol method = candidate.Method;
                    NamedTypeSymbol extension = method.ContainingType;
 
                    if (extension.Arity == 0)
                    {
                        if (isApplicableToReceiver(in candidate, operand, ref useSiteInfo))
                        {
                            continue;
                        }
                    }
                    else
                    {
                        // Infer type arguments 
                        var inferenceResult = MethodTypeInferrer.Infer(
                            _binder,
                            this.Conversions,
                            extension.TypeParameters,
                            extension,
                            [TypeWithAnnotations.Create(candidate.OperandType)],
                            method.ParameterRefKinds,
                            [operand],
                            ref useSiteInfo,
                            ordinals: null);
 
                        if (inferenceResult.Success)
                        {
                            extension = extension.Construct(inferenceResult.InferredTypeArguments);
                            method = method.AsMember(extension);
 
                            if (!FailsConstraintChecks(method, out ArrayBuilder<TypeParameterDiagnosticInfo> constraintFailureDiagnosticsOpt, template: CompoundUseSiteInfo<AssemblySymbol>.Discarded))
                            {
                                TypeSymbol operandType = method.GetParameterType(0);
                                TypeSymbol resultType = method.ReturnType;
 
                                UnaryOperatorSignature inferredCandidate;
 
                                if (candidate.Kind.IsLifted())
                                {
                                    inferredCandidate = new UnaryOperatorSignature(UnaryOperatorKind.Lifted | UnaryOperatorKind.UserDefined | kind, MakeNullable(operandType), MakeNullable(resultType), method, constrainedToTypeOpt: null);
                                }
                                else
                                {
                                    inferredCandidate = new UnaryOperatorSignature(UnaryOperatorKind.UserDefined | kind, operandType, resultType, method, constrainedToTypeOpt: null);
                                }
 
                                if (isApplicableToReceiver(in inferredCandidate, operand, ref useSiteInfo))
                                {
                                    operators[i] = inferredCandidate;
                                    continue;
                                }
                            }
 
                            constraintFailureDiagnosticsOpt?.Free();
                        }
                    }
 
                    operators.RemoveAt(i);
                }
            }
 
            bool isApplicableToReceiver(in UnaryOperatorSignature candidate, BoundExpression operand, ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo)
            {
                Debug.Assert(operand.Type is not null);
                Debug.Assert(candidate.Method.ContainingType.ExtensionParameter is not null);
 
                if (candidate.Kind.IsLifted())
                {
                    Debug.Assert(operand.Type.IsNullableType());
 
                    if (!candidate.Method.ContainingType.ExtensionParameter.Type.IsValidNullableTypeArgument() ||
                        !Conversions.ConvertExtensionMethodThisArg(MakeNullable(candidate.Method.ContainingType.ExtensionParameter.Type), operand.Type, ref useSiteInfo, isMethodGroupConversion: false).Exists)
                    {
                        return false;
                    }
                }
                else if (!Conversions.ConvertExtensionMethodThisArg(candidate.Method.ContainingType.ExtensionParameter.Type, operand.Type, ref useSiteInfo, isMethodGroupConversion: false).Exists)
                {
                    return false; // Conversion to 'this' parameter failed
                }
 
                return true;
            }
        }
 
        internal class PairedExtensionOperatorSignatureComparer : IEqualityComparer<MethodSymbol>
        {
            public static readonly PairedExtensionOperatorSignatureComparer Instance = new PairedExtensionOperatorSignatureComparer();
 
            private PairedExtensionOperatorSignatureComparer() { }
 
            public bool Equals(MethodSymbol? x, MethodSymbol? y)
            {
                Debug.Assert(x is { });
                Debug.Assert(y is { });
 
                if (x.OriginalDefinition.ContainingType.ContainingType != (object)x.OriginalDefinition.ContainingType.ContainingType)
                {
                    return false;
                }
 
                var xGroupingKey = new SourceMemberContainerTypeSymbol.ExtensionGroupingKey(x.OriginalDefinition.ContainingType);
                var yGroupingKey = new SourceMemberContainerTypeSymbol.ExtensionGroupingKey(y.OriginalDefinition.ContainingType);
 
                if (!xGroupingKey.Equals(yGroupingKey))
                {
                    return false;
                }
 
                return SourceMemberContainerTypeSymbol.DoOperatorsPair(
                           x.OriginalDefinition.AsMember(xGroupingKey.NormalizedExtension),
                           y.OriginalDefinition.AsMember(yGroupingKey.NormalizedExtension));
            }
 
            public int GetHashCode(MethodSymbol op)
            {
                var typeComparer = Symbols.SymbolEqualityComparer.AllIgnoreOptions;
 
                int result = typeComparer.GetHashCode(op.OriginalDefinition.ContainingType.ContainingType);
 
                var groupingKey = new SourceMemberContainerTypeSymbol.ExtensionGroupingKey(op.OriginalDefinition.ContainingType);
                result = Hash.Combine(result, groupingKey.GetHashCode());
 
                foreach (var parameter in op.OriginalDefinition.AsMember(groupingKey.NormalizedExtension).Parameters)
                {
                    result = Hash.Combine(result, typeComparer.GetHashCode(parameter.Type));
                }
 
                return result;
            }
        }
 
#nullable disable
 
        // Takes a list of candidates and mutates the list to throw out the ones that are worse than
        // another applicable candidate.
        private void UnaryOperatorOverloadResolution(
            BoundExpression operand,
            UnaryOperatorOverloadResolutionResult result,
            ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo)
        {
            // SPEC: Given the set of applicable candidate function members, the best function member in that set is located. 
            // SPEC: If the set contains only one function member, then that function member is the best function member. 
 
            if (result.SingleValid())
            {
                return;
            }
 
            var candidates = result.Results;
            RemoveLowerPriorityMembers<UnaryOperatorAnalysisResult, MethodSymbol>(candidates);
 
            // SPEC: Otherwise, the best function member is the one function member that is better than all other function 
            // SPEC: members with respect to the given argument list, provided that each function member is compared to all 
            // SPEC: other function members using the rules in 7.5.3.2. If there is not exactly one function member that is 
            // SPEC: better than all other function members, then the function member invocation is ambiguous and a binding-time 
            // SPEC: error occurs.
 
            // Try to find a single best candidate
            int bestIndex = GetTheBestCandidateIndex(operand, candidates, ref useSiteInfo);
            if (bestIndex != -1)
            {
                // Mark all other candidates as worse
                for (int index = 0; index < candidates.Count; ++index)
                {
                    if (candidates[index].Kind != OperatorAnalysisResultKind.Inapplicable && index != bestIndex)
                    {
                        candidates[index] = candidates[index].Worse();
                    }
                }
 
                return;
            }
 
            for (int i = 1; i < candidates.Count; ++i)
            {
                if (candidates[i].Kind != OperatorAnalysisResultKind.Applicable)
                {
                    continue;
                }
 
                // Is this applicable operator better than every other applicable method?
                for (int j = 0; j < i; ++j)
                {
                    if (candidates[j].Kind == OperatorAnalysisResultKind.Inapplicable)
                    {
                        continue;
                    }
 
                    var better = BetterOperator(candidates[i].Signature, candidates[j].Signature, operand, ref useSiteInfo);
                    if (better == BetterResult.Left)
                    {
                        candidates[j] = candidates[j].Worse();
                    }
                    else if (better == BetterResult.Right)
                    {
                        candidates[i] = candidates[i].Worse();
                    }
                }
            }
        }
 
        private int GetTheBestCandidateIndex(
            BoundExpression operand,
            ArrayBuilder<UnaryOperatorAnalysisResult> candidates,
            ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo)
        {
            int currentBestIndex = -1;
            for (int index = 0; index < candidates.Count; index++)
            {
                if (candidates[index].Kind != OperatorAnalysisResultKind.Applicable)
                {
                    continue;
                }
 
                // Assume that the current candidate is the best if we don't have any
                if (currentBestIndex == -1)
                {
                    currentBestIndex = index;
                }
                else
                {
                    var better = BetterOperator(candidates[currentBestIndex].Signature, candidates[index].Signature, operand, ref useSiteInfo);
                    if (better == BetterResult.Right)
                    {
                        // The current best is worse
                        currentBestIndex = index;
                    }
                    else if (better != BetterResult.Left)
                    {
                        // The current best is not better
                        currentBestIndex = -1;
                    }
                }
            }
 
            // Make sure that every candidate up to the current best is worse
            for (int index = 0; index < currentBestIndex; index++)
            {
                if (candidates[index].Kind == OperatorAnalysisResultKind.Inapplicable)
                {
                    continue;
                }
 
                var better = BetterOperator(candidates[currentBestIndex].Signature, candidates[index].Signature, operand, ref useSiteInfo);
                if (better != BetterResult.Left)
                {
                    // The current best is not better
                    return -1;
                }
            }
 
            return currentBestIndex;
        }
 
        private BetterResult BetterOperator(UnaryOperatorSignature op1, UnaryOperatorSignature op2, BoundExpression operand, ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo)
        {
            // First we see if the conversion from the operand to one operand type is better than 
            // the conversion to the other.
 
            BetterResult better = BetterConversionFromExpression(operand, op1.OperandType, op2.OperandType, ref useSiteInfo);
 
            if (better == BetterResult.Left || better == BetterResult.Right)
            {
                return better;
            }
 
            // There was no better member on the basis of conversions. Go to the tiebreaking round.
 
            // SPEC: In case the parameter type sequences P1, P2 and Q1, Q2 are equivalent -- that is, every Pi
            // SPEC: has an identity conversion to the corresponding Qi -- the following tie-breaking rules
            // SPEC: are applied:
 
            if (Conversions.HasIdentityConversion(op1.OperandType, op2.OperandType))
            {
                // SPEC: If Mp is a non-generic method and Mq is a generic method, then Mp is better than Mq.
                if (op1.Method?.GetMemberArityIncludingExtension() is null or 0)
                {
                    if (op2.Method?.GetMemberArityIncludingExtension() > 0)
                    {
                        return BetterResult.Left;
                    }
                }
                else if (op2.Method?.GetMemberArityIncludingExtension() is null or 0)
                {
                    return BetterResult.Right;
                }
 
                // SPEC: If Mp has more specific parameter types than Mq then Mp is better than Mq.
 
                // Under what circumstances can two unary operators with identical signatures be "more specific"
                // than another? With a binary operator you could have C<T>.op+(C<T>, T) and C<T>.op+(C<T>, int).
                // When doing overload resolution on C<int> + int, the latter is more specific. But with a unary
                // operator, the sole operand *must* be the containing type or its nullable type. Therefore
                // if there is an identity conversion, then the parameters really were identical. We therefore
                // skip checking for specificity.
 
                // SPEC: If one member is a non-lifted operator and the other is a lifted operator,
                // SPEC: the non-lifted one is better.
 
                bool lifted1 = op1.Kind.IsLifted();
                bool lifted2 = op2.Kind.IsLifted();
 
                if (lifted1 && !lifted2)
                {
                    return BetterResult.Right;
                }
                else if (!lifted1 && lifted2)
                {
                    return BetterResult.Left;
                }
            }
 
            // Always prefer operators with val parameters over operators with in parameters:
            if (op1.RefKind == RefKind.None && op2.RefKind == RefKind.In)
            {
                return BetterResult.Left;
            }
            else if (op2.RefKind == RefKind.None && op1.RefKind == RefKind.In)
            {
                return BetterResult.Right;
            }
 
            return BetterResult.Neither;
        }
 
        private void GetAllBuiltInOperators(UnaryOperatorKind kind, bool isChecked, BoundExpression operand, ArrayBuilder<UnaryOperatorAnalysisResult> results, ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo)
        {
            // The spec states that overload resolution is performed upon the infinite set of
            // operators defined on enumerated types, pointers and delegates. Clearly we cannot
            // construct the infinite set; we have to pare it down. Previous implementations of C#
            // implement a much stricter rule; they only add the special operators to the candidate
            // set if one of the operands is of the relevant type. This means that operands
            // involving user-defined implicit conversions from class or struct types to enum,
            // pointer and delegate types do not cause the right candidates to participate in
            // overload resolution. It also presents numerous problems involving delegate variance
            // and conversions from lambdas to delegate types.
            //
            // It is onerous to require the actually specified behavior. We should change the
            // specification to match the previous implementation.
 
            var operators = ArrayBuilder<UnaryOperatorSignature>.GetInstance();
            this.Compilation.BuiltInOperators.GetSimpleBuiltInOperators(kind, operators, skipNativeIntegerOperators: !operand.Type.IsNativeIntegerOrNullableThereof());
 
            GetEnumOperations(kind, operand, operators);
 
            var pointerOperator = GetPointerOperation(kind, operand);
            if (pointerOperator != null)
            {
                operators.Add(pointerOperator.Value);
            }
 
            CandidateOperators(isChecked, operators, operand, results, ref useSiteInfo);
            operators.Free();
        }
 
        // Returns true if there were any applicable candidates.
        private bool CandidateOperators(bool isChecked, ArrayBuilder<UnaryOperatorSignature> operators, BoundExpression operand, ArrayBuilder<UnaryOperatorAnalysisResult> results, ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo)
        {
            bool anyApplicable = false;
            foreach (var op in operators)
            {
                var conversion = Conversions.ClassifyConversionFromExpression(operand, op.OperandType, isChecked: isChecked, ref useSiteInfo);
                if (conversion.IsImplicit)
                {
                    anyApplicable = true;
                    results.Add(UnaryOperatorAnalysisResult.Applicable(op, conversion));
                }
                else
                {
                    results.Add(UnaryOperatorAnalysisResult.Inapplicable(op, conversion));
                }
            }
 
            return anyApplicable;
        }
 
        private void GetEnumOperations(UnaryOperatorKind kind, BoundExpression operand, ArrayBuilder<UnaryOperatorSignature> operators)
        {
            Debug.Assert(operand != null);
 
            var enumType = operand.Type;
            if ((object)enumType == null)
            {
                return;
            }
 
            enumType = enumType.StrippedType();
            if (!enumType.IsValidEnumType())
            {
                return;
            }
 
            var nullableEnum = Compilation.GetOrCreateNullableType(enumType);
 
            switch (kind)
            {
                case UnaryOperatorKind.PostfixIncrement:
                case UnaryOperatorKind.PostfixDecrement:
                case UnaryOperatorKind.PrefixIncrement:
                case UnaryOperatorKind.PrefixDecrement:
                case UnaryOperatorKind.BitwiseComplement:
                    operators.Add(new UnaryOperatorSignature(kind | UnaryOperatorKind.Enum, enumType, enumType));
                    operators.Add(new UnaryOperatorSignature(kind | UnaryOperatorKind.Lifted | UnaryOperatorKind.Enum, nullableEnum, nullableEnum));
                    break;
            }
        }
 
        private static UnaryOperatorSignature? GetPointerOperation(UnaryOperatorKind kind, BoundExpression operand)
        {
            Debug.Assert(operand != null);
 
            var pointerType = operand.Type as PointerTypeSymbol;
            if ((object)pointerType == null)
            {
                return null;
            }
 
            UnaryOperatorSignature? op = null;
            switch (kind)
            {
                case UnaryOperatorKind.PostfixIncrement:
                case UnaryOperatorKind.PostfixDecrement:
                case UnaryOperatorKind.PrefixIncrement:
                case UnaryOperatorKind.PrefixDecrement:
                    op = new UnaryOperatorSignature(kind | UnaryOperatorKind.Pointer, pointerType, pointerType);
                    break;
            }
            return op;
        }
 
        // Returns true if there were any applicable candidates.
        private bool GetUserDefinedOperators(
            UnaryOperatorKind kind,
            bool isChecked,
            string name1,
            string name2Opt,
            BoundExpression operand,
            ArrayBuilder<UnaryOperatorAnalysisResult> results,
            ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo)
        {
            Debug.Assert(operand != null);
 
            if ((object)operand.Type == null)
            {
                // If the operand has no type -- because it is a null reference or a lambda or a method group --
                // there is no way we can determine what type to search for user-defined operators.
                return false;
            }
 
            // Spec 7.3.5 Candidate user-defined operators
            // SPEC: Given a type T and an operation op(A) ... the set of candidate user-defined 
            // SPEC: operators provided by T for op(A) is determined as follows:
 
            // SPEC: If T is a nullable type then T0 is its underlying type; otherwise T0 is T.
            // SPEC: For all operator declarations in T0 and all lifted forms of such operators, if
            // SPEC: at least one operator is applicable with respect to A then the set of candidate
            // SPEC: operators consists of all such applicable operators. Otherwise, if T0 is object
            // SPEC: then the set of candidate operators is empty. Otherwise, the set of candidate
            // SPEC: operators is the set provided by the direct base class of T0, or the effective
            // SPEC: base class of T0 if T0 is a type parameter.
 
            // https://github.com/dotnet/roslyn/issues/34451: The spec quote should be adjusted to cover operators from interfaces as well.
            // From https://github.com/dotnet/csharplang/blob/main/meetings/2017/LDM-2017-06-27.md:
            // - We only even look for operator implementations in interfaces if one of the operands has a type that is an interface or
            // a type parameter with a non-empty effective base interface list.
            // - The applicable operators from classes / structs shadow those in interfaces.This matters for constrained type parameters:
            // the effective base class can shadow operators from effective base interfaces.
            // - If we find an applicable candidate in an interface, that candidate shadows all applicable operators in base interfaces:
            // we stop looking.
 
            TypeSymbol type0 = operand.Type.StrippedType();
            TypeSymbol constrainedToTypeOpt = type0 as TypeParameterSymbol;
 
            // Searching for user-defined operators is expensive; let's take an early out if we can.
            if (OperatorFacts.DefinitelyHasNoUserDefinedOperators(type0))
            {
                return false;
            }
 
            var operators = ArrayBuilder<UnaryOperatorSignature>.GetInstance();
            bool hadApplicableCandidates = false;
 
            NamedTypeSymbol current = type0 as NamedTypeSymbol;
            if ((object)current == null)
            {
                current = type0.BaseTypeWithDefinitionUseSiteDiagnostics(ref useSiteInfo);
            }
 
            if ((object)current == null && type0.IsTypeParameter())
            {
                current = ((TypeParameterSymbol)type0).EffectiveBaseClass(ref useSiteInfo);
            }
 
            for (; (object)current != null; current = current.BaseTypeWithDefinitionUseSiteDiagnostics(ref useSiteInfo))
            {
                operators.Clear();
 
                GetUserDefinedUnaryOperatorsFromType(constrainedToTypeOpt, current, kind, name1, name2Opt, operators);
 
                results.Clear();
                if (CandidateOperators(isChecked, operators, operand, results, ref useSiteInfo))
                {
                    hadApplicableCandidates = true;
                    break;
                }
            }
 
            // Look in base interfaces, or effective interfaces for type parameters  
            if (!hadApplicableCandidates)
            {
                ImmutableArray<NamedTypeSymbol> interfaces = default;
                if (type0.IsInterfaceType())
                {
                    interfaces = type0.AllInterfacesWithDefinitionUseSiteDiagnostics(ref useSiteInfo);
                }
                else if (type0.IsTypeParameter())
                {
                    interfaces = ((TypeParameterSymbol)type0).AllEffectiveInterfacesWithDefinitionUseSiteDiagnostics(ref useSiteInfo);
                }
 
                if (!interfaces.IsDefaultOrEmpty)
                {
                    var shadowedInterfaces = PooledHashSet<NamedTypeSymbol>.GetInstance();
                    var resultsFromInterface = ArrayBuilder<UnaryOperatorAnalysisResult>.GetInstance();
                    results.Clear();
 
                    foreach (NamedTypeSymbol @interface in interfaces)
                    {
                        if (!@interface.IsInterface)
                        {
                            // this code could be reachable in error situations
                            continue;
                        }
 
                        if (shadowedInterfaces.Contains(@interface))
                        {
                            // this interface is "shadowed" by a derived interface
                            continue;
                        }
 
                        operators.Clear();
                        resultsFromInterface.Clear();
                        GetUserDefinedUnaryOperatorsFromType(constrainedToTypeOpt, @interface, kind, name1, name2Opt, operators);
                        if (CandidateOperators(isChecked, operators, operand, resultsFromInterface, ref useSiteInfo))
                        {
                            hadApplicableCandidates = true;
                            results.AddRange(resultsFromInterface);
 
                            // this interface "shadows" all its base interfaces
                            shadowedInterfaces.AddAll(@interface.AllInterfacesWithDefinitionUseSiteDiagnostics(ref useSiteInfo));
                        }
                    }
 
                    shadowedInterfaces.Free();
                    resultsFromInterface.Free();
                }
            }
 
            operators.Free();
 
            return hadApplicableCandidates;
        }
 
#nullable enable
 
        internal static void GetStaticUserDefinedUnaryOperatorMethodNames(UnaryOperatorKind kind, bool isChecked, out string name1, out string? name2Opt)
        {
            name1 = OperatorFacts.UnaryOperatorNameFromOperatorKind(kind, isChecked);
 
            if (isChecked && SyntaxFacts.IsCheckedOperator(name1))
            {
                name2Opt = OperatorFacts.UnaryOperatorNameFromOperatorKind(kind, isChecked: false);
            }
            else
            {
                name2Opt = null;
            }
        }
 
        private void GetUserDefinedUnaryOperatorsFromType(
            TypeSymbol constrainedToTypeOpt,
            NamedTypeSymbol type,
            UnaryOperatorKind kind,
            string name1,
            string? name2Opt,
            ArrayBuilder<UnaryOperatorSignature> operators)
        {
            Debug.Assert(operators.Count == 0);
 
            GetDeclaredUserDefinedUnaryOperators(constrainedToTypeOpt, type, kind, name1, operators);
 
            if (name2Opt is not null)
            {
                var operators2 = ArrayBuilder<UnaryOperatorSignature>.GetInstance();
 
                // Add regular operators as well.
                GetDeclaredUserDefinedUnaryOperators(constrainedToTypeOpt, type, kind, name2Opt, operators2);
 
                // Drop operators that have a match among the checked ones.
                if (operators.Count != 0)
                {
                    for (int i = operators2.Count - 1; i >= 0; i--)
                    {
                        foreach (UnaryOperatorSignature signature1 in operators)
                        {
                            if (SourceMemberContainerTypeSymbol.DoOperatorsPair(signature1.Method, operators2[i].Method))
                            {
                                operators2.RemoveAt(i);
                                break;
                            }
                        }
                    }
                }
 
                operators.AddRange(operators2);
                operators2.Free();
            }
 
            AddLiftedUserDefinedUnaryOperators(constrainedToTypeOpt, kind, operators);
        }
 
        private static void GetDeclaredUserDefinedUnaryOperators(TypeSymbol? constrainedToTypeOpt, NamedTypeSymbol type, UnaryOperatorKind kind, string name, ArrayBuilder<UnaryOperatorSignature> operators)
        {
            var typeOperators = ArrayBuilder<MethodSymbol>.GetInstance();
            type.AddOperators(name, typeOperators);
 
            foreach (MethodSymbol op in typeOperators)
            {
                // If we're in error recovery, we might have bad operators. Just ignore it.
                if (op.ParameterCount != 1 || op.ReturnsVoid)
                {
                    continue;
                }
 
                TypeSymbol operandType = op.GetParameterType(0);
                TypeSymbol resultType = op.ReturnType;
 
                operators.Add(new UnaryOperatorSignature(UnaryOperatorKind.UserDefined | kind, operandType, resultType, op, constrainedToTypeOpt));
            }
 
            typeOperators.Free();
        }
 
        private void AddLiftedUserDefinedUnaryOperators(TypeSymbol? constrainedToTypeOpt, UnaryOperatorKind kind, ArrayBuilder<UnaryOperatorSignature> operators)
        {
            // SPEC: For the unary operators + ++ - -- ! ~ a lifted form of an operator exists
            // SPEC: if the operand and its result types are both non-nullable value types.
            // SPEC: The lifted form is constructed by adding a single ? modifier to the
            // SPEC: operator and result types. 
            switch (kind)
            {
                case UnaryOperatorKind.UnaryPlus:
                case UnaryOperatorKind.PrefixDecrement:
                case UnaryOperatorKind.PrefixIncrement:
                case UnaryOperatorKind.UnaryMinus:
                case UnaryOperatorKind.PostfixDecrement:
                case UnaryOperatorKind.PostfixIncrement:
                case UnaryOperatorKind.LogicalNegation:
                case UnaryOperatorKind.BitwiseComplement:
 
                    for (int i = operators.Count - 1; i >= 0; i--)
                    {
                        MethodSymbol op = operators[i].Method;
                        TypeSymbol operandType = op.GetParameterType(0);
                        TypeSymbol resultType = op.ReturnType;
 
                        if (operandType.IsValidNullableTypeArgument() &&
                            resultType.IsValidNullableTypeArgument())
                        {
                            operators.Add(new UnaryOperatorSignature(
                                UnaryOperatorKind.Lifted | UnaryOperatorKind.UserDefined | kind,
                                MakeNullable(operandType), MakeNullable(resultType), op, constrainedToTypeOpt));
                        }
                    }
                    break;
            }
        }
    }
}