From 1c9ea4761f2916f2b299f79d79756afb87528d22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Mon, 26 Jan 2026 12:18:06 +0100 Subject: [PATCH 01/13] absolutizing "" is error --- .../TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs | 2 +- .../TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs b/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs index bec24b034f7..9e4895b4bb5 100644 --- a/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs +++ b/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs @@ -41,7 +41,7 @@ public AbsolutePath ProjectDirectory public AbsolutePath GetAbsolutePath(string path) { // Opt-out for null path when Wave18_4 is disabled - return null as-is. - if (!ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_4) && path is null) + if (!ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_4) && string.IsNullOrEmpty(path)) { return new AbsolutePath(path!, path!, ignoreRootedCheck: true); } diff --git a/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs b/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs index 1bf9009054a..d356e75e996 100644 --- a/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs +++ b/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs @@ -72,7 +72,7 @@ public AbsolutePath ProjectDirectory public AbsolutePath GetAbsolutePath(string path) { // Opt-out for null path when Wave18_4 is disabled - return null as-is. - if (!ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_4) && path is null) + if (!ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_4) && string.IsNullOrEmpty(path)) { return new AbsolutePath(path!, path!, ignoreRootedCheck: true); } From 17bff5e129545f0337c7ba1932f49206457b688e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Mon, 26 Jan 2026 15:06:58 +0100 Subject: [PATCH 02/13] refactor and correct throwing --- .../MultiProcessTaskEnvironmentDriver.cs | 8 +------ .../MultiThreadedTaskEnvironmentDriver.cs | 8 +------ src/Framework/PathHelpers/AbsolutePath.cs | 22 +++++++++++++++++++ 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs b/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs index 9e4895b4bb5..6544b57accf 100644 --- a/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs +++ b/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs @@ -40,13 +40,7 @@ public AbsolutePath ProjectDirectory /// public AbsolutePath GetAbsolutePath(string path) { - // Opt-out for null path when Wave18_4 is disabled - return null as-is. - if (!ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_4) && string.IsNullOrEmpty(path)) - { - return new AbsolutePath(path!, path!, ignoreRootedCheck: true); - } - - return new AbsolutePath(path, basePath: ProjectDirectory); + return AbsolutePath.CreateFromRelative(path, ProjectDirectory); } /// diff --git a/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs b/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs index d356e75e996..7f18d99d7fb 100644 --- a/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs +++ b/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs @@ -71,13 +71,7 @@ public AbsolutePath ProjectDirectory /// public AbsolutePath GetAbsolutePath(string path) { - // Opt-out for null path when Wave18_4 is disabled - return null as-is. - if (!ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_4) && string.IsNullOrEmpty(path)) - { - return new AbsolutePath(path!, path!, ignoreRootedCheck: true); - } - - return new AbsolutePath(path, ProjectDirectory); + return AbsolutePath.CreateFromRelative(path, ProjectDirectory); } /// diff --git a/src/Framework/PathHelpers/AbsolutePath.cs b/src/Framework/PathHelpers/AbsolutePath.cs index 7fb31c57232..ebd8eb5d66b 100644 --- a/src/Framework/PathHelpers/AbsolutePath.cs +++ b/src/Framework/PathHelpers/AbsolutePath.cs @@ -97,6 +97,28 @@ private static void ValidatePath(string path) } #endif } + /// + /// Creates an from a relative path combined with a base path. + /// + /// The path to combine with the base path. + /// The base path to combine with. + /// The combined absolute path. + /// Thrown if is empty and ChangeWave 18.4 is enabled. + internal static AbsolutePath CreateFromRelative(string path, AbsolutePath basePath) + { + // Opt-out for null/empty path when Wave18_4 is disabled - return as-is for legacy behavior. + if (!ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_4) && string.IsNullOrEmpty(path)) + { + return new AbsolutePath(path!, path!, ignoreRootedCheck: true); + } + + if (path == "") + { + throw new ArgumentException("Path must not be empty.", nameof(path)); + } + + return new AbsolutePath(path, basePath); + } /// /// Initializes a new instance of the struct by combining an absolute path with a relative path. From 8371aa59be40ec00b219e997db3c803630321ffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Mon, 26 Jan 2026 15:30:14 +0100 Subject: [PATCH 03/13] tests --- .../BackEnd/TaskEnvironment_Tests.cs | 45 ++- src/Framework.UnitTests/AbsolutePath_Tests.cs | 312 ++++++++++++++++++ 2 files changed, 352 insertions(+), 5 deletions(-) diff --git a/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs b/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs index acb1bb6e832..35aa3770fff 100644 --- a/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs @@ -387,15 +387,50 @@ public void TaskEnvironment_GetAbsolutePath_WithInvalidPathChars_ShouldNotThrow( [Theory] [MemberData(nameof(EnvironmentTypes))] - public void TaskEnvironment_GetAbsolutePath_WithEmptyPath_ReturnsProjectDirectory(string environmentType) + public void TaskEnvironment_GetAbsolutePath_WithEmptyPath_WhenWave18_4Disabled_ReturnsEmptyPath(string environmentType) { + using TestEnvironment testEnv = TestEnvironment.Create(); + ChangeWaves.ResetStateForTests(); + testEnv.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_4.ToString()); + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); + var taskEnvironment = CreateTaskEnvironment(environmentType); - // Empty path should absolutize to project directory (Path.Combine behavior) - var absolutePath = taskEnvironment.GetAbsolutePath(string.Empty); + try + { + // When Wave18_4 is disabled, empty path returns as-is (legacy behavior) + var absolutePath = taskEnvironment.GetAbsolutePath(string.Empty); + + absolutePath.Value.ShouldBe(string.Empty); + absolutePath.OriginalValue.ShouldBe(string.Empty); + } + finally + { + DisposeTaskEnvironment(taskEnvironment); + ChangeWaves.ResetStateForTests(); + } + } + + [Theory] + [MemberData(nameof(EnvironmentTypes))] + public void TaskEnvironment_GetAbsolutePath_WithEmptyPath_WhenWave18_4Enabled_Throws(string environmentType) + { + using TestEnvironment testEnv = TestEnvironment.Create(); + ChangeWaves.ResetStateForTests(); + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); + + var taskEnvironment = CreateTaskEnvironment(environmentType); - absolutePath.Value.ShouldBe(taskEnvironment.ProjectDirectory.Value); - absolutePath.OriginalValue.ShouldBe(string.Empty); + try + { + // When Wave18_4 is enabled, empty path should throw + Should.Throw(() => taskEnvironment.GetAbsolutePath(string.Empty)); + } + finally + { + DisposeTaskEnvironment(taskEnvironment); + ChangeWaves.ResetStateForTests(); + } } [Theory] diff --git a/src/Framework.UnitTests/AbsolutePath_Tests.cs b/src/Framework.UnitTests/AbsolutePath_Tests.cs index 3a099ef36c5..bca6393b1c5 100644 --- a/src/Framework.UnitTests/AbsolutePath_Tests.cs +++ b/src/Framework.UnitTests/AbsolutePath_Tests.cs @@ -1,8 +1,10 @@ // 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.IO; using Microsoft.Build.Framework; +using Microsoft.Build.Shared; using Shouldly; using Xunit; @@ -10,6 +12,12 @@ namespace Microsoft.Build.UnitTests { public class AbsolutePath_Tests { + private static AbsolutePath GetTestBasePath() + { + string baseDirectory = Path.Combine(Path.GetTempPath(), "abspath_test_base"); + return new AbsolutePath(baseDirectory, ignoreRootedCheck: false); + } + private static void ValidatePathAcceptance(string path, bool shouldBeAccepted) { if (shouldBeAccepted) @@ -189,5 +197,309 @@ public void AbsolutePath_UnixPathValidation_ShouldAcceptOnlyTrueAbsolutePaths(st { ValidatePathAcceptance(path, shouldBeAccepted); } + + #region CreateFromRelative Tests + + [Theory] + [InlineData("subfolder")] + [InlineData("deep/nested/path")] + [InlineData(".")] + [InlineData("..")] + [InlineData("./relative")] + [InlineData("../parent")] + [InlineData("file.txt")] + public void CreateFromRelative_WithValidRelativePath_ReturnsAbsolutePath(string relativePath) + { + // Arrange + AbsolutePath basePath = GetTestBasePath(); + string expectedPath = Path.Combine(basePath.Value, relativePath); + + // Act + AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath); + + // Assert + result.Value.ShouldBe(expectedPath); + result.OriginalValue.ShouldBe(relativePath); + Path.IsPathRooted(result.Value).ShouldBeTrue(); + } + + [Fact] + public void CreateFromRelative_WithEmptyPath_WhenWave18_4Enabled_ThrowsArgumentException() + { + // Arrange - Wave18_4 is enabled by default + using TestEnvironment testEnv = TestEnvironment.Create(); + ChangeWaves.ResetStateForTests(); + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); + + AbsolutePath basePath = GetTestBasePath(); + + // Act & Assert + var exception = Should.Throw(() => AbsolutePath.CreateFromRelative(string.Empty, basePath)); + exception.ParamName.ShouldBe("path"); + exception.Message.ShouldContain("must not be empty"); + + ChangeWaves.ResetStateForTests(); + } + + [Fact] + public void CreateFromRelative_WithNullPath_WhenWave18_4Enabled_ThrowsArgumentNullException() + { + // Arrange - Wave18_4 is enabled by default + using TestEnvironment testEnv = TestEnvironment.Create(); + ChangeWaves.ResetStateForTests(); + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); + + AbsolutePath basePath = GetTestBasePath(); + + // Act & Assert - Path.Combine throws ArgumentNullException for null path + Should.Throw(() => AbsolutePath.CreateFromRelative(null!, basePath)); + + ChangeWaves.ResetStateForTests(); + } + + [Fact] + public void CreateFromRelative_WithEmptyPath_WhenWave18_4Disabled_ReturnsEmptyPath() + { + // Arrange + using TestEnvironment testEnv = TestEnvironment.Create(); + ChangeWaves.ResetStateForTests(); + testEnv.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_4.ToString()); + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); + + AbsolutePath basePath = GetTestBasePath(); + + // Act - Legacy behavior returns empty path as-is + AbsolutePath result = AbsolutePath.CreateFromRelative(string.Empty, basePath); + + // Assert + result.Value.ShouldBe(string.Empty); + result.OriginalValue.ShouldBe(string.Empty); + + ChangeWaves.ResetStateForTests(); + } + + [Fact] + public void CreateFromRelative_WithNullPath_WhenWave18_4Disabled_ReturnsNullPath() + { + // Arrange + using TestEnvironment testEnv = TestEnvironment.Create(); + ChangeWaves.ResetStateForTests(); + testEnv.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_4.ToString()); + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); + + AbsolutePath basePath = GetTestBasePath(); + + // Act - Legacy behavior returns null path as-is + AbsolutePath result = AbsolutePath.CreateFromRelative(null!, basePath); + + // Assert + result.Value.ShouldBeNull(); + result.OriginalValue.ShouldBeNull(); + + ChangeWaves.ResetStateForTests(); + } + + [WindowsOnlyTheory] + [InlineData("C:\\absolute\\path")] + [InlineData("D:\\another\\absolute")] + public void CreateFromRelative_WithWindowsAbsolutePath_ReturnsAbsolutePath(string absolutePath) + { + // Arrange + AbsolutePath basePath = GetTestBasePath(); + // Path.Combine with an absolute second path returns the second path + string expectedPath = absolutePath; + + // Act + AbsolutePath result = AbsolutePath.CreateFromRelative(absolutePath, basePath); + + // Assert + result.Value.ShouldBe(expectedPath); + result.OriginalValue.ShouldBe(absolutePath); + } + + [UnixOnlyTheory] + [InlineData("/absolute/path")] + [InlineData("/another/absolute")] + public void CreateFromRelative_WithUnixAbsolutePath_ReturnsAbsolutePath(string absolutePath) + { + // Arrange + AbsolutePath basePath = GetTestBasePath(); + // Path.Combine with an absolute second path returns the second path + string expectedPath = absolutePath; + + // Act + AbsolutePath result = AbsolutePath.CreateFromRelative(absolutePath, basePath); + + // Assert + result.Value.ShouldBe(expectedPath); + result.OriginalValue.ShouldBe(absolutePath); + } + + [Fact] + public void CreateFromRelative_WithNestedRelativePath_CombinesCorrectly() + { + // Arrange + AbsolutePath basePath = GetTestBasePath(); + string relativePath = Path.Combine("level1", "level2", "level3", "file.txt"); + string expectedPath = Path.Combine(basePath.Value, relativePath); + + // Act + AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath); + + // Assert + result.Value.ShouldBe(expectedPath); + result.OriginalValue.ShouldBe(relativePath); + } + + [Fact] + public void CreateFromRelative_WithCurrentDirectory_CombinesCorrectly() + { + // Arrange + AbsolutePath basePath = GetTestBasePath(); + string relativePath = "."; + string expectedPath = Path.Combine(basePath.Value, relativePath); + + // Act + AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath); + + // Assert + result.Value.ShouldBe(expectedPath); + result.OriginalValue.ShouldBe(relativePath); + } + + [Fact] + public void CreateFromRelative_WithParentDirectory_CombinesCorrectly() + { + // Arrange + AbsolutePath basePath = GetTestBasePath(); + string relativePath = ".."; + string expectedPath = Path.Combine(basePath.Value, relativePath); + + // Act + AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath); + + // Assert + result.Value.ShouldBe(expectedPath); + result.OriginalValue.ShouldBe(relativePath); + } + + [Theory] + [InlineData("../sibling")] + [InlineData("../../grandparent")] + [InlineData("../sibling/child")] + public void CreateFromRelative_WithParentTraversal_CombinesCorrectly(string relativePath) + { + // Arrange + AbsolutePath basePath = GetTestBasePath(); + string expectedPath = Path.Combine(basePath.Value, relativePath); + + // Act + AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath); + + // Assert + result.Value.ShouldBe(expectedPath); + result.OriginalValue.ShouldBe(relativePath); + } + + [Fact] + public void CreateFromRelative_WithPathContainingSpaces_CombinesCorrectly() + { + // Arrange + AbsolutePath basePath = GetTestBasePath(); + string relativePath = "folder with spaces/file name.txt"; + string expectedPath = Path.Combine(basePath.Value, relativePath); + + // Act + AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath); + + // Assert + result.Value.ShouldBe(expectedPath); + result.OriginalValue.ShouldBe(relativePath); + } + + [Fact] + public void CreateFromRelative_PreservesOriginalValue() + { + // Arrange + AbsolutePath basePath = GetTestBasePath(); + string relativePath = "some/relative/path"; + + // Act + AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath); + + // Assert - OriginalValue should be the relative path, not the combined path + result.OriginalValue.ShouldBe(relativePath); + result.Value.ShouldNotBe(relativePath); + result.Value.ShouldContain(relativePath); + } + + [Fact] + public void CreateFromRelative_ResultIsRooted() + { + // Arrange + AbsolutePath basePath = GetTestBasePath(); + string relativePath = "any/relative/path"; + + // Act + AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath); + + // Assert + Path.IsPathRooted(result.Value).ShouldBeTrue(); + } + + [Fact] + public void CreateFromRelative_WithWhitespaceOnlyPath_WhenWave18_4Enabled_CombinesCorrectly() + { + // Whitespace-only is not empty, so it should be processed by Path.Combine + using TestEnvironment testEnv = TestEnvironment.Create(); + ChangeWaves.ResetStateForTests(); + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); + + AbsolutePath basePath = GetTestBasePath(); + string relativePath = " "; + string expectedPath = Path.Combine(basePath.Value, relativePath); + + // Act + AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath); + + // Assert + result.Value.ShouldBe(expectedPath); + result.OriginalValue.ShouldBe(relativePath); + + ChangeWaves.ResetStateForTests(); + } + + [Fact] + public void CreateFromRelative_ChangeWaveTransition_BehaviorChangesCorrectly() + { + // This test verifies that the behavior changes when the wave is enabled/disabled + AbsolutePath basePath = GetTestBasePath(); + + // Test 1: Wave disabled - empty path returns as-is + using (TestEnvironment testEnv1 = TestEnvironment.Create()) + { + ChangeWaves.ResetStateForTests(); + testEnv1.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_4.ToString()); + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); + + AbsolutePath result = AbsolutePath.CreateFromRelative(string.Empty, basePath); + result.Value.ShouldBe(string.Empty); + + ChangeWaves.ResetStateForTests(); + } + + // Test 2: Wave enabled - empty path throws + using (TestEnvironment testEnv2 = TestEnvironment.Create()) + { + ChangeWaves.ResetStateForTests(); + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); + + Should.Throw(() => AbsolutePath.CreateFromRelative(string.Empty, basePath)); + + ChangeWaves.ResetStateForTests(); + } + } + + #endregion } } From 9bca29a4c16bc40de15877bf20104bf06c91d001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 27 Jan 2026 17:49:50 +0100 Subject: [PATCH 04/13] remove CreateFromRelative --- src/Framework.UnitTests/AbsolutePath_Tests.cs | 304 ------------------ src/Framework/PathHelpers/AbsolutePath.cs | 25 +- 2 files changed, 4 insertions(+), 325 deletions(-) diff --git a/src/Framework.UnitTests/AbsolutePath_Tests.cs b/src/Framework.UnitTests/AbsolutePath_Tests.cs index bca6393b1c5..5b864549960 100644 --- a/src/Framework.UnitTests/AbsolutePath_Tests.cs +++ b/src/Framework.UnitTests/AbsolutePath_Tests.cs @@ -197,309 +197,5 @@ public void AbsolutePath_UnixPathValidation_ShouldAcceptOnlyTrueAbsolutePaths(st { ValidatePathAcceptance(path, shouldBeAccepted); } - - #region CreateFromRelative Tests - - [Theory] - [InlineData("subfolder")] - [InlineData("deep/nested/path")] - [InlineData(".")] - [InlineData("..")] - [InlineData("./relative")] - [InlineData("../parent")] - [InlineData("file.txt")] - public void CreateFromRelative_WithValidRelativePath_ReturnsAbsolutePath(string relativePath) - { - // Arrange - AbsolutePath basePath = GetTestBasePath(); - string expectedPath = Path.Combine(basePath.Value, relativePath); - - // Act - AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath); - - // Assert - result.Value.ShouldBe(expectedPath); - result.OriginalValue.ShouldBe(relativePath); - Path.IsPathRooted(result.Value).ShouldBeTrue(); - } - - [Fact] - public void CreateFromRelative_WithEmptyPath_WhenWave18_4Enabled_ThrowsArgumentException() - { - // Arrange - Wave18_4 is enabled by default - using TestEnvironment testEnv = TestEnvironment.Create(); - ChangeWaves.ResetStateForTests(); - BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); - - AbsolutePath basePath = GetTestBasePath(); - - // Act & Assert - var exception = Should.Throw(() => AbsolutePath.CreateFromRelative(string.Empty, basePath)); - exception.ParamName.ShouldBe("path"); - exception.Message.ShouldContain("must not be empty"); - - ChangeWaves.ResetStateForTests(); - } - - [Fact] - public void CreateFromRelative_WithNullPath_WhenWave18_4Enabled_ThrowsArgumentNullException() - { - // Arrange - Wave18_4 is enabled by default - using TestEnvironment testEnv = TestEnvironment.Create(); - ChangeWaves.ResetStateForTests(); - BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); - - AbsolutePath basePath = GetTestBasePath(); - - // Act & Assert - Path.Combine throws ArgumentNullException for null path - Should.Throw(() => AbsolutePath.CreateFromRelative(null!, basePath)); - - ChangeWaves.ResetStateForTests(); - } - - [Fact] - public void CreateFromRelative_WithEmptyPath_WhenWave18_4Disabled_ReturnsEmptyPath() - { - // Arrange - using TestEnvironment testEnv = TestEnvironment.Create(); - ChangeWaves.ResetStateForTests(); - testEnv.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_4.ToString()); - BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); - - AbsolutePath basePath = GetTestBasePath(); - - // Act - Legacy behavior returns empty path as-is - AbsolutePath result = AbsolutePath.CreateFromRelative(string.Empty, basePath); - - // Assert - result.Value.ShouldBe(string.Empty); - result.OriginalValue.ShouldBe(string.Empty); - - ChangeWaves.ResetStateForTests(); - } - - [Fact] - public void CreateFromRelative_WithNullPath_WhenWave18_4Disabled_ReturnsNullPath() - { - // Arrange - using TestEnvironment testEnv = TestEnvironment.Create(); - ChangeWaves.ResetStateForTests(); - testEnv.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_4.ToString()); - BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); - - AbsolutePath basePath = GetTestBasePath(); - - // Act - Legacy behavior returns null path as-is - AbsolutePath result = AbsolutePath.CreateFromRelative(null!, basePath); - - // Assert - result.Value.ShouldBeNull(); - result.OriginalValue.ShouldBeNull(); - - ChangeWaves.ResetStateForTests(); - } - - [WindowsOnlyTheory] - [InlineData("C:\\absolute\\path")] - [InlineData("D:\\another\\absolute")] - public void CreateFromRelative_WithWindowsAbsolutePath_ReturnsAbsolutePath(string absolutePath) - { - // Arrange - AbsolutePath basePath = GetTestBasePath(); - // Path.Combine with an absolute second path returns the second path - string expectedPath = absolutePath; - - // Act - AbsolutePath result = AbsolutePath.CreateFromRelative(absolutePath, basePath); - - // Assert - result.Value.ShouldBe(expectedPath); - result.OriginalValue.ShouldBe(absolutePath); - } - - [UnixOnlyTheory] - [InlineData("/absolute/path")] - [InlineData("/another/absolute")] - public void CreateFromRelative_WithUnixAbsolutePath_ReturnsAbsolutePath(string absolutePath) - { - // Arrange - AbsolutePath basePath = GetTestBasePath(); - // Path.Combine with an absolute second path returns the second path - string expectedPath = absolutePath; - - // Act - AbsolutePath result = AbsolutePath.CreateFromRelative(absolutePath, basePath); - - // Assert - result.Value.ShouldBe(expectedPath); - result.OriginalValue.ShouldBe(absolutePath); - } - - [Fact] - public void CreateFromRelative_WithNestedRelativePath_CombinesCorrectly() - { - // Arrange - AbsolutePath basePath = GetTestBasePath(); - string relativePath = Path.Combine("level1", "level2", "level3", "file.txt"); - string expectedPath = Path.Combine(basePath.Value, relativePath); - - // Act - AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath); - - // Assert - result.Value.ShouldBe(expectedPath); - result.OriginalValue.ShouldBe(relativePath); - } - - [Fact] - public void CreateFromRelative_WithCurrentDirectory_CombinesCorrectly() - { - // Arrange - AbsolutePath basePath = GetTestBasePath(); - string relativePath = "."; - string expectedPath = Path.Combine(basePath.Value, relativePath); - - // Act - AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath); - - // Assert - result.Value.ShouldBe(expectedPath); - result.OriginalValue.ShouldBe(relativePath); - } - - [Fact] - public void CreateFromRelative_WithParentDirectory_CombinesCorrectly() - { - // Arrange - AbsolutePath basePath = GetTestBasePath(); - string relativePath = ".."; - string expectedPath = Path.Combine(basePath.Value, relativePath); - - // Act - AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath); - - // Assert - result.Value.ShouldBe(expectedPath); - result.OriginalValue.ShouldBe(relativePath); - } - - [Theory] - [InlineData("../sibling")] - [InlineData("../../grandparent")] - [InlineData("../sibling/child")] - public void CreateFromRelative_WithParentTraversal_CombinesCorrectly(string relativePath) - { - // Arrange - AbsolutePath basePath = GetTestBasePath(); - string expectedPath = Path.Combine(basePath.Value, relativePath); - - // Act - AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath); - - // Assert - result.Value.ShouldBe(expectedPath); - result.OriginalValue.ShouldBe(relativePath); - } - - [Fact] - public void CreateFromRelative_WithPathContainingSpaces_CombinesCorrectly() - { - // Arrange - AbsolutePath basePath = GetTestBasePath(); - string relativePath = "folder with spaces/file name.txt"; - string expectedPath = Path.Combine(basePath.Value, relativePath); - - // Act - AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath); - - // Assert - result.Value.ShouldBe(expectedPath); - result.OriginalValue.ShouldBe(relativePath); - } - - [Fact] - public void CreateFromRelative_PreservesOriginalValue() - { - // Arrange - AbsolutePath basePath = GetTestBasePath(); - string relativePath = "some/relative/path"; - - // Act - AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath); - - // Assert - OriginalValue should be the relative path, not the combined path - result.OriginalValue.ShouldBe(relativePath); - result.Value.ShouldNotBe(relativePath); - result.Value.ShouldContain(relativePath); - } - - [Fact] - public void CreateFromRelative_ResultIsRooted() - { - // Arrange - AbsolutePath basePath = GetTestBasePath(); - string relativePath = "any/relative/path"; - - // Act - AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath); - - // Assert - Path.IsPathRooted(result.Value).ShouldBeTrue(); - } - - [Fact] - public void CreateFromRelative_WithWhitespaceOnlyPath_WhenWave18_4Enabled_CombinesCorrectly() - { - // Whitespace-only is not empty, so it should be processed by Path.Combine - using TestEnvironment testEnv = TestEnvironment.Create(); - ChangeWaves.ResetStateForTests(); - BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); - - AbsolutePath basePath = GetTestBasePath(); - string relativePath = " "; - string expectedPath = Path.Combine(basePath.Value, relativePath); - - // Act - AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath); - - // Assert - result.Value.ShouldBe(expectedPath); - result.OriginalValue.ShouldBe(relativePath); - - ChangeWaves.ResetStateForTests(); - } - - [Fact] - public void CreateFromRelative_ChangeWaveTransition_BehaviorChangesCorrectly() - { - // This test verifies that the behavior changes when the wave is enabled/disabled - AbsolutePath basePath = GetTestBasePath(); - - // Test 1: Wave disabled - empty path returns as-is - using (TestEnvironment testEnv1 = TestEnvironment.Create()) - { - ChangeWaves.ResetStateForTests(); - testEnv1.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_4.ToString()); - BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); - - AbsolutePath result = AbsolutePath.CreateFromRelative(string.Empty, basePath); - result.Value.ShouldBe(string.Empty); - - ChangeWaves.ResetStateForTests(); - } - - // Test 2: Wave enabled - empty path throws - using (TestEnvironment testEnv2 = TestEnvironment.Create()) - { - ChangeWaves.ResetStateForTests(); - BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); - - Should.Throw(() => AbsolutePath.CreateFromRelative(string.Empty, basePath)); - - ChangeWaves.ResetStateForTests(); - } - } - - #endregion } } diff --git a/src/Framework/PathHelpers/AbsolutePath.cs b/src/Framework/PathHelpers/AbsolutePath.cs index ebd8eb5d66b..90a8910006d 100644 --- a/src/Framework/PathHelpers/AbsolutePath.cs +++ b/src/Framework/PathHelpers/AbsolutePath.cs @@ -97,36 +97,19 @@ private static void ValidatePath(string path) } #endif } + /// - /// Creates an from a relative path combined with a base path. + /// Initializes a new instance of the struct by combining an absolute path with a relative path. /// /// The path to combine with the base path. /// The base path to combine with. - /// The combined absolute path. - /// Thrown if is empty and ChangeWave 18.4 is enabled. - internal static AbsolutePath CreateFromRelative(string path, AbsolutePath basePath) + public AbsolutePath(string path, AbsolutePath basePath) { - // Opt-out for null/empty path when Wave18_4 is disabled - return as-is for legacy behavior. - if (!ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_4) && string.IsNullOrEmpty(path)) - { - return new AbsolutePath(path!, path!, ignoreRootedCheck: true); - } - - if (path == "") + if (path.Length == 0) { throw new ArgumentException("Path must not be empty.", nameof(path)); } - return new AbsolutePath(path, basePath); - } - - /// - /// Initializes a new instance of the struct by combining an absolute path with a relative path. - /// - /// The path to combine with the base path. - /// The base path to combine with. - public AbsolutePath(string path, AbsolutePath basePath) - { // This function should not throw when path has illegal characters. // For .NET Framework, Microsoft.IO.Path.Combine should be used instead of System.IO.Path.Combine to achieve it. // For .NET Core, System.IO.Path.Combine already does not throw in this case. From 7dfe40b83a9735792cc5e11d0005ac73e53c1aaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 27 Jan 2026 17:51:41 +0100 Subject: [PATCH 05/13] docs --- src/Framework/PathHelpers/AbsolutePath.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Framework/PathHelpers/AbsolutePath.cs b/src/Framework/PathHelpers/AbsolutePath.cs index 90a8910006d..4f5bd529d4d 100644 --- a/src/Framework/PathHelpers/AbsolutePath.cs +++ b/src/Framework/PathHelpers/AbsolutePath.cs @@ -103,11 +103,12 @@ private static void ValidatePath(string path) /// /// The path to combine with the base path. /// The base path to combine with. + /// Thrown if is null or empty. public AbsolutePath(string path, AbsolutePath basePath) { - if (path.Length == 0) + if (string.IsNullOrEmpty(path)) { - throw new ArgumentException("Path must not be empty.", nameof(path)); + throw new ArgumentException("Path must not be null or empty.", nameof(path)); } // This function should not throw when path has illegal characters. From 93fce8ee47b203c3b6ce961448a723cf477fcbbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 27 Jan 2026 17:52:39 +0100 Subject: [PATCH 06/13] docs --- src/Framework/PathHelpers/AbsolutePath.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Framework/PathHelpers/AbsolutePath.cs b/src/Framework/PathHelpers/AbsolutePath.cs index 4f5bd529d4d..3db31daff64 100644 --- a/src/Framework/PathHelpers/AbsolutePath.cs +++ b/src/Framework/PathHelpers/AbsolutePath.cs @@ -42,6 +42,7 @@ namespace Microsoft.Build.Framework /// Initializes a new instance of the struct. /// /// The absolute path string. + /// Thrown if is null, empty, or not a rooted path. public AbsolutePath(string path) { ValidatePath(path); From 9234a98f466833a63a13050d6ec4c7e4e847273e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 27 Jan 2026 17:56:23 +0100 Subject: [PATCH 07/13] get rid of usage --- .../MultiProcessTaskEnvironmentDriver.cs | 2 +- .../MultiThreadedTaskEnvironmentDriver.cs | 2 +- src/Framework.UnitTests/AbsolutePath_Tests.cs | 18 +++++++++++++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs b/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs index 6544b57accf..490ed0ca9bc 100644 --- a/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs +++ b/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs @@ -40,7 +40,7 @@ public AbsolutePath ProjectDirectory /// public AbsolutePath GetAbsolutePath(string path) { - return AbsolutePath.CreateFromRelative(path, ProjectDirectory); + return new AbsolutePath(path, ProjectDirectory); } /// diff --git a/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs b/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs index 7f18d99d7fb..cf54c430d0d 100644 --- a/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs +++ b/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs @@ -71,7 +71,7 @@ public AbsolutePath ProjectDirectory /// public AbsolutePath GetAbsolutePath(string path) { - return AbsolutePath.CreateFromRelative(path, ProjectDirectory); + return new AbsolutePath(path, ProjectDirectory); } /// diff --git a/src/Framework.UnitTests/AbsolutePath_Tests.cs b/src/Framework.UnitTests/AbsolutePath_Tests.cs index 5b864549960..b890d9938bc 100644 --- a/src/Framework.UnitTests/AbsolutePath_Tests.cs +++ b/src/Framework.UnitTests/AbsolutePath_Tests.cs @@ -44,11 +44,27 @@ public void AbsolutePath_FromAbsolutePath_ShouldPreservePath() Path.IsPathRooted(absolutePath.Value).ShouldBeTrue(); } + [Theory] + [InlineData(null)] + [InlineData("")] + public void AbsolutePath_NullOrEmpty_ShouldThrow(string? path) + { + Should.Throw(() => new AbsolutePath(path!)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void AbsolutePath_NullOrEmptyWithBasePath_ShouldThrow(string? path) + { + var basePath = GetTestBasePath(); + Should.Throw(() => new AbsolutePath(path!, basePath)); + } + [Theory] [InlineData("subfolder")] [InlineData("deep/nested/path")] [InlineData(".")] - [InlineData("")] [InlineData("..")] public void AbsolutePath_FromRelativePath_ShouldResolveAgainstBase(string relativePath) { From d43aa032e4ea020e118fa2ab450fded86b21f6f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 27 Jan 2026 18:07:10 +0100 Subject: [PATCH 08/13] fix copy to continue on path parsing error --- src/Tasks/Copy.cs | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/Tasks/Copy.cs b/src/Tasks/Copy.cs index a3adcf74260..bbf66e93406 100644 --- a/src/Tasks/Copy.cs +++ b/src/Tasks/Copy.cs @@ -311,7 +311,7 @@ private void LogAlwaysRetryDiagnosticFromResources(string messageResourceName, p if (FailIfNotIncremental) { - // Before the introduction of AbsolutePath, this logged full paths, so preserve that behavior + // Before the introduction of AbsolutePath, this logged full paths, so preserve that behavior Log.LogError(FileComment, sourceFileState.Path, destinationFileState.Path); return false; } @@ -508,8 +508,17 @@ private bool CopySingleThreaded( string destSpec = DestinationFiles[i].ItemSpec; // Compute absolute paths once - reused for ETW, deduplication dictionary, and FileState - AbsolutePath sourceAbsolutePath = TaskEnvironment.GetAbsolutePath(sourceSpec); - AbsolutePath destAbsolutePath = TaskEnvironment.GetAbsolutePath(destSpec); + try + { + AbsolutePath sourceAbsolutePath = TaskEnvironment.GetAbsolutePath(sourceSpec); + AbsolutePath destAbsolutePath = TaskEnvironment.GetAbsolutePath(destSpec); + } + catch (ArgumentException ex) + { + Log.LogErrorWithCodeFromResources("Copy.Error", sourceSpec, destSpec, ex.Message); + success = false; + continue; + } MSBuildEventSource.Log.CopyUpToDateStart(destAbsolutePath); if (filesActuallyCopied.TryGetValue(destAbsolutePath, out string originalSource)) @@ -663,9 +672,17 @@ void ProcessPartition() string destSpec = destItem.ItemSpec; // Compute absolute paths once - reused for ETW, deduplication check, and FileState - AbsolutePath sourceAbsolutePath = TaskEnvironment.GetAbsolutePath(sourceSpec); - AbsolutePath destAbsolutePath = TaskEnvironment.GetAbsolutePath(destSpec); - + try + { + AbsolutePath sourceAbsolutePath = TaskEnvironment.GetAbsolutePath(sourceSpec); + AbsolutePath destAbsolutePath = TaskEnvironment.GetAbsolutePath(destSpec); + } + catch (ArgumentException ex) + { + Log.LogErrorWithCodeFromResources("Copy.Error", sourceSpec, destSpec, ex.Message); + continue; + } + // Check if we just copied from this location to the destination, don't copy again. MSBuildEventSource.Log.CopyUpToDateStart(destAbsolutePath); bool copyComplete = false; From e83ec5226cbf29c2f488b8a769a368411dcb35a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 27 Jan 2026 18:13:11 +0100 Subject: [PATCH 09/13] fix touch --- src/Tasks/Touch.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Tasks/Touch.cs b/src/Tasks/Touch.cs index 8f442777033..9c826745873 100644 --- a/src/Tasks/Touch.cs +++ b/src/Tasks/Touch.cs @@ -95,8 +95,16 @@ internal bool ExecuteImpl( foreach (ITaskItem file in Files) { - AbsolutePath path = TaskEnvironment.GetAbsolutePath(FrameworkFileUtilities.FixFilePath(file.ItemSpec)); - + try + { + AbsolutePath path = TaskEnvironment.GetAbsolutePath(FrameworkFileUtilities.FixFilePath(file.ItemSpec)); + } + catch (ArgumentException ex) + { + Log.LogErrorWithCodeFromResources("Touch.FileDoesNotExist", file.ItemSpec, ex.Message); + retVal = false; + continue; + } // For speed, eliminate duplicates caused by poor targets authoring if (touchedFilesSet.Contains(path)) { From 6e085b68dd66f35a48736db66667d502ccceac35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 27 Jan 2026 18:23:03 +0100 Subject: [PATCH 10/13] success false --- src/Tasks/Copy.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Tasks/Copy.cs b/src/Tasks/Copy.cs index bbf66e93406..7dd5592a00e 100644 --- a/src/Tasks/Copy.cs +++ b/src/Tasks/Copy.cs @@ -680,6 +680,7 @@ void ProcessPartition() catch (ArgumentException ex) { Log.LogErrorWithCodeFromResources("Copy.Error", sourceSpec, destSpec, ex.Message); + success = false; continue; } From ff9e97bda1af830c3368cea0b866c2dd41c913d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 27 Jan 2026 18:25:18 +0100 Subject: [PATCH 11/13] remove obsolete test --- .../BackEnd/TaskEnvironment_Tests.cs | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs b/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs index 35aa3770fff..1d806552c25 100644 --- a/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs @@ -432,41 +432,5 @@ public void TaskEnvironment_GetAbsolutePath_WithEmptyPath_WhenWave18_4Enabled_Th ChangeWaves.ResetStateForTests(); } } - - [Theory] - [MemberData(nameof(EnvironmentTypes))] - public void TaskEnvironment_GetAbsolutePath_WithNullPath_WhenWave18_4Disabled_ReturnsNullPath(string environmentType) - { - using TestEnvironment testEnv = TestEnvironment.Create(); - ChangeWaves.ResetStateForTests(); - testEnv.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_4.ToString()); - BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); - - var taskEnvironment = CreateTaskEnvironment(environmentType); - - // When Wave18_4 is disabled, null path returns as-is - var absolutePath = taskEnvironment.GetAbsolutePath(null!); - - absolutePath.Value.ShouldBeNull(); - absolutePath.OriginalValue.ShouldBeNull(); - - ChangeWaves.ResetStateForTests(); - } - - [Theory] - [MemberData(nameof(EnvironmentTypes))] - public void TaskEnvironment_GetAbsolutePath_WithNullPath_WhenWave18_4Enabled_Throws(string environmentType) - { - using TestEnvironment testEnv = TestEnvironment.Create(); - ChangeWaves.ResetStateForTests(); - BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); - - var taskEnvironment = CreateTaskEnvironment(environmentType); - - // When Wave18_4 is enabled, null path should throw - Should.Throw(() => taskEnvironment.GetAbsolutePath(null!)); - - ChangeWaves.ResetStateForTests(); - } } } From 72d1eca5c2bad216a62b50a6dd3d342ddb4d332b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 27 Jan 2026 18:28:11 +0100 Subject: [PATCH 12/13] rm invalid tests --- .../BackEnd/TaskEnvironment_Tests.cs | 48 ------------------- 1 file changed, 48 deletions(-) diff --git a/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs b/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs index 1d806552c25..4a9d6126ced 100644 --- a/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs @@ -384,53 +384,5 @@ public void TaskEnvironment_GetAbsolutePath_WithInvalidPathChars_ShouldNotThrow( DisposeTaskEnvironment(taskEnvironment); } } - - [Theory] - [MemberData(nameof(EnvironmentTypes))] - public void TaskEnvironment_GetAbsolutePath_WithEmptyPath_WhenWave18_4Disabled_ReturnsEmptyPath(string environmentType) - { - using TestEnvironment testEnv = TestEnvironment.Create(); - ChangeWaves.ResetStateForTests(); - testEnv.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_4.ToString()); - BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); - - var taskEnvironment = CreateTaskEnvironment(environmentType); - - try - { - // When Wave18_4 is disabled, empty path returns as-is (legacy behavior) - var absolutePath = taskEnvironment.GetAbsolutePath(string.Empty); - - absolutePath.Value.ShouldBe(string.Empty); - absolutePath.OriginalValue.ShouldBe(string.Empty); - } - finally - { - DisposeTaskEnvironment(taskEnvironment); - ChangeWaves.ResetStateForTests(); - } - } - - [Theory] - [MemberData(nameof(EnvironmentTypes))] - public void TaskEnvironment_GetAbsolutePath_WithEmptyPath_WhenWave18_4Enabled_Throws(string environmentType) - { - using TestEnvironment testEnv = TestEnvironment.Create(); - ChangeWaves.ResetStateForTests(); - BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); - - var taskEnvironment = CreateTaskEnvironment(environmentType); - - try - { - // When Wave18_4 is enabled, empty path should throw - Should.Throw(() => taskEnvironment.GetAbsolutePath(string.Empty)); - } - finally - { - DisposeTaskEnvironment(taskEnvironment); - ChangeWaves.ResetStateForTests(); - } - } } } From c0a25d1900533f741843601cd3bb413cf7365327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Wed, 28 Jan 2026 10:57:51 +0100 Subject: [PATCH 13/13] variable scope --- src/Tasks/Copy.cs | 12 ++++++++---- src/Tasks/Touch.cs | 6 ++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Tasks/Copy.cs b/src/Tasks/Copy.cs index 7dd5592a00e..c3899515a90 100644 --- a/src/Tasks/Copy.cs +++ b/src/Tasks/Copy.cs @@ -508,10 +508,12 @@ private bool CopySingleThreaded( string destSpec = DestinationFiles[i].ItemSpec; // Compute absolute paths once - reused for ETW, deduplication dictionary, and FileState + AbsolutePath sourceAbsolutePath; + AbsolutePath destAbsolutePath; try { - AbsolutePath sourceAbsolutePath = TaskEnvironment.GetAbsolutePath(sourceSpec); - AbsolutePath destAbsolutePath = TaskEnvironment.GetAbsolutePath(destSpec); + sourceAbsolutePath = TaskEnvironment.GetAbsolutePath(sourceSpec); + destAbsolutePath = TaskEnvironment.GetAbsolutePath(destSpec); } catch (ArgumentException ex) { @@ -672,10 +674,12 @@ void ProcessPartition() string destSpec = destItem.ItemSpec; // Compute absolute paths once - reused for ETW, deduplication check, and FileState + AbsolutePath sourceAbsolutePath; + AbsolutePath destAbsolutePath; try { - AbsolutePath sourceAbsolutePath = TaskEnvironment.GetAbsolutePath(sourceSpec); - AbsolutePath destAbsolutePath = TaskEnvironment.GetAbsolutePath(destSpec); + sourceAbsolutePath = TaskEnvironment.GetAbsolutePath(sourceSpec); + destAbsolutePath = TaskEnvironment.GetAbsolutePath(destSpec); } catch (ArgumentException ex) { diff --git a/src/Tasks/Touch.cs b/src/Tasks/Touch.cs index 9c826745873..7e1e68f175c 100644 --- a/src/Tasks/Touch.cs +++ b/src/Tasks/Touch.cs @@ -95,16 +95,18 @@ internal bool ExecuteImpl( foreach (ITaskItem file in Files) { + AbsolutePath path; try { - AbsolutePath path = TaskEnvironment.GetAbsolutePath(FrameworkFileUtilities.FixFilePath(file.ItemSpec)); + path = TaskEnvironment.GetAbsolutePath(FrameworkFileUtilities.FixFilePath(file.ItemSpec)); } catch (ArgumentException ex) { Log.LogErrorWithCodeFromResources("Touch.FileDoesNotExist", file.ItemSpec, ex.Message); retVal = false; continue; - } + } + // For speed, eliminate duplicates caused by poor targets authoring if (touchedFilesSet.Contains(path)) {