diff --git a/src/Build/Evaluation/Expander.cs b/src/Build/Evaluation/Expander.cs
index 77b0417866e..f03b8a2858d 100644
--- a/src/Build/Evaluation/Expander.cs
+++ b/src/Build/Evaluation/Expander.cs
@@ -123,6 +123,123 @@ internal class Expander
where P : class, IProperty
where I : class, IItem
{
+ ///
+ /// A helper struct wrapping a and providing file path conversion
+ /// as used in e.g. property expansion.
+ ///
+ ///
+ /// If exactly one value is added and no concatenation takes places, this value is returned without
+ /// conversion. In other cases values are stringified and attempted to be interpreted as file paths
+ /// before concatenation.
+ ///
+ private struct SpanBasedConcatenator : IDisposable
+ {
+ ///
+ /// The backing , null until the second value is added.
+ ///
+ private SpanBasedStringBuilder _builder;
+
+ ///
+ /// The first value added to the concatenator. Tracked in its own field so it can be returned
+ /// without conversion if no concatenation takes place.
+ ///
+ private object _firstObject;
+
+ ///
+ /// The first value added to the concatenator if it is a span. Tracked in its own field so the
+ /// functionality doesn't have to be invoked if no concatenation
+ /// takes place.
+ ///
+ private ReadOnlyMemory _firstSpan;
+
+ ///
+ /// Adds an object to be concatenated.
+ ///
+ public void Add(object obj)
+ {
+ FlushFirstValueIfNeeded();
+ if (_builder != null)
+ {
+ _builder.Append(FileUtilities.MaybeAdjustFilePath(obj.ToString()));
+ }
+ else
+ {
+ _firstObject = obj;
+ }
+ }
+
+ ///
+ /// Adds a span to be concatenated.
+ ///
+ public void Add(ReadOnlyMemory span)
+ {
+ FlushFirstValueIfNeeded();
+ if (_builder != null)
+ {
+ _builder.Append(FileUtilities.MaybeAdjustFilePath(span));
+ }
+ else
+ {
+ _firstSpan = span;
+ }
+ }
+
+ ///
+ /// Lazily initializes and populates it with the first value
+ /// when the second value is being added.
+ ///
+ private void FlushFirstValueIfNeeded()
+ {
+ if (_firstObject != null)
+ {
+ _builder = Strings.GetSpanBasedStringBuilder();
+ _builder.Append(FileUtilities.MaybeAdjustFilePath(_firstObject.ToString()));
+ _firstObject = null;
+ }
+ else if (!_firstSpan.IsEmpty)
+ {
+ _builder = Strings.GetSpanBasedStringBuilder();
+#if FEATURE_SPAN
+ _builder.Append(FileUtilities.MaybeAdjustFilePath(_firstSpan));
+#else
+ _builder.Append(FileUtilities.MaybeAdjustFilePath(_firstSpan.ToString()));
+#endif
+ _firstSpan = new ReadOnlyMemory();
+ }
+ }
+
+ ///
+ /// Returns the result of the concatenation.
+ ///
+ ///
+ /// If only one value has been added and it is not a string, it is returned unchanged.
+ /// In all other cases (no value, one string value, multiple values) the result is a
+ /// concatenation of the string representation of the values, each additionally subjected
+ /// to file path adjustment.
+ ///
+ public object GetResult()
+ {
+ if (_firstObject != null)
+ {
+ if (_firstObject is string stringValue)
+ {
+ return FileUtilities.MaybeAdjustFilePath(stringValue);
+ }
+ return _firstObject;
+ }
+ if (!_firstSpan.IsEmpty)
+ {
+ return FileUtilities.MaybeAdjustFilePath(_firstSpan).ToString();
+ }
+ return _builder?.ToString() ?? string.Empty;
+ }
+
+ ///
+ /// Disposes of the struct by delegating the call to the underlying .
+ ///
+ public void Dispose() => _builder?.Dispose();
+ }
+
///
/// A limit for truncating string expansions within an evaluated Condition. Properties, item metadata, or item groups will be truncated to N characters such as 'N...'.
/// Enabled by ExpanderOptions.Truncate.