File: CompilationTestUtils.cs
Web Access
Project: src\src\Compilers\Test\Utilities\CSharp\Microsoft.CodeAnalysis.CSharp.Test.Utilities.csproj (Microsoft.CodeAnalysis.CSharp.Test.Utilities)
// 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 Microsoft.CodeAnalysis.CSharp.Emit;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Xunit;
using Microsoft.CodeAnalysis.CSharp.Test.Utilities;
using System.Diagnostics;
 
namespace Microsoft.CodeAnalysis.CSharp.UnitTests
{
    public static class CompilationUtils
    {
        internal static void CheckISymbols<TSymbol>(ImmutableArray<TSymbol> symbols, params string[] descriptions)
            where TSymbol : ISymbol
        {
            Assert.Equal(descriptions.Length, symbols.Length);
 
            string[] symbolDescriptions = (from s in symbols select s.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)).ToArray();
            Array.Sort(descriptions);
            Array.Sort(symbolDescriptions);
 
            for (int i = 0; i < descriptions.Length; i++)
            {
                Assert.Equal(symbolDescriptions[i], descriptions[i]);
            }
        }
 
        internal static void CheckSymbols<TSymbol>(ImmutableArray<TSymbol> symbols, params string[] descriptions)
            where TSymbol : Symbol
        {
            Assert.Equal(descriptions.Length, symbols.Length);
 
            string[] symbolDescriptions = (from s in symbols select s.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)).ToArray();
            Array.Sort(descriptions);
            Array.Sort(symbolDescriptions);
 
