// 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; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using Microsoft.AspNetCore.Razor.Evolution.Intermediate; using Microsoft.AspNetCore.Razor.Evolution.Legacy; namespace Microsoft.AspNetCore.Razor.Evolution { internal class DefaultRazorRuntimeCSharpLoweringPhase : RazorEnginePhaseBase, IRazorCSharpLoweringPhase { protected override void ExecuteCore(RazorCodeDocument codeDocument) { var irDocument = codeDocument.GetIRDocument(); ThrowForMissingDependency(irDocument); var syntaxTree = codeDocument.GetSyntaxTree(); ThrowForMissingDependency(syntaxTree); var renderingContext = new CSharpRenderingContext() { Writer = new CSharpCodeWriter(), SourceDocument = codeDocument.Source, Options = syntaxTree.Options, }; var visitor = new CSharpRenderer(renderingContext); visitor.VisitDefault(irDocument); var csharpDocument = new RazorCSharpDocument() { GeneratedCode = renderingContext.Writer.GenerateCode(), LineMappings = renderingContext.Writer.LineMappingManager.Mappings, }; codeDocument.SetCSharpDocument(csharpDocument); } public class CSharpRedirectRenderingConventions : CSharpRenderingConventions { private readonly string _redirectWriter; public CSharpRedirectRenderingConventions(string redirectWriter, CSharpCodeWriter writer) : base(writer) { _redirectWriter = redirectWriter; } public override string StartWriteMethod => "WriteTo(" + _redirectWriter + ", " /* ORIGINAL: WriteToMethodName */; public override string StartWriteLiteralMethod => "WriteLiteralTo(" + _redirectWriter + ", " /* ORIGINAL: WriteLiteralToMethodName */; public override string StartBeginWriteAttributeMethod => "BeginWriteAttributeTo(" + _redirectWriter + ", " /* ORIGINAL: BeginWriteAttributeToMethodName */; public override string StartWriteAttributeValueMethod => "WriteAttributeValueTo(" + _redirectWriter + ", " /* ORIGINAL: WriteAttributeValueToMethodName */; public override string StartEndWriteAttributeMethod => "EndWriteAttributeTo(" + _redirectWriter /* ORIGINAL: EndWriteAttributeToMethodName */; } public class CSharpRenderingConventions { public CSharpRenderingConventions(CSharpCodeWriter writer) { Writer = writer; } protected CSharpCodeWriter Writer { get; } public virtual string StartWriteMethod => "Write(" /* ORIGINAL: WriteMethodName */; public virtual string StartWriteLiteralMethod => "WriteLiteral(" /* ORIGINAL: WriteLiteralMethodName */; public virtual string StartBeginWriteAttributeMethod => "BeginWriteAttribute(" /* ORIGINAL: BeginWriteAttributeMethodName */; public virtual string StartWriteAttributeValueMethod => "WriteAttributeValue(" /* ORIGINAL: WriteAttributeValueMethodName */; public virtual string StartEndWriteAttributeMethod => "EndWriteAttribute(" /* ORIGINAL: EndWriteAttributeMethodName */; } public class CSharpRenderingContext { private CSharpRenderingConventions _renderingConventions; public ICollection Directives { get; set; } public CSharpCodeWriter Writer { get; set; } public CSharpRenderingConventions RenderingConventions { get { if (_renderingConventions == null) { _renderingConventions = new CSharpRenderingConventions(Writer); } return _renderingConventions; } set { _renderingConventions = value; } } public ICollection Errors { get; } = new List(); public RazorSourceDocument SourceDocument { get; set; } public RazorParserOptions Options { get; set; } } public class LinePragmaWriter : IDisposable { private readonly CSharpCodeWriter _writer; private readonly int _startIndent; public LinePragmaWriter(CSharpCodeWriter writer, MappingLocation documentLocation) { if (writer == null) { throw new ArgumentNullException(nameof(writer)); } _writer = writer; _startIndent = _writer.CurrentIndent; _writer.ResetIndent(); _writer.WriteLineNumberDirective(documentLocation, documentLocation.FilePath); } public void Dispose() { // Need to add an additional line at the end IF there wasn't one already written. // This is needed to work with the C# editor's handling of #line ... var builder = _writer.Builder; var endsWithNewline = builder.Length > 0 && builder[builder.Length - 1] == '\n'; // Always write at least 1 empty line to potentially separate code from pragmas. _writer.WriteLine(); // Check if the previous empty line wasn't enough to separate code from pragmas. if (!endsWithNewline) { _writer.WriteLine(); } _writer .WriteLineDefaultDirective() .WriteLineHiddenDirective() .SetIndent(_startIndent); } } public class PageStructureCSharpRenderer : RazorIRNodeWalker { protected readonly CSharpRenderingContext Context; public PageStructureCSharpRenderer(CSharpRenderingContext context) { Context = context; } public override void VisitNamespace(NamespaceDeclarationIRNode node) { Context.Writer .Write("namespace ") .WriteLine(node.Content); using (Context.Writer.BuildScope()) { Context.Writer.WriteLineHiddenDirective(); VisitDefault(node); } } public override void VisitRazorMethodDeclaration(RazorMethodDeclarationIRNode node) { Context.Writer .WriteLine("#pragma warning disable 1998") .Write(node.AccessModifier) .Write(" "); if (node.Modifiers != null) { for (var i = 0; i < node.Modifiers.Count; i++) { Context.Writer.Write(node.Modifiers[i]); if (i + 1 < node.Modifiers.Count) { Context.Writer.Write(" "); } } } Context.Writer .Write(" ") .Write(node.ReturnType) .Write(" ") .Write(node.Name) .WriteLine("()"); using (Context.Writer.BuildScope()) { VisitDefault(node); } Context.Writer.WriteLine("#pragma warning restore 1998"); } public override void VisitClass(ClassDeclarationIRNode node) { Context.Writer .Write(node.AccessModifier) .Write(" class ") .Write(node.Name); if (node.BaseType != null || node.Interfaces != null) { Context.Writer.Write(" : "); } if (node.BaseType != null) { Context.Writer.Write(node.BaseType); if (node.Interfaces != null) { Context.Writer.WriteParameterSeparator(); } } if (node.Interfaces != null) { for (var i = 0; i < node.Interfaces.Count; i++) { Context.Writer.Write(node.Interfaces[i]); if (i + 1 < node.Interfaces.Count) { Context.Writer.WriteParameterSeparator(); } } } Context.Writer.WriteLine(); using (Context.Writer.BuildScope()) { VisitDefault(node); } } } public class CSharpRenderer : PageStructureCSharpRenderer { public CSharpRenderer(CSharpRenderingContext context) : base(context) { } public override void VisitChecksum(ChecksumIRNode node) { if (!string.IsNullOrEmpty(node.Bytes)) { Context.Writer .Write("#pragma checksum \"") .Write(node.Filename) .Write("\" \"") .Write(node.Guid) .Write("\" \"") .Write(node.Bytes) .WriteLine("\""); } } public override void VisitCSharpToken(CSharpTokenIRNode node) { Context.Writer.Write(node.Content); } public override void VisitHtml(HtmlContentIRNode node) { const int MaxStringLiteralLength = 1024; var charactersConsumed = 0; // Render the string in pieces to avoid Roslyn OOM exceptions at compile time: https://github.com/aspnet/External/issues/54 while (charactersConsumed < node.Content.Length) { string textToRender; if (node.Content.Length <= MaxStringLiteralLength) { textToRender = node.Content; } else { var charactersToSubstring = Math.Min(MaxStringLiteralLength, node.Content.Length - charactersConsumed); textToRender = node.Content.Substring(charactersConsumed, charactersToSubstring); } Context.Writer .Write(Context.RenderingConventions.StartWriteLiteralMethod) .WriteStringLiteral(textToRender) .WriteEndMethodInvocation(); charactersConsumed += textToRender.Length; } } public override void VisitCSharpExpression(CSharpExpressionIRNode node) { IDisposable linePragmaScope = null; if (node.SourceRange != null) { linePragmaScope = new LinePragmaWriter(Context.Writer, node.SourceRange); var padding = BuildOffsetPadding(Context.RenderingConventions.StartWriteMethod.Length, node.SourceRange); Context.Writer.Write(padding); } Context.Writer.Write(Context.RenderingConventions.StartWriteMethod); VisitDefault(node); Context.Writer.WriteEndMethodInvocation(); linePragmaScope?.Dispose(); } public override void VisitUsingStatement(UsingStatementIRNode node) { Context.Writer.WriteUsing(node.Content); } public override void VisitHtmlAttribute(HtmlAttributeIRNode node) { var valuePieceCount = node .Children .Count(child => child is HtmlAttributeValueIRNode || child is CSharpAttributeValueIRNode); var prefixLocation = node.SourceRange.AbsoluteIndex; var suffixLocation = node.SourceRange.AbsoluteIndex + node.SourceRange.ContentLength - node.Suffix.Length; Context.Writer .Write(Context.RenderingConventions.StartBeginWriteAttributeMethod) .WriteStringLiteral(node.Name) .WriteParameterSeparator() .WriteStringLiteral(node.Prefix) .WriteParameterSeparator() .Write(prefixLocation.ToString(CultureInfo.InvariantCulture)) .WriteParameterSeparator() .WriteStringLiteral(node.Suffix) .WriteParameterSeparator() .Write(suffixLocation.ToString(CultureInfo.InvariantCulture)) .WriteParameterSeparator() .Write(valuePieceCount.ToString(CultureInfo.InvariantCulture)) .WriteEndMethodInvocation(); VisitDefault(node); Context.Writer .Write(Context.RenderingConventions.StartEndWriteAttributeMethod) .WriteEndMethodInvocation(); } public override void VisitHtmlAttributeValue(HtmlAttributeValueIRNode node) { var prefixLocation = node.SourceRange.AbsoluteIndex; var valueLocation = node.SourceRange.AbsoluteIndex + node.Prefix.Length; var valueLength = node.SourceRange.ContentLength; Context.Writer .Write(Context.RenderingConventions.StartWriteAttributeValueMethod) .WriteStringLiteral(node.Prefix) .WriteParameterSeparator() .Write(prefixLocation.ToString(CultureInfo.InvariantCulture)) .WriteParameterSeparator() .WriteStringLiteral(node.Content) .WriteParameterSeparator() .Write(valueLocation.ToString(CultureInfo.InvariantCulture)) .WriteParameterSeparator() .Write(valueLength.ToString(CultureInfo.InvariantCulture)) .WriteParameterSeparator() .WriteBooleanLiteral(true) .WriteEndMethodInvocation(); } public override void VisitCSharpAttributeValue(CSharpAttributeValueIRNode node) { const string ValueWriterName = "__razor_attribute_value_writer"; var expressionValue = node.Children.First() as CSharpExpressionIRNode; var linePragma = expressionValue != null ? new LinePragmaWriter(Context.Writer, node.SourceRange) : null; var prefixLocation = node.SourceRange.AbsoluteIndex; var valueLocation = node.SourceRange.AbsoluteIndex + node.Prefix.Length; var valueLength = node.SourceRange.ContentLength - node.Prefix.Length; Context.Writer .Write(Context.RenderingConventions.StartWriteAttributeValueMethod) .WriteStringLiteral(node.Prefix) .WriteParameterSeparator() .Write(prefixLocation.ToString(CultureInfo.InvariantCulture)) .WriteParameterSeparator(); if (expressionValue != null) { Debug.Assert(node.Children.Count == 1); RenderExpressionInline(expressionValue, Context); } else { // Not an expression; need to buffer the result. Context.Writer.WriteStartNewObject("HelperResult" /* ORIGINAL: TemplateTypeName */); var initialRenderingConventions = Context.RenderingConventions; var redirectConventions = new CSharpRedirectRenderingConventions(ValueWriterName, Context.Writer); Context.RenderingConventions = redirectConventions; using (Context.Writer.BuildAsyncLambda(endLine: false, parameterNames: ValueWriterName)) { VisitDefault(node); } Context.RenderingConventions = initialRenderingConventions; Context.Writer.WriteEndMethodInvocation(false); } Context.Writer .WriteParameterSeparator() .Write(valueLocation.ToString(CultureInfo.InvariantCulture)) .WriteParameterSeparator() .Write(valueLength.ToString(CultureInfo.InvariantCulture)) .WriteParameterSeparator() .WriteBooleanLiteral(false) .WriteEndMethodInvocation(); linePragma?.Dispose(); } public override void VisitCSharpStatement(CSharpStatementIRNode node) { if (string.IsNullOrWhiteSpace(node.Content)) { return; } if (node.SourceRange != null) { using (new LinePragmaWriter(Context.Writer, node.SourceRange)) { var padding = BuildOffsetPadding(0, node.SourceRange); Context.Writer .Write(padding) .WriteLine(node.Content); } } else { Context.Writer.WriteLine(node.Content); } } public override void VisitTemplate(TemplateIRNode node) { const string ItemParameterName = "item"; const string TemplateWriterName = "__razor_template_writer"; Context.Writer .Write(ItemParameterName).Write(" => ") .WriteStartNewObject("HelperResult" /* ORIGINAL: TemplateTypeName */); var initialRenderingConventions = Context.RenderingConventions; var redirectConventions = new CSharpRedirectRenderingConventions(TemplateWriterName, Context.Writer); Context.RenderingConventions = redirectConventions; using (Context.Writer.BuildAsyncLambda(endLine: false, parameterNames: TemplateWriterName)) { VisitDefault(node); } Context.RenderingConventions = initialRenderingConventions; Context.Writer.WriteEndMethodInvocation(endLine: false); } private static int CalculateExpressionPadding(MappingLocation sourceRange, CSharpRenderingContext context) { var spaceCount = 0; for (var i = sourceRange.AbsoluteIndex - 1; i >= 0; i--) { var @char = context.SourceDocument[i]; if (@char == '\n' || @char == '\r') { break; } else if (@char == '\t') { spaceCount += context.Options.TabSize; } else { spaceCount++; } } return spaceCount; } private string BuildOffsetPadding(int generatedOffset, MappingLocation sourceRange) { var basePadding = CalculateExpressionPadding(sourceRange, Context); var resolvedPadding = Math.Max(basePadding - generatedOffset, 0); if (Context.Options.IsIndentingWithTabs) { var spaces = resolvedPadding % Context.Options.TabSize; var tabs = resolvedPadding / Context.Options.TabSize; return new string('\t', tabs) + new string(' ', spaces); } else { return new string(' ', resolvedPadding); } } private static void RenderExpressionInline(RazorIRNode node, CSharpRenderingContext context) { if (node is CSharpTokenIRNode) { context.Writer.Write(((CSharpTokenIRNode)node).Content); } else { for (var i = 0; i < node.Children.Count; i++) { RenderExpressionInline(node.Children[i], context); } } } } } }