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 aa5c82ca5e
This commit is contained in:
Ryan Nowak 2018-12-19 22:43:10 -08:00
parent c37dada0dc
commit 51d19d1745
73 changed files with 11040 additions and 91 deletions

View File

@ -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<TagHelperPropertyIntermediateNode>();
var parents = new HashSet<IntermediateNode>();
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 <input bind="..." /> 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<TagHelperPropertyIntermediateNode>()
.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: <MyComponent bind-Value="@currentCount" />
// Output: <MyComponent Value ="...<get the value>..." ValueChanged ="... <set the value>..." />
//
// 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<object> 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(<code>) OR
// BindMethods.GetValue(<code>, <format>)
var valueExpressionTokens = new List<IntermediateToken>();
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 => <code> = __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 => <code> = __value, <code>) OR
// BindMethods.SetValueHandler(__value => <code> = __value, <code>, <format>)
//
// 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<IntermediateToken>()
{
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 <input type="text" bind="@Foo" /> 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 '<PropertyName>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<TemplateIntermediateNode>().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<IntermediateToken>().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<IntermediateToken>().Select(t => t.Content)),
};
}
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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
{
/// <summary>
/// Directs a <see cref="DocumentWriter"/> to use <see cref="BlazorRuntimeNodeWriter"/>.
/// </summary>
internal class BlazorCodeTarget : CodeTarget
{
private readonly RazorCodeGenerationOptions _options;
public BlazorCodeTarget(RazorCodeGenerationOptions options, IEnumerable<ICodeTargetExtension> 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<TExtension>()
{
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<TExtension>()
{
for (var i = 0; i < Extensions.Length; i++)
{
var match = Extensions[i] as TExtension;
if (match != null)
{
return true;
}
}
return false;
}
}
}

View File

@ -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<MyComponent>(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
// '<MyComponent>\r\n</MyComponent>' 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):
// <MyComponent Value="
//
// Or a CSharpExpressionIntermediateNode when the attribute has an explicit transition like:
// <MyComponent Value="@value" />
//
// 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<IntermediateToken> GetCSharpTokens(ComponentAttributeExtensionNode attribute)
{
// We generally expect all children to be CSharp, this is here just in case.
return attribute.FindDescendantNodes<IntermediateToken>().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>)((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<IntermediateToken> GetCSharpTokens(ComponentTypeArgumentExtensionNode arg)
{
// We generally expect all children to be CSharp, this is here just in case.
return arg.FindDescendantNodes<IntermediateToken>().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);
}
}
}
}

View File

@ -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
{
/// <summary>
/// Initializes the Blazor extension.
/// </summary>
public class BlazorExtensionInitializer : RazorExtensionInitializer
{
/// <summary>
/// Specifies the declaration configuration.
/// </summary>
public static readonly RazorConfiguration DeclarationConfiguration;
/// <summary>
/// Specifies the default configuration.
/// </summary>
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<RazorExtension>());
DefaultConfiguration = RazorConfiguration.Create(
RazorLanguageVersion.Experimental,
"Blazor-0.1",
Array.Empty<RazorExtension>());
}
/// <summary>
/// Registers the Blazor extension.
/// </summary>
/// <param name="builder">The <see cref="RazorProjectEngineBuilder"/>.</param>
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<IImportProjectFeature>().Single());
builder.Features.Add(new BlazorImportProjectFeature());
var index = builder.Phases.IndexOf(builder.Phases.OfType<IRazorCSharpLoweringPhase>().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());
}
}
/// <summary>
/// Initializes the Blazor extension.
/// </summary>
/// <param name="builder">The <see cref="RazorProjectEngineBuilder"/>.</param>
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;
}
}
}
}

View File

@ -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<RazorProjectItem> GetImports(RazorProjectItem projectItem)
{
if (projectItem == null)
{
throw new ArgumentNullException(nameof(projectItem));
}
var imports = new List<RazorProjectItem>()
{
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<RazorProjectItem> 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);
}
}
}

View File

@ -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";
/// <summary>
/// The name of the synthesized attribute used to set a child content parameter.
/// </summary>
public static readonly string ParameterAttributeName = "Context";
/// <summary>
/// The default name of the child content parameter (unless set by a Context attribute).
/// </summary>
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";
}
}
}

View File

@ -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 <elem onsomeevent=@{ /* some C# code */ } /> 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<IntermediateToken>().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<T1, T2>(RenderTreeBuilder builder, int seq, int __seq0, T1 __arg0, int __seq1, global::System.Collections.Generic.List<T2> __arg1, int __seq2, string __arg2)
// {
// builder.OpenComponent<Foo<T1, T2>>();
// 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<object>. 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<TComponent>(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;
}
}
}
}

View File

@ -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
{
/// <summary>
/// Generates the C# code corresponding to Razor source document contents.
/// </summary>
internal class BlazorRuntimeNodeWriter : BlazorNodeWriter
{
private readonly List<IntermediateToken> _currentAttributeValues = new List<IntermediateToken>();
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<MyComponent>(0);
// builder.AddAttribute(1, "Foo", ...);
// builder.AddAttribute(2, "ChildContent", ...);
// builder.AddElementCapture(3, (__value) => _field = __value);
// builder.CloseComponent();
// builder.OpenComponent<TComponent>(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<IntermediateToken> GetCSharpTokens(ComponentAttributeExtensionNode attribute)
{
// We generally expect all children to be CSharp, this is here just in case.
return attribute.FindDescendantNodes<IntermediateToken>().Where(t => t.IsCSharp).ToArray();
}
IReadOnlyList<IntermediateToken> GetHtmlTokens(HtmlContentIntermediateNode html)
{
// We generally expect all children to be HTML, this is here just in case.
return html.FindDescendantNodes<IntermediateToken>().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>)((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<IntermediateToken> 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<IntermediateToken>().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<IntermediateToken> 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");
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<ComponentExtensionNode>, IExtensionIntermediateNodeVisitor<ComponentChildContentIntermediateNode>
{
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);
}
}
}
}

View File

@ -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<string> 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<string> 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<string> modifiers,
string name,
string baseType,
IEnumerable<string> 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<KeyValuePair<string, string>> 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()
{
}
}
}
}

View File

@ -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<TagHelperIntermediateNode>();
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;
}
}
}

View File

@ -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<ComponentAttributeExtensionNode>(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);
}
}
}

View File

@ -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<ComponentChildContentIntermediateNode>(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);
}
}
}

View File

@ -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<BoundAttributeDescriptor> attributes)
//{
// Debug.Assert(component.Component.IsGenericTypedComponent());
public static RazorDiagnostic Create_GenericComponentMissingTypeArgument(
SourceSpan? source,
ComponentExtensionNode component,
IEnumerable<BoundAttributeDescriptor> 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<BoundAttributeDescriptor> attributes)
//{
// Debug.Assert(component.Component.IsGenericTypedComponent());
public static RazorDiagnostic Create_GenericComponentTypeInferenceUnderspecified(
SourceSpan? source,
ComponentExtensionNode component,
IEnumerable<BoundAttributeDescriptor> 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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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<ComponentAttributeExtensionNode> Attributes => Children.OfType<ComponentAttributeExtensionNode>();
public IEnumerable<RefExtensionNode> Captures => Children.OfType<RefExtensionNode>();
public IEnumerable<ComponentChildContentIntermediateNode> ChildContents => Children.OfType<ComponentChildContentIntermediateNode>();
public override IntermediateNodeCollection Children { get; } = new IntermediateNodeCollection();
public TagHelperDescriptor Component { get; set; }
/// <summary>
/// Gets the child content parameter name (null if unset) that was applied at the component level.
/// </summary>
public string ChildContentParameterName { get; set; }
public IEnumerable<ComponentTypeArgumentExtensionNode> TypeArguments => Children.OfType<ComponentTypeArgumentExtensionNode>();
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<ComponentExtensionNode>(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("</");
builder.Append(TagName);
builder.Append(">");
return builder.ToString();
}
}
}
}

View File

@ -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<TagHelperIntermediateNode>();
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 '<MyComponent> </MyComponent>
//
// 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: <MyComponent><ChildContent><div>...</div></ChildContent></MyComponent>
// compared with implicit: <MyComponent><div></div></MyComponent>
//
// 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<TagHelperIntermediateNode>().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.
//
// <MyComponent><ChildContent Context="@Foo()">...</ChildContent></MyComponent>
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<IntermediateToken>().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.
//
// <MyComponent Context="@Foo()">...</MyComponent>
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);
}
}
}
}

View File

@ -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<ComponentTypeArgumentExtensionNode>(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);
}
}
}

View File

@ -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
{
/// <summary>
/// Represents a type-inference thunk that is used by the generated component code.
/// </summary>
internal class ComponentTypeInferenceMethodIntermediateNode : ExtensionIntermediateNode
{
public override IntermediateNodeCollection Children => IntermediateNodeCollection.ReadOnly;
/// <summary>
/// Gets the component usage linked to this type inference method.
/// </summary>
public ComponentExtensionNode Component { get; set; }
/// <summary>
/// Gets the full type name of the generated class containing this method.
/// </summary>
public string FullTypeName { get; internal set; }
/// <summary>
/// Gets the name of the generated method.
/// </summary>
public string MethodName { get; set; }
public override void Accept(IntermediateNodeVisitor visitor)
{
if (visitor == null)
{
throw new ArgumentNullException(nameof(visitor));
}
AcceptExtensionNode<ComponentTypeInferenceMethodIntermediateNode>(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);
}
}
}

View File

@ -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";
}
}
}

View File

@ -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<TagHelperPropertyIntermediateNode>();
var parents = new HashSet<IntermediateNode>();
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<TagHelperPropertyIntermediateNode>()
.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<TDelegate>(<code>)
//
// 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<IntermediateToken>()
{
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<IntermediateToken> GetAttributeContent(TagHelperPropertyIntermediateNode node)
{
var template = node.FindDescendantNodes<TemplateIntermediateNode>().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<IntermediateToken>();
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<IntermediateToken>();
}
}
}
}

View File

@ -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<TypeNameFeature>();
}
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
{
var visitor = new Visitor(_typeNameFeature);
visitor.Visit(documentNode);
}
private class Visitor : IntermediateNodeWalker, IExtensionIntermediateNodeVisitor<ComponentExtensionNode>
{
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<string, Binding>();
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<string, TItem>
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<IntermediateToken>().Where(t => t.IsCSharp).Select(t => t.Content));
}
private static bool ValidateTypeArguments(ComponentExtensionNode node, Dictionary<string, Binding> bindings)
{
var missing = new List<BoundAttributeDescriptor>();
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<object>
attribute.TypeName = "System.Action<System.Object>";
}
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<object>
childContent.TypeName = ComponentsApi.RenderFragment.FullTypeName + "<System.Object>";
}
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<NamespaceDeclarationIntermediateNode>()
.Where(n => n.Annotations.Contains(new KeyValuePair<object, object>(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<ClassDeclarationIntermediateNode>()
.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; }
}
}
}

View File

@ -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<HtmlBlockIntermediateNode>(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;
}
}

View File

@ -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:
// <div>
// <a href="...">click me</a>
// </div>
//
// 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<HtmlElementIntermediateNode>
{
private bool _foundNonHtml;
public List<IntermediateNodeReference> Trees { get; } = new List<IntermediateNodeReference>();
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<HtmlElementIntermediateNode>
{
private readonly StringBuilder _encodingBuilder;
private readonly List<IntermediateNodeReference> _trees;
public RewriteVisitor(List<IntermediateNodeReference> 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("></");
Builder.Append(node.TagName);
Builder.Append(">");
return;
}
// start/end tag with body.
Builder.Append(">");
foreach (var item in node.Body)
{
Visit(item);
}
Builder.Append("</");
Builder.Append(node.TagName);
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);
}
}
}
}

View File

@ -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<HtmlAttributeIntermediateNode> Attributes => Children.OfType<HtmlAttributeIntermediateNode>();
public IEnumerable<RefExtensionNode> Captures => Children.OfType<RefExtensionNode>();
public IEnumerable<IntermediateNode> 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<HtmlElementIntermediateNode>(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("</");
builder.Append(TagName);
builder.Append(">");
return builder.ToString();
}
}
}
}

View File

@ -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());
}
}
}

View File

@ -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<string>();
}
foreach (var implements in documentNode.FindDirectiveReferences(ImplementsDirective.Directive))
{
var token = ((DirectiveIntermediateNode)implements.Node).Tokens.FirstOrDefault();
if (token != null)
{
@class.Interfaces.Add(token.Content);
}
}
}
}
}

View File

@ -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<string>(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<DirectiveIntermediateNode> Directives { get; }
= new List<DirectiveIntermediateNode>();
public override void VisitDirective(DirectiveIntermediateNode node)
{
if (node.Directive == Directive)
{
Directives.Add(node);
}
}
}
internal class InjectIntermediateNode : ExtensionIntermediateNode
{
private static readonly IList<string> _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);
}
}
}
}

View File

@ -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());
}
}
}

View File

@ -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;
}
}
}
}
}

View File

@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<TargetFrameworks Condition="'$(OS)' == 'Windows_NT'">$(TargetFrameworks);net461</TargetFrameworks>
<RootNamespace>Microsoft.AspNetCore.Components.Razor</RootNamespace>
<Description>Extensions to the Razor compiler to support building Razor Components.</Description>
<IsProductPackage>true</IsProductPackage>
<!-- Copy package references to output, needed so the build project can find them -->
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="0.9.9.2" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Language" Version="$(RazorPackageVersion)" />
<PackageReference Include="Microsoft.CodeAnalysis.Razor" Version="$(RazorPackageVersion)" />
</ItemGroup>
<ItemGroup>
<Compile Update="Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Compile Include="..\shared\ComponentsApi.cs" Link="shared\%(Filename)%(Extension)" />
</ItemGroup>
</Project>

View File

@ -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;
}
}
}

View File

@ -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));
}
}
}
}
}

View File

@ -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
{
/// <summary>
/// Represents a fatal error during the transformation of a Blazor component from
/// Razor source code to C# source code.
/// </summary>
public class RazorCompilerException : Exception
{
/// <summary>
/// Constructs an instance of <see cref="RazorCompilerException"/>.
/// </summary>
/// <param name="diagnostic"></param>
public RazorCompilerException(RazorDiagnostic diagnostic)
{
Diagnostic = diagnostic;
}
/// <summary>
/// Gets the diagnostic value.
/// </summary>
public RazorDiagnostic Diagnostic { get; }
}
}

View File

@ -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<RefExtensionNode>(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);
}
}
}

View File

@ -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<TagHelperPropertyIntermediateNode>();
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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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
{
/// <summary>
/// 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.
/// </summary>
internal class ScopeStack
{
private readonly Stack<ScopeEntry> _stack = new Stack<ScopeEntry>();
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,
}
}
}

View File

@ -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<HtmlElementIntermediateNode>
{
public void VisitExtension(HtmlElementIntermediateNode node)
{
// Disallow <script> in components as per #552
if (string.Equals(node.TagName, "script", StringComparison.OrdinalIgnoreCase))
{
for (var i = 0; i < node.Children.Count; i++)
{
// We allow you to suppress this error like:
// <script suppress-error="BL9992" />
var attribute = node.Children[i] as HtmlAttributeIntermediateNode;
if (attribute != null &&
attribute.AttributeName == "suppress-error" &&
attribute.Children.Count == 1 &&
attribute.Children[0] is HtmlAttributeValueIntermediateNode value &&
value.Children.Count == 1 &&
value.Children[0] is IntermediateToken token &&
token.IsHtml &&
string.Equals(token.Content, "BL9992", StringComparison.Ordinal))
{
node.Children.RemoveAt(i);
return;
}
}
var diagnostic = ComponentDiagnosticFactory.Create_DisallowedScriptTag(node.Source);
node.Diagnostics.Add(diagnostic);
}
base.VisitDefault(node);
}
}
}
}

