diff --git a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs index 46273a65ec1..6ebe0c313bc 100644 --- a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs +++ b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs @@ -393,7 +393,7 @@ public bool InitializeForBatch(TaskLoggingContext loggingContext, ItemBucket bat TaskInstance.BuildEngine = _buildEngine; TaskInstance.HostObject = _taskHost; - + if (TaskInstance is IMultiThreadableTask multiThreadableTask) { multiThreadableTask.TaskEnvironment = TaskEnvironment; @@ -1488,7 +1488,7 @@ private void GatherTaskItemOutputs(bool outputTargetIsItem, string outputTargetN ProjectItemInstance newItem; TaskItem outputAsProjectItem = output as TaskItem; - string parameterLocationEscaped = EscapingUtilities.EscapeWithCaching(parameterLocation.File); + string parameterLocationEscaped = EscapingUtilities.Escape(parameterLocation.File, cache: true); if (outputAsProjectItem != null) { diff --git a/src/Framework.UnitTests/BufferScopeTests.cs b/src/Framework.UnitTests/BufferScopeTests.cs new file mode 100644 index 00000000000..1c31b1a5ca4 --- /dev/null +++ b/src/Framework.UnitTests/BufferScopeTests.cs @@ -0,0 +1,468 @@ +// 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.Generic; +using Microsoft.Build.Utilities; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.Framework.UnitTests; + +public unsafe class BufferScopeTests +{ + [Fact] + public void Construct_WithStackAlloc() + { + using BufferScope buffer = new(stackalloc char[10]); + buffer.Length.ShouldBe(10); + buffer[0] = 'Y'; + buffer[..1].ToString().ShouldBe("Y"); + } + + [Fact] + public void Construct_WithStackAlloc_GrowAndCopy() + { + using BufferScope buffer = new(stackalloc char[10]); + buffer.Length.ShouldBe(10); + buffer[0] = 'Y'; + buffer.EnsureCapacity(64, copy: true); + buffer.Length.ShouldBeGreaterThanOrEqualTo(64); + buffer[..1].ToString().ShouldBe("Y"); + } + + [Fact] + public void Construct_WithStackAlloc_Pin() + { + using BufferScope buffer = new(stackalloc char[10]); + buffer.Length.ShouldBe(10); + buffer[0] = 'Y'; + fixed (char* c = buffer) + { + (*c).ShouldBe('Y'); + *c = 'Z'; + } + + buffer[..1].ToString().ShouldBe("Z"); + } + + [Fact] + public void Construct_GrowAndCopy() + { + using BufferScope buffer = new(32); + buffer.Length.ShouldBeGreaterThanOrEqualTo(32); + buffer[0] = 'Y'; + buffer.EnsureCapacity(64, copy: true); + buffer.Length.ShouldBeGreaterThanOrEqualTo(64); + buffer[..1].ToString().ShouldBe("Y"); + } + + [Fact] + public void Construct_Pin() + { + using BufferScope buffer = new(64); + buffer.Length.ShouldBeGreaterThanOrEqualTo(64); + buffer[0] = 'Y'; + fixed (char* c = buffer) + { + (*c).ShouldBe('Y'); + *c = 'Z'; + } + + buffer[..1].ToString().ShouldBe("Z"); + } + + [Fact] + public void Construct_WithMinimumLength_ZeroLength() + { + using BufferScope buffer = new(0); + buffer.Length.ShouldBeGreaterThanOrEqualTo(0); + } + + [Fact] + public void Construct_WithMinimumLength_SmallLength() + { + using BufferScope buffer = new(5); + buffer.Length.ShouldBeGreaterThanOrEqualTo(5); + } + + [Fact] + public void Construct_WithSpan_EmptySpan() + { + using BufferScope buffer = new([]); + buffer.Length.ShouldBe(0); + } + + [Fact] + public void Construct_WithSpanAndMinimumLength_SpanLargerThanMinimum() + { + using BufferScope buffer = new(stackalloc char[20], 10); + buffer.Length.ShouldBe(20); + buffer[0] = 'A'; + buffer[19] = 'Z'; + buffer[0].ShouldBe('A'); + buffer[19].ShouldBe('Z'); + } + + [Fact] + public void Construct_WithSpanAndMinimumLength_SpanSmallerThanMinimum() + { + using BufferScope buffer = new(stackalloc char[5], 20); + buffer.Length.ShouldBeGreaterThanOrEqualTo(20); + } + + [Fact] + public void Construct_WithSpanAndMinimumLength_SpanEqualsMinimum() + { + using BufferScope buffer = new(stackalloc char[10], 10); + buffer.Length.ShouldBe(10); + } + + [Fact] + public void EnsureCapacity_AlreadySufficientCapacity() + { + using BufferScope buffer = new(20); + int originalLength = buffer.Length; + buffer[0] = 42; + + buffer.EnsureCapacity(10); + + buffer.Length.ShouldBe(originalLength); + buffer[0].ShouldBe(42); + } + + [Fact] + public void EnsureCapacity_GrowWithoutCopy() + { + using BufferScope buffer = new(10); + buffer[0] = 42; + buffer[5] = 100; + + buffer.EnsureCapacity(50, copy: false); + + buffer.Length.ShouldBeGreaterThanOrEqualTo(50); + // Data should not be copied when copy is false + } + + [Fact] + public void EnsureCapacity_GrowFromStackAllocWithCopy() + { + using BufferScope buffer = new(stackalloc char[5]); + buffer[0] = 'H'; + buffer[1] = 'e'; + buffer[2] = 'l'; + buffer[3] = 'l'; + buffer[4] = 'o'; + + buffer.EnsureCapacity(20, copy: true); + + buffer.Length.ShouldBeGreaterThanOrEqualTo(20); + buffer[0].ShouldBe('H'); + buffer[1].ShouldBe('e'); + buffer[2].ShouldBe('l'); + buffer[3].ShouldBe('l'); + buffer[4].ShouldBe('o'); + } + + [Fact] + public void EnsureCapacity_MultipleGrows() + { + using BufferScope buffer = new(5); + buffer[0] = 1; + buffer[1] = 2; + + buffer.EnsureCapacity(10, copy: true); + buffer[0].ShouldBe(1); + buffer[1].ShouldBe(2); + + buffer.EnsureCapacity(50, copy: true); + buffer.Length.ShouldBeGreaterThanOrEqualTo(50); + buffer[0].ShouldBe(1); + buffer[1].ShouldBe(2); + } + + [Fact] + public void Indexer_Range_FullRange() + { + using BufferScope buffer = new(stackalloc char[5]); + buffer[0] = 'A'; + buffer[1] = 'B'; + buffer[2] = 'C'; + buffer[3] = 'D'; + buffer[4] = 'E'; + + Span slice = buffer[..]; + slice.Length.ShouldBe(5); + slice[0].ShouldBe('A'); + slice[4].ShouldBe('E'); + } + + [Fact] + public void Indexer_Range_PartialRange() + { + using BufferScope buffer = new(stackalloc int[10]); + for (int i = 0; i < 10; i++) + { + buffer[i] = i; + } + + Span slice = buffer[2..8]; + slice.Length.ShouldBe(6); + slice[0].ShouldBe(2); + slice[5].ShouldBe(7); + } + + [Fact] + public void Indexer_Range_EmptyRange() + { + using BufferScope buffer = new(10); + Span slice = buffer[5..5]; + slice.Length.ShouldBe(0); + } + + [Fact] + public void Slice_ValidRange() + { + using BufferScope buffer = new(stackalloc char[10]); + for (int i = 0; i < 10; i++) + { + buffer[i] = (char)('A' + i); + } + + Span slice = buffer.Slice(3, 4); + slice.Length.ShouldBe(4); + slice[0].ShouldBe('D'); + slice[3].ShouldBe('G'); + } + + [Fact] + public void Slice_ZeroLength() + { + using BufferScope buffer = new(10); + Span slice = buffer.Slice(5, 0); + slice.Length.ShouldBe(0); + } + + [Fact] + public void AsSpan_ReturnsCorrectSpan() + { + using BufferScope buffer = new(5); + buffer[0] = 3.14; + buffer[1] = 2.71; + + Span span = buffer.AsSpan(); + span.Length.ShouldBe(buffer.Length); + span[0].ShouldBe(3.14); + span[1].ShouldBe(2.71); + } + + [Fact] + public void ImplicitOperator_ToSpan() + { + using BufferScope buffer = new(stackalloc int[3]); + buffer[0] = 10; + buffer[1] = 20; + buffer[2] = 30; + + Span span = buffer; + span.Length.ShouldBe(3); + span[0].ShouldBe(10); + span[1].ShouldBe(20); + span[2].ShouldBe(30); + } + + [Fact] + public void ImplicitOperator_ToReadOnlySpan() + { + using BufferScope buffer = new(stackalloc char[3]); + buffer[0] = 'X'; + buffer[1] = 'Y'; + buffer[2] = 'Z'; + + ReadOnlySpan readOnlySpan = buffer; + readOnlySpan.Length.ShouldBe(3); + readOnlySpan[0].ShouldBe('X'); + readOnlySpan[1].ShouldBe('Y'); + readOnlySpan[2].ShouldBe('Z'); + } + + [Fact] + public void GetEnumerator_IteratesCorrectly() + { + using BufferScope buffer = new(stackalloc int[5]); + for (int i = 0; i < 5; i++) + { + buffer[i] = i * 10; + } + + var values = new List(); + foreach (int value in buffer) + { + values.Add(value); + } + + values.ShouldBe([0, 10, 20, 30, 40]); + } + + [Fact] + public void GetEnumerator_EmptyBuffer() + { + using BufferScope buffer = new([]); + + var values = new List(); + foreach (string value in buffer) + { + values.Add(value); + } + + values.ShouldBeEmpty(); + } + + [Fact] + public void ToString_WithCharBuffer() + { + using BufferScope buffer = new(stackalloc char[5]); + buffer[0] = 'H'; + buffer[1] = 'e'; + buffer[2] = 'l'; + buffer[3] = 'l'; + buffer[4] = 'o'; + + string result = buffer.ToString(); + result.ShouldBe("Hello"); + } + + [Fact] + public void ToString_EmptyBuffer() + { + using BufferScope buffer = new([]); + string result = buffer.ToString(); + result.ShouldBe(""); + } + + [Fact] + public void GetPinnableReference_WithStackAlloc() + { + using BufferScope buffer = new(stackalloc byte[10]); + buffer[0] = 255; + buffer[9] = 128; + + ref byte reference = ref buffer.GetPinnableReference(); + reference.ShouldBe((byte)255); + + // Modify through reference + reference = 100; + buffer[0].ShouldBe((byte)100); + } + + [Fact] + public void GetPinnableReference_EmptyBuffer() + { + using BufferScope buffer = new([]); + ref int reference = ref buffer.GetPinnableReference(); + // Should not throw, but reference may be to null location + } + + [Fact] + public void Dispose_MultipleCallsSafe() + { + BufferScope buffer = new(100); + buffer[0] = 42; + + buffer.Dispose(); + buffer.Dispose(); // Should not throw + } + + [Fact] + public void Dispose_StackAllocBuffer() + { + BufferScope buffer = new(stackalloc int[10]); + buffer[0] = 42; + + buffer.Dispose(); // Should not throw for stack allocated buffer + } + + [Theory] + [InlineData(1)] + [InlineData(100)] + [InlineData(1000)] + [InlineData(10000)] + public void Construct_WithMinimumLength_VariousSizes(int minimumLength) + { + using BufferScope buffer = new(minimumLength); + buffer.Length.ShouldBeGreaterThanOrEqualTo(minimumLength); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(10)] + [InlineData(100)] + public void EnsureCapacity_BoundaryConditions(int capacity) + { + using BufferScope buffer = new(5); + buffer.EnsureCapacity(capacity); + buffer.Length.ShouldBeGreaterThanOrEqualTo(Math.Max(5, capacity)); + } + + [Fact] + public void WorksWithDifferentTypes_String() + { + using BufferScope buffer = new(5); + buffer[0] = "Hello"; + buffer[1] = "World"; + buffer[0].ShouldBe("Hello"); + buffer[1].ShouldBe("World"); + } + + [Fact] + public void WorksWithDifferentTypes_CustomStruct() + { + using BufferScope buffer = new(3); + var date1 = new DateTime(2025, 1, 1); + var date2 = new DateTime(2025, 12, 31); + + buffer[0] = date1; + buffer[1] = date2; + + buffer[0].ShouldBe(date1); + buffer[1].ShouldBe(date2); + } + + [Fact] + public void CombinedOperations_ComplexScenario() + { + using BufferScope buffer = new(stackalloc int[5], 10); + + // Initial setup + for (int i = 0; i < buffer.Length; i++) + { + buffer[i] = i + 1; + } + + // Grow the buffer + buffer.EnsureCapacity(20, copy: true); + + // Verify data was copied + for (int i = 0; i < Math.Min(5, buffer.Length); i++) + { + buffer[i].ShouldBe(i + 1); + } + + // Test slicing + Span slice = buffer[1..4]; + slice.Length.ShouldBe(3); + slice[0].ShouldBe(2); + slice[2].ShouldBe(4); + + // Test enumeration + var firstFive = buffer.AsSpan()[..5]; + var values = new List(); + foreach (int value in firstFive) + { + values.Add(value); + } + + values.ShouldBe([1, 2, 3, 4, 5]); + } +} diff --git a/src/Framework.UnitTests/EscapingUtilities_Tests.cs b/src/Framework.UnitTests/EscapingUtilities_Tests.cs index 3a6ddc09c8a..693b2c30c3a 100644 --- a/src/Framework.UnitTests/EscapingUtilities_Tests.cs +++ b/src/Framework.UnitTests/EscapingUtilities_Tests.cs @@ -2,92 +2,94 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Build.Shared; +using Shouldly; using Xunit; -#nullable disable - namespace Microsoft.Build.Framework.UnitTests; public sealed class EscapingUtilities_Tests { - /// - /// - [Fact] - public void Unescape() - { - Assert.Equal("", EscapingUtilities.UnescapeAll("")); - Assert.Equal("foo", EscapingUtilities.UnescapeAll("foo")); - Assert.Equal("foo space", EscapingUtilities.UnescapeAll("foo%20space")); - Assert.Equal("foo2;", EscapingUtilities.UnescapeAll("foo2%3B")); - Assert.Equal(";foo3", EscapingUtilities.UnescapeAll("%3bfoo3")); - Assert.Equal(";", EscapingUtilities.UnescapeAll("%3b")); - Assert.Equal(";;;;;", EscapingUtilities.UnescapeAll("%3b%3B;%3b%3B")); - Assert.Equal("%3B", EscapingUtilities.UnescapeAll("%253B")); - Assert.Equal("===%ZZ %%%===", EscapingUtilities.UnescapeAll("===%ZZ%20%%%===")); - Assert.Equal("hello; escaping% how( are) you?", EscapingUtilities.UnescapeAll("hello%3B escaping%25 how%28 are%29 you%3f")); - - Assert.Equal("%*?*%*", EscapingUtilities.UnescapeAll("%25*?*%25*")); - Assert.Equal("%*?*%*", EscapingUtilities.UnescapeAll("%25%2a%3f%2a%25%2a")); - - Assert.Equal("*Star*craft or *War*cr@ft??", EscapingUtilities.UnescapeAll("%2aStar%2Acraft%20or %2aWar%2Acr%40ft%3f%3F")); - } - - /// - /// - [Fact] - public void Escape() - { - Assert.Equal("%2a", EscapingUtilities.Escape("*")); - Assert.Equal("%3f", EscapingUtilities.Escape("?")); - Assert.Equal("#%2a%3f%2a#%2a", EscapingUtilities.Escape("#*?*#*")); - Assert.Equal("%25%2a%3f%2a%25%2a", EscapingUtilities.Escape("%*?*%*")); - } - - /// - /// - [Fact] - public void UnescapeEscape() - { - string text = "*"; - Assert.Equal(text, EscapingUtilities.UnescapeAll(EscapingUtilities.Escape(text))); - - text = "?"; - Assert.Equal(text, EscapingUtilities.UnescapeAll(EscapingUtilities.Escape(text))); + [Theory] + [InlineData("", "")] + [InlineData("foo", "foo")] + [InlineData("foo%", "foo%")] + [InlineData("foo%3", "foo%3")] + [InlineData("foo%20space", "foo space")] + [InlineData("foo2%3B", "foo2;")] + [InlineData("%3bfoo3", ";foo3")] + [InlineData("%3b", ";")] + [InlineData("%3b%3B;%3b%3B", ";;;;;")] + [InlineData("%253B", "%3B")] + [InlineData("===%ZZ%20%%%===", "===%ZZ %%%===")] + [InlineData("hello%3B escaping%25 how%28 are%29 you%3f", "hello; escaping% how( are) you?")] + [InlineData("%25*?*%25*", "%*?*%*")] + [InlineData("%25%2a%3f%2a%25%2a", "%*?*%*")] + [InlineData("%2aStar%2Acraft%20or %2aWar%2Acr%40ft%3f%3F", "*Star*craft or *War*cr@ft??")] + public void Unescape(string value, string result) + => EscapingUtilities.UnescapeAll(value).ShouldBe(result); - text = "#*?*#*"; - Assert.Equal(text, EscapingUtilities.UnescapeAll(EscapingUtilities.Escape(text))); - } + [Theory] + [InlineData("", "")] + [InlineData(" ", "")] + [InlineData(" foo ", "foo")] + [InlineData("\tfoo\t", "foo")] + [InlineData(" %3B ", ";")] + [InlineData(" %3b%3B ", ";;")] + [InlineData("\t%2a\t", "*")] + [InlineData(" foo%3Bbar ", "foo;bar")] + [InlineData(" %3B", ";")] + [InlineData("%3B ", ";")] + [InlineData("%20foo", " foo")] + [InlineData("foo%20", "foo ")] + [InlineData(" %ZZ ", "%ZZ")] + public void UnescapeWithTrim(string value, string result) + => EscapingUtilities.UnescapeAll(value, trim: true).ShouldBe(result); - /// - /// - [Fact] - public void EscapeUnescape() - { - string text = "%2a"; - Assert.Equal(text, EscapingUtilities.Escape(EscapingUtilities.UnescapeAll(text))); + [Theory] + [InlineData("", "")] + [InlineData("foo", "foo")] + [InlineData("@", "%40")] + [InlineData("$", "%24")] + [InlineData("(", "%28")] + [InlineData(")", "%29")] + [InlineData(";", "%3b")] + [InlineData("'", "%27")] + [InlineData("*", "%2a")] + [InlineData("?", "%3f")] + [InlineData("#*?*#*", "#%2a%3f%2a#%2a")] + [InlineData("%*?*%*", "%25%2a%3f%2a%25%2a")] + public void Escape(string value, string result) + => EscapingUtilities.Escape(value).ShouldBe(result); - text = "%3f"; - Assert.Equal(text, EscapingUtilities.Escape(EscapingUtilities.UnescapeAll(text))); + [Theory] + [InlineData("*")] + [InlineData("?")] + [InlineData("#*?*#*")] + public void UnescapeEscape(string value) + => EscapingUtilities.UnescapeAll(EscapingUtilities.Escape(value)).ShouldBe(value); - text = "#%2a%3f%2a#%2a"; - Assert.Equal(text, EscapingUtilities.Escape(EscapingUtilities.UnescapeAll(text))); - } + [Theory] + [InlineData("%2a")] + [InlineData("%3f")] + [InlineData("#%2a%3f%2a#%2a")] + public void EscapeUnescape(string value) + => EscapingUtilities.Escape(EscapingUtilities.UnescapeAll(value)).ShouldBe(value); - [Fact] - public void ContainsEscapedWildcards() - { - Assert.False(EscapingUtilities.ContainsEscapedWildcards("NoStarOrQMark")); - Assert.False(EscapingUtilities.ContainsEscapedWildcards("%")); - Assert.False(EscapingUtilities.ContainsEscapedWildcards("%%")); - Assert.False(EscapingUtilities.ContainsEscapedWildcards("%2")); - Assert.False(EscapingUtilities.ContainsEscapedWildcards("%4")); - Assert.False(EscapingUtilities.ContainsEscapedWildcards("%3A")); - Assert.False(EscapingUtilities.ContainsEscapedWildcards("%2B")); - Assert.True(EscapingUtilities.ContainsEscapedWildcards("%2a")); - Assert.True(EscapingUtilities.ContainsEscapedWildcards("%2A")); - Assert.True(EscapingUtilities.ContainsEscapedWildcards("%3F")); - Assert.True(EscapingUtilities.ContainsEscapedWildcards("%3f")); - Assert.True(EscapingUtilities.ContainsEscapedWildcards("%%3f")); - Assert.True(EscapingUtilities.ContainsEscapedWildcards("%3%3f")); - } + [Theory] + [InlineData("", false)] + [InlineData("NoStarOrQMark", false)] + [InlineData("%", false)] + [InlineData("%%", false)] + [InlineData("%2", false)] + [InlineData("%4", false)] + [InlineData("%3A", false)] + [InlineData("%2B", false)] + [InlineData("%2a", true)] + [InlineData("%2A", true)] + [InlineData("%3F", true)] + [InlineData("%3f", true)] + [InlineData("%%3f", true)] + [InlineData("%3%3f", true)] + public void ContainsEscapedWildcards(string value, bool expectedResult) + => EscapingUtilities.ContainsEscapedWildcards(value).ShouldBe(expectedResult); } diff --git a/src/Framework.UnitTests/RefArrayBuilder_Tests.cs b/src/Framework.UnitTests/RefArrayBuilder_Tests.cs new file mode 100644 index 00000000000..34d9c4f6bfa --- /dev/null +++ b/src/Framework.UnitTests/RefArrayBuilder_Tests.cs @@ -0,0 +1,1548 @@ +// 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 Microsoft.Build.Collections; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests.Collections; + +public class RefArrayBuilder_Tests +{ + [Fact] + public void Constructor_InitializesWithCapacity() + { + using RefArrayBuilder builder = new(10); + builder.Count.ShouldBe(0); + } + + [Fact] + public void Constructor_WithScratchBuffer_UsesScratchBuffer() + { + using RefArrayBuilder builder = new(stackalloc int[10]); + + builder.Count.ShouldBe(0); + builder.Add(42); + + builder.Count.ShouldBe(1); + builder[0].ShouldBe(42); + } + + [Fact] + public void Constructor_WithScratchBuffer_CanGrowBeyondInitialCapacity() + { + using RefArrayBuilder builder = new(stackalloc int[2]); + + builder.Add(1); + builder.Add(2); + builder.Add(3); // Should trigger growth + + builder.Count.ShouldBe(3); + builder[0].ShouldBe(1); + builder[1].ShouldBe(2); + builder[2].ShouldBe(3); + } + + [Fact] + public void Constructor_WithEmptyScratchBuffer_CanStillAdd() + { + using RefArrayBuilder builder = new(scratchBuffer: []); + + builder.Count.ShouldBe(0); + builder.Add(42); // Should trigger growth + + builder.Count.ShouldBe(1); + builder[0].ShouldBe(42); + } + + [Fact] + public void Constructor_WithScratchBuffer_AddRangeWorks() + { + using RefArrayBuilder builder = new(stackalloc int[10]); + + builder.AddRange([1, 2, 3]); + + builder.Count.ShouldBe(3); + builder[0].ShouldBe(1); + builder[1].ShouldBe(2); + builder[2].ShouldBe(3); + } + + [Fact] + public void Add_SingleItem_IncreasesLength() + { + using RefArrayBuilder builder = new(4); + builder.Add(42); + + builder.Count.ShouldBe(1); + builder[0].ShouldBe(42); + } + + [Fact] + public void Add_MultipleItems_MaintainsOrder() + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + builder.Add(2); + builder.Add(3); + + builder.Count.ShouldBe(3); + builder[0].ShouldBe(1); + builder[1].ShouldBe(2); + builder[2].ShouldBe(3); + } + + [Fact] + public void Add_ExceedsCapacity_GrowsAutomatically() + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + builder.Add(2); + builder.Add(3); + builder.Add(4); + builder.Add(5); + + builder.Count.ShouldBe(5); + builder[0].ShouldBe(1); + builder[4].ShouldBe(5); + } + + [Fact] + public void Add_ScratchBuffer_ExceedsCapacity_GrowsAutomatically() + { + using RefArrayBuilder builder = new(stackalloc int[4]); + builder.Add(1); + builder.Add(2); + builder.Add(3); + builder.Add(4); + builder.Add(5); + + builder.Count.ShouldBe(5); + builder[0].ShouldBe(1); + builder[4].ShouldBe(5); + } + + [Fact] + public void AddRange_SingleElement_AddsCorrectly() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([42]); + + builder.Count.ShouldBe(1); + builder[0].ShouldBe(42); + } + + [Fact] + public void AddRange_MultipleElements_AddsAllInOrder() + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + builder.AddRange([2, 3, 4]); + + builder.Count.ShouldBe(4); + builder[0].ShouldBe(1); + builder[1].ShouldBe(2); + builder[2].ShouldBe(3); + builder[3].ShouldBe(4); + } + + [Fact] + public void AddRange_ExceedsCapacity_GrowsAutomatically() + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + builder.AddRange([2, 3, 4, 5]); + + builder.Count.ShouldBe(5); + builder[4].ShouldBe(5); + } + + [Fact] + public void AddRange_ScratchBuffer_ExceedsCapacity_GrowsAutomatically() + { + using RefArrayBuilder builder = new(stackalloc int[4]); + builder.Add(1); + builder.AddRange([2, 3, 4, 5]); + + builder.Count.ShouldBe(5); + builder[4].ShouldBe(5); + } + + [Fact] + public void AddRange_EmptySpan_DoesNothing() + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + builder.AddRange([]); + + builder.Count.ShouldBe(1); + builder[0].ShouldBe(1); + } + + [Fact] + public void Insert_AtBeginning_ShiftsExistingElements() + { + using RefArrayBuilder builder = new(4); + builder.Add(2); + builder.Add(3); + builder.Insert(0, 1); + + builder.Count.ShouldBe(3); + builder[0].ShouldBe(1); + builder[1].ShouldBe(2); + builder[2].ShouldBe(3); + } + + [Fact] + public void Insert_InMiddle_ShiftsSubsequentElements() + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + builder.Add(3); + builder.Insert(1, 2); + + builder.Count.ShouldBe(3); + builder[0].ShouldBe(1); + builder[1].ShouldBe(2); + builder[2].ShouldBe(3); + } + + [Fact] + public void Insert_AtEnd_AppendsElement() + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + builder.Add(2); + builder.Insert(2, 3); + + builder.Count.ShouldBe(3); + builder[2].ShouldBe(3); + } + + [Fact] + public void Insert_ExceedsCapacity_GrowsAutomatically() + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + builder.Add(3); + builder.Add(4); + builder.Add(5); + builder.Insert(1, 2); + + builder.Count.ShouldBe(5); + builder[0].ShouldBe(1); + builder[1].ShouldBe(2); + builder[2].ShouldBe(3); + builder[3].ShouldBe(4); + builder[4].ShouldBe(5); + } + + [Fact] + public void Insert_ScratchBuffer_ExceedsCapacity_GrowsAutomatically() + { + using RefArrayBuilder builder = new(stackalloc int[4]); + builder.Add(2); + builder.Add(3); + builder.Add(4); + builder.Add(5); + builder.Insert(0, 1); + + builder.Count.ShouldBe(5); + builder[0].ShouldBe(1); + builder[1].ShouldBe(2); + builder[2].ShouldBe(3); + builder[3].ShouldBe(4); + builder[4].ShouldBe(5); + } + + [Fact] + public void InsertRange_AtBeginning_ShiftsExistingElements() + { + using RefArrayBuilder builder = new(10); + builder.Add(4); + builder.Add(5); + builder.InsertRange(0, [1, 2, 3]); + + builder.Count.ShouldBe(5); + builder[0].ShouldBe(1); + builder[1].ShouldBe(2); + builder[2].ShouldBe(3); + builder[3].ShouldBe(4); + builder[4].ShouldBe(5); + } + + [Fact] + public void InsertRange_InMiddle_ShiftsSubsequentElements() + { + using RefArrayBuilder builder = new(10); + builder.Add(1); + builder.Add(5); + builder.InsertRange(1, [2, 3, 4]); + + builder.Count.ShouldBe(5); + builder[0].ShouldBe(1); + builder[1].ShouldBe(2); + builder[2].ShouldBe(3); + builder[3].ShouldBe(4); + builder[4].ShouldBe(5); + } + + [Fact] + public void InsertRange_AtEnd_AppendsElements() + { + using RefArrayBuilder builder = new(10); + builder.Add(1); + builder.Add(2); + builder.InsertRange(2, [3, 4, 5]); + + builder.Count.ShouldBe(5); + builder[2].ShouldBe(3); + builder[3].ShouldBe(4); + builder[4].ShouldBe(5); + } + + [Fact] + public void InsertRange_ExceedsCapacity_GrowsAutomatically() + { + using RefArrayBuilder builder = new(2); + builder.Add(1); + builder.Add(5); + builder.InsertRange(1, [2, 3, 4]); + + builder.Count.ShouldBe(5); + builder[0].ShouldBe(1); + builder[1].ShouldBe(2); + builder[2].ShouldBe(3); + builder[3].ShouldBe(4); + builder[4].ShouldBe(5); + } + + [Fact] + public void InsertRange_ScratchBuffer_ExceedsCapacity_GrowsAutomatically() + { + using RefArrayBuilder builder = new(stackalloc int[2]); + builder.Add(1); + builder.Add(5); + builder.InsertRange(1, [2, 3, 4]); + + builder.Count.ShouldBe(5); + builder[0].ShouldBe(1); + builder[1].ShouldBe(2); + builder[2].ShouldBe(3); + builder[3].ShouldBe(4); + builder[4].ShouldBe(5); + } + + [Fact] + public void InsertRange_AtBeginning_NearCapacity_ShiftsCorrectly() + { + // Use a capacity that we know ArrayPool will give us, then fill most of it. + // The bug is that InsertRange checks (index + source.Length) < capacity + // instead of (count + source.Length) <= capacity. + using RefArrayBuilder builder = new(16); + int capacity = builder.Capacity; + + // Fill all but 2 slots + int fillCount = capacity - 2; + for (int i = 1; i <= fillCount; i++) + { + builder.Add(i); + } + + // Insert 5 at index 0: index + 5 could be < capacity, but count + 5 exceeds it. + builder.InsertRange(0, [91, 92, 93, 94, 95]); + + builder.Count.ShouldBe(fillCount + 5); + builder[0].ShouldBe(91); + builder[1].ShouldBe(92); + builder[2].ShouldBe(93); + builder[3].ShouldBe(94); + builder[4].ShouldBe(95); + builder[5].ShouldBe(1); + builder[fillCount + 4].ShouldBe(fillCount); + } + + [Fact] + public void InsertRange_ScratchBuffer_AtBeginning_NearCapacity_ShiftsCorrectly() + { + using RefArrayBuilder builder = new(stackalloc int[12]); + for (int i = 3; i <= 12; i++) + { + builder.Add(i); + } + + builder.InsertRange(0, [22, 33, 44, 55, 66]); + + builder.Count.ShouldBe(15); + builder[0].ShouldBe(22); + builder[1].ShouldBe(33); + builder[2].ShouldBe(44); + builder[3].ShouldBe(55); + builder[4].ShouldBe(66); + builder[5].ShouldBe(3); + builder[14].ShouldBe(12); + } + + [Fact] + public void InsertRange_EmptySpan_DoesNothing() + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + builder.Add(2); + builder.InsertRange(1, []); + + builder.Count.ShouldBe(2); + builder[0].ShouldBe(1); + builder[1].ShouldBe(2); + } + + [Fact] + public void Count_CanBeSet() + { + var builder = new RefArrayBuilder(4); + try + { + builder.Add(1); + builder.Add(2); + builder.Add(3); + builder.Count = 2; + + builder.Count.ShouldBe(2); + builder[0].ShouldBe(1); + builder[1].ShouldBe(2); + } + finally + { + builder.Dispose(); + } + } + + [Fact] + public void Indexer_ReturnsReference() + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + builder.Add(2); + + ref int value = ref builder[1]; + value = 42; + + builder[1].ShouldBe(42); + } + + [Fact] + public void IsEmpty_NewBuilder_ReturnsTrue() + { + using RefArrayBuilder builder = new(4); + + builder.IsEmpty.ShouldBeTrue(); + } + + [Fact] + public void IsEmpty_AfterAdd_ReturnsFalse() + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + + builder.IsEmpty.ShouldBeFalse(); + } + + [Fact] + public void IsEmpty_AfterRemovingAllElements_ReturnsTrue() + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + builder.RemoveAt(0); + + builder.IsEmpty.ShouldBeTrue(); + } + + [Fact] + public void IsEmpty_AfterSettingCountToZero_ReturnsTrue() + { + var builder = new RefArrayBuilder(4); + try + { + builder.Add(1); + builder.Add(2); + builder.Count = 0; + + builder.IsEmpty.ShouldBeTrue(); + } + finally + { + builder.Dispose(); + } + } + + [Fact] + public void AsSpan_ReturnsCorrectSlice() + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + builder.Add(2); + builder.Add(3); + + ReadOnlySpan span = builder.AsSpan(); + + span.Length.ShouldBe(3); + span[0].ShouldBe(1); + span[1].ShouldBe(2); span[2].ShouldBe(3); + span[2].ShouldBe(3); + } + + [Fact] + public void AsSpan_EmptyBuilder_ReturnsEmptySpan() + { + using RefArrayBuilder builder = new(4); + + ReadOnlySpan span = builder.AsSpan(); + + span.Length.ShouldBe(0); + } + + [Fact] + public void AsSpan_ReflectsChanges() + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + builder.Add(2); + + ReadOnlySpan span1 = builder.AsSpan(); + span1.Length.ShouldBe(2); + + builder.Add(3); + + ReadOnlySpan span2 = builder.AsSpan(); + span2.Length.ShouldBe(3); + span2[2].ShouldBe(3); + } + + [Fact] + public void WithReferenceTypes_WorksCorrectly() + { + using RefArrayBuilder builder = new(4); + builder.Add("hello"); + builder.Add("world"); + + builder.Count.ShouldBe(2); + builder[0].ShouldBe("hello"); + builder[1].ShouldBe("world"); + } + + [Fact] + public void WithReferenceTypes_AddRange_WorksCorrectly() + { + using RefArrayBuilder builder = new(4); + builder.AddRange(["one", "two", "three"]); + + builder.Count.ShouldBe(3); + builder[0].ShouldBe("one"); + builder[1].ShouldBe("two"); + builder[2].ShouldBe("three"); + } + + [Fact] + public void WithReferenceTypes_Insert_WorksCorrectly() + { + using RefArrayBuilder builder = new(4); + builder.Add("first"); + builder.Add("third"); + builder.Insert(1, "second"); + + builder.Count.ShouldBe(3); + builder[0].ShouldBe("first"); + builder[1].ShouldBe("second"); + builder[2].ShouldBe("third"); + } + + [Fact] + public void MultipleOperations_ComplexScenario() + { + using RefArrayBuilder builder = new(2); + + // Start small and grow + builder.Add(1); + builder.Add(2); + builder.AddRange([3, 4, 5]); + builder.Insert(0, 0); + builder.InsertRange(6, [6, 7, 8]); + + builder.Count.ShouldBe(9); + + // Verify sequence + for (int i = 0; i < 9; i++) + { + builder[i].ShouldBe(i); + } + } + + [Fact] + public void LargeCapacity_HandlesGrowthCorrectly() + { + using RefArrayBuilder builder = new(2); + + // Add many items to trigger multiple growth operations + for (int i = 0; i < 100; i++) + { + builder.Add(i); + } + + builder.Count.ShouldBe(100); + builder[0].ShouldBe(0); + builder[50].ShouldBe(50); + builder[99].ShouldBe(99); + } + + [Fact] + public void Dispose_CanBeCalledMultipleTimes() + { + RefArrayBuilder builder = new(4); + builder.Add(1); + builder.Add(2); + + builder.Dispose(); + builder.Dispose(); // Should not throw + } + + [Fact] + public void RemoveAt_FirstElement_ShiftsSubsequentElements() + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + builder.Add(2); + builder.Add(3); + builder.RemoveAt(0); + + builder.Count.ShouldBe(2); + builder[0].ShouldBe(2); + builder[1].ShouldBe(3); + } + + [Fact] + public void RemoveAt_MiddleElement_ShiftsSubsequentElements() + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + builder.Add(2); + builder.Add(3); + builder.Add(4); + builder.RemoveAt(1); + + builder.Count.ShouldBe(3); + builder[0].ShouldBe(1); + builder[1].ShouldBe(3); + builder[2].ShouldBe(4); + } + + [Fact] + public void RemoveAt_LastElement_RemovesCorrectly() + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + builder.Add(2); + builder.Add(3); + builder.RemoveAt(2); + + builder.Count.ShouldBe(2); + builder[0].ShouldBe(1); + builder[1].ShouldBe(2); + } + + [Fact] + public void RemoveAt_SingleElement_BecomesEmpty() + { + using RefArrayBuilder builder = new(4); + builder.Add(42); + builder.RemoveAt(0); + + builder.Count.ShouldBe(0); + } + + [Fact] + public void RemoveAt_WithReferenceTypes_ClearsReference() + { + using RefArrayBuilder builder = new(4); + builder.Add("first"); + builder.Add("second"); + builder.Add("third"); + builder.RemoveAt(1); + + builder.Count.ShouldBe(2); + builder[0].ShouldBe("first"); + builder[1].ShouldBe("third"); + } + + [Fact] + public void RemoveAt_MultipleRemovals_WorksCorrectly() + { + using RefArrayBuilder builder = new(10); + builder.AddRange([1, 2, 3, 4, 5]); + builder.RemoveAt(2); // Remove 3 + builder.RemoveAt(1); // Remove 2 + + builder.Count.ShouldBe(3); + builder[0].ShouldBe(1); + builder[1].ShouldBe(4); + builder[2].ShouldBe(5); + } + + [Fact] + public void RemoveAt_RemoveAllElements_LeavesEmpty() + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + builder.Add(2); + builder.Add(3); + builder.RemoveAt(2); + builder.RemoveAt(1); + builder.RemoveAt(0); + + builder.Count.ShouldBe(0); + } + + [Fact] + public void RemoveAt_AfterGrowth_WorksCorrectly() + { + using RefArrayBuilder builder = new(2); + builder.Add(1); + builder.Add(2); + builder.Add(3); // Triggers growth + builder.Add(4); + builder.RemoveAt(1); + + builder.Count.ShouldBe(3); + builder[0].ShouldBe(1); + builder[1].ShouldBe(3); + builder[2].ShouldBe(4); + } + + [Fact] + public void RemoveAt_CanAddAfterRemoval() + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + builder.Add(2); + builder.Add(3); + builder.RemoveAt(1); + builder.Add(4); + + builder.Count.ShouldBe(3); + builder[0].ShouldBe(1); + builder[1].ShouldBe(3); + builder[2].ShouldBe(4); + } + + [Fact] + public void RemoveAt_WithScratchBuffer_WorksCorrectly() + { + using RefArrayBuilder builder = new(stackalloc int[10]); + builder.AddRange([1, 2, 3, 4, 5]); + builder.RemoveAt(2); + + builder.Count.ShouldBe(4); + builder[0].ShouldBe(1); + builder[1].ShouldBe(2); + builder[2].ShouldBe(4); + builder[3].ShouldBe(5); + } + + [Fact] + public void ToImmutable_EmptyBuilder_ReturnsEmptyImmutableArray() + { + using RefArrayBuilder builder = new(4); + + var result = builder.ToImmutable(); + + result.IsEmpty.ShouldBeTrue(); + } + + [Fact] + public void ToImmutable_WithItems_ReturnsImmutableArrayWithCorrectElements() + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + builder.Add(2); + builder.Add(3); + + var result = builder.ToImmutable(); + + result.Length.ShouldBe(3); + result[0].ShouldBe(1); + result[1].ShouldBe(2); + result[2].ShouldBe(3); + } + + [Fact] + public void ToImmutable_WithReferenceTypes_ReturnsImmutableArrayWithCorrectElements() + { + using RefArrayBuilder builder = new(4); + builder.Add("hello"); + builder.Add("world"); + + var result = builder.ToImmutable(); + + result.Length.ShouldBe(2); + result[0].ShouldBe("hello"); + result[1].ShouldBe("world"); + } + + [Fact] + public void ToImmutable_AfterMultipleOperations_ReturnsCorrectImmutableArray() + { + using RefArrayBuilder builder = new(2); + builder.Add(1); + builder.AddRange([2, 3]); + builder.Insert(0, 0); + builder.InsertRange(4, [4, 5]); + + var result = builder.ToImmutable(); + + result.Length.ShouldBe(6); + for (int i = 0; i < 6; i++) + { + result[i].ShouldBe(i); + } + } + + [Fact] + public void ToImmutable_DoesNotModifyBuilder() + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + builder.Add(2); + + var result1 = builder.ToImmutable(); + builder.Add(3); + var result2 = builder.ToImmutable(); + + result1.Length.ShouldBe(2); + result1[0].ShouldBe(1); + result1[1].ShouldBe(2); + + result2.Length.ShouldBe(3); + result2[0].ShouldBe(1); + result2[1].ShouldBe(2); + result2[2].ShouldBe(3); + } + + [Fact] + public void ToImmutable_AfterCountSet_ReturnsCorrectImmutableArray() + { + var builder = new RefArrayBuilder(4); + try + { + builder.Add(1); + builder.Add(2); + builder.Add(3); + builder.Count = 2; + + var result = builder.ToImmutable(); + + result.Length.ShouldBe(2); + result[0].ShouldBe(1); + result[1].ShouldBe(2); + } + finally + { + builder.Dispose(); + } + } + + [Fact] + public void Any_EmptyBuilder_ReturnsFalse() + { + using RefArrayBuilder builder = new(4); + + builder.Any().ShouldBeFalse(); + } + + [Fact] + public void Any_WithElements_ReturnsTrue() + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + + builder.Any().ShouldBeTrue(); + } + + [Fact] + public void Any_WithPredicate_NoMatch_ReturnsFalse() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3]); + + builder.Any(x => x > 5).ShouldBeFalse(); + } + + [Fact] + public void Any_WithPredicate_HasMatch_ReturnsTrue() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3, 4, 5]); + + builder.Any(x => x > 3).ShouldBeTrue(); + } + + [Fact] + public void Any_WithPredicate_EmptyBuilder_ReturnsFalse() + { + using RefArrayBuilder builder = new(4); + + builder.Any(x => x > 0).ShouldBeFalse(); + } + + [Fact] + public void Any_WithPredicateAndArg_NoMatch_ReturnsFalse() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3]); + + builder.Any(5, (x, threshold) => x > threshold).ShouldBeFalse(); + } + + [Fact] + public void Any_WithPredicateAndArg_HasMatch_ReturnsTrue() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3, 4, 5]); + + builder.Any(3, (x, threshold) => x > threshold).ShouldBeTrue(); + } + + [Fact] + public void Any_WithNullPredicate_ThrowsArgumentNullException() + { + Should.Throw(() => + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + + return builder.Any(null!); + }); + } + + [Fact] + public void Any_WithPredicateAndArg_NullPredicate_ThrowsArgumentNullException() + { + Should.Throw(() => + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + + return builder.Any(5, null!); + }); + } + + [Fact] + public void All_EmptyBuilder_ReturnsTrue() + { + using RefArrayBuilder builder = new(4); + + builder.All(x => x > 0).ShouldBeTrue(); + } + + [Fact] + public void All_AllElementsMatch_ReturnsTrue() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([2, 4, 6, 8]); + + builder.All(x => x % 2 == 0).ShouldBeTrue(); + } + + [Fact] + public void All_SomeElementsDontMatch_ReturnsFalse() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([2, 3, 4, 6]); + + builder.All(x => x % 2 == 0).ShouldBeFalse(); + } + + [Fact] + public void All_NoElementsMatch_ReturnsFalse() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 3, 5, 7]); + + builder.All(x => x % 2 == 0).ShouldBeFalse(); + } + + [Fact] + public void All_WithPredicateAndArg_AllMatch_ReturnsTrue() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([5, 10, 15, 20]); + + builder.All(5, (x, divisor) => x % divisor == 0).ShouldBeTrue(); + } + + [Fact] + public void All_WithPredicateAndArg_SomeDontMatch_ReturnsFalse() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([5, 10, 12, 20]); + + builder.All(5, (x, divisor) => x % divisor == 0).ShouldBeFalse(); + } + + [Fact] + public void All_WithNullPredicate_ThrowsArgumentNullException() + { + Should.Throw(() => + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + + return builder.All(null!); + }); + } + + [Fact] + public void All_WithPredicateAndArg_NullPredicate_ThrowsArgumentNullException() + { + Should.Throw(() => + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + + return builder.All(5, null!); + }); + } + + [Fact] + public void First_EmptyBuilder_ThrowsInvalidOperationException() + { + Should.Throw(() => + { + using RefArrayBuilder builder = new(4); + + return builder.First(); + }); + } + + [Fact] + public void First_WithElements_ReturnsFirstElement() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3]); + + builder.First().ShouldBe(1); + } + + [Fact] + public void First_SingleElement_ReturnsThatElement() + { + using RefArrayBuilder builder = new(4); + builder.Add(42); + + builder.First().ShouldBe(42); + } + + [Fact] + public void First_WithPredicate_NoMatch_ThrowsInvalidOperationException() + { + Should.Throw(() => + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3]); + + return builder.First(x => x > 5); + }); + } + + [Fact] + public void First_WithPredicate_HasMatch_ReturnsFirstMatch() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3, 4, 5]); + + builder.First(x => x > 2).ShouldBe(3); + } + + [Fact] + public void First_WithPredicate_EmptyBuilder_ThrowsInvalidOperationException() + { + Should.Throw(() => + { + using RefArrayBuilder builder = new(4); + + return builder.First(x => x > 0); + }); + } + + [Fact] + public void First_WithPredicateAndArg_NoMatch_ThrowsInvalidOperationException() + { + Should.Throw(() => + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3]); + + return builder.First(5, (x, threshold) => x > threshold); + }); + } + + [Fact] + public void First_WithPredicateAndArg_HasMatch_ReturnsFirstMatch() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3, 4, 5]); + + builder.First(2, (x, threshold) => x > threshold).ShouldBe(3); + } + + [Fact] + public void First_WithNullPredicate_ThrowsArgumentNullException() + { + Should.Throw(() => + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + + return builder.First(null!); + }); + } + + [Fact] + public void First_WithPredicateAndArg_NullPredicate_ThrowsArgumentNullException() + { + Should.Throw(() => + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + + return builder.First(5, null!); + }); + } + + [Fact] + public void FirstOrDefault_EmptyBuilder_ReturnsDefault() + { + using RefArrayBuilder builder = new(4); + + builder.FirstOrDefault().ShouldBe(0); + } + + [Fact] + public void FirstOrDefault_WithElements_ReturnsFirstElement() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3]); + + builder.FirstOrDefault().ShouldBe(1); + } + + [Fact] + public void FirstOrDefault_WithDefaultValue_EmptyBuilder_ReturnsDefaultValue() + { + using RefArrayBuilder builder = new(4); + + builder.FirstOrDefault(99).ShouldBe(99); + } + + [Fact] + public void FirstOrDefault_WithDefaultValue_WithElements_ReturnsFirstElement() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3]); + + builder.FirstOrDefault(99).ShouldBe(1); + } + + [Fact] + public void FirstOrDefault_WithPredicate_NoMatch_ReturnsDefault() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3]); + + builder.FirstOrDefault(x => x > 5).ShouldBe(0); + } + + [Fact] + public void FirstOrDefault_WithPredicate_HasMatch_ReturnsFirstMatch() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3, 4, 5]); + + builder.FirstOrDefault(x => x > 2).ShouldBe(3); + } + + [Fact] + public void FirstOrDefault_WithPredicateAndDefaultValue_NoMatch_ReturnsDefaultValue() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3]); + + builder.FirstOrDefault(x => x > 5, 99).ShouldBe(99); + } + + [Fact] + public void FirstOrDefault_WithPredicateAndDefaultValue_HasMatch_ReturnsFirstMatch() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3, 4, 5]); + + builder.FirstOrDefault(x => x > 2, 99).ShouldBe(3); + } + + [Fact] + public void FirstOrDefault_WithPredicateAndArg_NoMatch_ReturnsDefault() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3]); + + builder.FirstOrDefault(5, (x, threshold) => x > threshold).ShouldBe(0); + } + + [Fact] + public void FirstOrDefault_WithPredicateAndArg_HasMatch_ReturnsFirstMatch() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3, 4, 5]); + + builder.FirstOrDefault(2, (x, threshold) => x > threshold).ShouldBe(3); + } + + [Fact] + public void FirstOrDefault_WithPredicateArgAndDefaultValue_NoMatch_ReturnsDefaultValue() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3]); + + builder.FirstOrDefault(5, (x, threshold) => x > threshold, 99).ShouldBe(99); + } + + [Fact] + public void FirstOrDefault_WithPredicateArgAndDefaultValue_HasMatch_ReturnsFirstMatch() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3, 4, 5]); + + builder.FirstOrDefault(2, (x, threshold) => x > threshold, 99).ShouldBe(3); + } + + [Fact] + public void FirstOrDefault_WithNullPredicate_ThrowsArgumentNullException() + { + Should.Throw(() => + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + + return builder.FirstOrDefault(null!); + }); + } + + [Fact] + public void FirstOrDefault_WithReferenceType_EmptyBuilder_ReturnsNull() + { + using RefArrayBuilder builder = new(4); + + builder.FirstOrDefault().ShouldBeNull(); + } + + [Fact] + public void FirstOrDefault_WithReferenceType_WithElements_ReturnsFirstElement() + { + using RefArrayBuilder builder = new(4); + builder.AddRange(["hello", "world"]); + + builder.FirstOrDefault().ShouldBe("hello"); + } + + [Fact] + public void Last_EmptyBuilder_ThrowsInvalidOperationException() + { + Should.Throw(() => + { + using RefArrayBuilder builder = new(4); + + return builder.Last(); + }); + } + + [Fact] + public void Last_WithElements_ReturnsLastElement() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3]); + + builder.Last().ShouldBe(3); + } + + [Fact] + public void Last_SingleElement_ReturnsThatElement() + { + using RefArrayBuilder builder = new(4); + builder.Add(42); + + builder.Last().ShouldBe(42); + } + + [Fact] + public void Last_WithPredicate_NoMatch_ThrowsInvalidOperationException() + { + Should.Throw(() => + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3]); + + return builder.Last(x => x > 5); + }); + } + + [Fact] + public void Last_WithPredicate_HasMatch_ReturnsLastMatch() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3, 4, 5]); + + builder.Last(x => x < 4).ShouldBe(3); + } + + [Fact] + public void Last_WithPredicate_EmptyBuilder_ThrowsInvalidOperationException() + { + Should.Throw(() => + { + using RefArrayBuilder builder = new(4); + + return builder.Last(x => x > 0); + }); + } + + [Fact] + public void Last_WithPredicateAndArg_NoMatch_ThrowsInvalidOperationException() + { + Should.Throw(() => + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3]); + + return builder.Last(5, (x, threshold) => x > threshold); + }); + } + + [Fact] + public void Last_WithPredicateAndArg_HasMatch_ReturnsLastMatch() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3, 4, 5]); + + builder.Last(3, (x, threshold) => x < threshold).ShouldBe(2); + } + + [Fact] + public void Last_WithNullPredicate_ThrowsArgumentNullException() + { + Should.Throw(() => + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + + return builder.Last(null!); + }); + } + + [Fact] + public void Last_WithPredicateAndArg_NullPredicate_ThrowsArgumentNullException() + { + Should.Throw(() => + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + + return builder.Last(5, null!); + }); + } + + [Fact] + public void LastOrDefault_EmptyBuilder_ReturnsDefault() + { + using RefArrayBuilder builder = new(4); + + builder.LastOrDefault().ShouldBe(0); + } + + [Fact] + public void LastOrDefault_WithElements_ReturnsLastElement() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3]); + + builder.LastOrDefault().ShouldBe(3); + } + + [Fact] + public void LastOrDefault_WithDefaultValue_EmptyBuilder_ReturnsDefaultValue() + { + using RefArrayBuilder builder = new(4); + + builder.LastOrDefault(99).ShouldBe(99); + } + + [Fact] + public void LastOrDefault_WithDefaultValue_WithElements_ReturnsLastElement() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3]); + + builder.LastOrDefault(99).ShouldBe(3); + } + + [Fact] + public void LastOrDefault_WithPredicate_NoMatch_ReturnsDefault() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3]); + + builder.LastOrDefault(x => x > 5).ShouldBe(0); + } + + [Fact] + public void LastOrDefault_WithPredicate_HasMatch_ReturnsLastMatch() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3, 4, 5]); + + builder.LastOrDefault(x => x < 4).ShouldBe(3); + } + + [Fact] + public void LastOrDefault_WithPredicateAndDefaultValue_NoMatch_ReturnsDefaultValue() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3]); + + builder.LastOrDefault(x => x > 5, 99).ShouldBe(99); + } + + [Fact] + public void LastOrDefault_WithPredicateAndDefaultValue_HasMatch_ReturnsLastMatch() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3, 4, 5]); + + builder.LastOrDefault(x => x < 4, 99).ShouldBe(3); + } + + [Fact] + public void LastOrDefault_WithPredicateAndArg_NoMatch_ReturnsDefault() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3]); + + builder.LastOrDefault(5, (x, threshold) => x > threshold).ShouldBe(0); + } + + [Fact] + public void LastOrDefault_WithPredicateAndArg_HasMatch_ReturnsLastMatch() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3, 4, 5]); + + builder.LastOrDefault(3, (x, threshold) => x < threshold).ShouldBe(2); + } + + [Fact] + public void LastOrDefault_WithPredicateArgAndDefaultValue_NoMatch_ReturnsDefaultValue() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3]); + + builder.LastOrDefault(5, (x, threshold) => x > threshold, 99).ShouldBe(99); + } + + [Fact] + public void LastOrDefault_WithPredicateArgAndDefaultValue_HasMatch_ReturnsLastMatch() + { + using RefArrayBuilder builder = new(4); + builder.AddRange([1, 2, 3, 4, 5]); + + builder.LastOrDefault(3, (x, threshold) => x < threshold, 99).ShouldBe(2); + } + + [Fact] + public void LastOrDefault_WithNullPredicate_ThrowsArgumentNullException() + { + Should.Throw(() => + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + + return builder.LastOrDefault(null!); + }); + } + + [Fact] + public void LastOrDefault_WithReferenceType_EmptyBuilder_ReturnsNull() + { + using RefArrayBuilder builder = new(4); + + builder.LastOrDefault().ShouldBeNull(); + } + + [Fact] + public void LastOrDefault_WithReferenceType_WithElements_ReturnsLastElement() + { + using RefArrayBuilder builder = new(4); + builder.AddRange(["hello", "world"]); + + builder.LastOrDefault().ShouldBe("world"); + } + + [Fact] + public void AsRef_ModificationsViaRef_AreReflectedInOriginalBuilder() + { + using RefArrayBuilder builder = new(stackalloc int[4]); + + ref RefArrayBuilder builderRef = ref builder.AsRef(); + builderRef.Add(10); + builderRef.Add(20); + builderRef.Add(30); + + builder.Count.ShouldBe(3); + builder[0].ShouldBe(10); + builder[1].ShouldBe(20); + builder[2].ShouldBe(30); + } + + [Fact] + public void AsRef_AllowsPassingUsingDeclaredBuilderToRefMethod() + { + using RefArrayBuilder builder = new(4); + builder.Add(1); + builder.Add(2); + + AddItemsToBuilder(ref builder.AsRef(), [3, 4, 5]); + + builder.Count.ShouldBe(5); + builder[2].ShouldBe(3); + builder[3].ShouldBe(4); + builder[4].ShouldBe(5); + } + + [Fact] + public void AsRef_AllowsGrowthThroughRef() + { + using RefArrayBuilder builder = new(stackalloc int[2]); + builder.Add(1); + builder.Add(2); + + // Passing by ref allows the helper to grow the builder beyond the initial stack buffer. + AddItemsToBuilder(ref builder.AsRef(), [3, 4, 5]); + + builder.Count.ShouldBe(5); + + for (int i = 0; i < 5; i++) + { + builder[i].ShouldBe(i + 1); + } + } + + private static void AddItemsToBuilder(ref RefArrayBuilder builder, ReadOnlySpan items) + => builder.AddRange(items); +} diff --git a/src/Framework.UnitTests/TypeInfoTests.cs b/src/Framework.UnitTests/TypeInfoTests.cs new file mode 100644 index 00000000000..55c812b7208 --- /dev/null +++ b/src/Framework.UnitTests/TypeInfoTests.cs @@ -0,0 +1,79 @@ +// 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 Microsoft.Build.Utilities; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.Framework.UnitTests; + +public class TypeInfoTests +{ + [Fact] + public void IsReferenceOrContainsReferences_ReferenceTypes_ReturnsTrue() + { + // Reference types should return true + TypeInfo.IsReferenceOrContainsReferences().ShouldBeTrue(); + TypeInfo.IsReferenceOrContainsReferences().ShouldBeTrue(); + TypeInfo.IsReferenceOrContainsReferences().ShouldBeTrue(); + } + + [Fact] + public void IsReferenceOrContainsReferences_ValueTypesWithoutReferences_ReturnsFalse() + { + // Value types without references should return false + TypeInfo.IsReferenceOrContainsReferences().ShouldBeFalse(); + TypeInfo.IsReferenceOrContainsReferences().ShouldBeFalse(); + TypeInfo.IsReferenceOrContainsReferences().ShouldBeFalse(); + TypeInfo.IsReferenceOrContainsReferences().ShouldBeFalse(); + TypeInfo.IsReferenceOrContainsReferences().ShouldBeFalse(); + } + + [Fact] + public void IsReferenceOrContainsReferences_ValueTypesWithReferences_ReturnsTrue() + { + // Value types containing references should return true + TypeInfo.IsReferenceOrContainsReferences().ShouldBeTrue(); + TypeInfo.IsReferenceOrContainsReferences().ShouldBeTrue(); + } + + [Fact] + public void IsReferenceOrContainsReferences_ResultIsCached() + { + // First call should compute the result + bool firstResult = TypeInfo.IsReferenceOrContainsReferences(); + + // Second call should use cached result + bool secondResult = TypeInfo.IsReferenceOrContainsReferences(); + + // Both calls should return the same result + secondResult.ShouldBe(firstResult); + } + +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value + private enum TestEnum + { + Value1, + Value2 + } + + private struct SimpleStruct + { + public int X; + public int Y; + } + + private struct StructWithReference + { + public object Reference; + public int Value; + } + + private struct StructWithString + { + public string Text; + public double Number; + } +#pragma warning restore CS0649 // Field is never assigned to, and will always have its default value +} diff --git a/src/Framework/Collections/RefArrayBuilder.cs b/src/Framework/Collections/RefArrayBuilder.cs new file mode 100644 index 00000000000..1ec9b983d0b --- /dev/null +++ b/src/Framework/Collections/RefArrayBuilder.cs @@ -0,0 +1,857 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// This code is adapted from https://github.com/dotnet/runtime/blob/284c0ae38e3eac0f6ad5cdaa0156d22fc6fc3915/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/ValueListBuilder.cs. + +using System; +using System.Buffers; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.Collections; + +/// +/// A ref struct builder for arrays that uses pooled memory for efficient allocation. +/// This builder automatically grows as needed and returns memory to the pool when disposed. +/// +/// The type of elements in the array. +internal ref struct RefArrayBuilder +{ + private BufferScope _scope; + private int _count; + + /// + /// Initializes a new instance of the struct with the specified initial capacity. + /// + /// The initial capacity of the builder. + public RefArrayBuilder(int initialCapacity) + { + _scope = new BufferScope(initialCapacity); + } + + /// + /// Initializes a new instance of the struct with the specified scratch buffer. + /// + /// The initial buffer to use for storing elements. + /// + /// should generally be stack-allocated to avoid heap allocations. + /// + public RefArrayBuilder(Span scratchBuffer) + { + _scope = new BufferScope(scratchBuffer); + } + + /// + /// Releases the pooled array back to the shared . + /// This method can be called multiple times safely. + /// + public void Dispose() + { + _scope.Dispose(); + } + + /// + /// Gets the current capacity of the builder. + /// + public readonly int Capacity => _scope.Length; + + /// + /// Gets a value indicating whether the builder contains no elements. + /// + /// + /// if the builder contains no elements; otherwise, . + /// + public readonly bool IsEmpty => _count == 0; + + /// + /// Gets or sets the number of elements in the builder. + /// + public int Count + { + readonly get => _count; + set + { + Debug.Assert(value >= 0, "Count must be non-negative."); + Debug.Assert(value <= _scope.Length, "Count must not exceed the span length."); + + _count = value; + } + } + + /// + /// Gets a reference to the element at the specified index. + /// + /// The zero-based index of the element to get. + /// A reference to the element at the specified index. + public ref T this[int index] + { + get + { + Debug.Assert(index < _count, "Index must be less than Count."); + + return ref _scope[index]; + } + } + + /// + /// Returns a reference to this builder, allowing it to be passed by ref + /// even when declared in a statement. + /// + [UnscopedRef] + public ref RefArrayBuilder AsRef() => ref this; + + /// + /// Returns a view of the elements in the builder. + /// + /// A read-only span view of the elements. + public readonly Span AsSpan() + => _scope.AsSpan()[.._count]; + + /// + /// Adds an item to the end of the builder. The builder will automatically grow if needed. + /// + /// The item to add. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(T item) + { + int count = _count; + Span span = _scope; + + if ((uint)count < (uint)span.Length) + { + span[count] = item; + _count = count + 1; + } + else + { + AddWithResize(item); + } + } + + // Hide uncommon path + [MethodImpl(MethodImplOptions.NoInlining)] + private void AddWithResize(T item) + { + Debug.Assert(_count == _scope.Length, "AddWithResize should only be called when the span is full."); + + int count = _count; + + Grow(1); + + _scope[count] = item; + _count = count + 1; + } + + /// + /// Adds a range of elements to the end of the builder. The builder will automatically grow if needed. + /// + /// The span of elements to add. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddRange(scoped ReadOnlySpan source) + { + int count = _count; + Span span = _scope; + + if (source.Length == 1 && (uint)count < (uint)span.Length) + { + span[count] = source[0]; + _count = count + 1; + } + else + { + AddRangeCore(source); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void AddRangeCore(scoped ReadOnlySpan source) + { + int count = _count; + Span span = _scope; + + if ((uint)(count + source.Length) > (uint)span.Length) + { + Grow(size: source.Length); + + // Reset span since we grew. + span = _scope; + } + + source.CopyTo(span.Slice(start: count)); + _count = count + source.Length; + } + + /// + /// Inserts an item at the specified index, shifting subsequent elements. The builder will automatically grow if needed. + /// + /// The zero-based index at which to insert the item. + /// The item to insert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Insert(int index, T item) + { + Debug.Assert(index >= 0, "Insert index must be non-negative."); + Debug.Assert(index <= _count, "Insert index must not exceed Count."); + + int count = _count; + Span span = _scope; + + if ((uint)count < (uint)span.Length) + { + // Shift existing items + int toCopy = count - index; + span.Slice(index, toCopy).CopyTo(span.Slice(index + 1, toCopy)); + + span[index] = item; + _count = count + 1; + } + else + { + InsertWithResize(index, item); + } + } + + // Hide uncommon path + [MethodImpl(MethodImplOptions.NoInlining)] + private void InsertWithResize(int index, T item) + { + Debug.Assert(_count == _scope.Length, "InsertWithResize should only be called when the span is full."); + + Grow(size: 1, startIndex: index); + + _scope[index] = item; + _count += 1; + } + + /// + /// Inserts a range of elements at the specified index, shifting subsequent elements. + /// The builder will automatically grow if needed. + /// + /// The zero-based index at which to insert the elements. + /// The span of elements to insert. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void InsertRange(int index, scoped ReadOnlySpan source) + { + Debug.Assert(index >= 0, "Insert index must be non-negative."); + Debug.Assert(index <= _count, "Insert index must not exceed Count."); + + int count = _count; + Span span = _scope; + + if ((uint)(count + source.Length) <= (uint)span.Length) + { + // Shift existing items + int toCopy = count - index; + span.Slice(index, toCopy).CopyTo(span.Slice(index + source.Length, toCopy)); + + source.CopyTo(span.Slice(index)); + _count = count + source.Length; + } + else + { + InsertRangeCore(index, source); + } + } + + // Hide uncommon path + [MethodImpl(MethodImplOptions.NoInlining)] + private void InsertRangeCore(int index, scoped ReadOnlySpan source) + { + int count = _count; + Span span = _scope; + + if ((uint)(count + source.Length) > (uint)span.Length) + { + Grow(size: source.Length, startIndex: index); + + // Reset span since we grew. + span = _scope; + } + + source.CopyTo(span.Slice(index, source.Length)); + _count = count + source.Length; + } + + private void Grow(int size = 1, int startIndex = -1) + { + Debug.Assert(startIndex >= -1, "Start index must be -1 or non-negative."); + Debug.Assert(startIndex <= _count, "Start index must not exceed Count."); + + const int ArrayMaxLength = 0x7FFFFFC7; // Same as Array.MaxLength; + + Span span = _scope; + + // Double the size of the span. If it's currently empty, default to size 4, + // although it'll be increased in Rent to the pool's minimum bucket size. + int nextCapacity = Math.Max( + val1: span.Length != 0 ? span.Length * 2 : 4, + val2: span.Length + size); + + // If the computed doubled capacity exceeds the possible length of an array, then we + // want to downgrade to either the maximum array length if that's large enough to hold + // an additional item, or the current length + 1 if it's larger than the max length, in + // which case it'll result in an OOM when calling Rent below. In the exceedingly rare + // case where _span.Length is already int.MaxValue (in which case it couldn't be a managed + // array), just use that same value again and let it OOM in Rent as well. + if ((uint)nextCapacity > ArrayMaxLength) + { + nextCapacity = Math.Max(Math.Max(span.Length + 1, ArrayMaxLength), span.Length); + } + + if (startIndex == -1) + { + _scope.EnsureCapacity(nextCapacity, copy: true); + } + else + { + // Need to manually copy to new buffer to make room for inserted items + // at startIndex. The EnsureCapacity(copy: true) path won't work here + // because it always copies starting at index 0. So, we create a new + // buffer and copy the segments before and after startIndex separately. + var newScope = new BufferScope(nextCapacity); + + Span destination = newScope.AsSpan(); + + if (startIndex > 0) + { + span[..startIndex].CopyTo(destination); + } + + span[startIndex.._count].CopyTo(destination.Slice(startIndex + size)); + + _scope.Dispose(); + _scope = newScope; + } + } + + /// + /// Removes the element at the specified index, shifting subsequent elements. + /// + /// The zero-based index of the element to remove. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RemoveAt(int index) + { + Debug.Assert(index >= 0, "Remove index must be non-negative."); + Debug.Assert(index < _count, "Remove index must be less than Count."); + + int count = _count; + Span span = _scope; + + // Shift subsequent elements down by one + int toCopy = count - index - 1; + if (toCopy > 0) + { + span.Slice(index + 1, toCopy).CopyTo(span.Slice(index, toCopy)); + } + + // Clear the last element if it contains references + if (TypeInfo.IsReferenceOrContainsReferences()) + { + span[count - 1] = default!; + } + + _count = count - 1; + } + + /// + /// Creates an containing a copy of the elements in the builder. + /// + /// An immutable array containing the elements. + public readonly ImmutableArray ToImmutable() + => ImmutableArray.Create(AsSpan()); + + /// + /// Determines whether the builder contains any elements. + /// + /// + /// if the builder contains any elements; otherwise, . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool Any() + => !IsEmpty; + + /// + /// Determines whether any element in the builder satisfies a condition. + /// + /// A function to test each element for a condition. + /// + /// if any element satisfies the condition; otherwise, . + /// + /// is . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool Any(Func predicate) + { + ArgumentNullException.ThrowIfNull(predicate); + + foreach (T item in AsSpan()) + { + if (predicate(item)) + { + return true; + } + } + + return false; + } + + /// + /// Determines whether any element in the builder satisfies a condition using an additional argument. + /// + /// The type of the additional argument. + /// The additional argument to pass to the predicate. + /// A function to test each element for a condition. + /// + /// if any element satisfies the condition; otherwise, . + /// + /// is . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool Any(TArg arg, Func predicate) + { + ArgumentNullException.ThrowIfNull(predicate); + + foreach (T item in AsSpan()) + { + if (predicate(item, arg)) + { + return true; + } + } + + return false; + } + + /// + /// Determines whether all elements in the builder satisfy a condition. + /// + /// A function to test each element for a condition. + /// + /// if all elements satisfy the condition; otherwise, . + /// + /// is . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool All(Func predicate) + { + ArgumentNullException.ThrowIfNull(predicate); + + foreach (T item in AsSpan()) + { + if (!predicate(item)) + { + return false; + } + } + + return true; + } + + /// + /// Determines whether all elements in the builder satisfy a condition using an additional argument. + /// + /// The type of the additional argument. + /// The additional argument to pass to the predicate. + /// A function to test each element for a condition. + /// + /// if all elements satisfy the condition; otherwise, . + /// + /// is . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool All(TArg arg, Func predicate) + { + ArgumentNullException.ThrowIfNull(predicate); + + foreach (T item in AsSpan()) + { + if (!predicate(item, arg)) + { + return false; + } + } + + return true; + } + + /// + /// Returns the first element in the builder. + /// + /// The first element in the builder. + /// The builder is empty. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly T First() + { + T? first = TryGetFirst(out bool found); + + if (!found) + { + ThrowInvalidOperation(SR.Format_0_contains_no_elements(nameof(RefArrayBuilder<>))); + } + + return first!; + } + + /// + /// Returns the first element in the builder that satisfies a condition. + /// + /// A function to test each element for a condition. + /// The first element that satisfies the condition. + /// is . + /// No element satisfies the condition. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly T First(Func predicate) + { + T? first = TryGetFirst(predicate, out bool found); + + if (!found) + { + ThrowInvalidOperation(SR.Format_0_does_not_contain_matching_element(nameof(RefArrayBuilder<>))); + } + + return first!; + } + + /// + /// Returns the first element in the builder that satisfies a condition using an additional argument. + /// + /// The type of the additional argument. + /// The additional argument to pass to the predicate. + /// A function to test each element for a condition. + /// The first element that satisfies the condition. + /// is . + /// No element satisfies the condition. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly T First(TArg arg, Func predicate) + { + T? first = TryGetFirst(predicate, arg, out bool found); + + if (!found) + { + ThrowInvalidOperation(SR.Format_0_does_not_contain_matching_element(nameof(RefArrayBuilder<>))); + } + + return first!; + } + + /// + /// Returns the first element in the builder, or a default value if the builder is empty. + /// + /// The first element in the builder, or () if the builder is empty. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly T? FirstOrDefault() + => TryGetFirst(out _); + + /// + /// Returns the first element in the builder, or a specified default value if the builder is empty. + /// + /// The default value to return if the builder is empty. + /// + /// The first element in the builder, or if the builder is empty. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly T FirstOrDefault(T defaultValue) + { + T? first = TryGetFirst(out bool found); + return found ? first! : defaultValue; + } + + /// + /// Returns the first element that satisfies a condition, or a default value if no such element is found. + /// + /// A function to test each element for a condition. + /// + /// The first element that satisfies the condition, or () if no such element is found. + /// + /// is . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly T? FirstOrDefault(Func predicate) + => TryGetFirst(predicate, out _); + + /// + /// Returns the first element that satisfies a condition, or a specified default value if no such element is found. + /// + /// A function to test each element for a condition. + /// The default value to return if no element satisfies the condition. + /// + /// The first element that satisfies the condition, or if no such element is found. + /// + /// is . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly T FirstOrDefault(Func predicate, T defaultValue) + { + T? first = TryGetFirst(predicate, out bool found); + return found ? first! : defaultValue; + } + + /// + /// Returns the first element that satisfies a condition using an additional argument, or a default value if no such element is found. + /// + /// The type of the additional argument. + /// The additional argument to pass to the predicate. + /// A function to test each element for a condition. + /// + /// The first element that satisfies the condition, or () if no such element is found. + /// + /// is . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly T? FirstOrDefault(TArg arg, Func predicate) + => TryGetFirst(predicate, arg, out _); + + /// + /// Returns the first element that satisfies a condition using an additional argument, or a specified default value if no such element is found. + /// + /// The type of the additional argument. + /// The additional argument to pass to the predicate. + /// A function to test each element for a condition. + /// The default value to return if no element satisfies the condition. + /// + /// The first element that satisfies the condition, or if no such element is found. + /// + /// is . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly T FirstOrDefault(TArg arg, Func predicate, T defaultValue) + { + T? first = TryGetFirst(predicate, arg, out bool found); + return found ? first! : defaultValue; + } + + /// + /// Returns the last element in the builder. + /// + /// The last element in the builder. + /// The builder is empty. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly T Last() + { + T? last = TryGetLast(out bool found); + + if (!found) + { + ThrowInvalidOperation(SR.Format_0_contains_no_elements(nameof(RefArrayBuilder<>))); + } + + return last!; + } + + /// + /// Returns the last element in the builder that satisfies a condition. + /// + /// A function to test each element for a condition. + /// The last element that satisfies the condition. + /// is . + /// No element satisfies the condition. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly T Last(Func predicate) + { + T? last = TryGetLast(predicate, out bool found); + + if (!found) + { + ThrowInvalidOperation(SR.Format_0_does_not_contain_matching_element(nameof(RefArrayBuilder<>))); + } + + return last!; + } + + /// + /// Returns the last element in the builder that satisfies a condition using an additional argument. + /// + /// The type of the additional argument. + /// The additional argument to pass to the predicate. + /// A function to test each element for a condition. + /// The last element that satisfies the condition. + /// is . + /// No element satisfies the condition. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly T Last(TArg arg, Func predicate) + { + T? last = TryGetLast(predicate, arg, out bool found); + + if (!found) + { + ThrowInvalidOperation(SR.Format_0_does_not_contain_matching_element(nameof(RefArrayBuilder<>))); + } + + return last!; + } + + /// + /// Returns the last element in the builder, or a default value if the builder is empty. + /// + /// The last element in the builder, or () if the builder is empty. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly T? LastOrDefault() + => TryGetLast(out _); + + /// + /// Returns the last element in the builder, or a specified default value if the builder is empty. + /// + /// The default value to return if the builder is empty. + /// + /// The last element in the builder, or if the builder is empty. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly T LastOrDefault(T defaultValue) + { + T? last = TryGetLast(out bool found); + return found ? last! : defaultValue; + } + + /// + /// Returns the last element that satisfies a condition, or a default value if no such element is found. + /// + /// A function to test each element for a condition. + /// + /// The last element that satisfies the condition, or () if no such element is found. + /// + /// is . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly T? LastOrDefault(Func predicate) + => TryGetLast(predicate, out _); + + /// + /// Returns the last element that satisfies a condition, or a specified default value if no such element is found. + /// + /// A function to test each element for a condition. + /// The default value to return if no element satisfies the condition. + /// + /// The last element that satisfies the condition, or if no such element is found. + /// + /// is . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly T LastOrDefault(Func predicate, T defaultValue) + { + T? last = TryGetLast(predicate, out bool found); + return found ? last! : defaultValue; + } + + /// + /// Returns the last element that satisfies a condition using an additional argument, or a default value if no such element is found. + /// + /// The type of the additional argument. + /// The additional argument to pass to the predicate. + /// A function to test each element for a condition. + /// + /// The last element that satisfies the condition, or () if no such element is found. + /// + /// is . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly T? LastOrDefault(TArg arg, Func predicate) + => TryGetLast(predicate, arg, out _); + + /// + /// Returns the last element that satisfies a condition using an additional argument, or a specified default value if no such element is found. + /// + /// The type of the additional argument. + /// The additional argument to pass to the predicate. + /// A function to test each element for a condition. + /// The default value to return if no element satisfies the condition. + /// + /// The last element that satisfies the condition, or if no such element is found. + /// + /// is . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly T LastOrDefault(TArg arg, Func predicate, T defaultValue) + { + T? last = TryGetLast(predicate, arg, out bool found); + return found ? last! : defaultValue; + } + + private readonly T? TryGetFirst(out bool found) + { + if (!IsEmpty) + { + found = true; + return _scope[0]; + } + + found = false; + return default; + } + + private readonly T? TryGetFirst(Func predicate, out bool found) + { + ArgumentNullException.ThrowIfNull(predicate); + + foreach (T item in AsSpan()) + { + if (predicate(item)) + { + found = true; + return item; + } + } + + found = false; + return default; + } + + private readonly T? TryGetFirst(Func predicate, TArg arg, out bool found) + { + ArgumentNullException.ThrowIfNull(predicate); + + foreach (T item in AsSpan()) + { + if (predicate(item, arg)) + { + found = true; + return item; + } + } + + found = false; + return default; + } + + private readonly T? TryGetLast(out bool found) + { + if (!IsEmpty) + { + found = true; + return _scope[_count - 1]; + } + + found = false; + return default; + } + + private readonly T? TryGetLast(Func predicate, out bool found) + { + ArgumentNullException.ThrowIfNull(predicate); + + for (int i = _count - 1; i >= 0; i--) + { + T item = _scope[i]; + if (predicate(item)) + { + found = true; + return item; + } + } + + found = false; + return default; + } + + private readonly T? TryGetLast(Func predicate, TArg arg, out bool found) + { + ArgumentNullException.ThrowIfNull(predicate); + + for (int i = _count - 1; i >= 0; i--) + { + T item = _scope[i]; + if (predicate(item, arg)) + { + found = true; + return item; + } + } + + found = false; + return default; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + [DoesNotReturn] + private static void ThrowInvalidOperation(string message) + => throw new InvalidOperationException(message); +} diff --git a/src/Framework/EscapingUtilities.cs b/src/Framework/EscapingUtilities.cs index 8bde0027840..5f072bf282f 100644 --- a/src/Framework/EscapingUtilities.cs +++ b/src/Framework/EscapingUtilities.cs @@ -1,312 +1,357 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; +#if NET +using System.Buffers; +#endif using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text; - +using Microsoft.Build.Collections; using Microsoft.Build.Framework; +using Microsoft.Build.Framework.Utilities; using Microsoft.NET.StringTools; -#nullable disable +#pragma warning disable SA1519 // Braces should not be omitted from multi-line child statement + +namespace Microsoft.Build.Shared; -namespace Microsoft.Build.Shared +/// +/// Provides static methods for escaping and unescaping strings using the MSBuild %XX format, +/// where XX is the two-digit hexadecimal representation of the character's ASCII value. +/// +internal static class EscapingUtilities { /// - /// This class implements static methods to assist with unescaping of %XX codes - /// in the MSBuild file format. + /// Cache of escaped strings for use in performance-critical scenarios with significant expected string reuse. /// /// - /// PERF: since we escape and unescape relatively frequently, it may be worth caching - /// the last N strings that were (un)escaped + /// The cache currently grows unbounded. /// - internal static class EscapingUtilities + private static readonly Dictionary s_escapedStringCache = new(StringComparer.Ordinal); + + private static bool TryGetFromCache(string value, [NotNullWhen(true)] out string? result) { - /// - /// Optional cache of escaped strings for use when needing to escape in performance-critical scenarios with significant - /// expected string reuse. - /// - private static readonly Dictionary s_unescapedToEscapedStrings = new Dictionary(StringComparer.Ordinal); + lock (s_escapedStringCache) + { + return s_escapedStringCache.TryGetValue(value, out result); + } + } - private static bool TryDecodeHexDigit(char character, out int value) + private static void AddToCache(string key, string value) + { + lock (s_escapedStringCache) { - if (character >= '0' && character <= '9') - { - value = character - '0'; - return true; - } - if (character >= 'A' && character <= 'F') - { - value = character - 'A' + 10; - return true; - } - if (character >= 'a' && character <= 'f') + s_escapedStringCache[key] = value; + } + } + +#if NET + private static readonly SearchValues s_searchValues = SearchValues.Create(['%', '*', '?', '@', '$', '(', ')', ';', '\'']); + + private static int IndexOfAnyEscapeChar(string value, int startIndex = 0) + { + int i = value.AsSpan(startIndex).IndexOfAny(s_searchValues); + return i < 0 ? i : i + startIndex; + } +#else + // All chars in s_charsToEscape lie within the ASCII range ['$' (0x24) .. '@' (0x40)]. + // Encoding each as bit (c - '$') in a uint gives a 29-bit bitmask that replaces the + // per-char O(k) array scan inside IndexOfAny with a single range check + bit test. + // Bit: 0='$' 1='%' 3='\'' 4='(' 5=')' 6='*' 23=';' 27='?' 28='@' + private const uint EscapeCharBitmask = 0x1880_007Bu; + + private static int IndexOfAnyEscapeChar(string value, int startIndex = 0) + { + for (int i = startIndex; i < value.Length; i++) + { + int offset = value[i] - '$'; + if ((uint)offset <= 28u && ((EscapeCharBitmask >> offset) & 1u) != 0) { - value = character - 'a' + 10; - return true; + return i; } - value = default; - return false; } - /// - /// Replaces all instances of %XX in the input string with the character represented - /// by the hexadecimal number XX. - /// - /// The string to unescape. - /// If the string should be trimmed before being unescaped. - /// unescaped string - internal static string UnescapeAll(string escapedString, bool trim = false) + return -1; + } +#endif + + private static bool TryDecodeHexDigit(char c, out int digit) + { + digit = HexConverter.FromChar(c); + return digit != 0xff; + } + + /// + /// Returns the lowercase hexadecimal digit character for . + /// + /// A value in the range [0, 15]. + /// The character 09 or af. + private static char HexDigitChar(int value) + => (char)(value + (value < 10 ? '0' : 'a' - 10)); + + /// + /// Replaces all instances of %XX in the input string with the character represented + /// by the hexadecimal number XX. + /// + /// The string to unescape. + /// Whether the string should be trimmed before being unescaped. + /// + /// The unescaped string. + /// + [return: NotNullIfNotNull(nameof(value))] + public static string? UnescapeAll(string? value, bool trim = false) + { + if (value.IsNullOrEmpty()) + { + return value; + } + + int startIndex = 0; + int endIndex = value.Length; + + if (trim) { - // If the string doesn't contain anything, then by definition it doesn't - // need unescaping. - if (String.IsNullOrEmpty(escapedString)) + while (startIndex < endIndex && char.IsWhiteSpace(value[startIndex])) { - return escapedString; + startIndex++; } - // If there are no percent signs, just return the original string immediately. - // Don't even instantiate the StringBuilder. - int indexOfPercent = escapedString.IndexOf('%'); - if (indexOfPercent == -1) + if (startIndex == endIndex) { - return trim ? escapedString.Trim() : escapedString; + return string.Empty; } - // This is where we're going to build up the final string to return to the caller. - StringBuilder unescapedString = StringBuilderCache.Acquire(escapedString.Length); - - int currentPosition = 0; - int escapedStringLength = escapedString.Length; - if (trim) + while (char.IsWhiteSpace(value[endIndex - 1])) { - while (currentPosition < escapedString.Length && Char.IsWhiteSpace(escapedString[currentPosition])) - { - currentPosition++; - } - if (currentPosition == escapedString.Length) - { - return String.Empty; - } - while (Char.IsWhiteSpace(escapedString[escapedStringLength - 1])) - { - escapedStringLength--; - } + endIndex--; } + } - // Loop until there are no more percent signs in the input string. - while (indexOfPercent != -1) + // Search only within the active [startIndex, endIndex) window. + int percentIndex = value.IndexOf('%', startIndex, endIndex - startIndex); + if (percentIndex == -1) + { + // value contains no escape sequences. + return GetDefaultResult(value, startIndex, endIndex); + } + + StringBuilder? sb = null; + + do + { + // There must be two hex characters following the percent sign. + if (percentIndex <= endIndex - 3 && + TryDecodeHexDigit(value[percentIndex + 1], out int hi) && + TryDecodeHexDigit(value[percentIndex + 2], out int lo)) { - // There must be two hex characters following the percent sign - // for us to even consider doing anything with this. - if ( - (indexOfPercent <= (escapedStringLength - 3)) && - TryDecodeHexDigit(escapedString[indexOfPercent + 1], out int digit1) && - TryDecodeHexDigit(escapedString[indexOfPercent + 2], out int digit2)) - { - // First copy all the characters up to the current percent sign into - // the destination. - unescapedString.Append(escapedString, currentPosition, indexOfPercent - currentPosition); + sb ??= StringBuilderCache.Acquire(value.Length); - // Convert the %XX to an actual real character. - char unescapedCharacter = (char)((digit1 << 4) + digit2); + sb.Append(value, startIndex, percentIndex - startIndex); + sb.Append((char)((hi << 4) + lo)); + startIndex = percentIndex + 3; + } - // if the unescaped character is not on the exception list, append it - unescapedString.Append(unescapedCharacter); + int nextIndex = percentIndex + 1; + percentIndex = value.IndexOf('%', nextIndex, endIndex - nextIndex); + } + while (percentIndex >= 0); - // Advance the current pointer to reflect the fact that the destination string - // is up to date with everything up to and including this escape code we just found. - currentPosition = indexOfPercent + 3; - } + if (sb is null) + { + // No escape sequences were decoded; return the original string, or the trimmed + // slice if trim was requested. + return GetDefaultResult(value, startIndex, endIndex); + } - // Find the next percent sign. - indexOfPercent = escapedString.IndexOf('%', indexOfPercent + 1); - } + sb.Append(value, startIndex, endIndex - startIndex); - // Okay, there are no more percent signs in the input string, so just copy the remaining - // characters into the destination. - unescapedString.Append(escapedString, currentPosition, escapedStringLength - currentPosition); + return StringBuilderCache.GetStringAndRelease(sb); - return StringBuilderCache.GetStringAndRelease(unescapedString); + static string GetDefaultResult(string value, int startIndex, int endIndex) + => startIndex == 0 && endIndex == value.Length + ? value + : value.Substring(startIndex, endIndex - startIndex); + } + + /// + /// Escapes special characters in the input string by replacing them with their %XX equivalents. + /// + /// The string to escape. + /// + /// if the cache should be checked for an existing result and the + /// new result should be stored. Note: This is only recommended when significant repetition of + /// the escaped string is expected. The cache currently grows unbounded. + /// + /// The escaped string. + [return: NotNullIfNotNull(nameof(value))] + public static string? Escape(string? value, bool cache = false) + { + if (value.IsNullOrEmpty()) + { + return value; } + // Find the first special char; if none, return early without allocating anything. + int firstSpecialCharIndex = IndexOfAnyEscapeChar(value); + if (firstSpecialCharIndex < 0) + { + return value; + } - /// - /// Adds instances of %XX in the input string where the char to be escaped appears - /// XX is the hex value of the ASCII code for the char. Interns and caches the result. - /// - /// - /// NOTE: Only recommended for use in scenarios where there's expected to be significant - /// repetition of the escaped string. Cache currently grows unbounded. - /// - internal static string EscapeWithCaching(string unescapedString) + if (cache && TryGetFromCache(value, out string? result)) { - return EscapeWithOptionalCaching(unescapedString, cache: true); + return result; } - /// - /// Adds instances of %XX in the input string where the char to be escaped appears - /// XX is the hex value of the ASCII code for the char. - /// - /// The string to escape. - /// escaped string - internal static string Escape(string unescapedString) + using RefArrayBuilder specialCharIndices = new(initialCapacity: 16); + int specialCharIndex = firstSpecialCharIndex; + + do { - return EscapeWithOptionalCaching(unescapedString, cache: false); + specialCharIndices.Add(specialCharIndex); + specialCharIndex = IndexOfAnyEscapeChar(value, specialCharIndex + 1); } + while (specialCharIndex >= 0); + + result = Encode(value, specialCharIndices.AsSpan()); - /// - /// Adds instances of %XX in the input string where the char to be escaped appears - /// XX is the hex value of the ASCII code for the char. Caches if requested. - /// - /// The string to escape. - /// - /// True if the cache should be checked, and if the resultant string - /// should be cached. - /// - private static string EscapeWithOptionalCaching(string unescapedString, bool cache) + if (cache) { - // If there are no special chars, just return the original string immediately. - // Don't even instantiate the StringBuilder. - if (String.IsNullOrEmpty(unescapedString) || !ContainsReservedCharacters(unescapedString)) - { - return unescapedString; - } + result = Strings.WeakIntern(result); + AddToCache(value, result); + } + + return result; - // next, if we're caching, check to see if it's already there. - if (cache) + static string Encode(string value, ReadOnlySpan specialCharIndices) + { + // Each special char expands from 1 to 3 chars (%XX), a net gain of 2 each. + int length = value.Length + (specialCharIndices.Length * 2); + +#if NET + return string.Create(length, new EncodingHelper(value, specialCharIndices), static (destination, state) => { - lock (s_unescapedToEscapedStrings) + var (source, specialCharIndices) = state; + + int sourceIndex = 0; + + foreach (int specialCharIndex in specialCharIndices) { - string cachedEscapedString; - if (s_unescapedToEscapedStrings.TryGetValue(unescapedString, out cachedEscapedString)) + int charsToCopy = specialCharIndex - sourceIndex; + if (charsToCopy > 0) { - return cachedEscapedString; + source.Slice(sourceIndex, charsToCopy).CopyTo(destination); } + + destination = destination[charsToCopy..]; + + char ch = source[specialCharIndex]; + destination[0] = '%'; + destination[1] = HexDigitChar(ch >> 4); + destination[2] = HexDigitChar(ch & 0x0F); + destination = destination[3..]; + + sourceIndex = specialCharIndex + 1; } - } - // This is where we're going to build up the final string to return to the caller. - StringBuilder escapedStringBuilder = StringBuilderCache.Acquire(unescapedString.Length * 2); + if (sourceIndex < source.Length) + { + source.Slice(sourceIndex).CopyTo(destination); + } + }); - AppendEscapedString(escapedStringBuilder, unescapedString); +#else - if (!cache) + string result = new('\0', length); + + unsafe { - return StringBuilderCache.GetStringAndRelease(escapedStringBuilder); - } + fixed (char* src = value) + fixed (char* dst = result) + { + int srcIndex = 0; + int dstIndex = 0; - string escapedString = Strings.WeakIntern(escapedStringBuilder.ToString()); - StringBuilderCache.Release(escapedStringBuilder); + foreach (int specialCharIdx in specialCharIndices) + { + int charsToCopy = specialCharIdx - srcIndex; + if (charsToCopy > 0) + { + Buffer.MemoryCopy(src + srcIndex, dst + dstIndex, charsToCopy * sizeof(char), charsToCopy * sizeof(char)); + dstIndex += charsToCopy; + } + + char ch = src[specialCharIdx]; + dst[dstIndex] = '%'; + dst[dstIndex + 1] = HexDigitChar(ch >> 4); + dst[dstIndex + 2] = HexDigitChar(ch & 0x0F); + dstIndex += 3; + + srcIndex = specialCharIdx + 1; + } - lock (s_unescapedToEscapedStrings) - { - s_unescapedToEscapedStrings[unescapedString] = escapedString; + int remainingChars = value.Length - srcIndex; + if (remainingChars > 0) + { + Buffer.MemoryCopy(src + srcIndex, dst + dstIndex, remainingChars * sizeof(char), remainingChars * sizeof(char)); + } + } } - return escapedString; + return result; +#endif } + } + +#if NET + private readonly ref struct EncodingHelper(ReadOnlySpan value, ReadOnlySpan indices) + { + public readonly ReadOnlySpan Value = value; + public readonly ReadOnlySpan Indices = indices; - /// - /// Before trying to actually escape the string, it can be useful to call this method to determine - /// if escaping is necessary at all. This can save lots of calls to copy around item metadata - /// that is really the same whether escaped or not. - /// - /// - /// - private static bool ContainsReservedCharacters( - string unescapedString) + public void Deconstruct(out ReadOnlySpan value, out ReadOnlySpan indices) { - return -1 != unescapedString.IndexOfAny(s_charsToEscape); + value = Value; + indices = Indices; } + } +#endif - /// - /// Determines whether the string contains the escaped form of '*' or '?'. - /// - /// - /// - internal static bool ContainsEscapedWildcards(string escapedString) + /// + /// Determines whether contains the escaped form of + /// * (%2a/%2A) or ? (%3f/%3F). + /// + /// The string to check. + /// + /// if the string contains an escaped wildcard; otherwise, . + /// + public static bool ContainsEscapedWildcards(string value) + { + if (value.Length < 3) { - if (escapedString.Length < 3) - { - return false; - } - // Look for the first %. We know that it has to be followed by at least two more characters so we subtract 2 - // from the length to search. - int index = escapedString.IndexOf('%', 0, escapedString.Length - 2); - while (index != -1) - { - if (escapedString[index + 1] == '2' && (escapedString[index + 2] == 'a' || escapedString[index + 2] == 'A')) - { - // %2a or %2A - return true; - } - if (escapedString[index + 1] == '3' && (escapedString[index + 2] == 'f' || escapedString[index + 2] == 'F')) - { - // %3f or %3F - return true; - } - // Continue searching for % starting at (index + 1). We know that it has to be followed by at least two - // more characters so we subtract 2 from the length of the substring to search. - index = escapedString.IndexOf('%', index + 1, escapedString.Length - (index + 1) - 2); - } return false; } - /// - /// Convert the given integer into its hexadecimal representation. - /// - /// The number to convert, which must be non-negative and less than 16 - /// The character which is the hexadecimal representation of . - private static char HexDigitChar(int x) - { - return (char)(x + (x < 10 ? '0' : ('a' - 10))); - } + // Search for '%', knowing it must be followed by at least 2 more characters. + int percentIndex = value.IndexOf('%', startIndex: 0, value.Length - 2); - /// - /// Append the escaped version of the given character to a . - /// - /// The to which to append. - /// The character to escape. - private static void AppendEscapedChar(StringBuilder sb, char ch) + while (percentIndex != -1) { - // Append the escaped version which is a percent sign followed by two hexadecimal digits - sb.Append('%'); - sb.Append(HexDigitChar(ch / 0x10)); - sb.Append(HexDigitChar(ch & 0x0F)); - } + char c = value[percentIndex + 1]; - /// - /// Append the escaped version of the given string to a . - /// - /// The to which to append. - /// The unescaped string. - private static void AppendEscapedString(StringBuilder sb, string unescapedString) - { - // Replace each unescaped special character with an escape sequence one - for (int idx = 0; ;) + if ((c is '2' && value[percentIndex + 2] is 'a' or 'A') || + (c is '3' && value[percentIndex + 2] is 'f' or 'F')) { - int nextIdx = unescapedString.IndexOfAny(s_charsToEscape, idx); - if (nextIdx == -1) - { - sb.Append(unescapedString, idx, unescapedString.Length - idx); - break; - } - - sb.Append(unescapedString, idx, nextIdx - idx); - AppendEscapedChar(sb, unescapedString[nextIdx]); - idx = nextIdx + 1; + // %2a or %2A → '*' + // %3f or %3F → '?' + return true; } + + percentIndex = value.IndexOf('%', percentIndex + 1, value.Length - (percentIndex + 1) - 2); } - /// - /// Special characters that need escaping. - /// It's VERY important that the percent character is the FIRST on the list - since it's both a character - /// we escape and use in escape sequences, we can unintentionally escape other escape sequences if we - /// don't process it first. Of course we'll have a similar problem if we ever decide to escape hex digits - /// (that would require rewriting the algorithm) but since it seems unlikely that we ever do, this should - /// be good enough to avoid complicating the algorithm at this point. - /// - private static readonly char[] s_charsToEscape = { '%', '*', '?', '@', '$', '(', ')', ';', '\'' }; + return false; } } diff --git a/src/Framework/Polyfills/StringExtensions.cs b/src/Framework/Polyfills/StringExtensions.cs new file mode 100644 index 00000000000..188df515e3c --- /dev/null +++ b/src/Framework/Polyfills/StringExtensions.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Build; + +internal static class StringExtensions +{ + /// + public static bool IsNullOrEmpty([NotNullWhen(false)] this string? value) + => string.IsNullOrEmpty(value); + + /// + public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? value) + => string.IsNullOrWhiteSpace(value); +} diff --git a/src/Framework/Polyfills/UnscopedRefAttribute.cs b/src/Framework/Polyfills/UnscopedRefAttribute.cs new file mode 100644 index 00000000000..09d1c7e2438 --- /dev/null +++ b/src/Framework/Polyfills/UnscopedRefAttribute.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET7_0_OR_GREATER + +using System.Runtime.CompilerServices; + +// This is a supporting forwarder for an internal polyfill API +[assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.UnscopedRefAttribute))] + +#else + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// Used to indicate a byref escapes and is not scoped. +/// +/// +/// +/// There are several cases where the C# compiler treats a as implicitly +/// - where the compiler does not allow the to escape the method. +/// +/// +/// For example: +/// +/// for instance methods. +/// parameters that refer to types. +/// parameters. +/// +/// +/// +/// This attribute is used in those instances where the should be allowed to escape. +/// +/// +/// Applying this attribute, in any form, has impact on consumers of the applicable API. It is necessary for +/// API authors to understand the lifetime implications of applying this attribute and how it may impact their users. +/// +/// +[AttributeUsage( + AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter, + AllowMultiple = false, + Inherited = false)] +internal sealed class UnscopedRefAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + public UnscopedRefAttribute() + { + } +} +#endif diff --git a/src/Framework/Resources/SR.resx b/src/Framework/Resources/SR.resx index 91364f3bab2..ebcc88a74d5 100644 --- a/src/Framework/Resources/SR.resx +++ b/src/Framework/Resources/SR.resx @@ -193,4 +193,10 @@ When attempting to generate a reference assembly path from the path "{0}" and the framework moniker "{1}" there was an error. {2} + + '{0}' does not contain matching element. + + + '{0}' contains no elements. + \ No newline at end of file diff --git a/src/Framework/Resources/xlf/SR.cs.xlf b/src/Framework/Resources/xlf/SR.cs.xlf index 3b7bf110d8b..5c8295c4621 100644 --- a/src/Framework/Resources/xlf/SR.cs.xlf +++ b/src/Framework/Resources/xlf/SR.cs.xlf @@ -127,6 +127,16 @@ Verze {0} sady Visual Studio není podporovaná. Zadejte hodnotu z výčtu Microsoft.Build.Utilities.VisualStudioVersion. + + '{0}' contains no elements. + '{0}' contains no elements. + + + + '{0}' does not contain matching element. + '{0}' does not contain matching element. + + \ No newline at end of file diff --git a/src/Framework/Resources/xlf/SR.de.xlf b/src/Framework/Resources/xlf/SR.de.xlf index b16983f95a3..2e0c24d2abf 100644 --- a/src/Framework/Resources/xlf/SR.de.xlf +++ b/src/Framework/Resources/xlf/SR.de.xlf @@ -127,6 +127,16 @@ Visual Studio-Version "{0}" wird nicht unterstützt. Geben Sie einen Wert aus der Enumeration "Microsoft.Build.Utilities.VisualStudioVersion" an. + + '{0}' contains no elements. + '{0}' contains no elements. + + + + '{0}' does not contain matching element. + '{0}' does not contain matching element. + + \ No newline at end of file diff --git a/src/Framework/Resources/xlf/SR.es.xlf b/src/Framework/Resources/xlf/SR.es.xlf index e8fdc990dcc..2f1056cd32c 100644 --- a/src/Framework/Resources/xlf/SR.es.xlf +++ b/src/Framework/Resources/xlf/SR.es.xlf @@ -127,6 +127,16 @@ La versión "{0}" de Visual Studio no es compatible. Especifique un valor de la enumeración Microsoft.Build.Utilities.VisualStudioVersion. + + '{0}' contains no elements. + '{0}' contains no elements. + + + + '{0}' does not contain matching element. + '{0}' does not contain matching element. + + \ No newline at end of file diff --git a/src/Framework/Resources/xlf/SR.fr.xlf b/src/Framework/Resources/xlf/SR.fr.xlf index 5cb761031a6..f28cf91886d 100644 --- a/src/Framework/Resources/xlf/SR.fr.xlf +++ b/src/Framework/Resources/xlf/SR.fr.xlf @@ -127,6 +127,16 @@ La version "{0}" de Visual Studio n'est pas prise en charge. Spécifiez une valeur de l'énumération Microsoft.Build.Utilities.VisualStudioVersion. + + '{0}' contains no elements. + '{0}' contains no elements. + + + + '{0}' does not contain matching element. + '{0}' does not contain matching element. + + \ No newline at end of file diff --git a/src/Framework/Resources/xlf/SR.it.xlf b/src/Framework/Resources/xlf/SR.it.xlf index 6ebb060c611..16498bce12b 100644 --- a/src/Framework/Resources/xlf/SR.it.xlf +++ b/src/Framework/Resources/xlf/SR.it.xlf @@ -127,6 +127,16 @@ La versione "{0}" di Visual Studio non è supportata. Specificare un valore dall'enumerazione Microsoft.Build.Utilities.VisualStudioVersion. + + '{0}' contains no elements. + '{0}' contains no elements. + + + + '{0}' does not contain matching element. + '{0}' does not contain matching element. + + \ No newline at end of file diff --git a/src/Framework/Resources/xlf/SR.ja.xlf b/src/Framework/Resources/xlf/SR.ja.xlf index 800dbc3f2cd..668cf563a91 100644 --- a/src/Framework/Resources/xlf/SR.ja.xlf +++ b/src/Framework/Resources/xlf/SR.ja.xlf @@ -127,6 +127,16 @@ Visual Studio のバージョン "{0}" はサポートされていません。列挙 Microsoft.Build.Utilities.VisualStudioVersion から値を指定してください。 + + '{0}' contains no elements. + '{0}' contains no elements. + + + + '{0}' does not contain matching element. + '{0}' does not contain matching element. + + \ No newline at end of file diff --git a/src/Framework/Resources/xlf/SR.ko.xlf b/src/Framework/Resources/xlf/SR.ko.xlf index f16d86f8415..91aa56c2e76 100644 --- a/src/Framework/Resources/xlf/SR.ko.xlf +++ b/src/Framework/Resources/xlf/SR.ko.xlf @@ -127,6 +127,16 @@ Visual Studio 버전 "{0}"이(가) 지원되지 않습니다. Microsoft.Build.Utilities.VisualStudioVersion 열거형에서 값을 지정하세요. + + '{0}' contains no elements. + '{0}' contains no elements. + + + + '{0}' does not contain matching element. + '{0}' does not contain matching element. + + \ No newline at end of file diff --git a/src/Framework/Resources/xlf/SR.pl.xlf b/src/Framework/Resources/xlf/SR.pl.xlf index fe5f6d21fe3..b5e5451c0d4 100644 --- a/src/Framework/Resources/xlf/SR.pl.xlf +++ b/src/Framework/Resources/xlf/SR.pl.xlf @@ -127,6 +127,16 @@ Program Visual Studio w wersji „{0}” nie jest obsługiwany. Podaj wartość z wyliczenia Microsoft.Build.Utilities.VisualStudioVersion. + + '{0}' contains no elements. + '{0}' contains no elements. + + + + '{0}' does not contain matching element. + '{0}' does not contain matching element. + + \ No newline at end of file diff --git a/src/Framework/Resources/xlf/SR.pt-BR.xlf b/src/Framework/Resources/xlf/SR.pt-BR.xlf index d0265d397b2..dac25278675 100644 --- a/src/Framework/Resources/xlf/SR.pt-BR.xlf +++ b/src/Framework/Resources/xlf/SR.pt-BR.xlf @@ -127,6 +127,16 @@ O Visual Studio versão "{0}" não tem suporte. Especifique um valor da enumeração Microsoft.Build.Utilities.VisualStudioVersion. + + '{0}' contains no elements. + '{0}' contains no elements. + + + + '{0}' does not contain matching element. + '{0}' does not contain matching element. + + \ No newline at end of file diff --git a/src/Framework/Resources/xlf/SR.ru.xlf b/src/Framework/Resources/xlf/SR.ru.xlf index 9a075bf2564..8e1be411dd9 100644 --- a/src/Framework/Resources/xlf/SR.ru.xlf +++ b/src/Framework/Resources/xlf/SR.ru.xlf @@ -127,6 +127,16 @@ Visual Studio версии "{0}" не поддерживается. Укажите значение из перечисления Microsoft.Build.Utilities.VisualStudioVersion. + + '{0}' contains no elements. + '{0}' contains no elements. + + + + '{0}' does not contain matching element. + '{0}' does not contain matching element. + + \ No newline at end of file diff --git a/src/Framework/Resources/xlf/SR.tr.xlf b/src/Framework/Resources/xlf/SR.tr.xlf index 69d567ac46f..5a2098ed474 100644 --- a/src/Framework/Resources/xlf/SR.tr.xlf +++ b/src/Framework/Resources/xlf/SR.tr.xlf @@ -127,6 +127,16 @@ Visual Studio "{0}" sürümü desteklenmiyor. Lütfen Microsoft.Build.Utilities.VisualStudioVersion sabit listesinden bir değer belirtin. + + '{0}' contains no elements. + '{0}' contains no elements. + + + + '{0}' does not contain matching element. + '{0}' does not contain matching element. + + \ No newline at end of file diff --git a/src/Framework/Resources/xlf/SR.zh-Hans.xlf b/src/Framework/Resources/xlf/SR.zh-Hans.xlf index b1b11c53f40..4e83c50e0d7 100644 --- a/src/Framework/Resources/xlf/SR.zh-Hans.xlf +++ b/src/Framework/Resources/xlf/SR.zh-Hans.xlf @@ -127,6 +127,16 @@ 不支持 Visual Studio 版本“{0}”。请指定枚举 Microsoft.Build.Utilities.VisualStudioVersion 中的某个值。 + + '{0}' contains no elements. + '{0}' contains no elements. + + + + '{0}' does not contain matching element. + '{0}' does not contain matching element. + + \ No newline at end of file diff --git a/src/Framework/Resources/xlf/SR.zh-Hant.xlf b/src/Framework/Resources/xlf/SR.zh-Hant.xlf index 926f61fab5b..bcba22030a4 100644 --- a/src/Framework/Resources/xlf/SR.zh-Hant.xlf +++ b/src/Framework/Resources/xlf/SR.zh-Hant.xlf @@ -127,6 +127,16 @@ 不支援 Visual Studio 版本 "{0}"。請從列舉 Microsoft.Build.Utilities.VisualStudioVersion 中指定值。 + + '{0}' contains no elements. + '{0}' contains no elements. + + + + '{0}' does not contain matching element. + '{0}' does not contain matching element. + + \ No newline at end of file diff --git a/src/Framework/Utilities/BufferScope.cs b/src/Framework/Utilities/BufferScope.cs new file mode 100644 index 00000000000..539a2bc339d --- /dev/null +++ b/src/Framework/Utilities/BufferScope.cs @@ -0,0 +1,193 @@ +// 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.Buffers; +using System.Diagnostics; + +namespace Microsoft.Build.Utilities; + +/// +/// Allows renting a buffer from with a using statement. Can be used directly as if it +/// were a . +/// +internal ref struct BufferScope +{ + private T[]? _array; + private Span _span; + + /// + /// Initializes a new instance of the . + /// + /// The required minimum length. + public BufferScope(int minimumLength) + { + _array = ArrayPool.Shared.Rent(minimumLength); + _span = _array; + } + + /// + /// Create the with an initial buffer. Useful for creating with an initial stack + /// allocated buffer. + /// + /// The initial buffer to use. + public BufferScope(Span initialBuffer) + { + _array = null; + _span = initialBuffer; + } + + /// + /// Create the with an initial buffer. Useful for creating with an initial stack + /// allocated buffer. + /// + /// + /// + /// Creating with a stack allocated buffer: + /// using BufferScope<char> buffer = new(stackalloc char[64]); + /// + /// + /// + /// The initial buffer to use. If not large enough for , a buffer will be rented + /// from the shared . + /// + /// + /// The required minimum length. If the is not large enough, this will rent from + /// the shared . + /// + public BufferScope(Span initialBuffer, int minimumLength) + { + if (initialBuffer.Length >= minimumLength) + { + _array = null; + _span = initialBuffer; + } + else + { + _array = ArrayPool.Shared.Rent(minimumLength); + _span = _array; + } + } + + /// + /// Ensure that the buffer has enough space for number of elements. + /// + /// + /// + /// Consider if creating new instances is possible and cleaner than using + /// this method. + /// + /// + /// The minimum number of elements the buffer should be able to hold. + /// True to copy the existing elements when new space is allocated. + public void EnsureCapacity(int capacity, bool copy = false) + { + if (_span.Length >= capacity) + { + return; + } + + // Keep method separate for better inlining. + IncreaseCapacity(capacity, copy); + } + + private void IncreaseCapacity(int capacity, bool copy) + { + Debug.Assert(capacity > _span.Length); + + T[] newArray = ArrayPool.Shared.Rent(capacity); + if (copy) + { + _span.CopyTo(newArray); + } + + if (_array is not null) + { + ArrayPool.Shared.Return(_array, clearArray: TypeInfo.IsReferenceOrContainsReferences()); + } + + _array = newArray; + _span = _array; + } + + /// + /// Gets or sets the element at the specified index. + /// + /// The zero-based index of the element to get or set. + /// The element at the specified index. + public ref T this[int i] + => ref _span[i]; + + /// + /// Forms a slice out of the buffer starting at a specified index for a specified length. + /// + /// The index at which to begin the slice. + /// The desired length of the slice. + /// A span that consists of elements from the buffer starting at . + public readonly Span Slice(int start, int length) + => _span.Slice(start, length); + + /// + /// + /// This is used by C# to enable using the buffer in a fixed statement. + /// + public readonly ref T GetPinnableReference() + => ref _span.GetPinnableReference(); + + /// + /// Gets the number of elements in the buffer. + /// + public readonly int Length => _span.Length; + + /// + /// Returns the buffer as a . + /// + /// A span that represents the buffer. + public readonly Span AsSpan() + => _span; + + /// + /// Implicitly converts a to a . + /// + /// The buffer scope to convert. + /// A span that represents the buffer. + public static implicit operator Span(BufferScope scope) + => scope._span; + + /// + /// Implicitly converts a to a . + /// + /// The buffer scope to convert. + /// A read-only span that represents the buffer. + public static implicit operator ReadOnlySpan(BufferScope scope) + => scope._span; + + /// + /// Returns an enumerator for the buffer's backing . + /// + public readonly Span.Enumerator GetEnumerator() + => _span.GetEnumerator(); + + /// + /// Releases the rented array back to the if one was rented. + /// + public void Dispose() + { + // Clear the span to avoid accidental use after returning the array. + _span = default; + + if (_array is not null) + { + ArrayPool.Shared.Return(_array, clearArray: TypeInfo.IsReferenceOrContainsReferences()); + } + + _array = null; + } + + /// + /// Returns a string representation of the buffer. + /// + /// A string representation of the buffer. + public override readonly string ToString() + => _span.ToString(); +} diff --git a/src/Framework/Utilities/HexConverter.cs b/src/Framework/Utilities/HexConverter.cs new file mode 100644 index 00000000000..a2458cdcee9 --- /dev/null +++ b/src/Framework/Utilities/HexConverter.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET +using System; +#endif +using System.Runtime.CompilerServices; + +namespace Microsoft.Build.Framework.Utilities; + +internal static class HexConverter +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int FromChar(int c) +#if NET + => c >= CharToHexLookup.Length ? 0xFF : CharToHexLookup[c]; +#else + => c >= s_charToHexLookup.Length ? 0xFF : s_charToHexLookup[c]; +#endif + + /// + /// Map from an ASCII char to its hex value, e.g. arr['b'] == 11. 0xFF means it's not a hex digit. + /// +#if NET + private static ReadOnlySpan CharToHexLookup => +#else + private static readonly byte[] s_charToHexLookup = +#endif + [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 15 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 31 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 47 + 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 63 + 0xFF, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 79 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 95 + 0xFF, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 111 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 127 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 143 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 159 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 175 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 191 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 207 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 223 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 239 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // 255 + ]; +} diff --git a/src/Framework/Utilities/TypeInfo.cs b/src/Framework/Utilities/TypeInfo.cs new file mode 100644 index 00000000000..1a071b48798 --- /dev/null +++ b/src/Framework/Utilities/TypeInfo.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET +using System.Runtime.CompilerServices; +#else +using System; +using System.Runtime.InteropServices; +#endif + +namespace Microsoft.Build.Utilities; + +/// +/// Type information for a type . +/// +internal static partial class TypeInfo +{ + private static bool? s_hasReferences; + + /// + /// Returns if the type is a reference type or contains references. + /// + public static bool IsReferenceOrContainsReferences() + { +#if NET + return s_hasReferences ??= RuntimeHelpers.IsReferenceOrContainsReferences(); +#else + return s_hasReferences ??= HasReferences(); + + static bool HasReferences() + { + if (!typeof(T).IsValueType) + { + return true; + } + + if (typeof(T).IsPrimitive + || typeof(T).IsEnum + || typeof(T) == typeof(DateTime)) + { + return false; + } + + try + { + GCHandle handle = GCHandle.Alloc(default(T), GCHandleType.Pinned); + handle.Free(); + return false; + } + catch (Exception) + { + // Contained a reference + return true; + } + } +#endif + } +} diff --git a/src/MSBuild.Benchmarks/EscapingUtilitiesBenchmark.cs b/src/MSBuild.Benchmarks/EscapingUtilitiesBenchmark.cs new file mode 100644 index 00000000000..5102613d057 --- /dev/null +++ b/src/MSBuild.Benchmarks/EscapingUtilitiesBenchmark.cs @@ -0,0 +1,123 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using BenchmarkDotNet.Attributes; +using Microsoft.Build.Shared; + +namespace MSBuild.Benchmarks; + +[MemoryDiagnoser] +public class EscapingUtilitiesBenchmark +{ + /// + /// A typical file path with no special characters — the most common fast path. + /// + private const string NoSpecialChars = @"C:\repos\msbuild\src\Framework\EscapingUtilities.cs"; + + /// + /// A string with a few characters that need escaping (semicolons, parens, percent). + /// Represents a realistic property or item value. + /// + private const string FewSpecialChars = @"Reference=$(PkgPath);Version=1.0.0"; + + /// + /// A string where most characters are escapable — worst case for escaping. + /// + private const string ManySpecialChars = @"%;*?@$();'%;*?@$();'%;*?@$();'"; + + /// + /// An already-escaped string with a few %XX sequences — common unescape input. + /// + private const string FewEscapeSequences = @"Reference%3d%24%28PkgPath%29%3BVersion%3d1.0.0"; + + /// + /// A heavily-escaped string — worst case for unescaping. + /// + private const string ManyEscapeSequences = @"%25%3b%2a%3f%40%24%28%29%3b%27%25%3b%2a%3f%40%24%28%29%3b%27"; + + /// + /// A string with partial/invalid escape sequences that must be skipped. + /// + private const string InvalidEscapeSequences = @"100%done%Z%2%"; + + /// + /// An escaped string with leading/trailing whitespace for the trim path. + /// + private const string EscapedWithWhitespace = @" foo%20bar%3Bbaz "; + + /// + /// An escaped string containing wildcard escape sequences (%2a, %3f). + /// + private const string EscapedWildcards = @"src\**\%2a.cs%3f"; + + /// + /// A long escaped string without wildcard sequences — worst case scan for ContainsEscapedWildcards. + /// + private const string LongNoWildcards = @"abcdefghijklmnopqrstuvwxyz%3babcdefghijklmnopqrstuvwxyz%3babcdefghijklmnopqrstuvwxyz%3babcdefghijklmnopqrstuvwxyz"; + + // --- UnescapeAll --- + + [Benchmark] + public string UnescapeAll_NoSpecialChars() + => EscapingUtilities.UnescapeAll(NoSpecialChars); + + [Benchmark] + public string UnescapeAll_FewEscapeSequences() + => EscapingUtilities.UnescapeAll(FewEscapeSequences); + + [Benchmark] + public string UnescapeAll_ManyEscapeSequences() + => EscapingUtilities.UnescapeAll(ManyEscapeSequences); + + [Benchmark] + public string UnescapeAll_InvalidEscapeSequences() + => EscapingUtilities.UnescapeAll(InvalidEscapeSequences); + + [Benchmark] + public string UnescapeAll_WithTrim() + => EscapingUtilities.UnescapeAll(EscapedWithWhitespace, trim: true); + + // --- Escape --- + + [Benchmark] + public string Escape_NoSpecialChars() + => EscapingUtilities.Escape(NoSpecialChars); + + [Benchmark] + public string Escape_FewSpecialChars() + => EscapingUtilities.Escape(FewSpecialChars); + + [Benchmark] + public string Escape_ManySpecialChars() + => EscapingUtilities.Escape(ManySpecialChars); + + // --- EscapeWithCaching --- + + [Benchmark] + public string EscapeWithCaching_FewSpecialChars() + => EscapingUtilities.Escape(FewSpecialChars, cache: true); + + [Benchmark] + public string EscapeWithCaching_ManySpecialChars() + => EscapingUtilities.Escape(ManySpecialChars, cache: true); + + // --- ContainsEscapedWildcards --- + + [Benchmark] + public bool ContainsEscapedWildcards_NoPercent() + => EscapingUtilities.ContainsEscapedWildcards(NoSpecialChars); + + [Benchmark] + public bool ContainsEscapedWildcards_HasWildcards() + => EscapingUtilities.ContainsEscapedWildcards(EscapedWildcards); + + [Benchmark] + public bool ContainsEscapedWildcards_LongNoWildcards() + => EscapingUtilities.ContainsEscapedWildcards(LongNoWildcards); + + // --- Round-trip --- + + [Benchmark] + public string RoundTrip_EscapeThenUnescape() + => EscapingUtilities.UnescapeAll(EscapingUtilities.Escape(FewSpecialChars)); +} diff --git a/src/Shared/TaskParameter.cs b/src/Shared/TaskParameter.cs index 5fb6aff6ad9..75cd895b97c 100644 --- a/src/Shared/TaskParameter.cs +++ b/src/Shared/TaskParameter.cs @@ -593,7 +593,7 @@ internal TaskParameterTaskItem(ITaskItem copyFrom) // TaskParameterTaskItem's constructor expects escaped values, so escaping them all // is the closest approximation to correct we can get. _escapedItemSpec = EscapingUtilities.Escape(copyFrom.ItemSpec); - _escapedDefiningProject = EscapingUtilities.EscapeWithCaching(copyFrom.GetMetadata(ItemSpecModifiers.DefiningProjectFullPath)); + _escapedDefiningProject = EscapingUtilities.Escape(copyFrom.GetMetadata(ItemSpecModifiers.DefiningProjectFullPath), cache: true); IDictionary customMetadata = copyFrom.CloneCustomMetadata(); _customEscapedMetadata = new Dictionary(MSBuildNameIgnoreCaseComparer.Default); diff --git a/src/Utilities.UnitTests/StringExtensions_Tests.cs b/src/Utilities.UnitTests/StringExtensions_Tests.cs index bc97460ff1d..3bb5b19dfa5 100644 --- a/src/Utilities.UnitTests/StringExtensions_Tests.cs +++ b/src/Utilities.UnitTests/StringExtensions_Tests.cs @@ -67,7 +67,7 @@ public void ReplaceWithStringComparerTest(string aString, string oldValue, strin [InlineData("ab", "", "x", StringComparison.CurrentCulture, typeof(ArgumentException))] public void ReplaceWithStringComparerExceptionCases(string aString, string oldValue, string newValue, StringComparison stringComparison, Type expectedException) { - Should.Throw(() => StringExtensions.Replace(aString, oldValue, newValue, stringComparison), expectedException); + Should.Throw(() => Build.Shared.StringExtensions.Replace(aString, oldValue, newValue, stringComparison), expectedException); } } } diff --git a/src/Utilities/TaskItem.cs b/src/Utilities/TaskItem.cs index 8d0da54927c..88f1737439d 100644 --- a/src/Utilities/TaskItem.cs +++ b/src/Utilities/TaskItem.cs @@ -153,7 +153,7 @@ public TaskItem( if (!(sourceItem is ITaskItem2 sourceItemAsITaskItem2)) { _itemSpec = EscapingUtilities.Escape(sourceItem.ItemSpec); - _definingProject = EscapingUtilities.EscapeWithCaching(sourceItem.GetMetadata(ItemSpecModifiers.DefiningProjectFullPath)); + _definingProject = EscapingUtilities.Escape(sourceItem.GetMetadata(ItemSpecModifiers.DefiningProjectFullPath), cache: true); } else {