* Revert "Perf/generator (#8212)"

This reverts commit 4ab512e7f0.

* Add more tests
This commit is contained in:
Jan Jones 2023-05-22 18:07:23 +02:00 коммит произвёл GitHub
Родитель 37f2727000
Коммит a6a61fdfa7
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
22 изменённых файлов: 712 добавлений и 602 удалений

Просмотреть файл

@ -17,7 +17,7 @@ internal sealed class DefaultRazorTagHelperContextDiscoveryPhase : RazorEnginePh
{
protected override void ExecuteCore(RazorCodeDocument codeDocument)
{
var syntaxTree = codeDocument.GetPreTagHelperSyntaxTree() ?? codeDocument.GetSyntaxTree();
var syntaxTree = codeDocument.GetSyntaxTree();
ThrowForMissingDocumentDependency(syntaxTree);
var descriptors = codeDocument.GetTagHelpers();
@ -69,6 +69,7 @@ internal sealed class DefaultRazorTagHelperContextDiscoveryPhase : RazorEnginePh
var context = TagHelperDocumentContext.Create(tagHelperPrefix, descriptors);
codeDocument.SetTagHelperContext(context);
codeDocument.SetPreTagHelperSyntaxTree(syntaxTree);
}
private static bool MatchesDirective(TagHelperDescriptor descriptor, string typePattern, string assemblyName)

Просмотреть файл

@ -2,7 +2,6 @@
// 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;
@ -10,19 +9,17 @@ internal sealed class DefaultRazorTagHelperRewritePhase : RazorEnginePhaseBase
{
protected override void ExecuteCore(RazorCodeDocument codeDocument)
{
var syntaxTree = codeDocument.GetPreTagHelperSyntaxTree() ?? codeDocument.GetSyntaxTree();
ThrowForMissingDocumentDependency(syntaxTree);
var syntaxTree = codeDocument.GetPreTagHelperSyntaxTree();
var context = codeDocument.GetTagHelperContext();
if (context?.TagHelpers.Count > 0)
if (syntaxTree is null || context.TagHelpers.Count == 0)
{
var rewrittenSyntaxTree = TagHelperParseTreeRewriter.Rewrite(syntaxTree, context.Prefix, context.TagHelpers, out var usedHelpers);
codeDocument.SetSyntaxTree(rewrittenSyntaxTree);
codeDocument.SetReferencedTagHelpers(usedHelpers);
}
else
{
codeDocument.SetReferencedTagHelpers(new HashSet<TagHelperDescriptor>());
// No descriptors, no-op.
return;
}
var rewrittenSyntaxTree = TagHelperParseTreeRewriter.Rewrite(syntaxTree, context.Prefix, context.TagHelpers, out var usedHelpers);
codeDocument.SetReferencedTagHelpers(usedHelpers);
codeDocument.SetSyntaxTree(rewrittenSyntaxTree);
}
}

Просмотреть файл

@ -19,7 +19,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Shared\Microsoft.AspNetCore.Razor.Utilities.Shared\Microsoft.AspNetCore.Razor.Utilities.Shared.csproj" />
<ProjectReference Include="..\Microsoft.AspNetCore.Mvc.Razor.Extensions\src\Microsoft.AspNetCore.Mvc.Razor.Extensions.csproj" />
<ProjectReference Include="..\Microsoft.AspNetCore.Razor.Language\src\Microsoft.AspNetCore.Razor.Language.csproj" />
<ProjectReference Include="..\Microsoft.CodeAnalysis.Razor\src\Microsoft.CodeAnalysis.Razor.csproj" />

Просмотреть файл

@ -83,7 +83,8 @@ namespace Microsoft.NET.Sdk.Razor.SourceGenerators
return discoveryProjectEngine;
}
private static SourceGeneratorProjectEngine GetGenerationProjectEngine(
private static RazorProjectEngine GetGenerationProjectEngine(
IReadOnlyList<TagHelperDescriptor> tagHelpers,
SourceGeneratorProjectItem item,
IEnumerable<SourceGeneratorProjectItem> imports,
RazorSourceGenerationOptions razorSourceGeneratorOptions)
@ -95,7 +96,7 @@ namespace Microsoft.NET.Sdk.Razor.SourceGenerators
fileSystem.Add(import);
}
var projectEngine = (DefaultRazorProjectEngine)RazorProjectEngine.Create(razorSourceGeneratorOptions.Configuration, fileSystem, b =>
var projectEngine = RazorProjectEngine.Create(razorSourceGeneratorOptions.Configuration, fileSystem, b =>
{
b.Features.Add(new DefaultTypeNameFeature());
b.SetRootNamespace(razorSourceGeneratorOptions.RootNamespace);
@ -106,13 +107,16 @@ namespace Microsoft.NET.Sdk.Razor.SourceGenerators
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 new SourceGeneratorProjectEngine(projectEngine);
return projectEngine;
}
}
}