View File

@ -0,0 +1,154 @@
// 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;
namespace Microsoft.AspNetCore.Razor.Language.Components
{
internal static class TagHelperBoundAttributeDescriptorExtensions
{
public static bool IsDelegateProperty(this BoundAttributeDescriptor attribute)
{
if (attribute == null)
{
throw new ArgumentNullException(nameof(attribute));
}
var key = BlazorMetadata.Component.DelegateSignatureKey;
return
attribute.Metadata.TryGetValue(key, out var value) &&
string.Equals(value, bool.TrueString);
}
public static bool IsGenericTypedProperty(this BoundAttributeDescriptor attribute)
{
if (attribute == null)
{
throw new ArgumentNullException(nameof(attribute));
}
return
attribute.Metadata.TryGetValue(BlazorMetadata.Component.GenericTypedKey, out var value) &&
string.Equals(value, bool.TrueString);
}
public static bool IsTypeParameterProperty(this BoundAttributeDescriptor attribute)
{
if (attribute == null)
{
throw new ArgumentNullException(nameof(attribute));
}
return
attribute.Metadata.TryGetValue(BlazorMetadata.Component.TypeParameterKey, out var value) &&
string.Equals(value, bool.TrueString);
}
public static bool IsWeaklyTyped(this BoundAttributeDescriptor attribute)
{
if (attribute == null)
{
throw new ArgumentNullException(nameof(attribute));
}
var key = BlazorMetadata.Component.WeaklyTypedKey;
return
attribute.Metadata.TryGetValue(key, out var value) &&
string.Equals(value, bool.TrueString);
}
/// <summary>
/// Gets a value that indicates whether the property is a child content property. Properties are
/// considered child content if they have the type <c>RenderFragment</c> or <c>RenderFragment{T}</c>.
/// </summary>
/// <param name="attribute">The <see cref="BoundAttributeDescriptor"/>.</param>
/// <returns>Returns <c>true</c> if the property is child content, otherwise <c>false</c>.</returns>
public static bool IsChildContentProperty(this BoundAttributeDescriptor attribute)
{
if (attribute == null)
{
throw new ArgumentNullException(nameof(attribute));
}
var key = BlazorMetadata.Component.ChildContentKey;
return
attribute.Metadata.TryGetValue(key, out var value) &&
string.Equals(value, bool.TrueString);
}
/// <summary>
/// Gets a value that indicates whether the property is a child content property. Properties are
/// considered child content if they have the type <c>RenderFragment</c> or <c>RenderFragment{T}</c>.
/// </summary>
/// <param name="attribute">The <see cref="BoundAttributeDescriptorBuilder"/>.</param>
/// <returns>Returns <c>true</c> if the property is child content, otherwise <c>false</c>.</returns>
public static bool IsChildContentProperty(this BoundAttributeDescriptorBuilder attribute)
{
if (attribute == null)
{
throw new ArgumentNullException(nameof(attribute));
}
var key = BlazorMetadata.Component.ChildContentKey;
return
attribute.Metadata.TryGetValue(key, out var value) &&
string.Equals(value, bool.TrueString);
}
/// <summary>
/// Gets a value that indicates whether the property is a parameterized child content property. Properties are
/// considered parameterized child content if they have the type <c>RenderFragment{T}</c> (for some T).
/// </summary>
/// <param name="attribute">The <see cref="BoundAttributeDescriptor"/>.</param>
/// <returns>Returns <c>true</c> if the property is parameterized child content, otherwise <c>false</c>.</returns>
public static bool IsParameterizedChildContentProperty(this BoundAttributeDescriptor attribute)
{
if (attribute == null)
{
throw new ArgumentNullException(nameof(attribute));
}
return attribute.IsChildContentProperty() &&
!string.Equals(attribute.TypeName, ComponentsApi.RenderFragment.FullTypeName, StringComparison.Ordinal);
}
/// <summary>
/// Gets a value that indicates whether the property is a parameterized child content property. Properties are
/// considered parameterized child content if they have the type <c>RenderFragment{T}</c> (for some T).
/// </summary>
/// <param name="attribute">The <see cref="BoundAttributeDescriptor"/>.</param>
/// <returns>Returns <c>true</c> if the property is parameterized child content, otherwise <c>false</c>.</returns>
public static bool IsParameterizedChildContentProperty(this BoundAttributeDescriptorBuilder attribute)
{
if (attribute == null)
{
throw new ArgumentNullException(nameof(attribute));
}
return attribute.IsChildContentProperty() &&
!string.Equals(attribute.TypeName, ComponentsApi.RenderFragment.FullTypeName, StringComparison.Ordinal);
}
/// <summary>
/// Gets a value that indicates whether the property is used to specify the name of the parameter
/// for a parameterized child content property.
/// </summary>
/// <param name="attribute">The <see cref="BoundAttributeDescriptor"/>.</param>
/// <returns>
/// Returns <c>true</c> if the property specifies the name of a parameter for a parameterized child content,
/// otherwise <c>false</c>.
/// </returns>
public static bool IsChildContentParameterNameProperty(this BoundAttributeDescriptor attribute)
{
if (attribute == null)
{
throw new ArgumentNullException(nameof(attribute));
}
var key = BlazorMetadata.Component.ChildContentParameterNameKey;
return
attribute.Metadata.TryGetValue(key, out var value) &&
string.Equals(value, bool.TrueString);
}
}
}

View File

@ -0,0 +1,199 @@
// 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;
namespace Microsoft.AspNetCore.Razor.Language.Components
{
internal static class TagHelperDescriptorExtensions
{
public static bool IsBindTagHelper(this TagHelperDescriptor tagHelper)
{
if (tagHelper == null)
{
throw new ArgumentNullException(nameof(tagHelper));
}
return
tagHelper.Metadata.TryGetValue(BlazorMetadata.SpecialKindKey, out var kind) &&
string.Equals(BlazorMetadata.Bind.TagHelperKind, kind);
}
public static bool IsFallbackBindTagHelper(this TagHelperDescriptor tagHelper)
{
if (tagHelper == null)
{
throw new ArgumentNullException(nameof(tagHelper));
}
return
tagHelper.IsBindTagHelper() &&
tagHelper.Metadata.TryGetValue(BlazorMetadata.Bind.FallbackKey, out var fallback) &&
string.Equals(bool.TrueString, fallback);
}
public static bool IsGenericTypedComponent(this TagHelperDescriptor tagHelper)
{
if (tagHelper == null)
{
throw new ArgumentNullException(nameof(tagHelper));
}
return
IsComponentTagHelper(tagHelper) &&
tagHelper.Metadata.TryGetValue(BlazorMetadata.Component.GenericTypedKey, out var value) &&
string.Equals(bool.TrueString, value);
}
public static bool IsInputElementBindTagHelper(this TagHelperDescriptor tagHelper)
{
if (tagHelper == null)
{
throw new ArgumentNullException(nameof(tagHelper));
}
return
tagHelper.IsBindTagHelper() &&
tagHelper.TagMatchingRules.Count == 1 &&
string.Equals("input", tagHelper.TagMatchingRules[0].TagName);
}
public static bool IsInputElementFallbackBindTagHelper(this TagHelperDescriptor tagHelper)
{
if (tagHelper == null)
{
throw new ArgumentNullException(nameof(tagHelper));
}
return
tagHelper.IsInputElementBindTagHelper() &&
!tagHelper.Metadata.ContainsKey(BlazorMetadata.Bind.TypeAttribute);
}
public static string GetValueAttributeName(this TagHelperDescriptor tagHelper)
{
if (tagHelper == null)
{
throw new ArgumentNullException(nameof(tagHelper));
}
tagHelper.Metadata.TryGetValue(BlazorMetadata.Bind.ValueAttribute, out var result);
return result;
}
public static string GetChangeAttributeName(this TagHelperDescriptor tagHelper)
{
if (tagHelper == null)
{
throw new ArgumentNullException(nameof(tagHelper));
}
tagHelper.Metadata.TryGetValue(BlazorMetadata.Bind.ChangeAttribute, out var result);
return result;
}
public static bool IsChildContentTagHelper(this TagHelperDescriptor tagHelper)
{
if (tagHelper == null)
{
throw new ArgumentNullException(nameof(tagHelper));
}
return
tagHelper.Metadata.TryGetValue(BlazorMetadata.SpecialKindKey, out var value) &&
string.Equals(value, BlazorMetadata.ChildContent.TagHelperKind, StringComparison.Ordinal);
}
public static bool IsComponentTagHelper(this TagHelperDescriptor tagHelper)
{
if (tagHelper == null)
{
throw new ArgumentNullException(nameof(tagHelper));
}
return !tagHelper.Metadata.ContainsKey(BlazorMetadata.SpecialKindKey);
}
public static bool IsEventHandlerTagHelper(this TagHelperDescriptor tagHelper)
{
if (tagHelper == null)
{
throw new ArgumentNullException(nameof(tagHelper));
}
return
tagHelper.Metadata.TryGetValue(BlazorMetadata.SpecialKindKey, out var kind) &&
string.Equals(BlazorMetadata.EventHandler.TagHelperKind, kind);
}
public static bool IsRefTagHelper(this TagHelperDescriptor tagHelper)
{
if (tagHelper == null)
{
throw new ArgumentNullException(nameof(tagHelper));
}
return
tagHelper.Metadata.TryGetValue(BlazorMetadata.SpecialKindKey, out var kind) &&
string.Equals(BlazorMetadata.Ref.TagHelperKind, kind);
}
public static string GetEventArgsType(this TagHelperDescriptor tagHelper)
{
if (tagHelper == null)
{
throw new ArgumentNullException(nameof(tagHelper));
}
tagHelper.Metadata.TryGetValue(BlazorMetadata.EventHandler.EventArgsType, out var result);
return result;
}
/// <summary>
/// Gets the set of component attributes that can accept child content (<c>RenderFragment</c> or <c>RenderFragment{T}</c>).
/// </summary>
/// <param name="tagHelper">The <see cref="TagHelperDescriptor"/>.</param>
/// <returns>The child content attributes</returns>
public static IEnumerable<BoundAttributeDescriptor> GetChildContentProperties(this TagHelperDescriptor tagHelper)
{
if (tagHelper == null)
{
throw new ArgumentNullException(nameof(tagHelper));
}
for (var i = 0; i < tagHelper.BoundAttributes.Count; i++)
{
var attribute = tagHelper.BoundAttributes[i];
if (attribute.IsChildContentProperty())
{
yield return attribute;
}
}
}
/// <summary>
/// Gets the set of component attributes that represent generic type parameters of the component type.
/// </summary>
/// <param name="tagHelper">The <see cref="TagHelperDescriptor"/>.</param>
/// <returns>The type parameter attributes</returns>
public static IEnumerable<BoundAttributeDescriptor> GetTypeParameters(this TagHelperDescriptor tagHelper)
{
if (tagHelper == null)
{
throw new ArgumentNullException(nameof(tagHelper));
}
for (var i = 0; i < tagHelper.BoundAttributes.Count; i++)
{
var attribute = tagHelper.BoundAttributes[i];
if (attribute.IsTypeParameterProperty())
{
yield return attribute;
}
}
}
}
}

View File

@ -0,0 +1,60 @@
// 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.Extensions;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Razor.Language.Components
{
internal class TemplateDiagnosticPass : IntermediateNodePassBase, IRazorOptimizationPass
{
// Runs after components/eventhandlers/ref/bind. We need to check for templates in all of those
// places.
public override int Order => 150;
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
{
var visitor = new Visitor();
visitor.Visit(documentNode);
for (var i = 0; i < visitor.Candidates.Count; i++)
{
var candidate = visitor.Candidates[i];
candidate.Parent.Diagnostics.Add(ComponentDiagnosticFactory.Create_TemplateInvalidLocation(candidate.Node.Source));
// Remove the offending node since we don't know how to render it. This means that the user won't get C#
// completion at this location, which is fine because it's inside an HTML attribute.
candidate.Remove();
}
}
private class Visitor : IntermediateNodeWalker, IExtensionIntermediateNodeVisitor<TemplateIntermediateNode>
{
public List<IntermediateNodeReference> Candidates { get; } = new List<IntermediateNodeReference>();
public void VisitExtension(TemplateIntermediateNode node)
{
// We found a template, let's check where it's located.
for (var i = 0; i < Ancestors.Count; i++)
{
var ancestor = Ancestors[i];
if (
// Inside markup attribute
ancestor is HtmlAttributeIntermediateNode ||
// Inside component attribute
ancestor is ComponentAttributeExtensionNode ||
// Inside malformed ref attribute
ancestor is TagHelperPropertyIntermediateNode)
{
Candidates.Add(new IntermediateNodeReference(Parent, node));
}
}
}
}
}
}

View File

@ -0,0 +1,101 @@
// 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;
using System;
namespace Microsoft.AspNetCore.Razor.Language.Components
{
internal class TrimWhitespacePass : 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));
}
// There's no benefit running the whitespace trimmer during design-time builds
if (!documentNode.Options.DesignTime)
{
var method = documentNode.FindPrimaryMethod();
if (method != null)
{
RemoveContiguousWhitespace(method.Children, TraversalDirection.Forwards);
RemoveContiguousWhitespace(method.Children, TraversalDirection.Backwards);
}
}
}
private static void RemoveContiguousWhitespace(IntermediateNodeCollection nodes, TraversalDirection direction)
{
var position = direction == TraversalDirection.Forwards ? 0 : nodes.Count - 1;
while (position >= 0 && position < nodes.Count)
{
var node = nodes[position];
bool shouldRemoveNode;
bool shouldContinueIteration;
switch (node)
{
case IntermediateToken intermediateToken:
shouldRemoveNode = string.IsNullOrWhiteSpace(intermediateToken.Content);
shouldContinueIteration = shouldRemoveNode;
break;
case HtmlContentIntermediateNode htmlContentIntermediateNode:
RemoveContiguousWhitespace(htmlContentIntermediateNode.Children, direction);
shouldRemoveNode = htmlContentIntermediateNode.Children.Count == 0;
shouldContinueIteration = shouldRemoveNode;
break;
case HtmlElementIntermediateNode _:
case CSharpExpressionIntermediateNode _:
case TagHelperIntermediateNode _:
// These node types may produce non-whitespace output at runtime
shouldRemoveNode = false;
shouldContinueIteration = false;
break;
case CSharpCodeIntermediateNode codeIntermediateNode:
shouldRemoveNode = false;
shouldContinueIteration = ComponentDocumentClassifierPass.IsBuildRenderTreeBaseCall(codeIntermediateNode);
break;
default:
shouldRemoveNode = false;
shouldContinueIteration = true; // Because other types of nodes don't produce output
break;
}
if (shouldRemoveNode)
{
nodes.RemoveAt(position);
if (direction == TraversalDirection.Forwards)
{
position--;
}
}
position += direction == TraversalDirection.Forwards ? 1 : -1;
if (!shouldContinueIteration)
{
break;
}
}
}
enum TraversalDirection
{
Forwards,
Backwards
}
}
}

View File

