diff --git a/src/Cli/dotnet/Commands/New/PostActions/DotnetAddPostActionProcessor.cs b/src/Cli/dotnet/Commands/New/PostActions/DotnetAddPostActionProcessor.cs index bcb0cba14855..b28c6f9c541c 100644 --- a/src/Cli/dotnet/Commands/New/PostActions/DotnetAddPostActionProcessor.cs +++ b/src/Cli/dotnet/Commands/New/PostActions/DotnetAddPostActionProcessor.cs @@ -34,11 +34,23 @@ internal static IReadOnlyList FindProjFileAtOrAbovePath(IPhysicalFileSys protected override bool ProcessInternal(IEngineEnvironmentSettings environment, IPostAction action, ICreationEffects creationEffects, ICreationResult templateCreationResult, string outputBasePath) { + bool hasConfiguredTargetFiles = + action.Args.TryGetValue("targetFiles", out string? targetFilesArg) && + !string.IsNullOrWhiteSpace(targetFilesArg); + IReadOnlyList? projectsToProcess = GetConfiguredFiles(action.Args, creationEffects, "targetFiles", outputBasePath); - if (!projectsToProcess.Any()) + if (!projectsToProcess.Any() && hasConfiguredTargetFiles) + { + // targetFiles was specified but none matched in creationEffects (e.g., pre-existing project files). + // Try to resolve them directly from disk before falling back to directory search. + projectsToProcess = FindExistingTargetFiles(environment.Host.FileSystem, action.Args, outputBasePath); + } + + if (!projectsToProcess.Any() && !hasConfiguredTargetFiles) { - //If the author didn't opt in to the new behavior by specifying "targetFiles", search for project file in current output directory or above. + // The author didn't opt in to the new behavior by specifying "targetFiles", + // search for project file in current output directory or above. HashSet extensionLimiters = new(StringComparer.Ordinal); if (action.Args.TryGetValue("projectFileExtensions", out string? projectFileExtensions)) { @@ -65,11 +77,6 @@ protected override bool ProcessInternal(IEngineEnvironmentSettings environment, } } - if (!projectsToProcess.Any()) - { - projectsToProcess = FindExistingTargetFiles(environment.Host.FileSystem, action.Args, outputBasePath); - } - if (!projectsToProcess.Any()) { // no projects found. Error. diff --git a/test/dotnet.Tests/CommandTests/New/DotnetAddPostActionTests.cs b/test/dotnet.Tests/CommandTests/New/DotnetAddPostActionTests.cs index 907da662a0f6..05d53ebca2f9 100644 --- a/test/dotnet.Tests/CommandTests/New/DotnetAddPostActionTests.cs +++ b/test/dotnet.Tests/CommandTests/New/DotnetAddPostActionTests.cs @@ -247,6 +247,59 @@ public void AddRefCanHandleExistingProjectFiles() Assert.Equal(referencedProjectFileFullPath, callback.Reference); } + [Fact(DisplayName = nameof(AddRefWithTargetFilesIgnoresParentDirectoryProjects))] + public void AddRefWithTargetFilesIgnoresParentDirectoryProjects() + { + // Regression test: when targetFiles is specified pointing to a pre-existing project, + // the processor should find it on disk and NOT walk up the directory tree to find + // a different .csproj in a parent directory. + var callback = new MockAddProjectReferenceCallback(); + DotnetAddPostActionProcessor actionProcessor = new(callback.AddPackageReference, callback.AddProjectReference); + + string targetBasePath = _engineEnvironmentSettings.GetTempVirtualizedPath(); + _engineEnvironmentSettings.Host.VirtualizeDirectory(targetBasePath); + + // Create existing target project in a subdirectory (the correct target) + const string existingProjectFolder = "ExistingProject"; + string existingProjectPath = Path.Combine(targetBasePath, existingProjectFolder); + const string existingProjectFile = "ExistingProject.csproj"; + string existingProjectFileFullPath = Path.Combine(existingProjectPath, existingProjectFile); + _engineEnvironmentSettings.Host.FileSystem.WriteAllText(existingProjectFileFullPath, TestCsprojFile); + + // Create a conflicting .csproj in the output directory itself (simulates stale file pollution) + string conflictingProjFile = Path.Combine(targetBasePath, "Conflicting.csproj"); + _engineEnvironmentSettings.Host.FileSystem.WriteAllText(conflictingProjFile, TestCsprojFile); + + string referencedProjectFileFullPath = Path.Combine(targetBasePath, "Reference.csproj"); + + var args = new Dictionary() + { + { "targetFiles", $"[\"{existingProjectFolder}/{existingProjectFile}\"]" }, + { "referenceType", "project" }, + { "reference", "Reference.csproj" } + }; + var postAction = + new MockPostAction(default, default, default, default, default!) + { + ActionId = DotnetAddPostActionProcessor.ActionProcessorId, Args = args + }; + + // Empty creation effects — the existing project was NOT created by the template + MockCreationEffects creationEffects = new MockCreationEffects(); + + actionProcessor.Process( + _engineEnvironmentSettings, + postAction, + creationEffects, + new MockCreationResult(), + targetBasePath); + + // The callback should receive the explicitly configured target file, + // NOT the conflicting .csproj from the parent/output directory + Assert.Equal(existingProjectFileFullPath, callback.Target); + Assert.Equal(referencedProjectFileFullPath, callback.Reference); + } + [Fact(DisplayName = nameof(AddRefCanTargetASingleProjectWithAJsonArray))] public void AddRefCanTargetASingleProjectWithAJsonArray() { diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs index 35a845b28917..32012e4e9840 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests_CscOnlyAndApi.cs @@ -402,11 +402,10 @@ public void CscOnly_CompilationDiagnostics() Console.WriteLine("ran" + x); """); - new DotnetCommand(Log, "run", "Program.cs", "-bl") + new DotnetCommand(Log, "run", "Program.cs") .WithWorkingDirectory(testInstance.Path) .Execute() .Should().Pass() - .And.HaveStdOutContaining(CliCommandStrings.NoBinaryLogBecauseRunningJustCsc) // warning CS8600: Converting null literal or possible null value to non-nullable type. .And.HaveStdOutContaining("warning CS8600") .And.HaveStdOutContaining("ran"); @@ -415,11 +414,10 @@ public void CscOnly_CompilationDiagnostics() Console.Write """); - new DotnetCommand(Log, "run", "Program.cs", "-bl") + new DotnetCommand(Log, "run", "Program.cs") .WithWorkingDirectory(testInstance.Path) .Execute() .Should().Fail() - .And.HaveStdOutContaining(CliCommandStrings.NoBinaryLogBecauseRunningJustCsc) // error CS1002: ; expected .And.HaveStdOutContaining("error CS1002") .And.HaveStdErrContaining(CliCommandStrings.RunCommandException);