Просмотреть файл

@ -2,15 +2,13 @@
// 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
{
@ -68,12 +66,11 @@ namespace Microsoft.NET.Sdk.Razor.SourceGenerators
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.RelativePhysicalPath);
RazorSourceGeneratorEventSource.Log.GenerateDeclarationCodeStart(sourceItem.FilePath);
var projectEngine = GetDeclarationProjectEngine(sourceItem, importFiles, razorSourceGeneratorOptions);
@ -81,69 +78,57 @@ namespace Microsoft.NET.Sdk.Razor.SourceGenerators
var result = codeGen.GetCSharpDocument().GeneratedCode;
RazorSourceGeneratorEventSource.Log.GenerateDeclarationCodeStop(sourceItem.RelativePhysicalPath);
RazorSourceGeneratorEventSource.Log.GenerateDeclarationCodeStop(sourceItem.FilePath);
return (result, sourceItem.RelativePhysicalPath);
return result;
});
var generatedDeclarationSyntaxTrees = generatedDeclarationCode
.Combine(parseOptions)
.Select(static (pair, ct) =>
.Select(static (pair, _) =>
{
var ((generatedDeclarationCode, filePath), parseOptions) = pair;
return CSharpSyntaxTree.ParseText(generatedDeclarationCode, (CSharpParseOptions)parseOptions, filePath, cancellationToken: ct);
});
var tagHelpersFromComponents = generatedDeclarationSyntaxTrees
.Combine(compilation)
.Combine(razorSourceGeneratorOptions)
.SelectMany(static (pair, ct) =>
{
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(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 = targetSymbol;
var result = tagHelperFeature.GetDescriptors();
RazorSourceGeneratorEventSource.Log.DiscoverTagHelpersFromComponentStop(generatedDeclarationSyntaxTree.FilePath);
return result;
var (generatedDeclarationCode, parseOptions) = pair;
return CSharpSyntaxTree.ParseText(generatedDeclarationCode, (CSharpParseOptions)parseOptions);
});
var tagHelpersFromCompilation = compilation
.Combine(generatedDeclarationSyntaxTrees.Collect())
.Combine(razorSourceGeneratorOptions)
.Select(static (pair, _) =>
{
RazorSourceGeneratorEventSource.Log.DiscoverTagHelpersFromCompilationStart();
var (compilation, razorSourceGeneratorOptions) = pair;
var ((compilation, generatedDeclarationSyntaxTrees), razorSourceGeneratorOptions) = pair;
var tagHelperFeature = new StaticCompilationTagHelperFeature();
var discoveryProjectEngine = GetDiscoveryProjectEngine(compilation.References.ToImmutableArray(), tagHelperFeature);
tagHelperFeature.Compilation = compilation;
tagHelperFeature.TargetSymbol = compilation.Assembly;
var compilationWithDeclarations = compilation.AddSyntaxTrees(generatedDeclarationSyntaxTrees);
var result = tagHelperFeature.GetDescriptors();
tagHelperFeature.Compilation = compilationWithDeclarations;
tagHelperFeature.TargetSymbol = compilationWithDeclarations.Assembly;
var result = (IList<TagHelperDescriptor>)tagHelperFeature.GetDescriptors();
RazorSourceGeneratorEventSource.Log.DiscoverTagHelpersFromCompilationStop();
return result;
});
})
.WithLambdaComparer(static (a, b) =>
{
if (a.Count != b.Count)
{
return false;
}
for (var i = 0; i < a.Count; i++)
{
if (!a[i].Equals(b[i]))
{
return false;
}
}
return true;
}, getHashCode: static a => a.Count);
var tagHelpersFromReferences = compilation
.Combine(razorSourceGeneratorOptions)
@ -186,7 +171,7 @@ namespace Microsoft.NET.Sdk.Razor.SourceGenerators
var tagHelperFeature = new StaticCompilationTagHelperFeature();
var discoveryProjectEngine = GetDiscoveryProjectEngine(compilation.References.ToImmutableArray(), tagHelperFeature);
using var pool = ArrayBuilderPool<TagHelperDescriptor>.GetPooledObject(out var descriptors);
List<TagHelperDescriptor> descriptors = new();
tagHelperFeature.Compilation = compilation;
foreach (var reference in compilation.References)
{
@ -198,84 +183,47 @@ namespace Microsoft.NET.Sdk.Razor.SourceGenerators
}
RazorSourceGeneratorEventSource.Log.DiscoverTagHelpersFromReferencesStop();
return descriptors.ToImmutable();
return (ICollection<TagHelperDescriptor>)descriptors;
});
var allTagHelpers = tagHelpersFromComponents.Collect()
.Combine(tagHelpersFromCompilation)
var allTagHelpers = tagHelpersFromCompilation
.Combine(tagHelpersFromReferences)
.Select(static (pair, _) =>
{
var ((tagHelpersFromComponents, tagHelpersFromCompilation), tagHelpersFromReferences) = pair;
var count = tagHelpersFromCompilation.Length + tagHelpersFromReferences.Length + tagHelpersFromComponents.Length;
var (tagHelpersFromCompilation, tagHelpersFromReferences) = pair;
var count = tagHelpersFromCompilation.Count + tagHelpersFromReferences.Count;
if (count == 0)
{
return ImmutableArray<TagHelperDescriptor>.Empty;
return Array.Empty<TagHelperDescriptor>();
}
using var pool = ArrayBuilderPool<TagHelperDescriptor>.GetPooledObject(out var allTagHelpers);
allTagHelpers.AddRange(tagHelpersFromCompilation);
allTagHelpers.AddRange(tagHelpersFromReferences);
allTagHelpers.AddRange(tagHelpersFromComponents);
var allTagHelpers = new TagHelperDescriptor[count];
tagHelpersFromCompilation.CopyTo(allTagHelpers, 0);
tagHelpersFromReferences.CopyTo(allTagHelpers, tagHelpersFromCompilation.Count);
return allTagHelpers.ToImmutable();
return allTagHelpers;
});
var generatedOutput = sourceItems
.Combine(importFiles.Collect())
.WithLambdaComparer((old, @new) => old.Left.Equals(@new.Left) && old.Right.SequenceEqual(@new.Right), (a) => a.GetHashCode())
.Combine(allTagHelpers)
.Combine(razorSourceGeneratorOptions)
.Select(static (pair, _) =>
{
var ((sourceItem, imports), razorSourceGeneratorOptions) = pair;
var (((sourceItem, imports), allTagHelpers), razorSourceGeneratorOptions) = pair;
RazorSourceGeneratorEventSource.Log.ParseRazorDocumentStart(sourceItem.RelativePhysicalPath);
RazorSourceGeneratorEventSource.Log.RazorCodeGenerateStart(sourceItem.FilePath);
var projectEngine = GetGenerationProjectEngine(sourceItem, imports, razorSourceGeneratorOptions);
// Add a generated suffix so tools, such as coverlet, consider the file to be generated
var hintName = GetIdentifierFromPath(sourceItem.RelativePhysicalPath) + ".g.cs";
var document = projectEngine.ProcessInitialParse(sourceItem);
var projectEngine = GetGenerationProjectEngine(allTagHelpers, sourceItem, imports, razorSourceGeneratorOptions);
RazorSourceGeneratorEventSource.Log.ParseRazorDocumentStop(sourceItem.RelativePhysicalPath);
return (projectEngine, sourceItem.RelativePhysicalPath, document);
})
var codeDocument = projectEngine.Process(sourceItem);
var csharpDocument = codeDocument.GetCSharpDocument();
// 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);
codeDocument = projectEngine.ProcessTagHelpers(codeDocument, allTagHelpers, checkForIdempotency: false);
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, filePath, document), allTagHelpers) = pair;
RazorSourceGeneratorEventSource.Log.CheckAndRewriteTagHelpersStart(filePath);
document = projectEngine.ProcessTagHelpers(document, allTagHelpers, checkForIdempotency: true);
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);
RazorSourceGeneratorEventSource.Log.RazorCodeGenerateStop(sourceItem.FilePath);
return (hintName, csharpDocument);
})
.WithLambdaComparer(static (a, b) =>
{
@ -290,11 +238,7 @@ namespace Microsoft.NET.Sdk.Razor.SourceGenerators
context.RegisterSourceOutput(generatedOutput, static (context, 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";
var (hintName, csharpDocument) = pair;
RazorSourceGeneratorEventSource.Log.AddSyntaxTrees(hintName);
for (var i = 0; i < csharpDocument.Diagnostics.Count; i++)
{

Просмотреть файл

@ -59,37 +59,5 @@ namespace Microsoft.NET.Sdk.Razor.SourceGenerators
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);
}
}

Просмотреть файл

@ -1,115 +0,0 @@
// 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<TagHelperDescriptor> 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);
}
}
}