@ -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 class TypeParamDirective
{
public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective(
"typeparam",
DirectiveKind.SingleLine,
builder =>
{
builder.AddMemberToken(ComponentResources.TypeParamDirective_Token_Name, ComponentResources.TypeParamDirective_Token_Description);
builder.Usage = DirectiveUsage.FileScopedMultipleOccurring;
builder.Description = ComponentResources.TypeParamDirective_Description;
});
public static RazorProjectEngineBuilder Register(RazorProjectEngineBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.AddDirective(Directive);
return builder;
}
}
}

View File

@ -54,6 +54,19 @@ namespace Microsoft.AspNetCore.Razor.Language.Intermediate
visitor.Visit(node);
return visitor.Directives;
}
public static IReadOnlyList<IntermediateNodeReference> FindDescendantReferences<TNode>(this DocumentIntermediateNode document)
where TNode : IntermediateNode
{
if (document == null)
{
throw new ArgumentNullException(nameof(document));
}
var visitor = new ReferenceVisitor<TNode>();
visitor.Visit(document);
return visitor.References;
}
private static T FindWithAnnotation<T>(IntermediateNode node, object annotation) where T : IntermediateNode
{
@ -95,5 +108,25 @@ namespace Microsoft.AspNetCore.Razor.Language.Intermediate
base.VisitDirective(node);
}
}
private class ReferenceVisitor<TNode> : IntermediateNodeWalker
where TNode : IntermediateNode
{
public List<IntermediateNodeReference> References = new List<IntermediateNodeReference>();
public override void VisitDefault(IntermediateNode node)
{
base.VisitDefault(node);
// Use a post-order traversal because references are used to replace nodes, and thus
// change the parent nodes.
//
// This ensures that we always operate on the leaf nodes first.
if (node is TNode)
{
References.Add(new IntermediateNodeReference(Parent, node));
}
}
}
}
}

View File

@ -0,0 +1,18 @@
// 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;
namespace Microsoft.AspNetCore.Razor.Language
{
// Razor.Language doesn't reference Microsoft.CodeAnalysis.CSharp so we
// need some indirection.
internal abstract class TypeNameFeature : RazorEngineFeatureBase
{
public abstract IReadOnlyList<string> ParseTypeParameters(string typeName);
public abstract TypeNameRewriter CreateGenericTypeRewriter(Dictionary<string, string> bindings);
public abstract TypeNameRewriter CreateGlobalQualifiedTypeNameRewriter(ICollection<string> ignore);
}
}

View File

@ -0,0 +1,12 @@
// 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
{
// Razor.Language doesn't reference Microsoft.CodeAnalysis.CSharp so we
// need some indirection.
internal abstract class TypeNameRewriter
{
public abstract string Rewrite(string typeName);
}
}

View File

@ -5,10 +5,8 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.VisualStudio.LanguageServices.Razor.Serialization;
using Newtonsoft.Json;

View File

@ -0,0 +1,490 @@
// 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;
using Microsoft.AspNetCore.Razor.Language.Components;
namespace Microsoft.CodeAnalysis.Razor
{
internal class BindTagHelperDescriptorProvider : ITagHelperDescriptorProvider
{
// Run after the component tag helper provider, because we need to see the results.
public int Order { get; set; } = 1000;
public RazorEngine Engine { get; set; }
public void Execute(TagHelperDescriptorProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// This provider returns tag helper information for 'bind' which doesn't necessarily
// map to any real component. Bind behaviors more like a macro, which can map a single LValue to
// both a 'value' attribute and a 'value changed' attribute.
//
// User types:
// <input type="text" bind="@FirstName"/>
//
// We generate:
// <input type="text"
// value="@BindMethods.GetValue(FirstName)"
// onchange="@BindMethods.SetValue(__value => FirstName = __value, FirstName)"/>
//
// This isn't very different from code the user could write themselves - thus the pronouncement
// that bind is very much like a macro.
//
// A lot of the value that provide in this case is that the associations between the
// elements, and the attributes aren't straightforward.
//
// For instance on <input type="text" /> we need to listen to 'value' and 'onchange',
// but on <input type="checked" we need to listen to 'checked' and 'onchange'.
//
// We handle a few different cases here:
//
// 1. When given an attribute like **anywhere**'bind-value-onchange="@FirstName"' we will
// generate the 'value' attribute and 'onchange' attribute.
//
// We don't do any transformation or inference for this case, because the developer has
// told us exactly what to do. This is the *full* form of bind, and should support any
// combination of element, component, and attributes.
//
// This is the most general case, and is implemented with a built-in tag helper that applies
// to everything, and binds to a dictionary of attributes that start with bind-.
//
// 2. We also support cases like 'bind-value="@FirstName"' where we will generate the 'value'
// attribute and another attribute based for a changed handler based on the metadata.
//
// These mappings are provided by attributes that tell us what attributes, suffixes, and
// elements to map.
//
// 3. When given an attribute like 'bind="@FirstName"' we will generate a value and change
// attribute solely based on the context. We need the context of an HTML tag to know
// what attributes to generate.
//
// Similar to case #2, this should 'just work' from the users point of view. We expect
// using this syntax most frequently with input elements.
//
// These mappings are also provided by attributes. Primarily these are used by <input />
// and so we have a special case for input elements and their type attributes.
//
// 4. For components, we have a bit of a special case. We can infer a syntax that matches
// case #2 based on property names. So if a component provides both 'Value' and 'ValueChanged'
// we will turn that into an instance of bind.
//
// So case #1 here is the most general case. Case #2 and #3 are data-driven based on attribute data
// we have. Case #4 is data-driven based on component definitions.
//
// We provide a good set of attributes that map to the HTML dom. This set is user extensible.
var compilation = context.GetCompilation();
if (compilation == null)
{
return;
}
var bindMethods = compilation.GetTypeByMetadataName(ComponentsApi.BindMethods.FullTypeName);
if (bindMethods == null)
{
// If we can't find BindMethods, then just bail. We won't be able to compile the
// generated code anyway.
return;
}
// Tag Helper defintion for case #1. This is the most general case.
context.Results.Add(CreateFallbackBindTagHelper());
// For case #2 & #3 we have a whole bunch of attribute entries on BindMethods that we can use
// to data-drive the definitions of these tag helpers.
var elementBindData = GetElementBindData(compilation);
// Case #2 & #3
foreach (var tagHelper in CreateElementBindTagHelpers(elementBindData))
{
context.Results.Add(tagHelper);
}
// For case #4 we look at the tag helpers that were already created corresponding to components
// and pattern match on properties.
foreach (var tagHelper in CreateComponentBindTagHelpers(context.Results))
{
context.Results.Add(tagHelper);
}
}
private TagHelperDescriptor CreateFallbackBindTagHelper()
{
var builder = TagHelperDescriptorBuilder.Create(BlazorMetadata.Bind.TagHelperKind, "Bind", ComponentsApi.AssemblyName);
builder.Documentation = ComponentResources.BindTagHelper_Fallback_Documentation;
builder.Metadata.Add(BlazorMetadata.SpecialKindKey, BlazorMetadata.Bind.TagHelperKind);
builder.Metadata[TagHelperMetadata.Runtime.Name] = BlazorMetadata.Bind.RuntimeName;
builder.Metadata[BlazorMetadata.Bind.FallbackKey] = bool.TrueString;
// WTE has a bug in 15.7p1 where a Tag Helper without a display-name that looks like
// a C# property will crash trying to create the toolips.
builder.SetTypeName("Microsoft.AspNetCore.Components.Bind");
builder.TagMatchingRule(rule =>
{
rule.TagName = "*";
rule.Attribute(attribute =>
{
attribute.Name = "bind-";
attribute.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.PrefixMatch;
});
});
builder.BindAttribute(attribute =>
{
attribute.Documentation = ComponentResources.BindTagHelper_Fallback_Documentation;
attribute.Name = "bind-...";
attribute.AsDictionary("bind-", typeof(object).FullName);
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
// a C# property will crash trying to create the toolips.
attribute.SetPropertyName("Bind");
attribute.TypeName = "System.Collections.Generic.Dictionary<string, object>";
});
builder.BindAttribute(attribute =>
{
attribute.Documentation = ComponentResources.BindTagHelper_Fallback_Format_Documentation;
attribute.Name = "format-...";
attribute.AsDictionary("format-", typeof(string).FullName);
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
// a C# property will crash trying to create the toolips.
attribute.SetPropertyName("Format");
attribute.TypeName = "System.Collections.Generic.Dictionary<string, string>";
});
return builder.Build();
}
private List<ElementBindData> GetElementBindData(Compilation compilation)
{
var bindElement = compilation.GetTypeByMetadataName(ComponentsApi.BindElementAttribute.FullTypeName);
var bindInputElement = compilation.GetTypeByMetadataName(ComponentsApi.BindInputElementAttribute.FullTypeName);
if (bindElement == null || bindInputElement == null)
{
// This won't likely happen, but just in case.
return new List<ElementBindData>();
}
var types = new List<INamedTypeSymbol>();
var visitor = new BindElementDataVisitor(types);
// Visit the primary output of this compilation, as well as all references.
visitor.Visit(compilation.Assembly);
foreach (var reference in compilation.References)
{
// We ignore .netmodules here - there really isn't a case where they are used by user code
// even though the Roslyn APIs all support them.
if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly)
{
visitor.Visit(assembly);
}
}
var results = new List<ElementBindData>();
for (var i = 0; i < types.Count; i++)
{
var type = types[i];
var attributes = type.GetAttributes();
// Not handling duplicates here for now since we're the primary ones extending this.
// If we see users adding to the set of 'bind' constructs we will want to add deduplication
// and potentially diagnostics.
for (var j = 0; j < attributes.Length; j++)
{
var attribute = attributes[j];
if (attribute.AttributeClass == bindElement)
{
results.Add(new ElementBindData(
type.ContainingAssembly.Name,
type.ToDisplayString(),
(string)attribute.ConstructorArguments[0].Value,
null,
(string)attribute.ConstructorArguments[1].Value,
(string)attribute.ConstructorArguments[2].Value,
(string)attribute.ConstructorArguments[3].Value));
}
else if (attribute.AttributeClass == bindInputElement)
{
results.Add(new ElementBindData(
type.ContainingAssembly.Name,
type.ToDisplayString(),
"input",
(string)attribute.ConstructorArguments[0].Value,
(string)attribute.ConstructorArguments[1].Value,
(string)attribute.ConstructorArguments[2].Value,
(string)attribute.ConstructorArguments[3].Value));
}
}
}
return results;
}
private List<TagHelperDescriptor> CreateElementBindTagHelpers(List<ElementBindData> data)
{
var results = new List<TagHelperDescriptor>();
for (var i = 0; i < data.Count; i++)
{
var entry = data[i];
var name = entry.Suffix == null ? "Bind" : "Bind_" + entry.Suffix;
var attributeName = entry.Suffix == null ? "bind" : "bind-" + entry.Suffix;
var formatName = entry.Suffix == null ? "Format_" + entry.ValueAttribute : "Format_" + entry.Suffix;
var formatAttributeName = entry.Suffix == null ? "format-" + entry.ValueAttribute : "format-" + entry.Suffix;
var builder = TagHelperDescriptorBuilder.Create(BlazorMetadata.Bind.TagHelperKind, name, entry.Assembly);
builder.Documentation = string.Format(
ComponentResources.BindTagHelper_Element_Documentation,
entry.ValueAttribute,
entry.ChangeAttribute);
builder.Metadata.Add(BlazorMetadata.SpecialKindKey, BlazorMetadata.Bind.TagHelperKind);
builder.Metadata[TagHelperMetadata.Runtime.Name] = BlazorMetadata.Bind.RuntimeName;
builder.Metadata[BlazorMetadata.Bind.ValueAttribute] = entry.ValueAttribute;
builder.Metadata[BlazorMetadata.Bind.ChangeAttribute] = entry.ChangeAttribute;
if (entry.TypeAttribute != null)
{
// For entries that map to the <input /> element, we need to be able to know
// the difference between <input /> and <input type="text" .../> for which we
// want to use the same attributes.
//
// We provide a tag helper for <input /> that should match all input elements,
// but we only want it to be used when a more specific one is used.
//
// Therefore we use this metadata to know which one is more specific when two
// tag helpers match.
builder.Metadata[BlazorMetadata.Bind.TypeAttribute] = entry.TypeAttribute;
}
// WTE has a bug in 15.7p1 where a Tag Helper without a display-name that looks like
// a C# property will crash trying to create the toolips.
builder.SetTypeName(entry.TypeName);
builder.TagMatchingRule(rule =>
{
rule.TagName = entry.Element;
if (entry.TypeAttribute != null)
{
rule.Attribute(a =>
{
a.Name = "type";
a.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.FullMatch;
a.Value = entry.TypeAttribute;
a.ValueComparisonMode = RequiredAttributeDescriptor.ValueComparisonMode.FullMatch;
});
}
rule.Attribute(a =>
{
a.Name = attributeName;
a.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.FullMatch;
});
});
builder.BindAttribute(a =>
{
a.Documentation = string.Format(
ComponentResources.BindTagHelper_Element_Documentation,
entry.ValueAttribute,
entry.ChangeAttribute);
a.Name = attributeName;
a.TypeName = typeof(object).FullName;
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
// a C# property will crash trying to create the toolips.
a.SetPropertyName(name);
});
builder.BindAttribute(attribute =>
{
attribute.Documentation = string.Format(ComponentResources.BindTagHelper_Element_Format_Documentation, attributeName);
attribute.Name = formatAttributeName;
attribute.TypeName = "System.String";
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
// a C# property will crash trying to create the toolips.
attribute.SetPropertyName(formatName);
});
results.Add(builder.Build());
}
return results;
}
private List<TagHelperDescriptor> CreateComponentBindTagHelpers(ICollection<TagHelperDescriptor> tagHelpers)
{
var results = new List<TagHelperDescriptor>();
foreach (var tagHelper in tagHelpers)
{
if (!tagHelper.IsComponentTagHelper())
{
continue;
}
// We want to create a 'bind' tag helper everywhere we see a pair of properties like `Foo`, `FooChanged`
// where `FooChanged` is a delegate and `Foo` is not.
//
// The easiest way to figure this out without a lot of backtracking is to look for `FooChanged` and then
// try to find a matching "Foo".
for (var i = 0; i < tagHelper.BoundAttributes.Count; i++)
{
var changeAttribute = tagHelper.BoundAttributes[i];
if (!changeAttribute.Name.EndsWith("Changed") || !changeAttribute.IsDelegateProperty())
{
continue;
}
BoundAttributeDescriptor valueAttribute = null;
var valueAttributeName = changeAttribute.Name.Substring(0, changeAttribute.Name.Length - "Changed".Length);
for (var j = 0; j < tagHelper.BoundAttributes.Count; j++)
{
if (tagHelper.BoundAttributes[j].Name == valueAttributeName && !tagHelper.BoundAttributes[j].IsDelegateProperty())
{
valueAttribute = tagHelper.BoundAttributes[j];
break;
}
}
if (valueAttribute == null)
{
// No matching attribute found.
continue;
}
var builder = TagHelperDescriptorBuilder.Create(BlazorMetadata.Bind.TagHelperKind, tagHelper.Name, tagHelper.AssemblyName);
builder.DisplayName = tagHelper.DisplayName;
builder.Documentation = string.Format(
ComponentResources.BindTagHelper_Component_Documentation,
valueAttribute.Name,
changeAttribute.Name);
builder.Metadata.Add(BlazorMetadata.SpecialKindKey, BlazorMetadata.Bind.TagHelperKind);
builder.Metadata[TagHelperMetadata.Runtime.Name] = BlazorMetadata.Bind.RuntimeName;
builder.Metadata[BlazorMetadata.Bind.ValueAttribute] = valueAttribute.Name;
builder.Metadata[BlazorMetadata.Bind.ChangeAttribute] = changeAttribute.Name;
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
// a C# property will crash trying to create the toolips.
builder.SetTypeName(tagHelper.GetTypeName());
// Match the component and attribute name
builder.TagMatchingRule(rule =>
{
rule.TagName = tagHelper.TagMatchingRules.Single().TagName;
rule.Attribute(attribute =>
{
attribute.Name = "bind-" + valueAttribute.Name;
attribute.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.FullMatch;
});
});
builder.BindAttribute(attribute =>
{
attribute.Documentation = string.Format(
ComponentResources.BindTagHelper_Component_Documentation,
valueAttribute.Name,
changeAttribute.Name);
attribute.Name = "bind-" + valueAttribute.Name;
attribute.TypeName = valueAttribute.TypeName;
attribute.IsEnum = valueAttribute.IsEnum;
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
// a C# property will crash trying to create the toolips.
attribute.SetPropertyName(valueAttribute.GetPropertyName());
});
results.Add(builder.Build());
}
}
return results;
}
private struct ElementBindData
{
public ElementBindData(
string assembly,
string typeName,
string element,
string typeAttribute,
string suffix,
string valueAttribute,
string changeAttribute)
{
Assembly = assembly;
TypeName = typeName;
Element = element;
TypeAttribute = typeAttribute;
Suffix = suffix;
ValueAttribute = valueAttribute;
ChangeAttribute = changeAttribute;
}
public string Assembly { get; }
public string TypeName { get; }
public string Element { get; }
public string TypeAttribute { get; }
public string Suffix { get; }
public string ValueAttribute { get; }
public string ChangeAttribute { get; }
}
private class BindElementDataVisitor : SymbolVisitor
{
private List<INamedTypeSymbol> _results;
public BindElementDataVisitor(List<INamedTypeSymbol> results)
{
_results = results;
}
public override void VisitNamedType(INamedTypeSymbol symbol)
{
if (symbol.Name == "BindAttributes" && symbol.DeclaredAccessibility == Accessibility.Public)
{
_results.Add(symbol);
}
}
public override void VisitNamespace(INamespaceSymbol symbol)
{
foreach (var member in symbol.GetMembers())
{
Visit(member);
}
}
public override void VisitAssembly(IAssemblySymbol symbol)
{
// This as a simple yet high-value optimization that excludes the vast majority of
// assemblies that (by definition) can't contain a component.
if (symbol.Name != null && !symbol.Name.StartsWith("System.", StringComparison.Ordinal))
{
Visit(symbol.GlobalNamespace);
}
}
}
}
}

