|
// 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 Microsoft.Build.Shared;
namespace Microsoft.Build.Framework
{
/// <summary>
/// A strongly-typed wrapper around <see cref="ITaskItem"/> that parses the item's identity
/// as a value of type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type to parse the item identity as. Can be a value type, FileInfo, or DirectoryInfo.</typeparam>
/// <remarks>
/// This allows tasks to receive strongly-typed parameters while still working with MSBuild's item system.
/// The identity (ItemSpec) is parsed using <see cref="ValueTypeParser"/> which handles value types,
/// AbsolutePath, FileInfo, and DirectoryInfo.
/// </remarks>
public readonly struct TaskItem<T> : ITaskItem<T>, IEquatable<TaskItem<T>>
{
private readonly ITaskItem? _backingItem;
/// <summary>
/// Gets the strongly-typed value parsed from the item's identity.
/// </summary>
public T Value { get; }
/// <summary>
/// Initializes a new instance of <see cref="TaskItem{T}"/> from a strongly-typed value.
/// </summary>
/// <param name="value">The value to wrap.</param>
public TaskItem(T value)
{
Value = value;
// Use a mutable backing so metadata mutations are supported
string itemSpec = ValueTypeParser.ToString(value);
_backingItem = new MutableTaskItem(itemSpec);
}
/// <summary>
/// Initializes a new instance of <see cref="TaskItem{T}"/> from an <see cref="ITaskItem"/>.
/// </summary>
/// <param name="item">The task item to parse.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="item"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown if the item's identity cannot be parsed as type <typeparamref name="T"/>.</exception>
public TaskItem(ITaskItem item)
{
if (item == null)
{
throw new ArgumentNullException(nameof(item));
}
_backingItem = item;
string itemSpec = item.ItemSpec;
if (string.IsNullOrEmpty(itemSpec))
{
throw new ArgumentException($"Cannot create TaskItem<{typeof(T).Name}> from an item with empty identity.", nameof(item));
}
// For path-like types, prefer the FullPath metadata (which is always absolute)
// over ItemSpec which may be relative.
string parseFrom = TaskItemTypeDetector.IsSupportedPathType(typeof(T)) ? GetFullPathOrItemSpec(item, itemSpec) : itemSpec;
Value = ParseValue(parseFrom);
}
/// <summary>Returns the FullPath metadata value if non-empty, otherwise falls back to itemSpec.</summary>
private static string GetFullPathOrItemSpec(ITaskItem item, string itemSpec)
{
string fullPath = item.GetMetadata("FullPath");
return !string.IsNullOrEmpty(fullPath) ? fullPath : itemSpec;
}
/// <summary>
/// Parses a string value as type <typeparamref name="T"/>.
/// Uses the unified ValueTypeParser for consistent parsing behavior across MSBuild.
/// </summary>
private static T ParseValue(string value) => (T)ValueTypeParser.Parse(value, typeof(T));
/// <summary>
/// Converts the strongly-typed value to its string representation for use as ItemSpec.
/// Uses the unified ValueTypeParser for consistent formatting behavior across MSBuild.
/// </summary>
private string ValueToString() => ValueTypeParser.ToString(Value);
#region ITaskItem Implementation
/// <inheritdoc/>
public string? ItemSpec
{
get => _backingItem?.ItemSpec;
set => throw new NotSupportedException("TaskItem<T> ItemSpec is read-only. Create a new instance to change the value.");
}
/// <inheritdoc/>
public ICollection? MetadataNames => _backingItem?.MetadataNames;
/// <inheritdoc/>
public int MetadataCount => _backingItem?.MetadataCount ?? 0;
/// <inheritdoc/>
public string? GetMetadata(string metadataName) => _backingItem?.GetMetadata(metadataName);
/// <inheritdoc/>
public void SetMetadata(string metadataName, string metadataValue) =>
_backingItem?.SetMetadata(metadataName, metadataValue);
/// <inheritdoc/>
public void RemoveMetadata(string metadataName) =>
_backingItem?.RemoveMetadata(metadataName);
/// <inheritdoc/>
public void CopyMetadataTo(ITaskItem destinationItem) =>
_backingItem?.CopyMetadataTo(destinationItem);
/// <inheritdoc/>
public IDictionary? CloneCustomMetadata() => _backingItem?.CloneCustomMetadata();
#endregion
#region ITaskItem2 Implementation
/// <inheritdoc/>
public string? EvaluatedIncludeEscaped
{
get => (_backingItem as ITaskItem2)?.EvaluatedIncludeEscaped ?? ItemSpec;
set => throw new NotSupportedException("TaskItem<T> EvaluatedIncludeEscaped is read-only. Create a new instance to change the value.");
}
/// <inheritdoc/>
public string GetMetadataValueEscaped(string metadataName)
{
return (_backingItem as ITaskItem2)?.GetMetadataValueEscaped(metadataName) ?? string.Empty;
}
/// <inheritdoc/>
public void SetMetadataValueLiteral(string metadataName, string metadataValue)
{
if (_backingItem is ITaskItem2 taskItem2)
{
taskItem2.SetMetadataValueLiteral(metadataName, metadataValue);
}
else if (_backingItem is ITaskItem taskItem)
{
var escapedValue = EscapingUtilities.Escape(metadataValue);
taskItem.SetMetadata(metadataName, escapedValue);
}
}
/// <inheritdoc/>
public IDictionary CloneCustomMetadataEscaped()
{
return (_backingItem as ITaskItem2)?.CloneCustomMetadataEscaped() ?? new Hashtable();
}
#endregion
#region Equality and Conversion
/// <inheritdoc/>
public bool Equals(TaskItem<T> other) =>
EqualityComparer<T>.Default.Equals(Value, other.Value);
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is TaskItem<T> other && Equals(other);
/// <inheritdoc/>
public override int GetHashCode() => Value is null ? 0 : EqualityComparer<T>.Default.GetHashCode(Value);
/// <summary>
/// Determines whether two <see cref="TaskItem{T}"/> instances are equal.
/// </summary>
public static bool operator ==(TaskItem<T> left, TaskItem<T> right) => left.Equals(right);
/// <summary>
/// Determines whether two <see cref="TaskItem{T}"/> instances are not equal.
/// </summary>
public static bool operator !=(TaskItem<T> left, TaskItem<T> right) => !left.Equals(right);
/// <summary>
/// Implicitly converts a <see cref="TaskItem{T}"/> to its strongly-typed value.
/// </summary>
public static implicit operator T(TaskItem<T> taskItem) => taskItem.Value;
/// <summary>
/// Implicitly converts a strongly-typed value to a <see cref="TaskItem{T}"/>.
/// </summary>
public static implicit operator TaskItem<T>(T value) => new TaskItem<T>(value);
#endregion
/// <inheritdoc/>
public override string ToString() => ValueToString();
}
}
|