// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Operations;
namespace Microsoft.NetCore.Analyzers.Performance
{
public abstract partial class ConstantExpectedAnalyzer
{
private sealed class UnmanagedHelper<T> where T : unmanaged
{
private static readonly ConstantExpectedParameterFactory? _instance = CreateFactory();
private static ConstantExpectedParameterFactory Instance => _instance ?? throw new InvalidOperationException("unsupported type");
private static ConstantExpectedParameterFactory? CreateFactory()
{
if (typeof(T) == typeof(long))
{
var helper = new UnmanagedHelper<long>.TransformHelper(TryTransformInt64);
return new ConstantExpectedParameterFactory((TransformHelper)(object)helper);
}
else if (typeof(T) == typeof(ulong))
{
var helper = new UnmanagedHelper<ulong>.TransformHelper(TryTransformUInt64);
return new ConstantExpectedParameterFactory((TransformHelper)(object)helper);
}
else if (typeof(T) == typeof(float))
{
var helper = new UnmanagedHelper<float>.TransformHelper(TryTransformSingle);
return new ConstantExpectedParameterFactory((TransformHelper)(object)helper);
}
else if (typeof(T) == typeof(double))
{
var helper = new UnmanagedHelper<double>.TransformHelper(TryTransformDouble);
return new ConstantExpectedParameterFactory((TransformHelper)(object)helper);
}
else if (typeof(T) == typeof(char))
{
var helper = new UnmanagedHelper<char>.TransformHelper(TryTransformChar);
return new ConstantExpectedParameterFactory((TransformHelper)(object)helper);
}
else if (typeof(T) == typeof(bool))
{
var helper = new UnmanagedHelper<bool>.TransformHelper(TryTransformBoolean);
return new ConstantExpectedParameterFactory((TransformHelper)(object)helper);
}
return null;
}
#pragma warning disable CA1000 // Do not declare static members on generic types - https://github.com/dotnet/roslyn-analyzers/issues/6379
public static bool TryCreate(IParameterSymbol parameterSymbol, AttributeData attributeData, T typeMin, T typeMax, [NotNullWhen(true)] out ConstantExpectedParameter? parameter)
=> Instance.TryCreate(parameterSymbol, attributeData, typeMin, typeMax, out parameter);
public static bool Validate(IParameterSymbol parameterSymbol, AttributeData attributeData, T typeMin, T typeMax, DiagnosticHelper diagnosticHelper, out ImmutableArray<Diagnostic> diagnostics)
=> Instance.Validate(parameterSymbol, attributeData, typeMin, typeMax, diagnosticHelper, out diagnostics);
#pragma warning restore CA1000 // Do not declare static members on generic types
public delegate bool TryTransform(object constant, out T value, out bool isInvalid);
public sealed class TransformHelper
{
private readonly TryTransform _tryTransform;
public TransformHelper(TryTransform tryTransform)
{
_tryTransform = tryTransform;
}
#pragma warning disable CA1822 // Mark members as static - Suppressed for improved readability at callsites
public bool IsLessThan(T operand1, T operand2) => Comparer<T>.Default.Compare(operand1, operand2) < 0;
#pragma warning restore CA1822 // Mark members as static
public bool TryTransformMin(object constant, out T value, ref ErrorKind errorFlags)
{
if (_tryTransform(constant, out value, out bool isInvalid))
{
return true;
}
errorFlags |= isInvalid ? ErrorKind.MinIsIncompatible : ErrorKind.MinIsOutOfRange;
return false;
}
public bool TryTransformMax(object constant, out T value, ref ErrorKind errorFlags)
{
if (_tryTransform(constant, out value, out bool isInvalid))
{
return true;
}
errorFlags |= isInvalid ? ErrorKind.MaxIsIncompatible : ErrorKind.MaxIsOutOfRange;
return false;
}
public bool TryConvert(object val, out T value) => _tryTransform(val, out value, out _);
}
public sealed class ConstantExpectedParameterFactory
{
private readonly TransformHelper _helper;
public ConstantExpectedParameterFactory(TransformHelper helper)
{
_helper = helper;
}
public bool Validate(IParameterSymbol parameterSymbol, AttributeData attributeData, T typeMin, T typeMax, DiagnosticHelper diagnosticHelper, out ImmutableArray<Diagnostic> diagnostics)
{
if (!IsValidMinMax(attributeData, typeMin, typeMax, out _, out _, out ErrorKind errorFlags))
{
var syntax = attributeData.ApplicationSyntaxReference?.GetSyntax() ?? parameterSymbol.DeclaringSyntaxReferences[0].GetSyntax();
diagnostics = diagnosticHelper.GetError(errorFlags, parameterSymbol, syntax, typeMin.ToString(), typeMax.ToString());
return false;
}
diagnostics = ImmutableArray<Diagnostic>.Empty;
return true;
}
public bool TryCreate(IParameterSymbol parameterSymbol, AttributeData attributeData, T typeMin, T typeMax, [NotNullWhen(true)] out ConstantExpectedParameter? parameter)
{
if (!IsValidMinMax(attributeData, typeMin, typeMax, out T minValue, out T maxValue, out _))
{
parameter = null;
return false;
}
parameter = new UnmanagedConstantExpectedParameter(parameterSymbol, minValue, maxValue, _helper);
return true;
}
private bool IsValidMinMax(AttributeData attributeData, T typeMin, T typeMax, out T minValue, out T maxValue, out ErrorKind errorFlags)
{
minValue = typeMin;
maxValue = typeMax;
var ac = AttributeConstant.Get(attributeData);
errorFlags = ErrorKind.None;
if (ac.Min is not null && _helper.TryTransformMin(ac.Min, out minValue, ref errorFlags))
{
if (_helper.IsLessThan(minValue, typeMin) || _helper.IsLessThan(typeMax, minValue))
{
errorFlags |= ErrorKind.MinIsOutOfRange;
}
}
if (ac.Max is not null && _helper.TryTransformMax(ac.Max, out maxValue, ref errorFlags))
{
if (_helper.IsLessThan(maxValue, typeMin) || _helper.IsLessThan(typeMax, maxValue))
{
errorFlags |= ErrorKind.MaxIsOutOfRange;
}
}
if (errorFlags != ErrorKind.None)
{
return false;
}
if (_helper.IsLessThan(maxValue, minValue))
{
errorFlags = ErrorKind.MinMaxInverted;
return false;
}
return true;
}
}
public sealed class UnmanagedConstantExpectedParameter : ConstantExpectedParameter
{
private readonly TransformHelper _helper;
public UnmanagedConstantExpectedParameter(IParameterSymbol parameter, T min, T max, TransformHelper helper) : base(parameter)
{
Min = min;
Max = max;
_helper = helper;
}
public T Min { get; }
public T Max { get; }
public override bool ValidateParameterIsWithinRange(ConstantExpectedParameter subsetCandidate, IArgumentOperation argument, [NotNullWhen(false)] out Diagnostic? validationDiagnostics)
{
if (Parameter.Type.SpecialType != subsetCandidate.Parameter.Type.SpecialType ||
subsetCandidate is not UnmanagedConstantExpectedParameter subsetCandidateTParameter)
{
validationDiagnostics = CreateConstantInvalidConstantRuleDiagnostic(argument);
return false;
}
if (!_helper.IsLessThan(subsetCandidateTParameter.Min, Min) && !_helper.IsLessThan(Max, subsetCandidateTParameter.Max))
{
//within range
validationDiagnostics = null;
return true;
}
validationDiagnostics = CreateConstantOutOfBoundsRuleDiagnostic(argument, Min.ToString(), Max.ToString());
return false;
}
public override bool ValidateValue(IArgumentOperation argument, Optional<object?> constant, [NotNullWhen(false)] out Diagnostic? validationDiagnostics)
{
if (!ValidateConstant(argument, constant, out validationDiagnostics))
{
return false;
}
if (constant.Value is not null && _helper.TryConvert(constant.Value, out T value))
{
if (!_helper.IsLessThan(value, Min) && !_helper.IsLessThan(Max, value))
{
validationDiagnostics = null;
return true;
}
validationDiagnostics = CreateConstantOutOfBoundsRuleDiagnostic(argument, Min.ToString(), Max.ToString());
return false;
}
validationDiagnostics = CreateConstantInvalidConstantRuleDiagnostic(argument);
return false;
}
}
}
private static bool TryConvertSignedInteger(object constant, out long integer)
{
try
{
if (constant is string or bool)
{
integer = default;
return false;
}
integer = Convert.ToInt64(constant);
}
catch (Exception ex) when (CatchExceptionDuringConvert(ex))
{
integer = default;
return false;
}
return true;
}
private static bool CatchExceptionDuringConvert(Exception ex)
=> ex is FormatException or InvalidCastException or OverflowException or ArgumentNullException;
private static bool TryConvertUnsignedInteger(object constant, out ulong integer)
{
try
{
if (constant is string or bool)
{
integer = default;
return false;
}
integer = Convert.ToUInt64(constant);
}
catch (Exception ex) when (CatchExceptionDuringConvert(ex))
{
integer = default;
return false;
}
return true;
}
private static bool TryTransformInt64(object constant, out long value, out bool isInvalid)
{
bool isValidSigned = TryConvertSignedInteger(constant, out value);
isInvalid = false;
if (isValidSigned)
{
return isValidSigned;
}
if (!TryConvertUnsignedInteger(constant, out _))
{
isInvalid = true;
}
return isValidSigned;
}
private static bool TryTransformUInt64(object constant, out ulong value, out bool isInvalid)
{
bool isValidUnsigned = TryConvertUnsignedInteger(constant, out value);
isInvalid = false;
if (isValidUnsigned)
{
return isValidUnsigned;
}
if (!TryConvertSignedInteger(constant, out _))
{
isInvalid = true;
}
return isValidUnsigned;
}
private static bool TryTransformChar(object constant, out char value, out bool isInvalid)
{
try
{
if (constant is string or bool)
{
return Invalid(out value, out isInvalid);
}
value = Convert.ToChar(constant);
}
catch (Exception ex) when (CatchExceptionDuringConvert(ex))
{
return Invalid(out value, out isInvalid);
}
isInvalid = false;
return true;
}
private static bool TryTransformBoolean(object constant, out bool value, out bool isInvalid)
{
if (constant is bool b)
{
value = b;
isInvalid = false;
return true;
}
return Invalid(out value, out isInvalid);
}
private static bool TryTransformSingle(object constant, out float value, out bool isInvalid)
{
if (constant is string or bool)
{
return Invalid(out value, out isInvalid);
}
value = Convert.ToSingle(constant);
isInvalid = false;
return true;
}
private static bool TryTransformDouble(object constant, out double value, out bool isInvalid)
{
if (constant is string or bool)
{
return Invalid(out value, out isInvalid);
}
value = Convert.ToDouble(constant);
isInvalid = false;
return true;
}
private static bool Invalid<T>(out T value, out bool isInvalid) where T : unmanaged
{
value = default;
isInvalid = true;
return false;
}
}
}