diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorTagHelperContextDiscoveryPhase.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorTagHelperContextDiscoveryPhase.cs index 370d39e130b..21fbe74bc71 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorTagHelperContextDiscoveryPhase.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorTagHelperContextDiscoveryPhase.cs @@ -17,7 +17,7 @@ internal sealed class DefaultRazorTagHelperContextDiscoveryPhase : RazorEnginePh { protected override void ExecuteCore(RazorCodeDocument codeDocument) { - var syntaxTree = codeDocument.GetSyntaxTree(); + var syntaxTree = codeDocument.GetPreTagHelperSyntaxTree() ?? codeDocument.GetSyntaxTree(); ThrowForMissingDocumentDependency(syntaxTree); var descriptors = codeDocument.GetTagHelpers(); @@ -69,7 +69,6 @@ protected override void ExecuteCore(RazorCodeDocument codeDocument) var context = TagHelperDocumentContext.Create(tagHelperPrefix, descriptors); codeDocument.SetTagHelperContext(context); - codeDocument.SetPreTagHelperSyntaxTree(syntaxTree); } private static bool MatchesDirective(TagHelperDescriptor descriptor, string typePattern, string assemblyName) diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorTagHelperRewritePhase.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorTagHelperRewritePhase.cs index 402020d1afc..cb8632d70b0 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorTagHelperRewritePhase.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorTagHelperRewritePhase.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable enable +using System.Collections.Generic; using Microsoft.AspNetCore.Razor.Language.Legacy; namespace Microsoft.AspNetCore.Razor.Language; @@ -9,17 +10,19 @@ internal sealed class DefaultRazorTagHelperRewritePhase : RazorEnginePhaseBase { protected override void ExecuteCore(RazorCodeDocument codeDocument) { - var syntaxTree = codeDocument.GetPreTagHelperSyntaxTree(); + var syntaxTree = codeDocument.GetPreTagHelperSyntaxTree() ?? codeDocument.GetSyntaxTree(); + ThrowForMissingDocumentDependency(syntaxTree); + var context = codeDocument.GetTagHelperContext(); - if (syntaxTree is null || context.TagHelpers.Count == 0) + if (context?.TagHelpers.Count > 0) { - // No descriptors, no-op. - return; + var rewrittenSyntaxTree = TagHelperParseTreeRewriter.Rewrite(syntaxTree, context.Prefix, context.TagHelpers, out var usedHelpers); + codeDocument.SetSyntaxTree(rewrittenSyntaxTree); + codeDocument.SetReferencedTagHelpers(usedHelpers); + } + else + { + codeDocument.SetReferencedTagHelpers(new HashSet()); } - - var rewrittenSyntaxTree = TagHelperParseTreeRewriter.Rewrite(syntaxTree, context.Prefix, context.TagHelpers, out var usedHelpers); - - codeDocument.SetReferencedTagHelpers(usedHelpers); - codeDocument.SetSyntaxTree(rewrittenSyntaxTree); } } diff --git a/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/Microsoft.NET.Sdk.Razor.SourceGenerators.csproj b/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/Microsoft.NET.Sdk.Razor.SourceGenerators.csproj index 682739d6978..b5a9a50af34 100644 --- a/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/Microsoft.NET.Sdk.Razor.SourceGenerators.csproj +++ b/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/Microsoft.NET.Sdk.Razor.SourceGenerators.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/RazorSourceGenerator.Helpers.cs b/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/RazorSourceGenerator.Helpers.cs index d7cb91e4910..7dfece6c432 100644 --- a/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/RazorSourceGenerator.Helpers.cs +++ b/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/RazorSourceGenerator.Helpers.cs @@ -83,8 +83,7 @@ private static RazorProjectEngine GetDiscoveryProjectEngine( return discoveryProjectEngine; } - private static RazorProjectEngine GetGenerationProjectEngine( - IReadOnlyList tagHelpers, + private static SourceGeneratorProjectEngine GetGenerationProjectEngine( SourceGeneratorProjectItem item, IEnumerable imports, RazorSourceGenerationOptions razorSourceGeneratorOptions) @@ -96,7 +95,7 @@ private static RazorProjectEngine GetGenerationProjectEngine( fileSystem.Add(import); } - var projectEngine = RazorProjectEngine.Create(razorSourceGeneratorOptions.Configuration, fileSystem, b => + var projectEngine = (DefaultRazorProjectEngine)RazorProjectEngine.Create(razorSourceGeneratorOptions.Configuration, fileSystem, b => { b.Features.Add(new DefaultTypeNameFeature()); b.SetRootNamespace(razorSourceGeneratorOptions.RootNamespace); @@ -107,16 +106,13 @@ private static RazorProjectEngine GetGenerationProjectEngine( options.SupportLocalizedComponentNames = razorSourceGeneratorOptions.SupportLocalizedComponentNames; })); - b.Features.Add(new StaticTagHelperFeature { TagHelpers = tagHelpers }); - b.Features.Add(new DefaultTagHelperDescriptorProvider()); - CompilerFeatures.Register(b); RazorExtensions.Register(b); b.SetCSharpLanguageVersion(razorSourceGeneratorOptions.CSharpLanguageVersion); }); - return projectEngine; + return new SourceGeneratorProjectEngine(projectEngine); } } } diff --git a/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/RazorSourceGenerator.cs b/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/RazorSourceGenerator.cs index 4fd80d7e3b6..b813fdba645 100644 --- a/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/RazorSourceGenerator.cs +++ b/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/RazorSourceGenerator.cs @@ -2,13 +2,15 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.IO; using System.Linq; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Microsoft.NET.Sdk.Razor.SourceGenerators { @@ -66,11 +68,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var generatedDeclarationCode = componentFiles .Combine(importFiles.Collect()) .Combine(razorSourceGeneratorOptions) + .WithLambdaComparer((old, @new) => (old.Right.Equals(@new.Right) && old.Left.Left.Equals(@new.Left.Left) && old.Left.Right.SequenceEqual(@new.Left.Right)), (a) => a.GetHashCode()) .Select(static (pair, _) => { var ((sourceItem, importFiles), razorSourceGeneratorOptions) = pair; - RazorSourceGeneratorEventSource.Log.GenerateDeclarationCodeStart(sourceItem.FilePath); + RazorSourceGeneratorEventSource.Log.GenerateDeclarationCodeStart(sourceItem.RelativePhysicalPath); var projectEngine = GetDeclarationProjectEngine(sourceItem, importFiles, razorSourceGeneratorOptions); @@ -78,57 +81,69 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var result = codeGen.GetCSharpDocument().GeneratedCode; - RazorSourceGeneratorEventSource.Log.GenerateDeclarationCodeStop(sourceItem.FilePath); + RazorSourceGeneratorEventSource.Log.GenerateDeclarationCodeStop(sourceItem.RelativePhysicalPath); - return result; + return (result, sourceItem.RelativePhysicalPath); }); var generatedDeclarationSyntaxTrees = generatedDeclarationCode .Combine(parseOptions) - .Select(static (pair, _) => + .Select(static (pair, ct) => { - var (generatedDeclarationCode, parseOptions) = pair; - return CSharpSyntaxTree.ParseText(generatedDeclarationCode, (CSharpParseOptions)parseOptions); + var ((generatedDeclarationCode, filePath), parseOptions) = pair; + return CSharpSyntaxTree.ParseText(generatedDeclarationCode, (CSharpParseOptions)parseOptions, filePath, cancellationToken: ct); }); - var tagHelpersFromCompilation = compilation - .Combine(generatedDeclarationSyntaxTrees.Collect()) + var tagHelpersFromComponents = generatedDeclarationSyntaxTrees + .Combine(compilation) .Combine(razorSourceGeneratorOptions) - .Select(static (pair, _) => + .SelectMany(static (pair, ct) => { - RazorSourceGeneratorEventSource.Log.DiscoverTagHelpersFromCompilationStart(); - var ((compilation, generatedDeclarationSyntaxTrees), razorSourceGeneratorOptions) = pair; + var ((generatedDeclarationSyntaxTree, compilation), razorSourceGeneratorOptions) = pair; + RazorSourceGeneratorEventSource.Log.DiscoverTagHelpersFromComponentStart(generatedDeclarationSyntaxTree.FilePath); var tagHelperFeature = new StaticCompilationTagHelperFeature(); var discoveryProjectEngine = GetDiscoveryProjectEngine(compilation.References.ToImmutableArray(), tagHelperFeature); - var compilationWithDeclarations = compilation.AddSyntaxTrees(generatedDeclarationSyntaxTrees); + var compilationWithDeclarations = compilation.AddSyntaxTrees(generatedDeclarationSyntaxTree); + + // try and find the specific root class this component is declaring, falling back to the assembly if for any reason the code is not in the shape we expect + ISymbol targetSymbol = compilationWithDeclarations.Assembly; + var root = generatedDeclarationSyntaxTree.GetRoot(ct); + if (root is CompilationUnitSyntax { Members: [NamespaceDeclarationSyntax { Members: [ClassDeclarationSyntax classSyntax, ..] }, ..] }) + { + var declaredClass = compilationWithDeclarations.GetSemanticModel(generatedDeclarationSyntaxTree).GetDeclaredSymbol(classSyntax, ct); + Debug.Assert(declaredClass is null || declaredClass is { AllInterfaces: [{ Name: "IComponent" }, ..] }); + targetSymbol = declaredClass ?? targetSymbol; + } tagHelperFeature.Compilation = compilationWithDeclarations; - tagHelperFeature.TargetSymbol = compilationWithDeclarations.Assembly; + tagHelperFeature.TargetSymbol = targetSymbol; - var result = (IList)tagHelperFeature.GetDescriptors(); - RazorSourceGeneratorEventSource.Log.DiscoverTagHelpersFromCompilationStop(); + var result = tagHelperFeature.GetDescriptors(); + RazorSourceGeneratorEventSource.Log.DiscoverTagHelpersFromComponentStop(generatedDeclarationSyntaxTree.FilePath); return result; - }) - .WithLambdaComparer(static (a, b) => + }); + + var tagHelpersFromCompilation = compilation + .Combine(razorSourceGeneratorOptions) + .Select(static (pair, _) => { - if (a.Count != b.Count) - { - return false; - } + RazorSourceGeneratorEventSource.Log.DiscoverTagHelpersFromCompilationStart(); - for (var i = 0; i < a.Count; i++) - { - if (!a[i].Equals(b[i])) - { - return false; - } - } + var (compilation, razorSourceGeneratorOptions) = pair; + + var tagHelperFeature = new StaticCompilationTagHelperFeature(); + var discoveryProjectEngine = GetDiscoveryProjectEngine(compilation.References.ToImmutableArray(), tagHelperFeature); - return true; - }, getHashCode: static a => a.Count); + tagHelperFeature.Compilation = compilation; + tagHelperFeature.TargetSymbol = compilation.Assembly; + + var result = tagHelperFeature.GetDescriptors(); + RazorSourceGeneratorEventSource.Log.DiscoverTagHelpersFromCompilationStop(); + return result; + }); var tagHelpersFromReferences = compilation .Combine(razorSourceGeneratorOptions) @@ -171,7 +186,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var tagHelperFeature = new StaticCompilationTagHelperFeature(); var discoveryProjectEngine = GetDiscoveryProjectEngine(compilation.References.ToImmutableArray(), tagHelperFeature); - List descriptors = new(); + using var pool = ArrayBuilderPool.GetPooledObject(out var descriptors); tagHelperFeature.Compilation = compilation; foreach (var reference in compilation.References) { @@ -183,47 +198,84 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } RazorSourceGeneratorEventSource.Log.DiscoverTagHelpersFromReferencesStop(); - return (ICollection)descriptors; + return descriptors.ToImmutable(); }); - var allTagHelpers = tagHelpersFromCompilation + var allTagHelpers = tagHelpersFromComponents.Collect() + .Combine(tagHelpersFromCompilation) .Combine(tagHelpersFromReferences) .Select(static (pair, _) => { - var (tagHelpersFromCompilation, tagHelpersFromReferences) = pair; - var count = tagHelpersFromCompilation.Count + tagHelpersFromReferences.Count; + var ((tagHelpersFromComponents, tagHelpersFromCompilation), tagHelpersFromReferences) = pair; + var count = tagHelpersFromCompilation.Length + tagHelpersFromReferences.Length + tagHelpersFromComponents.Length; if (count == 0) { - return Array.Empty(); + return ImmutableArray.Empty; } - var allTagHelpers = new TagHelperDescriptor[count]; - tagHelpersFromCompilation.CopyTo(allTagHelpers, 0); - tagHelpersFromReferences.CopyTo(allTagHelpers, tagHelpersFromCompilation.Count); + using var pool = ArrayBuilderPool.GetPooledObject(out var allTagHelpers); + allTagHelpers.AddRange(tagHelpersFromCompilation); + allTagHelpers.AddRange(tagHelpersFromReferences); + allTagHelpers.AddRange(tagHelpersFromComponents); - return allTagHelpers; + return allTagHelpers.ToImmutable(); }); var generatedOutput = sourceItems .Combine(importFiles.Collect()) - .Combine(allTagHelpers) + .WithLambdaComparer((old, @new) => old.Left.Equals(@new.Left) && old.Right.SequenceEqual(@new.Right), (a) => a.GetHashCode()) .Combine(razorSourceGeneratorOptions) .Select(static (pair, _) => { - var (((sourceItem, imports), allTagHelpers), razorSourceGeneratorOptions) = pair; + var ((sourceItem, imports), razorSourceGeneratorOptions) = pair; + + RazorSourceGeneratorEventSource.Log.ParseRazorDocumentStart(sourceItem.RelativePhysicalPath); + + var projectEngine = GetGenerationProjectEngine(sourceItem, imports, razorSourceGeneratorOptions); + + var document = projectEngine.ProcessInitialParse(sourceItem); + + RazorSourceGeneratorEventSource.Log.ParseRazorDocumentStop(sourceItem.RelativePhysicalPath); + return (projectEngine, sourceItem.RelativePhysicalPath, document); + }) + + // Add the tag helpers in, but ignore if they've changed or not, only reprocessing the actual document changed + .Combine(allTagHelpers) + .WithLambdaComparer((old, @new) => old.Left.Equals(@new.Left), (item) => item.GetHashCode()) + .Select((pair, _) => + { + var ((projectEngine, filePath, codeDocument), allTagHelpers) = pair; + RazorSourceGeneratorEventSource.Log.RewriteTagHelpersStart(filePath); - RazorSourceGeneratorEventSource.Log.RazorCodeGenerateStart(sourceItem.FilePath); + codeDocument = projectEngine.ProcessTagHelpers(codeDocument, allTagHelpers, checkForIdempotency: false); - // Add a generated suffix so tools, such as coverlet, consider the file to be generated - var hintName = GetIdentifierFromPath(sourceItem.RelativePhysicalPath) + ".g.cs"; + RazorSourceGeneratorEventSource.Log.RewriteTagHelpersStop(filePath); + return (projectEngine, filePath, codeDocument); + }) + + // next we do a second parse, along with the helpers, but check for idempotency. If the tag helpers used on the previous parse match, the compiler can skip re-computing them + .Combine(allTagHelpers) + .Select((pair, _) => + { - var projectEngine = GetGenerationProjectEngine(allTagHelpers, sourceItem, imports, razorSourceGeneratorOptions); + var ((projectEngine, filePath, document), allTagHelpers) = pair; + RazorSourceGeneratorEventSource.Log.CheckAndRewriteTagHelpersStart(filePath); - var codeDocument = projectEngine.Process(sourceItem); - var csharpDocument = codeDocument.GetCSharpDocument(); + document = projectEngine.ProcessTagHelpers(document, allTagHelpers, checkForIdempotency: true); - RazorSourceGeneratorEventSource.Log.RazorCodeGenerateStop(sourceItem.FilePath); - return (hintName, csharpDocument); + RazorSourceGeneratorEventSource.Log.CheckAndRewriteTagHelpersStop(filePath); + return (projectEngine, filePath, document); + }) + + .Select((pair, _) => + { + var (projectEngine, filePath, document) = pair; + RazorSourceGeneratorEventSource.Log.RazorCodeGenerateStart(filePath); + document = projectEngine.ProcessRemaining(document); + var csharpDocument = document.CodeDocument.GetCSharpDocument(); + + RazorSourceGeneratorEventSource.Log.RazorCodeGenerateStop(filePath); + return (filePath, csharpDocument); }) .WithLambdaComparer(static (a, b) => { @@ -238,7 +290,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterSourceOutput(generatedOutput, static (context, pair) => { - var (hintName, csharpDocument) = pair; + var (filePath, csharpDocument) = pair; + + // Add a generated suffix so tools, such as coverlet, consider the file to be generated + var hintName = GetIdentifierFromPath(filePath) + ".g.cs"; + RazorSourceGeneratorEventSource.Log.AddSyntaxTrees(hintName); for (var i = 0; i < csharpDocument.Diagnostics.Count; i++) { diff --git a/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/RazorSourceGeneratorEventSource.cs b/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/RazorSourceGeneratorEventSource.cs index 758d0069b8b..b94646cc81a 100644 --- a/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/RazorSourceGeneratorEventSource.cs +++ b/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/RazorSourceGeneratorEventSource.cs @@ -59,5 +59,37 @@ private RazorSourceGeneratorEventSource() { } private const int GenerateDeclarationSyntaxTreeStopId = 14; [Event(GenerateDeclarationSyntaxTreeStopId, Level = EventLevel.Informational)] public void GenerateDeclarationSyntaxTreeStop() => WriteEvent(GenerateDeclarationSyntaxTreeStopId); + + private const int DiscoverTagHelpersFromComponentStartId = 15; + [Event(DiscoverTagHelpersFromComponentStartId, Level = EventLevel.Informational)] + public void DiscoverTagHelpersFromComponentStart(string filePath) => WriteEvent(DiscoverTagHelpersFromComponentStartId, filePath); + + private const int DiscoverTagHelpersFromComponentStopId = 16; + [Event(DiscoverTagHelpersFromComponentStopId, Level = EventLevel.Informational)] + public void DiscoverTagHelpersFromComponentStop(string filePath) => WriteEvent(DiscoverTagHelpersFromComponentStopId, filePath); + + private const int ParseRazorDocumentStartId = 17; + [Event(ParseRazorDocumentStartId, Level = EventLevel.Informational)] + public void ParseRazorDocumentStart(string file) => WriteEvent(ParseRazorDocumentStartId, file); + + private const int ParseRazorDocumentStopId = 18; + [Event(ParseRazorDocumentStopId, Level = EventLevel.Informational)] + public void ParseRazorDocumentStop(string file) => WriteEvent(ParseRazorDocumentStopId, file); + + private const int RewriteTagHelpersStartId = 19; + [Event(RewriteTagHelpersStartId, Level = EventLevel.Informational)] + public void RewriteTagHelpersStart(string file) => WriteEvent(RewriteTagHelpersStartId, file); + + private const int RewriteTagHelpersStopId = 20; + [Event(RewriteTagHelpersStopId, Level = EventLevel.Informational)] + public void RewriteTagHelpersStop(string file) => WriteEvent(RewriteTagHelpersStopId, file); + + private const int CheckAndRewriteTagHelpersStartId = 21; + [Event(CheckAndRewriteTagHelpersStartId, Level = EventLevel.Informational)] + public void CheckAndRewriteTagHelpersStart(string file) => WriteEvent(CheckAndRewriteTagHelpersStartId, file); + + private const int CheckAndRewriteTagHelpersStopId = 22; + [Event(CheckAndRewriteTagHelpersStopId, Level = EventLevel.Informational)] + public void CheckAndRewriteTagHelpersStop(string file) => WriteEvent(CheckAndRewriteTagHelpersStopId, file); } } diff --git a/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/SourceGeneratorProjectEngine.cs b/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/SourceGeneratorProjectEngine.cs new file mode 100644 index 00000000000..4acb91159b3 --- /dev/null +++ b/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/SourceGeneratorProjectEngine.cs @@ -0,0 +1,115 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.NET.Sdk.Razor.SourceGenerators; + +internal class SourceGeneratorProjectEngine : DefaultRazorProjectEngine +{ + private readonly int discoveryPhaseIndex = -1; + + private readonly int rewritePhaseIndex = -1; + + public SourceGeneratorProjectEngine(DefaultRazorProjectEngine projectEngine) + : base(projectEngine.Configuration, projectEngine.Engine, projectEngine.FileSystem, projectEngine.ProjectFeatures) + { + for (int i = 0; i < Engine.Phases.Count; i++) + { + if (Engine.Phases[i] is DefaultRazorTagHelperContextDiscoveryPhase) + { + discoveryPhaseIndex = i; + } + else if (Engine.Phases[i] is DefaultRazorTagHelperRewritePhase) + { + rewritePhaseIndex = i; + } + else if (discoveryPhaseIndex >= 0 && rewritePhaseIndex >= 0) + { + break; + } + } + Debug.Assert(discoveryPhaseIndex >= 0); + Debug.Assert(rewritePhaseIndex >= 0); + } + + public SourceGeneratorRazorCodeDocument ProcessInitialParse(RazorProjectItem projectItem) + { + var codeDocument = CreateCodeDocumentCore(projectItem); + ProcessPartial(codeDocument, 0, discoveryPhaseIndex); + + // record the syntax tree, before the tag helper re-writing occurs + codeDocument.SetPreTagHelperSyntaxTree(codeDocument.GetSyntaxTree()); + return new SourceGeneratorRazorCodeDocument(codeDocument); + } + + public SourceGeneratorRazorCodeDocument ProcessTagHelpers(SourceGeneratorRazorCodeDocument sgDocument, IReadOnlyList tagHelpers, bool checkForIdempotency) + { + Debug.Assert(sgDocument.CodeDocument.GetPreTagHelperSyntaxTree() is not null); + + int startIndex = discoveryPhaseIndex; + var codeDocument = sgDocument.CodeDocument; + var previousTagHelpers = codeDocument.GetTagHelpers(); + if (checkForIdempotency && previousTagHelpers is not null) + { + // compare the tag helpers with the ones the document last used + if (Enumerable.SequenceEqual(tagHelpers, previousTagHelpers)) + { + // tag helpers are the same, nothing to do! + return sgDocument; + } + else + { + // tag helpers have changed, figure out if we need to re-write + var oldContextHelpers = codeDocument.GetTagHelperContext().TagHelpers; + + // re-run the scope check to figure out which tag helpers this document can see + codeDocument.SetTagHelpers(tagHelpers); + Engine.Phases[discoveryPhaseIndex].Execute(codeDocument); + + // Check if any new tag helpers were added or ones we previously used were removed + var newContextHelpers = codeDocument.GetTagHelperContext().TagHelpers; + var added = newContextHelpers.Except(oldContextHelpers); + var referencedByRemoved = codeDocument.GetReferencedTagHelpers().Except(newContextHelpers); + if (!added.Any() && !referencedByRemoved.Any()) + { + // Either nothing new, or any that got removed weren't used by this document anyway + return sgDocument; + } + + // We need to re-write the document, but can skip the scoping as we just performed it + startIndex = rewritePhaseIndex; + } + } + else + { + codeDocument.SetTagHelpers(tagHelpers); + } + + ProcessPartial(codeDocument, startIndex, rewritePhaseIndex + 1); + return new SourceGeneratorRazorCodeDocument(codeDocument); + } + + public SourceGeneratorRazorCodeDocument ProcessRemaining(SourceGeneratorRazorCodeDocument sgDocument) + { + var codeDocument = sgDocument.CodeDocument; + Debug.Assert(codeDocument.GetReferencedTagHelpers() is not null); + + ProcessPartial(sgDocument.CodeDocument, rewritePhaseIndex, Engine.Phases.Count); + return new SourceGeneratorRazorCodeDocument(codeDocument); + } + + private void ProcessPartial(RazorCodeDocument codeDocument, int startIndex, int endIndex) + { + Debug.Assert(startIndex >= 0 && startIndex <= endIndex && endIndex <= Engine.Phases.Count); + for (var i = startIndex; i < endIndex; i++) + { + Engine.Phases[i].Execute(codeDocument); + } + } +} diff --git a/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/SourceGeneratorRazorCodeDocument.cs b/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/SourceGeneratorRazorCodeDocument.cs new file mode 100644 index 00000000000..707e4345e63 --- /dev/null +++ b/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/SourceGeneratorRazorCodeDocument.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.NET.Sdk.Razor.SourceGenerators; + +/// +/// A wrapper for +/// +/// +/// The razor compiler modifies the in place during the various phases, +/// meaning object identity is maintained even when the contents have changed. +/// +/// We need to be able to identify from the source generator if a given code document was modified or +/// returned unchanged. Rather than implementing deep equality on the +/// which can get expensive when the is large, we instead use a wrapper class. +/// If the underlying document is unchanged we return the original wrapper class. If the underlying +/// document is changed, we return a new instance of the wrapper. +/// +internal class SourceGeneratorRazorCodeDocument +{ + public RazorCodeDocument CodeDocument { get; } + + public SourceGeneratorRazorCodeDocument(RazorCodeDocument razorCodeDocument) + { + this.CodeDocument = razorCodeDocument; + } +} diff --git a/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/StaticCompilationTagHelperFeature.cs b/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/StaticCompilationTagHelperFeature.cs index 15d54b36587..4828c20dd5d 100644 --- a/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/StaticCompilationTagHelperFeature.cs +++ b/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/StaticCompilationTagHelperFeature.cs @@ -2,8 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; @@ -11,18 +13,16 @@ namespace Microsoft.NET.Sdk.Razor.SourceGenerators { internal sealed class StaticCompilationTagHelperFeature : RazorEngineFeatureBase, ITagHelperFeature { - private static readonly List EmptyList = new(); - private ITagHelperDescriptorProvider[]? _providers; - public List GetDescriptors() + public ImmutableArray GetDescriptors() { if (Compilation is null) { - return EmptyList; + return ImmutableArray.Empty; } - var results = new List(); + using var pool = ArrayBuilderPool.GetPooledObject(out var results); var context = TagHelperDescriptorProviderContext.Create(results); context.SetCompilation(Compilation); if (TargetSymbol is not null) @@ -35,7 +35,7 @@ public List GetDescriptors() _providers[i].Execute(context); } - return results; + return results.ToImmutable(); } IReadOnlyList ITagHelperFeature.GetDescriptors() => GetDescriptors(); diff --git a/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/StaticTagHelperFeature.cs b/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/StaticTagHelperFeature.cs deleted file mode 100644 index 89207e40b8a..00000000000 --- a/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/StaticTagHelperFeature.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; -using Microsoft.AspNetCore.Razor.Language; - -namespace Microsoft.NET.Sdk.Razor.SourceGenerators -{ - internal sealed class StaticTagHelperFeature : RazorEngineFeatureBase, ITagHelperFeature - { - public IReadOnlyList TagHelpers { get; set; } - - public IReadOnlyList GetDescriptors() => TagHelpers; - - public StaticTagHelperFeature() - { - TagHelpers = new List(); - } - - public StaticTagHelperFeature(IEnumerable tagHelpers) - { - TagHelpers = new List(tagHelpers); - } - } -} 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 6d93d01cec3..caf1a74d5ea 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 @@ -7,7 +7,6 @@ using System.IO; using System.Linq; using System.Text; -using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.CodeAnalysis; @@ -61,21 +60,6 @@ protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components. Assert.Single(result.GeneratedSources); } - internal class InMemoryAdditionalText : AdditionalText - { - private readonly SourceText _content; - - public InMemoryAdditionalText(string path, string content) - { - Path = path; - _content = SourceText.From(content, Encoding.UTF8); - } - - public override string Path { get; } - - public override SourceText GetText(CancellationToken cancellationToken = default) => _content; - } - [Fact] public async Task SourceGeneratorEvents_RazorFiles_Works() { @@ -143,74 +127,41 @@ protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components. Assert.Collection(eventListener.Events, e => Assert.Equal("ComputeRazorSourceGeneratorOptions", e.EventName), - e => - { - Assert.Equal("GenerateDeclarationCodeStart", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Index.razor", file); - }, - e => - { - Assert.Equal("GenerateDeclarationCodeStop", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Index.razor", file); - }, - e => - { - Assert.Equal("GenerateDeclarationCodeStart", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Counter.razor", file); - }, - e => - { - Assert.Equal("GenerateDeclarationCodeStop", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Counter.razor", file); - }, + e => e.AssertSingleItem("ParseRazorDocumentStart", "Pages/Index.razor"), + e => e.AssertSingleItem("ParseRazorDocumentStop", "Pages/Index.razor"), + e => e.AssertSingleItem("ParseRazorDocumentStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("ParseRazorDocumentStop", "Pages/Counter.razor"), + e => e.AssertSingleItem("GenerateDeclarationCodeStart", "Pages/Index.razor"), + e => e.AssertSingleItem("GenerateDeclarationCodeStop", "Pages/Index.razor"), + e => e.AssertSingleItem("GenerateDeclarationCodeStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("GenerateDeclarationCodeStop", "Pages/Counter.razor"), + e => e.AssertSingleItem("DiscoverTagHelpersFromComponentStart", "Pages/Index.razor"), + e => e.AssertSingleItem("DiscoverTagHelpersFromComponentStop", "Pages/Index.razor"), + e => e.AssertSingleItem("DiscoverTagHelpersFromComponentStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("DiscoverTagHelpersFromComponentStop", "Pages/Counter.razor"), e => Assert.Equal("DiscoverTagHelpersFromCompilationStart", e.EventName), e => Assert.Equal("DiscoverTagHelpersFromCompilationStop", e.EventName), e => Assert.Equal("DiscoverTagHelpersFromReferencesStart", e.EventName), e => Assert.Equal("DiscoverTagHelpersFromReferencesStop", e.EventName), - e => - { - Assert.Equal("RazorCodeGenerateStart", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Index.razor", file); - }, - e => - { - Assert.Equal("RazorCodeGenerateStop", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Index.razor", file); - }, - e => - { - Assert.Equal("RazorCodeGenerateStart", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Counter.razor", file); - }, - e => - { - Assert.Equal("RazorCodeGenerateStop", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Counter.razor", file); - }, - e => - { - Assert.Equal("AddSyntaxTrees", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("Pages_Index_razor.g.cs", file); - }, - e => - { - Assert.Equal("AddSyntaxTrees", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("Pages_Counter_razor.g.cs", file); - }); + e => e.AssertSingleItem("RewriteTagHelpersStart", "Pages/Index.razor"), + e => e.AssertSingleItem("RewriteTagHelpersStop", "Pages/Index.razor"), + e => e.AssertSingleItem("RewriteTagHelpersStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("RewriteTagHelpersStop", "Pages/Counter.razor"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStart", "Pages/Index.razor"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStop", "Pages/Index.razor"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStop", "Pages/Counter.razor"), + e => e.AssertSingleItem("RazorCodeGenerateStart", "Pages/Index.razor"), + e => e.AssertSingleItem("RazorCodeGenerateStop", "Pages/Index.razor"), + e => e.AssertSingleItem("RazorCodeGenerateStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("RazorCodeGenerateStop", "Pages/Counter.razor"), + e => e.AssertSingleItem("AddSyntaxTrees", "Pages_Index_razor.g.cs"), + e => e.AssertSingleItem("AddSyntaxTrees", "Pages_Counter_razor.g.cs") + ); } [Fact] - public async Task IncrementalCompilation_DoesNotReexecuteSteps_WhenRazorFilesAreUnchanged() + public async Task IncrementalCompilation_DoesNotReExecuteSteps_WhenRazorFilesAreUnchanged() { // Arrange using var eventListener = new RazorEventListener(); @@ -392,36 +343,18 @@ protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components. Assert.Equal(2, result.GeneratedSources.Length); Assert.Collection(eventListener.Events, - e => - { - Assert.Equal("GenerateDeclarationCodeStart", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Counter.razor", file); - }, - e => - { - Assert.Equal("GenerateDeclarationCodeStop", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Counter.razor", file); - }, - e => - { - Assert.Equal("RazorCodeGenerateStart", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Counter.razor", file); - }, - e => - { - Assert.Equal("RazorCodeGenerateStop", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Counter.razor", file); - }, - e => - { - Assert.Equal("AddSyntaxTrees", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("Pages_Counter_razor.g.cs", file); - }); + e => e.AssertSingleItem("ParseRazorDocumentStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("ParseRazorDocumentStop", "Pages/Counter.razor"), + e => e.AssertSingleItem("GenerateDeclarationCodeStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("GenerateDeclarationCodeStop", "Pages/Counter.razor"), + e => e.AssertSingleItem("RewriteTagHelpersStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("RewriteTagHelpersStop", "Pages/Counter.razor"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStop", "Pages/Counter.razor"), + e => e.AssertSingleItem("RazorCodeGenerateStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("RazorCodeGenerateStop", "Pages/Counter.razor"), + e => e.AssertSingleItem("AddSyntaxTrees", "Pages_Counter_razor.g.cs") + ); } [Fact] @@ -514,8 +447,13 @@ public class Person Assert.Equal(2, result.GeneratedSources.Length); Assert.Collection(eventListener.Events, - e => Assert.Equal("DiscoverTagHelpersFromCompilationStart", e.EventName), - e => Assert.Equal("DiscoverTagHelpersFromCompilationStop", e.EventName)); + e => e.AssertSingleItem("DiscoverTagHelpersFromComponentStart", "Pages/Index.razor"), + e => e.AssertSingleItem("DiscoverTagHelpersFromComponentStop", "Pages/Index.razor"), + e => e.AssertSingleItem("DiscoverTagHelpersFromComponentStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("DiscoverTagHelpersFromComponentStop", "Pages/Counter.razor"), + e => Assert.Equal("DiscoverTagHelpersFromCompilationStart", e.EventName), + e => Assert.Equal("DiscoverTagHelpersFromCompilationStop", e.EventName) + ); } [Fact] @@ -613,6 +551,8 @@ public class Person }", Encoding.UTF8)).Project; compilation = await project.GetCompilationAsync(); + eventListener.Events.Clear(); + result = RunGenerator(compilation!, ref driver, expectedDiagnostics) .VerifyOutputsMatch(result); @@ -620,8 +560,13 @@ public class Person Assert.Equal(2, result.GeneratedSources.Length); Assert.Collection(eventListener.Events, + e => e.AssertSingleItem("DiscoverTagHelpersFromComponentStart", "Pages/Index.razor"), + e => e.AssertSingleItem("DiscoverTagHelpersFromComponentStop", "Pages/Index.razor"), + e => e.AssertSingleItem("DiscoverTagHelpersFromComponentStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("DiscoverTagHelpersFromComponentStop", "Pages/Counter.razor"), e => Assert.Equal("DiscoverTagHelpersFromCompilationStart", e.EventName), - e => Assert.Equal("DiscoverTagHelpersFromCompilationStop", e.EventName)); + e => Assert.Equal("DiscoverTagHelpersFromCompilationStop", e.EventName) + ); } [Fact] @@ -766,38 +711,20 @@ protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components. Assert.Equal(2, result.GeneratedSources.Length); Assert.Collection(eventListener.Events, - e => - { - Assert.Equal("GenerateDeclarationCodeStart", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Counter.razor", file); - }, - e => - { - Assert.Equal("GenerateDeclarationCodeStop", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Counter.razor", file); - }, - e => Assert.Equal("DiscoverTagHelpersFromCompilationStart", e.EventName), - e => Assert.Equal("DiscoverTagHelpersFromCompilationStop", e.EventName), - e => - { - Assert.Equal("RazorCodeGenerateStart", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Counter.razor", file); - }, - e => - { - Assert.Equal("RazorCodeGenerateStop", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Counter.razor", file); - }, - e => - { - Assert.Equal("AddSyntaxTrees", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("Pages_Counter_razor.g.cs", file); - }); + e => e.AssertSingleItem("ParseRazorDocumentStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("ParseRazorDocumentStop", "Pages/Counter.razor"), + e => e.AssertSingleItem("GenerateDeclarationCodeStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("GenerateDeclarationCodeStop", "Pages/Counter.razor"), + e => e.AssertSingleItem("DiscoverTagHelpersFromComponentStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("DiscoverTagHelpersFromComponentStop", "Pages/Counter.razor"), + e => e.AssertSingleItem("RewriteTagHelpersStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("RewriteTagHelpersStop", "Pages/Counter.razor"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStop", "Pages/Counter.razor"), + e => e.AssertSingleItem("RazorCodeGenerateStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("RazorCodeGenerateStop", "Pages/Counter.razor"), + e => e.AssertSingleItem("AddSyntaxTrees", "Pages_Counter_razor.g.cs") + ); } [Fact] @@ -946,50 +873,24 @@ protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components. Assert.Equal(2, result.GeneratedSources.Length); Assert.Collection(eventListener.Events, - e => - { - Assert.Equal("GenerateDeclarationCodeStart", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Counter.razor", file); - }, - e => - { - Assert.Equal("GenerateDeclarationCodeStop", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Counter.razor", file); - }, - e => Assert.Equal("DiscoverTagHelpersFromCompilationStart", e.EventName), - e => Assert.Equal("DiscoverTagHelpersFromCompilationStop", e.EventName), - e => - { - Assert.Equal("RazorCodeGenerateStart", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Index.razor", file); - }, - e => - { - Assert.Equal("RazorCodeGenerateStop", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Index.razor", file); - }, - e => - { - Assert.Equal("RazorCodeGenerateStart", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Counter.razor", file); - }, - e => - { - Assert.Equal("RazorCodeGenerateStop", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Counter.razor", file); - }, - e => - { - Assert.Equal("AddSyntaxTrees", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("Pages_Counter_razor.g.cs", file); - }); + e => e.AssertSingleItem("ParseRazorDocumentStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("ParseRazorDocumentStop", "Pages/Counter.razor"), + e => e.AssertSingleItem("GenerateDeclarationCodeStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("GenerateDeclarationCodeStop", "Pages/Counter.razor"), + e => e.AssertSingleItem("DiscoverTagHelpersFromComponentStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("DiscoverTagHelpersFromComponentStop", "Pages/Counter.razor"), + e => e.AssertSingleItem("RewriteTagHelpersStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("RewriteTagHelpersStop", "Pages/Counter.razor"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStart", "Pages/Index.razor"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStop", "Pages/Index.razor"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStop", "Pages/Counter.razor"), + e => e.AssertSingleItem("RazorCodeGenerateStart", "Pages/Index.razor"), + e => e.AssertSingleItem("RazorCodeGenerateStop", "Pages/Index.razor"), + e => e.AssertSingleItem("RazorCodeGenerateStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("RazorCodeGenerateStop", "Pages/Counter.razor"), + e => e.AssertSingleItem("AddSyntaxTrees", "Pages_Counter_razor.g.cs") + ); } [Fact] @@ -1120,40 +1021,24 @@ protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components. Assert.Equal(2, result.GeneratedSources.Length); Assert.Collection(eventListener.Events, + e => e.AssertSingleItem("DiscoverTagHelpersFromComponentStart", "Pages/Index.razor"), + e => e.AssertSingleItem("DiscoverTagHelpersFromComponentStop", "Pages/Index.razor"), + e => e.AssertSingleItem("DiscoverTagHelpersFromComponentStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("DiscoverTagHelpersFromComponentStop", "Pages/Counter.razor"), e => Assert.Equal("DiscoverTagHelpersFromCompilationStart", e.EventName), e => Assert.Equal("DiscoverTagHelpersFromCompilationStop", e.EventName), e => Assert.Equal("DiscoverTagHelpersFromReferencesStart", e.EventName), e => Assert.Equal("DiscoverTagHelpersFromReferencesStop", e.EventName), - e => - { - Assert.Equal("RazorCodeGenerateStart", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Index.razor", file); - }, - e => - { - Assert.Equal("RazorCodeGenerateStop", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Index.razor", file); - }, - e => - { - Assert.Equal("RazorCodeGenerateStart", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Counter.razor", file); - }, - e => - { - Assert.Equal("RazorCodeGenerateStop", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Counter.razor", file); - }, - e => - { - Assert.Equal("AddSyntaxTrees", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("Pages_Index_razor.g.cs", file); - }); + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStart", "Pages/Index.razor"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStop", "Pages/Index.razor"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStop", "Pages/Counter.razor"), + e => e.AssertSingleItem("RazorCodeGenerateStart", "Pages/Index.razor"), + e => e.AssertSingleItem("RazorCodeGenerateStop", "Pages/Index.razor"), + e => e.AssertSingleItem("RazorCodeGenerateStart", "Pages/Counter.razor"), + e => e.AssertSingleItem("RazorCodeGenerateStop", "Pages/Counter.razor"), + e => e.AssertSingleItem("AddSyntaxTrees", "Pages_Index_razor.g.cs") + ); // Verify caching eventListener.Events.Clear(); @@ -1305,46 +1190,29 @@ internal sealed class Views_Shared__Layout : global::Microsoft.AspNetCore.Mvc.Ra Assert.Collection(eventListener.Events, e => Assert.Equal("ComputeRazorSourceGeneratorOptions", e.EventName), + e => e.AssertSingleItem("ParseRazorDocumentStart", "Pages/Index.cshtml"), + e => e.AssertSingleItem("ParseRazorDocumentStop", "Pages/Index.cshtml"), + e => e.AssertSingleItem("ParseRazorDocumentStart", "Views/Shared/_Layout.cshtml"), + e => e.AssertSingleItem("ParseRazorDocumentStop", "Views/Shared/_Layout.cshtml"), e => Assert.Equal("DiscoverTagHelpersFromCompilationStart", e.EventName), e => Assert.Equal("DiscoverTagHelpersFromCompilationStop", e.EventName), e => Assert.Equal("DiscoverTagHelpersFromReferencesStart", e.EventName), e => Assert.Equal("DiscoverTagHelpersFromReferencesStop", e.EventName), - e => - { - Assert.Equal("RazorCodeGenerateStart", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Index.cshtml", file); - }, - e => - { - Assert.Equal("RazorCodeGenerateStop", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Index.cshtml", file); - }, - e => - { - Assert.Equal("RazorCodeGenerateStart", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Views/Shared/_Layout.cshtml", file); - }, - e => - { - Assert.Equal("RazorCodeGenerateStop", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Views/Shared/_Layout.cshtml", file); - }, - e => - { - Assert.Equal("AddSyntaxTrees", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("Pages_Index_cshtml.g.cs", file); - }, - e => - { - Assert.Equal("AddSyntaxTrees", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("Views_Shared__Layout_cshtml.g.cs", file); - }); + e => e.AssertSingleItem("RewriteTagHelpersStart", "Pages/Index.cshtml"), + e => e.AssertSingleItem("RewriteTagHelpersStop", "Pages/Index.cshtml"), + e => e.AssertSingleItem("RewriteTagHelpersStart", "Views/Shared/_Layout.cshtml"), + e => e.AssertSingleItem("RewriteTagHelpersStop", "Views/Shared/_Layout.cshtml"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStart", "Pages/Index.cshtml"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStop", "Pages/Index.cshtml"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStart", "Views/Shared/_Layout.cshtml"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStop", "Views/Shared/_Layout.cshtml"), + e => e.AssertSingleItem("RazorCodeGenerateStart", "Pages/Index.cshtml"), + e => e.AssertSingleItem("RazorCodeGenerateStop", "Pages/Index.cshtml"), + e => e.AssertSingleItem("RazorCodeGenerateStart", "Views/Shared/_Layout.cshtml"), + e => e.AssertSingleItem("RazorCodeGenerateStop", "Views/Shared/_Layout.cshtml"), + e => e.AssertSingleItem("AddSyntaxTrees", "Pages_Index_cshtml.g.cs"), + e => e.AssertSingleItem("AddSyntaxTrees", "Views_Shared__Layout_cshtml.g.cs") + ); } [Fact] @@ -1536,24 +1404,16 @@ internal sealed class Views_Shared__Layout : global::Microsoft.AspNetCore.Mvc.Ra Assert.Equal(2, result.GeneratedSources.Length); Assert.Collection(eventListener.Events, - e => - { - Assert.Equal("RazorCodeGenerateStart", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Views/Shared/_Layout.cshtml", file); - }, - e => - { - Assert.Equal("RazorCodeGenerateStop", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Views/Shared/_Layout.cshtml", file); - }, - e => - { - Assert.Equal("AddSyntaxTrees", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("Views_Shared__Layout_cshtml.g.cs", file); - }); + e => e.AssertSingleItem("ParseRazorDocumentStart", "Views/Shared/_Layout.cshtml"), + e => e.AssertSingleItem("ParseRazorDocumentStop", "Views/Shared/_Layout.cshtml"), + e => e.AssertSingleItem("RewriteTagHelpersStart", "Views/Shared/_Layout.cshtml"), + e => e.AssertSingleItem("RewriteTagHelpersStop", "Views/Shared/_Layout.cshtml"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStart", "Views/Shared/_Layout.cshtml"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStop", "Views/Shared/_Layout.cshtml"), + e => e.AssertSingleItem("RazorCodeGenerateStart", "Views/Shared/_Layout.cshtml"), + e => e.AssertSingleItem("RazorCodeGenerateStop", "Views/Shared/_Layout.cshtml"), + e => e.AssertSingleItem("AddSyntaxTrees", "Views_Shared__Layout_cshtml.g.cs") + ); } [Fact] @@ -1712,8 +1572,8 @@ public class Person Assert.Collection(eventListener.Events, e => Assert.Equal("DiscoverTagHelpersFromCompilationStart", e.EventName), - e => Assert.Equal("DiscoverTagHelpersFromCompilationStop", e.EventName)); - + e => Assert.Equal("DiscoverTagHelpersFromCompilationStop", e.EventName) + ); } [Fact] @@ -1863,7 +1723,92 @@ public override void Process(TagHelperContext context, TagHelperOutput output) }", Encoding.UTF8)).Project; compilation = await project.GetCompilationAsync(); - result = RunGenerator(compilation!, ref driver); + result = RunGenerator(compilation!, ref driver) + .VerifyOutputsMatch(result, (0, @" +#pragma checksum ""Pages/Index.cshtml"" ""{ff1816ec-aa5e-4d10-87f7-6f4963833460}"" ""5d59ecd7b7cf7355d7f60234988be34b81a8b614"" +// +#pragma warning disable 1591 +[assembly: global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemAttribute(typeof(AspNetCoreGeneratedDocument.Pages_Index), @""mvc.1.0.view"", @""/Pages/Index.cshtml"")] +namespace AspNetCoreGeneratedDocument +{ + #line hidden + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Rendering; + using Microsoft.AspNetCore.Mvc.ViewFeatures; + [global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemMetadataAttribute(""Identifier"", ""/Pages/Index.cshtml"")] + [global::System.Runtime.CompilerServices.CreateNewOnMetadataUpdateAttribute] + #nullable restore + internal sealed class Pages_Index : global::Microsoft.AspNetCore.Mvc.Razor.RazorPage + #nullable disable + { + #line hidden + #pragma warning disable 0649 + private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperExecutionContext __tagHelperExecutionContext; + #pragma warning restore 0649 + private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperRunner __tagHelperRunner = new global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperRunner(); + #pragma warning disable 0169 + private string __tagHelperStringValueBuffer; + #pragma warning restore 0169 + private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperScopeManager __backed__tagHelperScopeManager = null; + private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperScopeManager __tagHelperScopeManager + { + get + { + if (__backed__tagHelperScopeManager == null) + { + __backed__tagHelperScopeManager = new global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperScopeManager(StartTagHelperWritingScope, EndTagHelperWritingScope); + } + return __backed__tagHelperScopeManager; + } + } + private global::MyApp.HeaderTagHelper __MyApp_HeaderTagHelper; + #pragma warning disable 1998 + public async override global::System.Threading.Tasks.Task ExecuteAsync() + { + WriteLiteral(""\r\n""); + __tagHelperExecutionContext = __tagHelperScopeManager.Begin(""h2"", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.StartTagAndEndTag, ""5d59ecd7b7cf7355d7f60234988be34b81a8b6142529"", async() => { + WriteLiteral(""Hello world""); + } + ); + __MyApp_HeaderTagHelper = CreateTagHelper(); + __tagHelperExecutionContext.Add(__MyApp_HeaderTagHelper); + await __tagHelperRunner.RunAsync(__tagHelperExecutionContext); + if (!__tagHelperExecutionContext.Output.IsContentModified) + { + await __tagHelperExecutionContext.SetOutputContentAsync(); + } + Write(__tagHelperExecutionContext.Output); + __tagHelperExecutionContext = __tagHelperScopeManager.End(); + } + #pragma warning restore 1998 + #nullable restore + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; } = default!; + #nullable disable + #nullable restore + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; } = default!; + #nullable disable + #nullable restore + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; } = default!; + #nullable disable + #nullable restore + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; } = default!; + #nullable disable + #nullable restore + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper Html { get; private set; } = default!; + #nullable disable + } +} +#pragma warning restore 1591 +")); Assert.Empty(result.Diagnostics); Assert.Equal(2, result.GeneratedSources.Length); @@ -1871,36 +1816,14 @@ public override void Process(TagHelperContext context, TagHelperOutput output) Assert.Collection(eventListener.Events, e => Assert.Equal("DiscoverTagHelpersFromCompilationStart", e.EventName), e => Assert.Equal("DiscoverTagHelpersFromCompilationStop", e.EventName), - e => - { - Assert.Equal("RazorCodeGenerateStart", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Index.cshtml", file); - }, - e => - { - Assert.Equal("RazorCodeGenerateStop", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Pages/Index.cshtml", file); - }, - e => - { - Assert.Equal("RazorCodeGenerateStart", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Views/Shared/_Layout.cshtml", file); - }, - e => - { - Assert.Equal("RazorCodeGenerateStop", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("/Views/Shared/_Layout.cshtml", file); - }, - e => - { - Assert.Equal("AddSyntaxTrees", e.EventName); - var file = Assert.Single(e.Payload); - Assert.Equal("Pages_Index_cshtml.g.cs", file); - }); + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStart", "Pages/Index.cshtml"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStop", "Pages/Index.cshtml"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStart", "Views/Shared/_Layout.cshtml"), + e => e.AssertSingleItem("CheckAndRewriteTagHelpersStop", "Views/Shared/_Layout.cshtml"), + e => e.AssertSingleItem("RazorCodeGenerateStart", "Pages/Index.cshtml"), + e => e.AssertSingleItem("RazorCodeGenerateStop", "Pages/Index.cshtml"), + e => e.AssertSingleItem("AddSyntaxTrees", "Pages_Index_cshtml.g.cs") + ); } [Fact] @@ -2567,5 +2490,60 @@ public async Task SourceGenerator_EmptyTargetPath(string targetPath) Assert.Empty(result.GeneratedSources); } + + [Fact] + public async Task SourceGenerator_Class_Inside_CodeBlock() + { + var project = CreateTestProject(new() + { + ["Component.Razor"] = +""" +

Hello world

+ +@code +{ + public class X {} +} +"""}); + + var compilation = await project.GetCompilationAsync(); + var driver = await GetDriverAsync(project); + + var result = RunGenerator(compilation!, ref driver).VerifyPageOutput( +@"#pragma checksum ""Component.Razor"" ""{ff1816ec-aa5e-4d10-87f7-6f4963833460}"" ""20b14071a74e1fd554d7b3dff6ff41722270ebee"" +// +#pragma warning disable 1591 +namespace MyApp +{ + #line hidden + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Components; + public partial class Component : global::Microsoft.AspNetCore.Components.ComponentBase + { + #pragma warning disable 1998 + protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) + { + __builder.AddMarkupContent(0, ""

Hello world

""); + } + #pragma warning restore 1998 +#nullable restore +#line 4 ""Component.Razor"" + + public class X {} + +#line default +#line hidden +#nullable disable + } +} +#pragma warning restore 1591 +"); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedSources); + } } } diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTestsBase.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTestsBase.cs index 9b00b6fab90..5c6945c03d6 100644 --- a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTestsBase.cs +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTestsBase.cs @@ -349,4 +349,11 @@ private static string TrimChecksum(string text) Assert.StartsWith("#pragma", trimmed); return trimmed.Substring(trimmed.IndexOf('\n') + 1); } + + public static void AssertSingleItem(this RazorEventListener.RazorEvent e, string expectedEventName, string expectedFileName) + { + Assert.Equal(expectedEventName, e.EventName); + var file = Assert.Single(e.Payload); + Assert.Equal(expectedFileName, file); + } }