|
// 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.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.Serialization;
using System.Threading;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis;
/// <summary>
/// <para>
/// A <see cref="SymbolKey"/> is a lightweight identifier for a symbol that can be used to
/// resolve the "same" symbol across compilations. Different symbols have different concepts
/// of "same-ness". Same-ness is recursively defined as follows:
/// <list type="number">
/// <item>Two <see cref="IArrayTypeSymbol"/>s are the "same" if they have
/// the "same" <see cref="IArrayTypeSymbol.ElementType"/> and
/// equal <see cref="IArrayTypeSymbol.Rank"/>.</item>
/// <item>Two <see cref="IAssemblySymbol"/>s are the "same" if
/// they have equal <see cref="IAssemblySymbol.Identity"/>.Name</item>
/// <item>Two <see cref="IEventSymbol"/>s are the "same" if they have
/// the "same" <see cref="ISymbol.ContainingType"/> and
/// equal <see cref="ISymbol.MetadataName"/>.</item>
/// <item>Two <see cref="IMethodSymbol"/>s are the "same" if they have
/// the "same" <see cref="ISymbol.ContainingType"/>,
/// equal <see cref="ISymbol.MetadataName"/>,
/// equal <see cref="IMethodSymbol.Arity"/>,
/// the "same" <see cref="IMethodSymbol.TypeArguments"/>, and have
/// the "same" <see cref="IParameterSymbol.Type"/>s and
/// equal <see cref="IParameterSymbol.RefKind"/>s.</item>
/// <item>Two <see cref="IModuleSymbol"/>s are the "same" if they have
/// the "same" <see cref="ISymbol.ContainingAssembly"/>.
/// <see cref="ISymbol.MetadataName"/> is not used because module identity is not important in practice.</item>
/// <item>Two <see cref="INamedTypeSymbol"/>s are the "same" if they have
/// the "same" <see cref="ISymbol.ContainingSymbol"/>,
/// equal <see cref="ISymbol.MetadataName"/>,
/// equal <see cref="INamedTypeSymbol.Arity"/> and
/// the "same" <see cref="INamedTypeSymbol.TypeArguments"/>.</item>
/// <item>Two <see cref="INamespaceSymbol"/>s are the "same" if they have
/// the "same" <see cref="ISymbol.ContainingSymbol"/> and
/// equal <see cref="ISymbol.MetadataName"/>.
/// If the <see cref="INamespaceSymbol"/> is the global namespace for a
/// compilation, then it will only match another
/// global namespace of another compilation.</item>
/// <item>Two <see cref="IParameterSymbol"/>s are the "same" if they have
/// the "same" <see cref="ISymbol.ContainingSymbol"/> and
/// equal <see cref="ISymbol.MetadataName"/>.</item>
/// <item>Two <see cref="IPointerTypeSymbol"/>s are the "same" if they have
/// the "same" <see cref="IPointerTypeSymbol.PointedAtType"/>.</item>
/// <item>Two <see cref="IPropertySymbol"/>s are the "same" if they have
/// the "same" the "same" <see cref="ISymbol.ContainingType"/>,
/// the "same" <see cref="ISymbol.MetadataName"/>, and have
/// the "same" <see cref="IParameterSymbol.Type"/>s and
/// the "same" <see cref="IParameterSymbol.RefKind"/>s.</item>
/// <item>Two <see cref="ITypeParameterSymbol"/> are the "same" if they have
/// the "same" <see cref="ISymbol.ContainingSymbol"/> and
/// the "same" <see cref="ISymbol.MetadataName"/>.</item>
/// <item>Two <see cref="IFieldSymbol"/>s are the "same" if they have
/// the "same" <see cref="ISymbol.ContainingSymbol"/> and
/// the "same" <see cref="ISymbol.MetadataName"/>.</item>
/// <item>Two <see cref="IPreprocessingSymbol"/>s are the "same" if they have
/// the "same" <see cref="ISymbol.Name"/>.</item>
/// </list>
/// </para>
/// <para>
/// Interior-method-level symbols (i.e. <see cref="ILabelSymbol"/>, <see cref="ILocalSymbol"/>, <see
/// cref="IRangeVariableSymbol"/> and <see cref="MethodKind.LocalFunction"/> <see cref="IMethodSymbol"/>s can also
/// be represented and restored in a different compilation. To resolve these the destination compilation's <see
/// cref="SyntaxTree"/> is enumerated to list all the symbols with the same <see cref="ISymbol.Name"/> and <see
/// cref="ISymbol.Kind"/> as the original symbol. The symbol with the same index in the destination tree as the
/// symbol in the original tree is returned. This allows these sorts of symbols to be resolved in a way that is
/// resilient to basic forms of edits. For example, adding whitespace edits, or adding removing symbols with
/// different names and types. However, it may not find a matching symbol in the face of other sorts of edits.
/// </para>
/// <para>
/// Symbol keys cannot be created for interior-method symbols that were created in a speculative semantic model.
/// </para>
/// <para>
/// Due to issues arising from errors and ambiguity, it's possible for a SymbolKey to resolve to
/// multiple symbols. For example, in the following type:
/// <code>
/// class C
/// {
/// int M();
/// bool M();
/// }
/// </code>
/// The SymbolKey for both 'M' methods will be the same. The SymbolKey will then resolve to both methods.
/// </para>
/// <para>
/// <see cref="SymbolKey"/>s are not guaranteed to work across different versions of Roslyn. They can be persisted
/// in their <see cref="ToString()"/> form and used across sessions with the same version of Roslyn. However, future
/// versions may change the encoded format and may no longer be able to <see cref="Resolve"/> previous keys. As
/// such, only persist if using for a cache that can be regenerated if necessary.
/// </para>
/// <para>
/// The string values produced by <see cref="CreateString"/> (or <see cref="SymbolKey.ToString"/>) should not be
/// directly compared for equality or used in hashing scenarios. Specifically, two symbol keys which represent the
/// 'same' symbol might produce different strings. Instead, to compare keys use <see cref="SymbolKey.GetComparer"/>
/// to get a suitable comparer that exposes the desired semantics.
/// </para>
/// </summary>
[DataContract]
internal partial struct SymbolKey(string data) : IEquatable<SymbolKey>
{
/// <summary>
/// Current format version. Any time we change anything about our format, we should
/// change this. This will help us detect and reject any cases where a person serializes
/// out a SymbolKey from a previous version of Roslyn and then attempt to use it in a
/// newer version where the encoding has changed.
/// </summary>
internal const int FormatVersion = 7;
[DataMember(Order = 0)]
private readonly string _symbolKeyData = data ?? throw new ArgumentNullException(nameof(data));
/// <summary>
/// Constructs a new <see cref="SymbolKey"/> representing the provided <paramref name="symbol"/>.
/// </summary>
public static SymbolKey Create(ISymbol? symbol, CancellationToken cancellationToken = default)
=> new(CreateString(symbol, cancellationToken));
/// <summary>
/// Returns an <see cref="IEqualityComparer{T}"/> that determines if two <see cref="SymbolKey"/>s
/// represent the same effective symbol.
/// </summary>
/// <param name="ignoreCase">Whether or not casing should be considered when comparing keys.
/// For example, with <c>ignoreCase=true</c> then <c>X.SomeClass</c> and <c>X.Someclass</c> would be
/// considered the same effective symbol</param>
/// <param name="ignoreAssemblyKeys">Whether or not the originating assembly of referenced
/// symbols should be compared when determining if two symbols are effectively the same.
/// For example, with <c>ignoreAssemblyKeys=true</c> then an <c>X.SomeClass</c> from assembly
/// <c>A</c> and <c>X.SomeClass</c> from assembly <c>B</c> will be considered the same
/// effective symbol.
/// </param>
public static IEqualityComparer<SymbolKey> GetComparer(bool ignoreCase = false, bool ignoreAssemblyKeys = false)
=> SymbolKeyComparer.GetComparer(ignoreCase, ignoreAssemblyKeys);
public static bool CanCreate(ISymbol symbol, CancellationToken cancellationToken)
{
if (IsBodyLevelSymbol(symbol))
{
var locations = BodyLevelSymbolKey.GetBodyLevelSourceLocations(symbol, cancellationToken);
if (locations.Length == 0)
return false;
// Ensure that the tree we're looking at is actually in this compilation. It may not be in the
// compilation in the case of work done with a speculative model.
var compilation = ((ISourceAssemblySymbol)symbol.ContainingAssembly).Compilation;
return compilation.SyntaxTrees.Contains(locations.First().SourceTree);
}
return true;
}
public static SymbolKeyResolution ResolveString(
string symbolKey, Compilation compilation,
bool ignoreAssemblyKey = false, CancellationToken cancellationToken = default)
{
return ResolveString(symbolKey, compilation, ignoreAssemblyKey, out _, cancellationToken);
}
public static SymbolKeyResolution ResolveString(
string symbolKey, Compilation compilation,
out string? failureReason, CancellationToken cancellationToken)
{
return ResolveString(symbolKey, compilation, ignoreAssemblyKey: false, out failureReason, cancellationToken);
}
public static SymbolKeyResolution ResolveString(
string symbolKey, Compilation compilation, bool ignoreAssemblyKey,
out string? failureReason, CancellationToken cancellationToken)
{
using var reader = SymbolKeyReader.GetReader(
symbolKey, compilation, ignoreAssemblyKey, cancellationToken);
var version = reader.ReadFormatVersion();
if (version != FormatVersion)
{
failureReason = $"({nameof(SymbolKey)} invalid format '${version}')";
return default;
}
// Read out the language info which was included just for diagnostic purposes.
var language = reader.ReadString();
// Initial entrypoint. No contextual symbol to pass along.
var result = reader.ReadSymbolKey(contextualSymbol: null, out failureReason);
Debug.Assert(reader.Position == symbolKey.Length);
return result;
}
public static string CreateString(ISymbol? symbol, CancellationToken cancellationToken = default)
=> CreateStringWorker(FormatVersion, symbol, cancellationToken);
// Internal for testing purposes.
internal static string CreateStringWorker(int version, ISymbol? symbol, CancellationToken cancellationToken = default)
{
using var writer = SymbolKeyWriter.GetWriter(cancellationToken);
writer.WriteFormatVersion(version);
// include the language just for help diagnosing issues. Note: the language is not considered part of the
// 'value' of the key. In other words two keys that represent the same symbol (like 'System.Int32'), but
// which differ on language, will still be considered equal. to each other.
writer.WriteString(symbol?.Language);
writer.WriteSymbolKey(symbol);
return writer.CreateKey();
}
/// <summary>
/// Tries to resolve this <see cref="SymbolKey"/> in the given
/// <paramref name="compilation"/> to a matching symbol.
/// </summary>
public readonly SymbolKeyResolution Resolve(
Compilation compilation, bool ignoreAssemblyKey = false, CancellationToken cancellationToken = default)
{
return ResolveString(_symbolKeyData, compilation, ignoreAssemblyKey, cancellationToken);
}
/// <summary>
/// Returns this <see cref="SymbolKey"/> encoded as a string. This can be persisted
/// and used later with <see cref="SymbolKey(string)"/> to then try to resolve back
/// to the corresponding <see cref="ISymbol"/> in a future <see cref="Compilation"/>.
///
/// This string form is not guaranteed to be reusable across all future versions of
/// Roslyn. As such it should only be used for caching data, with the knowledge that
/// the data may need to be recomputed if the cached data can no longer be used.
/// </summary>
public override readonly string ToString()
=> _symbolKeyData;
// Note: this method may clear 'symbols' before returning.
private static SymbolKeyResolution CreateResolution<TSymbol>(
PooledArrayBuilder<TSymbol> symbols, string reasonIfFailed, out string? failureReason)
where TSymbol : class, ISymbol
{
if (symbols.Builder.Count == 0)
{
failureReason = reasonIfFailed;
return default;
}
else if (symbols.Builder.Count == 1)
{
failureReason = null;
return new SymbolKeyResolution(symbols.Builder[0]);
}
else
{
failureReason = null;
return new SymbolKeyResolution(
ImmutableArray<ISymbol>.CastUp(symbols.Builder.ToImmutableAndClear()),
CandidateReason.Ambiguous);
}
}
private static T? SafeGet<T>(ImmutableArray<T> values, int index) where T : class
=> index < values.Length ? values[index] : null;
private static bool Equals(Compilation compilation, string? name1, string? name2)
=> Equals(compilation.IsCaseSensitive, name1, name2);
private static bool Equals(bool isCaseSensitive, string? name1, string? name2)
=> string.Equals(name1, name2, isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase);
private static bool ParameterRefKindsMatch(
ImmutableArray<IParameterSymbol> parameters,
PooledArrayBuilder<RefKind> refKinds)
{
if (parameters.Length != refKinds.Count)
return false;
for (var i = 0; i < refKinds.Count; i++)
{
// The ref-out distinction is not interesting for SymbolKey because you can't overload
// based on the difference.
if (!SymbolEquivalenceComparer.AreRefKindsEquivalent(
refKinds[i], parameters[i].RefKind, distinguishRefFromOut: false))
{
return false;
}
}
return true;
}
private static PooledArrayBuilder<TSymbol> GetMembersOfNamedType<TSymbol>(
SymbolKeyResolution containingTypeResolution,
string? metadataName) where TSymbol : ISymbol
{
var result = PooledArrayBuilder<TSymbol>.GetInstance();
foreach (var containingType in containingTypeResolution.OfType<INamedTypeSymbol>())
{
var members = metadataName == null
? containingType.GetMembers()
: containingType.GetMembers(metadataName);
foreach (var member in members)
{
if (member is TSymbol symbol)
result.AddIfNotNull(symbol);
}
}
return result;
}
public static bool IsBodyLevelSymbol(ISymbol symbol)
=> symbol switch
{
ILabelSymbol => true,
IRangeVariableSymbol => true,
ILocalSymbol => true,
IMethodSymbol { MethodKind: MethodKind.LocalFunction } => true,
_ => false,
};
private static int GetDataStartPosition(string key)
{
if (string.IsNullOrEmpty(key))
return 0;
using var reader = SymbolKeyReader.GetReader(key, compilation: null!, ignoreAssemblyKey: false, CancellationToken.None);
_ = reader.ReadFormatVersion();
_ = reader.ReadString();
return reader.Position;
}
public override readonly int GetHashCode()
{
var position = GetDataStartPosition(_symbolKeyData);
#if NETSTANDARD
var hashCode = 0;
foreach (var ch in _symbolKeyData[position..])
hashCode = Hash.Combine(ch, hashCode);
return hashCode;
#else
return string.GetHashCode(_symbolKeyData.AsSpan(position));
#endif
}
public override readonly bool Equals(object? obj)
=> obj is SymbolKey symbolKey && this.Equals(symbolKey);
public readonly bool Equals(SymbolKey other)
=> Equals(other, ignoreCase: false);
private readonly bool Equals(SymbolKey other, bool ignoreCase)
{
var position1 = GetDataStartPosition(_symbolKeyData);
var position2 = GetDataStartPosition(other._symbolKeyData);
var keySpan1 = _symbolKeyData.AsSpan(position1);
var keySpan2 = other._symbolKeyData.AsSpan(position2);
var comparison = ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
return keySpan1.Equals(keySpan2, comparison);
}
}
|