From b35d417361556c03a943a694ce904ac554a9b747 Mon Sep 17 00:00:00 2001 From: Chris Sienkiewicz Date: Mon, 8 Sep 2025 12:36:50 -0700 Subject: [PATCH 1/3] Add failing tests --- .../RazorSourceGeneratorTests.cs | 73 +++++++++++++++++++ .../SourceGeneratorProjectItemTest.cs | 30 ++++++++ 2 files changed, 103 insertions(+) diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTests.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTests.cs index 36a96268dab..616c250c661 100644 --- a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTests.cs +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTests.cs @@ -3508,5 +3508,78 @@ protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components. #pragma warning restore 1591 "); } + + [Fact] + public async Task IncrementalCompilation_RerunsGenerator_When_AdditionalFileRenamed() + { + // Arrange + using var eventListener = new RazorEventListener(); + var project = CreateTestProject(new() + { + ["Pages/Index.razor"] = "

Hello world

", + ["Pages/Counter.razor"] = "

Counter

", + }); + var compilation = await project.GetCompilationAsync(); + var (driver, additionalTexts, analyzerConfigOptionProvider) = await GetDriverWithAdditionalTextAndProviderAsync(project); + + var result = RunGenerator(compilation!, ref driver); + Assert.Empty(result.Diagnostics); + Assert.Equal(2, result.GeneratedSources.Length); + + eventListener.Clear(); + + // Verify no changes when re-running + result = RunGenerator(compilation!, ref driver) + .VerifyOutputsMatch(result); + + Assert.Empty(result.Diagnostics); + Assert.Equal(2, result.GeneratedSources.Length); + Assert.Empty(eventListener.Events); + + // Rename Counter.razor to NewCounter.razor by removing and re-adding with same content + var counterText = additionalTexts.First(f => f.Path.EndsWith("Counter.razor", StringComparison.OrdinalIgnoreCase)); + var renamedText = new TestAdditionalText("Pages/NewCounter.razor", counterText.GetText()!); + driver = driver.RemoveAdditionalTexts([counterText]) + .AddAdditionalTexts([renamedText]); + + // Update the analyzer config options with the new target path + analyzerConfigOptionProvider.AdditionalTextOptions[renamedText.Path] = new TestAnalyzerConfigOptions + { + ["build_metadata.AdditionalFiles.TargetPath"] = Convert.ToBase64String(Encoding.UTF8.GetBytes(renamedText.Path)) + }; + driver = driver.WithUpdatedAnalyzerConfigOptions(analyzerConfigOptionProvider); + + result = RunGenerator(compilation!, ref driver); + + // Should have generated source for Index.razor and NewCounter.razor + Assert.Empty(result.Diagnostics); + Assert.Equal(2, result.GeneratedSources.Length); + + // Verify the new file was processed + Assert.Collection(eventListener.Events, + e => e.AssertSingleItem("ParseRazorDocumentStart", "Pages/NewCounter.razor"), + e => e.AssertSingleItem("ParseRazorDocumentStop", "Pages/NewCounter.razor"), + e => e.AssertSingleItem("GenerateDeclarationCodeStart", "/Pages/NewCounter.razor"), + e => e.AssertSingleItem("GenerateDeclarationCodeStop", "/Pages/NewCounter.razor"), + e => Assert.Equal("DiscoverTagHelpersFromCompilationStart", e.EventName), + e => Assert.Equal("DiscoverTagHelpersFromCompilationStop", e.EventName), + e => e.AssertSingleItem("RewriteTagHelpersStart", "Pages/NewCounter.razor"), + e => e.AssertSingleItem("RewriteTagHelpersStop", "Pages/NewCounter.razor"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStart", "Pages/Index.razor"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStop", "Pages/Index.razor"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStart", "Pages/NewCounter.razor"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStop", "Pages/NewCounter.razor"), + e => e.AssertPair("RazorCodeGenerateStart", "Pages/Index.razor", "Runtime"), + e => e.AssertPair("RazorCodeGenerateStop", "Pages/Index.razor", "Runtime"), + e => e.AssertPair("RazorCodeGenerateStart", "Pages/NewCounter.razor", "Runtime"), + e => e.AssertPair("RazorCodeGenerateStop", "Pages/NewCounter.razor", "Runtime"), + e => e.AssertSingleItem("AddSyntaxTrees", "Pages_NewCounter_razor.g.cs") + ); + + // Verify the generated source has the correct namespace and class name + var newCounterSource = result.GeneratedSources.FirstOrDefault(s => s.HintName.Contains("NewCounter")); + Assert.Contains("namespace MyApp.Pages", newCounterSource.SourceText.ToString()); + Assert.Contains("public partial class NewCounter", newCounterSource.SourceText.ToString()); + } } } diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/SourceGeneratorProjectItemTest.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/SourceGeneratorProjectItemTest.cs index ebbefc8590f..1890d3a8b1c 100644 --- a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/SourceGeneratorProjectItemTest.cs +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/SourceGeneratorProjectItemTest.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text; using Microsoft.AspNetCore.Razor.Language; using Xunit; @@ -115,5 +116,34 @@ public void PathWithoutExtension_ExcludesExtension(string path, string expected) // Assert Assert.Equal(expected, fileName); } + + [Fact] + public void ProjectItems_WithDifferentPaths_SameContent_AreNotEqual() + { + // Two additional texts with same contents, but different paths + var content = "

