| File: src\roslyn\src\Analyzers\Core\Analyzers\Helpers\HashCodeAnalyzer\HashCodeAnalyzer.OperationDeconstructor.cs | Web Access |
| Project: src\roslyn\src\CodeStyle\Core\Analyzers\Microsoft.CodeAnalysis.CodeStyle.csproj (Microsoft.CodeAnalysis.CodeStyle) |
// 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( HashCodeAnalyzer analyzer, IMethodSymbol method, ILocalSymbol? hashCodeVariable) : IDisposable { private readonly HashCodeAnalyzer _analyzer = analyzer; private readonly IMethodSymbol _method = method; private readonly ILocalSymbol? _hashCodeVariable = hashCodeVariable; private readonly ArrayBuilder<ISymbol> _hashedSymbols = ArrayBuilder<ISymbol>.GetInstance(); private bool _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; } } }