            for (int i = 0; i < descriptions.Length; i++)
            {
                Assert.Equal(symbolDescriptions[i], descriptions[i]);
            }
        }
 
        public static void CheckSymbolsUnordered<TSymbol>(ImmutableArray<TSymbol> symbols, params string[] descriptions)
            where TSymbol : ISymbol
        {
            Assert.Equal(descriptions.Length, symbols.Length);
            AssertEx.SetEqual(symbols.Select(s => s.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)), descriptions);
        }
 
        public static void CheckSymbols<TSymbol>(TSymbol[] symbols, params string[] descriptions)
            where TSymbol : ISymbol
        {
            CheckISymbols(symbols.AsImmutableOrNull(), descriptions);
        }
 
        public static void CheckSymbol(ISymbol symbol, string description)
        {
            Assert.Equal(symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), description);
        }
 
        internal static void CheckSymbol(Symbol symbol, string description)
        {
            Assert.Equal(symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), description);
        }
 
        internal static void CheckConstraints(ITypeParameterSymbol symbol, TypeParameterConstraintKind constraints, params string[] constraintTypes)
        {
            Assert.Equal(constraints, GetTypeParameterConstraints(symbol));
            CheckISymbols(symbol.ConstraintTypes, constraintTypes);
        }
 
        internal static void CheckReducedExtensionMethod(
            MethodSymbol reducedMethod,
            string reducedMethodDescription,
            string reducedFromDescription,
            string constructedFromDescription,
            string reducedAndConstructedFromDescription)
        {
            var reducedFrom = reducedMethod.ReducedFrom;
            CheckReducedExtensionMethod(reducedMethod, reducedFrom);
            Assert.Equal(reducedMethod.CallsiteReducedFromMethod.Parameters[0].Type, reducedMethod.ReceiverType);
 
            var constructedFrom = reducedMethod.ConstructedFrom;
            CheckConstructedMethod(reducedMethod, constructedFrom);
 
            var reducedAndConstructedFrom = constructedFrom.ReducedFrom;
            CheckReducedExtensionMethod(constructedFrom, reducedAndConstructedFrom);
            Assert.Same(reducedFrom, reducedAndConstructedFrom);
 
            var constructedAndExtendedFrom = reducedFrom.ConstructedFrom;
            CheckConstructedMethod(reducedFrom, constructedAndExtendedFrom);
 
            CheckSymbol(reducedMethod, reducedMethodDescription);
            CheckSymbol(reducedFrom, reducedFromDescription);
            CheckSymbol(constructedFrom, constructedFromDescription);
            CheckSymbol(reducedAndConstructedFrom, reducedAndConstructedFromDescription);
            CheckSymbol(constructedAndExtendedFrom, reducedAndConstructedFromDescription);
        }
 
        public static void CheckReducedExtensionMethod(IMethodSymbol reducedMethod, IMethodSymbol reducedFrom)
        {
            Assert.NotNull(reducedFrom);
            Assert.Equal(reducedMethod.ReducedFrom, reducedFrom);
            Assert.Null(reducedFrom.ReducedFrom);
 
            Assert.True(reducedFrom.IsExtensionMethod);
            Assert.True(reducedMethod.IsExtensionMethod);
            Assert.Equal(reducedMethod.IsImplicitlyDeclared, reducedFrom.IsImplicitlyDeclared);
            Assert.Equal(reducedMethod.CanBeReferencedByName, reducedFrom.CanBeReferencedByName);
 
            int n = reducedMethod.Parameters.Count();
            Assert.Equal(reducedFrom.Parameters.Count(), n + 1);
 
            CheckTypeParameters(reducedMethod);
            CheckTypeParameters(reducedFrom);
        }
 
        internal static void CheckReducedExtensionMethod(MethodSymbol reducedMethod, MethodSymbol reducedFrom)
        {
            CheckReducedExtensionMethod(reducedMethod.GetPublicSymbol(), reducedFrom.GetPublicSymbol());
        }
 
        public static void CheckConstructedMethod(IMethodSymbol constructedMethod, IMethodSymbol constructedFrom)
        {
            Assert.NotNull(constructedFrom);
 
            Assert.Same(constructedFrom, constructedMethod.ConstructedFrom);
            Assert.Same(constructedFrom, constructedMethod.OriginalDefinition);
 
            Assert.Same(constructedFrom, constructedFrom.ConstructedFrom);
            Assert.Same(constructedFrom, constructedFrom.OriginalDefinition);
 
            CheckTypeParameters(constructedMethod);
            CheckTypeParameters(constructedFrom);
        }
 
        internal static void CheckConstructedMethod(MethodSymbol constructedMethod, MethodSymbol constructedFrom)
        {
            CheckConstructedMethod(constructedMethod.GetPublicSymbol(), constructedFrom.GetPublicSymbol());
        }
 
        private static void CheckTypeParameters(IMethodSymbol method)
        {
            var constructedFrom = method.ConstructedFrom;
            Assert.NotNull(constructedFrom);
 
            foreach (var typeParameter in method.TypeParameters)
            {
                Assert.Equal<ISymbol>(typeParameter.ContainingSymbol, constructedFrom);
            }
        }
 
        internal static TypeParameterConstraintKind GetTypeParameterConstraints(ITypeParameterSymbol typeParameter)
        {
            var constraints = TypeParameterConstraintKind.None;
            if (typeParameter.HasConstructorConstraint)
            {
                constraints |= TypeParameterConstraintKind.Constructor;
            }
            if (typeParameter.HasReferenceTypeConstraint)
            {
                constraints |= TypeParameterConstraintKind.ReferenceType;
            }
            if (typeParameter.HasValueTypeConstraint)
            {
                constraints |= TypeParameterConstraintKind.ValueType;
            }
            return constraints;
        }
 
        internal static TypeParameterConstraintKind GetTypeParameterConstraints(TypeParameterSymbol typeParameter)
        {
            var constraints = TypeParameterConstraintKind.None;
            if (typeParameter.HasConstructorConstraint)
            {
                constraints |= TypeParameterConstraintKind.Constructor;
            }
            if (typeParameter.HasReferenceTypeConstraint)
            {
                constraints |= TypeParameterConstraintKind.ReferenceType;
            }
            if (typeParameter.HasValueTypeConstraint)
            {
                constraints |= TypeParameterConstraintKind.ValueType;
            }
            return constraints;
        }
 
        public class SemanticInfoSummary
        {
            public ISymbol Symbol;
            public CandidateReason CandidateReason;
            public ImmutableArray<ISymbol> CandidateSymbols = ImmutableArray.Create<ISymbol>();
            public ITypeSymbol Type;
            public NullabilityInfo Nullability;
            public ITypeSymbol ConvertedType;
            public NullabilityInfo ConvertedNullability;
            public Conversion ImplicitConversion = default(Conversion);
            public IAliasSymbol Alias;
            public Optional<object> ConstantValue = default(Optional<object>);
            public bool IsCompileTimeConstant { get { return ConstantValue.HasValue; } }
            public ImmutableArray<ISymbol> MemberGroup = ImmutableArray.Create<ISymbol>();
 
            public ImmutableArray<IMethodSymbol> MethodGroup
            {
                get { return this.MemberGroup.WhereAsArray(s => s.Kind == SymbolKind.Method).SelectAsArray(s => (IMethodSymbol)s); }
            }
        }
 
        public static SemanticInfoSummary GetSemanticInfoSummary(this SemanticModel semanticModel, SyntaxNode node)
        {
            SemanticInfoSummary summary = new SemanticInfoSummary();
 
            // The information that is available varies by the type of the syntax node.
 
            SymbolInfo symbolInfo = SymbolInfo.None;
            if (node is ExpressionSyntax expr)
            {
                symbolInfo = semanticModel.GetSymbolInfo(expr);
                summary.ConstantValue = semanticModel.GetConstantValue(expr);
                var typeInfo = semanticModel.GetTypeInfo(expr);
                summary.Type = typeInfo.Type;
                summary.ConvertedType = typeInfo.ConvertedType;
                summary.Nullability = typeInfo.Nullability;
                summary.ConvertedNullability = typeInfo.ConvertedNullability;
                summary.ImplicitConversion = semanticModel.GetConversion(expr);
                summary.MemberGroup = semanticModel.GetMemberGroup(expr);
            }
            else if (node is AttributeSyntax attribute)
            {
                symbolInfo = semanticModel.GetSymbolInfo(attribute);
                var typeInfo = semanticModel.GetTypeInfo(attribute);
                summary.Type = typeInfo.Type;
                summary.ConvertedType = typeInfo.ConvertedType;
                summary.ImplicitConversion = semanticModel.GetConversion(attribute);
                summary.MemberGroup = semanticModel.GetMemberGroup(attribute);
            }
            else if (node is OrderingSyntax ordering)
            {
                symbolInfo = semanticModel.GetSymbolInfo(ordering);
            }
            else if (node is SelectOrGroupClauseSyntax selectOrGroupClause)
            {
                symbolInfo = semanticModel.GetSymbolInfo(selectOrGroupClause);
            }
            else if (node is ConstructorInitializerSyntax initializer)
            {
                symbolInfo = semanticModel.GetSymbolInfo(initializer);
                var typeInfo = semanticModel.GetTypeInfo(initializer);
                summary.Type = typeInfo.Type;
                summary.ConvertedType = typeInfo.ConvertedType;
                summary.ImplicitConversion = semanticModel.GetConversion(initializer);
                summary.MemberGroup = semanticModel.GetMemberGroup(initializer);
            }
            else if (node is PatternSyntax pattern)
            {
                symbolInfo = semanticModel.GetSymbolInfo(pattern);
                var typeInfo = semanticModel.GetTypeInfo(pattern);
                summary.Type = typeInfo.Type;
                summary.ConvertedType = typeInfo.ConvertedType;
                summary.Nullability = typeInfo.Nullability;
                summary.ConvertedNullability = typeInfo.ConvertedNullability;
                summary.ImplicitConversion = semanticModel.GetConversion(pattern);
                summary.MemberGroup = semanticModel.GetMemberGroup(pattern);
            }
            else
            {
                throw ExceptionUtilities.UnexpectedValue(node);
            }
 
            summary.Symbol = symbolInfo.Symbol;
            summary.CandidateReason = symbolInfo.CandidateReason;
            summary.CandidateSymbols = symbolInfo.CandidateSymbols;
 
            if (node is IdentifierNameSyntax identifier)
            {
                summary.Alias = semanticModel.GetAliasInfo(identifier);
            }
 
            return summary;
        }
 
        public static SemanticInfoSummary GetSpeculativeSemanticInfoSummary(this SemanticModel semanticModel, int position, SyntaxNode node, SpeculativeBindingOption bindingOption)
        {
            SemanticInfoSummary summary = new SemanticInfoSummary();
 
            // The information that is available varies by the type of the syntax node.
 
            SymbolInfo symbolInfo = new SymbolInfo();
            if (node is ExpressionSyntax)
            {
                ExpressionSyntax expr = (ExpressionSyntax)node;
                symbolInfo = semanticModel.GetSpeculativeSymbolInfo(position, expr, bindingOption);
                //summary.ConstantValue = semanticModel.GetSpeculativeConstantValue(expr);
                var typeInfo = semanticModel.GetSpeculativeTypeInfo(position, expr, bindingOption);
                summary.Type = typeInfo.Type;
                summary.ConvertedType = typeInfo.ConvertedType;
                summary.Nullability = typeInfo.Nullability;
                summary.ConvertedNullability = typeInfo.ConvertedNullability;
                summary.ImplicitConversion = semanticModel.GetSpeculativeConversion(position, expr, bindingOption);
                //summary.MethodGroup = semanticModel.GetSpeculativeMethodGroup(expr);
            }
            else if (node is ConstructorInitializerSyntax)
            {
                var initializer = (ConstructorInitializerSyntax)node;
                symbolInfo = semanticModel.GetSpeculativeSymbolInfo(position, initializer);
            }
            else
            {
                throw new NotSupportedException("Type of syntax node is not supported by GetSemanticInfoSummary");
            }
 
            summary.Symbol = symbolInfo.Symbol;
            summary.CandidateReason = symbolInfo.CandidateReason;
            summary.CandidateSymbols = symbolInfo.CandidateSymbols;
 
            if (node is IdentifierNameSyntax)
            {
                summary.Alias = semanticModel.GetSpeculativeAliasInfo(position, (IdentifierNameSyntax)node, bindingOption);
            }
 
            return summary;
        }
 
        public static List<string> LookupNames(this SemanticModel model, int position, INamespaceOrTypeSymbol container = null, bool namespacesAndTypesOnly = false, bool useBaseReferenceAccessibility = false)
        {
            Assert.True(!useBaseReferenceAccessibility || (object)container == null);
            Assert.True(!useBaseReferenceAccessibility || !namespacesAndTypesOnly);
            var symbols = useBaseReferenceAccessibility
                ? model.LookupBaseMembers(position)
                : namespacesAndTypesOnly
                    ? model.LookupNamespacesAndTypes(position, container)
                    : model.LookupSymbols(position, container);
            return symbols.Select(s => s.Name).Distinct().ToList();
        }
 
        internal static TypeInfo GetTypeInfoAndVerifyIOperation(this SemanticModel model, SyntaxNode expression)
        {
            var typeInfo = model.GetTypeInfo(expression);
            var iop = getOperation(model, expression);
            if (typeInfo.Type is null)
            {
                assertTypeInfoNull(iop, typeInfo);
            }
            else if (iop is { Type: { } })
            {
                Assert.Equal(typeInfo.Type.NullableAnnotation, iop.Type.NullableAnnotation);
            }
            else
            {
                Assert.True(isValidDeclaration(expression));
 
                static bool isValidDeclaration(SyntaxNode expression)
                    => (expression.Parent is VariableDeclarationSyntax decl && decl.Type == expression) ||
                       (expression.Parent is ForEachStatementSyntax forEach && forEach.Type == expression) ||
                       (expression.Parent is DeclarationExpressionSyntax declExpr && declExpr.Type == expression) ||
                       (expression.Parent is RefTypeSyntax refType && isValidDeclaration(refType));
            }
 
            if (iop is { Parent: IConversionOperation parentConversion })
            {
                iop = parentConversion;
            }
 
            if (typeInfo.ConvertedType is null)
            {
                Assert.Null(iop?.Type);
            }
            else if (iop is { Type: { } })
            {
                Assert.Equal(typeInfo.ConvertedType.NullableAnnotation, iop.Type.NullableAnnotation);
            }
 
            return typeInfo;
 
            static IOperation getOperation(SemanticModel model, SyntaxNode expression)
            {
                while (true)
                {
                    // Nullable suppressions and parenthesized expressions are not directly represented in the bound tree.
                    // Rather, they are set as flags on the bound node underlying the node. Therefore, there is similarly
                    // no representation in the IOperation tree, and we should retrieve the IOperation node underlying
                    // the expression.
                    switch (expression)
                    {
                        case PostfixUnaryExpressionSyntax { RawKind: (int)SyntaxKind.SuppressNullableWarningExpression, Operand: { } operand }:
                            expression = operand;
                            continue;
 
                        case ParenthesizedExpressionSyntax { Expression: { } nested }:
                            expression = nested;
                            continue;
 
                        default:
                            goto getOperation;
                    }
                }
 
getOperation:
                return model.GetOperation(expression);
            }
 
            static void assertTypeInfoNull(IOperation iop, TypeInfo typeInfo)
            {
                switch (iop)
                {
                    // For both of these types, their `IOperation.Type` property represents the converted type,
                    // because any conversions that need to occur are pushed into the branches. However, the
                    // `TypeInfo.Type` property represents the natural type of the switch expression.
                    case ITupleOperation { NaturalType: null }:
                    case ISwitchExpressionOperation _:
                        Assert.True(iop.Type?.NullableAnnotation == typeInfo.ConvertedType?.NullableAnnotation);
                        break;
 
                    default:
                        Assert.Null(iop?.Type);
                        break;
                }
            }
        }
 
        /// <summary>
        /// Verify the type and nullability inferred by NullabilityWalker of all expressions in the source
        /// that are followed by specific annotations. Annotations are of the form /*T:type*/.
        /// </summary>
        internal static void VerifyTypes(this CSharpCompilation compilation, SyntaxTree tree = null)
        {
            if (tree == null)
            {
                foreach (var syntaxTree in compilation.SyntaxTrees)
                {
                    VerifyTypes(compilation, syntaxTree);
                }
 
                return;
            }
 
            Assert.True(compilation.IsNullableAnalysisEnabledIn((CSharpSyntaxTree)tree, new TextSpan(0, tree.Length)));
 
            var root = tree.GetRoot();
            var allAnnotations = getAnnotations();
            if (allAnnotations.IsEmpty)
            {
                return;
            }
 
            var model = compilation.GetSemanticModel(tree);
            var annotationsByMethod = allAnnotations.GroupBy(annotation => annotation.Expression.Ancestors().OfType<BaseMethodDeclarationSyntax>().First()).ToArray();
            foreach (var annotations in annotationsByMethod)
            {
                var methodSyntax = annotations.Key;
                var method = model.GetDeclaredSymbol(methodSyntax);
 
                var expectedTypes = annotations.SelectAsArray(annotation => annotation.Text);
                var actualTypes = annotations.SelectAsArray(annotation =>
                    {
                        var typeInfo = model.GetTypeInfoAndVerifyIOperation(annotation.Expression);
                        Assert.NotEqual(CodeAnalysis.NullableFlowState.None, typeInfo.Nullability.FlowState);
                        // https://github.com/dotnet/roslyn/issues/35035: After refactoring symboldisplay, we should be able to just call something like typeInfo.Type.ToDisplayString(typeInfo.Nullability.FlowState, TypeWithState.TestDisplayFormat)
                        var type = TypeWithState.Create(
                            (annotation.IsConverted ? typeInfo.ConvertedType : typeInfo.Type).GetSymbol(),
                            (annotation.IsConverted ? typeInfo.ConvertedNullability : typeInfo.Nullability).FlowState.ToInternalFlowState()).ToTypeWithAnnotations(compilation);
                        return type.ToDisplayString(TypeWithAnnotations.TestDisplayFormat);
                    });
                // Consider reporting the correct source with annotations on mismatch.
                AssertEx.Equal(expectedTypes, actualTypes, message: method.ToTestDisplayString());
            }
 
            ImmutableArray<(ExpressionSyntax Expression, string Text, bool IsConverted)> getAnnotations()
            {
                var builder = ArrayBuilder<(ExpressionSyntax, string, bool)>.GetInstance();
                foreach (var token in root.DescendantTokens())
                {
                    foreach (var trivia in token.TrailingTrivia)
                    {
                        if (trivia.Kind() == SyntaxKind.MultiLineCommentTrivia)
                        {
                            var text = trivia.ToFullString();
                            const string typePrefix = "/*T:";
                            const string convertedPrefix = "/*CT:";
                            const string suffix = "*/";
                            bool startsWithTypePrefix = text.StartsWith(typePrefix);
                            if (text.EndsWith(suffix) && (startsWithTypePrefix || text.StartsWith(convertedPrefix)))
                            {
                                var prefix = startsWithTypePrefix ? typePrefix : convertedPrefix;
                                var expr = getEnclosingExpression(token);
                                Assert.True(expr != null, $"VerifyTypes could not find a matching expression for annotation '{text}'.");
 
                                var content = text.Substring(prefix.Length, text.Length - prefix.Length - suffix.Length);
                                builder.Add((expr, content, !startsWithTypePrefix));
                            }
                        }
                    }
                }
                return builder.ToImmutableAndFree();
            }
 
            ExpressionSyntax getEnclosingExpression(SyntaxToken token)
            {
                var node = token.Parent;
                while (true)
                {
                    var expr = asExpression(node);
                    if (expr != null)
                    {
                        return expr;
                    }
                    if (node == root)
                    {
                        break;
                    }
                    node = node.Parent;
                }
                return null;
            }
 
            ExpressionSyntax asExpression(SyntaxNode node)
            {
                while (true)
                {
                    switch (node)
                    {
                        case null:
                            return null;
                        case ParenthesizedExpressionSyntax paren:
                            return paren.Expression;
                        case IdentifierNameSyntax id when id.Parent is MemberAccessExpressionSyntax memberAccess && memberAccess.Name == node:
                            node = memberAccess;
                            continue;
                        case ExpressionSyntax expr when expr.Parent is ConditionalAccessExpressionSyntax cond && cond.WhenNotNull == node:
                            node = cond;
                            continue;
                        case ExpressionSyntax expr:
                            return expr;
                        case { Parent: var parent }:
                            node = parent;
                            continue;
                    }
                }
            }
        }
    }
}