Просмотреть файл

@ -1,31 +0,0 @@
// 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;
/// <summary>
/// A wrapper for <see cref="RazorCodeDocument"/>
/// </summary>
/// <remarks>
/// The razor compiler modifies the <see cref="RazorCodeDocument"/> 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 <see cref="RazorCodeDocument"/>
/// which can get expensive when the <see cref="ItemCollection"/> 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.
/// </remarks>
internal class SourceGeneratorRazorCodeDocument
{
public RazorCodeDocument CodeDocument { get; }
public SourceGeneratorRazorCodeDocument(RazorCodeDocument razorCodeDocument)
{
this.CodeDocument = razorCodeDocument;
}
}

Просмотреть файл

@ -2,10 +2,8 @@
// 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;
@ -13,16 +11,18 @@ namespace Microsoft.NET.Sdk.Razor.SourceGenerators
{
internal sealed class StaticCompilationTagHelperFeature : RazorEngineFeatureBase, ITagHelperFeature
{
private static readonly List<TagHelperDescriptor> EmptyList = new();
private ITagHelperDescriptorProvider[]? _providers;
public ImmutableArray<TagHelperDescriptor> GetDescriptors()
public List<TagHelperDescriptor> GetDescriptors()
{
if (Compilation is null)
{
return ImmutableArray<TagHelperDescriptor>.Empty;
return EmptyList;
}
using var pool = ArrayBuilderPool<TagHelperDescriptor>.GetPooledObject(out var results);
var results = new List<TagHelperDescriptor>();
var context = TagHelperDescriptorProviderContext.Create(results);
context.SetCompilation(Compilation);
if (TargetSymbol is not null)
@ -35,7 +35,7 @@ namespace Microsoft.NET.Sdk.Razor.SourceGenerators
_providers[i].Execute(context);
}
return results.ToImmutable();
return results;
}
IReadOnlyList<TagHelperDescriptor> ITagHelperFeature.GetDescriptors() => GetDescriptors();

Просмотреть файл

@ -0,0 +1,25 @@
// 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<TagHelperDescriptor> TagHelpers { get; set; }
public IReadOnlyList<TagHelperDescriptor> GetDescriptors() => TagHelpers;
public StaticTagHelperFeature()
{
TagHelpers = new List<TagHelperDescriptor>();
}
public StaticTagHelperFeature(IEnumerable<TagHelperDescriptor> tagHelpers)
{
TagHelpers = new List<TagHelperDescriptor>(tagHelpers);
}
}
}

