| File: Language\CodeGeneration\IntermediateNodeWriter.cs | Web Access |
| Project: src\roslyn\src\Razor\src\Compiler\Microsoft.CodeAnalysis.Razor.Compiler\src\Microsoft.CodeAnalysis.Razor.Compiler.csproj (Microsoft.CodeAnalysis.Razor.Compiler) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #nullable disable using System; using System.Linq; using Microsoft.AspNetCore.Razor.Language.Intermediate; using Microsoft.AspNetCore.Razor.PooledObjects; namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration; public class IntermediateNodeWriter { public static readonly IntermediateNodeWriter Instance = new IntermediateNodeWriter(); public virtual string WriteCSharpExpressionMethod => "Write"; public virtual string WriteHtmlContentMethod => "WriteLiteral"; public virtual string BeginWriteAttributeMethod => "BeginWriteAttribute"; public virtual string EndWriteAttributeMethod => "EndWriteAttribute"; public virtual string WriteAttributeValueMethod => "WriteAttributeValue"; public virtual string PushWriterMethod => "PushWriter"; public virtual string PopWriterMethod => "PopWriter"; public const string TemplateTypeName = "Microsoft.AspNetCore.Mvc.Razor.HelperResult"; protected IntermediateNodeWriter() { } public virtual void WriteUsingDirective(CodeRenderingContext context, UsingDirectiveIntermediateNode node) { if (node.Source is { FilePath: not null } sourceSpan) { using (context.BuildEnhancedLinePragma(sourceSpan, suppressLineDefaultAndHidden: true)) { context.CodeWriter.WriteUsing(node.Content, endLine: node.HasExplicitSemicolon); } if (!node.HasExplicitSemicolon) { context.CodeWriter.WriteLine(";"); } if (node.AppendLineDefaultAndHidden) { context.CodeWriter.WriteLine("#line default"); context.CodeWriter.WriteLine("#line hidden"); } } else { context.CodeWriter.WriteUsing(node.Content); if (node.AppendLineDefaultAndHidden) { context.CodeWriter.WriteLine("#line default"); context.CodeWriter.WriteLine("#line hidden"); } } } public virtual void WriteCSharpExpression(CodeRenderingContext context, CSharpExpressionIntermediateNode node) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (node == null) { throw new ArgumentNullException(nameof(node)); } // Offset past the "Write(" prefix so the #line directive maps to the expression itself while // still wrapping the entire method invocation. Without this wrapping, the C# compiler cannot // emit a usable (non-hidden) sequence point for exceptions thrown from the expression, which // causes stack traces to report the last non-hidden location (typically an unrelated @{ } block) // instead of the actual expression line. See ComponentNodeWriter.WriteCSharpExpression // for the analogous pattern used by Razor components. var characterOffset = WriteCSharpExpressionMethod.Length + 1; // for '(' // Sequence points can only be emitted when the eval stack is empty. Map just the first C# child // by placing the pragma before the method invocation and offsetting it past "Write(". This mirrors // the approach in ComponentNodeWriter. It is not a perfect mapping, but generally works: // - Common case: there is only a single C# node, so it maps correctly. // - Error cases: there are no C# children, so no pragma is emitted. var firstCSharpChild = node.Children.OfType<CSharpIntermediateToken>().FirstOrDefault(); using (context.BuildEnhancedLinePragma(firstCSharpChild?.Source, characterOffset)) { context.CodeWriter.WriteStartMethodInvocation(WriteCSharpExpressionMethod); if (firstCSharpChild is not null) { context.CodeWriter.Write(firstCSharpChild.Content); } } // Render the remaining children. We still emit #line pragmas for the remaining C# tokens but // these won't actually generate any sequence points for debugging. foreach (var child in node.Children) { if (child == firstCSharpChild) { continue; } if (child is CSharpIntermediateToken csharpToken) { using (context.BuildEnhancedLinePragma(csharpToken.Source)) { context.CodeWriter.Write(csharpToken.Content); } } else { // There may be something else inside the expression like an extension node. context.RenderNode(child); } } context.CodeWriter.WriteEndMethodInvocation(); } public virtual void WriteCSharpCode(CodeRenderingContext context, CSharpCodeIntermediateNode node) { var isWhitespaceStatement = true; for (var i = 0; i < node.Children.Count; i++) { var token = node.Children[i] as IntermediateToken; if (token == null || !string.IsNullOrWhiteSpace(token.Content)) { isWhitespaceStatement = false; break; } } if (isWhitespaceStatement) { return; } WriteCSharpChildren(node.Children, context); context.CodeWriter.WriteLine(); } private static void WriteCSharpChildren(IntermediateNodeCollection children, CodeRenderingContext context) { for (var i = 0; i < children.Count; i++) { if (children[i] is CSharpIntermediateToken token) { using (context.BuildEnhancedLinePragma(token.Source)) { context.CodeWriter.Write(token.Content); } } else { // There may be something else inside the statement like an extension node. context.RenderNode(children[i]); } } } public virtual void WriteHtmlAttribute(CodeRenderingContext context, HtmlAttributeIntermediateNode node) { var valuePieceCount = node .Children .Count(child => child is HtmlAttributeValueIntermediateNode || child is CSharpExpressionAttributeValueIntermediateNode || child is CSharpCodeAttributeValueIntermediateNode || child is ExtensionIntermediateNode); var prefixLocation = node.Source.Value.AbsoluteIndex; var suffixLocation = node.Source.Value.AbsoluteIndex + node.Source.Value.Length - node.Suffix.Length; context.CodeWriter .WriteStartMethodInvocation(BeginWriteAttributeMethod) .WriteStringLiteral(node.AttributeName) .WriteParameterSeparator() .WriteStringLiteral(node.Prefix) .WriteParameterSeparator() .WriteIntegerLiteral(prefixLocation) .WriteParameterSeparator() .WriteStringLiteral(node.Suffix) .WriteParameterSeparator() .WriteIntegerLiteral(suffixLocation) .WriteParameterSeparator() .WriteIntegerLiteral(valuePieceCount) .WriteEndMethodInvocation(); context.RenderChildren(node); context.CodeWriter .WriteStartMethodInvocation(EndWriteAttributeMethod) .WriteEndMethodInvocation(); } public virtual void WriteHtmlAttributeValue(CodeRenderingContext context, HtmlAttributeValueIntermediateNode node) { var prefixLocation = node.Source.Value.AbsoluteIndex; var valueLocation = node.Source.Value.AbsoluteIndex + node.Prefix.Length; var valueLength = node.Source.Value.Length; context.CodeWriter .WriteStartMethodInvocation(WriteAttributeValueMethod) .WriteStringLiteral(node.Prefix) .WriteParameterSeparator() .WriteIntegerLiteral(prefixLocation) .WriteParameterSeparator(); // Write content for (var i = 0; i < node.Children.Count; i++) { if (node.Children[i] is HtmlIntermediateToken token) { context.CodeWriter.WriteStringLiteral(token.Content); } else { // There may be something else inside the attribute value like an extension node. context.RenderNode(node.Children[i]); } } context.CodeWriter .WriteParameterSeparator() .WriteIntegerLiteral(valueLocation) .WriteParameterSeparator() .WriteIntegerLiteral(valueLength) .WriteParameterSeparator() .WriteBooleanLiteral(true) .WriteEndMethodInvocation(); } public virtual void WriteCSharpExpressionAttributeValue(CodeRenderingContext context, CSharpExpressionAttributeValueIntermediateNode node) { var prefixLocation = node.Source.Value.AbsoluteIndex; context.CodeWriter .WriteStartMethodInvocation(WriteAttributeValueMethod) .WriteStringLiteral(node.Prefix) .WriteParameterSeparator() .WriteIntegerLiteral(prefixLocation) .WriteParameterSeparator(); WriteCSharpChildren(node.Children, context); var valueLocation = node.Source.Value.AbsoluteIndex + node.Prefix.Length; var valueLength = node.Source.Value.Length - node.Prefix.Length; context.CodeWriter .WriteParameterSeparator() .WriteIntegerLiteral(valueLocation) .WriteParameterSeparator() .WriteIntegerLiteral(valueLength) .WriteParameterSeparator() .WriteBooleanLiteral(false) .WriteEndMethodInvocation(); } public virtual void WriteCSharpCodeAttributeValue(CodeRenderingContext context, CSharpCodeAttributeValueIntermediateNode node) { const string ValueWriterName = "__razor_attribute_value_writer"; var prefixLocation = node.Source.Value.AbsoluteIndex; var valueLocation = node.Source.Value.AbsoluteIndex + node.Prefix.Length; var valueLength = node.Source.Value.Length - node.Prefix.Length; context.CodeWriter .WriteStartMethodInvocation(WriteAttributeValueMethod) .WriteStringLiteral(node.Prefix) .WriteParameterSeparator() .WriteIntegerLiteral(prefixLocation) .WriteParameterSeparator(); context.CodeWriter.WriteStartNewObject(TemplateTypeName); using (context.CodeWriter.BuildAsyncLambda(ValueWriterName)) { BeginWriterScope(context, ValueWriterName); WriteCSharpChildren(node.Children, context); EndWriterScope(context); } context.CodeWriter.WriteEndMethodInvocation(false); context.CodeWriter .WriteParameterSeparator() .WriteIntegerLiteral(valueLocation) .WriteParameterSeparator() .WriteIntegerLiteral(valueLength) .WriteParameterSeparator() .WriteBooleanLiteral(false) .WriteEndMethodInvocation(); } public virtual void WriteHtmlContent(CodeRenderingContext context, HtmlContentIntermediateNode node) { const int MaxStringLiteralLength = 1024; using var htmlContentBuilder = new PooledArrayBuilder<ReadOnlyMemory<char>>(); var length = 0; foreach (var child in node.Children) { if (child is HtmlIntermediateToken token) { var htmlContent = token.Content.AsMemory(); htmlContentBuilder.Add(htmlContent); length += htmlContent.Length; } } // Can't use a pooled builder here as the memory will be stored in the context. var content = new char[length]; var contentIndex = 0; foreach (var htmlContent in htmlContentBuilder) { htmlContent.Span.CopyTo(content.AsSpan(contentIndex)); contentIndex += htmlContent.Length; } WriteHtmlLiteral(context, MaxStringLiteralLength, content.AsMemory()); } // Internal for testing internal void WriteHtmlLiteral(CodeRenderingContext context, int maxStringLiteralLength, ReadOnlyMemory<char> literal) { while (literal.Length > maxStringLiteralLength) { // String is too large, render the string in pieces to avoid Roslyn OOM exceptions at compile time: https://github.com/aspnet/External/issues/54 var lastCharBeforeSplit = literal.Span[maxStringLiteralLength - 1]; // If character at splitting point is a high surrogate, take one less character this iteration // as we're attempting to split a surrogate pair. This can happen when something like an // emoji sits on the barrier between splits; if we were to split the emoji we'd end up with // invalid bytes in our output. var renderCharCount = char.IsHighSurrogate(lastCharBeforeSplit) ? maxStringLiteralLength - 1 : maxStringLiteralLength; WriteLiteral(literal[..renderCharCount]); literal = literal[renderCharCount..]; } WriteLiteral(literal); return; void WriteLiteral(ReadOnlyMemory<char> content) { context.CodeWriter .WriteStartMethodInvocation(WriteHtmlContentMethod) .WriteStringLiteral(content, context.Options.WriteHtmlUtf8StringLiterals) .WriteEndMethodInvocation(); } } public virtual void BeginWriterScope(CodeRenderingContext context, string writer) { context.CodeWriter.WriteMethodInvocation(PushWriterMethod, writer); } public virtual void EndWriterScope(CodeRenderingContext context) { context.CodeWriter.WriteMethodInvocation(PopWriterMethod); } public virtual void WriteComponent(CodeRenderingContext context, ComponentIntermediateNode node) { throw new NotSupportedException("This writer does not support components."); } public virtual void WriteComponentAttribute(CodeRenderingContext context, ComponentAttributeIntermediateNode node) { throw new NotSupportedException("This writer does not support components."); } public virtual void WriteComponentChildContent(CodeRenderingContext context, ComponentChildContentIntermediateNode node) { throw new NotSupportedException("This writer does not support components."); } public virtual void WriteComponentTypeArgument(CodeRenderingContext context, ComponentTypeArgumentIntermediateNode node) { throw new NotSupportedException("This writer does not support components."); } public virtual void WriteComponentTypeInferenceMethod(CodeRenderingContext context, ComponentTypeInferenceMethodIntermediateNode node) { throw new NotSupportedException("This writer does not support components."); } public virtual void WriteMarkupElement(CodeRenderingContext context, MarkupElementIntermediateNode node) { throw new NotSupportedException("This writer does not support components."); } public virtual void WriteMarkupBlock(CodeRenderingContext context, MarkupBlockIntermediateNode node) { throw new NotSupportedException("This writer does not support components."); } public virtual void WriteReferenceCapture(CodeRenderingContext context, ReferenceCaptureIntermediateNode node) { throw new NotSupportedException("This writer does not support components."); } public virtual void WriteSetKey(CodeRenderingContext context, SetKeyIntermediateNode node) { throw new NotSupportedException("This writer does not support components."); } public virtual void WriteSplat(CodeRenderingContext context, SplatIntermediateNode node) { throw new NotSupportedException("This writer does not support components."); } public virtual void WriteRenderMode(CodeRenderingContext context, RenderModeIntermediateNode node) { throw new NotSupportedException("This writer does not support components."); } public virtual void WriteFormName(CodeRenderingContext context, FormNameIntermediateNode node) { throw new NotSupportedException("This writer does not support components."); } protected static void RenderCSharpCode(CodeRenderingContext context, CSharpCodeIntermediateNode node) { var writer = context.CodeWriter; foreach (var child in node.Children) { if (child is CSharpIntermediateToken token) { context.AddSourceMappingFor(token); writer.Write(token.Content); } else { // There may be something else inside the expression like an extension node. context.RenderNode(child); } } } }