View File

@ -1,14 +1,29 @@
// 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;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Components;
using Microsoft.CodeAnalysis.CSharp;
namespace Microsoft.CodeAnalysis.Razor
{
public class ComponentTagHelperDescriptorProvider : RazorEngineFeatureBase, ITagHelperDescriptorProvider
internal class ComponentTagHelperDescriptorProvider : RazorEngineFeatureBase, ITagHelperDescriptorProvider
{
private static readonly SymbolDisplayFormat FullNameTypeDisplayFormat =
SymbolDisplayFormat.FullyQualifiedFormat
.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)
.WithMiscellaneousOptions(SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions & (~SymbolDisplayMiscellaneousOptions.UseSpecialTypes));
private static MethodInfo WithMetadataImportOptionsMethodInfo =
typeof(CSharpCompilationOptions)
.GetMethod("WithMetadataImportOptions", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
public bool IncludeDocumentation { get; set; }
public int Order { get; set; }
public void Execute(TagHelperDescriptorProviderContext context)
@ -25,72 +40,422 @@ namespace Microsoft.CodeAnalysis.Razor
return;
}
var componentSymbol = compilation.GetTypeByMetadataName(TagHelperTypes.IComponent);
if (componentSymbol == null || componentSymbol.TypeKind == TypeKind.Error)
{
// Could not find attributes we care about in the compilation. Nothing to do.
return;
}
// We need to see private members too
compilation = WithMetadataImportOptionsAll(compilation);
var symbols = BlazorSymbols.Create(compilation);
var types = new List<INamedTypeSymbol>();
var visitor = new ComponentTypeVisitor(componentSymbol, types);
// We always visit the global namespace.
visitor.Visit(compilation.Assembly.GlobalNamespace);
var visitor = new ComponentTypeVisitor(symbols, types);
// Visit the primary output of this compilation, as well as all references.
visitor.Visit(compilation.Assembly);
foreach (var reference in compilation.References)
{
// We ignore .netmodules here - there really isn't a case where they are used by user code
// even though the Roslyn APIs all support them.
if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly)
{
if (IsTagHelperAssembly(assembly))
{
visitor.Visit(assembly.GlobalNamespace);
}
visitor.Visit(assembly);
}
}
for (var i = 0; i < types.Count; i++)
{
var type = types[i];
var descriptor = CreateDescriptor(type);
var descriptor = CreateDescriptor(symbols, type);
context.Results.Add(descriptor);
if (descriptor != null)
foreach (var childContent in descriptor.GetChildContentProperties())
{
context.Results.Add(descriptor);
// Synthesize a separate tag helper for each child content property that's declared.
context.Results.Add(CreateChildContentDescriptor(symbols, descriptor, childContent));
}
}
}
private static TagHelperDescriptor CreateDescriptor(INamedTypeSymbol type)
private Compilation WithMetadataImportOptionsAll(Compilation compilation)
{
var typeName = type.ToDisplayString(DefaultTagHelperDescriptorFactory.FullNameTypeDisplayFormat);
var newCompilationOptions = (CSharpCompilationOptions)WithMetadataImportOptionsMethodInfo
.Invoke(compilation.Options, new object[] { /* All */ (byte)2 });
return compilation.WithOptions(newCompilationOptions);
}
private TagHelperDescriptor CreateDescriptor(BlazorSymbols symbols, INamedTypeSymbol type)
{
var typeName = type.ToDisplayString(FullNameTypeDisplayFormat);
var assemblyName = type.ContainingAssembly.Identity.Name;
var descriptorBuilder = TagHelperDescriptorBuilder.Create(TagHelperConventions.ComponentKind, typeName, assemblyName);
descriptorBuilder.SetTypeName(typeName);
descriptorBuilder.Metadata[TagHelperMetadata.Runtime.Name] = TagHelperConventions.ComponentKind;
var builder = TagHelperDescriptorBuilder.Create(BlazorMetadata.Component.TagHelperKind, typeName, assemblyName);
builder.SetTypeName(typeName);
// This opts out this 'component' tag helper for any processing that's specific to the default
// Razor ITagHelper runtime.
builder.Metadata[TagHelperMetadata.Runtime.Name] = BlazorMetadata.Component.RuntimeName;
if (type.IsGenericType)
{
builder.Metadata[BlazorMetadata.Component.GenericTypedKey] = bool.TrueString;
for (var i = 0; i < type.TypeArguments.Length; i++)
{
var typeParameter = type.TypeArguments[i] as ITypeParameterSymbol;
if (typeParameter != null)
{
CreateTypeParameterProperty(builder, typeParameter);
}
}
}
var xml = type.GetDocumentationCommentXml();
if (!string.IsNullOrEmpty(xml))
{
builder.Documentation = xml;
}
// Components have very simple matching rules. The type name (short) matches the tag name.
descriptorBuilder.TagMatchingRule(r => r.TagName = type.Name);
builder.TagMatchingRule(r => r.TagName = type.Name);
var descriptor = descriptorBuilder.Build();
foreach (var property in GetProperties(symbols, type))
{
if (property.kind == PropertyKind.Ignored)
{
continue;
}
CreateProperty(builder, property.property, property.kind);
}
if (builder.BoundAttributes.Any(a => a.IsParameterizedChildContentProperty()) &&
!builder.BoundAttributes.Any(a => string.Equals(a.Name, BlazorMetadata.ChildContent.ParameterAttributeName, StringComparison.OrdinalIgnoreCase)))
{
// If we have any parameterized child content parameters, synthesize a 'Context' parameter to be
// able to set the variable name (for all child content). If the developer defined a 'Context' parameter
// already, then theirs wins.
CreateContextParameter(builder, childContentName: null);
}
var descriptor = builder.Build();
return descriptor;
}
private bool IsTagHelperAssembly(IAssemblySymbol assembly)
private void CreateProperty(TagHelperDescriptorBuilder builder, IPropertySymbol property, PropertyKind kind)
{
return assembly.Name != null && !assembly.Name.StartsWith("System.", StringComparison.Ordinal);
builder.BindAttribute(pb =>
{
pb.Name = property.Name;
pb.TypeName = property.Type.ToDisplayString(FullNameTypeDisplayFormat);
pb.SetPropertyName(property.Name);
if (kind == PropertyKind.Enum)
{
pb.IsEnum = true;
}
if (kind == PropertyKind.ChildContent)
{
pb.Metadata.Add(BlazorMetadata.Component.ChildContentKey, bool.TrueString);
}
if (kind == PropertyKind.Delegate)
{
pb.Metadata.Add(BlazorMetadata.Component.DelegateSignatureKey, bool.TrueString);
}
if (HasTypeParameter(property.Type))
{
pb.Metadata.Add(BlazorMetadata.Component.GenericTypedKey, bool.TrueString);
}
var xml = property.GetDocumentationCommentXml();
if (!string.IsNullOrEmpty(xml))
{
pb.Documentation = xml;
}
});
bool HasTypeParameter(ITypeSymbol type)
{
if (type is ITypeParameterSymbol)
{
return true;
}
// We need to check for cases like:
// [Parameter] List<T> MyProperty { get; set; }
// AND
// [Parameter] List<string> MyProperty { get; set; }
//
// We need to inspect the type arguments to tell the difference between a property that
// uses the containing class' type parameter(s) and a vanilla usage of generic types like
// List<> and Dictionary<,>
//
// Since we need to handle cases like RenderFragment<List<T>>, this check must be recursive.
if (type is INamedTypeSymbol namedType && namedType.IsGenericType)
{
var typeArguments = namedType.TypeArguments;
for (var i = 0; i < typeArguments.Length; i++)
{
if (HasTypeParameter(typeArguments[i]))
{
return true;
}
}
// Another case to handle - if the type being inspected is a nested type
// inside a generic containing class. The common usage for this would be a case
// where a generic templated component defines a 'context' nested class.
if (namedType.ContainingType != null && HasTypeParameter(namedType.ContainingType))
{
return true;
}
}
return false;
}
}
// Visits top-level types and finds interface implementations.
internal class ComponentTypeVisitor : SymbolVisitor
private void CreateTypeParameterProperty(TagHelperDescriptorBuilder builder, ITypeSymbol typeParameter)
{
private readonly INamedTypeSymbol _componentSymbol;
builder.BindAttribute(pb =>
{
pb.DisplayName = typeParameter.Name;
pb.Name = typeParameter.Name;
pb.TypeName = typeof(Type).FullName;
pb.SetPropertyName(typeParameter.Name);
pb.Metadata[BlazorMetadata.Component.TypeParameterKey] = bool.TrueString;
pb.Documentation = string.Format(ComponentResources.ComponentTypeParameter_Documentation, typeParameter.Name, builder.Name);
});
}
private TagHelperDescriptor CreateChildContentDescriptor(BlazorSymbols symbols, TagHelperDescriptor component, BoundAttributeDescriptor attribute)
{
var typeName = component.GetTypeName() + "." + attribute.Name;
var assemblyName = component.AssemblyName;
var builder = TagHelperDescriptorBuilder.Create(BlazorMetadata.ChildContent.TagHelperKind, typeName, assemblyName);
builder.SetTypeName(typeName);
// This opts out this 'component' tag helper for any processing that's specific to the default
// Razor ITagHelper runtime.
builder.Metadata[TagHelperMetadata.Runtime.Name] = BlazorMetadata.ChildContent.RuntimeName;
// Opt out of processing as a component. We'll process this specially as part of the component's body.
builder.Metadata[BlazorMetadata.SpecialKindKey] = BlazorMetadata.ChildContent.TagHelperKind;
var xml = attribute.Documentation;
if (!string.IsNullOrEmpty(xml))
{
builder.Documentation = xml;
}
// Child content matches the property name, but only as a direct child of the component.
builder.TagMatchingRule(r =>
{
r.TagName = attribute.Name;
r.ParentTag = component.TagMatchingRules.First().TagName;
});
if (attribute.IsParameterizedChildContentProperty())
{
// For child content attributes with a parameter, synthesize an attribute that allows you to name
// the parameter.
CreateContextParameter(builder, attribute.Name);
}
var descriptor = builder.Build();
return descriptor;
}
private void CreateContextParameter(TagHelperDescriptorBuilder builder, string childContentName)
{
builder.BindAttribute(b =>
{
b.Name = BlazorMetadata.ChildContent.ParameterAttributeName;
b.TypeName = typeof(string).FullName;
b.Metadata.Add(BlazorMetadata.Component.ChildContentParameterNameKey, bool.TrueString);
if (childContentName == null)
{
b.Documentation = ComponentResources.ChildContentParameterName_TopLevelDocumentation;
}
else
{
b.Documentation = string.Format(ComponentResources.ChildContentParameterName_Documentation, childContentName);
}
});
}
// Does a walk up the inheritance chain to determine the set of parameters by using
// a dictionary keyed on property name.
//
// We consider parameters to be defined by properties satisfying all of the following:
// - are visible (not shadowed)
// - have the [Parameter] attribute
// - have a setter, even if private
// - are not indexers
private IEnumerable<(IPropertySymbol property, PropertyKind kind)> GetProperties(BlazorSymbols symbols, INamedTypeSymbol type)
{
var properties = new Dictionary<string, (IPropertySymbol, PropertyKind)>(StringComparer.Ordinal);
do
{
if (type == symbols.ComponentBase)
{
// The ComponentBase base class doesn't have any [Parameter].
// Bail out now to avoid walking through its many members, plus the members
// of the System.Object base class.
break;
}
var members = type.GetMembers();
for (var i = 0; i < members.Length; i++)
{
var property = members[i] as IPropertySymbol;
if (property == null)
{
// Not a property
continue;
}
if (properties.ContainsKey(property.Name))
{
// Not visible
continue;
}
var kind = PropertyKind.Default;
if (property.Parameters.Length != 0)
{
// Indexer
kind = PropertyKind.Ignored;
}
if (property.SetMethod == null)
{
// No setter
kind = PropertyKind.Ignored;
}
if (property.IsStatic)
{
kind = PropertyKind.Ignored;
}
if (!property.GetAttributes().Any(a => a.AttributeClass == symbols.ParameterAttribute))
{
// Does not have [Parameter]
kind = PropertyKind.Ignored;
}
if (kind == PropertyKind.Default && property.Type.TypeKind == TypeKind.Enum)
{
kind = PropertyKind.Enum;
}
if (kind == PropertyKind.Default && property.Type == symbols.RenderFragment)
{
kind = PropertyKind.ChildContent;
}
if (kind == PropertyKind.Default &&
property.Type is INamedTypeSymbol namedType &&
namedType.IsGenericType &&
namedType.ConstructedFrom == symbols.RenderFragmentOfT)
{
kind = PropertyKind.ChildContent;
}
if (kind == PropertyKind.Default && property.Type.TypeKind == TypeKind.Delegate)
{
kind = PropertyKind.Delegate;
}
properties.Add(property.Name, (property, kind));
}
type = type.BaseType;
}
while (type != null);
return properties.Values;
}
private enum PropertyKind
{
Ignored,
Default,
Enum,
ChildContent,
Delegate,
}
private class BlazorSymbols
{
public static BlazorSymbols Create(Compilation compilation)
{
var symbols = new BlazorSymbols();
symbols.ComponentBase = compilation.GetTypeByMetadataName(ComponentsApi.ComponentBase.MetadataName);
if (symbols.ComponentBase == null)
{
// No definition for ComponentBase, nothing to do.
return null;
}
symbols.IComponent = compilation.GetTypeByMetadataName(ComponentsApi.IComponent.MetadataName);
if (symbols.IComponent == null)
{
// No definition for IComponent, nothing to do.
return null;
}
symbols.ParameterAttribute = compilation.GetTypeByMetadataName(ComponentsApi.ParameterAttribute.MetadataName);
if (symbols.ParameterAttribute == null)
{
// No definition for [Parameter], nothing to do.
return null;
}
symbols.RenderFragment = compilation.GetTypeByMetadataName(ComponentsApi.RenderFragment.MetadataName);
if (symbols.RenderFragment == null)
{
// No definition for RenderFragment, nothing to do.
}
symbols.RenderFragmentOfT = compilation.GetTypeByMetadataName(ComponentsApi.RenderFragmentOfT.MetadataName);
if (symbols.RenderFragmentOfT == null)
{
// No definition for RenderFragment, nothing to do.
}
return symbols;
}
private BlazorSymbols()
{
}
public INamedTypeSymbol ComponentBase { get; private set; }
public INamedTypeSymbol IComponent { get; private set; }
public INamedTypeSymbol ParameterAttribute { get; private set; }
public INamedTypeSymbol RenderFragment { get; private set; }
public INamedTypeSymbol RenderFragmentOfT { get; private set; }
}
private class ComponentTypeVisitor : SymbolVisitor
{
private readonly BlazorSymbols _symbols;
private readonly List<INamedTypeSymbol> _results;
public ComponentTypeVisitor(INamedTypeSymbol componentSymbol, List<INamedTypeSymbol> results)
public ComponentTypeVisitor(BlazorSymbols symbols, List<INamedTypeSymbol> results)
{
_componentSymbol = componentSymbol;
_symbols = symbols;
_results = results;
}
@ -122,11 +487,16 @@ namespace Microsoft.CodeAnalysis.Razor
internal bool IsComponent(INamedTypeSymbol symbol)
{
if (_symbols == null)
{
return false;
}
return
symbol.DeclaredAccessibility == Accessibility.Public &&
!symbol.IsAbstract &&
symbol.AllInterfaces.Contains(_componentSymbol);
symbol.AllInterfaces.Contains(_symbols.IComponent);
}
}
}
}
}

