File: src\Workspaces\SharedUtilitiesAndExtensions\Workspace\CSharp\CodeGeneration\PropertyGenerator.cs
Web Access
Project: src\src\CodeStyle\CSharp\CodeFixes\Microsoft.CodeAnalysis.CSharp.CodeStyle.Fixes.csproj (Microsoft.CodeAnalysis.CSharp.CodeStyle.Fixes)
// 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.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.CodeGeneration;
 
using static CodeGenerationHelpers;
using static CSharpCodeGenerationHelpers;
using static CSharpSyntaxTokens;
using static SyntaxFactory;
 
internal static class PropertyGenerator
{
    public static bool CanBeGenerated(IPropertySymbol property)
        => property.IsIndexer || property.Parameters.Length == 0;
 
    private static MemberDeclarationSyntax? LastPropertyOrField(
        SyntaxList<MemberDeclarationSyntax> members)
    {
        var lastProperty = members.LastOrDefault(m => m is PropertyDeclarationSyntax);
        return lastProperty ?? LastField(members);
    }
 
    internal static CompilationUnitSyntax AddPropertyTo(
        CompilationUnitSyntax destination,
        IPropertySymbol property,
        CSharpCodeGenerationContextInfo info,
        IList<bool>? availableIndices,
        CancellationToken cancellationToken)
    {
        var declaration = GeneratePropertyOrIndexer(
            property, CodeGenerationDestination.CompilationUnit, info,
            cancellationToken);
 
        var members = Insert(destination.Members, declaration, info,
            availableIndices, after: LastPropertyOrField, before: FirstMember);
        return destination.WithMembers(members);
    }
 
    internal static TypeDeclarationSyntax AddPropertyTo(
        TypeDeclarationSyntax destination,
        IPropertySymbol property,
        CSharpCodeGenerationContextInfo info,
        IList<bool>? availableIndices,
        CancellationToken cancellationToken)
    {
        var declaration = GeneratePropertyOrIndexer(property, GetDestination(destination), info, cancellationToken);
 
        // Create a clone of the original type with the new method inserted. 
        var members = Insert(destination.Members, declaration, info,
            availableIndices, after: LastPropertyOrField, before: FirstMember);
 
        // Find the best place to put the field.  It should go after the last field if we already
        // have fields, or at the beginning of the file if we don't.
        return AddMembersTo(destination, members, cancellationToken);
    }
 
    public static MemberDeclarationSyntax GeneratePropertyOrIndexer(
        IPropertySymbol property,
        CodeGenerationDestination destination,
        CSharpCodeGenerationContextInfo info,
        CancellationToken cancellationToken)
    {
        var reusableSyntax = GetReuseableSyntaxNodeForSymbol<MemberDeclarationSyntax>(property, info);
        if (reusableSyntax != null)
        {
            return reusableSyntax;
        }
 
        var declaration = property.IsIndexer
            ? GenerateIndexerDeclaration(property, destination, info, cancellationToken)
            : GeneratePropertyDeclaration(property, destination, info, cancellationToken);
 
        return ConditionallyAddDocumentationCommentTo(declaration, property, info, cancellationToken);
    }
 
    private static MemberDeclarationSyntax GenerateIndexerDeclaration(
        IPropertySymbol property,
        CodeGenerationDestination destination,
        CSharpCodeGenerationContextInfo info,
        CancellationToken cancellationToken)
    {
        var explicitInterfaceSpecifier = GenerateExplicitInterfaceSpecifier(property.ExplicitInterfaceImplementations);
 
        var isExplicit = explicitInterfaceSpecifier is not null;
        var declaration = IndexerDeclaration(
                attributeLists: GenerateAttributes(property, isExplicit, info),
                modifiers: GenerateModifiers(property, destination, info),
                type: GenerateTypeSyntax(property),
                explicitInterfaceSpecifier: explicitInterfaceSpecifier,
                parameterList: ParameterGenerator.GenerateBracketedParameterList(property.Parameters, isExplicit: isExplicit, info),
                accessorList: GenerateAccessorList(property, destination, info, cancellationToken));
        declaration = UseExpressionBodyIfDesired(info, declaration, cancellationToken);
 
        return AddFormatterAndCodeGeneratorAnnotationsTo(
            AddAnnotationsTo(property, declaration));
    }
 
