// 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;
}
}
}
}