Hello World

"; + var additionalText1 = new TestAdditionalText(content, Encoding.UTF8, "File1.cshtml"); + var additionalText2 = new TestAdditionalText(content, Encoding.UTF8, "File2.cshtml"); + + var projectItem1 = new SourceGeneratorProjectItem( + filePath: "/Views/Home/Index.cshtml", + basePath: "/", + relativePhysicalPath: "/Views/Home", + fileKind: RazorFileKind.Legacy, + additionalText: additionalText1, + cssScope: null); + + var projectItem2 = new SourceGeneratorProjectItem( + filePath: "/Views/About/Index.cshtml", + basePath: "/", + relativePhysicalPath: "/Views/About", + fileKind: RazorFileKind.Legacy, + additionalText: additionalText2, + cssScope: null); + + // Act & Assert + Assert.NotEqual(projectItem1, projectItem2); + Assert.False(projectItem1.Equals(projectItem2)); + } } } From 4f8dbad6b6209b5335acb72c53384633307b03d0 Mon Sep 17 00:00:00 2001 From: Chris Sienkiewicz Date: Mon, 8 Sep 2025 12:37:21 -0700 Subject: [PATCH 2/3] Fix comparison to include additional text path --- .../src/SourceGenerators/SourceGeneratorProjectItem.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/SourceGeneratorProjectItem.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/SourceGeneratorProjectItem.cs index c8419f93e26..826819b4586 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/SourceGeneratorProjectItem.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/SourceGeneratorProjectItem.cs @@ -61,7 +61,8 @@ public override Stream Read() public bool Equals(SourceGeneratorProjectItem? other) { if (other is null || - CssScope != other.CssScope) + CssScope != other.CssScope || + PhysicalPath != other.PhysicalPath) { return false; } From 4d3eddd0a5d4ad3b9bbe73329da2d567ce90a204 Mon Sep 17 00:00:00 2001 From: Chris Sienkiewicz Date: Mon, 8 Sep 2025 14:57:29 -0700 Subject: [PATCH 3/3] Update test to include case edit --- .../RazorSourceGeneratorTests.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTests.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTests.cs index 616c250c661..20e5cba815c 100644 --- a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTests.cs +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTests.cs @@ -3551,7 +3551,6 @@ public async Task IncrementalCompilation_RerunsGenerator_When_AdditionalFileRena result = RunGenerator(compilation!, ref driver); - // Should have generated source for Index.razor and NewCounter.razor Assert.Empty(result.Diagnostics); Assert.Equal(2, result.GeneratedSources.Length); @@ -3580,6 +3579,24 @@ public async Task IncrementalCompilation_RerunsGenerator_When_AdditionalFileRena var newCounterSource = result.GeneratedSources.FirstOrDefault(s => s.HintName.Contains("NewCounter")); Assert.Contains("namespace MyApp.Pages", newCounterSource.SourceText.ToString()); Assert.Contains("public partial class NewCounter", newCounterSource.SourceText.ToString()); + + // Do a case-only rename and make sure we update the generated class name still + // as component names are case sensitive even on Windows. + var renamedText2 = new TestAdditionalText("Pages/NewCouNter.razor", counterText.GetText()!); + driver = driver.RemoveAdditionalTexts([renamedText]) + .AddAdditionalTexts([renamedText2]); + + // Update the analyzer config options with the new target path + analyzerConfigOptionProvider.AdditionalTextOptions[renamedText2.Path] = new TestAnalyzerConfigOptions + { + ["build_metadata.AdditionalFiles.TargetPath"] = Convert.ToBase64String(Encoding.UTF8.GetBytes(renamedText2.Path)) + }; + driver = driver.WithUpdatedAnalyzerConfigOptions(analyzerConfigOptionProvider); + + result = RunGenerator(compilation!, ref driver); + + var newCouNterSource = result.GeneratedSources.FirstOrDefault(s => s.HintName.Contains("NewCouNter")); + Assert.Contains("public partial class NewCouNter", newCouNterSource.SourceText.ToString()); } } }