diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorCSharpLoweringPhase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorCSharpLoweringPhase.cs new file mode 100644 index 0000000000..1136449f15 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorCSharpLoweringPhase.cs @@ -0,0 +1,571 @@ +// 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 DefaultRazorCSharpLoweringPhase : RazorEnginePhaseBase, IRazorCSharpLoweringPhase + { + private IRazorConfigureParserFeature[] _parserOptionsCallbacks; + + protected override void OnIntialized() + { + _parserOptionsCallbacks = Engine.Features.OfType().ToArray(); + } + + 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) + .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 VisitDirective(DirectiveIRNode node) + { + if (string.Equals(node.Name, CSharpCodeParser.SectionDirectiveDescriptor.Name, StringComparison.Ordinal)) + { + const string SectionWriterName = "__razor_section_writer"; + + Context.Writer + .WriteStartMethodInvocation("DefineSection" /* ORIGINAL: DefineSectionMethodName */) + .WriteStringLiteral(node.Tokens.FirstOrDefault()?.Content) + .WriteParameterSeparator(); + + var initialRenderingConventions = Context.RenderingConventions; + var redirectConventions = new CSharpRedirectRenderingConventions(SectionWriterName, Context.Writer); + Context.RenderingConventions = redirectConventions; + using (Context.Writer.BuildAsyncLambda(endLine: false, parameterNames: SectionWriterName)) + { + VisitDefault(node); + } + Context.RenderingConventions = initialRenderingConventions; + + Context.Writer.WriteEndMethodInvocation(); + } + } + + 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); + } + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIRLoweringPhase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIRLoweringPhase.cs index 7d503773ec..4b52f1e478 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIRLoweringPhase.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIRLoweringPhase.cs @@ -1,7 +1,9 @@ // 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.Linq; using Microsoft.AspNetCore.Razor.Evolution.Intermediate; using Microsoft.AspNetCore.Razor.Evolution.Legacy; @@ -9,12 +11,19 @@ namespace Microsoft.AspNetCore.Razor.Evolution { internal class DefaultRazorIRLoweringPhase : RazorEnginePhaseBase, IRazorIRLoweringPhase { + private IRazorConfigureParserFeature[] _parserOptionsCallbacks; + + protected override void OnIntialized() + { + _parserOptionsCallbacks = Engine.Features.OfType().ToArray(); + } + protected override void ExecuteCore(RazorCodeDocument codeDocument) { var syntaxTree = codeDocument.GetSyntaxTree(); ThrowForMissingDependency(syntaxTree); - var visitor = new Visitor(); + var visitor = new Visitor(codeDocument, syntaxTree.Options); visitor.VisitBlock(syntaxTree.Root); @@ -25,16 +34,34 @@ namespace Microsoft.AspNetCore.Razor.Evolution private class Visitor : ParserVisitor { private readonly Stack _builders; + private readonly RazorParserOptions _options; + private readonly RazorCodeDocument _codeDocument; - public Visitor() + public Visitor(RazorCodeDocument codeDocument, RazorParserOptions options) { + _codeDocument = codeDocument; + _options = options; _builders = new Stack(); var document = RazorIRBuilder.Document(); _builders.Push(document); + var checksum = ChecksumIRNode.Create(codeDocument.Source); + Builder.Add(checksum); + Namespace = new NamespaceDeclarationIRNode(); Builder.Push(Namespace); + foreach (var namespaceImport in options.NamespaceImports) + { + var @using = new UsingStatementIRNode() + { + Content = namespaceImport, + Parent = Namespace, + }; + + Builder.Add(@using); + } + Class = new ClassDeclarationIRNode(); Builder.Push(Class); @@ -62,7 +89,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution Name = chunkGenerator.Name, Prefix = chunkGenerator.Prefix, Suffix = chunkGenerator.Suffix, - SourceRange = new MappingLocation(block.Start, block.Length), + SourceRange = BuildSourceRangeFromNode(block), }); } @@ -80,7 +107,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution Builder.Push(new CSharpAttributeValueIRNode() { Prefix = chunkGenerator.Prefix, - SourceRange = new MappingLocation(block.Start, block.Length), + SourceRange = BuildSourceRangeFromNode(block), }); } @@ -95,7 +122,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution { Prefix = chunkGenerator.Prefix, Content = chunkGenerator.Value, - SourceRange = new MappingLocation(span.Start, span.Length), + SourceRange = BuildSourceRangeFromNode(span), }); } @@ -126,20 +153,18 @@ namespace Microsoft.AspNetCore.Razor.Evolution if (expressionNode.Children.Count > 0) { - var sourceRangeStart = expressionNode.Children[0].SourceRange; - var contentLength = 0; - - for (var i = 0; i < expressionNode.Children.Count; i++) - { - contentLength += expressionNode.Children[i].SourceRange.ContentLength; - } + var sourceRangeStart = expressionNode + .Children + .FirstOrDefault(child => child.SourceRange != null) + ?.SourceRange; + var contentLength = expressionNode.Children.Sum(child => child.SourceRange?.ContentLength ?? 0); expressionNode.SourceRange = new MappingLocation( sourceRangeStart.AbsoluteIndex, sourceRangeStart.LineIndex, sourceRangeStart.CharacterIndex, contentLength, - sourceRangeStart.FilePath); + sourceRangeStart.FilePath ?? _codeDocument.Source.Filename); } } @@ -149,7 +174,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution Builder.Add(new CSharpTokenIRNode() { Content = span.Content, - SourceRange = new MappingLocation(span.Start, span.Length), + SourceRange = BuildSourceRangeFromNode(span), }); } @@ -158,7 +183,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution Builder.Add(new CSharpStatementIRNode() { Content = span.Content, - SourceRange = new MappingLocation(span.Start, span.Length), + SourceRange = BuildSourceRangeFromNode(span), }); } @@ -175,13 +200,22 @@ namespace Microsoft.AspNetCore.Razor.Evolution Builder.Add(new HtmlContentIRNode() { Content = span.Content, - SourceRange = new MappingLocation(span.Start, span.Length), + SourceRange = BuildSourceRangeFromNode(span), }); } } public override void VisitImportSpan(AddImportChunkGenerator chunkGenerator, Span span) { + var namespaceImport = chunkGenerator.Namespace.Trim(); + + if (_options.NamespaceImports.Contains(namespaceImport, StringComparer.Ordinal)) + { + // Already added by default + + return; + } + // For prettiness, let's insert the usings before the class declaration. var i = 0; for (; i < Namespace.Children.Count; i++) @@ -194,9 +228,9 @@ namespace Microsoft.AspNetCore.Razor.Evolution var @using = new UsingStatementIRNode() { - Content = span.Content, + Content = namespaceImport, Parent = Namespace, - SourceRange = new MappingLocation(span.Start, span.Length), + SourceRange = BuildSourceRangeFromNode(span), }; Namespace.Children.Insert(i, @using); @@ -208,7 +242,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution { Content = span.Content, Descriptor = chunkGenerator.Descriptor, - SourceRange = new MappingLocation(span.Start, span.Length), + SourceRange = BuildSourceRangeFromNode(span), }); } @@ -225,6 +259,19 @@ namespace Microsoft.AspNetCore.Razor.Evolution { Builder.Pop(); } + + private MappingLocation BuildSourceRangeFromNode(SyntaxTreeNode node) + { + var location = node.Start; + var sourceRange = new MappingLocation( + location.AbsoluteIndex, + location.LineIndex, + location.CharacterIndex, + node.Length, + location.FilePath ?? _codeDocument.Source.Filename); + + return sourceRange; + } } } } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/IRazorCSharpLoweringPhase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorCSharpLoweringPhase.cs new file mode 100644 index 0000000000..7ef32517d4 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorCSharpLoweringPhase.cs @@ -0,0 +1,9 @@ +// 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. + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + public interface IRazorCSharpLoweringPhase : IRazorEnginePhase + { + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/ChecksumIRNode.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/ChecksumIRNode.cs new file mode 100644 index 0000000000..afe63378b3 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/ChecksumIRNode.cs @@ -0,0 +1,69 @@ +// 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 System.Security.Cryptography; +using System.Text; +using Microsoft.AspNetCore.Razor.Evolution.Legacy; + +namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate +{ + public class ChecksumIRNode : RazorIRNode + { + public override IList Children => EmptyArray; + + public override RazorIRNode Parent { get; set; } + + internal override MappingLocation SourceRange { get; set; } + + public string Bytes { get; set; } + + public string Filename { get; set; } + + public string Guid { get; set; } + + public override void Accept(RazorIRNodeVisitor visitor) + { + visitor.VisitChecksum(this); + } + + public override TResult Accept(RazorIRNodeVisitor visitor) + { + return visitor.VisitChecksum(this); + } + + public static ChecksumIRNode Create(RazorSourceDocument sourceDocument) + { + // See http://msdn.microsoft.com/en-us/library/system.codedom.codechecksumpragma.checksumalgorithmid.aspx + const string Sha1AlgorithmId = "{ff1816ec-aa5e-4d10-87f7-6f4963833460}"; + + var node = new ChecksumIRNode() + { + Filename = sourceDocument.Filename, + Guid = Sha1AlgorithmId + }; + + var charBuffer = new char[sourceDocument.Length]; + sourceDocument.CopyTo(0, charBuffer, 0, sourceDocument.Length); + + var encoder = sourceDocument.Encoding.GetEncoder(); + var byteCount = encoder.GetByteCount(charBuffer, 0, charBuffer.Length, flush: true); + var checksumBytes = new byte[byteCount]; + encoder.GetBytes(charBuffer, 0, charBuffer.Length, checksumBytes, 0, flush: true); + + using (var hashAlgorithm = SHA1.Create()) + { + var hashedBytes = hashAlgorithm.ComputeHash(checksumBytes); + var fileHashBuilder = new StringBuilder(hashedBytes.Length * 2); + foreach (var value in hashedBytes) + { + fileHashBuilder.Append(value.ToString("x2")); + } + + node.Bytes = fileHashBuilder.ToString(); + } + + return node; + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRNodeVisitor.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRNodeVisitor.cs index 52297168ec..e2488a5aba 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRNodeVisitor.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRNodeVisitor.cs @@ -14,6 +14,11 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate { } + public virtual void VisitChecksum(ChecksumIRNode node) + { + VisitDefault(node); + } + public virtual void VisitDirectiveToken(DirectiveTokenIRNode node) { VisitDefault(node); diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRNodeVisitorOfT.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRNodeVisitorOfT.cs index 53fac5db72..71026ca802 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRNodeVisitorOfT.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRNodeVisitorOfT.cs @@ -15,6 +15,11 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate return default(TResult); } + public virtual TResult VisitChecksum(ChecksumIRNode node) + { + return VisitDefault(node); + } + public virtual TResult VisitDirectiveToken(DirectiveTokenIRNode node) { return VisitDefault(node); diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpCodeWriter.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpCodeWriter.cs index 47c21ab977..3adf902dc7 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpCodeWriter.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpCodeWriter.cs @@ -198,7 +198,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy /// The location to generate the line pragma for. /// The file to generate the line pragma for. /// The current instance of . - public CSharpCodeWriter WriteLineNumberDirective(SourceLocation location, string file) + public CSharpCodeWriter WriteLineNumberDirective(MappingLocation location, string file) { if (location.FilePath != null) { diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs index c720d43812..b84a851f34 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs @@ -34,6 +34,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution builder.Phases.Add(new DefaultRazorSyntaxTreePhase()); builder.Phases.Add(new DefaultRazorIRLoweringPhase()); builder.Phases.Add(new DefaultRazorIRPhase()); + builder.Phases.Add(new DefaultRazorCSharpLoweringPhase()); // Syntax Tree passes builder.Features.Add(new DefaultDirectiveSyntaxTreePass()); diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorParserOptions.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorParserOptions.cs index f57d9d3f98..42dfc84083 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/RazorParserOptions.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorParserOptions.cs @@ -1,7 +1,9 @@ // 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.Threading.Tasks; namespace Microsoft.AspNetCore.Razor.Evolution { @@ -15,10 +17,17 @@ namespace Microsoft.AspNetCore.Razor.Evolution private RazorParserOptions() { Directives = new List(); + NamespaceImports = new HashSet(StringComparer.Ordinal) { nameof(System), typeof(Task).Namespace }; } public bool DesignTimeMode { get; set; } + public int TabSize { get; set; } = 4; + + public bool IsIndentingWithTabs { get; set; } + public ICollection Directives { get; } + + public HashSet NamespaceImports { get; } } } diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorCSharpLoweringPhaseTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorCSharpLoweringPhaseTest.cs new file mode 100644 index 0000000000..d30aad9895 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorCSharpLoweringPhaseTest.cs @@ -0,0 +1,30 @@ +// 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 Microsoft.AspNetCore.Razor.Evolution.Intermediate; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + public class DefaultRazorCSharpLoweringPhaseTest + { + [Fact] + public void Execute_ThrowsForMissingDependency() + { + // Arrange + var phase = new DefaultRazorCSharpLoweringPhase(); + + var engine = RazorEngine.CreateEmpty(b => b.Phases.Add(phase)); + + var codeDocument = TestRazorCodeDocument.CreateEmpty(); + + // Act & Assert + ExceptionAssert.Throws( + () => phase.Execute(codeDocument), + $"The '{nameof(DefaultRazorCSharpLoweringPhase)}' phase requires a '{nameof(DocumentIRNode)}' " + + $"provided by the '{nameof(RazorCodeDocument)}'."); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/IntegrationTests/BasicIntegrationTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/IntegrationTests/BasicIntegrationTest.cs index d09f67e083..d0e71d70e2 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/IntegrationTests/BasicIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/IntegrationTests/BasicIntegrationTest.cs @@ -97,8 +97,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution.IntegrationTests Assert.Equal("test_directive", directiveSpan.Content); var irDocument = document.GetIRDocument(); - var irNamespace = irDocument.Children[0]; - var irClass = irNamespace.Children[0]; + var irNamespace = irDocument.Children[1]; + var irClass = irNamespace.Children[2]; var irMethod = irClass.Children[0]; var irDirective = (DirectiveIRNode)irMethod.Children[1]; Assert.Equal("test_directive", irDirective.Name); diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/DefaultRazorIRLoweringPhaseIntegrationTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/DefaultRazorIRLoweringPhaseIntegrationTest.cs index 183bfbdfeb..37268f8cc8 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/DefaultRazorIRLoweringPhaseIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/DefaultRazorIRLoweringPhaseIntegrationTest.cs @@ -4,6 +4,7 @@ using static Microsoft.AspNetCore.Razor.Evolution.Intermediate.RazorIRAssert; using Xunit; using System; +using System.Threading.Tasks; namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate { @@ -19,8 +20,15 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate var irDocument = Lower(codeDocument); // Assert - var @namespace = SingleChild(irDocument); - var @class = SingleChild(@namespace); + Children(irDocument, + n => Assert.IsType(n), + n => Assert.IsType(n)); + var @namespace = irDocument.Children[1]; + Children(@namespace, + n => Assert.IsType(n), + n => Assert.IsType(n), + n => Assert.IsType(n)); + var @class = @namespace.Children[2]; var method = SingleChild(@class); var html = SingleChild(method); @@ -37,8 +45,15 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate var irDocument = Lower(codeDocument); // Assert - var @namespace = SingleChild(irDocument); - var @class = SingleChild(@namespace); + Children(irDocument, + n => Assert.IsType(n), + n => Assert.IsType(n)); + var @namespace = irDocument.Children[1]; + Children(@namespace, + n => Assert.IsType(n), + n => Assert.IsType(n), + n => Assert.IsType(n)); + var @class = @namespace.Children[2]; var method = SingleChild(@class); var html = SingleChild(method); @@ -60,8 +75,15 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate var irDocument = Lower(codeDocument); // Assert - var @namespace = SingleChild(irDocument); - var @class = SingleChild(@namespace); + Children(irDocument, + n => Assert.IsType(n), + n => Assert.IsType(n)); + var @namespace = irDocument.Children[1]; + Children(@namespace, + n => Assert.IsType(n), + n => Assert.IsType(n), + n => Assert.IsType(n)); + var @class = @namespace.Children[2]; var method = SingleChild(@class); Children(method, n => Html( @@ -90,8 +112,15 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate var irDocument = Lower(codeDocument); // Assert - var @namespace = SingleChild(irDocument); - var @class = SingleChild(@namespace); + Children(irDocument, + n => Assert.IsType(n), + n => Assert.IsType(n)); + var @namespace = irDocument.Children[1]; + Children(@namespace, + n => Assert.IsType(n), + n => Assert.IsType(n), + n => Assert.IsType(n)); + var @class = @namespace.Children[2]; var method = SingleChild(@class); Children(method, n => Html( @@ -125,8 +154,15 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate var irDocument = Lower(codeDocument); // Assert - var @namespace = SingleChild(irDocument); - var @class = SingleChild(@namespace); + Children(irDocument, + n => Assert.IsType(n), + n => Assert.IsType(n)); + var @namespace = irDocument.Children[1]; + Children(@namespace, + n => Assert.IsType(n), + n => Assert.IsType(n), + n => Assert.IsType(n)); + var @class = @namespace.Children[2]; Children(@class, n => Assert.IsType(n), n => Assert.IsType(n)); @@ -142,9 +178,13 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate var irDocument = Lower(codeDocument); // Assert - var @namespace = SingleChild(irDocument); + Children(irDocument, + n => Assert.IsType(n), + n => Assert.IsType(n)); + var @namespace = irDocument.Children[1]; Children(@namespace, - n => Using("using System", n), + n => Using("System", n), + n => Using(typeof(Task).Namespace, n), n => Assert.IsType(n)); } diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpCodeWriterTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpCodeWriterTest.cs index f9daf97f1f..5dbf4b9221 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpCodeWriterTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpCodeWriterTest.cs @@ -15,9 +15,10 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy var writer = new CSharpCodeWriter(); var expected = $"#line 5 \"{filePath}\"" + writer.NewLine; var sourceLocation = new SourceLocation(10, 4, 3); + var mappingLocation = new MappingLocation(sourceLocation, 9); // Act - writer.WriteLineNumberDirective(sourceLocation, filePath); + writer.WriteLineNumberDirective(mappingLocation, filePath); var code = writer.GenerateCode(); // Assert @@ -35,9 +36,10 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy var writer = new CSharpCodeWriter(); var expected = $"#line 5 \"{sourceLocationFilePath}\"" + writer.NewLine; var sourceLocation = new SourceLocation(sourceLocationFilePath, 10, 4, 3); + var mappingLocation = new MappingLocation(sourceLocation, 9); // Act - writer.WriteLineNumberDirective(sourceLocation, filePath); + writer.WriteLineNumberDirective(mappingLocation, filePath); var code = writer.GenerateCode(); // Assert diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs index 8450754015..3c990517a4 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs @@ -88,7 +88,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution phase => Assert.IsType(phase), phase => Assert.IsType(phase), phase => Assert.IsType(phase), - phase => Assert.IsType(phase)); + phase => Assert.IsType(phase), + phase => Assert.IsType(phase)); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestFiles/IntegrationTests/BasicIntegrationTest/CustomDirective.ir.txt b/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestFiles/IntegrationTests/BasicIntegrationTest/CustomDirective.ir.txt index 021224c893..3f064ee5b9 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestFiles/IntegrationTests/BasicIntegrationTest/CustomDirective.ir.txt +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestFiles/IntegrationTests/BasicIntegrationTest/CustomDirective.ir.txt @@ -1,7 +1,10 @@ Document - + Checksum - NamespaceDeclaration - - + UsingStatement - - System + UsingStatement - - System.Threading.Tasks ClassDeclaration - - - - - RazorMethodDeclaration - - - - - - HtmlContent - (0:0,0 [0] ) - + HtmlContent - (0:0,0 [0] TestFiles/IntegrationTests/BasicIntegrationTest/CustomDirective.cshtml) - Directive - - test_directive - HtmlContent - (15:0,15 [0] ) - + HtmlContent - (15:0,15 [0] TestFiles/IntegrationTests/BasicIntegrationTest/CustomDirective.cshtml) - diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestFiles/IntegrationTests/BasicIntegrationTest/Empty.ir.txt b/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestFiles/IntegrationTests/BasicIntegrationTest/Empty.ir.txt index a26b3a9973..14b78633dc 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestFiles/IntegrationTests/BasicIntegrationTest/Empty.ir.txt +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestFiles/IntegrationTests/BasicIntegrationTest/Empty.ir.txt @@ -1,5 +1,8 @@ Document - + Checksum - NamespaceDeclaration - - + UsingStatement - - System + UsingStatement - - System.Threading.Tasks ClassDeclaration - - - - - RazorMethodDeclaration - - - - - - HtmlContent - (0:0,0 [0] ) - + HtmlContent - (0:0,0 [0] TestFiles/IntegrationTests/BasicIntegrationTest/Empty.cshtml) - diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestFiles/IntegrationTests/BasicIntegrationTest/HelloWorld.ir.txt b/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestFiles/IntegrationTests/BasicIntegrationTest/HelloWorld.ir.txt index f8e644a9c5..8a90797f4b 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestFiles/IntegrationTests/BasicIntegrationTest/HelloWorld.ir.txt +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestFiles/IntegrationTests/BasicIntegrationTest/HelloWorld.ir.txt @@ -1,5 +1,8 @@ Document - + Checksum - NamespaceDeclaration - - + UsingStatement - - System + UsingStatement - - System.Threading.Tasks ClassDeclaration - - - - - RazorMethodDeclaration - - - - - - HtmlContent - (0:0,0 [13] ) - Hello, World! + HtmlContent - (0:0,0 [13] TestFiles/IntegrationTests/BasicIntegrationTest/HelloWorld.cshtml) - Hello, World!