File: AbstractAddRequiredParenthesesDiagnosticAnalyzer.cs
Web Access
Project: ..\..\..\src\CodeStyle\Core\Analyzers\Microsoft.CodeAnalysis.CodeStyle.csproj (Microsoft.CodeAnalysis.CodeStyle)
// 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 Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Precedence;
using Microsoft.CodeAnalysis.RemoveUnnecessaryParentheses;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.AddRequiredParentheses
{
    internal abstract class AbstractAddRequiredParenthesesDiagnosticAnalyzer<
        TExpressionSyntax, TBinaryLikeExpressionSyntax, TLanguageKindEnum>
        : AbstractBuiltInCodeStyleDiagnosticAnalyzer
        where TExpressionSyntax : SyntaxNode
        where TBinaryLikeExpressionSyntax : TExpressionSyntax
        where TLanguageKindEnum : struct
    {
        private static readonly Dictionary<(bool includeInFixAll, string equivalenceKey), ImmutableDictionary<string, string?>> s_cachedProperties =
            new();
 
        private readonly IPrecedenceService _precedenceService;
 
        static AbstractAddRequiredParenthesesDiagnosticAnalyzer()
        {
            var includeArray = new[] { false, true };
 
            foreach (var equivalenceKey in GetAllEquivalenceKeys())
            {
                foreach (var includeInFixAll in includeArray)
                {
                    var properties = ImmutableDictionary<string, string?>.Empty;
                    if (includeInFixAll)
                    {
                        properties = properties.Add(AddRequiredParenthesesConstants.IncludeInFixAll, "");
                    }
 
                    properties = properties.Add(AddRequiredParenthesesConstants.EquivalenceKey, equivalenceKey);
                    s_cachedProperties.Add((includeInFixAll, equivalenceKey), properties);
                }
            }
        }
 
        protected static string GetEquivalenceKey(PrecedenceKind precedenceKind)
            => precedenceKind switch
            {
                PrecedenceKind.Arithmetic or PrecedenceKind.Shift or PrecedenceKind.Bitwise => "ArithmeticBinary",
                PrecedenceKind.Relational or PrecedenceKind.Equality => "RelationalBinary",
                PrecedenceKind.Logical or PrecedenceKind.Coalesce => "OtherBinary",
                PrecedenceKind.Other => "Other",
                _ => throw ExceptionUtilities.UnexpectedValue(precedenceKind),
            };
 
        protected static ImmutableArray<string> GetAllEquivalenceKeys()
            => ImmutableArray.Create("ArithmeticBinary", "RelationalBinary", "OtherBinary", "Other");
 
        private static ImmutableDictionary<string, string?> GetProperties(bool includeInFixAll, string equivalenceKey)
            => s_cachedProperties[(includeInFixAll, equivalenceKey)];
 
        protected abstract int GetPrecedence(TBinaryLikeExpressionSyntax binaryLike);
        protected abstract TExpressionSyntax? TryGetAppropriateParent(TBinaryLikeExpressionSyntax binaryLike);
        protected abstract bool IsBinaryLike(TExpressionSyntax node);
        protected abstract (TExpressionSyntax, SyntaxToken, TExpressionSyntax) GetPartsOfBinaryLike(TBinaryLikeExpressionSyntax binaryLike);
 
        protected AbstractAddRequiredParenthesesDiagnosticAnalyzer(IPrecedenceService precedenceService)
            : base(IDEDiagnosticIds.AddRequiredParenthesesDiagnosticId,
                   EnforceOnBuildValues.AddRequiredParentheses,
                   options: ParenthesesDiagnosticAnalyzersHelper.Options,
                   new LocalizableResourceString(nameof(AnalyzersResources.Add_parentheses_for_clarity), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
                   new LocalizableResourceString(nameof(AnalyzersResources.Parentheses_should_be_added_for_clarity), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)))
        {
            _precedenceService = precedenceService;
        }
 
        public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory()
            => DiagnosticAnalyzerCategory.SemanticSpanAnalysis;
 
        protected sealed override void InitializeWorker(AnalysisContext context)
            => context.RegisterSyntaxNodeAction(AnalyzeSyntax, GetSyntaxNodeKinds());
 
        protected abstract ImmutableArray<TLanguageKindEnum> GetSyntaxNodeKinds();
 
        private void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
        {
            var binaryLike = (TBinaryLikeExpressionSyntax)context.Node;
            var parent = TryGetAppropriateParent(binaryLike);
            if (parent == null || !IsBinaryLike(parent))
            {
                return;
            }
 
            var parentBinaryLike = (TBinaryLikeExpressionSyntax)parent;
            if (GetPrecedence(binaryLike) == GetPrecedence(parentBinaryLike))
            {
                return;
            }
 
            var options = context.GetAnalyzerOptions();
            var childPrecedenceKind = _precedenceService.GetPrecedenceKind(binaryLike);
            var parentPrecedenceKind = _precedenceService.GetPrecedenceKind(parentBinaryLike);
 
            var childEquivalenceKey = GetEquivalenceKey(childPrecedenceKind);
            var parentEquivalenceKey = GetEquivalenceKey(parentPrecedenceKind);
 
            // only add parentheses within the same precedence band.
            if (childEquivalenceKey != parentEquivalenceKey)
            {
                return;
            }
 
            var preference = ParenthesesDiagnosticAnalyzersHelper.GetLanguageOption(options, childPrecedenceKind);
            if (preference.Value != ParenthesesPreference.AlwaysForClarity)
            {
                return;
            }
 
            var additionalLocations = ImmutableArray.Create(binaryLike.GetLocation());
            var precedence = GetPrecedence(binaryLike);
 
            // In a case like "a + b * c * d", we'll add parens to make "a + (b * c * d)".
            // To make this user experience more pleasant, we will place the diagnostic on
            // both *'s.
            AddDiagnostics(
                context, binaryLike, precedence, preference.Notification.Severity,
                additionalLocations, childEquivalenceKey, includeInFixAll: true);
        }
 
        private void AddDiagnostics(
            SyntaxNodeAnalysisContext context, TBinaryLikeExpressionSyntax? binaryLikeOpt, int precedence,
            ReportDiagnostic severity, ImmutableArray<Location> additionalLocations,
            string equivalenceKey, bool includeInFixAll)
        {
            if (binaryLikeOpt != null &&
                IsBinaryLike(binaryLikeOpt) &&
                GetPrecedence(binaryLikeOpt) == precedence)
            {
                var (left, operatorToken, right) = GetPartsOfBinaryLike(binaryLikeOpt);
 
                var properties = GetProperties(includeInFixAll, equivalenceKey);
 
                context.ReportDiagnostic(DiagnosticHelper.Create(
                    Descriptor,
                    operatorToken.GetLocation(),
                    severity,
                    additionalLocations,
                    properties));
 
                // We're adding diagnostics for all subcomponents so that the user can get the
                // lightbulb on any of the operator tokens.  However, we don't actually want to
                // 'fix' all of these if the user does a fix-all.  if we did, we'd end up adding far
                // too many parens to the same expr.
                AddDiagnostics(context, left as TBinaryLikeExpressionSyntax, precedence, severity, additionalLocations, equivalenceKey, includeInFixAll: false);
                AddDiagnostics(context, right as TBinaryLikeExpressionSyntax, precedence, severity, additionalLocations, equivalenceKey, includeInFixAll: false);
            }
        }
    }
}