File: Microsoft.CodeQuality.Analyzers\ApiDesignGuidelines\OperatorOverloadsHaveNamedAlternates.cs
Web Access
Project: src\sdk\src\Microsoft.CodeAnalysis.NetAnalyzers\src\Microsoft.CodeAnalysis.NetAnalyzers\Microsoft.CodeAnalysis.NetAnalyzers.csproj (Microsoft.CodeAnalysis.NetAnalyzers)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines
{
    using static MicrosoftCodeQualityAnalyzersResources;

    /// <summary>
    /// CA2225: <inheritdoc cref="OperatorOverloadsHaveNamedAlternatesTitle"/>
    /// </summary>
    [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
    public sealed class OperatorOverloadsHaveNamedAlternatesAnalyzer : DiagnosticAnalyzer
    {
        internal const string RuleId = "CA2225";
        internal const string DiagnosticKindText = "DiagnosticKind";
        internal const string AddAlternateText = "AddAlternate";
        internal const string FixVisibilityText = "FixVisibility";
        internal const string IsTrueText = "IsTrue";
        private const string OpTrueText = "op_True";
        private const string OpFalseText = "op_False";

        private static readonly LocalizableString s_localizableTitle = CreateLocalizableResourceString(nameof(OperatorOverloadsHaveNamedAlternatesTitle));
        private static readonly LocalizableString s_localizableDescription = CreateLocalizableResourceString(nameof(OperatorOverloadsHaveNamedAlternatesDescription));

        internal static readonly DiagnosticDescriptor DefaultRule = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(OperatorOverloadsHaveNamedAlternatesMessageDefault)),
            DiagnosticCategory.Usage,
            RuleLevel.Disabled,
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);

        internal static readonly DiagnosticDescriptor PropertyRule = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(OperatorOverloadsHaveNamedAlternatesMessageProperty)),
            DiagnosticCategory.Usage,
            RuleLevel.Disabled,
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);

        internal static readonly DiagnosticDescriptor MultipleRule = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(OperatorOverloadsHaveNamedAlternatesMessageMultiple)),
            DiagnosticCategory.Usage,
            RuleLevel.Disabled,
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);

        internal static readonly DiagnosticDescriptor VisibilityRule = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(OperatorOverloadsHaveNamedAlternatesMessageVisibility)),
            DiagnosticCategory.Usage,
            RuleLevel.Disabled,
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(DefaultRule, PropertyRule, MultipleRule, VisibilityRule);

        public override void Initialize(AnalysisContext context)
        {
            context.EnableConcurrentExecution();
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

            context.RegisterSymbolAction(context =>
            {
                var namedType = (INamedTypeSymbol)context.Symbol;
                foreach (var member in namedType.GetMembers())
                {
                    if (member is IMethodSymbol method &&
                        method.MethodKind is MethodKind.UserDefinedOperator or MethodKind.Conversion)
                    {
                        AnalyzeMethod(method, context);
                    }
                }
            }, SymbolKind.NamedType);
        }

        private static void AnalyzeMethod(IMethodSymbol methodSymbol, SymbolAnalysisContext symbolContext)
        {
            Debug.Assert(methodSymbol.MethodKind is MethodKind.UserDefinedOperator or MethodKind.Conversion);

            // FxCop compat: only analyze externally visible symbols by default.
            // Note all the descriptors/rules for this analyzer have the same ID and category and hence
            // will always have identical configured visibility.
            if (!symbolContext.Options.MatchesConfiguredVisibility(DefaultRule, methodSymbol, symbolContext.Compilation))
            {
                return;
            }

            string operatorName = methodSymbol.Name;
            var typeSymbol = methodSymbol.ContainingType;
            if (IsPropertyExpected(operatorName) && operatorName != OpFalseText)
            {
                // don't report a diagnostic on the `op_False` method because then the user would see two diagnostics for what is really one error
                // special-case looking for `IsTrue` instance property
                // named properties can't be overloaded so there will only ever be 0 or 1
                IPropertySymbol property = typeSymbol.GetMembers(IsTrueText).OfType<IPropertySymbol>().FirstOrDefault();
                if (property == null || property.Type.SpecialType != SpecialType.System_Boolean)
                {
                    symbolContext.ReportDiagnostic(CreateDiagnostic(PropertyRule, GetSymbolLocation(methodSymbol), AddAlternateText, IsTrueText, operatorName));
                }
                else if (!property.IsPublic())
                {
                    symbolContext.ReportDiagnostic(CreateDiagnostic(VisibilityRule, GetSymbolLocation(property), FixVisibilityText, IsTrueText, operatorName));
                }
            }
            else
            {
                ExpectedAlternateMethodGroup? expectedGroup = GetExpectedAlternateMethodGroup(operatorName, methodSymbol.ReturnType, methodSymbol.Parameters.FirstOrDefault()?.Type);
                if (expectedGroup == null)
                {
                    // no alternate methods required
                    return;
                }

                var matchedMethods = new List<IMethodSymbol>();
                var unmatchedMethods = new HashSet<string>() { expectedGroup.AlternateMethod1 };
                if (expectedGroup.AlternateMethod2 != null)
                {
                    unmatchedMethods.Add(expectedGroup.AlternateMethod2);
                }

                foreach (IMethodSymbol candidateMethod in typeSymbol.GetMembers().OfType<IMethodSymbol>())
                {
                    if (candidateMethod.Name == expectedGroup.AlternateMethod1 || candidateMethod.Name == expectedGroup.AlternateMethod2)
                    {
                        // found an appropriately-named method
                        matchedMethods.Add(candidateMethod);
                        unmatchedMethods.Remove(candidateMethod.Name);
                    }
                }

                // only one public method match is required
                if (matchedMethods.Any(m => m.IsPublic()))
                {
                    // at least one public alternate method was found, do nothing
                }
                else
                {
                    // either we found at least one method that should be public or we didn't find anything
                    IMethodSymbol notPublicMethod = matchedMethods.FirstOrDefault(m => !m.IsPublic());
                    if (notPublicMethod != null)
                    {
                        // report error for improper visibility directly on the method itself
                        symbolContext.ReportDiagnostic(CreateDiagnostic(VisibilityRule, GetSymbolLocation(notPublicMethod), FixVisibilityText, notPublicMethod.Name, operatorName));
                    }
                    else
                    {
                        // report error for missing methods on the operator overload
                        if (expectedGroup.AlternateMethod2 == null)
                        {
                            // only one alternate expected
                            symbolContext.ReportDiagnostic(CreateDiagnostic(DefaultRule, GetSymbolLocation(methodSymbol), AddAlternateText, expectedGroup.AlternateMethod1, operatorName));
                        }
                        else
                        {
                            // one of two alternates expected
                            symbolContext.ReportDiagnostic(CreateDiagnostic(MultipleRule, GetSymbolLocation(methodSymbol), AddAlternateText, expectedGroup.AlternateMethod1, expectedGroup.AlternateMethod2, operatorName));
                        }
                    }
                }
            }
        }

        private static Location GetSymbolLocation(ISymbol symbol)
        {
            return symbol.OriginalDefinition.Locations.First();
        }

        private static Diagnostic CreateDiagnostic(DiagnosticDescriptor descriptor, Location location, string kind, params string[] messageArgs)
        {
            return Diagnostic.Create(descriptor, location, ImmutableDictionary.Create<string, string?>().Add(DiagnosticKindText, kind), messageArgs);
        }

        internal static bool IsPropertyExpected(string operatorName)
        {
            return operatorName switch
            {
                OpTrueText or OpFalseText => true,
                _ => false,
            };
        }

        internal static ExpectedAlternateMethodGroup? GetExpectedAlternateMethodGroup(string operatorName, ITypeSymbol returnType, ITypeSymbol? parameterType)
        {
            // list of operator alternate names: https://learn.microsoft.com/visualstudio/code-quality/ca2225

            // the most common case; create a static method with the already specified types
            static ExpectedAlternateMethodGroup createSingle(string methodName) => new(methodName);
            return operatorName switch
            {
                "op_Addition"
                or "op_AdditonAssignment" => createSingle("Add"),
                "op_BitwiseAnd"
                or "op_BitwiseAndAssignment" => createSingle("BitwiseAnd"),
                "op_BitwiseOr"
                or "op_BitwiseOrAssignment" => createSingle("BitwiseOr"),
                "op_Decrement" => createSingle("Decrement"),
                "op_Division"
                or "op_DivisionAssignment" => createSingle("Divide"),
                "op_Equality"
                or "op_Inequality" => createSingle("Equals"),
                "op_ExclusiveOr"
                or "op_ExclusiveOrAssignment" => createSingle("Xor"),
                "op_GreaterThan"
                or "op_GreaterThanOrEqual" or "op_LessThan" or "op_LessThanOrEqual" => new ExpectedAlternateMethodGroup(alternateMethod1: "CompareTo", alternateMethod2: "Compare"),
                "op_Increment" => createSingle("Increment"),
                "op_LeftShift"
                or "op_LeftShiftAssignment" => createSingle("LeftShift"),
                "op_LogicalAnd" => createSingle("LogicalAnd"),
                "op_LogicalOr" => createSingle("LogicalOr"),
                "op_LogicalNot" => createSingle("LogicalNot"),
                "op_Modulus"
                or "op_ModulusAssignment" => new ExpectedAlternateMethodGroup(alternateMethod1: "Mod", alternateMethod2: "Remainder"),
                "op_MultiplicationAssignment"
                or "op_Multiply" => createSingle("Multiply"),
                "op_OnesComplement" => createSingle("OnesComplement"),
                "op_RightShift"
                or "op_RightShiftAssignment"
                or "op_SignedRightShift"
                or "op_UnsignedRightShift"
                or "op_UnsignedRightShiftAssignment" => createSingle("RightShift"),
                "op_Subtraction"
                or "op_SubtractionAssignment" => createSingle("Subtract"),
                "op_UnaryNegation" => createSingle("Negate"),
                "op_UnaryPlus" => createSingle("Plus"),
                "op_Implicit"
                or "op_Explicit" => new ExpectedAlternateMethodGroup(alternateMethod1: $"To{GetTypeName(returnType)}", alternateMethod2: parameterType != null ? $"From{GetTypeName(parameterType)}" : null),
                _ => null,
            };

            static string GetTypeName(ITypeSymbol typeSymbol)
            {
                if (typeSymbol.TypeKind != TypeKind.Array)
                {
                    return typeSymbol.Name;
                }

                var elementType = typeSymbol;
                do
                {
                    elementType = ((IArrayTypeSymbol)elementType).ElementType;
                }
                while (elementType.TypeKind == TypeKind.Array);

                return elementType.Name + "Array";
            }
        }

        internal class ExpectedAlternateMethodGroup
        {
            public string AlternateMethod1 { get; }
            public string? AlternateMethod2 { get; }

            public ExpectedAlternateMethodGroup(string alternateMethod1, string? alternateMethod2 = null)
            {
                AlternateMethod1 = alternateMethod1;
                AlternateMethod2 = alternateMethod2;
            }
        }
    }
}