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; } 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..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 @@ -3508,5 +3508,95 @@ 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); + + 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()); + + // 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()); + } } } 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)); + } } }