|
// 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;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
#nullable disable
namespace Microsoft.Build.Collections
{
/// <summary>
/// A dictionary of unordered property or metadata name/value pairs.
/// </summary>
/// <remarks>
/// The value that this adds over IDictionary<string, T> is:
/// - enforces that key = T.Name
/// - default enumerator is over values
/// - (marginal) enforces the correct key comparer
/// - potentially makes copy on write possible
///
/// Really a Dictionary<string, T> where the key (the name) is obtained from IKeyed.Key.
/// Is not observable, so if clients wish to observe modifications they must mediate them themselves and
/// either not expose this collection or expose it through a readonly wrapper.
/// At various places in this class locks are taken on the backing collection. The reason for this is to allow
/// this class to be asynchronously enumerated. This is accomplished by the CopyOnReadEnumerable which will
/// lock the backing collection when it does its deep cloning. This prevents asynchronous access from corrupting
/// the state of the enumeration until the collection has been fully copied.
///
/// Since we use the mutable ignore case comparer we need to make sure that we lock our self before we call the comparer since the comparer can call back
/// into this dictionary which could cause a deadlock if another thread is also accessing another method in the dictionary.
/// </remarks>
/// <typeparam name="T">Property or Metadata class type to store</typeparam>
[DebuggerDisplay("#Entries={Count}")]
internal sealed class PropertyDictionary<T> : IEnumerable<T>, IEquatable<PropertyDictionary<T>>, IPropertyProvider<T>, IDictionary<string, T>, IConstrainableDictionary<T>
where T : class, IKeyed, IValued, IEquatable<T>
{
/// <summary>
/// Backing dictionary
/// </summary>
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
private readonly IRetrievableValuedEntryHashSet<T> _properties;
/// <summary>
/// Creates empty dictionary
/// </summary>
public PropertyDictionary()
{
_properties = new RetrievableValuedEntryHashSet<T>(MSBuildNameIgnoreCaseComparer.Default);
}
/// <summary>
/// Creates empty dictionary, optionally specifying initial capacity
/// </summary>
internal PropertyDictionary(int capacity)
{
_properties = new RetrievableValuedEntryHashSet<T>(capacity, MSBuildNameIgnoreCaseComparer.Default);
}
/// <summary>
/// Create a new dictionary from an enumerator
/// </summary>
internal PropertyDictionary(IEnumerable<T> elements)
: this()
{
foreach (T element in elements)
{
Set(element);
}
}
/// <summary>
/// Creates empty dictionary, specifying a comparer
/// </summary>
internal PropertyDictionary(MSBuildNameIgnoreCaseComparer comparer)
{
_properties = new RetrievableValuedEntryHashSet<T>(comparer);
}
/// <summary>
/// Create a new dictionary from an enumerator
/// </summary>
internal PropertyDictionary(int capacity, IEnumerable<T> elements)
: this(capacity)
{
foreach (T element in elements)
{
Set(element);
}
}
/// <summary>
/// Initializes a new instance of the <see cref="PropertyDictionary{T}"/> class.
/// </summary>
/// <param name="propertiesHashSet">The collection of properties to use.</param>
internal PropertyDictionary(IRetrievableValuedEntryHashSet<T> propertiesHashSet)
{
_properties = propertiesHashSet;
}
/// <summary>
/// Accessor for the list of property names
/// </summary>
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
ICollection<string> IDictionary<string, T>.Keys
{
get
{
ErrorUtilities.ThrowInternalError("Keys is not supported on PropertyDictionary.");
// Show the compiler that this always throws:
throw new NotImplementedException();
}
}
/// <summary>
/// Accessor for the list of properties
/// </summary>
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
ICollection<T> IDictionary<string, T>.Values
{
get
{
lock (_properties)
{
return _properties.Values;
}
}
}
/// <summary>
/// Returns the number of properties in the collection
/// </summary>
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
int ICollection<KeyValuePair<string, T>>.Count => Count;
/// <summary>
/// Whether the collection is read-only.
/// </summary>
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
bool ICollection<KeyValuePair<string, T>>.IsReadOnly => false;
/// <summary>
/// Returns the number of property in the collection.
/// </summary>
public int Count
{
get
{
lock (_properties)
{
return ((ICollection<T>)_properties).Count;
}
}
}
/// <summary>
/// Get the property with the specified name, or null if none exists.
/// Sets the property with the specified name, overwriting it if already exists.
/// </summary>
/// <remarks>
/// Unlike Dictionary<K,V>[K], the getter returns null instead of throwing if the key does not exist.
/// This better matches the semantics of property, which are considered to have a blank value if they
/// are not defined.
/// </remarks>
T IDictionary<string, T>.this[string name]
{
get => this[name];
set => this[name] = value;
}
/// <summary>
/// Get the property with the specified name, or null if none exists.
/// Sets the property with the specified name, overwriting it if already exists.
/// </summary>
/// <remarks>
/// Unlike Dictionary<K,V>[K], the getter returns null instead of throwing if the key does not exist.
/// This better matches the semantics of property, which are considered to have a blank value if they
/// are not defined.
/// </remarks>
internal T this[string name]
{
get
{
// We don't want to check for a zero length name here, since that is a valid name
// and should return a null instance which will be interpreted as blank
T projectProperty;
lock (_properties)
{
_properties.TryGetValue(name, out projectProperty);
}
return projectProperty;
}
set
{
ErrorUtilities.VerifyThrowInternalNull(value, "Properties can't have null value");
ErrorUtilities.VerifyThrow(String.Equals(name, value.Key, StringComparison.OrdinalIgnoreCase), "Key must match value's key");
Set(value);
}
}
/// <summary>
/// Returns an enumerable which copies the underlying data on read.
/// </summary>
public IEnumerable<TResult> GetCopyOnReadEnumerable<TResult>(Func<T, TResult> selector)
{
return new CopyOnReadEnumerable<T, TResult>(this, _properties, selector);
}
/// <summary>
/// Returns true if a property with the specified name is present in the collection,
/// otherwise false.
/// </summary>
public bool Contains(string name)
{
return ((IDictionary<string, T>)this).ContainsKey(name);
}
/// <summary>
/// Empties the collection
/// </summary>
public void Clear()
{
lock (_properties)
{
((ICollection<T>)_properties).Clear();
}
}
/// <summary>
/// Gets an enumerator over all the properties in the collection
/// Enumeration is in undefined order
/// </summary>
public IEnumerator<T> GetEnumerator()
{
lock (_properties)
{
return _properties.Values.GetEnumerator();
}
}
/// <summary>
/// Get an enumerator over entries
/// </summary>
IEnumerator IEnumerable.GetEnumerator()
{
lock (_properties)
{
return ((IEnumerable)_properties.Values).GetEnumerator();
}
}
#region IEquatable<PropertyDictionary<T>> Members
/// <summary>
/// Compares two property dictionaries for equivalence. They are equal if each contains the same properties with the
/// same values as the other, unequal otherwise.
/// </summary>
/// <param name="other">The dictionary to which this should be compared</param>
/// <returns>True if they are equivalent, false otherwise.</returns>
public bool Equals(PropertyDictionary<T> other)
{
if (other == null)
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
if (Count != other.Count)
{
return false;
}
lock (_properties)
{
foreach (T leftProp in this)
{
T rightProp = other[leftProp.Key];
if (rightProp?.Equals(leftProp) != true)
{
return false;
}
}
}
return true;
}
#endregion
/// <summary>
/// Get the property with the specified name or null if it is not present
/// </summary>
public T GetProperty(string name)
{
// The properties lock is locked in indexor
return this[name];
}
/// <summary>
/// Get the property with the specified name or null if it is not present.
/// Name is the segment of the provided string with the provided start and end indexes.
/// </summary>
public T GetProperty(string name, int startIndex, int endIndex)
{
lock (_properties)
{
return _properties.Get(name, startIndex, endIndex - startIndex + 1);
}
}
/// <inheritdoc />
public T Get(string keyString, int startIndex, int endIndex)
{
return GetProperty(keyString, startIndex, endIndex);
}
/// <summary>
/// Gets the unescaped value of a particular property.
/// </summary>
/// <param name="propertyName">The name of the property whose value is sought.</param>
/// <param name="unescapedValue">The out parameter by which a successfully retrieved value is returned.</param>
/// <returns>True if a property with a matching name was found. False otherwise.</returns>
public bool TryGetPropertyUnescapedValue(string propertyName, out string unescapedValue)
{
if (_properties.TryGetEscapedValue(propertyName, out string escapedValue) && escapedValue != null)
{
unescapedValue = EscapingUtilities.UnescapeAll(escapedValue);
return true;
}
unescapedValue = null;
return false;
}
#region IDictionary<string,T> Members
/// <summary>
/// Adds a property
/// </summary>
void IDictionary<string, T>.Add(string key, T value)
{
ErrorUtilities.VerifyThrow(key == value.Key, "Key must match value's key");
// The properties lock is locked in the set method
Set(value);
}
/// <summary>
/// Returns true if the dictionary contains the key
/// </summary>
bool IDictionary<string, T>.ContainsKey(string key)
{
lock (_properties)
{
return _properties.ContainsKey(key);
}
}
/// <summary>
/// Removes a property
/// </summary>
bool IDictionary<string, T>.Remove(string key)
{
return Remove(key);
}
/// <summary>
/// Attempts to retrieve the a property.
/// </summary>
bool IDictionary<string, T>.TryGetValue(string key, out T value)
{
value = this[key];
return value != null;
}
#endregion
#region ICollection<KeyValuePair<string,T>> Members
/// <summary>
/// Adds a property
/// </summary>
void ICollection<KeyValuePair<string, T>>.Add(KeyValuePair<string, T> item)
{
((IDictionary<string, T>)this).Add(item.Key, item.Value);
}
/// <summary>
/// Clears the property collection
/// </summary>
void ICollection<KeyValuePair<string, T>>.Clear()
{
Clear();
}
/// <summary>
/// Checks for a property in the collection
/// </summary>
bool ICollection<KeyValuePair<string, T>>.Contains(KeyValuePair<string, T> item)
{
lock (_properties)
{
if (_properties.TryGetValue(item.Key, out T value))
{
return ReferenceEquals(value, item.Value);
}
}
return false;
}
/// <summary>
/// Not implemented
/// </summary>
void ICollection<KeyValuePair<string, T>>.CopyTo(KeyValuePair<string, T>[] array, int arrayIndex)
{
ErrorUtilities.ThrowInternalError("CopyTo is not supported on PropertyDictionary.");
}
/// <summary>
/// Removes a property from the collection
/// </summary>
bool ICollection<KeyValuePair<string, T>>.Remove(KeyValuePair<string, T> item)
{
ErrorUtilities.VerifyThrow(item.Key == item.Value.Key, "Key must match value's key");
// The properties lock is locked in the remove method
return ((IDictionary<string, T>)this).Remove(item.Key);
}
#endregion
#region IEnumerable<KeyValuePair<string,T>> Members
/// <summary>
/// Get an enumerator over the entries.
/// </summary>
IEnumerator<KeyValuePair<string, T>> IEnumerable<KeyValuePair<string, T>>.GetEnumerator()
{
return ((IEnumerable<KeyValuePair<string, T>>)_properties).GetEnumerator();
}
#endregion
/// <summary>
/// Removes any property with the specified name.
/// Returns true if the property was in the collection, otherwise false.
/// </summary>
internal bool Remove(string name)
{
ErrorUtilities.VerifyThrowArgumentLength(name);
lock (_properties)
{
bool result = _properties.Remove(name);
return result;
}
}
/// <summary>
/// Add the specified property to the collection.
/// Overwrites any property with the same name already in the collection.
/// To remove a property, use Remove(...) instead.
/// </summary>
internal void Set(T projectProperty)
{
ErrorUtilities.VerifyThrowArgumentNull(projectProperty);
lock (_properties)
{
_properties[projectProperty.Key] = projectProperty;
}
}
/// <summary>
/// Adds the specified properties to this dictionary.
/// </summary>
/// <param name="other">An enumerator over the properties to add.</param>
internal void ImportProperties(IEnumerable<T> other)
{
// The properties lock is locked in the set method
foreach (T property in other)
{
Set(property);
}
}
/// <summary>
/// Removes the specified properties from this dictionary
/// </summary>
/// <param name="other">An enumerator over the properties to remove.</param>
internal void RemoveProperties(IEnumerable<T> other)
{
// The properties lock is locked in the set method
foreach (T property in other)
{
Remove(property.Key);
}
}
/// <summary>
/// Helper to convert into a read-only dictionary of string, string.
/// TODO: for performance, consider switching to returning IDictionary
/// and returning ArrayDictionary if lookup of results is not needed.
/// </summary>
internal Dictionary<string, string> ToDictionary()
{
lock (_properties)
{
var dictionary = new Dictionary<string, string>(((ICollection<T>)_properties).Count, MSBuildNameIgnoreCaseComparer.Default);
foreach (T property in this)
{
dictionary[property.Key] = property.EscapedValue;
}
return dictionary;
}
}
internal IEnumerable<PropertyData> Enumerate()
{
lock (_properties)
{
foreach (var kvp in (ICollection<T>)_properties)
{
yield return new(kvp.Key, EscapingUtilities.UnescapeAll(kvp.EscapedValue));
}
}
}
internal void Enumerate(Action<string, string> keyValueCallback)
{
foreach (var property in Enumerate())
{
keyValueCallback(property.Name, property.Value);
}
}
internal IEnumerable<TResult> Filter<TResult>(Func<T, bool> filter, Func<T, TResult> selector)
{
List<TResult> result = new();
lock (_properties)
{
foreach (T property in (ICollection<T>)_properties)
{
if (filter(property))
{
result.Add(selector(property));
}
}
}
return result;
}
}
}
|