View File

@ -0,0 +1,56 @@
// 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;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Microsoft.CodeAnalysis.Razor
{
internal class DefaultTypeNameFeature : TypeNameFeature
{
public override IReadOnlyList<string> ParseTypeParameters(string typeName)
{
if (typeName == null)
{
throw new ArgumentNullException(nameof(typeName));
}
var parsed = SyntaxFactory.ParseTypeName(typeName);
if (parsed is IdentifierNameSyntax identifier)
{
return Array.Empty<string>();
}
else
{
return parsed.DescendantNodesAndSelf()
.OfType<TypeArgumentListSyntax>()
.SelectMany(arg => arg.Arguments)
.Select(a => a.ToString()).ToList();
}
}
public override TypeNameRewriter CreateGenericTypeRewriter(Dictionary<string, string> bindings)
{
if (bindings == null)
{
throw new ArgumentNullException(nameof(bindings));
}
return new GenericTypeNameRewriter(bindings);
}
public override TypeNameRewriter CreateGlobalQualifiedTypeNameRewriter(ICollection<string> ignore)
{
if (ignore == null)
{
throw new ArgumentNullException(nameof(ignore));
}
return new GlobalQualifiedTypeNameRewriter(ignore);
}
}
}

View File

@ -0,0 +1,218 @@
// 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;
using Microsoft.AspNetCore.Razor.Language.Components;
namespace Microsoft.CodeAnalysis.Razor
{
internal class EventHandlerTagHelperDescriptorProvider : ITagHelperDescriptorProvider
{
public int Order { get; set; }
public RazorEngine Engine { get; set; }
public void Execute(TagHelperDescriptorProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var compilation = context.GetCompilation();
if (compilation == null)
{
return;
}
var bindMethods = compilation.GetTypeByMetadataName(ComponentsApi.BindMethods.FullTypeName);
if (bindMethods == null)
{
// If we can't find BindMethods, then just bail. We won't be able to compile the
// generated code anyway.
return;
}
var eventHandlerData = GetEventHandlerData(compilation);
foreach (var tagHelper in CreateEventHandlerTagHelpers(eventHandlerData))
{
context.Results.Add(tagHelper);
}
}
private List<EventHandlerData> GetEventHandlerData(Compilation compilation)
{
var eventHandlerAttribute = compilation.GetTypeByMetadataName(ComponentsApi.EventHandlerAttribute.FullTypeName);
if (eventHandlerAttribute == null)
{
// This won't likely happen, but just in case.
return new List<EventHandlerData>();
}
var types = new List<INamedTypeSymbol>();
var visitor = new EventHandlerDataVisitor(types);
// Visit the primary output of this compilation, as well as all references.
visitor.Visit(compilation.Assembly);
foreach (var reference in compilation.References)
{
// We ignore .netmodules here - there really isn't a case where they are used by user code
// even though the Roslyn APIs all support them.
if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly)
{
visitor.Visit(assembly);
}
}
var results = new List<EventHandlerData>();
for (var i = 0; i < types.Count; i++)
{
var type = types[i];
var attributes = type.GetAttributes();
// Not handling duplicates here for now since we're the primary ones extending this.
// If we see users adding to the set of event handler constructs we will want to add deduplication
// and potentially diagnostics.
for (var j = 0; j < attributes.Length; j++)
{
var attribute = attributes[j];
if (attribute.AttributeClass == eventHandlerAttribute)
{
results.Add(new EventHandlerData(
type.ContainingAssembly.Name,
type.ToDisplayString(),
(string)attribute.ConstructorArguments[0].Value,
(INamedTypeSymbol)attribute.ConstructorArguments[1].Value));
}
}
}
return results;
}
private List<TagHelperDescriptor> CreateEventHandlerTagHelpers(List<EventHandlerData> data)
{
var results = new List<TagHelperDescriptor>();
for (var i = 0; i < data.Count; i++)
{
var entry = data[i];
var builder = TagHelperDescriptorBuilder.Create(BlazorMetadata.EventHandler.TagHelperKind, entry.Attribute, entry.Assembly);
builder.Documentation = string.Format(
ComponentResources.EventHandlerTagHelper_Documentation,
entry.Attribute,
entry.EventArgsType.ToDisplayString());
builder.Metadata.Add(BlazorMetadata.SpecialKindKey, BlazorMetadata.EventHandler.TagHelperKind);
builder.Metadata.Add(BlazorMetadata.EventHandler.EventArgsType, entry.EventArgsType.ToDisplayString());
builder.Metadata[TagHelperMetadata.Runtime.Name] = BlazorMetadata.EventHandler.RuntimeName;
// WTE has a bug in 15.7p1 where a Tag Helper without a display-name that looks like
// a C# property will crash trying to create the tooltips.
builder.SetTypeName(entry.TypeName);
builder.TagMatchingRule(rule =>
{
rule.TagName = "*";
rule.Attribute(a =>
{
a.Name = entry.Attribute;
a.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.FullMatch;
});
});
builder.BindAttribute(a =>
{
a.Documentation = string.Format(
ComponentResources.EventHandlerTagHelper_Documentation,
entry.Attribute,
entry.EventArgsType.ToDisplayString());
a.Name = entry.Attribute;
// Use a string here so that we get HTML context by default.
a.TypeName = typeof(string).FullName;
// But make this weakly typed (don't type check) - delegates have their own type-checking
// logic that we don't want to interfere with.
a.Metadata.Add(BlazorMetadata.Component.WeaklyTypedKey, bool.TrueString);
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
// a C# property will crash trying to create the tooltips.
a.SetPropertyName(entry.Attribute);
});
results.Add(builder.Build());
}
return results;
}
private struct EventHandlerData
{
public EventHandlerData(
string assembly,
string typeName,
string element,
INamedTypeSymbol eventArgsType)
{
Assembly = assembly;
TypeName = typeName;
Attribute = element;
EventArgsType = eventArgsType;
}
public string Assembly { get; }
public string TypeName { get; }
public string Attribute { get; }
public INamedTypeSymbol EventArgsType { get; }
}
private class EventHandlerDataVisitor : SymbolVisitor
{
private List<INamedTypeSymbol> _results;
public EventHandlerDataVisitor(List<INamedTypeSymbol> results)
{
_results = results;
}
public override void VisitNamedType(INamedTypeSymbol symbol)
{
if (symbol.Name == "EventHandlers" && symbol.DeclaredAccessibility == Accessibility.Public)
{
_results.Add(symbol);
}
}
public override void VisitNamespace(INamespaceSymbol symbol)
{
foreach (var member in symbol.GetMembers())
{
Visit(member);
}
}
public override void VisitAssembly(IAssemblySymbol symbol)
{
// This as a simple yet high-value optimization that excludes the vast majority of
// assemblies that (by definition) can't contain a component.
if (symbol.Name != null && !symbol.Name.StartsWith("System.", StringComparison.Ordinal))
{
Visit(symbol.GlobalNamespace);
}
}
}
}
}

View File

@ -0,0 +1,70 @@
// 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 Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Microsoft.CodeAnalysis.Razor
{
internal class GenericTypeNameRewriter : TypeNameRewriter
{
private readonly Dictionary<string, string> _bindings;
public GenericTypeNameRewriter(Dictionary<string, string> bindings)
{
_bindings = bindings;
}
public override string Rewrite(string typeName)
{
var parsed = SyntaxFactory.ParseTypeName(typeName);
var rewritten = (TypeSyntax)new Visitor(_bindings).Visit(parsed);
return rewritten.ToFullString();
}
private class Visitor : CSharpSyntaxRewriter
{
private readonly Dictionary<string, string> _bindings;
public Visitor(Dictionary<string, string> bindings)
{
_bindings = bindings;
}
public override SyntaxNode Visit(SyntaxNode node)
{
// We can handle a single IdentifierNameSyntax at the top level (like 'TItem)
// OR a GenericNameSyntax recursively (like `List<T>`)
if (node is IdentifierNameSyntax identifier && !(identifier.Parent is QualifiedNameSyntax))
{
if (_bindings.TryGetValue(identifier.Identifier.Text, out var binding))
{
// If we don't have a valid replacement, use object. This will make the code at least reasonable
// compared to leaving the type parameter in place.
//
// We add our own diagnostics for missing/invalid type parameters anyway.
var replacement = binding == null ? typeof(object).FullName : binding;
return identifier.Update(SyntaxFactory.Identifier(replacement));
}
}
return base.Visit(node);
}
public override SyntaxNode VisitGenericName(GenericNameSyntax node)
{
var args = node.TypeArgumentList.Arguments;
for (var i = 0; i < args.Count; i++)
{
var typeArgument = args[i];
args = args.Replace(typeArgument, (TypeSyntax)Visit(typeArgument));
}
return node.WithTypeArgumentList(node.TypeArgumentList.WithArguments(args));
}
}
}
}

View File

@ -0,0 +1,79 @@
// 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 Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Microsoft.CodeAnalysis.Razor
{
// Rewrites type names to use the 'global::' prefix for identifiers.
//
// This is useful when we're generating code in a different namespace than
// what the user code lives in. When we synthesize a namespace it's easy to have
// clashes.
internal class GlobalQualifiedTypeNameRewriter : TypeNameRewriter
{
// List of names to ignore.
//
// NOTE: this is the list of type parameters defined on the component.
private readonly HashSet<string> _ignore;
public GlobalQualifiedTypeNameRewriter(ICollection<string> ignore)
{
_ignore = new HashSet<string>(ignore);
}
public override string Rewrite(string typeName)
{
var parsed = SyntaxFactory.ParseTypeName(typeName);
var rewritten = (TypeSyntax)new Visitor(_ignore).Visit(parsed);
return rewritten.ToFullString();
}
private class Visitor : CSharpSyntaxRewriter
{
private readonly HashSet<string> _ignore;
public Visitor(HashSet<string> ignore)
{
_ignore = ignore;
}
public override SyntaxNode Visit(SyntaxNode node)
{
return base.Visit(node);
}
public override SyntaxNode VisitQualifiedName(QualifiedNameSyntax node)
{
if (node.Parent is QualifiedNameSyntax)
{
return base.VisitQualifiedName(node);
}
// Need to rewrite postorder so we can rewrite the names of generic type arguments.
node = (QualifiedNameSyntax)base.VisitQualifiedName(node);
// Rewriting these is complicated, best to just tostring and parse again.
return SyntaxFactory.ParseTypeName("global::" + node.ToString());
}
public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node)
{
if (_ignore.Contains(node.ToString()))
{
return node;
}
if (node.Parent != null)
{
return node;
}
return SyntaxFactory.AliasQualifiedName(SyntaxFactory.IdentifierName(SyntaxFactory.Token(CSharp.SyntaxKind.GlobalKeyword)), node);
}
}
}
}

View File