    private static PropertyDeclarationSyntax GeneratePropertyDeclaration(
       IPropertySymbol property, CodeGenerationDestination destination,
       CSharpCodeGenerationContextInfo info, CancellationToken cancellationToken)
    {
        var initializer = CodeGenerationPropertyInfo.GetInitializer(property) is ExpressionSyntax initializerNode
            ? EqualsValueClause(initializerNode)
            : null;
 
        var explicitInterfaceSpecifier = GenerateExplicitInterfaceSpecifier(property.ExplicitInterfaceImplementations);
 
        var isExplicit = explicitInterfaceSpecifier is not null;
        var accessorList = GenerateAccessorList(property, destination, info, cancellationToken);
 
        var propertyDeclaration = PropertyDeclaration(
            attributeLists: GenerateAttributes(property, isExplicit, info),
            modifiers: GenerateModifiers(property, destination, info),
            type: GenerateTypeSyntax(property),
            explicitInterfaceSpecifier: explicitInterfaceSpecifier,
            identifier: property.Name.ToIdentifierToken(),
            accessorList: accessorList,
            expressionBody: null,
            initializer: initializer,
            semicolonToken: initializer is null ? default : SemicolonToken);
 
        propertyDeclaration = UseExpressionBodyIfDesired(info, propertyDeclaration, cancellationToken);
 
        return AddFormatterAndCodeGeneratorAnnotationsTo(
            AddAnnotationsTo(property, propertyDeclaration));
    }
 
    private static SyntaxList<AttributeListSyntax> GenerateAttributes(
        IPropertySymbol property, bool isExplicit, CSharpCodeGenerationContextInfo info)
    {
        if (isExplicit)
        {
            return default;
        }
 
        var attributes = property.GetAttributes();
        if (attributes.Length == 0)
        {
            return default;
        }
 
        return AttributeGenerator.GenerateAttributeLists(attributes, info);
    }
 
    private static TypeSyntax GenerateTypeSyntax(IPropertySymbol property)
    {
        var returnType = property.Type;
 
        if (property.ReturnsByRef)
        {
            return returnType.GenerateRefTypeSyntax();
        }
        else if (property.ReturnsByRefReadonly)
        {
            return returnType.GenerateRefReadOnlyTypeSyntax();
        }
        else
        {
            return returnType.GenerateTypeSyntax();
        }
    }
 
    private static bool TryGetExpressionBody(
        BasePropertyDeclarationSyntax baseProperty, LanguageVersion languageVersion, ExpressionBodyPreference preference, CancellationToken cancellationToken,
        [NotNullWhen(true)] out ArrowExpressionClauseSyntax? arrowExpression, out SyntaxToken semicolonToken)
    {
        var accessorList = baseProperty.AccessorList;
        if (preference != ExpressionBodyPreference.Never &&
            accessorList?.Accessors.Count == 1)
        {
            var accessor = accessorList.Accessors[0];
            if (accessor.IsKind(SyntaxKind.GetAccessorDeclaration))
            {
                return TryGetArrowExpressionBody(
                    baseProperty.Kind(), accessor, languageVersion, preference, cancellationToken,
                    out arrowExpression, out semicolonToken);
            }
        }
 
        arrowExpression = null;
        semicolonToken = default;
        return false;
    }
 
    private static PropertyDeclarationSyntax UseExpressionBodyIfDesired(
        CSharpCodeGenerationContextInfo info, PropertyDeclarationSyntax declaration, CancellationToken cancellationToken)
    {
        if (declaration.ExpressionBody == null &&
            declaration.Initializer == null &&
            TryGetExpressionBody(
                declaration, info.LanguageVersion, info.Options.PreferExpressionBodiedProperties.Value, cancellationToken,
                out var expressionBody, out var semicolonToken))
        {
            declaration = declaration.WithAccessorList(null)
                                     .WithExpressionBody(expressionBody)
                                     .WithSemicolonToken(semicolonToken);
        }
 
        return declaration;
    }
 
