File: Symbols\Synthesized\Records\SynthesizedRecordPrintMembers.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.
 
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.Symbols
{
    /// <summary>
    /// The `bool PrintMembers(StringBuilder)` method is responsible for printing members declared
    /// in the containing type that are "printable" (public fields and properties),
    /// and delegating to the base to print inherited printable members. Base members get printed first.
    /// It returns true if the record contains some printable members.
    /// The method is used to implement `ToString()`.
    /// </summary>
    internal sealed class SynthesizedRecordPrintMembers : SynthesizedRecordOrdinaryMethod
    {
        public SynthesizedRecordPrintMembers(
            SourceMemberContainerTypeSymbol containingType,
            IEnumerable<Symbol> userDefinedMembers,
            int memberOffset)
            : base(
                  containingType,
                  WellKnownMemberNames.PrintMembersMethodName,
                  memberOffset: memberOffset,
                  MakeDeclarationModifiers(containingType, userDefinedMembers))
        {
        }
 
        private static DeclarationModifiers MakeDeclarationModifiers(SourceMemberContainerTypeSymbol containingType, IEnumerable<Symbol> userDefinedMembers)
        {
            var result = (containingType.IsRecordStruct || (containingType.BaseTypeNoUseSiteDiagnostics.IsObjectType() && containingType.IsSealed)) ?
                DeclarationModifiers.Private :
                DeclarationModifiers.Protected;
 
            if (containingType.IsRecord && !containingType.BaseTypeNoUseSiteDiagnostics.IsObjectType())
            {
                result |= DeclarationModifiers.Override;
            }
            else
            {
                result |= containingType.IsSealed ? DeclarationModifiers.None : DeclarationModifiers.Virtual;
            }
 
#if DEBUG
            Debug.Assert(modifiersAreValid(result));
#endif
            if (IsReadOnly(containingType, userDefinedMembers))
            {
                result |= DeclarationModifiers.ReadOnly;
            }
 
            return result;
 
#if DEBUG
            bool modifiersAreValid(DeclarationModifiers modifiers)
            {
                if (containingType.IsRecordStruct)
                {
                    return modifiers == DeclarationModifiers.Private;
                }
 
                if ((modifiers & DeclarationModifiers.AccessibilityMask) != DeclarationModifiers.Private &&
                    (modifiers & DeclarationModifiers.AccessibilityMask) != DeclarationModifiers.Protected)
                {
                    return false;
                }
 
                modifiers &= ~DeclarationModifiers.AccessibilityMask;
 
                switch (modifiers)
                {
                    case DeclarationModifiers.None:
                    case DeclarationModifiers.Override:
                    case DeclarationModifiers.Virtual:
                        return true;
                    default:
                        return false;
                }
            }
#endif
        }
 
        protected override (TypeWithAnnotations ReturnType, ImmutableArray<ParameterSymbol> Parameters) MakeParametersAndBindReturnType(BindingDiagnosticBag diagnostics)
        {
            var compilation = DeclaringCompilation;
            var location = ReturnTypeLocation;
            var annotation = ContainingType.IsRecordStruct ? NullableAnnotation.Oblivious : NullableAnnotation.NotAnnotated;
            return (ReturnType: TypeWithAnnotations.Create(Binder.GetSpecialType(compilation, SpecialType.System_Boolean, location, diagnostics)),
                    Parameters: ImmutableArray.Create<ParameterSymbol>(
                        new SourceSimpleParameterSymbol(owner: this,
                            TypeWithAnnotations.Create(Binder.GetWellKnownType(compilation, WellKnownType.System_Text_StringBuilder, diagnostics, location), annotation),
                            ordinal: 0, RefKind.None, "builder", Locations)));
        }
 
        protected override int GetParameterCountFromSyntax() => 1;
 
        protected override void MethodChecks(BindingDiagnosticBag diagnostics)
        {
            base.MethodChecks(diagnostics);
 
            var overridden = OverriddenMethod;
 
            if (overridden is object &&
                !overridden.ContainingType.Equals(ContainingType.BaseTypeNoUseSiteDiagnostics, TypeCompareKind.AllIgnoreOptions))
            {
                diagnostics.Add(ErrorCode.ERR_DoesNotOverrideBaseMethod, GetFirstLocation(), this, ContainingType.BaseTypeNoUseSiteDiagnostics);
            }
        }
 
        internal override void GenerateMethodBody(TypeCompilationState compilationState, BindingDiagnosticBag diagnostics)
        {
            var F = new SyntheticBoundNodeFactory(this, ContainingType.GetNonNullSyntaxNode(), compilationState, diagnostics);
            try
            {
                ImmutableArray<Symbol> printableMembers = ContainingType.GetMembers().WhereAsArray(m => isPrintable(m));
 
                if (ReturnType.IsErrorType() ||
                    printableMembers.Any(static m => m.GetTypeOrReturnType().Type.IsErrorType()))
                {
                    F.CloseMethod(F.ThrowNull());
                    return;
                }
 
                ArrayBuilder<BoundStatement> block;
                BoundParameter builder = F.Parameter(this.Parameters[0]);
                if (ContainingType.BaseTypeNoUseSiteDiagnostics.IsObjectType() || ContainingType.IsRecordStruct)
                {
                    if (printableMembers.IsEmpty)
                    {
                        // return false;
                        F.CloseMethod(F.Return(F.Literal(false)));
                        return;
                    }
                    block = ArrayBuilder<BoundStatement>.GetInstance();
 
                    if (!ContainingType.IsRecordStruct)
                    {
                        var ensureStackMethod = F.WellKnownMethod(
                            WellKnownMember.System_Runtime_CompilerServices_RuntimeHelpers__EnsureSufficientExecutionStack,
                            isOptional: true);
                        if (ensureStackMethod is not null)
                        {
                            block.Add(F.ExpressionStatement(
                                F.Call(receiver: null, ensureStackMethod)));
                        }
                    }
                }
                else
                {
                    MethodSymbol? basePrintMethod = OverriddenMethod;
                    if (basePrintMethod is null ||
                        basePrintMethod.ReturnType.SpecialType != SpecialType.System_Boolean)
                    {
                        F.CloseMethod(F.ThrowNull()); // an error was reported in base checks already
                        return;
                    }
 
                    var basePrintCall = F.Call(receiver: F.Base(ContainingType.BaseTypeNoUseSiteDiagnostics), basePrintMethod, builder);
                    if (printableMembers.IsEmpty)
                    {
                        // return base.PrintMembers(builder);
                        F.CloseMethod(F.Return(basePrintCall));
                        return;
                    }
                    else
                    {
                        block = ArrayBuilder<BoundStatement>.GetInstance();
                        // if (base.PrintMembers(builder))
                        //     builder.Append(", ")
                        block.Add(F.If(basePrintCall, makeAppendString(F, builder, ", ")));
                    }
                }
 
                Debug.Assert(!printableMembers.IsEmpty);
 
                for (var i = 0; i < printableMembers.Length; i++)
                {
                    // builder.Append(", <name> = "); // if previous members exist
                    // builder.Append("<name> = "); // if it is the first member
 
                    // The only printable members are fields and properties,
                    // which cannot be generic so as to have variant names
 
                    var member = printableMembers[i];
                    var memberHeader = $"{member.Name} = ";
                    if (i > 0)
                    {
                        memberHeader = ", " + memberHeader;
                    }
 
                    block.Add(makeAppendString(F, builder, memberHeader));
 
                    var value = member.Kind switch
                    {
                        SymbolKind.Field => F.Field(F.This(), (FieldSymbol)member),
                        SymbolKind.Property => F.Property(F.This(), (PropertySymbol)member),
                        _ => throw ExceptionUtilities.UnexpectedValue(member.Kind)
                    };
 
                    // builder.Append((object)<value>); OR builder.Append(<value>.ToString()); for value types
 
                    Debug.Assert(value.Type is not null);
                    if (value.Type.IsValueType)
                    {
                        block.Add(F.ExpressionStatement(
                            F.Call(receiver: builder,
                                F.WellKnownMethod(WellKnownMember.System_Text_StringBuilder__AppendString),
                                F.Call(value, F.SpecialMethod(SpecialMember.System_Object__ToString)))));
                    }
                    else if (!value.Type.IsRestrictedType())
                    {
                        // Otherwise, an error has been reported elsewhere (SourceMemberFieldSymbol.TypeChecks)
                        block.Add(F.ExpressionStatement(
                            F.Call(receiver: builder,
                                F.WellKnownMethod(WellKnownMember.System_Text_StringBuilder__AppendObject),
                                F.Convert(F.SpecialType(SpecialType.System_Object), value))));
                    }
                }
 
                block.Add(F.Return(F.Literal(true)));
 
                F.CloseMethod(F.Block(block.ToImmutableAndFree()));
            }
            catch (SyntheticBoundNodeFactory.MissingPredefinedMember ex)
            {
                diagnostics.Add(ex.Diagnostic);
                F.CloseMethod(F.ThrowNull());
            }
 
            static BoundStatement makeAppendString(SyntheticBoundNodeFactory F, BoundParameter builder, string value)
            {
                return F.ExpressionStatement(F.Call(receiver: builder, F.WellKnownMethod(WellKnownMember.System_Text_StringBuilder__AppendString), F.StringLiteral(value)));
            }
 
            static bool isPrintable(Symbol m)
            {
                if (!IsPublicInstanceMember(m))
                {
                    return false;
                }
 
                if (m.Kind is SymbolKind.Field && m is not TupleErrorFieldSymbol)
                {
                    return true;
                }
 
                if (m.Kind is SymbolKind.Property)
                {
                    var property = (PropertySymbol)m;
                    return IsPrintableProperty(property);
                }
 
                return false;
            }
        }
 
        internal static void VerifyOverridesPrintMembersFromBase(MethodSymbol overriding, BindingDiagnosticBag diagnostics)
        {
            NamedTypeSymbol baseType = overriding.ContainingType.BaseTypeNoUseSiteDiagnostics;
            if (baseType.IsObjectType())
            {
                return;
            }
 
            bool reportAnError = false;
 
            if (!overriding.IsOverride)
            {
                reportAnError = true;
            }
            else
            {
                var overridden = overriding.OverriddenMethod;
 
                if (overridden is object &&
                    !overridden.ContainingType.Equals(baseType, TypeCompareKind.AllIgnoreOptions))
                {
                    reportAnError = true;
                }
            }
 
            if (reportAnError)
            {
                diagnostics.Add(ErrorCode.ERR_DoesNotOverrideBaseMethod, overriding.GetFirstLocation(), overriding, baseType);
            }
        }
 
        private static bool IsReadOnly(NamedTypeSymbol containingType, IEnumerable<Symbol> userDefinedMembers)
        {
            return containingType.IsReadOnly || (containingType.IsRecordStruct && AreAllPrintablePropertyGettersReadOnly(userDefinedMembers));
        }
 
        private static bool AreAllPrintablePropertyGettersReadOnly(IEnumerable<Symbol> members)
        {
            foreach (var member in members)
            {
                if (member.Kind != SymbolKind.Property)
                {
                    continue;
                }
 
                var property = (PropertySymbol)member;
                if (!IsPublicInstanceMember(property) || !IsPrintableProperty(property))
                {
                    continue;
                }
 
                var getterMethod = property.GetMethod;
                if (property.GetMethod is not null && !getterMethod.IsEffectivelyReadOnly)
                {
                    return false;
                }
            }
 
            return true;
        }
 
        private static bool IsPublicInstanceMember(Symbol m)
        {
            return m.DeclaredAccessibility == Accessibility.Public && !m.IsStatic;
        }
 
        private static bool IsPrintableProperty(PropertySymbol property)
        {
            return !property.IsIndexer && !property.IsOverride && property.GetMethod is not null;
        }
    }
}