// 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.CodeAnalysis;
using System.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
namespace Microsoft.CodeQuality.Analyzers.Maintainability
{
using static MicrosoftCodeQualityAnalyzersResources;
/// <summary>
/// CA1514: <inheritdoc cref="AvoidLengthCalculationWhenSlicingToEndTitle"/>
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public sealed class AvoidLengthCalculationWhenSlicingToEndAnalyzer : DiagnosticAnalyzer
{
internal const string RuleId = "CA1514";
private const string Substring = nameof(Substring);
internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
RuleId,
CreateLocalizableResourceString(nameof(AvoidLengthCalculationWhenSlicingToEndTitle)),
CreateLocalizableResourceString(nameof(AvoidLengthCalculationWhenSlicingToEndMessage)),
DiagnosticCategory.Maintainability,
RuleLevel.IdeSuggestion,
CreateLocalizableResourceString(nameof(AvoidLengthCalculationWhenSlicingToEndDescription)),
isPortedFxCopRule: false,
isDataflowRule: false);
public sealed override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);
public sealed override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterCompilationStartAction(OnCompilationStart);
}
private void OnCompilationStart(CompilationStartAnalysisContext context)
{
if (!RequiredSymbols.TryGetSymbols(context.Compilation, out var symbols))
{
return;
}
context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation);
void AnalyzeInvocation(OperationAnalysisContext context)
{
var invocation = (IInvocationOperation)context.Operation;
if (!symbols.IsAnyStartLengthMethod(invocation.TargetMethod))
{
return;
}
var instance = invocation.Instance;
var argumentsInParameterOrder = invocation.Arguments.GetArgumentsInParameterOrder();
var startArgument = argumentsInParameterOrder[0];
var lengthArgument = argumentsInParameterOrder[1];
if (OperationHasSideEffects(instance) ||
ArgumentHasSideEffects(startArgument) ||
!symbols.HasLengthPropertyOnInstance(lengthArgument, instance, out var lengthProperty) ||
!StartIsSubtractedFromLength(startArgument, lengthArgument, lengthProperty))
{
return;
}
context.ReportDiagnostic(invocation.CreateDiagnostic(Rule, invocation.TargetMethod.ToDisplayString()));
}
// Fields, locals, parameters and instance references have no side effects.
// Properties have no side effects if they are auto-properties.
// For unary, binary and parenthesized operations it depends on the operand and whether the operator is supported.
bool OperationHasSideEffects(IOperation? operation)
{
return operation switch
{
IFieldReferenceOperation => false,
ILocalReferenceOperation => false,
IParameterReferenceOperation => false,
ILiteralOperation => false,
IInstanceReferenceOperation => false,
IPropertyReferenceOperation propertyReferenceOperation => !propertyReferenceOperation.Property.IsAutoProperty(),
IUnaryOperation { OperatorKind: UnaryOperatorKind.Plus or UnaryOperatorKind.Minus } unaryOperation
=> OperationHasSideEffects(unaryOperation.Operand),
IBinaryOperation { OperatorKind: BinaryOperatorKind.Add or BinaryOperatorKind.Subtract } binaryOperation
=> OperationHasSideEffects(binaryOperation.LeftOperand) || OperationHasSideEffects(binaryOperation.RightOperand),
IParenthesizedOperation parenthesizedOperation => OperationHasSideEffects(parenthesizedOperation.Operand),
_ => true
};
}
bool ArgumentHasSideEffects(IArgumentOperation argument)
{
return argument
.Descendants()
.OfType<IOperation>()
.Any(OperationHasSideEffects);
}
bool StartIsSubtractedFromLength(IArgumentOperation startArgument, IArgumentOperation lengthArgument, IPropertyReferenceOperation lengthProperty)
{
// Keep track of constants: Add constants from start argument and subtract constants from the length argument.
// The first condition that the start argument is subtracted from the length argument is that the constant sum is zero.
int constantSum = 0;
// Build two sets containing all symbols in the expressions that are part of the start and length argument.
// The second condition that the start argument is subtracted from the length argument is that the symbol sets are equal.
// For this reason, the symbols of the length argument start negated.
var startArgumentSymbols = new HashSet<SymbolWithNegation>();
int constantsSign = 1;
AddExpressionPartToSet(startArgument.Value, isNegated: false, startArgumentSymbols);
var lengthArgumentSymbols = new HashSet<SymbolWithNegation>();
constantsSign = -1;
AddExpressionPartToSet(lengthArgument.Value, isNegated: true, lengthArgumentSymbols);
// Remove length property which is not present in start argument.
lengthArgumentSymbols.Remove(new SymbolWithNegation(lengthProperty.Member, true));
return startArgumentSymbols.SetEquals(lengthArgumentSymbols) && constantSum == 0;
void AddExpressionPartToSet(IOperation operation, bool isNegated, HashSet<SymbolWithNegation> set)
{
if (operation is IBinaryOperation binaryOperation)
{
AddExpressionPartToSet(binaryOperation.LeftOperand, isNegated, set);
AddExpressionPartToSet(binaryOperation.RightOperand, binaryOperation.OperatorKind == BinaryOperatorKind.Subtract ? !isNegated : isNegated, set);
}
else if (operation is IUnaryOperation unaryOperation)
{
AddExpressionPartToSet(unaryOperation.Operand, unaryOperation.OperatorKind == UnaryOperatorKind.Minus ? !isNegated : isNegated, set);
}
else if (operation is IParenthesizedOperation parenthesizedOperation)
{
AddExpressionPartToSet(parenthesizedOperation.Operand, isNegated, set);
}
else if (operation is ILiteralOperation { ConstantValue.Value: int } literalOperation)
{
constantSum += (int)literalOperation.ConstantValue.Value! * (isNegated ? -1 : 1) * constantsSign;
}
else
{
set.Add(new SymbolWithNegation(operation.GetReferencedMemberOrLocalOrParameter(), isNegated));
}
}
}
}
private readonly record struct SymbolWithNegation(ISymbol? Symbol, bool IsNegated);
private sealed class RequiredSymbols
{
private RequiredSymbols(
IMethodSymbol? substringStartLength,
IMethodSymbol? spanSliceStartLength,
IMethodSymbol? readOnlySpanSliceStartLength,
IMethodSymbol? memorySliceStartLength,
IPropertySymbol? stringLength,
IPropertySymbol? spanLength,
IPropertySymbol? readOnlySpanLength,
IPropertySymbol? memoryLength)
{
SubstringStartLength = substringStartLength;
SpanSliceStartLength = spanSliceStartLength;
ReadOnlySpanSliceStartLength = readOnlySpanSliceStartLength;
MemorySliceStartLength = memorySliceStartLength;
StringLength = stringLength;
SpanLength = spanLength;
ReadOnlySpanLength = readOnlySpanLength;
MemoryLength = memoryLength;
}
public static bool TryGetSymbols(Compilation compilation, [NotNullWhen(true)] out RequiredSymbols? symbols)
{
symbols = default;
var int32Type = compilation.GetSpecialType(SpecialType.System_Int32);
var stringType = compilation.GetSpecialType(SpecialType.System_String);
var spanType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemSpan1);
var readOnlySpanType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemReadOnlySpan1);
var memoryType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemMemory1);
// Bail out if we have no integer type or all target types are null
if (int32Type is null ||
(stringType is null && spanType is null && readOnlySpanType is null && memoryType is null))
{
return false;
}
var int32ParamInfo = ParameterInfo.GetParameterInfo(int32Type);
var substringMembers = stringType?.GetMembers(Substring).OfType<IMethodSymbol>();
var substringStartLength = substringMembers.GetFirstOrDefaultMemberWithParameterInfos(int32ParamInfo, int32ParamInfo);
var stringLength = stringType?.GetMembers(WellKnownMemberNames.LengthPropertyName).OfType<IPropertySymbol>().FirstOrDefault();
var spanSliceMembers = spanType?.GetMembers(WellKnownMemberNames.SliceMethodName).OfType<IMethodSymbol>();
var spanSliceStartLength = spanSliceMembers.GetFirstOrDefaultMemberWithParameterInfos(int32ParamInfo, int32ParamInfo);
var spanLength = spanType?.GetMembers(WellKnownMemberNames.LengthPropertyName).OfType<IPropertySymbol>().FirstOrDefault();
var readOnlySpanSliceMembers = readOnlySpanType?.GetMembers(WellKnownMemberNames.SliceMethodName).OfType<IMethodSymbol>();
var readOnlySpanSliceStartLength = readOnlySpanSliceMembers.GetFirstOrDefaultMemberWithParameterInfos(int32ParamInfo, int32ParamInfo);
var readOnlySpanLength = readOnlySpanType?.GetMembers(WellKnownMemberNames.LengthPropertyName).OfType<IPropertySymbol>().FirstOrDefault();
var memorySliceMembers = memoryType?.GetMembers(WellKnownMemberNames.SliceMethodName).OfType<IMethodSymbol>();
var memorySliceStartLength = memorySliceMembers.GetFirstOrDefaultMemberWithParameterInfos(int32ParamInfo, int32ParamInfo);
var memoryLength = memoryType?.GetMembers(WellKnownMemberNames.LengthPropertyName).OfType<IPropertySymbol>().FirstOrDefault();
// Bail out if we have no complete method pair
if ((substringStartLength is null || stringLength is null) &&
(spanSliceStartLength is null || spanLength is null) &&
(readOnlySpanSliceStartLength is null || readOnlySpanLength is null) &&
(memorySliceStartLength is null || memoryLength is null))
{
return false;
}
symbols = new RequiredSymbols(
substringStartLength, spanSliceStartLength, readOnlySpanSliceStartLength, memorySliceStartLength,
stringLength, spanLength, readOnlySpanLength, memoryLength);
return true;
}
public IMethodSymbol? SubstringStartLength { get; }
public IMethodSymbol? SpanSliceStartLength { get; }
public IMethodSymbol? ReadOnlySpanSliceStartLength { get; }
public IMethodSymbol? MemorySliceStartLength { get; }
public IPropertySymbol? StringLength { get; }
public IPropertySymbol? SpanLength { get; }
public IPropertySymbol? ReadOnlySpanLength { get; }
public IPropertySymbol? MemoryLength { get; }
public bool IsAnyStartLengthMethod(IMethodSymbol method)
{
return SymbolEqualityComparer.Default.Equals(method.OriginalDefinition, SubstringStartLength) ||
SymbolEqualityComparer.Default.Equals(method.OriginalDefinition, SpanSliceStartLength) ||
SymbolEqualityComparer.Default.Equals(method.OriginalDefinition, ReadOnlySpanSliceStartLength) ||
SymbolEqualityComparer.Default.Equals(method.OriginalDefinition, MemorySliceStartLength);
}
public bool IsAnyLengthProperty(IPropertySymbol property)
{
return SymbolEqualityComparer.Default.Equals(property.OriginalDefinition, StringLength) ||
SymbolEqualityComparer.Default.Equals(property.OriginalDefinition, SpanLength) ||
SymbolEqualityComparer.Default.Equals(property.OriginalDefinition, ReadOnlySpanLength) ||
SymbolEqualityComparer.Default.Equals(property.OriginalDefinition, MemoryLength);
}
public bool HasLengthPropertyOnInstance(IArgumentOperation argument, IOperation? instance, [NotNullWhen(true)] out IPropertyReferenceOperation? lengthProperty)
{
lengthProperty = argument
.DescendantsAndSelf()
.OfType<IPropertyReferenceOperation>()
.Where(p => IsAnyLengthProperty(p.Property))
.FirstOrDefault();
if (lengthProperty is null)
{
return false;
}
return AreSameInstance(lengthProperty.Instance, instance);
}
}
private static bool AreSameInstance(IOperation? instance1, IOperation? instance2)
{
return (instance1, instance2) switch
{
(IFieldReferenceOperation fieldRef1, IFieldReferenceOperation fieldRef2) => fieldRef1.Member == fieldRef2.Member,
(IPropertyReferenceOperation propRef1, IPropertyReferenceOperation propRef2) => propRef1.Member == propRef2.Member,
(IParameterReferenceOperation paramRef1, IParameterReferenceOperation paramRef2) => paramRef1.Parameter == paramRef2.Parameter,
(ILocalReferenceOperation localRef1, ILocalReferenceOperation localRef2) => localRef1.Local == localRef2.Local,
(ILiteralOperation literalRef1, ILiteralOperation literalRef2) => literalRef1.ConstantValue.HasValue && literalRef1.ConstantValue.Value!.Equals(literalRef2.ConstantValue.Value),
_ => false,
};
}
}
}