    private static IndexerDeclarationSyntax UseExpressionBodyIfDesired(
        CSharpCodeGenerationContextInfo info, IndexerDeclarationSyntax declaration, CancellationToken cancellationToken)
    {
        if (declaration.ExpressionBody == null)
        {
            if (TryGetExpressionBody(
                    declaration, info.LanguageVersion, info.Options.PreferExpressionBodiedIndexers.Value, cancellationToken,
                    out var expressionBody, out var semicolonToken))
            {
                declaration = declaration.WithAccessorList(null)
                                         .WithExpressionBody(expressionBody)
                                         .WithSemicolonToken(semicolonToken);
            }
        }
 
        return declaration;
    }
 
    private static AccessorDeclarationSyntax UseExpressionBodyIfDesired(
        CSharpCodeGenerationContextInfo info, AccessorDeclarationSyntax declaration, CancellationToken cancellationToken)
    {
        if (declaration.ExpressionBody == null)
        {
            if (declaration.Body?.TryConvertToArrowExpressionBody(
                declaration.Kind(), info.LanguageVersion, info.Options.PreferExpressionBodiedAccessors.Value, cancellationToken,
                out var expressionBody, out var semicolonToken) == true)
            {
                declaration = declaration.WithBody(null)
                                         .WithExpressionBody(expressionBody)
                                         .WithSemicolonToken(semicolonToken);
            }
        }
 
        return declaration;
    }
 
    private static bool TryGetArrowExpressionBody(
        SyntaxKind declarationKind, AccessorDeclarationSyntax accessor, LanguageVersion languageVersion, ExpressionBodyPreference preference, CancellationToken cancellationToken,
        [NotNullWhen(true)] out ArrowExpressionClauseSyntax? arrowExpression, out SyntaxToken semicolonToken)
    {
        // If the accessor has an expression body already, then use that as the expression body
        // for the property.
        if (accessor.ExpressionBody != null)
        {
            arrowExpression = accessor.ExpressionBody;
            semicolonToken = accessor.SemicolonToken;
            return true;
        }
 
        if (accessor.Body == null)
        {
            arrowExpression = null;
            semicolonToken = default;
            return false;
        }
 
        return accessor.Body.TryConvertToArrowExpressionBody(
            declarationKind, languageVersion, preference, cancellationToken, out arrowExpression, out semicolonToken);
    }
 
    private static AccessorListSyntax? GenerateAccessorList(
        IPropertySymbol property, CodeGenerationDestination destination,
        CSharpCodeGenerationContextInfo info, CancellationToken cancellationToken)
    {
        var setAccessorKind = property.SetMethod?.IsInitOnly == true ? SyntaxKind.InitAccessorDeclaration : SyntaxKind.SetAccessorDeclaration;
        var accessors = new[]
        {
            GenerateAccessorDeclaration(property, property.GetMethod, SyntaxKind.GetAccessorDeclaration, destination, info, cancellationToken),
            GenerateAccessorDeclaration(property, property.SetMethod, setAccessorKind, destination, info, cancellationToken),
        };
 
        return accessors[0] == null && accessors[1] == null
            ? null
            : AccessorList([.. accessors.WhereNotNull()]);
    }
 
    private static AccessorDeclarationSyntax? GenerateAccessorDeclaration(
        IPropertySymbol property,
        IMethodSymbol? accessor,
        SyntaxKind kind,
        CodeGenerationDestination destination,
        CSharpCodeGenerationContextInfo info,
        CancellationToken cancellationToken)
    {
        var hasBody = info.Context.GenerateMethodBodies && HasAccessorBodies(property, destination, accessor);
        return accessor == null
            ? null
            : GenerateAccessorDeclaration(property, accessor, kind, hasBody, info, cancellationToken);
    }
 
