From 51d19d174575621ec25ccddbf80bc2beae8adf2b Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 19 Dec 2018 22:43:10 -0800 Subject: [PATCH] Code dump of Blazor compiler BindTagHelperDescriptorProvider This change adds all of the code from the Blazor/Components compiler into the Razor language assembly. Minimal refactoring or integration work has been done, that is next :) All of the code compiles and all unit tests pass, except the one I had to skip until integration work is done. \n\nCommit migrated from https://github.com/dotnet/aspnetcore-tooling/commit/aa5c82ca5eb057ca7ce20824be4ad9a7b1f1c995 --- .../src/Components/BindLoweringPass.cs | 517 +++++++ .../Components/BlazorCSharpLoweringPhase.cs | 42 + .../src/Components/BlazorCodeTarget.cs | 59 + .../Components/BlazorDesignTimeNodeWriter.cs | 773 ++++++++++ .../Components/BlazorExtensionInitializer.cs | 130 ++ .../Components/BlazorImportProjectFeature.cs | 97 ++ .../src/Components/BlazorMetadata.cs | 83 ++ .../src/Components/BlazorNodeWriter.cs | 217 +++ .../src/Components/BlazorRuntimeNodeWriter.cs | 764 ++++++++++ .../BlazorTemplateTargetExtension.cs | 16 + .../Components/ChildContentDiagnosticPass.cs | 72 + .../src/Components/CodeWriterExtensions.cs | 647 +++++++++ .../Components/ComplexAttributeContentPass.cs | 107 ++ .../ComponentAttributeExtensionNode.cs | 131 ++ .../ComponentChildContentIntermediateNode.cs | 50 + .../ComponentDiagnosticFactory.cs | 94 +- .../ComponentDocumentClassifierPass.cs | 7 +- .../src/Components/ComponentExtensionNode.cs | 107 ++ .../src/Components/ComponentLoweringPass.cs | 488 +++++++ .../ComponentTypeArgumentExtensionNode.cs | 69 + ...nentTypeInferenceMethodIntermediateNode.cs | 59 + .../src/Components/ComponentsApi.cs | 133 ++ .../Components/EventHandlerLoweringPass.cs | 209 +++ .../src/Components/GenericComponentPass.cs | 314 +++++ .../Components/HtmlBlockIntermediateNode.cs | 46 + .../src/Components/HtmlBlockPass.cs | 331 +++++ .../Components/HtmlElementIntermediateNode.cs | 91 ++ .../src/Components/ImplementsDirective.cs | 32 + .../src/Components/ImplementsDirectivePass.cs | 37 + .../src/Components/InjectDirective.cs | 111 ++ .../src/Components/LayoutDirective.cs | 32 + .../src/Components/LayoutDirectivePass.cs | 51 + ...NetCore.Components.Razor.Extensions.csproj | 38 + .../src/Components/PageDirective.cs | 44 + .../src/Components/PageDirectivePass.cs | 82 ++ .../src/Components/RazorCompilerException.cs | 29 + .../src/Components/RefExtensionNode.cs | 66 + .../src/Components/RefLoweringPass.cs | 82 ++ .../Components/RouteAttributeExtensionNode.cs | 33 + .../src/Components/ScopeStack.cs | 91 ++ .../src/Components/ScriptTagPass.cs | 59 + ...elperBoundAttributeDescriptorExtensions.cs | 154 ++ .../TagHelperDescriptorExtensions.cs | 199 +++ .../src/Components/TemplateDiagnosticPass.cs | 60 + .../src/Components/TrimWhitespacePass.cs | 101 ++ .../src/Components/TypeParamDirective.cs | 32 + .../DocumentIntermediateNodeExtensions.cs | 33 + .../src/TypeNameFeature.cs | 18 + .../src/TypeNameRewriter.cs | 12 + .../src/GenerateCommand.cs | 2 - .../src/BindTagHelperDescriptorProvider.cs | 490 +++++++ .../ComponentTagHelperDescriptorProvider.cs | 440 +++++- .../src/DefaultTypeNameFeature.cs | 56 + ...EventHandlerTagHelperDescriptorProvider.cs | 218 +++ .../src/GenericTypeNameRewriter.cs | 70 + .../src/GlobalQualifiedTypeNameRewriter.cs | 79 ++ .../src/Properties/AssemblyInfo.cs | 1 + .../src/RefTagHelperDescriptorProvider.cs | 62 + .../BaseTagHelperDescriptorProviderTest.cs | 41 + .../BindTagHelperDescriptorProviderTest.cs | 683 +++++++++ ...omponentTagHelperDescriptorProviderTest.cs | 1243 +++++++++++++++++ ...tHandlerTagHelperDescriptorProviderTest.cs | 125 ++ .../test/GenericTypeNameRewriterTest.cs | 44 + .../GlobalQualifiedTypeNameRewriterTest.cs | 35 + .../RefTagHelperDescriptorProviderTest.cs | 85 ++ .../src/Microsoft.NET.Sdk.Razor.csproj | 8 +- .../BindElementAttribute.cs | 64 + .../BindInputElementAttribute.cs | 59 + .../BindMethods.cs | 229 +++ .../EventHandlerAttribute.cs | 45 + .../Layouts}/LayoutAttribute.cs | 0 .../RenderFrame.cs | 0 .../test/testassets/Directory.Build.props | 3 - 73 files changed, 11040 insertions(+), 91 deletions(-) create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BindLoweringPass.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorCSharpLoweringPhase.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorCodeTarget.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorDesignTimeNodeWriter.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorExtensionInitializer.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorImportProjectFeature.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorMetadata.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorNodeWriter.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorRuntimeNodeWriter.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorTemplateTargetExtension.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ChildContentDiagnosticPass.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/CodeWriterExtensions.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComplexAttributeContentPass.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentAttributeExtensionNode.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentChildContentIntermediateNode.cs rename src/Razor/Microsoft.AspNetCore.Razor.Language/src/{ => Components}/ComponentDiagnosticFactory.cs (82%) create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentExtensionNode.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentLoweringPass.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentTypeArgumentExtensionNode.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentTypeInferenceMethodIntermediateNode.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentsApi.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/EventHandlerLoweringPass.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/GenericComponentPass.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/HtmlBlockIntermediateNode.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/HtmlBlockPass.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/HtmlElementIntermediateNode.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ImplementsDirective.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ImplementsDirectivePass.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/InjectDirective.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/LayoutDirective.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/LayoutDirectivePass.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/Microsoft.AspNetCore.Components.Razor.Extensions.csproj create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/PageDirective.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/PageDirectivePass.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/RazorCompilerException.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/RefExtensionNode.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/RefLoweringPass.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/RouteAttributeExtensionNode.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ScopeStack.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ScriptTagPass.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/TagHelperBoundAttributeDescriptorExtensions.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/TagHelperDescriptorExtensions.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/TemplateDiagnosticPass.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/TrimWhitespacePass.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/TypeParamDirective.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/TypeNameFeature.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/TypeNameRewriter.cs create mode 100644 src/Razor/Microsoft.CodeAnalysis.Razor/src/BindTagHelperDescriptorProvider.cs create mode 100644 src/Razor/Microsoft.CodeAnalysis.Razor/src/DefaultTypeNameFeature.cs create mode 100644 src/Razor/Microsoft.CodeAnalysis.Razor/src/EventHandlerTagHelperDescriptorProvider.cs create mode 100644 src/Razor/Microsoft.CodeAnalysis.Razor/src/GenericTypeNameRewriter.cs create mode 100644 src/Razor/Microsoft.CodeAnalysis.Razor/src/GlobalQualifiedTypeNameRewriter.cs create mode 100644 src/Razor/Microsoft.CodeAnalysis.Razor/src/RefTagHelperDescriptorProvider.cs create mode 100644 src/Razor/Microsoft.CodeAnalysis.Razor/test/BaseTagHelperDescriptorProviderTest.cs create mode 100644 src/Razor/Microsoft.CodeAnalysis.Razor/test/BindTagHelperDescriptorProviderTest.cs create mode 100644 src/Razor/Microsoft.CodeAnalysis.Razor/test/ComponentTagHelperDescriptorProviderTest.cs create mode 100644 src/Razor/Microsoft.CodeAnalysis.Razor/test/EventHandlerTagHelperDescriptorProviderTest.cs create mode 100644 src/Razor/Microsoft.CodeAnalysis.Razor/test/GenericTypeNameRewriterTest.cs create mode 100644 src/Razor/Microsoft.CodeAnalysis.Razor/test/GlobalQualifiedTypeNameRewriterTest.cs create mode 100644 src/Razor/Microsoft.CodeAnalysis.Razor/test/RefTagHelperDescriptorProviderTest.cs create mode 100644 src/Razor/test/Microsoft.AspNetCore.Razor.Test.ComponentShim/Microsoft.AspNetCore.Components/BindElementAttribute.cs create mode 100644 src/Razor/test/Microsoft.AspNetCore.Razor.Test.ComponentShim/Microsoft.AspNetCore.Components/BindInputElementAttribute.cs create mode 100644 src/Razor/test/Microsoft.AspNetCore.Razor.Test.ComponentShim/Microsoft.AspNetCore.Components/BindMethods.cs create mode 100644 src/Razor/test/Microsoft.AspNetCore.Razor.Test.ComponentShim/Microsoft.AspNetCore.Components/EventHandlerAttribute.cs rename src/Razor/test/Microsoft.AspNetCore.Razor.Test.ComponentShim/{ => Microsoft.AspNetCore.Components/Layouts}/LayoutAttribute.cs (100%) rename src/Razor/test/Microsoft.AspNetCore.Razor.Test.ComponentShim/{ => Microsoft.AspNetCore.Components}/RenderFrame.cs (100%) diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BindLoweringPass.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BindLoweringPass.cs new file mode 100644 index 0000000000..c03cb3e707 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BindLoweringPass.cs @@ -0,0 +1,517 @@ +// 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.Linq; +using Microsoft.AspNetCore.Razor.Language.Extensions; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal class BindLoweringPass : IntermediateNodePassBase, IRazorOptimizationPass + { + // Run after event handler pass + public override int Order => 100; + + protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + var @namespace = documentNode.FindPrimaryNamespace(); + var @class = documentNode.FindPrimaryClass(); + if (@namespace == null || @class == null) + { + // Nothing to do, bail. We can't function without the standard structure. + return; + } + + // For each bind *usage* we need to rewrite the tag helper node to map to basic constructs. + var references = documentNode.FindDescendantReferences(); + + var parents = new HashSet(); + for (var i = 0; i < references.Count; i++) + { + parents.Add(references[i].Parent); + } + + foreach (var parent in parents) + { + ProcessDuplicates(parent); + } + + for (var i = 0; i < references.Count; i++) + { + var reference = references[i]; + var node = (TagHelperPropertyIntermediateNode)reference.Node; + + if (!reference.Parent.Children.Contains(node)) + { + // This node was removed as a duplicate, skip it. + continue; + } + + if (node.TagHelper.IsBindTagHelper() && node.AttributeName.StartsWith("bind")) + { + // Workaround for https://github.com/aspnet/Blazor/issues/703 + var rewritten = RewriteUsage(reference.Parent, node); + reference.Remove(); + + for (var j = 0; j < rewritten.Length; j++) + { + reference.Parent.Children.Add(rewritten[j]); + } + } + } + } + + private void ProcessDuplicates(IntermediateNode node) + { + // Reverse order because we will remove nodes. + // + // Each 'property' node could be duplicated if there are multiple tag helpers that match that + // particular attribute. This is common in our approach, which relies on 'fallback' tag helpers + // that overlap with more specific ones. + for (var i = node.Children.Count - 1; i >= 0; i--) + { + // For each usage of the general 'fallback' bind tag helper, it could duplicate + // the usage of a more specific one. Look for duplicates and remove the fallback. + var attribute = node.Children[i] as TagHelperPropertyIntermediateNode; + if (attribute != null && + attribute.TagHelper != null && + attribute.TagHelper.IsFallbackBindTagHelper()) + { + for (var j = 0; j < node.Children.Count; j++) + { + var duplicate = node.Children[j] as TagHelperPropertyIntermediateNode; + if (duplicate != null && + duplicate.TagHelper != null && + duplicate.TagHelper.IsBindTagHelper() && + duplicate.AttributeName == attribute.AttributeName && + !object.ReferenceEquals(attribute, duplicate)) + { + // Found a duplicate - remove the 'fallback' in favor of the + // more specific tag helper. + node.Children.RemoveAt(i); + break; + } + } + } + + // Also treat the general as a 'fallback' for that case and remove it. + // This is a workaround for a limitation where you can't write a tag helper that binds only + // when a specific attribute is **not** present. + if (attribute != null && + attribute.TagHelper != null && + attribute.TagHelper.IsInputElementFallbackBindTagHelper()) + { + for (var j = 0; j < node.Children.Count; j++) + { + var duplicate = node.Children[j] as TagHelperPropertyIntermediateNode; + if (duplicate != null && + duplicate.TagHelper != null && + duplicate.TagHelper.IsInputElementBindTagHelper() && + duplicate.AttributeName == attribute.AttributeName && + !object.ReferenceEquals(attribute, duplicate)) + { + // Found a duplicate - remove the 'fallback' input tag helper in favor of the + // more specific tag helper. + node.Children.RemoveAt(i); + break; + } + } + } + } + + // If we still have duplicates at this point then they are genuine conflicts. + var duplicates = node.Children + .OfType() + .GroupBy(p => p.AttributeName) + .Where(g => g.Count() > 1); + + foreach (var duplicate in duplicates) + { + node.Diagnostics.Add(ComponentDiagnosticFactory.CreateBindAttribute_Duplicates( + node.Source, + duplicate.Key, + duplicate.ToArray())); + foreach (var property in duplicate) + { + node.Children.Remove(property); + } + } + } + + private IntermediateNode[] RewriteUsage(IntermediateNode parent, TagHelperPropertyIntermediateNode node) + { + // Bind works similarly to a macro, it always expands to code that the user could have written. + // + // For the nodes that are related to the bind-attribute rewrite them to look like a pair of + // 'normal' HTML attributes similar to the following transformation. + // + // Input: + // Output: + // + // This means that the expression that appears inside of 'bind' must be an LValue or else + // there will be errors. In general the errors that come from C# in this case are good enough + // to understand the problem. + // + // The BindMethods calls are required in this case because to give us a good experience. They + // use overloading to ensure that can get an Action that will convert and set an arbitrary + // value. + // + // We also assume that the element will be treated as a component for now because + // multiple passes handle 'special' tag helpers. We have another pass that translates + // a tag helper node back into 'regular' element when it doesn't have an associated component + if (!TryComputeAttributeNames( + parent, + node, + node.AttributeName, + out var valueAttributeName, + out var changeAttributeName, + out var valueAttribute, + out var changeAttribute)) + { + // Skip anything we can't understand. It's important that we don't crash, that will bring down + // the build. + node.Diagnostics.Add(ComponentDiagnosticFactory.CreateBindAttribute_InvalidSyntax( + node.Source, + node.AttributeName)); + return new[] { node }; + } + + var original = GetAttributeContent(node); + if (string.IsNullOrEmpty(original.Content)) + { + // This can happen in error cases, the parser will already have flagged this + // as an error, so ignore it. + return new[] { node }; + } + + // Look for a matching format node. If we find one then we need to pass the format into the + // two nodes we generate. + IntermediateToken format = null; + if (TryGetFormatNode( + parent, + node, + valueAttributeName, + out var formatNode)) + { + // Don't write the format out as its own attribute, just capture it as a string + // or expression. + parent.Children.Remove(formatNode); + format = GetAttributeContent(formatNode); + } + + // Now rewrite the content of the value node to look like: + // + // BindMethods.GetValue() OR + // BindMethods.GetValue(, ) + var valueExpressionTokens = new List(); + valueExpressionTokens.Add(new IntermediateToken() + { + Content = $"{ComponentsApi.BindMethods.GetValue}(", + Kind = TokenKind.CSharp + }); + valueExpressionTokens.Add(original); + if (!string.IsNullOrEmpty(format?.Content)) + { + valueExpressionTokens.Add(new IntermediateToken() + { + Content = ", ", + Kind = TokenKind.CSharp, + }); + valueExpressionTokens.Add(format); + } + valueExpressionTokens.Add(new IntermediateToken() + { + Content = ")", + Kind = TokenKind.CSharp, + }); + + // Now rewrite the content of the change-handler node. There are two cases we care about + // here. If it's a component attribute, then don't use the 'BindMethods wrapper. We expect + // component attributes to always 'match' on type. + // + // __value => = __value + // + // For general DOM attributes, we need to be able to create a delegate that accepts UIEventArgs + // so we use BindMethods.SetValueHandler + // + // BindMethods.SetValueHandler(__value => = __value, ) OR + // BindMethods.SetValueHandler(__value => = __value, , ) + // + // Note that the linemappings here are applied to the value attribute, not the change attribute. + + string changeExpressionContent = null; + if (changeAttribute == null && format == null) + { + changeExpressionContent = $"{ComponentsApi.BindMethods.SetValueHandler}(__value => {original.Content} = __value, {original.Content})"; + } + else if (changeAttribute == null && format != null) + { + changeExpressionContent = $"{ComponentsApi.BindMethods.SetValueHandler}(__value => {original.Content} = __value, {original.Content}, {format.Content})"; + } + else + { + changeExpressionContent = $"__value => {original.Content} = __value"; + } + var changeExpressionTokens = new List() + { + new IntermediateToken() + { + Content = changeExpressionContent, + Kind = TokenKind.CSharp + } + }; + + if (parent is HtmlElementIntermediateNode) + { + var valueNode = new HtmlAttributeIntermediateNode() + { + AttributeName = valueAttributeName, + Source = node.Source, + + Prefix = valueAttributeName + "=\"", + Suffix = "\"", + }; + + for (var i = 0; i < node.Diagnostics.Count; i++) + { + valueNode.Diagnostics.Add(node.Diagnostics[i]); + } + + valueNode.Children.Add(new CSharpExpressionAttributeValueIntermediateNode()); + for (var i = 0; i < valueExpressionTokens.Count; i++) + { + valueNode.Children[0].Children.Add(valueExpressionTokens[i]); + } + + var changeNode = new HtmlAttributeIntermediateNode() + { + AttributeName = changeAttributeName, + Source = node.Source, + + Prefix = changeAttributeName + "=\"", + Suffix = "\"", + }; + + changeNode.Children.Add(new CSharpExpressionAttributeValueIntermediateNode()); + for (var i = 0; i < changeExpressionTokens.Count; i++) + { + changeNode.Children[0].Children.Add(changeExpressionTokens[i]); + } + + return new[] { valueNode, changeNode }; + } + else + { + var valueNode = new ComponentAttributeExtensionNode(node) + { + AttributeName = valueAttributeName, + BoundAttribute = valueAttribute, // Might be null if it doesn't match a component attribute + PropertyName = valueAttribute?.GetPropertyName(), + TagHelper = valueAttribute == null ? null : node.TagHelper, + TypeName = valueAttribute?.IsWeaklyTyped() == false ? valueAttribute.TypeName : null, + }; + + valueNode.Children.Clear(); + valueNode.Children.Add(new CSharpExpressionIntermediateNode()); + for (var i = 0; i < valueExpressionTokens.Count; i++) + { + valueNode.Children[0].Children.Add(valueExpressionTokens[i]); + } + + var changeNode = new ComponentAttributeExtensionNode(node) + { + AttributeName = changeAttributeName, + BoundAttribute = changeAttribute, // Might be null if it doesn't match a component attribute + PropertyName = changeAttribute?.GetPropertyName(), + TagHelper = changeAttribute == null ? null : node.TagHelper, + TypeName = changeAttribute?.IsWeaklyTyped() == false ? changeAttribute.TypeName : null, + }; + + changeNode.Children.Clear(); + changeNode.Children.Add(new CSharpExpressionIntermediateNode()); + for (var i = 0; i < changeExpressionTokens.Count; i++) + { + changeNode.Children[0].Children.Add(changeExpressionTokens[i]); + } + + return new[] { valueNode, changeNode }; + } + } + + private bool TryParseBindAttribute( + string attributeName, + out string valueAttributeName, + out string changeAttributeName) + { + valueAttributeName = null; + changeAttributeName = null; + + if (!attributeName.StartsWith("bind")) + { + return false; + } + + if (attributeName == "bind") + { + return true; + } + + var segments = attributeName.Split('-'); + for (var i = 0; i < segments.Length; i++) + { + if (string.IsNullOrEmpty(segments[i])) + { + return false; + } + } + + switch (segments.Length) + { + case 2: + valueAttributeName = segments[1]; + return true; + + case 3: + changeAttributeName = segments[2]; + valueAttributeName = segments[1]; + return true; + + default: + return false; + } + } + + // Attempts to compute the attribute names that should be used for an instance of 'bind'. + private bool TryComputeAttributeNames( + IntermediateNode parent, + TagHelperPropertyIntermediateNode node, + string attributeName, + out string valueAttributeName, + out string changeAttributeName, + out BoundAttributeDescriptor valueAttribute, + out BoundAttributeDescriptor changeAttribute) + { + valueAttribute = null; + changeAttribute = null; + + // Even though some of our 'bind' tag helpers specify the attribute names, they + // should still satisfy one of the valid syntaxes. + if (!TryParseBindAttribute(attributeName, out valueAttributeName, out changeAttributeName)) + { + return false; + } + + // The tag helper specifies attribute names, they should win. + // + // This handles cases like where the tag helper is + // generated to match a specific tag and has metadata that identify the attributes. + // + // We expect 1 bind tag helper per-node. + valueAttributeName = node.TagHelper.GetValueAttributeName() ?? valueAttributeName; + changeAttributeName = node.TagHelper.GetChangeAttributeName() ?? changeAttributeName; + + // We expect 0-1 components per-node. + var componentTagHelper = (parent as ComponentExtensionNode)?.Component; + if (componentTagHelper == null) + { + // If it's not a component node then there isn't too much else to figure out. + return attributeName != null && changeAttributeName != null; + } + + // If this is a component, we need an attribute name for the value. + if (attributeName == null) + { + return false; + } + + // If this is a component, then we can infer 'Changed' as the name + // of the change event. + if (changeAttributeName == null) + { + changeAttributeName = valueAttributeName + "Changed"; + } + + for (var i = 0; i < componentTagHelper.BoundAttributes.Count; i++) + { + var attribute = componentTagHelper.BoundAttributes[i]; + + if (string.Equals(valueAttributeName, attribute.Name)) + { + valueAttribute = attribute; + } + + if (string.Equals(changeAttributeName, attribute.Name)) + { + changeAttribute = attribute; + } + } + + return true; + } + + private bool TryGetFormatNode( + IntermediateNode node, + TagHelperPropertyIntermediateNode attributeNode, + string valueAttributeName, + out TagHelperPropertyIntermediateNode formatNode) + { + for (var i = 0; i < node.Children.Count; i++) + { + var child = node.Children[i] as TagHelperPropertyIntermediateNode; + if (child != null && + child.TagHelper != null && + child.TagHelper == attributeNode.TagHelper && + child.AttributeName == "format-" + valueAttributeName) + { + formatNode = child; + return true; + } + } + + formatNode = null; + return false; + } + + private static IntermediateToken GetAttributeContent(TagHelperPropertyIntermediateNode node) + { + var template = node.FindDescendantNodes().FirstOrDefault(); + if (template != null) + { + // See comments in TemplateDiagnosticPass + node.Diagnostics.Add(ComponentDiagnosticFactory.Create_TemplateInvalidLocation(template.Source)); + return new IntermediateToken() { Kind = TokenKind.CSharp, Content = string.Empty, }; + } + + if (node.Children[0] is HtmlContentIntermediateNode htmlContentNode) + { + // This case can be hit for a 'string' attribute. We want to turn it into + // an expression. + var content = "\"" + string.Join(string.Empty, htmlContentNode.Children.OfType().Select(t => t.Content)) + "\""; + return new IntermediateToken() { Kind = TokenKind.CSharp, Content = content }; + } + else if (node.Children[0] is CSharpExpressionIntermediateNode cSharpNode) + { + // This case can be hit when the attribute has an explicit @ inside, which + // 'escapes' any special sugar we provide for codegen. + return GetToken(cSharpNode); + } + else + { + // This is the common case for 'mixed' content + return GetToken(node); + } + + // In error cases we won't have a single token, but we still want to generate the code. + IntermediateToken GetToken(IntermediateNode parent) + { + return + parent.Children.Count == 1 ? (IntermediateToken)parent.Children[0] : new IntermediateToken() + { + Kind = TokenKind.CSharp, + Content = string.Join(string.Empty, parent.Children.OfType().Select(t => t.Content)), + }; + } + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorCSharpLoweringPhase.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorCSharpLoweringPhase.cs new file mode 100644 index 0000000000..fd9a026f13 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorCSharpLoweringPhase.cs @@ -0,0 +1,42 @@ +// 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.Language; +using Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal class BlazorRazorCSharpLoweringPhase : RazorEnginePhaseBase, IRazorCSharpLoweringPhase + { + protected override void ExecuteCore(RazorCodeDocument codeDocument) + { + var documentNode = codeDocument.GetDocumentIntermediateNode(); + ThrowForMissingDocumentDependency(documentNode); +#pragma warning disable CS0618 + var writer = new DocumentWriterWorkaround().Create(documentNode.Target, documentNode.Options); +#pragma warning restore CS0618 + try + { + var cSharpDocument = writer.WriteDocument(codeDocument, documentNode); + codeDocument.SetCSharpDocument(cSharpDocument); + } + catch (RazorCompilerException ex) + { + // Currently the Blazor code generation has some 'fatal errors' that can cause code generation + // to fail completely. This class is here to make that implementation work gracefully. + var cSharpDocument = RazorCSharpDocument.Create("", documentNode.Options, new[] { ex.Diagnostic }); + codeDocument.SetCSharpDocument(cSharpDocument); + } + } + + private class DocumentWriterWorkaround : DocumentWriter + { + public override RazorCSharpDocument WriteDocument(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + throw new NotImplementedException(); + } + } + } +} \ No newline at end of file diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorCodeTarget.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorCodeTarget.cs new file mode 100644 index 0000000000..1437e5432b --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorCodeTarget.cs @@ -0,0 +1,59 @@ +// 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.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.CodeGeneration; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + /// + /// Directs a to use . + /// + internal class BlazorCodeTarget : CodeTarget + { + private readonly RazorCodeGenerationOptions _options; + + public BlazorCodeTarget(RazorCodeGenerationOptions options, IEnumerable extensions) + { + _options = options; + Extensions = extensions.ToArray(); + } + + public ICodeTargetExtension[] Extensions { get; } + + public override IntermediateNodeWriter CreateNodeWriter() + { + return _options.DesignTime ? (BlazorNodeWriter)new BlazorDesignTimeNodeWriter() : new BlazorRuntimeNodeWriter(); + } + + public override TExtension GetExtension() + { + for (var i = 0; i < Extensions.Length; i++) + { + var match = Extensions[i] as TExtension; + if (match != null) + { + return match; + } + } + + return null; + } + + public override bool HasExtension() + { + for (var i = 0; i < Extensions.Length; i++) + { + var match = Extensions[i] as TExtension; + if (match != null) + { + return true; + } + } + + return false; + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorDesignTimeNodeWriter.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorDesignTimeNodeWriter.cs new file mode 100644 index 0000000000..963ea24f44 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorDesignTimeNodeWriter.cs @@ -0,0 +1,773 @@ +// 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.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Extensions; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + // Based on the DesignTimeNodeWriter from Razor repo. + internal class BlazorDesignTimeNodeWriter : BlazorNodeWriter + { + private readonly ScopeStack _scopeStack = new ScopeStack(); + + private static readonly string DesignTimeVariable = "__o"; + + public override void WriteHtmlBlock(CodeRenderingContext context, HtmlBlockIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + // Do nothing + } + + public override void WriteHtmlElement(CodeRenderingContext context, HtmlElementIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + context.RenderChildren(node); + } + + public override void WriteUsingDirective(CodeRenderingContext context, UsingDirectiveIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + if (node.Source.HasValue) + { + using (context.CodeWriter.BuildLinePragma(node.Source.Value)) + { + context.AddSourceMappingFor(node); + context.CodeWriter.WriteUsing(node.Content); + } + } + else + { + context.CodeWriter.WriteUsing(node.Content); + } + } + + public override void WriteCSharpExpression(CodeRenderingContext context, CSharpExpressionIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + if (node.Children.Count == 0) + { + return; + } + + if (node.Source != null) + { + using (context.CodeWriter.BuildLinePragma(node.Source.Value)) + { + var offset = DesignTimeVariable.Length + " = ".Length; + context.CodeWriter.WritePadding(offset, node.Source, context); + context.CodeWriter.WriteStartAssignment(DesignTimeVariable); + + for (var i = 0; i < node.Children.Count; i++) + { + if (node.Children[i] is IntermediateToken token && token.IsCSharp) + { + context.AddSourceMappingFor(token); + context.CodeWriter.Write(token.Content); + } + else + { + // There may be something else inside the expression like a Template or another extension node. + context.RenderNode(node.Children[i]); + } + } + + context.CodeWriter.WriteLine(";"); + } + } + else + { + context.CodeWriter.WriteStartAssignment(DesignTimeVariable); + for (var i = 0; i < node.Children.Count; i++) + { + if (node.Children[i] is IntermediateToken token && token.IsCSharp) + { + context.CodeWriter.Write(token.Content); + } + else + { + // There may be something else inside the expression like a Template or another extension node. + context.RenderNode(node.Children[i]); + } + } + context.CodeWriter.WriteLine(";"); + } + } + + public override void WriteCSharpCode(CodeRenderingContext context, CSharpCodeIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(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; + } + } + + IDisposable linePragmaScope = null; + if (node.Source != null) + { + if (!isWhitespaceStatement) + { + linePragmaScope = context.CodeWriter.BuildLinePragma(node.Source.Value); + } + + context.CodeWriter.WritePadding(0, node.Source.Value, context); + } + else if (isWhitespaceStatement) + { + // Don't write whitespace if there is no line mapping for it. + return; + } + + for (var i = 0; i < node.Children.Count; i++) + { + if (node.Children[i] is IntermediateToken token && token.IsCSharp) + { + context.AddSourceMappingFor(token); + context.CodeWriter.Write(token.Content); + } + else + { + // There may be something else inside the statement like an extension node. + context.RenderNode(node.Children[i]); + } + } + + if (linePragmaScope != null) + { + linePragmaScope.Dispose(); + } + else + { + context.CodeWriter.WriteLine(); + } + } + + public override void WriteHtmlAttribute(CodeRenderingContext context, HtmlAttributeIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + context.RenderChildren(node); + } + + public override void WriteHtmlAttributeValue(CodeRenderingContext context, HtmlAttributeValueIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + // Do nothing, this can't contain code. + } + + public override void WriteCSharpExpressionAttributeValue(CodeRenderingContext context, CSharpExpressionAttributeValueIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + if (node.Children.Count == 0) + { + return; + } + + var firstChild = node.Children[0]; + if (firstChild.Source != null) + { + using (context.CodeWriter.BuildLinePragma(firstChild.Source.Value)) + { + var offset = DesignTimeVariable.Length + " = ".Length; + context.CodeWriter.WritePadding(offset, firstChild.Source, context); + context.CodeWriter.WriteStartAssignment(DesignTimeVariable); + + for (var i = 0; i < node.Children.Count; i++) + { + if (node.Children[i] is IntermediateToken token && token.IsCSharp) + { + context.AddSourceMappingFor(token); + context.CodeWriter.Write(token.Content); + } + else + { + // There may be something else inside the expression like a Template or another extension node. + context.RenderNode(node.Children[i]); + } + } + + context.CodeWriter.WriteLine(";"); + } + } + else + { + context.CodeWriter.WriteStartAssignment(DesignTimeVariable); + for (var i = 0; i < node.Children.Count; i++) + { + if (node.Children[i] is IntermediateToken token && token.IsCSharp) + { + if (token.Source != null) + { + context.AddSourceMappingFor(token); + } + + context.CodeWriter.Write(token.Content); + } + else + { + // There may be something else inside the expression like a Template or another extension node. + context.RenderNode(node.Children[i]); + } + } + context.CodeWriter.WriteLine(";"); + } + } + + public override void WriteHtmlContent(CodeRenderingContext context, HtmlContentIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + // Do nothing + } + + public override void BeginWriteAttribute(CodeWriter codeWriter, string key) + { + if (codeWriter == null) + { + throw new ArgumentNullException(nameof(codeWriter)); + } + + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + codeWriter + .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(ComponentsApi.RenderTreeBuilder.AddAttribute)}") + .Write("-1") + .WriteParameterSeparator() + .WriteStringLiteral(key) + .WriteParameterSeparator(); + } + + public override void WriteComponent(CodeRenderingContext context, ComponentExtensionNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + if (node.TypeInferenceNode == null) + { + // Writes something like: + // + // builder.OpenComponent(0); + // builder.AddAttribute(1, "Foo", ...); + // builder.AddAttribute(2, "ChildContent", ...); + // builder.AddElementCapture(3, (__value) => _field = __value); + // builder.CloseComponent(); + foreach (var typeArgument in node.TypeArguments) + { + context.RenderNode(typeArgument); + } + + foreach (var attribute in node.Attributes) + { + context.RenderNode(attribute); + } + + if (node.ChildContents.Any()) + { + foreach (var childContent in node.ChildContents) + { + context.RenderNode(childContent); + } + } + else + { + // We eliminate 'empty' child content when building the tree so that usage like + // '\r\n' doesn't create a child content. + // + // Consider what would happen if the user's cursor was inside the element. At + // design -time we want to render an empty lambda to provide proper scoping + // for any code that the user types. + context.RenderNode(new ComponentChildContentIntermediateNode() + { + TypeName = ComponentsApi.RenderFragment.FullTypeName, + }); + } + + foreach (var capture in node.Captures) + { + context.RenderNode(capture); + } + } + else + { + // When we're doing type inference, we can't write all of the code inline to initialize + // the component on the builder. We generate a method elsewhere, and then pass all of the information + // to that method. We pass in all of the attribute values + the sequence numbers. + // + // __Blazor.MyComponent.TypeInference.CreateMyComponent_0(builder, 0, 1, ..., 2, ..., 3, ....); + var attributes = node.Attributes.ToList(); + var childContents = node.ChildContents.ToList(); + var captures = node.Captures.ToList(); + var remaining = attributes.Count + childContents.Count + captures.Count; + + context.CodeWriter.Write(node.TypeInferenceNode.FullTypeName); + context.CodeWriter.Write("."); + context.CodeWriter.Write(node.TypeInferenceNode.MethodName); + context.CodeWriter.Write("("); + + context.CodeWriter.Write(_scopeStack.BuilderVarName); + context.CodeWriter.Write(", "); + + context.CodeWriter.Write("-1"); + context.CodeWriter.Write(", "); + + for (var i = 0; i < attributes.Count; i++) + { + context.CodeWriter.Write("-1"); + context.CodeWriter.Write(", "); + + // Don't type check generics, since we can't actually write the type name. + // The type checking with happen anyway since we defined a method and we're generating + // a call to it. + WriteComponentAttributeInnards(context, attributes[i], canTypeCheck: false); + + remaining--; + if (remaining > 0) + { + context.CodeWriter.Write(", "); + } + } + + for (var i = 0; i < childContents.Count; i++) + { + context.CodeWriter.Write("-1"); + context.CodeWriter.Write(", "); + + WriteComponentChildContentInnards(context, childContents[i]); + + remaining--; + if (remaining > 0) + { + context.CodeWriter.Write(", "); + } + } + + for (var i = 0; i < captures.Count; i++) + { + context.CodeWriter.Write("-1"); + context.CodeWriter.Write(", "); + + WriteReferenceCaptureInnards(context, captures[i], shouldTypeCheck: false); + + remaining--; + if (remaining > 0) + { + context.CodeWriter.Write(", "); + } + } + + context.CodeWriter.Write(");"); + context.CodeWriter.WriteLine(); + } + } + + public override void WriteComponentAttribute(CodeRenderingContext context, ComponentAttributeExtensionNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + // Looks like: + // __o = 17; + context.CodeWriter.Write(DesignTimeVariable); + context.CodeWriter.Write(" = "); + + // Following the same design pattern as the runtime codegen + WriteComponentAttributeInnards(context, node, canTypeCheck: true); + + context.CodeWriter.Write(";"); + context.CodeWriter.WriteLine(); + } + + private void WriteComponentAttributeInnards(CodeRenderingContext context, ComponentAttributeExtensionNode node, bool canTypeCheck) + { + // We limit component attributes to simple cases. However there is still a lot of complexity + // to handle here, since there are a few different cases for how an attribute might be structured. + // + // This roughly follows the design of the runtime writer for simplicity. + if (node.AttributeStructure == AttributeStructure.Minimized) + { + // Minimized attributes always map to 'true' + context.CodeWriter.Write("true"); + } + else if (node.Children.Count > 1) + { + // We don't expect this to happen, we just want to know if it can. + throw new InvalidOperationException("Attribute nodes should either be minimized or a single type of content." + string.Join(", ", node.Children)); + } + else if (node.Children.Count == 1 && node.Children[0] is HtmlContentIntermediateNode) + { + // We don't actually need the content at designtime, an empty string will do. + context.CodeWriter.Write("\"\""); + } + else + { + // There are a few different forms that could be used to contain all of the tokens, but we don't really care + // exactly what it looks like - we just want all of the content. + // + // This can include an empty list in some cases like the following (sic): + // + // + // Of a list of tokens directly in the attribute. + var tokens = GetCSharpTokens(node); + + if ((node.BoundAttribute?.IsDelegateProperty() ?? false) || + (node.BoundAttribute?.IsChildContentProperty() ?? false)) + { + // We always surround the expression with the delegate constructor. This makes type + // inference inside lambdas, and method group conversion do the right thing. + if (canTypeCheck) + { + context.CodeWriter.Write("new "); + context.CodeWriter.Write(node.TypeName); + context.CodeWriter.Write("("); + } + context.CodeWriter.WriteLine(); + + for (var i = 0; i < tokens.Count; i++) + { + WriteCSharpToken(context, tokens[i]); + } + + if (canTypeCheck) + { + context.CodeWriter.Write(")"); + } + } + else + { + // This is the case when an attribute contains C# code + // + // If we have a parameter type, then add a type check. + if (canTypeCheck && NeedsTypeCheck(node)) + { + context.CodeWriter.Write(ComponentsApi.RuntimeHelpers.TypeCheck); + context.CodeWriter.Write("<"); + context.CodeWriter.Write(node.TypeName); + context.CodeWriter.Write(">"); + context.CodeWriter.Write("("); + } + + for (var i = 0; i < tokens.Count; i++) + { + WriteCSharpToken(context, tokens[i]); + } + + if (canTypeCheck && NeedsTypeCheck(node)) + { + context.CodeWriter.Write(")"); + } + } + } + + bool NeedsTypeCheck(ComponentAttributeExtensionNode n) + { + return n.BoundAttribute != null && !n.BoundAttribute.IsWeaklyTyped(); + } + + IReadOnlyList GetCSharpTokens(ComponentAttributeExtensionNode attribute) + { + // We generally expect all children to be CSharp, this is here just in case. + return attribute.FindDescendantNodes().Where(t => t.IsCSharp).ToArray(); + } + } + + public override void WriteComponentChildContent(CodeRenderingContext context, ComponentChildContentIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + // Writes something like: + // + // builder.AddAttribute(1, "ChildContent", (RenderFragment)((__builder73) => { ... })); + // OR + // builder.AddAttribute(1, "ChildContent", (RenderFragment)((person) => (__builder73) => { ... })); + BeginWriteAttribute(context.CodeWriter, node.AttributeName); + context.CodeWriter.Write($"({node.TypeName})("); + + WriteComponentChildContentInnards(context, node); + + context.CodeWriter.Write(")"); + context.CodeWriter.WriteEndMethodInvocation(); + } + + private void WriteComponentChildContentInnards(CodeRenderingContext context, ComponentChildContentIntermediateNode node) + { + // Writes something like: + // + // ((__builder73) => { ... }) + // OR + // ((person) => (__builder73) => { }) + _scopeStack.OpenComponentScope( + context, + node.AttributeName, + node.IsParameterized ? node.ParameterName : null); + for (var i = 0; i < node.Children.Count; i++) + { + context.RenderNode(node.Children[i]); + } + _scopeStack.CloseScope(context); + } + + public override void WriteComponentTypeArgument(CodeRenderingContext context, ComponentTypeArgumentExtensionNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + // At design type we want write the equivalent of: + // + // __o = typeof(TItem); + context.CodeWriter.Write(DesignTimeVariable); + context.CodeWriter.Write(" = "); + context.CodeWriter.Write("typeof("); + + var tokens = GetCSharpTokens(node); + for (var i = 0; i < tokens.Count; i++) + { + WriteCSharpToken(context, tokens[i]); + } + + context.CodeWriter.Write(");"); + context.CodeWriter.WriteLine(); + + IReadOnlyList GetCSharpTokens(ComponentTypeArgumentExtensionNode arg) + { + // We generally expect all children to be CSharp, this is here just in case. + return arg.FindDescendantNodes().Where(t => t.IsCSharp).ToArray(); + } + } + + public override void WriteTemplate(CodeRenderingContext context, TemplateIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + // Looks like: + // + // (__builder73) => { ... } + _scopeStack.OpenTemplateScope(context); + context.RenderChildren(node); + _scopeStack.CloseScope(context); + } + + public override void WriteReferenceCapture(CodeRenderingContext context, RefExtensionNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + // Looks like: + // + // __field = default(MyComponent); + WriteReferenceCaptureInnards(context, node, shouldTypeCheck: true); + } + + protected override void WriteReferenceCaptureInnards(CodeRenderingContext context, RefExtensionNode node, bool shouldTypeCheck) + { + // We specialize this code based on whether or not we can type check. When we're calling into + // a type-inferenced component, we can't do the type check. See the comments in WriteTypeInferenceMethod. + if (shouldTypeCheck) + { + // The runtime node writer moves the call elsewhere. At design time we + // just want sufficiently similar code that any unknown-identifier or type + // errors will be equivalent + var captureTypeName = node.IsComponentCapture + ? node.ComponentCaptureTypeName + : ComponentsApi.ElementRef.FullTypeName; + WriteCSharpCode(context, new CSharpCodeIntermediateNode + { + Source = node.Source, + Children = + { + node.IdentifierToken, + new IntermediateToken + { + Kind = TokenKind.CSharp, + Content = $" = default({captureTypeName});" + } + } + }); + } + else + { + // Looks like: + // + // (__value) = { _field = (MyComponent)__value; } + // OR + // (__value) = { _field = (ElementRef)__value; } + const string refCaptureParamName = "__value"; + using (var lambdaScope = context.CodeWriter.BuildLambda(refCaptureParamName)) + { + WriteCSharpCode(context, new CSharpCodeIntermediateNode + { + Source = node.Source, + Children = + { + node.IdentifierToken, + new IntermediateToken + { + Kind = TokenKind.CSharp, + Content = $" = {refCaptureParamName};" + } + } + }); + } + } + } + + private void WriteCSharpToken(CodeRenderingContext context, IntermediateToken token) + { + if (string.IsNullOrWhiteSpace(token.Content)) + { + return; + } + + if (token.Source?.FilePath == null) + { + context.CodeWriter.Write(token.Content); + return; + } + + using (context.CodeWriter.BuildLinePragma(token.Source)) + { + context.CodeWriter.WritePadding(0, token.Source.Value, context); + context.AddSourceMappingFor(token); + context.CodeWriter.Write(token.Content); + } + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorExtensionInitializer.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorExtensionInitializer.cs new file mode 100644 index 0000000000..9a8003de3d --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorExtensionInitializer.cs @@ -0,0 +1,130 @@ +// 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.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Extensions; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + /// + /// Initializes the Blazor extension. + /// + public class BlazorExtensionInitializer : RazorExtensionInitializer + { + /// + /// Specifies the declaration configuration. + /// + public static readonly RazorConfiguration DeclarationConfiguration; + + /// + /// Specifies the default configuration. + /// + public static readonly RazorConfiguration DefaultConfiguration; + + static BlazorExtensionInitializer() + { + // The configuration names here need to match what we put in the MSBuild configuration + DeclarationConfiguration = RazorConfiguration.Create( + RazorLanguageVersion.Experimental, + "BlazorDeclaration-0.1", + Array.Empty()); + + DefaultConfiguration = RazorConfiguration.Create( + RazorLanguageVersion.Experimental, + "Blazor-0.1", + Array.Empty()); + } + + /// + /// Registers the Blazor extension. + /// + /// The . + public static void Register(RazorProjectEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + FunctionsDirective.Register(builder); + ImplementsDirective.Register(builder); + InheritsDirective.Register(builder); + InjectDirective.Register(builder); + LayoutDirective.Register(builder); + PageDirective.Register(builder); + TypeParamDirective.Register(builder); + + builder.Features.Remove(builder.Features.OfType().Single()); + builder.Features.Add(new BlazorImportProjectFeature()); + + var index = builder.Phases.IndexOf(builder.Phases.OfType().Single()); + builder.Phases[index] = new BlazorRazorCSharpLoweringPhase(); + + builder.Features.Add(new ConfigureBlazorCodeGenerationOptions()); + + builder.AddTargetExtension(new BlazorTemplateTargetExtension()); + + var isDeclarationOnlyCompile = builder.Configuration.ConfigurationName == DeclarationConfiguration.ConfigurationName; + + // Blazor-specific passes, in order. + if (!isDeclarationOnlyCompile) + { + // There's no benefit in this optimization during the declaration-only compile + builder.Features.Add(new TrimWhitespacePass()); + } + builder.Features.Add(new ComponentDocumentClassifierPass()); + builder.Features.Add(new ScriptTagPass()); + builder.Features.Add(new ComplexAttributeContentPass()); + builder.Features.Add(new ComponentLoweringPass()); + builder.Features.Add(new EventHandlerLoweringPass()); + builder.Features.Add(new RefLoweringPass()); + builder.Features.Add(new BindLoweringPass()); + builder.Features.Add(new TemplateDiagnosticPass()); + builder.Features.Add(new GenericComponentPass()); + builder.Features.Add(new ChildContentDiagnosticPass()); + builder.Features.Add(new HtmlBlockPass()); + + if (isDeclarationOnlyCompile) + { + // This is for 'declaration only' processing. We don't want to try and emit any method bodies during + // the design time build because we can't do it correctly until the set of components is known. + builder.Features.Add(new EliminateMethodBodyPass()); + } + } + + /// + /// Initializes the Blazor extension. + /// + /// The . + public override void Initialize(RazorProjectEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + Register(builder); + } + + private class ConfigureBlazorCodeGenerationOptions : IConfigureRazorCodeGenerationOptionsFeature + { + public int Order => 0; + + public RazorEngine Engine { get; set; } + + public void Configure(RazorCodeGenerationOptionsBuilder options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + // These metadata attributes require a reference to the Razor.Runtime package which we don't + // otherwise need. + options.SuppressMetadataAttributes = true; + } + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorImportProjectFeature.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorImportProjectFeature.cs new file mode 100644 index 0000000000..a180d58f74 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorImportProjectFeature.cs @@ -0,0 +1,97 @@ +// 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.IO; +using System.Linq; +using System.Text; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal class BlazorImportProjectFeature : IImportProjectFeature + { + private const string ImportsFileName = "_ViewImports.cshtml"; + + private static readonly char[] PathSeparators = new char[]{ '/', '\\' }; + + // Using explicit newlines here to avoid fooling our baseline tests + private readonly static string DefaultUsingImportContent = + "\r\n" + + "@using System\r\n" + + "@using System.Collections.Generic\r\n" + + "@using System.Linq\r\n" + + "@using System.Threading.Tasks\r\n" + + "@using " + ComponentsApi.RenderFragment.Namespace + "\r\n"; // Microsoft.AspNetCore.Components + + public RazorProjectEngine ProjectEngine { get; set; } + + public IReadOnlyList GetImports(RazorProjectItem projectItem) + { + if (projectItem == null) + { + throw new ArgumentNullException(nameof(projectItem)); + } + + var imports = new List() + { + new VirtualProjectItem(DefaultUsingImportContent), + new VirtualProjectItem(@"@addTagHelper ""*, Microsoft.AspNetCore.Components"""), + }; + + // Try and infer a namespace from the project directory. We don't yet have the ability to pass + // the namespace through from the project. + if (projectItem.PhysicalPath != null && projectItem.FilePath != null) + { + // Avoiding the path-specific APIs here, we want to handle all styles of paths + // on all platforms + var trimLength = projectItem.FilePath.Length + (projectItem.FilePath.StartsWith("/") ? 0 : 1); + var baseDirectory = projectItem.PhysicalPath.Substring(0, projectItem.PhysicalPath.Length - trimLength); + + var lastSlash = baseDirectory.LastIndexOfAny(PathSeparators); + var baseNamespace = lastSlash == -1 ? baseDirectory : baseDirectory.Substring(lastSlash + 1); + if (!string.IsNullOrEmpty(baseNamespace)) + { + imports.Add(new VirtualProjectItem($@"@addTagHelper ""*, {baseNamespace}""")); + } + } + + // We add hierarchical imports second so any default directive imports can be overridden. + imports.AddRange(GetHierarchicalImports(ProjectEngine.FileSystem, projectItem)); + + return imports; + } + + // Temporary API until we fully convert to RazorProjectEngine + public IEnumerable GetHierarchicalImports(RazorProject project, RazorProjectItem projectItem) + { + // We want items in descending order. FindHierarchicalItems returns items in ascending order. + return project.FindHierarchicalItems(projectItem.FilePath, ImportsFileName).Reverse(); + } + + private class VirtualProjectItem : RazorProjectItem + { + private readonly byte[] _bytes; + + public VirtualProjectItem(string content) + { + var preamble = Encoding.UTF8.GetPreamble(); + var contentBytes = Encoding.UTF8.GetBytes(content); + + _bytes = new byte[preamble.Length + contentBytes.Length]; + preamble.CopyTo(_bytes, 0); + contentBytes.CopyTo(_bytes, preamble.Length); + } + + public override string BasePath => null; + + public override string FilePath => null; + + public override string PhysicalPath => null; + + public override bool Exists => true; + + public override Stream Read() => new MemoryStream(_bytes); + } + } +} \ No newline at end of file diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorMetadata.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorMetadata.cs new file mode 100644 index 0000000000..688d3a6135 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorMetadata.cs @@ -0,0 +1,83 @@ +// 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.Language.Components +{ + // Metadata used for Blazor's interactions with the tag helper system + internal static class BlazorMetadata + { + // There's a bug in the 15.7 preview 1 Razor that prevents 'Kind' from being serialized + // this affects both tooling and build. For now our workaround is to ignore 'Kind' and + // use our own metadata entry to denote non-Component tag helpers. + public static readonly string SpecialKindKey = "Blazor.IsSpecialKind"; + + public static class Bind + { + public static readonly string RuntimeName = "Blazor.None"; + + public readonly static string TagHelperKind = "Blazor.Bind"; + + public readonly static string FallbackKey = "Blazor.Bind.Fallback"; + + public readonly static string TypeAttribute = "Blazor.Bind.TypeAttribute"; + + public readonly static string ValueAttribute = "Blazor.Bind.ValueAttribute"; + + public readonly static string ChangeAttribute = "Blazor.Bind.ChangeAttribute"; + } + + public static class ChildContent + { + public static readonly string RuntimeName = "Blazor.None"; + + public static readonly string TagHelperKind = "Blazor.ChildContent"; + + public static readonly string ParameterNameBoundAttributeKind = "Blazor.ChildContentParameterName"; + + /// + /// The name of the synthesized attribute used to set a child content parameter. + /// + public static readonly string ParameterAttributeName = "Context"; + + /// + /// The default name of the child content parameter (unless set by a Context attribute). + /// + public static readonly string DefaultParameterName = "context"; + } + + public static class Component + { + public static readonly string ChildContentKey = "Blazor.ChildContent"; + + public static readonly string ChildContentParameterNameKey = "Blazor.ChildContentParameterName"; + + public static readonly string DelegateSignatureKey = "Blazor.DelegateSignature"; + + public static readonly string WeaklyTypedKey = "Blazor.IsWeaklyTyped"; + + public static readonly string RuntimeName = "Blazor.IComponent"; + + public readonly static string TagHelperKind = "Blazor.Component"; + + public readonly static string GenericTypedKey = "Blazor.GenericTyped"; + + public readonly static string TypeParameterKey = "Blazor.TypeParameter"; + } + + public static class EventHandler + { + public static readonly string EventArgsType = "Blazor.EventHandler.EventArgs"; + + public static readonly string RuntimeName = "Blazor.None"; + + public readonly static string TagHelperKind = "Blazor.EventHandler"; + } + + public static class Ref + { + public readonly static string TagHelperKind = "Blazor.Ref"; + + public static readonly string RuntimeName = "Blazor.None"; + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorNodeWriter.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorNodeWriter.cs new file mode 100644 index 0000000000..7e3a8e199d --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorNodeWriter.cs @@ -0,0 +1,217 @@ +// 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.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Extensions; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal abstract class BlazorNodeWriter : IntermediateNodeWriter + { + public abstract void BeginWriteAttribute(CodeWriter codeWriter, string key); + + public abstract void WriteComponent(CodeRenderingContext context, ComponentExtensionNode node); + + public abstract void WriteComponentAttribute(CodeRenderingContext context, ComponentAttributeExtensionNode node); + + public abstract void WriteComponentChildContent(CodeRenderingContext context, ComponentChildContentIntermediateNode node); + + public abstract void WriteComponentTypeArgument(CodeRenderingContext context, ComponentTypeArgumentExtensionNode node); + + public abstract void WriteHtmlElement(CodeRenderingContext context, HtmlElementIntermediateNode node); + + public abstract void WriteHtmlBlock(CodeRenderingContext context, HtmlBlockIntermediateNode node); + + public abstract void WriteReferenceCapture(CodeRenderingContext context, RefExtensionNode node); + + protected abstract void WriteReferenceCaptureInnards(CodeRenderingContext context, RefExtensionNode node, bool shouldTypeCheck); + + public abstract void WriteTemplate(CodeRenderingContext context, TemplateIntermediateNode node); + + public sealed override void BeginWriterScope(CodeRenderingContext context, string writer) + { + throw new NotImplementedException(nameof(BeginWriterScope)); + } + + public sealed override void EndWriterScope(CodeRenderingContext context) + { + throw new NotImplementedException(nameof(EndWriterScope)); + } + + public sealed override void WriteCSharpCodeAttributeValue(CodeRenderingContext context, CSharpCodeAttributeValueIntermediateNode node) + { + // We used to support syntaxes like but this is no longer the + // case. + // + // We provide an error for this case just to be friendly. + var content = string.Join("", node.Children.OfType().Select(t => t.Content)); + context.Diagnostics.Add(ComponentDiagnosticFactory.Create_CodeBlockInAttribute(node.Source, content)); + return; + } + + + // Currently the same for design time and runtime + public void WriteComponentTypeInferenceMethod(CodeRenderingContext context, ComponentTypeInferenceMethodIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + // This is ugly because CodeWriter doesn't allow us to erase, but we need to comma-delimit. So we have to + // materizalize something can iterate, or use string.Join. We'll need this multiple times, so materializing + // it. + var parameters = GetParameterDeclarations(); + + // This is really similar to the code in WriteComponentAttribute and WriteComponentChildContent - except simpler because + // attributes and child contents look like variables. + // + // Looks like: + // + // public static void CreateFoo_0(RenderTreeBuilder builder, int seq, int __seq0, T1 __arg0, int __seq1, global::System.Collections.Generic.List __arg1, int __seq2, string __arg2) + // { + // builder.OpenComponent>(); + // builder.AddAttribute(__seq0, "Attr0", __arg0); + // builder.AddAttribute(__seq1, "Attr1", __arg1); + // builder.AddAttribute(__seq2, "Attr2", __arg2); + // builder.CloseComponent(); + // } + // + // As a special case, we need to generate a thunk for captures in this block instead of using + // them verbatim. + // + // The problem is that RenderTreeBuilder wants an Action. The caller can't write the type + // name if it contains generics, and we can't write the variable they want to assign to. + var writer = context.CodeWriter; + + writer.Write("public static void "); + writer.Write(node.MethodName); + + writer.Write("<"); + writer.Write(string.Join(", ", node.Component.Component.GetTypeParameters().Select(a => a.Name))); + writer.Write(">"); + + writer.Write("("); + writer.Write("global::"); + writer.Write(ComponentsApi.RenderTreeBuilder.FullTypeName); + writer.Write(" builder"); + writer.Write(", "); + writer.Write("int seq"); + + if (parameters.Count > 0) + { + writer.Write(", "); + } + + for (var i = 0; i < parameters.Count; i++) + { + writer.Write("int "); + writer.Write(parameters[i].seqName); + + writer.Write(", "); + writer.Write(parameters[i].typeName); + writer.Write(" "); + writer.Write(parameters[i].parameterName); + + if (i < parameters.Count - 1) + { + writer.Write(", "); + } + } + + writer.Write(")"); + writer.WriteLine(); + + writer.WriteLine("{"); + + // builder.OpenComponent(42); + context.CodeWriter.Write("builder"); + context.CodeWriter.Write("."); + context.CodeWriter.Write(ComponentsApi.RenderTreeBuilder.OpenComponent); + context.CodeWriter.Write("<"); + context.CodeWriter.Write(node.Component.TypeName); + context.CodeWriter.Write(">("); + context.CodeWriter.Write("seq"); + context.CodeWriter.Write(");"); + context.CodeWriter.WriteLine(); + + var index = 0; + foreach (var attribute in node.Component.Attributes) + { + context.CodeWriter.WriteStartInstanceMethodInvocation("builder", ComponentsApi.RenderTreeBuilder.AddAttribute); + context.CodeWriter.Write(parameters[index].seqName); + context.CodeWriter.Write(", "); + + context.CodeWriter.Write($"\"{attribute.AttributeName}\""); + context.CodeWriter.Write(", "); + + context.CodeWriter.Write(parameters[index].parameterName); + context.CodeWriter.WriteEndMethodInvocation(); + + index++; + } + + foreach (var childContent in node.Component.ChildContents) + { + context.CodeWriter.WriteStartInstanceMethodInvocation("builder", ComponentsApi.RenderTreeBuilder.AddAttribute); + context.CodeWriter.Write(parameters[index].seqName); + context.CodeWriter.Write(", "); + + context.CodeWriter.Write($"\"{childContent.AttributeName}\""); + context.CodeWriter.Write(", "); + + context.CodeWriter.Write(parameters[index].parameterName); + context.CodeWriter.WriteEndMethodInvocation(); + + index++; + } + + foreach (var capture in node.Component.Captures) + { + context.CodeWriter.WriteStartInstanceMethodInvocation("builder", capture.IsComponentCapture ? ComponentsApi.RenderTreeBuilder.AddComponentReferenceCapture : ComponentsApi.RenderTreeBuilder.AddElementReferenceCapture); + context.CodeWriter.Write(parameters[index].seqName); + context.CodeWriter.Write(", "); + + var cast = capture.IsComponentCapture ? $"({capture.ComponentCaptureTypeName})" : string.Empty; + context.CodeWriter.Write($"(__value) => {{ {parameters[index].parameterName}({cast}__value); }}"); + context.CodeWriter.WriteEndMethodInvocation(); + + index++; + } + + context.CodeWriter.WriteInstanceMethodInvocation("builder", ComponentsApi.RenderTreeBuilder.CloseComponent); + + writer.WriteLine("}"); + + List<(string seqName, string typeName, string parameterName)> GetParameterDeclarations() + { + var p = new List<(string seqName, string typeName, string parameterName)>(); + foreach (var attribute in node.Component.Attributes) + { + p.Add(($"__seq{p.Count}", attribute.TypeName, $"__arg{p.Count}")); + } + + foreach (var childContent in node.Component.ChildContents) + { + p.Add(($"__seq{p.Count}", childContent.TypeName, $"__arg{p.Count}")); + } + + foreach (var capture in node.Component.Captures) + { + p.Add(($"__seq{p.Count}", capture.TypeName, $"__arg{p.Count}")); + } + + return p; + } + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorRuntimeNodeWriter.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorRuntimeNodeWriter.cs new file mode 100644 index 0000000000..70a8965c18 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorRuntimeNodeWriter.cs @@ -0,0 +1,764 @@ +// 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 System.Text; +using Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Extensions; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + /// + /// Generates the C# code corresponding to Razor source document contents. + /// + internal class BlazorRuntimeNodeWriter : BlazorNodeWriter + { + private readonly List _currentAttributeValues = new List(); + private readonly ScopeStack _scopeStack = new ScopeStack(); + private int _sourceSequence = 0; + + public override void WriteCSharpCode(CodeRenderingContext context, CSharpCodeIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(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) + { + // The runtime and design time code differ in their handling of whitespace-only + // statements. At runtime we can discard them completely. At design time we need + // to keep them for the editor. + return; + } + + IDisposable linePragmaScope = null; + if (node.Source != null) + { + linePragmaScope = context.CodeWriter.BuildLinePragma(node.Source.Value); + context.CodeWriter.WritePadding(0, node.Source.Value, context); + } + + for (var i = 0; i < node.Children.Count; i++) + { + if (node.Children[i] is IntermediateToken token && token.IsCSharp) + { + context.AddSourceMappingFor(token); + context.CodeWriter.Write(token.Content); + } + else + { + // There may be something else inside the statement like an extension node. + context.RenderNode(node.Children[i]); + } + } + + if (linePragmaScope != null) + { + linePragmaScope.Dispose(); + } + else + { + context.CodeWriter.WriteLine(); + } + } + + public override void WriteCSharpExpression(CodeRenderingContext context, CSharpExpressionIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + // Since we're not in the middle of writing an element, this must evaluate as some + // text to display + context.CodeWriter + .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(ComponentsApi.RenderTreeBuilder.AddContent)}") + .Write((_sourceSequence++).ToString()) + .WriteParameterSeparator(); + + for (var i = 0; i < node.Children.Count; i++) + { + if (node.Children[i] is IntermediateToken token && token.IsCSharp) + { + context.CodeWriter.Write(token.Content); + } + else + { + // There may be something else inside the expression like a Template or another extension node. + context.RenderNode(node.Children[i]); + } + } + + context.CodeWriter.WriteEndMethodInvocation(); + } + + public override void WriteCSharpExpressionAttributeValue(CodeRenderingContext context, CSharpExpressionAttributeValueIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + // In cases like "somestring @variable", Razor tokenizes it as: + // [0] HtmlContent="somestring" + // [1] CsharpContent="variable" Prefix=" " + // ... so to avoid losing whitespace, convert the prefix to a further token in the list + if (!string.IsNullOrEmpty(node.Prefix)) + { + _currentAttributeValues.Add(new IntermediateToken() { Kind = TokenKind.Html, Content = node.Prefix }); + } + + for (var i = 0; i < node.Children.Count; i++) + { + _currentAttributeValues.Add((IntermediateToken)node.Children[i]); + } + } + + public override void WriteHtmlBlock(CodeRenderingContext context, HtmlBlockIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + context.CodeWriter + .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(ComponentsApi.RenderTreeBuilder.AddMarkupContent)}") + .Write((_sourceSequence++).ToString()) + .WriteParameterSeparator() + .WriteStringLiteral(node.Content) + .WriteEndMethodInvocation(); + } + + public override void WriteHtmlElement(CodeRenderingContext context, HtmlElementIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + context.CodeWriter + .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(ComponentsApi.RenderTreeBuilder.OpenElement)}") + .Write((_sourceSequence++).ToString()) + .WriteParameterSeparator() + .WriteStringLiteral(node.TagName) + .WriteEndMethodInvocation(); + + // Render Attributes before creating the scope. + foreach (var attribute in node.Attributes) + { + context.RenderNode(attribute); + } + + foreach (var capture in node.Captures) + { + context.RenderNode(capture); + } + + // Render body of the tag inside the scope + foreach (var child in node.Body) + { + context.RenderNode(child); + } + + context.CodeWriter + .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{ComponentsApi.RenderTreeBuilder.CloseElement}") + .WriteEndMethodInvocation(); + } + + public override void WriteHtmlAttribute(CodeRenderingContext context, HtmlAttributeIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + Debug.Assert(_currentAttributeValues.Count == 0); + context.RenderChildren(node); + + WriteAttribute(context.CodeWriter, node.AttributeName, _currentAttributeValues); + _currentAttributeValues.Clear(); + } + + public override void WriteHtmlAttributeValue(CodeRenderingContext context, HtmlAttributeValueIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + var stringContent = ((IntermediateToken)node.Children.Single()).Content; + _currentAttributeValues.Add(new IntermediateToken() { Kind = TokenKind.Html, Content = node.Prefix + stringContent, }); + } + + public override void WriteHtmlContent(CodeRenderingContext context, HtmlContentIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + // Text node + var content = GetHtmlContent(node); + context.CodeWriter + .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(ComponentsApi.RenderTreeBuilder.AddContent)}") + .Write((_sourceSequence++).ToString()) + .WriteParameterSeparator() + .WriteStringLiteral(content) + .WriteEndMethodInvocation(); + } + + public override void WriteUsingDirective(CodeRenderingContext context, UsingDirectiveIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + context.CodeWriter.WriteUsing(node.Content, endLine: true); + } + + public override void WriteComponent(CodeRenderingContext context, ComponentExtensionNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + if (node.TypeInferenceNode == null) + { + // If the component is using not using type inference then we just write an open/close with a series + // of add attribute calls in between. + // + // Writes something like: + // + // builder.OpenComponent(0); + // builder.AddAttribute(1, "Foo", ...); + // builder.AddAttribute(2, "ChildContent", ...); + // builder.AddElementCapture(3, (__value) => _field = __value); + // builder.CloseComponent(); + + // builder.OpenComponent(42); + context.CodeWriter.Write(_scopeStack.BuilderVarName); + context.CodeWriter.Write("."); + context.CodeWriter.Write(ComponentsApi.RenderTreeBuilder.OpenComponent); + context.CodeWriter.Write("<"); + context.CodeWriter.Write(node.TypeName); + context.CodeWriter.Write(">("); + context.CodeWriter.Write((_sourceSequence++).ToString()); + context.CodeWriter.Write(");"); + context.CodeWriter.WriteLine(); + + // We can skip type arguments during runtime codegen, they are handled in the + // type/parameter declarations. + + foreach (var attribute in node.Attributes) + { + context.RenderNode(attribute); + } + + foreach (var childContent in node.ChildContents) + { + context.RenderNode(childContent); + } + + foreach (var capture in node.Captures) + { + context.RenderNode(capture); + } + + // builder.CloseComponent(); + context.CodeWriter.Write(_scopeStack.BuilderVarName); + context.CodeWriter.Write("."); + context.CodeWriter.Write(ComponentsApi.RenderTreeBuilder.CloseComponent); + context.CodeWriter.Write("();"); + context.CodeWriter.WriteLine(); + } + else + { + // When we're doing type inference, we can't write all of the code inline to initialize + // the component on the builder. We generate a method elsewhere, and then pass all of the information + // to that method. We pass in all of the attribute values + the sequence numbers. + // + // __Blazor.MyComponent.TypeInference.CreateMyComponent_0(builder, 0, 1, ..., 2, ..., 3, ...); + var attributes = node.Attributes.ToList(); + var childContents = node.ChildContents.ToList(); + var captures = node.Captures.ToList(); + var remaining = attributes.Count + childContents.Count + captures.Count; + + context.CodeWriter.Write(node.TypeInferenceNode.FullTypeName); + context.CodeWriter.Write("."); + context.CodeWriter.Write(node.TypeInferenceNode.MethodName); + context.CodeWriter.Write("("); + + context.CodeWriter.Write(_scopeStack.BuilderVarName); + context.CodeWriter.Write(", "); + + context.CodeWriter.Write((_sourceSequence++).ToString()); + context.CodeWriter.Write(", "); + + for (var i = 0; i < attributes.Count; i++) + { + context.CodeWriter.Write((_sourceSequence++).ToString()); + context.CodeWriter.Write(", "); + + // Don't type check generics, since we can't actually write the type name. + // The type checking with happen anyway since we defined a method and we're generating + // a call to it. + WriteComponentAttributeInnards(context, attributes[i], canTypeCheck: false); + + remaining--; + if (remaining > 0) + { + context.CodeWriter.Write(", "); + } + } + + for (var i = 0; i < childContents.Count; i++) + { + context.CodeWriter.Write((_sourceSequence++).ToString()); + context.CodeWriter.Write(", "); + + WriteComponentChildContentInnards(context, childContents[i]); + + remaining--; + if (remaining > 0) + { + context.CodeWriter.Write(", "); + } + } + + for (var i = 0; i < captures.Count; i++) + { + context.CodeWriter.Write((_sourceSequence++).ToString()); + context.CodeWriter.Write(", "); + + WriteReferenceCaptureInnards(context, captures[i], shouldTypeCheck: false); + + remaining--; + if (remaining > 0) + { + context.CodeWriter.Write(", "); + } + } + + context.CodeWriter.Write(");"); + context.CodeWriter.WriteLine(); + } + } + + public override void WriteComponentAttribute(CodeRenderingContext context, ComponentAttributeExtensionNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + // builder.AddAttribute(1, "Foo", 42); + context.CodeWriter.Write(_scopeStack.BuilderVarName); + context.CodeWriter.Write("."); + context.CodeWriter.Write(ComponentsApi.RenderTreeBuilder.AddAttribute); + context.CodeWriter.Write("("); + context.CodeWriter.Write((_sourceSequence++).ToString()); + context.CodeWriter.Write(", "); + context.CodeWriter.WriteStringLiteral(node.AttributeName); + context.CodeWriter.Write(", "); + + WriteComponentAttributeInnards(context, node, canTypeCheck: true); + + context.CodeWriter.Write(");"); + context.CodeWriter.WriteLine(); + } + + private void WriteComponentAttributeInnards(CodeRenderingContext context, ComponentAttributeExtensionNode node, bool canTypeCheck) + { + if (node.AttributeStructure == AttributeStructure.Minimized) + { + // Minimized attributes always map to 'true' + context.CodeWriter.Write("true"); + } + else if (node.Children.Count > 1) + { + // We don't expect this to happen, we just want to know if it can. + throw new InvalidOperationException("Attribute nodes should either be minimized or a single type of content." + string.Join(", ", node.Children)); + } + else if (node.Children.Count == 1 && node.Children[0] is HtmlContentIntermediateNode htmlNode) + { + // This is how string attributes are lowered by default, a single HTML node with a single HTML token. + var content = string.Join(string.Empty, GetHtmlTokens(htmlNode).Select(t => t.Content)); + context.CodeWriter.WriteStringLiteral(content); + } + else + { + // See comments in BlazorDesignTimeNodeWriter for a description of the cases that are possible. + var tokens = GetCSharpTokens(node); + if ((node.BoundAttribute?.IsDelegateProperty() ?? false) || + (node.BoundAttribute?.IsChildContentProperty() ?? false)) + { + if (canTypeCheck) + { + context.CodeWriter.Write("new "); + context.CodeWriter.Write(node.TypeName); + context.CodeWriter.Write("("); + } + + for (var i = 0; i < tokens.Count; i++) + { + context.CodeWriter.Write(tokens[i].Content); + } + + if (canTypeCheck) + { + context.CodeWriter.Write(")"); + } + } + else + { + if (canTypeCheck && NeedsTypeCheck(node)) + { + context.CodeWriter.Write(ComponentsApi.RuntimeHelpers.TypeCheck); + context.CodeWriter.Write("<"); + context.CodeWriter.Write(node.TypeName); + context.CodeWriter.Write(">"); + context.CodeWriter.Write("("); + } + + for (var i = 0; i < tokens.Count; i++) + { + context.CodeWriter.Write(tokens[i].Content); + } + + if (canTypeCheck && NeedsTypeCheck(node)) + { + context.CodeWriter.Write(")"); + } + } + } + + IReadOnlyList GetCSharpTokens(ComponentAttributeExtensionNode attribute) + { + // We generally expect all children to be CSharp, this is here just in case. + return attribute.FindDescendantNodes().Where(t => t.IsCSharp).ToArray(); + } + + IReadOnlyList GetHtmlTokens(HtmlContentIntermediateNode html) + { + // We generally expect all children to be HTML, this is here just in case. + return html.FindDescendantNodes().Where(t => t.IsHtml).ToArray(); + } + + bool NeedsTypeCheck(ComponentAttributeExtensionNode n) + { + return node.BoundAttribute != null && !node.BoundAttribute.IsWeaklyTyped(); + } + } + + public override void WriteComponentChildContent(CodeRenderingContext context, ComponentChildContentIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + // Writes something like: + // + // builder.AddAttribute(1, "ChildContent", (RenderFragment)((__builder73) => { ... })); + // OR + // builder.AddAttribute(1, "ChildContent", (RenderFragment)((person) => (__builder73) => { ... })); + BeginWriteAttribute(context.CodeWriter, node.AttributeName); + context.CodeWriter.Write($"({node.TypeName})("); + + WriteComponentChildContentInnards(context, node); + + context.CodeWriter.Write(")"); + context.CodeWriter.WriteEndMethodInvocation(); + } + + private void WriteComponentChildContentInnards(CodeRenderingContext context, ComponentChildContentIntermediateNode node) + { + // Writes something like: + // + // ((__builder73) => { ... }) + // OR + // ((person) => (__builder73) => { }) + _scopeStack.OpenComponentScope( + context, + node.AttributeName, + node.IsParameterized ? node.ParameterName : null); + for (var i = 0; i < node.Children.Count; i++) + { + context.RenderNode(node.Children[i]); + } + _scopeStack.CloseScope(context); + } + + public override void WriteComponentTypeArgument(CodeRenderingContext context, ComponentTypeArgumentExtensionNode node) + { + // We can skip type arguments during runtime codegen, they are handled in the + // type/parameter declarations. + } + + public override void WriteTemplate(CodeRenderingContext context, TemplateIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + // Looks like: + // + // (__builder73) => { ... } + _scopeStack.OpenTemplateScope(context); + context.RenderChildren(node); + _scopeStack.CloseScope(context); + } + + public override void WriteReferenceCapture(CodeRenderingContext context, RefExtensionNode node) + { + // Looks like: + // + // builder.AddComponentReferenceCapture(2, (__value) = { _field = (MyComponent)__value; }); + // OR + // builder.AddElementReferenceCapture(2, (__value) = { _field = (ElementRef)__value; }); + var codeWriter = context.CodeWriter; + + var methodName = node.IsComponentCapture + ? nameof(ComponentsApi.RenderTreeBuilder.AddComponentReferenceCapture) + : nameof(ComponentsApi.RenderTreeBuilder.AddElementReferenceCapture); + codeWriter + .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{methodName}") + .Write((_sourceSequence++).ToString()) + .WriteParameterSeparator(); + + WriteReferenceCaptureInnards(context, node, shouldTypeCheck: true); + + codeWriter.WriteEndMethodInvocation(); + } + + protected override void WriteReferenceCaptureInnards(CodeRenderingContext context, RefExtensionNode node, bool shouldTypeCheck) + { + // Looks like: + // + // (__value) = { _field = (MyComponent)__value; } + // OR + // (__value) = { _field = (ElementRef)__value; } + const string refCaptureParamName = "__value"; + using (var lambdaScope = context.CodeWriter.BuildLambda(refCaptureParamName)) + { + var typecastIfNeeded = shouldTypeCheck && node.IsComponentCapture ? $"({node.ComponentCaptureTypeName})" : string.Empty; + WriteCSharpCode(context, new CSharpCodeIntermediateNode + { + Source = node.Source, + Children = + { + node.IdentifierToken, + new IntermediateToken + { + Kind = TokenKind.CSharp, + Content = $" = {typecastIfNeeded}{refCaptureParamName};" + } + } + }); + } + } + + private void WriteAttribute(CodeWriter codeWriter, string key, IList value) + { + BeginWriteAttribute(codeWriter, key); + WriteAttributeValue(codeWriter, value); + codeWriter.WriteEndMethodInvocation(); + } + + public override void BeginWriteAttribute(CodeWriter codeWriter, string key) + { + codeWriter + .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(ComponentsApi.RenderTreeBuilder.AddAttribute)}") + .Write((_sourceSequence++).ToString()) + .WriteParameterSeparator() + .WriteStringLiteral(key) + .WriteParameterSeparator(); + } + + private static string GetHtmlContent(HtmlContentIntermediateNode node) + { + var builder = new StringBuilder(); + var htmlTokens = node.Children.OfType().Where(t => t.IsHtml); + foreach (var htmlToken in htmlTokens) + { + builder.Append(htmlToken.Content); + } + return builder.ToString(); + } + + // There are a few cases here, we need to handle: + // - Pure HTML + // - Pure CSharp + // - Mixed HTML and CSharp + // + // Only the mixed case is complicated, we want to turn it into code that will concatenate + // the values into a string at runtime. + + private static void WriteAttributeValue(CodeWriter writer, IList tokens) + { + if (tokens == null) + { + throw new ArgumentNullException(nameof(tokens)); + } + + var hasHtml = false; + var hasCSharp = false; + for (var i = 0; i < tokens.Count; i++) + { + if (tokens[i].IsCSharp) + { + hasCSharp |= true; + } + else + { + hasHtml |= true; + } + } + + if (hasHtml && hasCSharp) + { + // If it's a C# expression, we have to wrap it in parens, otherwise things like ternary + // expressions don't compose with concatenation. However, this is a little complicated + // because C# tokens themselves aren't guaranteed to be distinct expressions. We want + // to treat all contiguous C# tokens as a single expression. + var insideCSharp = false; + for (var i = 0; i < tokens.Count; i++) + { + var token = tokens[i]; + if (token.IsCSharp) + { + if (!insideCSharp) + { + if (i != 0) + { + writer.Write(" + "); + } + + writer.Write("("); + insideCSharp = true; + } + + writer.Write(token.Content); + } + else + { + if (insideCSharp) + { + writer.Write(")"); + insideCSharp = false; + } + + if (i != 0) + { + writer.Write(" + "); + } + + writer.WriteStringLiteral(token.Content); + } + } + + if (insideCSharp) + { + writer.Write(")"); + } + } + else if (hasCSharp) + { + writer.Write(string.Join("", tokens.Select(t => t.Content))); + } + else if (hasHtml) + { + writer.WriteStringLiteral(string.Join("", tokens.Select(t => t.Content))); + } + else + { + // Minimized attributes always map to 'true' + writer.Write("true"); + } + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorTemplateTargetExtension.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorTemplateTargetExtension.cs new file mode 100644 index 0000000000..9c90eadc94 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/BlazorTemplateTargetExtension.cs @@ -0,0 +1,16 @@ +// 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 Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Extensions; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal class BlazorTemplateTargetExtension : ITemplateTargetExtension + { + public void WriteTemplate(CodeRenderingContext context, TemplateIntermediateNode node) + { + ((BlazorNodeWriter)context.NodeWriter).WriteTemplate(context, node); + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ChildContentDiagnosticPass.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ChildContentDiagnosticPass.cs new file mode 100644 index 0000000000..bcf31bb031 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ChildContentDiagnosticPass.cs @@ -0,0 +1,72 @@ +// 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.Language; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal class ChildContentDiagnosticPass : IntermediateNodePassBase, IRazorOptimizationPass + { + // Runs after components/eventhandlers/ref/bind/templates. We want to validate every component + // and it's usage of ChildContent. + public override int Order => 160; + + protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + var visitor = new Visitor(); + visitor.Visit(documentNode); + } + + private class Visitor : IntermediateNodeWalker, IExtensionIntermediateNodeVisitor, IExtensionIntermediateNodeVisitor + { + public void VisitExtension(ComponentExtensionNode node) + { + // Check for properties that are set by both element contents (body) and the attribute itself. + foreach (var childContent in node.ChildContents) + { + foreach (var attribute in node.Attributes) + { + if (attribute.AttributeName == childContent.AttributeName) + { + node.Diagnostics.Add(ComponentDiagnosticFactory.Create_ChildContentSetByAttributeAndBody( + attribute.Source, + attribute.AttributeName)); + } + } + } + + base.VisitDefault(node); + } + + public void VisitExtension(ComponentChildContentIntermediateNode node) + { + // Check that each child content has a unique parameter name within its scope. This is important + // because the parameter name can be implicit, and it doesn't work well when nested. + if (node.IsParameterized) + { + for (var i = 0; i < Ancestors.Count - 1; i++) + { + var ancestor = Ancestors[i] as ComponentChildContentIntermediateNode; + if (ancestor != null && + ancestor.IsParameterized && + string.Equals(node.ParameterName, ancestor.ParameterName, StringComparison.Ordinal)) + { + // Duplicate name. We report an error because this will almost certainly also lead to an error + // from the C# compiler that's way less clear. + node.Diagnostics.Add(ComponentDiagnosticFactory.Create_ChildContentRepeatedParameterName( + node.Source, + node, + (ComponentExtensionNode)Ancestors[0], // Enclosing component + ancestor, // conflicting child content node + (ComponentExtensionNode)Ancestors[i + 1])); // Enclosing component of conflicting child content node + } + } + } + + base.VisitDefault(node); + } + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/CodeWriterExtensions.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/CodeWriterExtensions.cs new file mode 100644 index 0000000000..e458d88d6a --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/CodeWriterExtensions.cs @@ -0,0 +1,647 @@ +// 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 Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; + +// Copied directly from https://github.com/aspnet/Razor/blob/ff40124594b58b17988d50841175430a4b73d1a9/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/CodeWriterExtensions.cs +// (other than the namespace change) because it's internal + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal static class CodeWriterExtensions + { + private const string InstanceMethodFormat = "{0}.{1}"; + + private static readonly char[] CStyleStringLiteralEscapeChars = + { + '\r', + '\t', + '\"', + '\'', + '\\', + '\0', + '\n', + '\u2028', + '\u2029', + }; + + public static bool IsAtBeginningOfLine(this CodeWriter writer) + { + return writer.Length == 0 || writer[writer.Length - 1] == '\n'; + } + + public static CodeWriter WritePadding(this CodeWriter writer, int offset, SourceSpan? span, CodeRenderingContext context) + { + if (span == null) + { + return writer; + } + + var basePadding = CalculatePadding(); + var resolvedPadding = Math.Max(basePadding - offset, 0); + + if (context.Options.IndentWithTabs) + { + // Avoid writing directly to the StringBuilder here, that will throw off the manual indexing + // done by the base class. + var tabs = resolvedPadding / context.Options.IndentSize; + for (var i = 0; i < tabs; i++) + { + writer.Write("\t"); + } + + var spaces = resolvedPadding % context.Options.IndentSize; + for (var i = 0; i < spaces; i++) + { + writer.Write(" "); + } + } + else + { + for (var i = 0; i < resolvedPadding; i++) + { + writer.Write(" "); + } + } + + return writer; + + int CalculatePadding() + { + var spaceCount = 0; + for (var i = span.Value.AbsoluteIndex - 1; i >= 0; i--) + { + var @char = context.SourceDocument[i]; + if (@char == '\n' || @char == '\r') + { + break; + } + else if (@char == '\t') + { + spaceCount += context.Options.IndentSize; + } + else + { + spaceCount++; + } + } + + return spaceCount; + } + } + + public static CodeWriter WriteVariableDeclaration(this CodeWriter writer, string type, string name, string value) + { + writer.Write(type).Write(" ").Write(name); + if (!string.IsNullOrEmpty(value)) + { + writer.Write(" = ").Write(value); + } + else + { + writer.Write(" = null"); + } + + writer.WriteLine(";"); + + return writer; + } + + public static CodeWriter WriteBooleanLiteral(this CodeWriter writer, bool value) + { + return writer.Write(value.ToString().ToLowerInvariant()); + } + + public static CodeWriter WriteStartAssignment(this CodeWriter writer, string name) + { + return writer.Write(name).Write(" = "); + } + + public static CodeWriter WriteParameterSeparator(this CodeWriter writer) + { + return writer.Write(", "); + } + + public static CodeWriter WriteStartNewObject(this CodeWriter writer, string typeName) + { + return writer.Write("new ").Write(typeName).Write("("); + } + + public static CodeWriter WriteStringLiteral(this CodeWriter writer, string literal) + { + if (literal.Length >= 256 && literal.Length <= 1500 && literal.IndexOf('\0') == -1) + { + WriteVerbatimStringLiteral(writer, literal); + } + else + { + WriteCStyleStringLiteral(writer, literal); + } + + return writer; + } + + public static CodeWriter WriteUsing(this CodeWriter writer, string name) + { + return WriteUsing(writer, name, endLine: true); + } + + public static CodeWriter WriteUsing(this CodeWriter writer, string name, bool endLine) + { + writer.Write("using "); + writer.Write(name); + + if (endLine) + { + writer.WriteLine(";"); + } + + return writer; + } + + public static CodeWriter WriteLineNumberDirective(this CodeWriter writer, SourceSpan span) + { + if (writer.Length >= writer.NewLine.Length && !IsAtBeginningOfLine(writer)) + { + writer.WriteLine(); + } + + var lineNumberAsString = (span.LineIndex + 1).ToString(CultureInfo.InvariantCulture); + return writer.Write("#line ").Write(lineNumberAsString).Write(" \"").Write(span.FilePath).WriteLine("\""); + } + + public static CodeWriter WriteStartMethodInvocation(this CodeWriter writer, string methodName) + { + writer.Write(methodName); + + return writer.Write("("); + } + + public static CodeWriter WriteEndMethodInvocation(this CodeWriter writer) + { + return WriteEndMethodInvocation(writer, endLine: true); + } + + public static CodeWriter WriteEndMethodInvocation(this CodeWriter writer, bool endLine) + { + writer.Write(")"); + if (endLine) + { + writer.WriteLine(";"); + } + + return writer; + } + + // Writes a method invocation for the given instance name. + public static CodeWriter WriteInstanceMethodInvocation( + this CodeWriter writer, + string instanceName, + string methodName, + params string[] parameters) + { + if (instanceName == null) + { + throw new ArgumentNullException(nameof(instanceName)); + } + + if (methodName == null) + { + throw new ArgumentNullException(nameof(methodName)); + } + + return WriteInstanceMethodInvocation(writer, instanceName, methodName, endLine: true, parameters: parameters); + } + + // Writes a method invocation for the given instance name. + public static CodeWriter WriteInstanceMethodInvocation( + this CodeWriter writer, + string instanceName, + string methodName, + bool endLine, + params string[] parameters) + { + if (instanceName == null) + { + throw new ArgumentNullException(nameof(instanceName)); + } + + if (methodName == null) + { + throw new ArgumentNullException(nameof(methodName)); + } + + return WriteMethodInvocation( + writer, + string.Format(CultureInfo.InvariantCulture, InstanceMethodFormat, instanceName, methodName), + endLine, + parameters); + } + + public static CodeWriter WriteStartInstanceMethodInvocation(this CodeWriter writer, string instanceName, string methodName) + { + if (instanceName == null) + { + throw new ArgumentNullException(nameof(instanceName)); + } + + if (methodName == null) + { + throw new ArgumentNullException(nameof(methodName)); + } + + return WriteStartMethodInvocation( + writer, + string.Format(CultureInfo.InvariantCulture, InstanceMethodFormat, instanceName, methodName)); + } + + public static CodeWriter WriteField(this CodeWriter writer, IList modifiers, string typeName, string fieldName) + { + if (modifiers == null) + { + throw new ArgumentNullException(nameof(modifiers)); + } + + if (typeName == null) + { + throw new ArgumentNullException(nameof(typeName)); + } + + if (fieldName == null) + { + throw new ArgumentNullException(nameof(fieldName)); + } + + for (var i = 0; i < modifiers.Count; i++) + { + writer.Write(modifiers[i]); + writer.Write(" "); + } + + writer.Write(typeName); + writer.Write(" "); + writer.Write(fieldName); + writer.Write(";"); + writer.WriteLine(); + + return writer; + } + + public static CodeWriter WriteMethodInvocation(this CodeWriter writer, string methodName, params string[] parameters) + { + return WriteMethodInvocation(writer, methodName, endLine: true, parameters: parameters); + } + + public static CodeWriter WriteMethodInvocation(this CodeWriter writer, string methodName, bool endLine, params string[] parameters) + { + return + WriteStartMethodInvocation(writer, methodName) + .Write(string.Join(", ", parameters)) + .WriteEndMethodInvocation(endLine); + } + + public static CodeWriter WriteAutoPropertyDeclaration(this CodeWriter writer, IList modifiers, string typeName, string propertyName) + { + if (modifiers == null) + { + throw new ArgumentNullException(nameof(modifiers)); + } + + if (typeName == null) + { + throw new ArgumentNullException(nameof(typeName)); + } + + if (propertyName == null) + { + throw new ArgumentNullException(nameof(propertyName)); + } + + for (var i = 0; i < modifiers.Count; i++) + { + writer.Write(modifiers[i]); + writer.Write(" "); + } + + writer.Write(typeName); + writer.Write(" "); + writer.Write(propertyName); + writer.Write(" { get; set; }"); + writer.WriteLine(); + + return writer; + } + + public static CSharpCodeWritingScope BuildScope(this CodeWriter writer) + { + return new CSharpCodeWritingScope(writer); + } + + public static CSharpCodeWritingScope BuildLambda(this CodeWriter writer, params string[] parameterNames) + { + return BuildLambda(writer, async: false, parameterNames: parameterNames); + } + + public static CSharpCodeWritingScope BuildAsyncLambda(this CodeWriter writer, params string[] parameterNames) + { + return BuildLambda(writer, async: true, parameterNames: parameterNames); + } + + private static CSharpCodeWritingScope BuildLambda(CodeWriter writer, bool async, string[] parameterNames) + { + if (async) + { + writer.Write("async"); + } + + writer.Write("(").Write(string.Join(", ", parameterNames)).Write(") => "); + + var scope = new CSharpCodeWritingScope(writer); + + return scope; + } + + public static CSharpCodeWritingScope BuildNamespace(this CodeWriter writer, string name) + { + writer.Write("namespace ").WriteLine(name); + + return new CSharpCodeWritingScope(writer); + } + + public static CSharpCodeWritingScope BuildClassDeclaration( + this CodeWriter writer, + IList modifiers, + string name, + string baseType, + IEnumerable interfaces) + { + for (var i = 0; i < modifiers.Count; i++) + { + writer.Write(modifiers[i]); + writer.Write(" "); + } + + writer.Write("class "); + writer.Write(name); + + var hasBaseType = !string.IsNullOrEmpty(baseType); + var hasInterfaces = interfaces != null && interfaces.Count() > 0; + + if (hasBaseType || hasInterfaces) + { + writer.Write(" : "); + + if (hasBaseType) + { + writer.Write(baseType); + + if (hasInterfaces) + { + WriteParameterSeparator(writer); + } + } + + if (hasInterfaces) + { + writer.Write(string.Join(", ", interfaces)); + } + } + + writer.WriteLine(); + + return new CSharpCodeWritingScope(writer); + } + + public static CSharpCodeWritingScope BuildMethodDeclaration( + this CodeWriter writer, + string accessibility, + string returnType, + string name, + IEnumerable> parameters) + { + writer.Write(accessibility) + .Write(" ") + .Write(returnType) + .Write(" ") + .Write(name) + .Write("(") + .Write(string.Join(", ", parameters.Select(p => p.Key + " " + p.Value))) + .WriteLine(")"); + + return new CSharpCodeWritingScope(writer); + } + + public static IDisposable BuildLinePragma(this CodeWriter writer, SourceSpan? span) + { + if (string.IsNullOrEmpty(span?.FilePath)) + { + // Can't build a valid line pragma without a file path. + return NullDisposable.Default; + } + + return new LinePragmaWriter(writer, span.Value); + } + + private static void WriteVerbatimStringLiteral(CodeWriter writer, string literal) + { + writer.Write("@\""); + + // We need to suppress indenting during the writing of the string's content. A + // verbatim string literal could contain newlines that don't get escaped. + var indent = writer.CurrentIndent; + writer.CurrentIndent = 0; + + // We need to find the index of each '"' (double-quote) to escape it. + var start = 0; + int end; + while ((end = literal.IndexOf('\"', start)) > -1) + { + writer.Write(literal, start, end - start); + + writer.Write("\"\""); + + start = end + 1; + } + + Debug.Assert(end == -1); // We've hit all of the double-quotes. + + // Write the remainder after the last double-quote. + writer.Write(literal, start, literal.Length - start); + + writer.Write("\""); + + writer.CurrentIndent = indent; + } + + private static void WriteCStyleStringLiteral(CodeWriter writer, string literal) + { + // From CSharpCodeGenerator.QuoteSnippetStringCStyle in CodeDOM + writer.Write("\""); + + // We need to find the index of each escapable character to escape it. + var start = 0; + int end; + while ((end = literal.IndexOfAny(CStyleStringLiteralEscapeChars, start)) > -1) + { + writer.Write(literal, start, end - start); + + switch (literal[end]) + { + case '\r': + writer.Write("\\r"); + break; + case '\t': + writer.Write("\\t"); + break; + case '\"': + writer.Write("\\\""); + break; + case '\'': + writer.Write("\\\'"); + break; + case '\\': + writer.Write("\\\\"); + break; + case '\0': + writer.Write("\\\0"); + break; + case '\n': + writer.Write("\\n"); + break; + case '\u2028': + case '\u2029': + writer.Write("\\u"); + writer.Write(((int)literal[end]).ToString("X4", CultureInfo.InvariantCulture)); + break; + default: + Debug.Assert(false, "Unknown escape character."); + break; + } + + start = end + 1; + } + + Debug.Assert(end == -1); // We've hit all of chars that need escaping. + + // Write the remainder after the last escaped char. + writer.Write(literal, start, literal.Length - start); + + writer.Write("\""); + } + + public struct CSharpCodeWritingScope : IDisposable + { + private CodeWriter _writer; + private bool _autoSpace; + private int _tabSize; + private int _startIndent; + + public CSharpCodeWritingScope(CodeWriter writer, int tabSize = 4, bool autoSpace = true) + { + _writer = writer; + _autoSpace = autoSpace; + _tabSize = tabSize; + _startIndent = -1; // Set in WriteStartScope + + WriteStartScope(); + } + + public void Dispose() + { + WriteEndScope(); + } + + private void WriteStartScope() + { + TryAutoSpace(" "); + + _writer.WriteLine("{"); + _writer.CurrentIndent += _tabSize; + _startIndent = _writer.CurrentIndent; + } + + private void WriteEndScope() + { + TryAutoSpace(_writer.NewLine); + + // Ensure the scope hasn't been modified + if (_writer.CurrentIndent == _startIndent) + { + _writer.CurrentIndent -= _tabSize; + } + + _writer.WriteLine("}"); + } + + private void TryAutoSpace(string spaceCharacter) + { + if (_autoSpace && + _writer.Length > 0 && + !char.IsWhiteSpace(_writer[_writer.Length - 1])) + { + _writer.Write(spaceCharacter); + } + } + } + + private class LinePragmaWriter : IDisposable + { + private readonly CodeWriter _writer; + private readonly int _startIndent; + + public LinePragmaWriter(CodeWriter writer, SourceSpan span) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + _writer = writer; + _startIndent = _writer.CurrentIndent; + _writer.CurrentIndent = 0; + WriteLineNumberDirective(writer, span); + } + + 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 endsWithNewline = _writer.Length > 0 && _writer[_writer.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 + .WriteLine("#line default") + .WriteLine("#line hidden"); + + _writer.CurrentIndent = _startIndent; + } + } + + private class NullDisposable : IDisposable + { + public static readonly NullDisposable Default = new NullDisposable(); + + private NullDisposable() + { + } + + public void Dispose() + { + } + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComplexAttributeContentPass.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComplexAttributeContentPass.cs new file mode 100644 index 0000000000..f500fedcb2 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComplexAttributeContentPass.cs @@ -0,0 +1,107 @@ +// 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.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + // We don't support 'complex' content for components (mixed C# and markup) right now. + // It's not clear yet if Blazor will have a good scenario to use these constructs. + // + // This is where a lot of the complexity in the Razor/TagHelpers model creeps in and we + // might be able to avoid it if these features aren't needed. + internal class ComplexAttributeContentPass : IntermediateNodePassBase, IRazorOptimizationPass + { + // Run before other Blazor passes + public override int Order => -1000; + + protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + var nodes = documentNode.FindDescendantNodes(); + for (var i = 0; i < nodes.Count; i++) + { + ProcessAttributes(nodes[i]); + } + } + + private void ProcessAttributes(TagHelperIntermediateNode node) + { + for (var i = node.Children.Count - 1; i >= 0; i--) + { + if (node.Children[i] is TagHelperPropertyIntermediateNode propertyNode) + { + if (TrySimplifyContent(propertyNode) && node.TagHelpers.Any(t => t.IsComponentTagHelper())) + { + node.Diagnostics.Add(ComponentDiagnosticFactory.Create_UnsupportedComplexContent( + propertyNode, + propertyNode.AttributeName)); + node.Children.RemoveAt(i); + continue; + } + } + else if (node.Children[i] is TagHelperHtmlAttributeIntermediateNode htmlNode) + { + if (TrySimplifyContent(htmlNode) && node.TagHelpers.Any(t => t.IsComponentTagHelper())) + { + node.Diagnostics.Add(ComponentDiagnosticFactory.Create_UnsupportedComplexContent( + htmlNode, + htmlNode.AttributeName)); + node.Children.RemoveAt(i); + continue; + } + } + } + } + + private static bool TrySimplifyContent(IntermediateNode node) + { + if (node.Children.Count == 1 && + node.Children[0] is HtmlAttributeIntermediateNode htmlNode && + htmlNode.Children.Count > 1) + { + // This case can be hit for a 'string' attribute + return true; + } + else if (node.Children.Count == 1 && + node.Children[0] is CSharpExpressionIntermediateNode cSharpNode && + cSharpNode.Children.Count > 1) + { + // This case can be hit when the attribute has an explicit @ inside, which + // 'escapes' any special sugar we provide for codegen. + // + // There's a special case here for explicit expressions. See https://github.com/aspnet/Razor/issues/2203 + // handling this case as a tactical matter since it's important for lambdas. + if (cSharpNode.Children.Count == 3 && + cSharpNode.Children[0] is IntermediateToken token0 && + cSharpNode.Children[2] is IntermediateToken token2 && + token0.Content == "(" && + token2.Content == ")") + { + cSharpNode.Children.RemoveAt(2); + cSharpNode.Children.RemoveAt(0); + + // We were able to simplify it, all good. + return false; + } + + return true; + } + else if (node.Children.Count == 1 && + node.Children[0] is CSharpCodeIntermediateNode cSharpCodeNode) + { + // This is the case when an attribute contains a code block @{ ... } + // We don't support this. + return true; + } + else if (node.Children.Count > 1) + { + // This is the common case for 'mixed' content + return true; + } + + return false; + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentAttributeExtensionNode.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentAttributeExtensionNode.cs new file mode 100644 index 0000000000..075ae4db4a --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentAttributeExtensionNode.cs @@ -0,0 +1,131 @@ +// 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.Language; +using Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal class ComponentAttributeExtensionNode : ExtensionIntermediateNode + { + public ComponentAttributeExtensionNode() + { + } + + public ComponentAttributeExtensionNode(TagHelperHtmlAttributeIntermediateNode attributeNode) + { + if (attributeNode == null) + { + throw new ArgumentNullException(nameof(attributeNode)); + } + + AttributeName = attributeNode.AttributeName; + AttributeStructure = attributeNode.AttributeStructure; + Source = attributeNode.Source; + + for (var i = 0; i < attributeNode.Children.Count; i++) + { + Children.Add(attributeNode.Children[i]); + } + + for (var i = 0; i < attributeNode.Diagnostics.Count; i++) + { + Diagnostics.Add(attributeNode.Diagnostics[i]); + } + } + + public ComponentAttributeExtensionNode(TagHelperPropertyIntermediateNode propertyNode) + { + if (propertyNode == null) + { + throw new ArgumentNullException(nameof(propertyNode)); + } + + AttributeName = propertyNode.AttributeName; + AttributeStructure = propertyNode.AttributeStructure; + BoundAttribute = propertyNode.BoundAttribute; + PropertyName = propertyNode.BoundAttribute.GetPropertyName(); + Source = propertyNode.Source; + TagHelper = propertyNode.TagHelper; + TypeName = propertyNode.BoundAttribute.IsWeaklyTyped() ? null : propertyNode.BoundAttribute.TypeName; + + for (var i = 0; i < propertyNode.Children.Count; i++) + { + Children.Add(propertyNode.Children[i]); + } + + for (var i = 0; i < propertyNode.Diagnostics.Count; i++) + { + Diagnostics.Add(propertyNode.Diagnostics[i]); + } + } + + public ComponentAttributeExtensionNode(ComponentAttributeExtensionNode attributeNode) + { + if (attributeNode == null) + { + throw new ArgumentNullException(nameof(attributeNode)); + } + + AttributeName = attributeNode.AttributeName; + AttributeStructure = attributeNode.AttributeStructure; + BoundAttribute = attributeNode.BoundAttribute; + PropertyName = attributeNode.BoundAttribute.GetPropertyName(); + Source = attributeNode.Source; + TagHelper = attributeNode.TagHelper; + TypeName = attributeNode.BoundAttribute.IsWeaklyTyped() ? null : attributeNode.BoundAttribute.TypeName; + + for (var i = 0; i < attributeNode.Children.Count; i++) + { + Children.Add(attributeNode.Children[i]); + } + + for (var i = 0; i < attributeNode.Diagnostics.Count; i++) + { + Diagnostics.Add(attributeNode.Diagnostics[i]); + } + } + + public override IntermediateNodeCollection Children { get; } = new IntermediateNodeCollection(); + + public string AttributeName { get; set; } + + public AttributeStructure AttributeStructure { get; set; } + + public BoundAttributeDescriptor BoundAttribute { get; set; } + + public string PropertyName { get; set; } + + public TagHelperDescriptor TagHelper { get; set; } + + public string TypeName { get; set; } + + public override void Accept(IntermediateNodeVisitor visitor) + { + if (visitor == null) + { + throw new ArgumentNullException(nameof(visitor)); + } + + AcceptExtensionNode(this, visitor); + } + + public override void WriteNode(CodeTarget target, CodeRenderingContext context) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var writer = (BlazorNodeWriter)context.NodeWriter; + writer.WriteComponentAttribute(context, this); + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentChildContentIntermediateNode.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentChildContentIntermediateNode.cs new file mode 100644 index 0000000000..e5236bb1f7 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentChildContentIntermediateNode.cs @@ -0,0 +1,50 @@ +// 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.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal class ComponentChildContentIntermediateNode : ExtensionIntermediateNode + { + public string AttributeName => BoundAttribute?.Name ?? ComponentsApi.RenderTreeBuilder.ChildContent; + + public BoundAttributeDescriptor BoundAttribute { get; set; } + + public override IntermediateNodeCollection Children { get; } = new IntermediateNodeCollection(); + + public bool IsParameterized => BoundAttribute?.IsParameterizedChildContentProperty() ?? false; + + public string ParameterName { get; set; } + + public string TypeName { get; set; } + + public override void Accept(IntermediateNodeVisitor visitor) + { + if (visitor == null) + { + throw new ArgumentNullException(nameof(visitor)); + } + + AcceptExtensionNode(this, visitor); + } + + public override void WriteNode(CodeTarget target, CodeRenderingContext context) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var writer = (BlazorNodeWriter)context.NodeWriter; + writer.WriteComponentChildContent(context, this); + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/ComponentDiagnosticFactory.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDiagnosticFactory.cs similarity index 82% rename from src/Razor/Microsoft.AspNetCore.Razor.Language/src/ComponentDiagnosticFactory.cs rename to src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDiagnosticFactory.cs index 684bcbcec7..14df6420ad 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/ComponentDiagnosticFactory.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDiagnosticFactory.cs @@ -8,7 +8,7 @@ using System.IO; using System.Linq; using Microsoft.AspNetCore.Razor.Language.Intermediate; -namespace Microsoft.AspNetCore.Razor.Language +namespace Microsoft.AspNetCore.Razor.Language.Components { internal static class ComponentDiagnosticFactory { @@ -217,11 +217,11 @@ namespace Microsoft.AspNetCore.Razor.Language "following top-level items: {1}.", RazorDiagnosticSeverity.Error); - //public static RazorDiagnostic Create_ChildContentMixedWithExplicitChildContent(SourceSpan? source, ComponentExtensionNode component) - //{ - // var supportedElements = string.Join(", ", component.Component.GetChildContentProperties().Select(p => $"'{p.Name}'")); - // return RazorDiagnostic.Create(ChildContentMixedWithExplicitChildContent, source ?? SourceSpan.Undefined, component.TagName, supportedElements); - //} + public static RazorDiagnostic Create_ChildContentMixedWithExplicitChildContent(SourceSpan? source, ComponentExtensionNode component) + { + var supportedElements = string.Join(", ", component.Component.GetChildContentProperties().Select(p => $"'{p.Name}'")); + return RazorDiagnostic.Create(ChildContentMixedWithExplicitChildContent, source ?? SourceSpan.Undefined, component.TagName, supportedElements); + } public static readonly RazorDiagnosticDescriptor ChildContentHasInvalidAttribute = new RazorDiagnosticDescriptor( @@ -252,26 +252,26 @@ namespace Microsoft.AspNetCore.Razor.Language "element '{3}' of component '{4}'. Specify the parameter name like: '<{0} Context=\"another_name\"> to resolve the ambiguity", RazorDiagnosticSeverity.Error); - //public static RazorDiagnostic Create_ChildContentRepeatedParameterName( - // SourceSpan? source, - // ComponentChildContentIntermediateNode childContent1, - // ComponentExtensionNode component1, - // ComponentChildContentIntermediateNode childContent2, - // ComponentExtensionNode component2) - //{ - // Debug.Assert(childContent1.ParameterName == childContent2.ParameterName); - // Debug.Assert(childContent1.IsParameterized); - // Debug.Assert(childContent2.IsParameterized); + public static RazorDiagnostic Create_ChildContentRepeatedParameterName( + SourceSpan? source, + ComponentChildContentIntermediateNode childContent1, + ComponentExtensionNode component1, + ComponentChildContentIntermediateNode childContent2, + ComponentExtensionNode component2) + { + Debug.Assert(childContent1.ParameterName == childContent2.ParameterName); + Debug.Assert(childContent1.IsParameterized); + Debug.Assert(childContent2.IsParameterized); - // return RazorDiagnostic.Create( - // ChildContentRepeatedParameterName, - // source ?? SourceSpan.Undefined, - // childContent1.AttributeName, - // component1.TagName, - // childContent1.ParameterName, - // childContent2.AttributeName, - // component2.TagName); - //} + return RazorDiagnostic.Create( + ChildContentRepeatedParameterName, + source ?? SourceSpan.Undefined, + childContent1.AttributeName, + component1.TagName, + childContent1.ParameterName, + childContent2.AttributeName, + component2.TagName); + } public static readonly RazorDiagnosticDescriptor GenericComponentMissingTypeArgument = new RazorDiagnosticDescriptor( @@ -279,16 +279,16 @@ namespace Microsoft.AspNetCore.Razor.Language () => "The component '{0}' is missing required type arguments. Specify the missing types using the attributes: {1}.", RazorDiagnosticSeverity.Error); - //public static RazorDiagnostic Create_GenericComponentMissingTypeArgument( - // SourceSpan? source, - // ComponentExtensionNode component, - // IEnumerable attributes) - //{ - // Debug.Assert(component.Component.IsGenericTypedComponent()); + public static RazorDiagnostic Create_GenericComponentMissingTypeArgument( + SourceSpan? source, + ComponentExtensionNode component, + IEnumerable attributes) + { + Debug.Assert(component.Component.IsGenericTypedComponent()); - // var attributesText = string.Join(", ", attributes.Select(a => $"'{a.Name}'")); - // return RazorDiagnostic.Create(GenericComponentMissingTypeArgument, source ?? SourceSpan.Undefined, component.TagName, attributesText); - //} + var attributesText = string.Join(", ", attributes.Select(a => $"'{a.Name}'")); + return RazorDiagnostic.Create(GenericComponentMissingTypeArgument, source ?? SourceSpan.Undefined, component.TagName, attributesText); + } public static readonly RazorDiagnosticDescriptor GenericComponentTypeInferenceUnderspecified = new RazorDiagnosticDescriptor( @@ -297,16 +297,16 @@ namespace Microsoft.AspNetCore.Razor.Language "directly using the following attributes: {1}.", RazorDiagnosticSeverity.Error); - //public static RazorDiagnostic Create_GenericComponentTypeInferenceUnderspecified( - // SourceSpan? source, - // ComponentExtensionNode component, - // IEnumerable attributes) - //{ - // Debug.Assert(component.Component.IsGenericTypedComponent()); + public static RazorDiagnostic Create_GenericComponentTypeInferenceUnderspecified( + SourceSpan? source, + ComponentExtensionNode component, + IEnumerable attributes) + { + Debug.Assert(component.Component.IsGenericTypedComponent()); - // var attributesText = string.Join(", ", attributes.Select(a => $"'{a.Name}'")); - // return RazorDiagnostic.Create(GenericComponentTypeInferenceUnderspecified, source ?? SourceSpan.Undefined, component.TagName, attributesText); - //} + var attributesText = string.Join(", ", attributes.Select(a => $"'{a.Name}'")); + return RazorDiagnostic.Create(GenericComponentTypeInferenceUnderspecified, source ?? SourceSpan.Undefined, component.TagName, attributesText); + } public static readonly RazorDiagnosticDescriptor ChildContentHasInvalidParameterOnComponent = new RazorDiagnosticDescriptor( @@ -314,9 +314,9 @@ namespace Microsoft.AspNetCore.Razor.Language () => "Invalid parameter name. The parameter name attribute '{0}' on component '{1}' can only include literal text.", RazorDiagnosticSeverity.Error); - //public static RazorDiagnostic Create_ChildContentHasInvalidParameterOnComponent(SourceSpan? source, string attribute, string element) - //{ - // return RazorDiagnostic.Create(ChildContentHasInvalidParameterOnComponent, source ?? SourceSpan.Undefined, attribute, element); - //} + public static RazorDiagnostic Create_ChildContentHasInvalidParameterOnComponent(SourceSpan? source, string attribute, string element) + { + return RazorDiagnostic.Create(ChildContentHasInvalidParameterOnComponent, source ?? SourceSpan.Undefined, attribute, element); + } } } diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDocumentClassifierPass.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDocumentClassifierPass.cs index a48f49994f..dbf5bb100d 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDocumentClassifierPass.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDocumentClassifierPass.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -135,5 +135,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Components return true; } + + internal static bool IsBuildRenderTreeBaseCall(CSharpCodeIntermediateNode node) + => node.Annotations[BuildRenderTreeBaseCallAnnotation] != null; } -} +} \ No newline at end of file diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentExtensionNode.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentExtensionNode.cs new file mode 100644 index 0000000000..cbd9ac592a --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentExtensionNode.cs @@ -0,0 +1,107 @@ +// 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 System.Text; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal class ComponentExtensionNode : ExtensionIntermediateNode + { + public IEnumerable Attributes => Children.OfType(); + + public IEnumerable Captures => Children.OfType(); + + public IEnumerable ChildContents => Children.OfType(); + + public override IntermediateNodeCollection Children { get; } = new IntermediateNodeCollection(); + + public TagHelperDescriptor Component { get; set; } + + /// + /// Gets the child content parameter name (null if unset) that was applied at the component level. + /// + public string ChildContentParameterName { get; set; } + + public IEnumerable TypeArguments => Children.OfType(); + + public string TagName { get; set; } + + // An optional type inference node. This will be populated (and point to a different part of the tree) + // if this component call site requires type inference. + public ComponentTypeInferenceMethodIntermediateNode TypeInferenceNode { get; set; } + + public string TypeName { get; set; } + + public override void Accept(IntermediateNodeVisitor visitor) + { + if (visitor == null) + { + throw new ArgumentNullException(nameof(visitor)); + } + + AcceptExtensionNode(this, visitor); + } + + public override void WriteNode(CodeTarget target, CodeRenderingContext context) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var writer = (BlazorNodeWriter)context.NodeWriter; + writer.WriteComponent(context, this); + } + + private string DebuggerDisplay + { + get + { + var builder = new StringBuilder(); + builder.Append("Component: "); + builder.Append("<"); + builder.Append(TagName); + + foreach (var attribute in Attributes) + { + builder.Append(" "); + builder.Append(attribute.AttributeName); + builder.Append("=\"...\""); + } + + foreach (var capture in Captures) + { + builder.Append(" "); + builder.Append("ref"); + builder.Append("=\"...\""); + } + + foreach (var typeArgument in TypeArguments) + { + builder.Append(" "); + builder.Append(typeArgument.TypeParameterName); + builder.Append("=\"...\""); + } + + builder.Append(">"); + builder.Append(ChildContents.Any() ? "..." : string.Empty); + builder.Append(""); + + return builder.ToString(); + } + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentLoweringPass.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentLoweringPass.cs new file mode 100644 index 0000000000..0f9008c09e --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentLoweringPass.cs @@ -0,0 +1,488 @@ +// 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.Linq; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal class ComponentLoweringPass : IntermediateNodePassBase, IRazorOptimizationPass + { + // This pass runs earlier than our other passes that 'lower' specific kinds of attributes. + public override int Order => 0; + + protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + var @namespace = documentNode.FindPrimaryNamespace(); + var @class = documentNode.FindPrimaryClass(); + if (@namespace == null || @class == null) + { + // Nothing to do, bail. We can't function without the standard structure. + return; + } + + // For each component *usage* we need to rewrite the tag helper node to map to the relevant component + // APIs. + var references = documentNode.FindDescendantReferences(); + for (var i = 0; i < references.Count; i++) + { + var reference = references[i]; + var node = (TagHelperIntermediateNode)reference.Node; + + var count = 0; + for (var j = 0; j < node.TagHelpers.Count; j++) + { + if (node.TagHelpers[j].IsComponentTagHelper()) + { + // Only allow a single component tag helper per element. If there are multiple, we'll just consider + // the first one and ignore the others. + if (count++ > 1) + { + node.Diagnostics.Add(ComponentDiagnosticFactory.Create_MultipleComponents(node.Source, node.TagName, node.TagHelpers)); + break; + } + } + } + + if (count >= 1) + { + reference.Replace(RewriteAsComponent(node, node.TagHelpers.First(t => t.IsComponentTagHelper()))); + } + else if (node.TagHelpers.Any(t => t.IsChildContentTagHelper())) + { + // Ignore, this will be handled when we rewrite the parent. + } + else + { + reference.Replace(RewriteAsElement(node)); + } + } + } + + private ComponentExtensionNode RewriteAsComponent(TagHelperIntermediateNode node, TagHelperDescriptor tagHelper) + { + var component = new ComponentExtensionNode() + { + Component = tagHelper, + Source = node.Source, + TagName = node.TagName, + TypeName = tagHelper.GetTypeName(), + }; + + for (var i = 0; i < node.Diagnostics.Count; i++) + { + component.Diagnostics.Add(node.Diagnostics[i]); + } + + var visitor = new ComponentRewriteVisitor(component); + visitor.Visit(node); + + // Fixup the parameter names of child content elements. We can't do this during the rewrite + // because we see the nodes in the wrong order. + foreach (var childContent in component.ChildContents) + { + childContent.ParameterName = childContent.ParameterName ?? component.ChildContentParameterName ?? BlazorMetadata.ChildContent.DefaultParameterName; + } + + return component; + } + + private HtmlElementIntermediateNode RewriteAsElement(TagHelperIntermediateNode node) + { + var result = new HtmlElementIntermediateNode() + { + Source = node.Source, + TagName = node.TagName, + }; + + for (var i = 0; i < node.Diagnostics.Count; i++) + { + result.Diagnostics.Add(node.Diagnostics[i]); + } + + var visitor = new ElementRewriteVisitor(result.Children); + visitor.Visit(node); + + return result; + } + + private class ComponentRewriteVisitor : IntermediateNodeWalker + { + private readonly ComponentExtensionNode _component; + private readonly IntermediateNodeCollection _children; + + public ComponentRewriteVisitor(ComponentExtensionNode component) + { + _component = component; + _children = component.Children; + } + + public override void VisitTagHelper(TagHelperIntermediateNode node) + { + // Visit children, we're replacing this node. + base.VisitDefault(node); + } + + public override void VisitTagHelperBody(TagHelperBodyIntermediateNode node) + { + // Wrap the component's children in a ChildContent node if we have some significant + // content. + if (node.Children.Count == 0) + { + return; + } + + // If we get a single HTML content node containing only whitespace, + // then this is probably a tag that looks like ' + // + // We don't want to create a child content for this case, because it can conflict + // with a child content that's set via an attribute. We don't want the formatting + // of insignificant whitespace to be annoying when setting attributes directly. + if (node.Children.Count == 1 && IsIgnorableWhitespace(node.Children[0])) + { + return; + } + + // From here we fork and behave differently based on whether the component's child content is + // implicit or explicit. + // + // Explicit child content will look like:
...
+ // compared with implicit:
+ // + // Using implicit child content: + // 1. All content is grouped into a single child content lambda, and assigned to the property 'ChildContent' + // + // Using explicit child content: + // 1. All content must be contained within 'child content' elements that are direct children + // 2. Whitespace outside of 'child content' elements will be ignored (not an error) + // 3. Non-whitespace outside of 'child content' elements will cause an error + // 4. All 'child content' elements must match parameters on the component (exception for ChildContent, + // which is always allowed. + // 5. Each 'child content' element will generate its own lambda, and be assigned to the property + // that matches the element name. + if (!node.Children.OfType().Any(t => t.TagHelpers.Any(th => th.IsChildContentTagHelper()))) + { + // This node has implicit child content. It may or may not have an attribute that matches. + var attribute = _component.Component.BoundAttributes + .Where(a => string.Equals(a.Name, ComponentsApi.RenderTreeBuilder.ChildContent, StringComparison.Ordinal)) + .FirstOrDefault(); + _children.Add(RewriteChildContent(attribute, node.Source, node.Children)); + return; + } + + // OK this node has explicit child content, we can rewrite it by visiting each node + // in sequence, since we: + // a) need to rewrite each child content element + // b) any significant content outside of a child content is an error + for (var i = 0; i < node.Children.Count; i++) + { + var child = node.Children[i]; + if (IsIgnorableWhitespace(child)) + { + continue; + } + + if (child is TagHelperIntermediateNode tagHelperNode && + tagHelperNode.TagHelpers.Any(th => th.IsChildContentTagHelper())) + { + // This is a child content element + var attribute = _component.Component.BoundAttributes + .Where(a => string.Equals(a.Name, tagHelperNode.TagName, StringComparison.Ordinal)) + .FirstOrDefault(); + _children.Add(RewriteChildContent(attribute, child.Source, child.Children)); + continue; + } + + // If we get here then this is significant content inside a component with explicit child content. + child.Diagnostics.Add(ComponentDiagnosticFactory.Create_ChildContentMixedWithExplicitChildContent(child.Source, _component)); + _children.Add(child); + } + + bool IsIgnorableWhitespace(IntermediateNode n) + { + if (n is HtmlContentIntermediateNode html && + html.Children.Count == 1 && + html.Children[0] is IntermediateToken token && + string.IsNullOrWhiteSpace(token.Content)) + { + return true; + } + + return false; + } + } + + private ComponentChildContentIntermediateNode RewriteChildContent(BoundAttributeDescriptor attribute, SourceSpan? source, IntermediateNodeCollection children) + { + var childContent = new ComponentChildContentIntermediateNode() + { + BoundAttribute = attribute, + Source = source, + TypeName = attribute?.TypeName ?? ComponentsApi.RenderFragment.FullTypeName, + }; + + // There are two cases here: + // 1. Implicit child content - the children will be non-taghelper nodes, just accept them + // 2. Explicit child content - the children will be various tag helper nodes, that need special processing. + for (var i = 0; i < children.Count; i++) + { + var child = children[i]; + if (child is TagHelperBodyIntermediateNode body) + { + // The body is all of the content we want to render, the rest of the children will + // be the attributes. + for (var j = 0; j < body.Children.Count; j++) + { + childContent.Children.Add(body.Children[j]); + } + } + else if (child is TagHelperPropertyIntermediateNode property) + { + if (property.BoundAttribute.IsChildContentParameterNameProperty()) + { + // Check for each child content with a parameter name, that the parameter name is specified + // with literal text. For instance, the following is not allowed and should generate a diagnostic. + // + // ... + if (TryGetAttributeStringContent(property, out var parameterName)) + { + childContent.ParameterName = parameterName; + continue; + } + + // The parameter name is invalid. + childContent.Diagnostics.Add(ComponentDiagnosticFactory.Create_ChildContentHasInvalidParameter(property.Source, property.AttributeName, attribute.Name)); + continue; + } + + // This is an unrecognized attribute, this is possible if you try to do something like put 'ref' on a child content. + childContent.Diagnostics.Add(ComponentDiagnosticFactory.Create_ChildContentHasInvalidAttribute(property.Source, property.AttributeName, attribute.Name)); + } + else if (child is TagHelperHtmlAttributeIntermediateNode a) + { + // This is an HTML attribute on a child content. + childContent.Diagnostics.Add(ComponentDiagnosticFactory.Create_ChildContentHasInvalidAttribute(a.Source, a.AttributeName, attribute.Name)); + } + else + { + // This is some other kind of node (likely an implicit child content) + childContent.Children.Add(child); + } + } + + return childContent; + } + + private bool TryGetAttributeStringContent(TagHelperPropertyIntermediateNode property, out string content) + { + // The success path looks like - a single HTML Attribute Value node with tokens + if (property.Children.Count == 1 && + property.Children[0] is HtmlContentIntermediateNode html) + { + content = string.Join(string.Empty, html.Children.OfType().Select(n => n.Content)); + return true; + } + + content = null; + return false; + } + + public override void VisitTagHelperHtmlAttribute(TagHelperHtmlAttributeIntermediateNode node) + { + var attribute = new ComponentAttributeExtensionNode(node); + _children.Add(attribute); + + // Since we don't support complex content, we can rewrite the inside of this + // node to the rather simpler form that property nodes usually have. + for (var i = 0; i < attribute.Children.Count; i++) + { + if (attribute.Children[i] is HtmlAttributeValueIntermediateNode htmlValue) + { + attribute.Children[i] = new HtmlContentIntermediateNode() + { + Children = + { + htmlValue.Children.Single(), + }, + Source = htmlValue.Source, + }; + } + else if (attribute.Children[i] is CSharpExpressionAttributeValueIntermediateNode expressionValue) + { + attribute.Children[i] = new CSharpExpressionIntermediateNode() + { + Children = + { + expressionValue.Children.Single(), + }, + Source = expressionValue.Source, + }; + } + else if (attribute.Children[i] is CSharpCodeAttributeValueIntermediateNode codeValue) + { + attribute.Children[i] = new CSharpExpressionIntermediateNode() + { + Children = + { + codeValue.Children.Single(), + }, + Source = codeValue.Source, + }; + } + } + } + + public override void VisitTagHelperProperty(TagHelperPropertyIntermediateNode node) + { + // Each 'tag helper property' belongs to a specific tag helper. We want to handle + // the cases for components, but leave others alone. This allows our other passes + // to handle those cases. + if (!node.TagHelper.IsComponentTagHelper()) + { + _children.Add(node); + return; + } + + // Another special case here - this might be a type argument. These don't represent 'real' parameters + // that get passed to the component, it needs special code generation support. + if (node.TagHelper.IsGenericTypedComponent() && node.BoundAttribute.IsTypeParameterProperty()) + { + _children.Add(new ComponentTypeArgumentExtensionNode(node)); + return; + } + + // Another special case here -- this might be a 'Context' parameter, which specifies the name + // for lambda parameter for parameterized child content + if (node.BoundAttribute.IsChildContentParameterNameProperty()) + { + // Check for each child content with a parameter name, that the parameter name is specified + // with literal text. For instance, the following is not allowed and should generate a diagnostic. + // + // ... + if (TryGetAttributeStringContent(node, out var parameterName)) + { + _component.ChildContentParameterName = parameterName; + return; + } + + // The parameter name is invalid. + _component.Diagnostics.Add(ComponentDiagnosticFactory.Create_ChildContentHasInvalidParameterOnComponent(node.Source, node.AttributeName, _component.TagName)); + return; + } + + _children.Add(new ComponentAttributeExtensionNode(node)); + } + + public override void VisitDefault(IntermediateNode node) + { + _children.Add(node); + } + } + + private class ElementRewriteVisitor : IntermediateNodeWalker + { + private readonly IntermediateNodeCollection _children; + + public ElementRewriteVisitor(IntermediateNodeCollection children) + { + _children = children; + } + + public override void VisitTagHelper(TagHelperIntermediateNode node) + { + // Visit children, we're replacing this node. + for (var i = 0; i < node.Children.Count; i++) + { + Visit(node.Children[i]); + } + } + + public override void VisitTagHelperBody(TagHelperBodyIntermediateNode node) + { + for (var i = 0; i < node.Children.Count; i++) + { + _children.Add(node.Children[i]); + } + } + + public override void VisitTagHelperHtmlAttribute(TagHelperHtmlAttributeIntermediateNode node) + { + var attribute = new HtmlAttributeIntermediateNode() + { + AttributeName = node.AttributeName, + Source = node.Source, + }; + _children.Add(attribute); + + for (var i = 0; i < node.Diagnostics.Count; i++) + { + attribute.Diagnostics.Add(node.Diagnostics[i]); + } + + switch (node.AttributeStructure) + { + case AttributeStructure.Minimized: + + attribute.Prefix = node.AttributeName; + attribute.Suffix = string.Empty; + break; + + case AttributeStructure.NoQuotes: + case AttributeStructure.SingleQuotes: + case AttributeStructure.DoubleQuotes: + + // We're ignoring attribute structure here for simplicity, it doesn't effect us. + attribute.Prefix = node.AttributeName + "=\""; + attribute.Suffix = "\""; + + for (var i = 0; i < node.Children.Count; i++) + { + attribute.Children.Add(RewriteAttributeContent(node.Children[i])); + } + + break; + } + + IntermediateNode RewriteAttributeContent(IntermediateNode content) + { + if (content is HtmlContentIntermediateNode html) + { + var value = new HtmlAttributeValueIntermediateNode() + { + Source = content.Source, + }; + + for (var i = 0; i < html.Children.Count; i++) + { + value.Children.Add(html.Children[i]); + } + + for (var i = 0; i < html.Diagnostics.Count; i++) + { + value.Diagnostics.Add(html.Diagnostics[i]); + } + + return value; + } + + + return content; + } + } + + public override void VisitTagHelperProperty(TagHelperPropertyIntermediateNode node) + { + // Each 'tag helper property' belongs to a specific tag helper. We want to handle + // the cases for components, but leave others alone. This allows our other passes + // to handle those cases. + _children.Add(node.TagHelper.IsComponentTagHelper() ? (IntermediateNode)new ComponentAttributeExtensionNode(node) : node); + } + + public override void VisitDefault(IntermediateNode node) + { + _children.Add(node); + } + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentTypeArgumentExtensionNode.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentTypeArgumentExtensionNode.cs new file mode 100644 index 0000000000..cbefe42a7b --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentTypeArgumentExtensionNode.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; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal class ComponentTypeArgumentExtensionNode : ExtensionIntermediateNode + { + public ComponentTypeArgumentExtensionNode(TagHelperPropertyIntermediateNode propertyNode) + { + if (propertyNode == null) + { + throw new ArgumentNullException(nameof(propertyNode)); + } + + BoundAttribute = propertyNode.BoundAttribute; + Source = propertyNode.Source; + TagHelper = propertyNode.TagHelper; + + for (var i = 0; i < propertyNode.Children.Count; i++) + { + Children.Add(propertyNode.Children[i]); + } + + for (var i = 0; i < propertyNode.Diagnostics.Count; i++) + { + Diagnostics.Add(propertyNode.Diagnostics[i]); + } + } + + public override IntermediateNodeCollection Children { get; } = new IntermediateNodeCollection(); + + public BoundAttributeDescriptor BoundAttribute { get; set; } + + public string TypeParameterName => BoundAttribute.Name; + + public TagHelperDescriptor TagHelper { get; set; } + + public override void Accept(IntermediateNodeVisitor visitor) + { + if (visitor == null) + { + throw new ArgumentNullException(nameof(visitor)); + } + + AcceptExtensionNode(this, visitor); + } + + public override void WriteNode(CodeTarget target, CodeRenderingContext context) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var writer = (BlazorNodeWriter)context.NodeWriter; + writer.WriteComponentTypeArgument(context, this); + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentTypeInferenceMethodIntermediateNode.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentTypeInferenceMethodIntermediateNode.cs new file mode 100644 index 0000000000..8411198cb9 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentTypeInferenceMethodIntermediateNode.cs @@ -0,0 +1,59 @@ +// 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 Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + /// + /// Represents a type-inference thunk that is used by the generated component code. + /// + internal class ComponentTypeInferenceMethodIntermediateNode : ExtensionIntermediateNode + { + public override IntermediateNodeCollection Children => IntermediateNodeCollection.ReadOnly; + + /// + /// Gets the component usage linked to this type inference method. + /// + public ComponentExtensionNode Component { get; set; } + + /// + /// Gets the full type name of the generated class containing this method. + /// + public string FullTypeName { get; internal set; } + + /// + /// Gets the name of the generated method. + /// + public string MethodName { get; set; } + + public override void Accept(IntermediateNodeVisitor visitor) + { + if (visitor == null) + { + throw new ArgumentNullException(nameof(visitor)); + } + + AcceptExtensionNode(this, visitor); + } + + public override void WriteNode(CodeTarget target, CodeRenderingContext context) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var writer = (BlazorNodeWriter)context.NodeWriter; + writer.WriteComponentTypeInferenceMethod(context, this); + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentsApi.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentsApi.cs new file mode 100644 index 0000000000..8d3133b03c --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentsApi.cs @@ -0,0 +1,133 @@ +// 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.Language.Components +{ + // Constants for method names used in code-generation + // Keep these in sync with the actual definitions + internal static class ComponentsApi + { + public static readonly string AssemblyName = "Microsoft.AspNetCore.Components"; + + public static class ComponentBase + { + public static readonly string Namespace = "Microsoft.AspNetCore.Components"; + public static readonly string FullTypeName = Namespace + ".ComponentBase"; + public static readonly string MetadataName = FullTypeName; + + public static readonly string BuildRenderTree = nameof(BuildRenderTree); + } + + public static class ParameterAttribute + { + public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.ParameterAttribute"; + public static readonly string MetadataName = FullTypeName; + } + + public static class LayoutAttribute + { + public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.Layouts.LayoutAttribute"; + } + + public static class InjectAttribute + { + public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.InjectAttribute"; + } + + public static class IComponent + { + public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.IComponent"; + + public static readonly string MetadataName = FullTypeName; + } + + public static class IDictionary + { + public static readonly string MetadataName = "System.Collection.IDictionary`2"; + } + + public static class RenderFragment + { + public static readonly string Namespace = "Microsoft.AspNetCore.Components"; + public static readonly string FullTypeName = Namespace + ".RenderFragment"; + public static readonly string MetadataName = FullTypeName; + } + + public static class RenderFragmentOfT + { + public static readonly string Namespace = "Microsoft.AspNetCore.Components"; + public static readonly string FullTypeName = Namespace + ".RenderFragment<>"; + public static readonly string MetadataName = Namespace + ".RenderFragment`1"; + } + + public static class RenderTreeBuilder + { + public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder"; + + public static readonly string OpenElement = nameof(OpenElement); + + public static readonly string CloseElement = nameof(CloseElement); + + public static readonly string OpenComponent = nameof(OpenComponent); + + public static readonly string CloseComponent = nameof(CloseComponent); + + public static readonly string AddMarkupContent = nameof(AddMarkupContent); + + public static readonly string AddContent = nameof(AddContent); + + public static readonly string AddAttribute = nameof(AddAttribute); + + public static readonly string AddElementReferenceCapture = nameof(AddElementReferenceCapture); + + public static readonly string AddComponentReferenceCapture = nameof(AddComponentReferenceCapture); + + public static readonly string Clear = nameof(Clear); + + public static readonly string GetFrames = nameof(GetFrames); + + public static readonly string ChildContent = nameof(ChildContent); + } + + public static class RuntimeHelpers + { + public static readonly string TypeCheck = "Microsoft.AspNetCore.Components.RuntimeHelpers.TypeCheck"; + } + + public static class RouteAttribute + { + public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.RouteAttribute"; + } + + public static class BindElementAttribute + { + public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.BindElementAttribute"; + } + + public static class BindInputElementAttribute + { + public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.BindInputElementAttribute"; + } + + public static class BindMethods + { + public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.BindMethods"; + + public static readonly string GetValue = "Microsoft.AspNetCore.Components.BindMethods.GetValue"; + + public static readonly string GetEventHandlerValue = "Microsoft.AspNetCore.Components.BindMethods.GetEventHandlerValue"; + + public static readonly string SetValueHandler = "Microsoft.AspNetCore.Components.BindMethods.SetValueHandler"; + } + + public static class EventHandlerAttribute + { + public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.EventHandlerAttribute"; + } + + public static class ElementRef + { + public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.ElementRef"; + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/EventHandlerLoweringPass.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/EventHandlerLoweringPass.cs new file mode 100644 index 0000000000..49c05935e1 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/EventHandlerLoweringPass.cs @@ -0,0 +1,209 @@ +// 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.Linq; +using Microsoft.AspNetCore.Razor.Language.Extensions; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal class EventHandlerLoweringPass : IntermediateNodePassBase, IRazorOptimizationPass + { + public override int Order => 50; + + protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + var @namespace = documentNode.FindPrimaryNamespace(); + var @class = documentNode.FindPrimaryClass(); + if (@namespace == null || @class == null) + { + // Nothing to do, bail. We can't function without the standard structure. + return; + } + + // For each event handler *usage* we need to rewrite the tag helper node to map to basic constructs. + // Each usage will be represented by a tag helper property that is a descendant of either + // a component or element. + var references = documentNode.FindDescendantReferences(); + + var parents = new HashSet(); + for (var i = 0; i < references.Count; i++) + { + parents.Add(references[i].Parent); + } + + foreach (var parent in parents) + { + ProcessDuplicates(parent); + } + + for (var i = 0; i < references.Count; i++) + { + var reference = references[i]; + var node = (TagHelperPropertyIntermediateNode)reference.Node; + + if (!reference.Parent.Children.Contains(node)) + { + // This node was removed as a duplicate, skip it. + continue; + } + + if (node.TagHelper.IsEventHandlerTagHelper()) + { + reference.Replace(RewriteUsage(reference.Parent, node)); + } + } + } + + private void ProcessDuplicates(IntermediateNode parent) + { + // Reverse order because we will remove nodes. + // + // Each 'property' node could be duplicated if there are multiple tag helpers that match that + // particular attribute. This is likely to happen when a component also defines something like + // OnClick. We want to remove the 'onclick' and let it fall back to be handled by the component. + for (var i = parent.Children.Count - 1; i >= 0; i--) + { + var eventHandler = parent.Children[i] as TagHelperPropertyIntermediateNode; + if (eventHandler != null && + eventHandler.TagHelper != null && + eventHandler.TagHelper.IsEventHandlerTagHelper()) + { + for (var j = 0; j < parent.Children.Count; j++) + { + var componentAttribute = parent.Children[j] as ComponentAttributeExtensionNode; + if (componentAttribute != null && + componentAttribute.TagHelper != null && + componentAttribute.TagHelper.IsComponentTagHelper() && + componentAttribute.AttributeName == eventHandler.AttributeName) + { + // Found a duplicate - remove the 'fallback' in favor of the component's own handling. + parent.Children.RemoveAt(i); + break; + } + } + } + } + + // If we still have duplicates at this point then they are genuine conflicts. + var duplicates = parent.Children + .OfType() + .Where(p => p.TagHelper?.IsEventHandlerTagHelper() ?? false) + .GroupBy(p => p.AttributeName) + .Where(g => g.Count() > 1); + + foreach (var duplicate in duplicates) + { + parent.Diagnostics.Add(ComponentDiagnosticFactory.CreateEventHandler_Duplicates( + parent.Source, + duplicate.Key, + duplicate.ToArray())); + foreach (var property in duplicate) + { + parent.Children.Remove(property); + } + } + } + + private IntermediateNode RewriteUsage(IntermediateNode parent, TagHelperPropertyIntermediateNode node) + { + var original = GetAttributeContent(node); + if (original.Count == 0) + { + // This can happen in error cases, the parser will already have flagged this + // as an error, so ignore it. + return node; + } + + // Now rewrite the content of the value node to look like: + // + // BindMethods.GetEventHandlerValue() + // + // This method is overloaded on string and TDelegate, which means that it will put the code in the + // correct context for intellisense when typing in the attribute. + var eventArgsType = node.TagHelper.GetEventArgsType(); + var tokens = new List() + { + new IntermediateToken() + { + Content = $"{ComponentsApi.BindMethods.GetEventHandlerValue}<{eventArgsType}>(", + Kind = TokenKind.CSharp + }, + new IntermediateToken() + { + Content = $")", + Kind = TokenKind.CSharp + } + }; + + for (var i = 0; i < original.Count; i++) + { + tokens.Insert(i + 1, original[i]); + } + + if (parent is HtmlElementIntermediateNode) + { + var result = new HtmlAttributeIntermediateNode() + { + AttributeName = node.AttributeName, + Source = node.Source, + + Prefix = node.AttributeName + "=\"", + Suffix = "\"", + }; + + for (var i = 0; i < node.Diagnostics.Count; i++) + { + result.Diagnostics.Add(node.Diagnostics[i]); + } + + result.Children.Add(new CSharpExpressionAttributeValueIntermediateNode()); + for (var i = 0; i < tokens.Count; i++) + { + result.Children[0].Children.Add(tokens[i]); + } + + return result; + } + else + { + var result = new ComponentAttributeExtensionNode(node); + + result.Children.Clear(); + result.Children.Add(new CSharpExpressionIntermediateNode()); + for (var i = 0; i < tokens.Count; i++) + { + result.Children[0].Children.Add(tokens[i]); + } + + return result; + } + } + + private static IReadOnlyList GetAttributeContent(TagHelperPropertyIntermediateNode node) + { + var template = node.FindDescendantNodes().FirstOrDefault(); + if (template != null) + { + // See comments in TemplateDiagnosticPass + node.Diagnostics.Add(ComponentDiagnosticFactory.Create_TemplateInvalidLocation(template.Source)); + return new[] { new IntermediateToken() { Kind = TokenKind.CSharp, Content = string.Empty, }, }; + } + + if (node.Children.Count == 1 && node.Children[0] is HtmlContentIntermediateNode htmlContentNode) + { + // This case can be hit for a 'string' attribute. We want to turn it into + // an expression. + var tokens = htmlContentNode.FindDescendantNodes(); + + var content = "\"" + string.Join(string.Empty, tokens.Select(t => t.Content.Replace("\"", "\\\""))) + "\""; + return new[] { new IntermediateToken() { Content = content, Kind = TokenKind.CSharp, } }; + } + else + { + return node.FindDescendantNodes(); + } + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/GenericComponentPass.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/GenericComponentPass.cs new file mode 100644 index 0000000000..eebe7db984 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/GenericComponentPass.cs @@ -0,0 +1,314 @@ +// 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.Linq; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + // This pass: + // 1. Adds diagnostics for missing generic type arguments + // 2. Rewrites the type name of the component to substitute generic type arguments + // 3. Rewrites the type names of parameters/child content to substitute generic type arguments + internal class GenericComponentPass : IntermediateNodePassBase, IRazorOptimizationPass + { + private TypeNameFeature _typeNameFeature; + + // Runs after components/eventhandlers/ref/bind/templates. We want to validate every component + // and it's usage of ChildContent. + public override int Order => 160; + + protected override void OnInitialized() + { + _typeNameFeature = GetRequiredFeature(); + } + + protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + var visitor = new Visitor(_typeNameFeature); + visitor.Visit(documentNode); + } + + private class Visitor : IntermediateNodeWalker, IExtensionIntermediateNodeVisitor + { + private readonly TypeNameFeature _typeNameFeature; + + // Incrementing ID for type inference method names + private int _id; + + public Visitor(TypeNameFeature typeNameFeature) + { + _typeNameFeature = typeNameFeature; + } + + public void VisitExtension(ComponentExtensionNode node) + { + if (node.Component.IsGenericTypedComponent()) + { + // Not generic, ignore. + Process(node); + } + + base.VisitDefault(node); + } + + private void Process(ComponentExtensionNode node) + { + // First collect all of the information we have about each type parameter + // + // Listing all type parameters that exist + var bindings = new Dictionary(); + foreach (var attribute in node.Component.GetTypeParameters()) + { + bindings.Add(attribute.Name, new Binding() { Attribute = attribute, }); + } + + // Listing all type arguments that have been specified. + var hasTypeArgumentSpecified = false; + foreach (var typeArgumentNode in node.TypeArguments) + { + hasTypeArgumentSpecified = true; + + var binding = bindings[typeArgumentNode.TypeParameterName]; + binding.Node = typeArgumentNode; + binding.Content = GetContent(typeArgumentNode); + } + + if (hasTypeArgumentSpecified) + { + // OK this means that the developer has specified at least one type parameter. + // Either they specified everything and its OK to rewrite, or its an error. + if (ValidateTypeArguments(node, bindings)) + { + var mappings = bindings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Content); + RewriteTypeNames(_typeNameFeature.CreateGenericTypeRewriter(mappings), node); + } + + return; + } + + // OK if we get here that means that no type arguments were specified, so we will try to infer + // the type. + // + // The actual inference is done by the C# compiler, we just emit an a method that represents the + // use of this component. + + // Since we're generating code in a different namespace, we need to 'global qualify' all of the types + // to avoid clashes with our generated code. + RewriteTypeNames(_typeNameFeature.CreateGlobalQualifiedTypeNameRewriter(bindings.Keys), node); + + // + // We need to verify that an argument was provided that 'covers' each type parameter. + // + // For example, consider a repeater where the generic type is the 'item' type, but the developer has + // not set the items. We won't be able to do type inference on this and so it will just be nonsense. + var attributes = node.Attributes.Select(a => a.BoundAttribute).Concat(node.ChildContents.Select(c => c.BoundAttribute)); + foreach (var attribute in attributes) + { + if (attribute == null) + { + // Will be null for attributes set on the component that don't match a declared component parameter + continue; + } + + // Now we need to parse the type name and extract the generic parameters. + // + // Two cases; + // 1. name is a simple identifier like TItem + // 2. name contains type parameters like Dictionary + if (!attribute.IsGenericTypedProperty()) + { + continue; + } + + var typeParameters = _typeNameFeature.ParseTypeParameters(attribute.TypeName); + if (typeParameters.Count == 0) + { + bindings.Remove(attribute.TypeName); + } + else + { + for (var i = 0; i < typeParameters.Count; i++) + { + var typeParameter = typeParameters[i]; + bindings.Remove(typeParameter.ToString()); + } + } + } + + // If any bindings remain then this means we would never be able to infer the arguments of this + // component usage because the user hasn't set properties that include all of the types. + if (bindings.Count > 0) + { + // However we still want to generate 'type inference' code because we want the errors to be as + // helpful as possible. So let's substitute 'object' for all of those type parameters, and add + // an error. + var mappings = bindings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Content); + RewriteTypeNames(_typeNameFeature.CreateGenericTypeRewriter(mappings), node); + + node.Diagnostics.Add(ComponentDiagnosticFactory.Create_GenericComponentTypeInferenceUnderspecified(node.Source, node, node.Component.GetTypeParameters())); + } + + // Next we need to generate a type inference 'method' node. This represents a method that we will codegen that + // contains all of the operations on the render tree building. Calling a method to operate on the builder + // will allow the C# compiler to perform type inference. + var documentNode = (DocumentIntermediateNode)Ancestors[Ancestors.Count - 1]; + CreateTypeInferenceMethod(documentNode, node); + } + + private string GetContent(ComponentTypeArgumentExtensionNode node) + { + return string.Join(string.Empty, node.FindDescendantNodes().Where(t => t.IsCSharp).Select(t => t.Content)); + } + + private static bool ValidateTypeArguments(ComponentExtensionNode node, Dictionary bindings) + { + var missing = new List(); + foreach (var binding in bindings) + { + if (binding.Value.Node == null || string.IsNullOrWhiteSpace(binding.Value.Content)) + { + missing.Add(binding.Value.Attribute); + } + } + + if (missing.Count > 0) + { + // We add our own error for this because its likely the user will see other errors due + // to incorrect codegen without the types. Our errors message will pretty clearly indicate + // what to do, whereas the other errors might be confusing. + node.Diagnostics.Add(ComponentDiagnosticFactory.Create_GenericComponentMissingTypeArgument(node.Source, node, missing)); + return false; + } + + return true; + } + + private void RewriteTypeNames(TypeNameRewriter rewriter, ComponentExtensionNode node) + { + // Rewrite the component type name + node.TypeName = rewriter.Rewrite(node.TypeName); + + foreach (var attribute in node.Attributes) + { + if (attribute.BoundAttribute?.IsGenericTypedProperty() ?? false && attribute.TypeName != null) + { + // If we know the type name, then replace any generic type parameter inside it with + // the known types. + attribute.TypeName = rewriter.Rewrite(attribute.TypeName); + } + else if (attribute.TypeName == null && (attribute.BoundAttribute?.IsDelegateProperty() ?? false)) + { + // This is a weakly typed delegate, treat it as Action + attribute.TypeName = "System.Action"; + } + else if (attribute.TypeName == null) + { + // This is a weakly typed attribute, treat it as System.Object + attribute.TypeName = "System.Object"; + } + } + + foreach (var capture in node.Captures) + { + if (capture.IsComponentCapture && capture.ComponentCaptureTypeName != null) + { + capture.ComponentCaptureTypeName = rewriter.Rewrite(capture.ComponentCaptureTypeName); + } + else if (capture.IsComponentCapture) + { + capture.ComponentCaptureTypeName = "System.Object"; + } + } + + foreach (var childContent in node.ChildContents) + { + if (childContent.BoundAttribute?.IsGenericTypedProperty() ?? false && childContent.TypeName != null) + { + // If we know the type name, then replace any generic type parameter inside it with + // the known types. + childContent.TypeName = rewriter.Rewrite(childContent.TypeName); + } + else if (childContent.IsParameterized) + { + // This is a weakly typed parameterized child content, treat it as RenderFragment + childContent.TypeName = ComponentsApi.RenderFragment.FullTypeName + ""; + } + else + { + // This is a weakly typed child content, treat it as RenderFragment + childContent.TypeName = ComponentsApi.RenderFragment.FullTypeName; + } + } + } + + private void CreateTypeInferenceMethod(DocumentIntermediateNode documentNode, ComponentExtensionNode node) + { + var @namespace = documentNode.FindPrimaryNamespace().Content; + @namespace = string.IsNullOrEmpty(@namespace) ? "__Blazor" : "__Blazor." + @namespace; + @namespace += "." + documentNode.FindPrimaryClass().ClassName; + + var typeInferenceNode = new ComponentTypeInferenceMethodIntermediateNode() + { + Component = node, + + // Method name is generated and guaranteed not to collide, since it's unique for each + // component call site. + MethodName = $"Create{node.TagName}_{_id++}", + FullTypeName = @namespace + ".TypeInference", + }; + + node.TypeInferenceNode = typeInferenceNode; + + // Now we need to insert the type inference node into the tree. + var namespaceNode = documentNode.Children + .OfType() + .Where(n => n.Annotations.Contains(new KeyValuePair(BlazorMetadata.Component.GenericTypedKey, bool.TrueString))) + .FirstOrDefault(); + if (namespaceNode == null) + { + namespaceNode = new NamespaceDeclarationIntermediateNode() + { + Annotations = + { + { BlazorMetadata.Component.GenericTypedKey, bool.TrueString }, + }, + Content = @namespace, + }; + + documentNode.Children.Add(namespaceNode); + } + + var classNode = namespaceNode.Children + .OfType() + .Where(n => n.ClassName == "TypeInference") + .FirstOrDefault(); + if (classNode == null) + { + classNode = new ClassDeclarationIntermediateNode() + { + ClassName = "TypeInference", + Modifiers = + { + "internal", + "static", + }, + }; + namespaceNode.Children.Add(classNode); + } + + classNode.Children.Add(typeInferenceNode); + } + } + + private class Binding + { + public BoundAttributeDescriptor Attribute { get; set; } + + public string Content { get; set; } + + public ComponentTypeArgumentExtensionNode Node { get; set; } + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/HtmlBlockIntermediateNode.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/HtmlBlockIntermediateNode.cs new file mode 100644 index 0000000000..756474f366 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/HtmlBlockIntermediateNode.cs @@ -0,0 +1,46 @@ +// 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.Diagnostics; +using Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + internal class HtmlBlockIntermediateNode : ExtensionIntermediateNode + { + public override IntermediateNodeCollection Children { get; } = new IntermediateNodeCollection(); + + public string Content { get; set; } + + public override void Accept(IntermediateNodeVisitor visitor) + { + if (visitor == null) + { + throw new ArgumentNullException(nameof(visitor)); + } + + AcceptExtensionNode(this, visitor); + } + + public override void WriteNode(CodeTarget target, CodeRenderingContext context) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var writer = (BlazorNodeWriter)context.NodeWriter; + writer.WriteHtmlBlock(context, this); + } + + private string DebuggerDisplay => Content; + } +} \ No newline at end of file diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/HtmlBlockPass.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/HtmlBlockPass.cs new file mode 100644 index 0000000000..3106bbbaaf --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/HtmlBlockPass.cs @@ -0,0 +1,331 @@ +// 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 System.Text; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + // Rewrites contiguous subtrees of HTML into a special node type to reduce the + // size of the Render tree. + // + // Does not preserve insignificant details of the HTML, like tag closing style + // or quote style. + internal class HtmlBlockPass : IntermediateNodePassBase, IRazorOptimizationPass + { + // Runs LATE because we want to destroy structure. + public override int Order => 10000; + + protected override void ExecuteCore( + RazorCodeDocument codeDocument, + DocumentIntermediateNode documentNode) + { + if (documentNode.Options.DesignTime) + { + // Nothing to do during design time. + return; + } + + var findVisitor = new FindHtmlTreeVisitor(); + findVisitor.Visit(documentNode); + + var trees = findVisitor.Trees; + var rewriteVisitor = new RewriteVisitor(trees); + while (trees.Count > 0) + { + // Walk backwards since we did a postorder traversal. + var reference = trees[trees.Count - 1]; + + // Forcibly remove a node to prevent infinite loops. + trees.RemoveAt(trees.Count - 1); + + // We want to fold together siblings where possible. To do this, first we find + // the index of the node we're looking at now - then we need to walk backwards + // and identify a set of contiguous nodes we can merge. + var start = reference.Parent.Children.Count - 1; + for (; start >= 0; start--) + { + if (ReferenceEquals(reference.Node, reference.Parent.Children[start])) + { + break; + } + } + + // This is the current node. Check if the left sibling is always a candidate + // for rewriting. Due to the order we processed the nodes, we know that the + // left sibling is next in the list to process if it's a candidate. + var end = start; + while (start - 1 >= 0) + { + var candidate = reference.Parent.Children[start - 1]; + if (trees.Count == 0 || !ReferenceEquals(trees[trees.Count - 1].Node, candidate)) + { + // This means the we're out of nodes, or the left sibling is not in the list. + break; + } + + // This means that the left sibling is valid to merge. + start--; + + // Remove this since we're combining it. + trees.RemoveAt(trees.Count - 1); + } + + // As a degenerate case, don't bother rewriting an single HtmlContent node + // It doesn't add any value. + if (end - start == 0 && reference.Node is HtmlContentIntermediateNode) + { + continue; + } + + // Now we know the range of nodes to rewrite (end is inclusive) + var length = end + 1 - start; + while (length > 0) + { + // Keep using start since we're removing nodes. + var node = reference.Parent.Children[start]; + reference.Parent.Children.RemoveAt(start); + + rewriteVisitor.Visit(node); + + length--; + } + + reference.Parent.Children.Insert(start, new HtmlBlockIntermediateNode() + { + Content = rewriteVisitor.Builder.ToString(), + }); + + rewriteVisitor.Builder.Clear(); + } + } + + // Finds HTML-blocks using a postorder traversal. We store nodes in an + // ordered list so we can avoid redundant rewrites. + // + // Consider a case like: + //
+ // click me + //
+ // + // We would store both the div and a tag in a list, but make sure to visit + // the div first. Then when we process the div (recursively), we would remove + // the a from the list. + private class FindHtmlTreeVisitor : + IntermediateNodeWalker, + IExtensionIntermediateNodeVisitor + { + private bool _foundNonHtml; + + public List Trees { get; } = new List(); + + public override void VisitDefault(IntermediateNode node) + { + // If we get here, we found a non-HTML node. Keep traversing. + _foundNonHtml = true; + base.VisitDefault(node); + } + + public void VisitExtension(HtmlElementIntermediateNode node) + { + // We need to restore the state after processing this node. + // We might have found a leaf-block of HTML, but that shouldn't + // affect our parent's state. + var originalState = _foundNonHtml; + + _foundNonHtml = false; + + if (node.HasDiagnostics) + { + // Treat node with errors as non-HTML - don't let the parent rewrite this either. + _foundNonHtml = true; + } + + if (string.Equals("script", node.TagName, StringComparison.OrdinalIgnoreCase)) + { + // Treat script tags as non-HTML - we trigger errors for script tags + // later. + _foundNonHtml = true; + } + + base.VisitDefault(node); + + if (!_foundNonHtml) + { + Trees.Add(new IntermediateNodeReference(Parent, node)); + } + + _foundNonHtml = originalState |= _foundNonHtml; + } + + public override void VisitHtmlAttribute(HtmlAttributeIntermediateNode node) + { + if (node.HasDiagnostics) + { + // Treat node with errors as non-HTML + _foundNonHtml = true; + } + + // Visit Children + base.VisitDefault(node); + } + + public override void VisitHtmlAttributeValue(HtmlAttributeValueIntermediateNode node) + { + if (node.HasDiagnostics) + { + // Treat node with errors as non-HTML + _foundNonHtml = true; + } + + // Visit Children + base.VisitDefault(node); + } + + public override void VisitHtml(HtmlContentIntermediateNode node) + { + // We need to restore the state after processing this node. + // We might have found a leaf-block of HTML, but that shouldn't + // affect our parent's state. + var originalState = _foundNonHtml; + + _foundNonHtml = false; + + if (node.HasDiagnostics) + { + // Treat node with errors as non-HTML + _foundNonHtml = true; + } + + // Visit Children + base.VisitDefault(node); + + if (!_foundNonHtml) + { + Trees.Add(new IntermediateNodeReference(Parent, node)); + } + + _foundNonHtml = originalState |= _foundNonHtml; + } + + public override void VisitToken(IntermediateToken node) + { + if (node.HasDiagnostics) + { + // Treat node with errors as non-HTML + _foundNonHtml = true; + } + + if (node.IsCSharp) + { + _foundNonHtml = true; + } + } + } + + private class RewriteVisitor : + IntermediateNodeWalker, + IExtensionIntermediateNodeVisitor + { + private readonly StringBuilder _encodingBuilder; + + private readonly List _trees; + + public RewriteVisitor(List trees) + { + _trees = trees; + + _encodingBuilder = new StringBuilder(); + } + + public StringBuilder Builder { get; } = new StringBuilder(); + + public void VisitExtension(HtmlElementIntermediateNode node) + { + for (var i = 0; i < _trees.Count; i++) + { + // Remove this node if it's in the list. This ensures that we don't + // do redundant operations. + if (ReferenceEquals(_trees[i].Node, node)) + { + _trees.RemoveAt(i); + break; + } + } + + var isVoid = Legacy.ParserHelpers.VoidElements.Contains(node.TagName); + var hasBodyContent = node.Body.Any(); + + Builder.Append("<"); + Builder.Append(node.TagName); + + foreach (var attribute in node.Attributes) + { + Visit(attribute); + } + + // If for some reason a void element contains body, then treat it as a + // start/end tag. + if (!hasBodyContent && isVoid) + { + // void + Builder.Append(">"); + return; + } + else if (!hasBodyContent) + { + // In HTML5, we can't have self-closing non-void elements, so explicitly + // add a close tag + Builder.Append(">"); + return; + } + + // start/end tag with body. + Builder.Append(">"); + + foreach (var item in node.Body) + { + Visit(item); + } + + Builder.Append(""); + } + + public override void VisitHtmlAttribute(HtmlAttributeIntermediateNode node) + { + Builder.Append(" "); + Builder.Append(node.AttributeName); + + if (node.Children.Count == 0) + { + // Minimized attribute + return; + } + + Builder.Append("=\""); + + // Visit Children + base.VisitDefault(node); + + Builder.Append("\""); + } + + public override void VisitHtmlAttributeValue(HtmlAttributeValueIntermediateNode node) + { + Builder.Append(node.Children); + } + + public override void VisitHtml(HtmlContentIntermediateNode node) + { + Builder.Append(node.Children); + } + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/HtmlElementIntermediateNode.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/HtmlElementIntermediateNode.cs new file mode 100644 index 0000000000..839547106c --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/HtmlElementIntermediateNode.cs @@ -0,0 +1,91 @@ +// 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 System.Text; +using Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + internal class HtmlElementIntermediateNode : ExtensionIntermediateNode + { + public IEnumerable Attributes => Children.OfType(); + + public IEnumerable Captures => Children.OfType(); + + public IEnumerable Body => Children.Where(c => + { + return + c as HtmlAttributeIntermediateNode == null && + c as RefExtensionNode == null; + }); + + public override IntermediateNodeCollection Children { get; } = new IntermediateNodeCollection(); + + public string TagName { get; set; } + + public override void Accept(IntermediateNodeVisitor visitor) + { + if (visitor == null) + { + throw new ArgumentNullException(nameof(visitor)); + } + + AcceptExtensionNode(this, visitor); + } + + public override void WriteNode(CodeTarget target, CodeRenderingContext context) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var writer = (BlazorNodeWriter)context.NodeWriter; + writer.WriteHtmlElement(context, this); + } + + private string DebuggerDisplay + { + get + { + var builder = new StringBuilder(); + builder.Append("Element: "); + builder.Append("<"); + builder.Append(TagName); + + foreach (var attribute in Attributes) + { + builder.Append(" "); + builder.Append(attribute.AttributeName); + builder.Append("=\"...\""); + } + + foreach (var capture in Captures) + { + builder.Append(" "); + builder.Append("ref"); + builder.Append("=\"...\""); + } + + builder.Append(">"); + builder.Append(Body.Any() ? "..." : string.Empty); + builder.Append(""); + + return builder.ToString(); + } + } + } +} \ No newline at end of file diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ImplementsDirective.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ImplementsDirective.cs new file mode 100644 index 0000000000..ec450ea8a2 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ImplementsDirective.cs @@ -0,0 +1,32 @@ +// 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.Language; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal static class ImplementsDirective + { + public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective( + "implements", + DirectiveKind.SingleLine, + builder => + { + builder.AddTypeToken(ComponentResources.ImplementsDirective_TypeToken_Name, ComponentResources.ImplementsDirective_TypeToken_Description); + builder.Usage = DirectiveUsage.FileScopedMultipleOccurring; + builder.Description = ComponentResources.ImplementsDirective_Description; + }); + + public static void Register(RazorProjectEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AddDirective(Directive); + builder.Features.Add(new ImplementsDirectivePass()); + } + } +} \ No newline at end of file diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ImplementsDirectivePass.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ImplementsDirectivePass.cs new file mode 100644 index 0000000000..e67b22736c --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ImplementsDirectivePass.cs @@ -0,0 +1,37 @@ +// 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.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Extensions; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal class ImplementsDirectivePass : IntermediateNodePassBase, IRazorDirectiveClassifierPass + { + protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + var @class = documentNode.FindPrimaryClass(); + if (@class == null) + { + return; + } + + if (@class.Interfaces == null) + { + @class.Interfaces = new List(); + } + + foreach (var implements in documentNode.FindDirectiveReferences(ImplementsDirective.Directive)) + { + var token = ((DirectiveIntermediateNode)implements.Node).Tokens.FirstOrDefault(); + if (token != null) + { + @class.Interfaces.Add(token.Content); + } + } + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/InjectDirective.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/InjectDirective.cs new file mode 100644 index 0000000000..1fcead8afa --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/InjectDirective.cs @@ -0,0 +1,111 @@ +// 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.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + // Much of the following is equivalent to Microsoft.AspNetCore.Mvc.Razor.Extensions's InjectDirective, + // but this one outputs properties annotated for Blazor's property injector, plus it doesn't need to + // support multiple CodeTargets. + + internal class InjectDirective + { + public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective( + "inject", + DirectiveKind.SingleLine, + builder => + { + builder.AddTypeToken("TypeName", "The type of the service to inject."); + builder.AddMemberToken("PropertyName", "The name of the property."); + builder.Usage = DirectiveUsage.FileScopedMultipleOccurring; + builder.Description = "Inject a service from the application's service container into a property."; + }); + + public static void Register(RazorProjectEngineBuilder builder) + { + builder.AddDirective(Directive); + builder.Features.Add(new Pass()); + } + + private class Pass : IntermediateNodePassBase, IRazorDirectiveClassifierPass + { + protected override void ExecuteCore( + RazorCodeDocument codeDocument, + DocumentIntermediateNode documentNode) + { + var visitor = new Visitor(); + visitor.Visit(documentNode); + + var properties = new HashSet(StringComparer.Ordinal); + var classNode = documentNode.FindPrimaryClass(); + + for (var i = visitor.Directives.Count - 1; i >= 0; i--) + { + var directive = visitor.Directives[i]; + var tokens = directive.Tokens.ToArray(); + if (tokens.Length < 2) + { + continue; + } + + var typeName = tokens[0].Content; + var memberName = tokens[1].Content; + + if (!properties.Add(memberName)) + { + continue; + } + + classNode.Children.Add(new InjectIntermediateNode(typeName, memberName)); + } + } + + private class Visitor : IntermediateNodeWalker + { + public IList Directives { get; } + = new List(); + + public override void VisitDirective(DirectiveIntermediateNode node) + { + if (node.Directive == Directive) + { + Directives.Add(node); + } + } + } + + internal class InjectIntermediateNode : ExtensionIntermediateNode + { + private static readonly IList _injectedPropertyModifiers = new[] + { + $"[global::{ComponentsApi.InjectAttribute.FullTypeName}]", + "private" // Encapsulation is the default + }; + + public string TypeName { get; } + public string MemberName { get; } + public override IntermediateNodeCollection Children => IntermediateNodeCollection.ReadOnly; + + public InjectIntermediateNode(string typeName, string memberName) + { + TypeName = typeName; + MemberName = memberName; + } + + public override void Accept(IntermediateNodeVisitor visitor) + => AcceptExtensionNode(this, visitor); + + public override void WriteNode(CodeTarget target, CodeRenderingContext context) + => context.CodeWriter.WriteAutoPropertyDeclaration( + _injectedPropertyModifiers, + TypeName, + MemberName); + } + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/LayoutDirective.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/LayoutDirective.cs new file mode 100644 index 0000000000..0d2d586260 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/LayoutDirective.cs @@ -0,0 +1,32 @@ +// 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.Language; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal static class LayoutDirective + { + public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective( + "layout", + DirectiveKind.SingleLine, + builder => + { + builder.AddTypeToken(ComponentResources.LayoutDirective_TypeToken_Name, ComponentResources.LayoutDirective_TypeToken_Description); + builder.Usage = DirectiveUsage.FileScopedSinglyOccurring; + builder.Description = ComponentResources.LayoutDirective_Description; + }); + + public static void Register(RazorProjectEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AddDirective(Directive); + builder.Features.Add(new LayoutDirectivePass()); + } + } +} \ No newline at end of file diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/LayoutDirectivePass.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/LayoutDirectivePass.cs new file mode 100644 index 0000000000..a22160570d --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/LayoutDirectivePass.cs @@ -0,0 +1,51 @@ +// 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.Linq; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal class LayoutDirectivePass : IntermediateNodePassBase, IRazorDirectiveClassifierPass + { + protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + var @namespace = documentNode.FindPrimaryNamespace(); + var @class = documentNode.FindPrimaryClass(); + if (@namespace == null || @class == null) + { + return; + } + + var directives = documentNode.FindDirectiveReferences(LayoutDirective.Directive); + if (directives.Count == 0) + { + return; + } + + var token = ((DirectiveIntermediateNode)directives[0].Node).Tokens.FirstOrDefault(); + if (token == null) + { + return; + } + + var attributeNode = new CSharpCodeIntermediateNode(); + attributeNode.Children.Add(new IntermediateToken() + { + Kind = TokenKind.CSharp, + Content = $"[{ComponentsApi.LayoutAttribute.FullTypeName}(typeof({token.Content}))]" + Environment.NewLine, + }); + + // Insert the new attribute on top of the class + for (var i = 0; i < @namespace.Children.Count; i++) + { + if (object.ReferenceEquals(@namespace.Children[i], @class)) + { + @namespace.Children.Insert(i, attributeNode); + break; + } + } + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/Microsoft.AspNetCore.Components.Razor.Extensions.csproj b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/Microsoft.AspNetCore.Components.Razor.Extensions.csproj new file mode 100644 index 0000000000..215a562aaa --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/Microsoft.AspNetCore.Components.Razor.Extensions.csproj @@ -0,0 +1,38 @@ + + + + netstandard2.0 + $(TargetFrameworks);net461 + Microsoft.AspNetCore.Components.Razor + Extensions to the Razor compiler to support building Razor Components. + true + + + true + + + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + + diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/PageDirective.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/PageDirective.cs new file mode 100644 index 0000000000..08b5ec74b8 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/PageDirective.cs @@ -0,0 +1,44 @@ +// 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.Language; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal class PageDirective + { + public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective( + "page", + DirectiveKind.SingleLine, + builder => + { + builder.AddStringToken(ComponentResources.PageDirective_RouteToken_Name, ComponentResources.PageDirective_RouteToken_Description); + builder.Usage = DirectiveUsage.FileScopedMultipleOccurring; + builder.Description = ComponentResources.PageDirective_Description; + }); + + private PageDirective(string routeTemplate, IntermediateNode directiveNode) + { + RouteTemplate = routeTemplate; + DirectiveNode = directiveNode; + } + + public string RouteTemplate { get; } + + public IntermediateNode DirectiveNode { get; } + + public static RazorProjectEngineBuilder Register(RazorProjectEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AddDirective(Directive); + builder.Features.Add(new PageDirectivePass()); + return builder; + } + } +} \ No newline at end of file diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/PageDirectivePass.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/PageDirectivePass.cs new file mode 100644 index 0000000000..224b85b503 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/PageDirectivePass.cs @@ -0,0 +1,82 @@ +// 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.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal class PageDirectivePass : IntermediateNodePassBase, IRazorDirectiveClassifierPass + { + protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + if (codeDocument == null) + { + throw new ArgumentNullException(nameof(codeDocument)); + } + + if (documentNode == null) + { + throw new ArgumentNullException(nameof(documentNode)); + } + + var @namespace = documentNode.FindPrimaryNamespace(); + var @class = documentNode.FindPrimaryClass(); + if (@namespace == null || @class == null) + { + return; + } + + var directives = documentNode.FindDirectiveReferences(PageDirective.Directive); + if (directives.Count == 0) + { + return; + } + + // We don't allow @page directives in imports + for (var i = 0; i < directives.Count; i++) + { + var directive = directives[i]; + if (directive.Node.IsImported()) + { + directive.Node.Diagnostics.Add(ComponentDiagnosticFactory.CreatePageDirective_CannotBeImported(directive.Node.Source.Value)); + } + } + + // Insert the attributes 'on-top' of the class declaration, since classes don't directly support attributes. + var index = 0; + for (; index < @namespace.Children.Count; index++) + { + if (object.ReferenceEquals(@class, @namespace.Children[index])) + { + break; + } + } + + for (var i = 0; i < directives.Count; i++) + { + var pageDirective = (DirectiveIntermediateNode)directives[i].Node; + + // The parser also adds errors for invalid syntax, we just need to not crash. + var routeToken = pageDirective.Tokens.FirstOrDefault(); + + if (routeToken != null && + routeToken.Content.Length >= 3 && + routeToken.Content[0] == '\"' && + routeToken.Content[1] == '/' && + routeToken.Content[routeToken.Content.Length - 1] == '\"') + { + var template = routeToken.Content.Substring(1, routeToken.Content.Length - 2); + @namespace.Children.Insert(index++, new RouteAttributeExtensionNode(template)); + } + else + { + pageDirective.Diagnostics.Add(ComponentDiagnosticFactory.CreatePageDirective_MustSpecifyRoute(pageDirective.Source)); + } + } + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/RazorCompilerException.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/RazorCompilerException.cs new file mode 100644 index 0000000000..d44b29db2c --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/RazorCompilerException.cs @@ -0,0 +1,29 @@ +// 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 Microsoft.AspNetCore.Razor.Language; +using System; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + /// + /// Represents a fatal error during the transformation of a Blazor component from + /// Razor source code to C# source code. + /// + public class RazorCompilerException : Exception + { + /// + /// Constructs an instance of . + /// + /// + public RazorCompilerException(RazorDiagnostic diagnostic) + { + Diagnostic = diagnostic; + } + + /// + /// Gets the diagnostic value. + /// + public RazorDiagnostic Diagnostic { get; } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/RefExtensionNode.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/RefExtensionNode.cs new file mode 100644 index 0000000000..b822b9345a --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/RefExtensionNode.cs @@ -0,0 +1,66 @@ +// 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.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal class RefExtensionNode : ExtensionIntermediateNode + { + public RefExtensionNode(IntermediateToken identifierToken) + { + IdentifierToken = identifierToken ?? throw new ArgumentNullException(nameof(identifierToken)); + Source = IdentifierToken.Source; + } + + public RefExtensionNode(IntermediateToken identifierToken, string componentCaptureTypeName) + : this(identifierToken) + { + if (string.IsNullOrEmpty(componentCaptureTypeName)) + { + throw new ArgumentException("Cannot be null or empty", nameof(componentCaptureTypeName)); + } + + IsComponentCapture = true; + ComponentCaptureTypeName = componentCaptureTypeName; + } + + public override IntermediateNodeCollection Children => IntermediateNodeCollection.ReadOnly; + + public IntermediateToken IdentifierToken { get; } + + public bool IsComponentCapture { get; } + + public string ComponentCaptureTypeName { get; set; } + + public string TypeName => $"global::System.Action<{(IsComponentCapture ? ComponentCaptureTypeName : "global::" + ComponentsApi.ElementRef.FullTypeName)}>"; + + public override void Accept(IntermediateNodeVisitor visitor) + { + if (visitor == null) + { + throw new ArgumentNullException(nameof(visitor)); + } + + AcceptExtensionNode(this, visitor); + } + + public override void WriteNode(CodeTarget target, CodeRenderingContext context) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var writer = (BlazorNodeWriter)context.NodeWriter; + writer.WriteReferenceCapture(context, this); + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/RefLoweringPass.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/RefLoweringPass.cs new file mode 100644 index 0000000000..f53c7d33a2 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/RefLoweringPass.cs @@ -0,0 +1,82 @@ +// 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 Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal class RefLoweringPass : IntermediateNodePassBase, IRazorOptimizationPass + { + // Run after component lowering pass + public override int Order => 50; + + protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + var @namespace = documentNode.FindPrimaryNamespace(); + var @class = documentNode.FindPrimaryClass(); + if (@namespace == null || @class == null) + { + // Nothing to do, bail. We can't function without the standard structure. + return; + } + + var references = documentNode.FindDescendantReferences(); + for (var i = 0; i < references.Count; i++) + { + var reference = references[i]; + var node = (TagHelperPropertyIntermediateNode)reference.Node; + + if (node.TagHelper.IsRefTagHelper()) + { + reference.Replace(RewriteUsage(@class, reference.Parent, node)); + } + } + } + + private IntermediateNode RewriteUsage(ClassDeclarationIntermediateNode classNode, IntermediateNode parent, TagHelperPropertyIntermediateNode node) + { + // If we can't get a nonempty attribute name, do nothing because there will + // already be a diagnostic for empty values + var identifierToken = DetermineIdentifierToken(node); + if (identifierToken == null) + { + return node; + } + + // Determine whether this is an element capture or a component capture, and + // if applicable the type name that will appear in the resulting capture code + var componentTagHelper = (parent as ComponentExtensionNode)?.Component; + if (componentTagHelper != null) + { + return new RefExtensionNode(identifierToken, componentTagHelper.GetTypeName()); + } + else + { + return new RefExtensionNode(identifierToken); + } + } + + private IntermediateToken DetermineIdentifierToken(TagHelperPropertyIntermediateNode attributeNode) + { + IntermediateToken foundToken = null; + + if (attributeNode.Children.Count == 1) + { + if (attributeNode.Children[0] is IntermediateToken token) + { + foundToken = token; + } + else if (attributeNode.Children[0] is CSharpExpressionIntermediateNode csharpNode) + { + if (csharpNode.Children.Count == 1) + { + foundToken = csharpNode.Children[0] as IntermediateToken; + } + } + } + + return !string.IsNullOrWhiteSpace(foundToken?.Content) ? foundToken : null; + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/RouteAttributeExtensionNode.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/RouteAttributeExtensionNode.cs new file mode 100644 index 0000000000..5d70fbb034 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/RouteAttributeExtensionNode.cs @@ -0,0 +1,33 @@ +// 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 Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal class RouteAttributeExtensionNode : ExtensionIntermediateNode + { + public RouteAttributeExtensionNode(string template) + { + Template = template; + } + + public string Template { get; } + + public override IntermediateNodeCollection Children => IntermediateNodeCollection.ReadOnly; + + public override void Accept(IntermediateNodeVisitor visitor) => AcceptExtensionNode(this, visitor); + + public override void WriteNode(CodeTarget target, CodeRenderingContext context) + { + context.CodeWriter.Write("["); + context.CodeWriter.Write(ComponentsApi.RouteAttribute.FullTypeName); + context.CodeWriter.Write("(\""); + context.CodeWriter.Write(Template); + context.CodeWriter.Write("\")"); + context.CodeWriter.Write("]"); + context.CodeWriter.WriteLine(); + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ScopeStack.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ScopeStack.cs new file mode 100644 index 0000000000..5349310415 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ScopeStack.cs @@ -0,0 +1,91 @@ +// 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 Microsoft.AspNetCore.Razor.Language.CodeGeneration; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + /// + /// Keeps track of the nesting of elements/containers while writing out the C# source code + /// for a component. This allows us to detect mismatched start/end tags, as well as inject + /// additional C# source to capture component descendants in a lambda. + /// + internal class ScopeStack + { + private readonly Stack _stack = new Stack(); + private int _builderVarNumber = 1; + + public string BuilderVarName { get; private set; } = "builder"; + + public void OpenComponentScope(CodeRenderingContext context, string name, string parameterName) + { + var scope = new ScopeEntry(name, ScopeKind.Component); + _stack.Push(scope); + + OffsetBuilderVarNumber(1); + + // Writes code that looks like: + // + // ((__builder) => { ... }) + // OR + // ((context) => (__builder) => { ... }) + + if (parameterName != null) + { + context.CodeWriter.Write($"({parameterName}) => "); + } + + scope.LambdaScope = context.CodeWriter.BuildLambda(BuilderVarName); + } + + public void OpenTemplateScope(CodeRenderingContext context) + { + var currentScope = new ScopeEntry("__template", ScopeKind.Template); + _stack.Push(currentScope); + + // Templates always get a lambda scope, because they are defined as a lambda. + OffsetBuilderVarNumber(1); + currentScope.LambdaScope = context.CodeWriter.BuildLambda(BuilderVarName); + } + + public void CloseScope(CodeRenderingContext context) + { + var currentScope = _stack.Pop(); + currentScope.LambdaScope.Dispose(); + OffsetBuilderVarNumber(-1); + } + + private void OffsetBuilderVarNumber(int delta) + { + _builderVarNumber += delta; + BuilderVarName = _builderVarNumber == 1 + ? "builder" + : $"builder{_builderVarNumber}"; + } + + private class ScopeEntry + { + public readonly string Name; + public ScopeKind Kind; + public int ChildCount; + public IDisposable LambdaScope; + + public ScopeEntry(string name, ScopeKind kind) + { + Name = name; + Kind = kind; + ChildCount = 0; + } + + public override string ToString() => $"<{Name}> ({Kind})"; + } + + private enum ScopeKind + { + Component, + Template, + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ScriptTagPass.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ScriptTagPass.cs new file mode 100644 index 0000000000..52c2b39a50 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ScriptTagPass.cs @@ -0,0 +1,59 @@ +// 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.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal class ScriptTagPass : IntermediateNodePassBase, IRazorDocumentClassifierPass + { + // Run as soon as possible after the Component rewrite pass + public override int Order => ComponentDocumentClassifierPass.DefaultFeatureOrder + 2; + + protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + if (documentNode.DocumentKind != ComponentDocumentClassifierPass.ComponentDocumentKind) + { + return; + } + + var visitor = new Visitor(); + visitor.Visit(documentNode); + } + + private class Visitor : IntermediateNodeWalker, IExtensionIntermediateNodeVisitor + { + public void VisitExtension(HtmlElementIntermediateNode node) + { + // Disallow