@ -3,6 +3,7 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("rzls, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("rzc, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Razor.LanguageServer.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.VisualStudio.LiveShare.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.VisualStudio.LiveShare.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

View File

@ -0,0 +1,62 @@
// 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.Components;
namespace Microsoft.CodeAnalysis.Razor
{
internal class RefTagHelperDescriptorProvider : ITagHelperDescriptorProvider
{
// Run after the component tag helper provider, because later we may want component-type-specific variants of this
public int Order { get; set; } = 1000;
public RazorEngine Engine { get; set; }
public void Execute(TagHelperDescriptorProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
context.Results.Add(CreateRefTagHelper());
}
private TagHelperDescriptor CreateRefTagHelper()
{
var builder = TagHelperDescriptorBuilder.Create(BlazorMetadata.Ref.TagHelperKind, "Ref", ComponentsApi.AssemblyName);
builder.Documentation = ComponentResources.RefTagHelper_Documentation;
builder.Metadata.Add(BlazorMetadata.SpecialKindKey, BlazorMetadata.Ref.TagHelperKind);
builder.Metadata[TagHelperMetadata.Runtime.Name] = BlazorMetadata.Ref.RuntimeName;
// WTE has a bug in 15.7p1 where a Tag Helper without a display-name that looks like
// a C# property will crash trying to create the tooltips.
builder.SetTypeName("Microsoft.AspNetCore.Components.Ref");
builder.TagMatchingRule(rule =>
{
rule.TagName = "*";
rule.Attribute(attribute =>
{
attribute.Name = "ref";
});
});
builder.BindAttribute(attribute =>
{
attribute.Documentation = ComponentResources.RefTagHelper_Documentation;
attribute.Name = "ref";
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
// a C# property will crash trying to create the tooltips.
attribute.SetPropertyName("Ref");
attribute.TypeName = typeof(object).FullName;
});
return builder.Build();
}
}
}

View File

@ -0,0 +1,41 @@
// 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.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.Extensions.DependencyModel;
namespace Microsoft.CodeAnalysis.Razor
{
public abstract class BaseTagHelperDescriptorProviderTest
{
static BaseTagHelperDescriptorProviderTest()
{
BaseCompilation = TestCompilation.Create(typeof(ComponentTagHelperDescriptorProviderTest).Assembly);
CSharpParseOptions = new CSharpParseOptions(LanguageVersion.CSharp7_3);
}
protected static Compilation BaseCompilation { get; }
protected static CSharpParseOptions CSharpParseOptions { get; }
protected static CSharpSyntaxTree Parse(string text)
{
return (CSharpSyntaxTree)CSharpSyntaxTree.ParseText(text, CSharpParseOptions);
}
// For simplicity in testing, exclude the built-in components. We'll add more and we
// don't want to update the tests when that happens.
protected static TagHelperDescriptor[] ExcludeBuiltInComponents(TagHelperDescriptorProviderContext context)
{
return context.Results
.Where(c => c.AssemblyName == "TestAssembly")
.OrderBy(c => c.Name)
.ToArray();
}
}
}

View File

@ -0,0 +1,683 @@
// 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.Components;
using Xunit;
namespace Microsoft.CodeAnalysis.Razor
{
public class BindTagHelperDescriptorProviderTest : BaseTagHelperDescriptorProviderTest
{
[Fact]
public void Execute_FindsBindTagHelperOnComponentType_CreatesDescriptor()
{
// Arrange
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
using System;
using Microsoft.AspNetCore.Components;
namespace Test
{
public class MyComponent : IComponent
{
public void Init(RenderHandle renderHandle) { }
public void SetParameters(ParameterCollection parameters) { }
[Parameter]
string MyProperty { get; set; }
[Parameter]
Action<string> MyPropertyChanged { get; set; }
}
}
"));
Assert.Empty(compilation.GetDiagnostics());
var context = TagHelperDescriptorProviderContext.Create();
context.SetCompilation(compilation);
// We run after component discovery and depend on the results.
var componentProvider = new ComponentTagHelperDescriptorProvider();
componentProvider.Execute(context);
var provider = new BindTagHelperDescriptorProvider();
// Act
provider.Execute(context);
// Assert
var matches = GetBindTagHelpers(context);
var bind = Assert.Single(matches);
// These are features Bind Tags Helpers don't use. Verifying them once here and
// then ignoring them.
Assert.Empty(bind.AllowedChildTags);
Assert.Null(bind.TagOutputHint);
// These are features that are invariants of all Bind Tag Helpers. Verifying them once
// here and then ignoring them.
Assert.Empty(bind.Diagnostics);
Assert.False(bind.HasErrors);
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, bind.Kind);
Assert.Equal(BlazorMetadata.Bind.RuntimeName, bind.Metadata[TagHelperMetadata.Runtime.Name]);
Assert.False(bind.IsDefaultKind());
Assert.False(bind.KindUsesDefaultTagHelperRuntime());
Assert.Equal("MyProperty", bind.Metadata[BlazorMetadata.Bind.ValueAttribute]);
Assert.Equal("MyPropertyChanged", bind.Metadata[BlazorMetadata.Bind.ChangeAttribute]);
Assert.Equal(
"Binds the provided expression to the 'MyProperty' property and a change event " +
"delegate to the 'MyPropertyChanged' property of the component.",
bind.Documentation);
// These are all trivially derived from the assembly/namespace/type name
Assert.Equal("TestAssembly", bind.AssemblyName);
Assert.Equal("Test.MyComponent", bind.Name);
Assert.Equal("Test.MyComponent", bind.DisplayName);
Assert.Equal("Test.MyComponent", bind.GetTypeName());
var rule = Assert.Single(bind.TagMatchingRules);
Assert.Empty(rule.Diagnostics);
Assert.False(rule.HasErrors);
Assert.Null(rule.ParentTag);
Assert.Equal("MyComponent", rule.TagName);
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
var requiredAttribute = Assert.Single(rule.Attributes);
Assert.Empty(requiredAttribute.Diagnostics);
Assert.Equal("bind-MyProperty", requiredAttribute.DisplayName);
Assert.Equal("bind-MyProperty", requiredAttribute.Name);
Assert.Equal(RequiredAttributeDescriptor.NameComparisonMode.FullMatch, requiredAttribute.NameComparison);
Assert.Null(requiredAttribute.Value);
Assert.Equal(RequiredAttributeDescriptor.ValueComparisonMode.None, requiredAttribute.ValueComparison);
var attribute = Assert.Single(bind.BoundAttributes);
// Invariants
Assert.Empty(attribute.Diagnostics);
Assert.False(attribute.HasErrors);
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, attribute.Kind);
Assert.False(attribute.IsDefaultKind());
Assert.False(attribute.HasIndexer);
Assert.Null(attribute.IndexerNamePrefix);
Assert.Null(attribute.IndexerTypeName);
Assert.False(attribute.IsIndexerBooleanProperty);
Assert.False(attribute.IsIndexerStringProperty);
Assert.Equal(
"Binds the provided expression to the 'MyProperty' property and a change event " +
"delegate to the 'MyPropertyChanged' property of the component.",
attribute.Documentation);
Assert.Equal("bind-MyProperty", attribute.Name);
Assert.Equal("MyProperty", attribute.GetPropertyName());
Assert.Equal("string Test.MyComponent.MyProperty", attribute.DisplayName);
// Defined from the property type
Assert.Equal("System.String", attribute.TypeName);
Assert.True(attribute.IsStringProperty);
Assert.False(attribute.IsBooleanProperty);
Assert.False(attribute.IsEnum);
}
[Fact]
public void Execute_NoMatchedPropertiesOnComponent_IgnoresComponent()
{
// Arrange
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
using System;
using Microsoft.AspNetCore.Components;
namespace Test
{
public class MyComponent : IComponent
{
public void Init(RenderHandle renderHandle) { }
public void SetParameters(ParameterCollection parameters) { }
public string MyProperty { get; set; }
public Action<string> MyPropertyChangedNotMatch { get; set; }
}
}
"));
Assert.Empty(compilation.GetDiagnostics());
var context = TagHelperDescriptorProviderContext.Create();
context.SetCompilation(compilation);
// We run after component discovery and depend on the results.
var componentProvider = new ComponentTagHelperDescriptorProvider();
componentProvider.Execute(context);
var provider = new BindTagHelperDescriptorProvider();
// Act
provider.Execute(context);
// Assert
var matches = GetBindTagHelpers(context);
Assert.Empty(matches);
}
[Fact]
public void Execute_BindOnElement_CreatesDescriptor()
{
// Arrange
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
using Microsoft.AspNetCore.Components;
namespace Test
{
[BindElement(""div"", null, ""myprop"", ""myevent"")]
public class BindAttributes
{
}
}
"));
Assert.Empty(compilation.GetDiagnostics());
var context = TagHelperDescriptorProviderContext.Create();
context.SetCompilation(compilation);
var provider = new BindTagHelperDescriptorProvider();
// Act
provider.Execute(context);
// Assert
var matches = GetBindTagHelpers(context);
var bind = Assert.Single(matches);
// These are features Bind Tags Helpers don't use. Verifying them once here and
// then ignoring them.
Assert.Empty(bind.AllowedChildTags);
Assert.Null(bind.TagOutputHint);
// These are features that are invariants of all Bind Tag Helpers. Verifying them once
// here and then ignoring them.
Assert.Empty(bind.Diagnostics);
Assert.False(bind.HasErrors);
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, bind.Kind);
Assert.Equal(BlazorMetadata.Bind.RuntimeName, bind.Metadata[TagHelperMetadata.Runtime.Name]);
Assert.False(bind.IsDefaultKind());
Assert.False(bind.KindUsesDefaultTagHelperRuntime());
Assert.Equal("myprop", bind.Metadata[BlazorMetadata.Bind.ValueAttribute]);
Assert.Equal("myevent", bind.Metadata[BlazorMetadata.Bind.ChangeAttribute]);
Assert.False(bind.IsInputElementBindTagHelper());
Assert.False(bind.IsInputElementFallbackBindTagHelper());
Assert.Equal(
"Binds the provided expression to the 'myprop' attribute and a change event " +
"delegate to the 'myevent' attribute.",
bind.Documentation);
// These are all trivially derived from the assembly/namespace/type name
Assert.Equal("TestAssembly", bind.AssemblyName);
Assert.Equal("Bind", bind.Name);
Assert.Equal("Test.BindAttributes", bind.DisplayName);
Assert.Equal("Test.BindAttributes", bind.GetTypeName());
// The tag matching rule for a bind-Component is always the component name + the attribute name
var rule = Assert.Single(bind.TagMatchingRules);
Assert.Empty(rule.Diagnostics);
Assert.False(rule.HasErrors);
Assert.Null(rule.ParentTag);
Assert.Equal("div", rule.TagName);
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
var requiredAttribute = Assert.Single(rule.Attributes);
Assert.Empty(requiredAttribute.Diagnostics);
Assert.Equal("bind", requiredAttribute.DisplayName);
Assert.Equal("bind", requiredAttribute.Name);
Assert.Equal(RequiredAttributeDescriptor.NameComparisonMode.FullMatch, requiredAttribute.NameComparison);
Assert.Null(requiredAttribute.Value);
Assert.Equal(RequiredAttributeDescriptor.ValueComparisonMode.None, requiredAttribute.ValueComparison);
var attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("bind"));
// Invariants
Assert.Empty(attribute.Diagnostics);
Assert.False(attribute.HasErrors);
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, attribute.Kind);
Assert.False(attribute.IsDefaultKind());
Assert.False(attribute.HasIndexer);
Assert.Null(attribute.IndexerNamePrefix);
Assert.Null(attribute.IndexerTypeName);
Assert.False(attribute.IsIndexerBooleanProperty);
Assert.False(attribute.IsIndexerStringProperty);
Assert.Equal(
"Binds the provided expression to the 'myprop' attribute and a change event " +
"delegate to the 'myevent' attribute.",
attribute.Documentation);
Assert.Equal("bind", attribute.Name);
Assert.Equal("Bind", attribute.GetPropertyName());
Assert.Equal("object Test.BindAttributes.Bind", attribute.DisplayName);
// Defined from the property type
Assert.Equal("System.Object", attribute.TypeName);
Assert.False(attribute.IsStringProperty);
Assert.False(attribute.IsBooleanProperty);
Assert.False(attribute.IsEnum);
attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("format"));
// Invariants
Assert.Empty(attribute.Diagnostics);
Assert.False(attribute.HasErrors);
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, attribute.Kind);
Assert.False(attribute.IsDefaultKind());
Assert.False(attribute.HasIndexer);
Assert.Null(attribute.IndexerNamePrefix);
Assert.Null(attribute.IndexerTypeName);
Assert.False(attribute.IsIndexerBooleanProperty);
Assert.False(attribute.IsIndexerStringProperty);
Assert.Equal(
"Specifies a format to convert the value specified by the 'bind' attribute. " +
"The format string can currently only be used with expressions of type <code>DateTime</code>.",
attribute.Documentation);
Assert.Equal("format-myprop", attribute.Name);
Assert.Equal("Format_myprop", attribute.GetPropertyName());
Assert.Equal("string Test.BindAttributes.Format_myprop", attribute.DisplayName);
// Defined from the property type
Assert.Equal("System.String", attribute.TypeName);
Assert.True(attribute.IsStringProperty);
Assert.False(attribute.IsBooleanProperty);
Assert.False(attribute.IsEnum);
}
[Fact]
public void Execute_BindOnElementWithSuffix_CreatesDescriptor()
{
// Arrange
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
using Microsoft.AspNetCore.Components;
namespace Test
{
[BindElement(""div"", ""myprop"", ""myprop"", ""myevent"")]
public class BindAttributes
{
}
}
"));
Assert.Empty(compilation.GetDiagnostics());
var context = TagHelperDescriptorProviderContext.Create();
context.SetCompilation(compilation);
var provider = new BindTagHelperDescriptorProvider();
// Act
provider.Execute(context);
// Assert
var matches = GetBindTagHelpers(context);
var bind = Assert.Single(matches);
Assert.Equal("myprop", bind.Metadata[BlazorMetadata.Bind.ValueAttribute]);
Assert.Equal("myevent", bind.Metadata[BlazorMetadata.Bind.ChangeAttribute]);
Assert.False(bind.IsInputElementBindTagHelper());
Assert.False(bind.IsInputElementFallbackBindTagHelper());
var rule = Assert.Single(bind.TagMatchingRules);
Assert.Equal("div", rule.TagName);
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
var requiredAttribute = Assert.Single(rule.Attributes);
Assert.Equal("bind-myprop", requiredAttribute.DisplayName);
Assert.Equal("bind-myprop", requiredAttribute.Name);
var attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("bind"));
Assert.Equal("bind-myprop", attribute.Name);
Assert.Equal("Bind_myprop", attribute.GetPropertyName());
Assert.Equal("object Test.BindAttributes.Bind_myprop", attribute.DisplayName);
attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("format"));
Assert.Equal("format-myprop", attribute.Name);
Assert.Equal("Format_myprop", attribute.GetPropertyName());
Assert.Equal("string Test.BindAttributes.Format_myprop", attribute.DisplayName);
}
[Fact]
public void Execute_BindOnInputElementWithoutTypeAttribute_CreatesDescriptor()
{
// Arrange
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
using Microsoft.AspNetCore.Components;
namespace Test
{
[BindInputElement(null, null, ""myprop"", ""myevent"")]
public class BindAttributes
{
}
}
"));
Assert.Empty(compilation.GetDiagnostics());
var context = TagHelperDescriptorProviderContext.Create();
context.SetCompilation(compilation);
var provider = new BindTagHelperDescriptorProvider();
// Act
provider.Execute(context);
// Assert
var matches = GetBindTagHelpers(context);
var bind = Assert.Single(matches);
Assert.Equal("myprop", bind.Metadata[BlazorMetadata.Bind.ValueAttribute]);
Assert.Equal("myevent", bind.Metadata[BlazorMetadata.Bind.ChangeAttribute]);
Assert.False(bind.Metadata.ContainsKey(BlazorMetadata.Bind.TypeAttribute));
Assert.True(bind.IsInputElementBindTagHelper());
Assert.True(bind.IsInputElementFallbackBindTagHelper());
var rule = Assert.Single(bind.TagMatchingRules);
Assert.Equal("input", rule.TagName);
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
var requiredAttribute = Assert.Single(rule.Attributes);
Assert.Equal("bind", requiredAttribute.DisplayName);
Assert.Equal("bind", requiredAttribute.Name);
var attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("bind"));
Assert.Equal("bind", attribute.Name);
Assert.Equal("Bind", attribute.GetPropertyName());
Assert.Equal("object Test.BindAttributes.Bind", attribute.DisplayName);
attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("format"));
Assert.Equal("format-myprop", attribute.Name);
Assert.Equal("Format_myprop", attribute.GetPropertyName());
Assert.Equal("string Test.BindAttributes.Format_myprop", attribute.DisplayName);
}
[Fact]
public void Execute_BindOnInputElementWithTypeAttribute_CreatesDescriptor()
{
// Arrange
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
using Microsoft.AspNetCore.Components;
namespace Test
{
[BindInputElement(""checkbox"", null, ""myprop"", ""myevent"")]
public class BindAttributes
{
}
}
"));
Assert.Empty(compilation.GetDiagnostics());
var context = TagHelperDescriptorProviderContext.Create();
context.SetCompilation(compilation);
var provider = new BindTagHelperDescriptorProvider();
// Act
provider.Execute(context);
// Assert
var matches = GetBindTagHelpers(context);
var bind = Assert.Single(matches);
Assert.Equal("myprop", bind.Metadata[BlazorMetadata.Bind.ValueAttribute]);
Assert.Equal("myevent", bind.Metadata[BlazorMetadata.Bind.ChangeAttribute]);
Assert.Equal("checkbox", bind.Metadata[BlazorMetadata.Bind.TypeAttribute]);
Assert.True(bind.IsInputElementBindTagHelper());
Assert.False(bind.IsInputElementFallbackBindTagHelper());
var rule = Assert.Single(bind.TagMatchingRules);
Assert.Equal("input", rule.TagName);
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
Assert.Collection(
rule.Attributes,
a =>
{
Assert.Equal("type", a.DisplayName);
Assert.Equal("type", a.Name);
Assert.Equal(RequiredAttributeDescriptor.NameComparisonMode.FullMatch, a.NameComparison);
Assert.Equal("checkbox", a.Value);
Assert.Equal(RequiredAttributeDescriptor.ValueComparisonMode.FullMatch, a.ValueComparison);
},
a =>
{
Assert.Equal("bind", a.DisplayName);
Assert.Equal("bind", a.Name);
});
var attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("bind"));
Assert.Equal("bind", attribute.Name);
Assert.Equal("Bind", attribute.GetPropertyName());
Assert.Equal("object Test.BindAttributes.Bind", attribute.DisplayName);
attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("format"));
Assert.Equal("format-myprop", attribute.Name);
Assert.Equal("Format_myprop", attribute.GetPropertyName());
Assert.Equal("string Test.BindAttributes.Format_myprop", attribute.DisplayName);
}
[Fact]
public void Execute_BindOnInputElementWithTypeAttributeAndSuffix_CreatesDescriptor()
{
// Arrange
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
using Microsoft.AspNetCore.Components;
namespace Test
{
[BindInputElement(""checkbox"", ""somevalue"", ""myprop"", ""myevent"")]
public class BindAttributes
{
}
}
"));
Assert.Empty(compilation.GetDiagnostics());
var context = TagHelperDescriptorProviderContext.Create();
context.SetCompilation(compilation);
var provider = new BindTagHelperDescriptorProvider();
// Act
provider.Execute(context);
// Assert
var matches = GetBindTagHelpers(context);
var bind = Assert.Single(matches);
Assert.Equal("myprop", bind.Metadata[BlazorMetadata.Bind.ValueAttribute]);
Assert.Equal("myevent", bind.Metadata[BlazorMetadata.Bind.ChangeAttribute]);
Assert.Equal("checkbox", bind.Metadata[BlazorMetadata.Bind.TypeAttribute]);
Assert.True(bind.IsInputElementBindTagHelper());
Assert.False(bind.IsInputElementFallbackBindTagHelper());
var rule = Assert.Single(bind.TagMatchingRules);
Assert.Equal("input", rule.TagName);
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
Assert.Collection(
rule.Attributes,
a =>
{
Assert.Equal("type", a.DisplayName);
Assert.Equal("type", a.Name);
Assert.Equal(RequiredAttributeDescriptor.NameComparisonMode.FullMatch, a.NameComparison);
Assert.Equal("checkbox", a.Value);
Assert.Equal(RequiredAttributeDescriptor.ValueComparisonMode.FullMatch, a.ValueComparison);
},
a =>
{
Assert.Equal("bind-somevalue", a.DisplayName);
Assert.Equal("bind-somevalue", a.Name);
});
var attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("bind"));
Assert.Equal("bind-somevalue", attribute.Name);
Assert.Equal("Bind_somevalue", attribute.GetPropertyName());
Assert.Equal("object Test.BindAttributes.Bind_somevalue", attribute.DisplayName);
attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("format"));
Assert.Equal("format-somevalue", attribute.Name);
Assert.Equal("Format_somevalue", attribute.GetPropertyName());
Assert.Equal("string Test.BindAttributes.Format_somevalue", attribute.DisplayName);
}
[Fact]
public void Execute_BindFallback_CreatesDescriptor()
{
// Arrange
var compilation = BaseCompilation;
Assert.Empty(compilation.GetDiagnostics());
var context = TagHelperDescriptorProviderContext.Create();
context.SetCompilation(compilation);
var provider = new BindTagHelperDescriptorProvider();
// Act
provider.Execute(context);
// Assert
var bind = Assert.Single(context.Results, r => r.IsFallbackBindTagHelper());
// These are features Bind Tags Helpers don't use. Verifying them once here and
// then ignoring them.
Assert.Empty(bind.AllowedChildTags);
Assert.Null(bind.TagOutputHint);
// These are features that are invariants of all Bind Tag Helpers. Verifying them once
// here and then ignoring them.
Assert.Empty(bind.Diagnostics);
Assert.False(bind.HasErrors);
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, bind.Kind);
Assert.Equal(BlazorMetadata.Bind.RuntimeName, bind.Metadata[TagHelperMetadata.Runtime.Name]);
Assert.False(bind.IsDefaultKind());
Assert.False(bind.KindUsesDefaultTagHelperRuntime());
Assert.False(bind.Metadata.ContainsKey(BlazorMetadata.Bind.ValueAttribute));
Assert.False(bind.Metadata.ContainsKey(BlazorMetadata.Bind.ChangeAttribute));
Assert.True(bind.IsFallbackBindTagHelper());
Assert.Equal(
"Binds the provided expression to an attribute and a change event, based on the naming of " +
"the bind attribute. For example: <code>bind-value-onchange=\"...\"</code> will assign the " +
"current value of the expression to the 'value' attribute, and assign a delegate that attempts " +
"to set the value to the 'onchange' attribute.",
bind.Documentation);
// These are all trivially derived from the assembly/namespace/type name
Assert.Equal("Microsoft.AspNetCore.Components", bind.AssemblyName);
Assert.Equal("Bind", bind.Name);
Assert.Equal("Microsoft.AspNetCore.Components.Bind", bind.DisplayName);
Assert.Equal("Microsoft.AspNetCore.Components.Bind", bind.GetTypeName());
// The tag matching rule for a bind-Component is always the component name + the attribute name
var rule = Assert.Single(bind.TagMatchingRules);
Assert.Empty(rule.Diagnostics);
Assert.False(rule.HasErrors);
Assert.Null(rule.ParentTag);
Assert.Equal("*", rule.TagName);
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
var requiredAttribute = Assert.Single(rule.Attributes);
Assert.Empty(requiredAttribute.Diagnostics);
Assert.Equal("bind-...", requiredAttribute.DisplayName);
Assert.Equal("bind-", requiredAttribute.Name);
Assert.Equal(RequiredAttributeDescriptor.NameComparisonMode.PrefixMatch, requiredAttribute.NameComparison);
Assert.Null(requiredAttribute.Value);
Assert.Equal(RequiredAttributeDescriptor.ValueComparisonMode.None, requiredAttribute.ValueComparison);
var attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("bind"));
// Invariants
Assert.Empty(attribute.Diagnostics);
Assert.False(attribute.HasErrors);
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, attribute.Kind);
Assert.False(attribute.IsDefaultKind());
Assert.False(attribute.IsIndexerBooleanProperty);
Assert.False(attribute.IsIndexerStringProperty);
Assert.True(attribute.HasIndexer);
Assert.Equal("bind-", attribute.IndexerNamePrefix);
Assert.Equal("System.Object", attribute.IndexerTypeName);
Assert.Equal(
"Binds the provided expression to an attribute and a change event, based on the naming of " +
"the bind attribute. For example: <code>bind-value-onchange=\"...\"</code> will assign the " +
"current value of the expression to the 'value' attribute, and assign a delegate that attempts " +
"to set the value to the 'onchange' attribute.",
attribute.Documentation);
Assert.Equal("bind-...", attribute.Name);
Assert.Equal("Bind", attribute.GetPropertyName());
Assert.Equal(
"System.Collections.Generic.Dictionary<string, object> Microsoft.AspNetCore.Components.Bind.Bind",
attribute.DisplayName);
// Defined from the property type
Assert.Equal("System.Collections.Generic.Dictionary<string, object>", attribute.TypeName);
Assert.False(attribute.IsStringProperty);
Assert.False(attribute.IsBooleanProperty);
Assert.False(attribute.IsEnum);
attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("format"));
// Invariants
Assert.Empty(attribute.Diagnostics);
Assert.False(attribute.HasErrors);
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, attribute.Kind);
Assert.False(attribute.IsDefaultKind());
Assert.True(attribute.HasIndexer);
Assert.Equal("format-", attribute.IndexerNamePrefix);
Assert.Equal("System.String", attribute.IndexerTypeName);
Assert.False(attribute.IsIndexerBooleanProperty);
Assert.True(attribute.IsIndexerStringProperty);
Assert.Equal(
"Specifies a format to convert the value specified by the corresponding bind attribute. " +
"For example: <code>format-value=\"...\"</code> will apply a format string to the value " +
"specified in <code>bind-value-...</code>. The format string can currently only be used with " +
"expressions of type <code>DateTime</code>.",
attribute.Documentation);
Assert.Equal("format-...", attribute.Name);
Assert.Equal("Format", attribute.GetPropertyName());
Assert.Equal(
"System.Collections.Generic.Dictionary<string, string> Microsoft.AspNetCore.Components.Bind.Format",
attribute.DisplayName);
// Defined from the property type
Assert.Equal("System.Collections.Generic.Dictionary<string, string>", attribute.TypeName);
Assert.False(attribute.IsStringProperty);
Assert.False(attribute.IsBooleanProperty);
Assert.False(attribute.IsEnum);
}
private static TagHelperDescriptor[] GetBindTagHelpers(TagHelperDescriptorProviderContext context)
{
return ExcludeBuiltInComponents(context).Where(t => t.IsBindTagHelper()).ToArray();
}
}
}