    private static AccessorDeclarationSyntax GenerateAccessorDeclaration(
        IPropertySymbol property,
        IMethodSymbol accessor,
        SyntaxKind kind,
        bool hasBody,
        CSharpCodeGenerationContextInfo info,
        CancellationToken cancellationToken)
    {
        var declaration = AccessorDeclaration(kind)
                                       .WithModifiers(GenerateAccessorModifiers(property, accessor, info))
                                       .WithBody(hasBody ? GenerateBlock(accessor) : null)
                                       .WithSemicolonToken(hasBody ? default : SemicolonToken);
 
        declaration = UseExpressionBodyIfDesired(info, declaration, cancellationToken);
 
        return AddAnnotationsTo(accessor, declaration);
    }
 
    private static BlockSyntax GenerateBlock(IMethodSymbol accessor)
    {
        return Block(
            StatementGenerator.GenerateStatements(CodeGenerationMethodInfo.GetStatements(accessor)));
    }
 
    private static bool HasAccessorBodies(
        IPropertySymbol property,
        CodeGenerationDestination destination,
        IMethodSymbol? accessor)
    {
        return destination != CodeGenerationDestination.InterfaceType &&
            !property.IsAbstract &&
            accessor != null &&
            !accessor.IsAbstract;
    }
 
    private static SyntaxTokenList GenerateAccessorModifiers(
        IPropertySymbol property,
        IMethodSymbol accessor,
        CSharpCodeGenerationContextInfo info)
    {
        using var _ = ArrayBuilder<SyntaxToken>.GetInstance(out var modifiers);
 
        if (accessor.DeclaredAccessibility != Accessibility.NotApplicable &&
            accessor.DeclaredAccessibility != property.DeclaredAccessibility)
        {
            AddAccessibilityModifiers(accessor.DeclaredAccessibility, modifiers, info, property.DeclaredAccessibility);
        }
 
        var hasNonReadOnlyAccessor = property.GetMethod?.IsReadOnly == false || property.SetMethod?.IsReadOnly == false;
        if (hasNonReadOnlyAccessor && accessor.IsReadOnly)
            modifiers.Add(ReadOnlyKeyword);
 
        return [.. modifiers];
    }
 
    private static SyntaxTokenList GenerateModifiers(
        IPropertySymbol property, CodeGenerationDestination destination, CSharpCodeGenerationContextInfo info)
    {
        var tokens = ArrayBuilder<SyntaxToken>.GetInstance();
 
        // Only "static" allowed if we're an explicit impl.
        if (property.ExplicitInterfaceImplementations.Any())
        {
            if (property.IsStatic)
                tokens.Add(StaticKeyword);
        }
        else
        {
            // If we're generating into an interface, then allow modifiers for static abstract members
            if (destination is CodeGenerationDestination.InterfaceType)
            {
                if (property.IsStatic)
                {
                    tokens.Add(StaticKeyword);
 
                    if (property.IsAbstract)
                        tokens.Add(AbstractKeyword);
                }
            }
            else if (destination is not CodeGenerationDestination.CompilationUnit)
            {
                AddAccessibilityModifiers(property.DeclaredAccessibility, tokens, info, Accessibility.Private);
 
                if (property.IsStatic)
                    tokens.Add(StaticKeyword);
 
                // note: explicit interface impls are allowed to be 'readonly' but it never actually affects callers
                // because of the boxing requirement in order to call the method.
                // therefore it seems like a small oversight to leave out the keyword for an explicit impl from metadata.
                var hasAllReadOnlyAccessors = property.GetMethod?.IsReadOnly != false && property.SetMethod?.IsReadOnly != false;
 
                // Don't show the readonly modifier if the containing type is already readonly
                if (hasAllReadOnlyAccessors && !property.ContainingType.IsReadOnly)
                    tokens.Add(ReadOnlyKeyword);
 
                if (property.IsSealed)
                    tokens.Add(SealedKeyword);
 
                if (property.IsOverride)
                    tokens.Add(OverrideKeyword);
 
                if (property.IsVirtual)
                    tokens.Add(VirtualKeyword);
 
                if (property.IsAbstract)
                    tokens.Add(AbstractKeyword);
 
                if (property.IsRequired)
                    tokens.Add(RequiredKeyword);
            }
        }
 
        if (CodeGenerationPropertyInfo.GetIsUnsafe(property))
            tokens.Add(UnsafeKeyword);
 
        return [.. tokens];
    }
}