File: UseExpressionBodyHelper`1.cs
Web Access
Project: ..\..\..\src\Features\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.Features)
// 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;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.CodeGeneration;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Options;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody
{
    /// <summary>
    /// Helper class that allows us to share lots of logic between the diagnostic analyzer and the
    /// code refactoring provider.  Those can't share a common base class due to their own inheritance
    /// requirements with <see cref="DiagnosticAnalyzer"/> and "CodeRefactoringProvider".
    /// </summary>
    internal abstract class UseExpressionBodyHelper<TDeclaration> : UseExpressionBodyHelper
        where TDeclaration : SyntaxNode
    {
        public override Option2<CodeStyleOption2<ExpressionBodyPreference>> Option { get; }
        public override LocalizableString UseExpressionBodyTitle { get; }
        public override LocalizableString UseBlockBodyTitle { get; }
        public override string DiagnosticId { get; }
        public override EnforceOnBuild EnforceOnBuild { get; }
        public override ImmutableArray<SyntaxKind> SyntaxKinds { get; }
 
        protected UseExpressionBodyHelper(
            string diagnosticId,
            EnforceOnBuild enforceOnBuild,
            LocalizableString useExpressionBodyTitle,
            LocalizableString useBlockBodyTitle,
            Option2<CodeStyleOption2<ExpressionBodyPreference>> option,
            ImmutableArray<SyntaxKind> syntaxKinds)
        {
            DiagnosticId = diagnosticId;
            EnforceOnBuild = enforceOnBuild;
            Option = option;
            UseExpressionBodyTitle = useExpressionBodyTitle;
            UseBlockBodyTitle = useBlockBodyTitle;
            SyntaxKinds = syntaxKinds;
        }
 
        protected static AccessorDeclarationSyntax? GetSingleGetAccessor(AccessorListSyntax? accessorList)
        {
            return accessorList is { Accessors: [{ AttributeLists.Count: 0, RawKind: (int)SyntaxKind.GetAccessorDeclaration } accessor] }
                ? accessor
                : null;
        }
 
        protected static BlockSyntax? GetBodyFromSingleGetAccessor(AccessorListSyntax accessorList)
            => GetSingleGetAccessor(accessorList)?.Body;
 
        public override BlockSyntax? GetBody(SyntaxNode declaration)
            => GetBody((TDeclaration)declaration);
 
        public override ArrowExpressionClauseSyntax? GetExpressionBody(SyntaxNode declaration)
            => GetExpressionBody((TDeclaration)declaration);
 
        public override bool IsRelevantDeclarationNode(SyntaxNode node)
            => node is TDeclaration;
 
        public override bool CanOfferUseExpressionBody(CodeStyleOption2<ExpressionBodyPreference> preference, SyntaxNode declaration, bool forAnalyzer)
            => CanOfferUseExpressionBody(preference, (TDeclaration)declaration, forAnalyzer);
 
        public override bool CanOfferUseBlockBody(CodeStyleOption2<ExpressionBodyPreference> preference, SyntaxNode declaration, bool forAnalyzer, out bool fixesError, [NotNullWhen(true)] out ArrowExpressionClauseSyntax? expressionBody)
            => CanOfferUseBlockBody(preference, (TDeclaration)declaration, forAnalyzer, out fixesError, out expressionBody);
 
        public sealed override SyntaxNode Update(SemanticModel semanticModel, SyntaxNode declaration, bool useExpressionBody)
            => Update(semanticModel, (TDeclaration)declaration, useExpressionBody);
 
        public override Location GetDiagnosticLocation(SyntaxNode declaration)
            => GetDiagnosticLocation((TDeclaration)declaration);
 
        protected virtual Location GetDiagnosticLocation(TDeclaration declaration)
        {
            var body = GetBody(declaration);
            Contract.ThrowIfNull(body);
            return body.Statements[0].GetLocation();
        }
 
        public bool CanOfferUseExpressionBody(
            CodeStyleOption2<ExpressionBodyPreference> preference, TDeclaration declaration, bool forAnalyzer)
        {
            var userPrefersExpressionBodies = preference.Value != ExpressionBodyPreference.Never;
            var analyzerDisabled = preference.Notification.Severity == ReportDiagnostic.Suppress;
 
            // If the user likes expression bodies, then we offer expression bodies from the diagnostic analyzer.
            // If the user does not like expression bodies then we offer expression bodies from the refactoring provider.
            // If the analyzer is disabled completely, the refactoring is enabled in both directions.
            if (userPrefersExpressionBodies == forAnalyzer || (!forAnalyzer && analyzerDisabled))
            {
                var expressionBody = GetExpressionBody(declaration);
                if (expressionBody == null)
                {
                    // They don't have an expression body.  See if we could convert the block they
                    // have into one.
 
                    var conversionPreference = forAnalyzer ? preference.Value : ExpressionBodyPreference.WhenPossible;
 
                    return TryConvertToExpressionBody(declaration, conversionPreference,
                        expressionWhenOnSingleLine: out _, semicolonWhenOnSingleLine: out _);
                }
            }
 
            return false;
        }
 
        protected virtual bool TryConvertToExpressionBody(
            TDeclaration declaration,
            ExpressionBodyPreference conversionPreference,
            [NotNullWhen(true)] out ArrowExpressionClauseSyntax? expressionWhenOnSingleLine,
            out SyntaxToken semicolonWhenOnSingleLine)
        {
            return TryConvertToExpressionBodyWorker(
                declaration, conversionPreference,
                out expressionWhenOnSingleLine, out semicolonWhenOnSingleLine);
        }
 
        private bool TryConvertToExpressionBodyWorker(
            SyntaxNode declaration, ExpressionBodyPreference conversionPreference,
            [NotNullWhen(true)] out ArrowExpressionClauseSyntax? expressionWhenOnSingleLine, out SyntaxToken semicolonWhenOnSingleLine)
        {
            var body = GetBody(declaration);
            if (body is null)
            {
                expressionWhenOnSingleLine = null;
                semicolonWhenOnSingleLine = default;
                return false;
            }
 
            var languageVersion = body.SyntaxTree.Options.LanguageVersion();
 
            return body.TryConvertToArrowExpressionBody(
                declaration.Kind(), languageVersion, conversionPreference,
                out expressionWhenOnSingleLine, out semicolonWhenOnSingleLine);
        }
 
        protected bool TryConvertToExpressionBodyForBaseProperty(
            BasePropertyDeclarationSyntax declaration,
            ExpressionBodyPreference conversionPreference,
            [NotNullWhen(true)] out ArrowExpressionClauseSyntax? arrowExpression,
            out SyntaxToken semicolonToken)
        {
            if (TryConvertToExpressionBodyWorker(declaration, conversionPreference, out arrowExpression, out semicolonToken))
            {
                return true;
            }
 
            var getAccessor = GetSingleGetAccessor(declaration.AccessorList);
            if (getAccessor?.ExpressionBody != null &&
                BlockSyntaxExtensions.MatchesPreference(getAccessor.ExpressionBody.Expression, conversionPreference))
            {
                arrowExpression = SyntaxFactory.ArrowExpressionClause(getAccessor.ExpressionBody.Expression);
                semicolonToken = getAccessor.SemicolonToken;
                return true;
            }
 
            return false;
        }
 
        public bool CanOfferUseBlockBody(
            CodeStyleOption2<ExpressionBodyPreference> preference,
            TDeclaration declaration,
            bool forAnalyzer,
            out bool fixesError,
            [NotNullWhen(true)] out ArrowExpressionClauseSyntax? expressionBody)
        {
            var userPrefersBlockBodies = preference.Value == ExpressionBodyPreference.Never;
            var analyzerDisabled = preference.Notification.Severity == ReportDiagnostic.Suppress;
 
            expressionBody = GetExpressionBody(declaration);
            if (expressionBody?.TryConvertToBlock(
                SyntaxFactory.Token(SyntaxKind.SemicolonToken), false, block: out _) != true)
            {
                fixesError = false;
                return false;
            }
 
            var languageVersion = declaration.GetLanguageVersion();
            if (languageVersion < LanguageVersion.CSharp7)
            {
                if (expressionBody!.Expression.IsKind(SyntaxKind.ThrowExpression))
                {
                    // If they're using a throw expression in a declaration and it's prior to C# 7
                    // then always mark this as something that can be fixed by the analyzer.  This way
                    // we'll also get 'fix all' working to fix all these cases.
                    fixesError = true;
                    return true;
                }
 
                if (declaration is AccessorDeclarationSyntax or ConstructorDeclarationSyntax)
                {
                    // If they're using expression bodies for accessors/constructors and it's prior to C# 7
                    // then always mark this as something that can be fixed by the analyzer.  This way
                    // we'll also get 'fix all' working to fix all these cases.
                    fixesError = true;
                    return true;
                }
            }
 
            if (languageVersion < LanguageVersion.CSharp6)
            {
                // If they're using expression bodies prior to C# 6, then always mark this as something
                // that can be fixed by the analyzer.  This way we'll also get 'fix all' working to fix
                // all these cases.
                fixesError = true;
                return true;
            }
 
            // If the user likes block bodies, then we offer block bodies from the diagnostic analyzer.
            // If the user does not like block bodies then we offer block bodies from the refactoring provider.
            // If the analyzer is disabled completely, the refactoring is enabled in both directions.
            fixesError = false;
            return userPrefersBlockBodies == forAnalyzer || (!forAnalyzer && analyzerDisabled);
        }
 
        public TDeclaration Update(SemanticModel semanticModel, TDeclaration declaration, bool useExpressionBody)
        {
            if (useExpressionBody)
            {
                TryConvertToExpressionBody(declaration, ExpressionBodyPreference.WhenPossible, out var expressionBody, out var semicolonToken);
 
                var trailingTrivia = semicolonToken.TrailingTrivia
                                                   .Where(t => t.Kind() != SyntaxKind.EndOfLineTrivia)
                                                   .Concat(declaration.GetTrailingTrivia());
                semicolonToken = semicolonToken.WithTrailingTrivia(trailingTrivia);
 
                return WithSemicolonToken(
                           WithExpressionBody(
                               WithBody(declaration, body: null),
                               expressionBody),
                           semicolonToken);
            }
            else
            {
                return WithSemicolonToken(
                           WithExpressionBody(
                               WithGenerateBody(semanticModel, declaration),
                               expressionBody: null),
                           default);
            }
        }
 
        protected abstract BlockSyntax? GetBody(TDeclaration declaration);
 
        protected abstract ArrowExpressionClauseSyntax? GetExpressionBody(TDeclaration declaration);
 
        protected abstract bool CreateReturnStatementForExpression(SemanticModel semanticModel, TDeclaration declaration);
 
        protected abstract SyntaxToken GetSemicolonToken(TDeclaration declaration);
 
        protected abstract TDeclaration WithSemicolonToken(TDeclaration declaration, SyntaxToken token);
        protected abstract TDeclaration WithExpressionBody(TDeclaration declaration, ArrowExpressionClauseSyntax? expressionBody);
        protected abstract TDeclaration WithBody(TDeclaration declaration, BlockSyntax? body);
 
        protected virtual TDeclaration WithGenerateBody(SemanticModel semanticModel, TDeclaration declaration)
        {
            var expressionBody = GetExpressionBody(declaration);
 
            if (expressionBody.TryConvertToBlock(
                    GetSemicolonToken(declaration),
                    CreateReturnStatementForExpression(semanticModel, declaration),
                    out var block))
            {
                return WithBody(declaration, block);
            }
 
            return declaration;
        }
 
        protected TDeclaration WithAccessorList(SemanticModel semanticModel, TDeclaration declaration)
        {
            var expressionBody = GetExpressionBody(declaration);
            var semicolonToken = GetSemicolonToken(declaration);
 
            // When converting an expression-bodied property to a block body, always attempt to
            // create an accessor with a block body (even if the user likes expression bodied
            // accessors.  While this technically doesn't match their preferences, it fits with
            // the far more likely scenario that the user wants to convert this property into
            // a full property so that they can flesh out the body contents.  If we keep around
            // an expression bodied accessor they'll just have to convert that to a block as well
            // and that means two steps to take instead of one.
 
            expressionBody.TryConvertToBlock(
                GetSemicolonToken(declaration),
                CreateReturnStatementForExpression(semanticModel, declaration),
                out var block);
 
            var accessor = SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration);
            accessor = block != null
                ? accessor.WithBody(block)
                : accessor.WithExpressionBody(expressionBody)
                          .WithSemicolonToken(semicolonToken);
 
            return WithAccessorList(declaration, SyntaxFactory.AccessorList(
                SyntaxFactory.SingletonList(accessor)));
        }
 
        protected virtual TDeclaration WithAccessorList(TDeclaration declaration, AccessorListSyntax accessorListSyntax)
            => throw new NotImplementedException();
    }
}