View File

@ -0,0 +1,125 @@
// 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.CodeAnalysis.Razor;
using Xunit;
namespace Microsoft.AspNetCore.Razor.Language.Components
{
public class EventHandlerTagHelperDescriptorProviderTest : BaseTagHelperDescriptorProviderTest
{
[Fact]
public void Execute_EventHandler_CreatesDescriptor()
{
// Arrange
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
using System;
using Microsoft.AspNetCore.Components;
namespace Test
{
[EventHandler(""onclick"", typeof(Action<UIMouseEventArgs>))]
public class EventHandlers
{
}
}
"));
Assert.Empty(compilation.GetDiagnostics());
var context = TagHelperDescriptorProviderContext.Create();
context.SetCompilation(compilation);
var provider = new EventHandlerTagHelperDescriptorProvider();
// Act
provider.Execute(context);
// Assert
var matches = GetEventHandlerTagHelpers(context);
var item = Assert.Single(matches);
// These are features Event Handler Tag Helpers don't use. Verifying them once here and
// then ignoring them.
Assert.Empty(item.AllowedChildTags);
Assert.Null(item.TagOutputHint);
// These are features that are invariants of all Event Handler Helpers. Verifying them once
// here and then ignoring them.
Assert.Empty(item.Diagnostics);
Assert.False(item.HasErrors);
Assert.Equal(BlazorMetadata.EventHandler.TagHelperKind, item.Kind);
Assert.Equal(BlazorMetadata.EventHandler.RuntimeName, item.Metadata[TagHelperMetadata.Runtime.Name]);
Assert.False(item.IsDefaultKind());
Assert.False(item.KindUsesDefaultTagHelperRuntime());
Assert.Equal(
"Sets the 'onclick' attribute to the provided string or delegate value. " +
"A delegate value should be of type 'System.Action<Microsoft.AspNetCore.Components.UIMouseEventArgs>'.",
item.Documentation);
// These are all trivially derived from the assembly/namespace/type name
Assert.Equal("TestAssembly", item.AssemblyName);
Assert.Equal("onclick", item.Name);
Assert.Equal("Test.EventHandlers", item.DisplayName);
Assert.Equal("Test.EventHandlers", item.GetTypeName());
// The tag matching rule for an event handler is just the attribute name
var rule = Assert.Single(item.TagMatchingRules);
Assert.Empty(rule.Diagnostics);
Assert.False(rule.HasErrors);
Assert.Null(rule.ParentTag);
Assert.Equal("*", rule.TagName);
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
var requiredAttribute = Assert.Single(rule.Attributes);
Assert.Empty(requiredAttribute.Diagnostics);
Assert.Equal("onclick", requiredAttribute.DisplayName);
Assert.Equal("onclick", requiredAttribute.Name);
Assert.Equal(RequiredAttributeDescriptor.NameComparisonMode.FullMatch, requiredAttribute.NameComparison);
Assert.Null(requiredAttribute.Value);
Assert.Equal(RequiredAttributeDescriptor.ValueComparisonMode.None, requiredAttribute.ValueComparison);
var attribute = Assert.Single(item.BoundAttributes);
// Invariants
Assert.Empty(attribute.Diagnostics);
Assert.False(attribute.HasErrors);
Assert.Equal(BlazorMetadata.EventHandler.TagHelperKind, attribute.Kind);
Assert.False(attribute.IsDefaultKind());
Assert.False(attribute.HasIndexer);
Assert.Null(attribute.IndexerNamePrefix);
Assert.Null(attribute.IndexerTypeName);
Assert.False(attribute.IsIndexerBooleanProperty);
Assert.False(attribute.IsIndexerStringProperty);
Assert.Collection(
attribute.Metadata.OrderBy(kvp => kvp.Key),
kvp => Assert.Equal(kvp, new KeyValuePair<string, string>(BlazorMetadata.Component.WeaklyTypedKey, bool.TrueString)),
kvp => Assert.Equal(kvp, new KeyValuePair<string, string>("Common.PropertyName", "onclick")));
Assert.Equal(
"Sets the 'onclick' attribute to the provided string or delegate value. " +
"A delegate value should be of type 'System.Action<Microsoft.AspNetCore.Components.UIMouseEventArgs>'.",
attribute.Documentation);
Assert.Equal("onclick", attribute.Name);
Assert.Equal("onclick", attribute.GetPropertyName());
Assert.Equal("string Test.EventHandlers.onclick", attribute.DisplayName);
// Defined from the property type
Assert.Equal("System.String", attribute.TypeName);
Assert.True(attribute.IsStringProperty);
Assert.False(attribute.IsBooleanProperty);
Assert.False(attribute.IsEnum);
}
private static TagHelperDescriptor[] GetEventHandlerTagHelpers(TagHelperDescriptorProviderContext context)
{
return ExcludeBuiltInComponents(context).Where(t => t.IsEventHandlerTagHelper()).ToArray();
}
}
}

View File

@ -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.Collections.Generic;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
namespace Microsoft.CodeAnalysis.Razor
{
public class GenericTypeNameRewriterTest
{
[Theory]
[InlineData("TItem2", "Type2")]
// Unspecified argument -> System.Object
[InlineData("TItem3", "System.Object")]
// Not a type parameter
[InlineData("TItem4", "TItem4")]
// In a qualified name, not a type parameter
[InlineData("TItem1.TItem2", "TItem1.TItem2")]
// Type parameters can't have type parameters
[InlineData("TItem1.TItem2<TItem1, TItem2, TItem3>", "TItem1.TItem2<Type1, Type2, System.Object>")]
[InlineData("TItem2<TItem1<TItem3>, System.TItem2, RenderFragment<List<TItem1>>", "TItem2<TItem1<System.Object>, System.TItem2, RenderFragment<List<Type1>>")]
public void GenericTypeNameRewriter_CanReplaceTypeParametersWithTypeArguments(string original, string expected)
{
// Arrange
var visitor = new GenericTypeNameRewriter(new Dictionary<string, string>()
{
{ "TItem1", "Type1" },
{ "TItem2", "Type2" },
{ "TItem3", null },
});
// Act
var actual = visitor.Rewrite(original);
// Assert
Assert.Equal(expected, actual.ToString());
}
}
}

