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));
+ }
}
}