aspnetcore/src/Microsoft.AspNetCore.Razor..../DefaultRazorIntermediateNod...

792 lines
31 KiB
C#

// 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.Linq;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.AspNetCore.Razor.Language.Legacy;
namespace Microsoft.AspNetCore.Razor.Language
{
internal class DefaultRazorIntermediateNodeLoweringPhase : RazorEnginePhaseBase, IRazorIntermediateNodeLoweringPhase
{
private IRazorCodeGenerationOptionsFeature[] _optionsCallbacks;
protected override void OnIntialized()
{
_optionsCallbacks = Engine.Features.OfType<IRazorCodeGenerationOptionsFeature>().OrderBy(f => f.Order).ToArray();
}
protected override void ExecuteCore(RazorCodeDocument codeDocument)
{
var syntaxTree = codeDocument.GetSyntaxTree();
ThrowForMissingDocumentDependency(syntaxTree);
// This might not have been set if there are no tag helpers.
var tagHelperContext = codeDocument.GetTagHelperContext();
var document = new DocumentIntermediateNode();
var builder = IntermediateNodeBuilder.Create(document);
document.Options = CreateCodeGenerationOptions();
var namespaces = new Dictionary<string, SourceSpan?>(StringComparer.Ordinal);
// The import documents should be inserted logically before the main document.
var imports = codeDocument.GetImportSyntaxTrees();
if (imports != null)
{
var importsVisitor = new ImportsVisitor(document, builder, namespaces);
for (var j = 0; j < imports.Count; j++)
{
var import = imports[j];
importsVisitor.FilePath = import.Source.FilePath;
importsVisitor.VisitBlock(import.Root);
}
}
var tagHelperPrefix = tagHelperContext?.Prefix;
var visitor = new MainSourceVisitor(document, builder, namespaces, tagHelperPrefix)
{
FilePath = syntaxTree.Source.FilePath,
};
visitor.VisitBlock(syntaxTree.Root);
// In each lowering piece above, namespaces were tracked. We render them here to ensure every
// lowering action has a chance to add a source location to a namespace. Ultimately, closest wins.
var i = 0;
foreach (var @namespace in namespaces)
{
var @using = new UsingDirectiveIntermediateNode()
{
Content = @namespace.Key,
Source = @namespace.Value,
};
builder.Insert(i++, @using);
}
ImportDirectives(document);
// The document should contain all errors that currently exist in the system. This involves
// adding the errors from the primary and imported syntax trees.
for (i = 0; i < syntaxTree.Diagnostics.Count; i++)
{
document.Diagnostics.Add(syntaxTree.Diagnostics[i]);
}
if (imports != null)
{
for (i = 0; i < imports.Count; i++)
{
var import = imports[i];
for (var j = 0; j < import.Diagnostics.Count; j++)
{
document.Diagnostics.Add(import.Diagnostics[j]);
}
}
}
codeDocument.SetDocumentIntermediateNode(document);
}
private void ImportDirectives(DocumentIntermediateNode document)
{
var visitor = new DirectiveVisitor();
visitor.VisitDocument(document);
var seenDirectives = new HashSet<DirectiveDescriptor>();
for (var i = visitor.Directives.Count - 1; i >= 0; i--)
{
var reference = visitor.Directives[i];
var directive = (DirectiveIntermediateNode)reference.Node;
var descriptor = directive.Descriptor;
var seenDirective = !seenDirectives.Add(descriptor);
var imported = ReferenceEquals(directive.Annotations[CommonAnnotations.Imported], CommonAnnotations.Imported);
if (!imported)
{
continue;
}
switch (descriptor.Kind)
{
case DirectiveKind.SingleLine:
if (seenDirective && descriptor.Usage == DirectiveUsage.FileScopedSinglyOccurring)
{
// This directive has been overridden, it should be removed from the document.
break;
}
continue;
case DirectiveKind.RazorBlock:
case DirectiveKind.CodeBlock:
if (descriptor.Usage == DirectiveUsage.FileScopedSinglyOccurring)
{
// A block directive cannot be imported.
document.Diagnostics.Add(
RazorDiagnosticFactory.CreateDirective_BlockDirectiveCannotBeImported(descriptor.Directive));
}
break;
default:
throw new InvalidOperationException(Resources.FormatUnexpectedDirectiveKind(typeof(DirectiveKind).FullName));
}
// Overridden and invalid imported directives make it to here. They should be removed from the document.
reference.Remove();
}
}
private RazorCodeGenerationOptions CreateCodeGenerationOptions()
{
var builder = new DefaultRazorCodeGenerationOptionsBuilder();
for (var i = 0; i < _optionsCallbacks.Length; i++)
{
_optionsCallbacks[i].Configure(builder);
}
return builder.Build();
}
private class LoweringVisitor : ParserVisitor
{
protected readonly IntermediateNodeBuilder _builder;
protected readonly DocumentIntermediateNode _document;
protected readonly Dictionary<string, SourceSpan?> _namespaces;
public LoweringVisitor(DocumentIntermediateNode document, IntermediateNodeBuilder builder, Dictionary<string, SourceSpan?> namespaces)
{
_document = document;
_builder = builder;
_namespaces = namespaces;
}
public string FilePath { get; set; }
public override void VisitDirectiveToken(DirectiveTokenChunkGenerator chunkGenerator, Span span)
{
_builder.Add(new DirectiveTokenIntermediateNode()
{
Content = span.Content,
Descriptor = chunkGenerator.Descriptor,
Source = BuildSourceSpanFromNode(span),
});
}
public override void VisitDirectiveBlock(DirectiveChunkGenerator chunkGenerator, Block block)
{
IntermediateNode directiveNode;
if (IsMalformed(chunkGenerator.Diagnostics))
{
directiveNode = new MalformedDirectiveIntermediateNode()
{
Name = chunkGenerator.Descriptor.Directive,
Descriptor = chunkGenerator.Descriptor,
Source = BuildSourceSpanFromNode(block),
};
}
else
{
directiveNode = new DirectiveIntermediateNode()
{
Name = chunkGenerator.Descriptor.Directive,
Descriptor = chunkGenerator.Descriptor,
Source = BuildSourceSpanFromNode(block),
};
}
for (var i = 0; i < chunkGenerator.Diagnostics.Count; i++)
{
directiveNode.Diagnostics.Add(chunkGenerator.Diagnostics[i]);
}
_builder.Push(directiveNode);
VisitDefault(block);
_builder.Pop();
}
public override void VisitImportSpan(AddImportChunkGenerator chunkGenerator, Span span)
{
var namespaceImport = chunkGenerator.Namespace.Trim();
var namespaceSpan = BuildSourceSpanFromNode(span);
_namespaces[namespaceImport] = namespaceSpan;
}
public override void VisitAddTagHelperSpan(AddTagHelperChunkGenerator chunkGenerator, Span span)
{
IntermediateNode directiveNode;
if (IsMalformed(chunkGenerator.Diagnostics))
{
directiveNode = new MalformedDirectiveIntermediateNode()
{
Name = CSharpCodeParser.AddTagHelperDirectiveDescriptor.Directive,
Descriptor = CSharpCodeParser.AddTagHelperDirectiveDescriptor,
Source = BuildSourceSpanFromNode(span),
};
}
else
{
directiveNode = new DirectiveIntermediateNode()
{
Name = CSharpCodeParser.AddTagHelperDirectiveDescriptor.Directive,
Descriptor = CSharpCodeParser.AddTagHelperDirectiveDescriptor,
Source = BuildSourceSpanFromNode(span),
};
}
for (var i = 0; i < chunkGenerator.Diagnostics.Count; i++)
{
directiveNode.Diagnostics.Add(chunkGenerator.Diagnostics[i]);
}
_builder.Push(directiveNode);
_builder.Add(new DirectiveTokenIntermediateNode()
{
Content = chunkGenerator.LookupText,
Descriptor = CSharpCodeParser.AddTagHelperDirectiveDescriptor.Tokens.First(),
Source = BuildSourceSpanFromNode(span),
});
_builder.Pop();
}
public override void VisitRemoveTagHelperSpan(RemoveTagHelperChunkGenerator chunkGenerator, Span span)
{
IntermediateNode directiveNode;
if (IsMalformed(chunkGenerator.Diagnostics))
{
directiveNode = new MalformedDirectiveIntermediateNode()
{
Name = CSharpCodeParser.RemoveTagHelperDirectiveDescriptor.Directive,
Descriptor = CSharpCodeParser.RemoveTagHelperDirectiveDescriptor,
Source = BuildSourceSpanFromNode(span),
};
}
else
{
directiveNode = new DirectiveIntermediateNode()
{
Name = CSharpCodeParser.RemoveTagHelperDirectiveDescriptor.Directive,
Descriptor = CSharpCodeParser.RemoveTagHelperDirectiveDescriptor,
Source = BuildSourceSpanFromNode(span),
};
}
for (var i = 0; i < chunkGenerator.Diagnostics.Count; i++)
{
directiveNode.Diagnostics.Add(chunkGenerator.Diagnostics[i]);
}
_builder.Push(directiveNode);
_builder.Add(new DirectiveTokenIntermediateNode()
{
Content = chunkGenerator.LookupText,
Descriptor = CSharpCodeParser.RemoveTagHelperDirectiveDescriptor.Tokens.First(),
Source = BuildSourceSpanFromNode(span),
});
_builder.Pop();
}
public override void VisitTagHelperPrefixDirectiveSpan(TagHelperPrefixDirectiveChunkGenerator chunkGenerator, Span span)
{
IntermediateNode directiveNode;
if (IsMalformed(chunkGenerator.Diagnostics))
{
directiveNode = new MalformedDirectiveIntermediateNode()
{
Name = CSharpCodeParser.TagHelperPrefixDirectiveDescriptor.Directive,
Descriptor = CSharpCodeParser.TagHelperPrefixDirectiveDescriptor,
Source = BuildSourceSpanFromNode(span),
};
}
else
{
directiveNode = new DirectiveIntermediateNode()
{
Name = CSharpCodeParser.TagHelperPrefixDirectiveDescriptor.Directive,
Descriptor = CSharpCodeParser.TagHelperPrefixDirectiveDescriptor,
Source = BuildSourceSpanFromNode(span),
};
}
for (var i = 0; i < chunkGenerator.Diagnostics.Count; i++)
{
directiveNode.Diagnostics.Add(chunkGenerator.Diagnostics[i]);
}
_builder.Push(directiveNode);
_builder.Add(new DirectiveTokenIntermediateNode()
{
Content = chunkGenerator.Prefix,
Descriptor = CSharpCodeParser.TagHelperPrefixDirectiveDescriptor.Tokens.First(),
Source = BuildSourceSpanFromNode(span),
});
_builder.Pop();
}
protected SourceSpan? BuildSourceSpanFromNode(SyntaxTreeNode node)
{
var location = node.Start;
if (location == SourceLocation.Undefined)
{
return null;
}
var span = new SourceSpan(
node.Start.FilePath ?? FilePath,
node.Start.AbsoluteIndex,
node.Start.LineIndex,
node.Start.CharacterIndex,
node.Length);
return span;
}
}
private class MainSourceVisitor : LoweringVisitor
{
private readonly string _tagHelperPrefix;
public MainSourceVisitor(DocumentIntermediateNode document, IntermediateNodeBuilder builder, Dictionary<string, SourceSpan?> namespaces, string tagHelperPrefix)
: base(document, builder, namespaces)
{
_tagHelperPrefix = tagHelperPrefix;
}
// Example
// <input` checked="hello-world @false"`/>
// Name=checked
// Prefix= checked="
// Suffix="
public override void VisitAttributeBlock(AttributeBlockChunkGenerator chunkGenerator, Block block)
{
_builder.Push(new HtmlAttributeIntermediateNode()
{
AttributeName = chunkGenerator.Name,
Prefix = chunkGenerator.Prefix,
Suffix = chunkGenerator.Suffix,
Source = BuildSourceSpanFromNode(block),
});
VisitDefault(block);
_builder.Pop();
}
// Example
// <input checked="hello-world `@false`"/>
// Prefix= (space)
// Children will contain a token for @false.
public override void VisitDynamicAttributeBlock(DynamicAttributeBlockChunkGenerator chunkGenerator, Block block)
{
var firstChild = block.Children.FirstOrDefault(c => c.IsBlock) as Block;
if (firstChild == null || firstChild.Type == BlockKindInternal.Expression)
{
_builder.Push(new CSharpExpressionAttributeValueIntermediateNode()
{
Prefix = chunkGenerator.Prefix,
Source = BuildSourceSpanFromNode(block),
});
}
else
{
_builder.Push(new CSharpCodeAttributeValueIntermediateNode()
{
Prefix = chunkGenerator.Prefix,
Source = BuildSourceSpanFromNode(block),
});
}
VisitDefault(block);
_builder.Pop();
}
public override void VisitLiteralAttributeSpan(LiteralAttributeChunkGenerator chunkGenerator, Span span)
{
_builder.Push(new HtmlAttributeValueIntermediateNode()
{
Prefix = chunkGenerator.Prefix,
Source = BuildSourceSpanFromNode(span),
});
var location = chunkGenerator.Value.Location;
SourceSpan? valueSpan = null;
if (location != SourceLocation.Undefined)
{
valueSpan = new SourceSpan(
location.FilePath ?? FilePath,
location.AbsoluteIndex,
location.LineIndex,
location.CharacterIndex,
chunkGenerator.Value.Value.Length);
}
_builder.Add(new IntermediateToken()
{
Content = chunkGenerator.Value,
Kind = IntermediateToken.TokenKind.Html,
Source = valueSpan
});
_builder.Pop();
}
public override void VisitTemplateBlock(TemplateBlockChunkGenerator chunkGenerator, Block block)
{
var templateNode = new TemplateIntermediateNode();
_builder.Push(templateNode);
VisitDefault(block);
_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 ?? FilePath,
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 VisitExpressionBlock(ExpressionChunkGenerator chunkGenerator, Block block)
{
if (_builder.Current is CSharpExpressionAttributeValueIntermediateNode)
{
VisitDefault(block);
return;
}
var expressionNode = new CSharpExpressionIntermediateNode();
_builder.Push(expressionNode);
VisitDefault(block);
_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 ?? FilePath,
sourceRangeStart.Value.AbsoluteIndex,
sourceRangeStart.Value.LineIndex,
sourceRangeStart.Value.CharacterIndex,
contentLength);
}
}
}
public override void VisitExpressionSpan(ExpressionChunkGenerator chunkGenerator, Span span)
{
_builder.Add(new IntermediateToken()
{
Content = span.Content,
Kind = IntermediateToken.TokenKind.CSharp,
Source = BuildSourceSpanFromNode(span),
});
}
public override void VisitStatementSpan(StatementChunkGenerator chunkGenerator, Span span)
{
var isAttributeValue = _builder.Current is CSharpCodeAttributeValueIntermediateNode;
if (!isAttributeValue)
{
var statementNode = new CSharpCodeIntermediateNode()
{
Source = BuildSourceSpanFromNode(span)
};
_builder.Push(statementNode);
}
_builder.Add(new IntermediateToken()
{
Content = span.Content,
Kind = IntermediateToken.TokenKind.CSharp,
Source = BuildSourceSpanFromNode(span),
});
if (!isAttributeValue)
{
_builder.Pop();
}
}
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 source = BuildSourceSpanFromNode(span);
var currentChildren = _builder.Current.Children;
if (currentChildren.Count > 0 && currentChildren[currentChildren.Count - 1] is HtmlContentIntermediateNode)
{
var existingHtmlContent = (HtmlContentIntermediateNode)currentChildren[currentChildren.Count - 1];
if (existingHtmlContent.Source == null && source == null)
{
Combine(existingHtmlContent, span);
return;
}
if (source != null &&
existingHtmlContent.Source != null &&
existingHtmlContent.Source.Value.FilePath == source.Value.FilePath &&
existingHtmlContent.Source.Value.AbsoluteIndex + existingHtmlContent.Source.Value.Length == source.Value.AbsoluteIndex)
{
Combine(existingHtmlContent, span);
return;
}
}
var contentNode = new HtmlContentIntermediateNode()
{
Source = source
};
_builder.Push(contentNode);
_builder.Add(new IntermediateToken()
{
Content = span.Content,
Kind = IntermediateToken.TokenKind.Html,
Source = source,
});
_builder.Pop();
}
public override void VisitTagHelperBlock(TagHelperChunkGenerator chunkGenerator, Block block)
{
var tagHelperBlock = block as TagHelperBlock;
if (tagHelperBlock == null)
{
return;
}
var tagName = tagHelperBlock.TagName;
if (_tagHelperPrefix != null)
{
tagName = tagName.Substring(_tagHelperPrefix.Length);
}
var tagHelperNode = new TagHelperIntermediateNode()
{
TagName = tagName,
TagMode = tagHelperBlock.TagMode,
Source = BuildSourceSpanFromNode(block)
};
foreach (var tagHelper in tagHelperBlock.Binding.Descriptors)
{
tagHelperNode.TagHelpers.Add(tagHelper);
}
_builder.Push(tagHelperNode);
_builder.Push(new TagHelperBodyIntermediateNode());
VisitDefault(block);
_builder.Pop(); // Pop InitializeTagHelperStructureIntermediateNode
AddTagHelperAttributes(tagHelperBlock.Attributes, tagHelperBlock.Binding);
_builder.Pop(); // Pop TagHelperIntermediateNode
}
private void Combine(HtmlContentIntermediateNode node, Span span)
{
node.Children.Add(new IntermediateToken()
{
Content = span.Content,
Kind = IntermediateToken.TokenKind.Html,
Source = BuildSourceSpanFromNode(span),
});
if (node.Source != null)
{
Debug.Assert(node.Source.Value.FilePath != null);
node.Source = new SourceSpan(
node.Source.Value.FilePath,
node.Source.Value.AbsoluteIndex,
node.Source.Value.LineIndex,
node.Source.Value.CharacterIndex,
node.Source.Value.Length + span.Content.Length);
}
}
private void AddTagHelperAttributes(IList<TagHelperAttributeNode> attributes, TagHelperBinding tagHelperBinding)
{
var descriptors = tagHelperBinding.Descriptors;
var renderedBoundAttributeNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var attribute in attributes)
{
var attributeValueNode = attribute.Value;
var associatedDescriptors = descriptors.Where(descriptor =>
descriptor.BoundAttributes.Any(attributeDescriptor => TagHelperMatchingConventions.CanSatisfyBoundAttribute(attribute.Name, attributeDescriptor)));
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.BoundAttributes.First(a =>
{
return TagHelperMatchingConventions.CanSatisfyBoundAttribute(attribute.Name, a);
});
var setTagHelperProperty = new TagHelperPropertyIntermediateNode()
{
AttributeName = attribute.Name,
BoundAttribute = associatedAttributeDescriptor,
TagHelper = associatedDescriptor,
AttributeStructure = attribute.AttributeStructure,
Source = BuildSourceSpanFromNode(attributeValueNode),
IsIndexerNameMatch = TagHelperMatchingConventions.SatisfiesBoundAttributeIndexer(attribute.Name, associatedAttributeDescriptor),
};
_builder.Push(setTagHelperProperty);
attributeValueNode.Accept(this);
_builder.Pop();
}
}
else
{
var addHtmlAttribute = new TagHelperHtmlAttributeIntermediateNode()
{
AttributeName = attribute.Name,
AttributeStructure = attribute.AttributeStructure
};
_builder.Push(addHtmlAttribute);
if (attributeValueNode != null)
{
attributeValueNode.Accept(this);
}
_builder.Pop();
}
}
}
}
private class ImportsVisitor : LoweringVisitor
{
public ImportsVisitor(DocumentIntermediateNode document, IntermediateNodeBuilder builder, Dictionary<string, SourceSpan?> namespaces)
: base(document, new ImportBuilder(builder), namespaces)
{
}
private class ImportBuilder : IntermediateNodeBuilder
{
private readonly IntermediateNodeBuilder _innerBuilder;
public ImportBuilder(IntermediateNodeBuilder innerBuilder)
{
_innerBuilder = innerBuilder;
}
public override IntermediateNode Current => _innerBuilder.Current;
public override void Add(IntermediateNode node)
{
node.Annotations[CommonAnnotations.Imported] = CommonAnnotations.Imported;
_innerBuilder.Add(node);
}
public override IntermediateNode Build() => _innerBuilder.Build();
public override void Insert(int index, IntermediateNode node)
{
node.Annotations[CommonAnnotations.Imported] = CommonAnnotations.Imported;
_innerBuilder.Insert(index, node);
}
public override IntermediateNode Pop() => _innerBuilder.Pop();
public override void Push(IntermediateNode node)
{
node.Annotations[CommonAnnotations.Imported] = CommonAnnotations.Imported;
_innerBuilder.Push(node);
}
}
}
private class DirectiveVisitor : IntermediateNodeWalker
{
public List<IntermediateNodeReference> Directives = new List<IntermediateNodeReference>();
public override void VisitDirective(DirectiveIntermediateNode node)
{
Directives.Add(new IntermediateNodeReference(Parent, node));
base.VisitDirective(node);
}
}
private static bool IsMalformed(List<RazorDiagnostic> diagnostics)
=> diagnostics.Count > 0 && diagnostics.Any(diagnostic => diagnostic.Severity == RazorDiagnosticSeverity.Error);
}
}