|
// 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;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Shared.Utilities
{
internal partial struct HashCodeAnalyzer
{
/// <summary>
/// Breaks down complex <see cref="IOperation"/> trees, looking for particular
/// <see cref="object.GetHashCode"/> patterns and extracting out the field and property
/// symbols use to compute the hash.
/// </summary>
private struct OperationDeconstructor : IDisposable
{
private readonly HashCodeAnalyzer _analyzer;
private readonly IMethodSymbol _method;
private readonly ILocalSymbol? _hashCodeVariable;
private readonly ArrayBuilder<ISymbol> _hashedSymbols;
private bool _accessesBase;
public OperationDeconstructor(
HashCodeAnalyzer analyzer, IMethodSymbol method, ILocalSymbol? hashCodeVariable)
{
_analyzer = analyzer;
_method = method;
_hashCodeVariable = hashCodeVariable;
_hashedSymbols = ArrayBuilder<ISymbol>.GetInstance();
_accessesBase = false;
}
public readonly void Dispose()
=> _hashedSymbols.Free();
public readonly (bool accessesBase, ImmutableArray<ISymbol> hashedSymbol) GetResult()
=> (_accessesBase, _hashedSymbols.ToImmutable());
/// <summary>
/// Recursive function that decomposes <paramref name="value"/>, looking for particular
/// forms that VS or ReSharper generate to hash fields in the containing type.
/// </summary>
/// <param name="seenHash">'seenHash' is used to determine if we actually saw something
/// that indicates that we really hashed a field/property and weren't just simply
/// referencing it. This is used as we recurse down to make sure we've seen a
/// pattern we explicitly recognize by the time we hit a field/prop.</param>
public bool TryAddHashedSymbol(IOperation value, bool seenHash)
{
value = Unwrap(value);
switch (value)
{
case IBinaryOperation topBinary:
// (hashCode op1 constant) op1 hashed_value
//
// This is generated by both VS and ReSharper. Though each use different mathematical
// ops to combine the values.
return _hashCodeVariable != null &&
topBinary.LeftOperand is IBinaryOperation leftBinary &&
IsLocalReference(leftBinary.LeftOperand, _hashCodeVariable) &&
IsLiteralNumber(leftBinary.RightOperand) &&
TryAddHashedSymbol(topBinary.RightOperand, seenHash: true);
case IInvocationOperation invocation:
var targetMethod = invocation.TargetMethod;
if (_analyzer.OverridesSystemObject(targetMethod))
{
// Either:
//
// a.GetHashCode()
//
// or
//
// (hashCode * -1521134295 + a.GetHashCode()).GetHashCode()
//
// recurse on the value we're calling GetHashCode on.
RoslynDebug.Assert(invocation.Instance is not null);
return TryAddHashedSymbol(invocation.Instance, seenHash: true);
}
if (targetMethod.Name == nameof(GetHashCode) &&
Equals(_analyzer._equalityComparerType, targetMethod.ContainingType.OriginalDefinition) &&
invocation.Arguments.Length == 1)
{
// EqualityComparer<T>.Default.GetHashCode(i)
//
// VS codegen only.
return TryAddHashedSymbol(invocation.Arguments[0].Value, seenHash: true);
}
// No other invocations supported.
return false;
case IConditionalOperation conditional:
// (StringProperty != null ? StringProperty.GetHashCode() : 0)
//
// ReSharper codegen only.
if (conditional.Condition is IBinaryOperation binary &&
Unwrap(binary.RightOperand).IsNullLiteral() &&
TryGetFieldOrProperty(binary.LeftOperand, out _))
{
if (binary.OperatorKind == BinaryOperatorKind.Equals)
{
// (StringProperty == null ? 0 : StringProperty.GetHashCode())
RoslynDebug.Assert(conditional.WhenFalse is not null);
return TryAddHashedSymbol(conditional.WhenFalse, seenHash: true);
}
else if (binary.OperatorKind == BinaryOperatorKind.NotEquals)
{
// (StringProperty != null ? StringProperty.GetHashCode() : 0)
return TryAddHashedSymbol(conditional.WhenTrue, seenHash: true);
}
}
// no other conditional forms supported.
return false;
}
// Look to see if we're referencing some field/prop/base. However, we only accept
// this reference if we've at least been through something that indicates that we've
// hashed the value.
if (seenHash)
{
if (value is IInstanceReferenceOperation instanceReference &&
instanceReference.ReferenceKind == InstanceReferenceKind.ContainingTypeInstance &&
Equals(_method.ContainingType.BaseType, instanceReference.Type))
{
if (_accessesBase)
{
// already had a reference to base.GetHashCode();
return false;
}
// reference to base.
//
// Happens with code like: `var hashCode = base.GetHashCode();`
_accessesBase = true;
return true;
}
// After decomposing all of the above patterns, we must end up with an operation that is
// a reference to an instance-field (or prop) in our type. If so, and this is the only
// time we've seen that field/prop, then we're good.
//
// We only do this if we actually did something that counts as hashing along the way. This
// way
if (TryGetFieldOrProperty(value, out var fieldOrProp) &&
Equals(fieldOrProp.ContainingType.OriginalDefinition, _method.ContainingType))
{
return TryAddSymbol(fieldOrProp);
}
if (value is ITupleOperation tupleOperation)
{
foreach (var element in tupleOperation.Elements)
{
if (!TryAddHashedSymbol(element, seenHash: true))
{
return false;
}
}
return true;
}
}
// Anything else is not recognized.
return false;
}
private static bool TryGetFieldOrProperty(IOperation operation, [NotNullWhen(true)] out ISymbol? symbol)
{
operation = Unwrap(operation);
if (operation is IFieldReferenceOperation fieldReference)
{
symbol = fieldReference.Member;
return !symbol.IsStatic;
}
if (operation is IPropertyReferenceOperation propertyReference)
{
symbol = propertyReference.Member;
return !symbol.IsStatic;
}
symbol = null;
return false;
}
private readonly bool TryAddSymbol(ISymbol member)
{
// Not a legal GetHashCode to convert if we refer to members multiple times.
if (_hashedSymbols.Contains(member))
{
return false;
}
_hashedSymbols.Add(member);
return true;
}
}
}
}
|