// 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; namespace Microsoft.AspNetCore.Razor.Evolution { internal class DefaultRazorIRLoweringPhase : RazorEnginePhaseBase, IRazorIRLoweringPhase { protected override void ExecuteCore(RazorCodeDocument codeDocument) { var syntaxTree = codeDocument.GetSyntaxTree(); ThrowForMissingDependency(syntaxTree); var visitor = new Visitor(codeDocument, syntaxTree.Options); var i = 0; var builder = visitor.Builder; foreach (var namespaceImport in syntaxTree.Options.NamespaceImports) { if (visitor.Namespaces.Add(namespaceImport)) { var @using = new UsingStatementIRNode() { Content = namespaceImport, }; builder.Insert(i++, @using); } } var checksum = ChecksumIRNode.Create(codeDocument.Source); visitor.Builder.Insert(0, checksum); visitor.VisitBlock(syntaxTree.Root); var irDocument = (DocumentIRNode)visitor.Builder.Build(); codeDocument.SetIRDocument(irDocument); } private class Visitor : ParserVisitor { private readonly RazorParserOptions _options; private readonly RazorCodeDocument _codeDocument; private DeclareTagHelperFieldsIRNode _tagHelperFields; public Visitor(RazorCodeDocument codeDocument, RazorParserOptions options) { _codeDocument = codeDocument; _options = options; Namespaces = new HashSet(); Builder = RazorIRBuilder.Document(); } public RazorIRBuilder Builder { get; } public HashSet Namespaces { get; } // Example // // Name=checked // Prefix= checked=" // Suffix=" public override void VisitStartAttributeBlock(AttributeBlockChunkGenerator chunkGenerator, Block block) { Builder.Push(new HtmlAttributeIRNode() { Name = chunkGenerator.Name, Prefix = chunkGenerator.Prefix, Suffix = chunkGenerator.Suffix, Source = BuildSourceRangeFromNode(block), }); } public override void VisitEndAttributeBlock(AttributeBlockChunkGenerator chunkGenerator, Block block) { Builder.Pop(); } // Example // // Prefix= (space) // Children will contain a token for @false. public override void VisitStartDynamicAttributeBlock(DynamicAttributeBlockChunkGenerator chunkGenerator, Block block) { Builder.Push(new CSharpAttributeValueIRNode() { Prefix = chunkGenerator.Prefix, Source = BuildSourceRangeFromNode(block), }); } public override void VisitEndDynamicAttributeBlock(DynamicAttributeBlockChunkGenerator chunkGenerator, Block block) { Builder.Pop(); } public override void VisitLiteralAttributeSpan(LiteralAttributeChunkGenerator chunkGenerator, Span span) { Builder.Add(new HtmlAttributeValueIRNode() { Prefix = chunkGenerator.Prefix, Content = chunkGenerator.Value, Source = BuildSourceRangeFromNode(span), }); } public override void VisitStartTemplateBlock(TemplateBlockChunkGenerator chunkGenerator, Block block) { Builder.Push(new TemplateIRNode()); } public override void VisitEndTemplateBlock(TemplateBlockChunkGenerator chunkGenerator, Block block) { var templateNode = Builder.Pop(); if (templateNode.Children.Count > 0) { var sourceRangeStart = templateNode .Children .FirstOrDefault(child => child.Source != null) ?.Source; if (sourceRangeStart != null) { var contentLength = templateNode.Children.Sum(child => child.Source?.Length ?? 0); templateNode.Source = new SourceSpan( sourceRangeStart.Value.FilePath ?? _codeDocument.Source.Filename, sourceRangeStart.Value.AbsoluteIndex, sourceRangeStart.Value.LineIndex, sourceRangeStart.Value.CharacterIndex, contentLength); } } } // CSharp expressions are broken up into blocks and spans because Razor allows Razor comments // inside an expression. // Ex: // @DateTime.@*This is a comment*@Now // // We need to capture this in the IR so that we can give each piece the correct source mappings public override void VisitStartExpressionBlock(ExpressionChunkGenerator chunkGenerator, Block block) { Builder.Push(new CSharpExpressionIRNode()); } public override void VisitEndExpressionBlock(ExpressionChunkGenerator chunkGenerator, Block block) { var expressionNode = Builder.Pop(); if (expressionNode.Children.Count > 0) { var sourceRangeStart = expressionNode .Children .FirstOrDefault(child => child.Source != null) ?.Source; if (sourceRangeStart != null) { var contentLength = expressionNode.Children.Sum(child => child.Source?.Length ?? 0); expressionNode.Source = new SourceSpan( sourceRangeStart.Value.FilePath ?? _codeDocument.Source.Filename, sourceRangeStart.Value.AbsoluteIndex, sourceRangeStart.Value.LineIndex, sourceRangeStart.Value.CharacterIndex, contentLength); } } } public override void VisitExpressionSpan(ExpressionChunkGenerator chunkGenerator, Span span) { if (span.Symbols.Count == 1) { var symbol = span.Symbols[0] as CSharpSymbol; if (symbol != null && symbol.Type == CSharpSymbolType.Unknown && symbol.Content.Length == 0) { // We don't want to create IR nodes for marker symbols. return; } } Builder.Add(new CSharpTokenIRNode() { Content = span.Content, Source = BuildSourceRangeFromNode(span), }); } public override void VisitStatementSpan(StatementChunkGenerator chunkGenerator, Span span) { Builder.Add(new CSharpStatementIRNode() { Content = span.Content, Source = BuildSourceRangeFromNode(span), }); } public override void VisitMarkupSpan(MarkupChunkGenerator chunkGenerator, Span span) { if (span.Symbols.Count == 1) { var symbol = span.Symbols[0] as HtmlSymbol; if (symbol != null && symbol.Type == HtmlSymbolType.Unknown && symbol.Content.Length == 0) { // We don't want to create IR nodes for marker symbols. return; } } var currentChildren = Builder.Current.Children; if (currentChildren.Count > 0 && currentChildren[currentChildren.Count - 1] is HtmlContentIRNode) { var existingHtmlContent = (HtmlContentIRNode)currentChildren[currentChildren.Count - 1]; existingHtmlContent.Content = string.Concat(existingHtmlContent.Content, span.Content); if (existingHtmlContent.Source != null) { var contentLength = existingHtmlContent.Source.Value.Length + span.Content.Length; existingHtmlContent.Source = new SourceSpan( existingHtmlContent.Source.Value.FilePath ?? _codeDocument.Source.Filename, existingHtmlContent.Source.Value.AbsoluteIndex, existingHtmlContent.Source.Value.LineIndex, existingHtmlContent.Source.Value.CharacterIndex, contentLength); } } else { Builder.Add(new HtmlContentIRNode() { Content = span.Content, Source = BuildSourceRangeFromNode(span), }); } } public override void VisitImportSpan(AddImportChunkGenerator chunkGenerator, Span span) { var namespaceImport = chunkGenerator.Namespace.Trim(); // Track seen namespaces so we don't add duplicates from options. if (Namespaces.Add(namespaceImport)) { Builder.Add(new UsingStatementIRNode() { Content = namespaceImport, Source = BuildSourceRangeFromNode(span), }); } } public override void VisitDirectiveToken(DirectiveTokenChunkGenerator chunkGenerator, Span span) { Builder.Add(new DirectiveTokenIRNode() { Content = span.Content, Descriptor = chunkGenerator.Descriptor, Source = BuildSourceRangeFromNode(span), }); } public override void VisitStartDirectiveBlock(DirectiveChunkGenerator chunkGenerator, Block block) { Builder.Push(new DirectiveIRNode() { Name = chunkGenerator.Descriptor.Name, Descriptor = chunkGenerator.Descriptor, }); } public override void VisitEndDirectiveBlock(DirectiveChunkGenerator chunkGenerator, Block block) { Builder.Pop(); } public override void VisitStartTagHelperBlock(TagHelperChunkGenerator chunkGenerator, Block block) { var tagHelperBlock = block as TagHelperBlock; if (tagHelperBlock == null) { return; } DeclareTagHelperFields(tagHelperBlock); Builder.Push(new TagHelperIRNode()); Builder.Push(new InitializeTagHelperStructureIRNode() { TagName = tagHelperBlock.TagName, TagMode = tagHelperBlock.TagMode }); } public override void VisitEndTagHelperBlock(TagHelperChunkGenerator chunkGenerator, Block block) { var tagHelperBlock = block as TagHelperBlock; if (tagHelperBlock == null) { return; } Builder.Pop(); // Pop InitializeTagHelperStructureIRNode AddTagHelperCreation(tagHelperBlock.Descriptors); AddTagHelperAttributes(tagHelperBlock.Attributes, tagHelperBlock.Descriptors); AddExecuteTagHelpers(); Builder.Pop(); // Pop TagHelperIRNode } public override void VisitAddTagHelperSpan(AddTagHelperChunkGenerator chunkGenerator, Span span) { } public override void VisitRemoveTagHelperSpan(RemoveTagHelperChunkGenerator chunkGenerator, Span span) { } public override void VisitTagHelperPrefixDirectiveSpan(TagHelperPrefixDirectiveChunkGenerator chunkGenerator, Span span) { } private void DeclareTagHelperFields(TagHelperBlock block) { if (_tagHelperFields == null) { _tagHelperFields = new DeclareTagHelperFieldsIRNode(); Builder.Add(_tagHelperFields); } foreach (var descriptor in block.Descriptors) { _tagHelperFields.UsedTagHelperTypeNames.Add(descriptor.TypeName); } } private void AddTagHelperCreation(IEnumerable descriptors) { foreach (var descriptor in descriptors) { var createTagHelper = new CreateTagHelperIRNode() { TagHelperTypeName = descriptor.TypeName, Descriptor = descriptor }; Builder.Add(createTagHelper); } } private void AddTagHelperAttributes(IList attributes, IEnumerable descriptors) { var renderedBoundAttributeNames = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var attribute in attributes) { var attributeValueNode = attribute.Value; var associatedDescriptors = descriptors.Where(descriptor => descriptor.Attributes.Any(attributeDescriptor => attributeDescriptor.IsNameMatch(attribute.Name))); if (associatedDescriptors.Any() && renderedBoundAttributeNames.Add(attribute.Name)) { if (attributeValueNode == null) { // Minimized attributes are not valid for bound attributes. TagHelperBlockRewriter has already // logged an error if it was a bound attribute; so we can skip. continue; } foreach (var associatedDescriptor in associatedDescriptors) { var associatedAttributeDescriptor = associatedDescriptor.Attributes.First( attributeDescriptor => attributeDescriptor.IsNameMatch(attribute.Name)); var setTagHelperProperty = new SetTagHelperPropertyIRNode() { PropertyName = associatedAttributeDescriptor.PropertyName, AttributeName = attribute.Name, TagHelperTypeName = associatedDescriptor.TypeName, Descriptor = associatedAttributeDescriptor, ValueStyle = attribute.ValueStyle, Source = BuildSourceRangeFromNode(attributeValueNode) }; Builder.Push(setTagHelperProperty); attributeValueNode.Accept(this); Builder.Pop(); } } else { var addHtmlAttribute = new AddTagHelperHtmlAttributeIRNode() { Name = attribute.Name, ValueStyle = attribute.ValueStyle }; Builder.Push(addHtmlAttribute); if (attributeValueNode != null) { attributeValueNode.Accept(this); } Builder.Pop(); } } } private void AddExecuteTagHelpers() { Builder.Add(new ExecuteTagHelpersIRNode()); } private SourceSpan BuildSourceRangeFromNode(SyntaxTreeNode node) { var location = node.Start; var sourceRange = new SourceSpan( node.Start.FilePath ?? _codeDocument.Source.Filename, node.Start.AbsoluteIndex, node.Start.LineIndex, node.Start.CharacterIndex, node.Length); return sourceRange; } } } }