509 lines
21 KiB
C#
509 lines
21 KiB
C#
// Copyright (c) .NET Foundation. All rights reserved.
|
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Microsoft.AspNetCore.Blazor.Shared;
|
|
using Microsoft.AspNetCore.Razor.Language;
|
|
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
|
|
|
namespace Microsoft.AspNetCore.Blazor.Razor
|
|
{
|
|
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(BlazorDiagnosticFactory.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(BlazorDiagnosticFactory.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 = $"{BlazorApi.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 = $"{BlazorApi.BindMethods.SetValueHandler}(__value => {original.Content} = __value, {original.Content})";
|
|
}
|
|
else if (changeAttribute == null && format != null)
|
|
{
|
|
changeExpressionContent = $"{BlazorApi.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,
|
|
};
|
|
|
|
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,
|
|
};
|
|
|
|
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)
|
|
{
|
|
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)),
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|