View File

@ -0,0 +1,35 @@
// 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.CodeAnalysis.CSharp;
using Xunit;
namespace Microsoft.CodeAnalysis.Razor
{
public class GlobalQualifiedTypeNameRewriterTest
{
[Theory]
[InlineData("String", "global::String")]
[InlineData("System.String", "global::System.String")]
[InlineData("TItem2", "TItem2")]
[InlineData("System.Collections.Generic.List<System.String>", "global::System.Collections.Generic.List<global::System.String>")]
[InlineData("System.Collections.Generic.Dictionary<System.String, TItem1>", "global::System.Collections.Generic.Dictionary<global::System.String, TItem1>")]
[InlineData("System.Collections.TItem3.Dictionary<System.String, TItem1>", "global::System.Collections.TItem3.Dictionary<global::System.String, TItem1>")]
[InlineData("System.Collections.TItem3.TItem1<System.String, TItem1>", "global::System.Collections.TItem3.TItem1<global::System.String, TItem1>")]
// This case is interesting because we know TITem2 to be a generic type parameter,
// and we know that this will never be valid, which is why we don't bother rewriting.
[InlineData("TItem2<System.String, TItem1>", "TItem2<global::System.String, TItem1>")]
public void GlobalQualifiedTypeNameRewriter_CanQualifyNames(string original, string expected)
{
// Arrange
var visitor = new GlobalQualifiedTypeNameRewriter(new[] { "TItem1", "TItem2", "TItem3" });
// Act
var actual = visitor.Rewrite(original);
// Assert
Assert.Equal(expected, actual.ToString());
}
}
}

View File

@ -0,0 +1,85 @@
// 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.Components;
using Xunit;
namespace Microsoft.CodeAnalysis.Razor
{
public class RefTagHelperDescriptorProviderTest : BaseTagHelperDescriptorProviderTest
{
[Fact]
public void Execute_CreatesDescriptor()
{
// Arrange
var context = TagHelperDescriptorProviderContext.Create();
var provider = new RefTagHelperDescriptorProvider();
// Act
provider.Execute(context);
// Assert
var matches = context.Results.Where(result => result.IsRefTagHelper());
var item = Assert.Single(matches);
Assert.Empty(item.AllowedChildTags);
Assert.Null(item.TagOutputHint);
Assert.Empty(item.Diagnostics);
Assert.False(item.HasErrors);
Assert.Equal(BlazorMetadata.Ref.TagHelperKind, item.Kind);
Assert.Equal(BlazorMetadata.Ref.RuntimeName, item.Metadata[TagHelperMetadata.Runtime.Name]);
Assert.False(item.IsDefaultKind());
Assert.False(item.KindUsesDefaultTagHelperRuntime());
Assert.Equal(
"Populates the specified field or property with a reference to the element or component.",
item.Documentation);
Assert.Equal("Microsoft.AspNetCore.Components", item.AssemblyName);
Assert.Equal("Ref", item.Name);
Assert.Equal("Microsoft.AspNetCore.Components.Ref", item.DisplayName);
Assert.Equal("Microsoft.AspNetCore.Components.Ref", item.GetTypeName());
// The tag matching rule for a ref is just the attribute name "ref"
var rule = Assert.Single(item.TagMatchingRules);
Assert.Empty(rule.Diagnostics);
Assert.False(rule.HasErrors);
Assert.Null(rule.ParentTag);
Assert.Equal("*", rule.TagName);
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
var requiredAttribute = Assert.Single(rule.Attributes);
Assert.Empty(requiredAttribute.Diagnostics);
Assert.Equal("ref", requiredAttribute.DisplayName);
Assert.Equal("ref", requiredAttribute.Name);
Assert.Equal(RequiredAttributeDescriptor.NameComparisonMode.FullMatch, requiredAttribute.NameComparison);
Assert.Null(requiredAttribute.Value);
Assert.Equal(RequiredAttributeDescriptor.ValueComparisonMode.None, requiredAttribute.ValueComparison);
var attribute = Assert.Single(item.BoundAttributes);
Assert.Empty(attribute.Diagnostics);
Assert.False(attribute.HasErrors);
Assert.Equal(BlazorMetadata.Ref.TagHelperKind, attribute.Kind);
Assert.False(attribute.IsDefaultKind());
Assert.False(attribute.HasIndexer);
Assert.Null(attribute.IndexerNamePrefix);
Assert.Null(attribute.IndexerTypeName);
Assert.False(attribute.IsIndexerBooleanProperty);
Assert.False(attribute.IsIndexerStringProperty);
Assert.Equal(
"Populates the specified field or property with a reference to the element or component.",
attribute.Documentation);
Assert.Equal("ref", attribute.Name);
Assert.Equal("Ref", attribute.GetPropertyName());
Assert.Equal("object Microsoft.AspNetCore.Components.Ref.Ref", attribute.DisplayName);
Assert.Equal("System.Object", attribute.TypeName);
Assert.False(attribute.IsStringProperty);
Assert.False(attribute.IsBooleanProperty);
Assert.False(attribute.IsEnum);
}
}
}

View File

@ -48,14 +48,18 @@
<MvcRazorExtenionOutput Include="$(ArtifactsBinDir)Microsoft.AspNetCore.Mvc.Razor.Extensions\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll" />
</ItemGroup>
<Copy SourceFiles="@(MvcRazorExtenionOutput)" DestinationFolder="$(SdkOutputPath)extensions\mvc-3-0\" />
<Copy SourceFiles="@(MvcRazorExtenionOutput)" DestinationFolder="$(SdkOutputPath)extensions\mvc-3-0\" SkipUnchangedFiles="true">
<Output TaskParameter="CopiedFiles" ItemName="FileWrites" />
</Copy>
<ItemGroup>
<ProjectOutput Include="$(ArtifactsBinDir)Microsoft.NET.Sdk.Razor\$(Configuration)\net46*\Microsoft.NET.Sdk.Razor.Tasks.*" />
<ProjectOutput Include="$(ArtifactsBinDir)Microsoft.NET.Sdk.Razor\$(Configuration)\netstandard2.0*\Microsoft.NET.Sdk.Razor.Tasks.*" />
</ItemGroup>
<Copy SourceFiles="@(ProjectOutput)" DestinationFiles="$(SdkOutputPath)tasks\%(RecursiveDir)%(FileName)%(Extension)" />
<Copy SourceFiles="@(ProjectOutput)" DestinationFiles="$(SdkOutputPath)tasks\%(RecursiveDir)%(FileName)%(Extension)" SkipUnchangedFiles="true">
<Output TaskParameter="CopiedFiles" ItemName="FileWrites" />
</Copy>
</Target>
<Target Name="PopulateNuspec" AfterTargets="InitializeStandardNuspecProperties" DependsOnTargets="LayoutDependencies">

View File

@ -0,0 +1,64 @@
// 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;
namespace Microsoft.AspNetCore.Components
{
/// <summary>
/// Configures options for binding specific element types.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public sealed class BindElementAttribute : Attribute
{
/// <summary>
/// Constructs an instance of <see cref="BindElementAttribute"/>.
/// </summary>
/// <param name="element">The tag name of the element.</param>
/// <param name="suffix">The suffix value. For example, set this to <code>value</code> for <code>bind-value</code>, or set this to <code>null</code> for <code>bind</code>.</param>
/// <param name="valueAttribute">The name of the value attribute to be bound.</param>
/// <param name="changeAttribute">The name of an attribute that will register an associated change event.</param>
public BindElementAttribute(string element, string suffix, string valueAttribute, string changeAttribute)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
if (valueAttribute == null)
{
throw new ArgumentNullException(nameof(valueAttribute));
}
if (changeAttribute == null)
{
throw new ArgumentNullException(nameof(changeAttribute));
}
Element = element;
ValueAttribute = valueAttribute;
ChangeAttribute = changeAttribute;
}
/// <summary>
/// Gets the tag name of the element.
/// </summary>
public string Element { get; }
/// <summary>
/// Gets the suffix value.
/// For example, this will be <code>value</code> to mean <code>bind-value</code>, or <code>null</code> to mean <code>bind</code>.
/// </summary>
public string Suffix { get; }
/// <summary>
/// Gets the name of the value attribute to be bound.
/// </summary>
public string ValueAttribute { get; }
/// <summary>
/// Gets the name of an attribute that will register an associated change event.
/// </summary>
public string ChangeAttribute { get; }
}
}

View File

@ -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;
namespace Microsoft.AspNetCore.Components
{
/// <summary>
/// Configures options for binding subtypes of an HTML <code>input</code> element.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public sealed class BindInputElementAttribute : Attribute
{
/// <summary>
/// Constructs an instance of <see cref="BindInputElementAttribute"/>.
/// </summary>
/// <param name="type">The value of the element's <code>type</code> attribute.</param>
/// <param name="suffix">The suffix value.</param>
/// <param name="valueAttribute">The name of the value attribute to be bound.</param>
/// <param name="changeAttribute">The name of an attribute that will register an associated change event.</param>
public BindInputElementAttribute(string type, string suffix, string valueAttribute, string changeAttribute)
{
if (valueAttribute == null)
{
throw new ArgumentNullException(nameof(valueAttribute));
}
if (changeAttribute == null)
{
throw new ArgumentNullException(nameof(changeAttribute));
}
Type = type;
Suffix = suffix;
ValueAttribute = valueAttribute;
ChangeAttribute = changeAttribute;
}
/// <summary>
/// Gets the value of the element's <code>type</code> attribute.
/// </summary>
public string Type { get; }
/// <summary>
/// Gets the suffix value.
/// </summary>
public string Suffix { get; }
/// <summary>
/// Gets the name of the value attribute to be bound.
/// </summary>
public string ValueAttribute { get; }
/// <summary>
/// Gets the name of an attribute that will register an associated change event.
/// </summary>
public string ChangeAttribute { get; }
}
}

View File

@ -0,0 +1,229 @@
// 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.Globalization;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Components
{
/// <summary>
/// Methods used internally by @bind syntax. Not intended to be used directly.
/// </summary>
public static class BindMethods
{
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static T GetValue<T>(T value) => value;
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static string GetValue(DateTime value, string format) =>
value == default ? null
: (format == null ? value.ToString() : value.ToString(format));
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static string GetEventHandlerValue<T>(string value)
where T : UIEventArgs
{
return value;
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static MulticastDelegate GetEventHandlerValue<T>(Action value)
where T : UIEventArgs
{
return value;
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static MulticastDelegate GetEventHandlerValue<T>(Func<Task> value)
where T : UIEventArgs
{
return value;
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static MulticastDelegate GetEventHandlerValue<T>(Action<T> value)
where T : UIEventArgs
{
return value;
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static MulticastDelegate GetEventHandlerValue<T>(Func<T, Task> value)
where T : UIEventArgs
{
return value;
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<string> setter, string existingValue)
{
return _ => setter((string)((UIChangeEventArgs)_).Value);
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<bool> setter, bool existingValue)
{
return _ => setter((bool)((UIChangeEventArgs)_).Value);
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<bool?> setter, bool? existingValue)
{
return _ => setter((bool?)((UIChangeEventArgs)_).Value);
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<int> setter, int existingValue)
{
return _ => setter(int.Parse((string)((UIChangeEventArgs)_).Value));
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<int?> setter, int? existingValue)
{
return _ => setter(int.TryParse((string)((UIChangeEventArgs)_).Value, out var tmpvalue)
? tmpvalue
: (int?)null);
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<long> setter, long existingValue)
{
return _ => setter(long.Parse((string)((UIChangeEventArgs)_).Value));
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<long?> setter, long? existingValue)
{
return _ => setter(long.TryParse((string)((UIChangeEventArgs)_).Value, out var tmpvalue)
? tmpvalue
: (long?)null);
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<float> setter, float existingValue)
{
return _ => setter(float.Parse((string)((UIChangeEventArgs)_).Value));
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<float?> setter, float? existingValue)
{
return _ => setter(float.TryParse((string)((UIChangeEventArgs)_).Value, out var tmpvalue)
? tmpvalue
: (float?)null);
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<double> setter, double existingValue)
{
return _ => setter(double.Parse((string)((UIChangeEventArgs)_).Value));
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<double?> setter, double? existingValue)
{
return _ => setter(double.TryParse((string)((UIChangeEventArgs)_).Value, out var tmpvalue)
? tmpvalue
: (double?)null);
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<decimal> setter, decimal existingValue)
{
return _ => setter(decimal.Parse((string)((UIChangeEventArgs)_).Value));
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<decimal?> setter, decimal? existingValue)
{
return _ => setter(decimal.TryParse((string)((UIChangeEventArgs)_).Value, out var tmpvalue)
? tmpvalue
: (decimal?)null);
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<DateTime> setter, DateTime existingValue)
{
return _ => SetDateTimeValue(setter, ((UIChangeEventArgs)_).Value, null);
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static Action<UIEventArgs> SetValueHandler(Action<DateTime> setter, DateTime existingValue, string format)
{
return _ => SetDateTimeValue(setter, ((UIChangeEventArgs)_).Value, format);
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static Action<UIEventArgs> SetValueHandler<T>(Action<T> setter, T existingValue)
{
if (!typeof(T).IsEnum)
{
throw new ArgumentException($"'bind' does not accept values of type {typeof(T).FullName}. To read and write this value type, wrap it in a property of type string with suitable getters and setters.");
}
return _ =>
{
var value = (string)((UIChangeEventArgs)_).Value;
var parsed = (T)Enum.Parse(typeof(T), value);
setter(parsed);
};
}
private static void SetDateTimeValue(Action<DateTime> setter, object objValue, string format)
{
var stringValue = (string)objValue;
var parsedValue = string.IsNullOrEmpty(stringValue) ? default
: format != null && DateTime.TryParseExact(stringValue, format, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var parsedExact) ? parsedExact
: DateTime.Parse(stringValue);
setter(parsedValue);
}
}
}

View File

@ -0,0 +1,45 @@
// 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;
namespace Microsoft.AspNetCore.Components
{
/// <summary>
/// Associates an event argument type with an event attribute name.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public sealed class EventHandlerAttribute : Attribute
{
/// <summary>
/// Constructs an instance of <see cref="EventHandlerAttribute"/>.
/// </summary>
/// <param name="attributeName"></param>
/// <param name="eventArgsType"></param>
public EventHandlerAttribute(string attributeName, Type eventArgsType)
{
if (attributeName == null)
{
throw new ArgumentNullException(nameof(attributeName));
}
if (eventArgsType == null)
{
throw new ArgumentNullException(nameof(eventArgsType));
}
AttributeName = attributeName;
EventArgsType = eventArgsType;
}
/// <summary>
/// Gets the attribute name.
/// </summary>
public string AttributeName { get; }
/// <summary>
/// Gets the event argument type.
/// </summary>
public Type EventArgsType { get; }
}
}

View File

@ -12,9 +12,6 @@
<RazorSdkCurrentVersionTargets>$(SolutionRoot)src\Microsoft.NET.Sdk.Razor\build\netstandard2.0\Sdk.Razor.CurrentVersion.targets</RazorSdkCurrentVersionTargets>
</PropertyGroup>
<!-- Import solution dependencies.props when building in place -->
<Import Project="..\..\build\dependencies.props" Condition="'$(BinariesRoot)'==''"/>
<PropertyGroup>
<!-- aspnet/BuildTools#662 Don't police what version of NetCoreApp we use -->
<NETCoreAppMaximumVersion>99.9</NETCoreAppMaximumVersion>