Просмотреть файл

@ -0,0 +1,141 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Test.Common;
using Xunit;
namespace Microsoft.NET.Sdk.Razor.SourceGenerators;
public sealed class RazorSourceGeneratorComponentTests : RazorSourceGeneratorTestsBase
{
[Fact, WorkItem("https://github.com/dotnet/razor/issues/8718")]
public async Task PartialClass()
{
// Arrange
var project = CreateTestProject(new()
{
["Views/Home/Index.cshtml"] = """
@(await Html.RenderComponentAsync<MyApp.Shared.Component1>(RenderMode.Static))
""",
["Shared/Component1.razor"] = """
<Component2 Param="42" />
""",
["Shared/Component2.razor"] = """
@inherits ComponentBase
Value: @(Param + 1)
@code {
[Parameter]
public int Param { get; set; }
}
"""
}, new()
{
["Component2.razor.cs"] = """
using Microsoft.AspNetCore.Components;
namespace MyApp.Shared;
public partial class Component2 : ComponentBase { }
"""
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project, options =>
{
options.TestGlobalOptions["build_property.RazorLangVersion"] = "7.0";
});
// Act
var result = RunGenerator(compilation!, ref driver, out compilation);
// Assert
Assert.Empty(result.Diagnostics);
Assert.Equal(3, result.GeneratedSources.Length);
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}
[Fact, WorkItem("https://github.com/dotnet/razor/issues/8718")]
public async Task PartialClass_NoBaseInCSharp()
{
// Arrange
var project = CreateTestProject(new()
{
["Views/Home/Index.cshtml"] = """
@(await Html.RenderComponentAsync<MyApp.Shared.Component1>(RenderMode.Static))
""",
["Shared/Component1.razor"] = """
<Component2 Param="42" />
""",
["Shared/Component2.razor"] = """
@inherits ComponentBase
Value: @(Param + 1)
@code {
[Parameter]
public int Param { get; set; }
}
"""
}, new()
{
["Component2.razor.cs"] = """
using Microsoft.AspNetCore.Components;
namespace MyApp.Shared;
public partial class Component2 { }
"""
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project, options =>
{
options.TestGlobalOptions["build_property.RazorLangVersion"] = "7.0";
});
// Act
var result = RunGenerator(compilation!, ref driver, out compilation);
// Assert
Assert.Empty(result.Diagnostics);
Assert.Equal(3, result.GeneratedSources.Length);
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}
[Fact, WorkItem("https://github.com/dotnet/razor/issues/8718")]
public async Task ComponentInheritsFromComponent()
{
// Arrange
var project = CreateTestProject(new()
{
["Views/Home/Index.cshtml"] = """
@(await Html.RenderComponentAsync<MyApp.Shared.Component1>(RenderMode.Static))
""",
["Shared/Component1.razor"] = """
Hello from Component1
<DerivedComponent />
""",
["Shared/BaseComponent.razor"] = """
Hello from Base
""",
["Shared/DerivedComponent.razor"] = """
@inherits BaseComponent
Hello from Derived
"""
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project, options =>
{
options.TestGlobalOptions["build_property.RazorLangVersion"] = "7.0";
});
// Act
var result = RunGenerator(compilation!, ref driver, out compilation);
// Assert
Assert.Empty(result.Diagnostics);
Assert.Equal(4, result.GeneratedSources.Length);
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}
}

Просмотреть файл

@ -5,6 +5,7 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Test.Common;
using Xunit;
namespace Microsoft.NET.Sdk.Razor.SourceGenerators;
@ -88,4 +89,107 @@ public sealed class RazorSourceGeneratorTagHelperTests : RazorSourceGeneratorTes
result.VerifyOutputsMatchBaseline();
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}
[Fact, WorkItem("https://github.com/dotnet/razor/issues/8718")]
public async Task ComponentAndTagHelper()
{
// Arrange
var project = CreateTestProject(new()
{
["Views/Home/Index.cshtml"] = """
@addTagHelper *, TestProject
<email mail="example">custom tag helper</email>
""",
["Shared/EmailTagHelper.razor"] = """
@inherits ComponentAndTagHelper
@code {
public string? Mail { get; set; }
}
""",
}, new()
{
["EmailTagHelper.cs"] = """
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace MyApp.Shared;
public abstract class ComponentAndTagHelper : TagHelper
{
protected abstract void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder);
}
public partial class EmailTagHelper : ComponentAndTagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a";
output.Attributes.SetAttribute("href", $"mailto:{Mail}");
}
}
"""
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project);
// Act
var result = RunGenerator(compilation!, ref driver, out compilation);
// Assert
Assert.Empty(result.Diagnostics);
Assert.Equal(2, result.GeneratedSources.Length);
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}
[Fact, WorkItem("https://github.com/dotnet/razor/issues/8718")]
public async Task ComponentAndTagHelper_HtmlTargetElement()
{
// Arrange
var project = CreateTestProject(new()
{
["Views/Home/Index.cshtml"] = """
@addTagHelper *, TestProject
<email mail="example1">inside email</email>
<mail mail="example2">inside mail</mail>
""",
["Shared/EmailTagHelper.razor"] = """
@using Microsoft.AspNetCore.Razor.TagHelpers;
@attribute [HtmlTargetElement("mail")]
@inherits ComponentAndTagHelper
@code {
public string? Mail { get; set; }
}
""",
}, new()
{
["EmailTagHelper.cs"] = """
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace MyApp.Shared;
public abstract class ComponentAndTagHelper : TagHelper
{
protected abstract void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder);
}
public partial class EmailTagHelper : ComponentAndTagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a";
output.Attributes.SetAttribute("href", $"mailto:{Mail}");
}
}
"""
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project);
// Act
var result = RunGenerator(compilation!, ref driver, out compilation);
// Assert
Assert.Empty(result.Diagnostics);
Assert.Equal(2, result.GeneratedSources.Length);
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}
}

Просмотреть файл

@ -7,6 +7,7 @@ using System;
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;
@ -60,6 +61,42 @@ namespace MyApp.Pages
Assert.Single(result.GeneratedSources);
}
[Fact, WorkItem("https://github.com/dotnet/razor/issues/8610")]
public async Task SourceGenerator_RazorFiles_UsingAlias_NestedClass()
{
// Arrange
var project = CreateTestProject(new()
{
["Pages/Index.razor"] = """
@code {
public class MyModel { }
}
""",
["Shared/MyComponent.razor"] = """
@using MyAlias = Pages.Index.MyModel;
<MyComponent Data="@Data" />
@code {
[Parameter]
public MyAlias? Data { get; set; }
}
""",
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project, options =>
{
options.TestGlobalOptions["build_property.RazorLangVersion"] = "7.0";
});
// Act
var result = RunGenerator(compilation!, ref driver);
// Assert
Assert.Empty(result.Diagnostics);
Assert.Equal(2, result.GeneratedSources.Length);
}
[Fact]
public async Task SourceGeneratorEvents_RazorFiles_Works()
{
@ -127,41 +164,74 @@ namespace MyApp.Pages
Assert.Collection(eventListener.Events,
e => Assert.Equal("ComputeRazorSourceGeneratorOptions", e.EventName),
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("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 => Assert.Equal("DiscoverTagHelpersFromCompilationStart", e.EventName),
e => Assert.Equal("DiscoverTagHelpersFromCompilationStop", e.EventName),
e => Assert.Equal("DiscoverTagHelpersFromReferencesStart", e.EventName),
e => Assert.Equal("DiscoverTagHelpersFromReferencesStop", e.EventName),
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")
);
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);
});
}
[Fact]
public async Task IncrementalCompilation_DoesNotReExecuteSteps_WhenRazorFilesAreUnchanged()
public async Task IncrementalCompilation_DoesNotReexecuteSteps_WhenRazorFilesAreUnchanged()
{
// Arrange
using var eventListener = new RazorEventListener();
@ -343,18 +413,36 @@ namespace MyApp.Pages
Assert.Equal(2, result.GeneratedSources.Length);
Assert.Collection(eventListener.Events,
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")
);
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);
});
}
[Fact]
@ -447,13 +535,8 @@ 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("DiscoverTagHelpersFromCompilationStart", e.EventName),
e => Assert.Equal("DiscoverTagHelpersFromCompilationStop", e.EventName));
}
[Fact]
@ -551,8 +634,6 @@ public class Person
}", Encoding.UTF8)).Project;
compilation = await project.GetCompilationAsync();
eventListener.Events.Clear();
result = RunGenerator(compilation!, ref driver, expectedDiagnostics)
.VerifyOutputsMatch(result);
@ -560,13 +641,8 @@ 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]
@ -711,20 +787,38 @@ __builder.AddContent(3, count);
Assert.Equal(2, result.GeneratedSources.Length);
Assert.Collection(eventListener.Events,
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")
);
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);
});
}
[Fact]
@ -873,24 +967,50 @@ __builder.AddContent(3, count);
Assert.Equal(2, result.GeneratedSources.Length);
Assert.Collection(eventListener.Events,
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")
);
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);
});
}
[Fact]
@ -1021,24 +1141,40 @@ using SurveyPromptRootNamspace;
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 => 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 =>
{
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);
});
// Verify caching
eventListener.Events.Clear();
@ -1190,29 +1326,46 @@ namespace AspNetCoreGeneratedDocument
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 => 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")
);
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);
});
}
[Fact]
@ -1404,16 +1557,24 @@ namespace AspNetCoreGeneratedDocument
Assert.Equal(2, result.GeneratedSources.Length);
Assert.Collection(eventListener.Events,
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")
);
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);
});
}
[Fact]
@ -1572,8 +1733,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]
@ -1723,92 +1884,7 @@ public class HeaderTagHelper : TagHelper
}", Encoding.UTF8)).Project;
compilation = await project.GetCompilationAsync();
result = RunGenerator(compilation!, ref driver)
.VerifyOutputsMatch(result, (0, @"
#pragma checksum ""Pages/Index.cshtml"" ""{ff1816ec-aa5e-4d10-87f7-6f4963833460}"" ""5d59ecd7b7cf7355d7f60234988be34b81a8b614""
// <auto-generated/>
#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<dynamic>
#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<global::MyApp.HeaderTagHelper>();
__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<dynamic> Html { get; private set; } = default!;
#nullable disable
}
}
#pragma warning restore 1591
"));
result = RunGenerator(compilation!, ref driver);
Assert.Empty(result.Diagnostics);
Assert.Equal(2, result.GeneratedSources.Length);
@ -1816,14 +1892,36 @@ namespace AspNetCoreGeneratedDocument
Assert.Collection(eventListener.Events,
e => Assert.Equal("DiscoverTagHelpersFromCompilationStart", e.EventName),
e => Assert.Equal("DiscoverTagHelpersFromCompilationStop", e.EventName),
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")
);
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);
});
}
[Fact]
@ -2490,60 +2588,5 @@ namespace MyApp
Assert.Empty(result.GeneratedSources);
}
[Fact]
public async Task SourceGenerator_Class_Inside_CodeBlock()
{
var project = CreateTestProject(new()
{
["Component.Razor"] =
"""
<h1>Hello world</h1>
@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""
// <auto-generated/>
#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, ""<h1>Hello world</h1>"");
}
#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);
}
}
}

Просмотреть файл

@ -18,8 +18,10 @@ using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
@ -43,9 +45,9 @@ namespace Microsoft.NET.Sdk.Razor.SourceGenerators;
[Collection(nameof(RazorSourceGenerator))]
public abstract class RazorSourceGeneratorTestsBase
{
protected static async ValueTask<GeneratorDriver> GetDriverAsync(Project project)
protected static async ValueTask<GeneratorDriver> GetDriverAsync(Project project, Action<TestAnalyzerConfigOptionsProvider>? configureGlobalOptions = null)
{
var (driver, _) = await GetDriverWithAdditionalTextAsync(project);
var (driver, _) = await GetDriverWithAdditionalTextAsync(project, configureGlobalOptions);
return driver;
}
@ -134,12 +136,27 @@ public abstract class RazorSourceGeneratorTestsBase
// Create ViewContext.
var appBuilder = WebApplication.CreateBuilder();
appBuilder.Services.AddMvc().AddApplicationPart(assembly);
appBuilder.Services.AddMvc().ConfigureApplicationPartManager(manager =>
{
var partFactory = new ConsolidatedAssemblyApplicationPartFactory();
foreach (var applicationPart in partFactory.GetApplicationParts(assembly))
{
manager.ApplicationParts.Add(applicationPart);
}
});
var app = appBuilder.Build();
var httpContext = new DefaultHttpContext
{
RequestServices = app.Services
};
var requestFeature = new HttpRequestFeature
{
Method = HttpMethods.Get,
Protocol = HttpProtocol.Http2,
Scheme = "http"
};
requestFeature.Headers.Host = "localhost";
httpContext.Features.Set<IHttpRequestFeature>(requestFeature);
var actionContext = new ActionContext(
httpContext,
new AspNetCore.Routing.RouteData(),
@ -158,7 +175,10 @@ public abstract class RazorSourceGeneratorTestsBase
page.HtmlEncoder = HtmlEncoder.Default;
// Render the page.
await page.ExecuteAsync();
var view = ActivatorUtilities.CreateInstance<RazorView>(app.Services,
/* IReadOnlyList<IRazorPage> viewStartPages */ Array.Empty<IRazorPage>(),
/* IRazorPage razorPage */ page);
await view.RenderAsync(viewContext);
assemblyLoadContext.Unload();
@ -469,11 +489,4 @@ internal static class Extensions
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);
}
}

Просмотреть файл

@ -0,0 +1,2 @@
Hello from Component1
Hello from Derived

Просмотреть файл

@ -0,0 +1,2 @@
Hello from Component1
Hello from Derived

Просмотреть файл

@ -0,0 +1,2 @@

Value: 43

Просмотреть файл

@ -0,0 +1,2 @@

<a href="mailto:example">custom tag helper</a>

Просмотреть файл

@ -0,0 +1,3 @@

<email mail="example1">inside email</email>
<a href="mailto:example2">inside mail</a>