From 63f03929b1ad123ddf650a687fa1b80bd152a2f9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 21:07:10 +0000 Subject: [PATCH 1/2] Initial plan From 36e0a8ad7f948f180c361047921cb6093aa91344 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 21:41:37 +0000 Subject: [PATCH 2/2] Fix hint-name collision in source generator when two .refitter files share a filename - Replace filename-only hint name with full path-based disambiguation - Honor settings.OutputFilename when present - Remove dead `filename` variable - Add regression tests for hint-name uniqueness Agent-Logs-Url: https://github.com/christianhelle/refitter/sessions/cc94e817-e939-47bb-8be7-8bcc4353697f Co-authored-by: christianhelle <710400+christianhelle@users.noreply.github.com> --- .../RefitterSourceGenerator.cs | 36 +++++++++++---- .../SourceGeneratorFileIOTests.cs | 46 +++++++++++++++++++ 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/src/Refitter.SourceGenerator/RefitterSourceGenerator.cs b/src/Refitter.SourceGenerator/RefitterSourceGenerator.cs index e8b50e588..1c9fbc623 100644 --- a/src/Refitter.SourceGenerator/RefitterSourceGenerator.cs +++ b/src/Refitter.SourceGenerator/RefitterSourceGenerator.cs @@ -145,19 +145,37 @@ private static GeneratedCode GenerateCode( var refit = generator.Generate(); cancellationToken.ThrowIfCancellationRequested(); - var filename = settings.OutputFilename ?? Path.GetFileName(file.Path).Replace(".refitter", ".g.cs"); - if (filename == ".g.cs") + + string hintName; + if (!string.IsNullOrEmpty(settings.OutputFilename)) { - filename = "Refitter.g.cs"; + // Honor the user-specified output filename as the hint name + hintName = settings.OutputFilename!; + if (!hintName.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)) + { + hintName += ".g.cs"; + } } - - // Create unique hint name based on the .refitter file path to avoid collisions - var hintName = Path.GetFileNameWithoutExtension(file.Path); - if (string.IsNullOrEmpty(hintName) || hintName == ".") + else { - hintName = "Refitter"; + // Disambiguate using the full directory path to avoid collisions when multiple + // .refitter files share the same filename in different directories (e.g. src/ApiA/petstore.refitter + // and src/ApiB/petstore.refitter would both produce "petstore.g.cs" without this). + var fileNameWithoutExt = Path.GetFileNameWithoutExtension(file.Path); + if (string.IsNullOrEmpty(fileNameWithoutExt) || fileNameWithoutExt == ".") + { + fileNameWithoutExt = "Refitter"; + } + + var dir = Path.GetDirectoryName(file.Path) ?? string.Empty; + var safeDir = dir + .Replace(Path.DirectorySeparatorChar, '_') + .Replace(Path.AltDirectorySeparatorChar, '_') + .Replace(':', '_'); + hintName = string.IsNullOrEmpty(safeDir) + ? $"{fileNameWithoutExt}.g.cs" + : $"{safeDir}_{fileNameWithoutExt}.g.cs"; } - hintName = hintName + ".g.cs"; return new GeneratedCode(diagnostics, refit, hintName); } diff --git a/src/Refitter.Tests/SourceGeneratorFileIOTests.cs b/src/Refitter.Tests/SourceGeneratorFileIOTests.cs index de13819df..86a23fa16 100644 --- a/src/Refitter.Tests/SourceGeneratorFileIOTests.cs +++ b/src/Refitter.Tests/SourceGeneratorFileIOTests.cs @@ -156,4 +156,50 @@ public async Task Test_SourceGenerator_OutputFilename_NotCreatedOnDisk() generatedCode.Should().NotBeNullOrWhiteSpace(); } + + [Test] + public void Test_HintName_IsUnique_For_SameFilename_InDifferentDirectories() + { + // Regression test for: hint-name collisions when two .refitter files share a filename + // Two .refitter files with the same filename in different directories must produce different hint names. + static string ComputeHintName(string filePath) + { + var fileNameWithoutExt = Path.GetFileNameWithoutExtension(filePath); + if (string.IsNullOrEmpty(fileNameWithoutExt) || fileNameWithoutExt == ".") + fileNameWithoutExt = "Refitter"; + + var dir = Path.GetDirectoryName(filePath) ?? string.Empty; + var safeDir = dir + .Replace(Path.DirectorySeparatorChar, '_') + .Replace(Path.AltDirectorySeparatorChar, '_') + .Replace(':', '_'); + + return string.IsNullOrEmpty(safeDir) + ? $"{fileNameWithoutExt}.g.cs" + : $"{safeDir}_{fileNameWithoutExt}.g.cs"; + } + + var pathA = Path.Combine("src", "ApiA", "petstore.refitter"); + var pathB = Path.Combine("src", "ApiB", "petstore.refitter"); + + var hintA = ComputeHintName(pathA); + var hintB = ComputeHintName(pathB); + + hintA.Should().NotBe(hintB, + "two .refitter files with the same name in different directories must produce different hint names"); + } + + [Test] + public void Test_HintName_UsesOutputFilename_WhenSpecified() + { + // When settings.OutputFilename is set, it should be used as the hint name directly. + var outputFilename = "MyCustomOutput.g.cs"; + + var hintName = outputFilename.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) + ? outputFilename + : outputFilename + ".g.cs"; + + hintName.Should().Be("MyCustomOutput.g.cs", + "OutputFilename should be used as-is when it already ends with .cs"); + } }