Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add compiler support for @formname attribute #9153

Merged
merged 3 commits into from
Sep 1, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,11 @@ public override void VisitSplat(SplatIntermediateNode node)
Context.NodeWriter.WriteSplat(Context, node);
}

public override void VisitFormName(FormNameIntermediateNode node)
{
Context.NodeWriter.WriteFormName(Context, node);
}

public override void VisitDefault(IntermediateNode node)
{
Context.RenderChildren(node);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ public virtual void WriteSplat(CodeRenderingContext context, SplatIntermediateNo
throw new NotSupportedException("This writer does not support components.");
}

public virtual void WriteFormName(CodeRenderingContext context, FormNameIntermediateNode node)
{
throw new NotSupportedException("This writer does not support components.");
}

public abstract void BeginWriterScope(CodeRenderingContext context, string writer);

public abstract void EndWriterScope(CodeRenderingContext context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@
<data name="EventHandlerTagHelper_StopPropagation_Documentation" xml:space="preserve">
<value>Specifies whether to prevent further propagation of the '{0}' event in the capturing and bubbling phases.</value>
</data>
<data name="FormNameTagHelper_Documentation" xml:space="preserve">
<value>Name used to match the form handler on the server.</value>
</data>
<data name="ImplementsDirective_Description" xml:space="preserve">
<value>Declares an interface implementation for the current class.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,32 @@ private void WriteSplatInnards(CodeRenderingContext context, SplatIntermediateNo
}
}

public sealed override void WriteFormName(CodeRenderingContext context, FormNameIntermediateNode node)
{
var tokens = node.FindDescendantNodes<IntermediateToken>();
if (tokens.Count == 0)
{
return;
}

// Either all tokens should be C# or none of them.
if (tokens[0].IsCSharp)
{
context.CodeWriter.Write(ComponentsApi.RuntimeHelpers.TypeCheck);
context.CodeWriter.Write("<string>(");
foreach (var token in tokens)
{
Debug.Assert(token.IsCSharp);
WriteCSharpToken(context, token);
}
context.CodeWriter.Write(");");
}
else
{
Debug.Assert(!tokens.Any(t => t.IsCSharp));
}
}

public override void WriteReferenceCapture(CodeRenderingContext context, ReferenceCaptureIntermediateNode node)
{
if (context == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -549,4 +549,26 @@ public static RazorDiagnostic CreateBindAttributeParameter_UnsupportedSyntaxBind
attribute);
return diagnostic;
}

public static readonly RazorDiagnosticDescriptor FormName_MissingOnSubmit =
new RazorDiagnosticDescriptor(
$"{DiagnosticPrefix}10021",
() => "Attribute '@formname' can only be used when '@onsubmit' event handler is also present.",
RazorDiagnosticSeverity.Warning);

public static RazorDiagnostic CreateFormName_MissingOnSubmit(SourceSpan? source)
{
return RazorDiagnostic.Create(FormName_MissingOnSubmit, source ?? SourceSpan.Undefined);
}

public static readonly RazorDiagnosticDescriptor FormName_NotAForm =
new RazorDiagnosticDescriptor(
$"{DiagnosticPrefix}10022",
() => "Attribute '@formname' can only be applied to 'form' elements.",
RazorDiagnosticSeverity.Warning);

public static RazorDiagnostic CreateFormName_NotAForm(SourceSpan? source)
{
return RazorDiagnostic.Create(FormName_NotAForm, source ?? SourceSpan.Undefined);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Linq;
using Microsoft.AspNetCore.Razor.Language.Intermediate;

namespace Microsoft.AspNetCore.Razor.Language.Components;

internal sealed class ComponentFormNameLoweringPass : ComponentIntermediateNodePassBase, IRazorOptimizationPass
{
// Run after component lowering pass
public override int Order => 50;

protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
{
if (!IsComponentDocument(documentNode))
333fred marked this conversation as resolved.
Show resolved Hide resolved
{
return;
}

var references = documentNode.FindDescendantReferences<TagHelperDirectiveAttributeIntermediateNode>();
foreach (var reference in references)
{
var node = (TagHelperDirectiveAttributeIntermediateNode)reference.Node;
if (node.TagHelper.IsFormNameTagHelper())
{
var parent = reference.Parent;

if (parent is not MarkupElementIntermediateNode { TagName: "form" })
{
node.Diagnostics.Add(ComponentDiagnosticFactory.CreateFormName_NotAForm(node.Source));
reference.Replace(RewriteForErrorRecovery(node, parent));
continue;
}

if (!parent.Children.Any(c => c is HtmlAttributeIntermediateNode { AttributeName: "@onsubmit" }))
{
node.Diagnostics.Add(ComponentDiagnosticFactory.CreateFormName_MissingOnSubmit(node.Source));
reference.Replace(RewriteForErrorRecovery(node, parent));
continue;
}

reference.Replace(Rewrite(node));
}
}

static IntermediateNode Rewrite(TagHelperDirectiveAttributeIntermediateNode node)
{
return RewriteCore(node, new FormNameIntermediateNode
{
Source = node.Source
});
}

static IntermediateNode RewriteForErrorRecovery(TagHelperDirectiveAttributeIntermediateNode node, IntermediateNode parent)
{
if (parent is ComponentIntermediateNode)
{
return RewriteCore(node, new ComponentAttributeIntermediateNode
{
Source = node.Source,
AttributeName = node.OriginalAttributeName,
});
}

var replacement = new HtmlAttributeIntermediateNode
333fred marked this conversation as resolved.
Show resolved Hide resolved
{
Source = node.Source,
AttributeName = node.OriginalAttributeName,
Prefix = node.OriginalAttributeName + "=\"",
Suffix = "\"",
};
replacement.Children.AddRange(node.Children.Select(static child =>
{
IntermediateNode result = child is CSharpExpressionIntermediateNode
? new CSharpExpressionAttributeValueIntermediateNode()
: new HtmlAttributeValueIntermediateNode();
result.Source = child.Source;
result.Children.AddRange(child.Children);
result.Diagnostics.AddRange(child.Diagnostics);
return result;
}));
replacement.Diagnostics.AddRange(node.Diagnostics);
return replacement;
}

static IntermediateNode RewriteCore(TagHelperDirectiveAttributeIntermediateNode node, IntermediateNode replacement)
{
replacement.Children.AddRange(node.Children);
replacement.Diagnostics.AddRange(node.Diagnostics);
return replacement;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,12 @@ public static class EventHandler
public const string TagHelperKind = "Components.EventHandler";
}

public static class FormName
{
public const string TagHelperKind = "Components.FormName";
public const string RuntimeName = "Components.None";
}

public static class Key
{
public const string TagHelperKind = "Components.Key";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

using System;
using System.Collections.Generic;
using System.Globalization;
jjonescz marked this conversation as resolved.
Show resolved Hide resolved
using System.Linq;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Extensions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ public override void WriteMarkupElement(CodeRenderingContext context, MarkupElem
.WriteStringLiteral(node.TagName)
.WriteEndMethodInvocation();

bool hasFormName = false;

// Render attributes and splats (in order) before creating the scope.
foreach (var child in node.Children)
{
Expand All @@ -213,6 +215,12 @@ public override void WriteMarkupElement(CodeRenderingContext context, MarkupElem
{
context.RenderNode(splat);
}
else if (child is FormNameIntermediateNode formName)
{
Debug.Assert(!hasFormName);
context.RenderNode(formName);
hasFormName = true;
}
}

foreach (var setKey in node.SetKeys)
Expand All @@ -231,6 +239,20 @@ public override void WriteMarkupElement(CodeRenderingContext context, MarkupElem
context.RenderNode(child);
}

// AddNamedEvent must be called last.
if (hasFormName)
{
// _builder.AddNamedEvent("onsubmit", __formName);
context.CodeWriter.Write(_scopeStack.BuilderVarName);
context.CodeWriter.Write(".");
context.CodeWriter.Write(ComponentsApi.RenderTreeBuilder.AddNamedEvent);
context.CodeWriter.Write("(\"onsubmit\", ");
context.CodeWriter.Write(_scopeStack.FormNameVarName);
context.CodeWriter.Write(");");
context.CodeWriter.WriteLine();
_scopeStack.IncrementFormName();
}

context.CodeWriter
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{ComponentsApi.RenderTreeBuilder.CloseElement}")
.WriteEndMethodInvocation();
Expand Down Expand Up @@ -900,6 +922,19 @@ private void WriteSplatInnards(CodeRenderingContext context, SplatIntermediateNo
}
}

public sealed override void WriteFormName(CodeRenderingContext context, FormNameIntermediateNode node)
{
// string __formName = expression;
context.CodeWriter.Write("string ");
context.CodeWriter.Write(_scopeStack.FormNameVarName);
context.CodeWriter.Write(" = ");
context.CodeWriter.Write(ComponentsApi.RuntimeHelpers.TypeCheck);
context.CodeWriter.Write("<string>(");
WriteAttributeValue(context, node.FindDescendantNodes<IntermediateToken>());
context.CodeWriter.Write(")");
context.CodeWriter.WriteLine(";");
}

public override void WriteReferenceCapture(CodeRenderingContext context, ReferenceCaptureIntermediateNode node)
{
// Looks like:
Expand Down Expand Up @@ -949,7 +984,7 @@ protected override void WriteReferenceCaptureInnards(CodeRenderingContext contex
}
}

private void WriteAttribute(CodeRenderingContext context, string key, IList<IntermediateToken> value)
private void WriteAttribute(CodeRenderingContext context, string key, IReadOnlyList<IntermediateToken> value)
{
BeginWriteAttribute(context, key);

Expand All @@ -969,7 +1004,7 @@ private void WriteAttribute(CodeRenderingContext context, string key, IList<Inte
context.CodeWriter.WriteEndMethodInvocation();
}

private void WriteAttribute(CodeRenderingContext context, IntermediateNode nameExpression, IList<IntermediateToken> value)
private void WriteAttribute(CodeRenderingContext context, IntermediateNode nameExpression, IReadOnlyList<IntermediateToken> value)
{
BeginWriteAttribute(context, nameExpression);
if (value.Count > 0)
Expand Down Expand Up @@ -1023,7 +1058,7 @@ private static string GetHtmlContent(HtmlContentIntermediateNode node)
// 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(CodeRenderingContext context, IList<IntermediateToken> tokens)
private static void WriteAttributeValue(CodeRenderingContext context, IReadOnlyList<IntermediateToken> tokens)
{
if (tokens == null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ public static class RenderTreeBuilder

public const string BuilderParameter = "__builder";

public const string FormNameVariableName = "__formName";

public const string OpenElement = nameof(OpenElement);

public const string CloseElement = nameof(CloseElement);
Expand All @@ -86,8 +88,11 @@ public static class RenderTreeBuilder
public const string AddAttribute = nameof(AddAttribute);

public const string AddMultipleAttributes = nameof(AddMultipleAttributes);

public const string AddComponentParameter = nameof(AddComponentParameter);

public const string AddNamedEvent = nameof(AddNamedEvent);

public const string AddElementReferenceCapture = nameof(AddElementReferenceCapture);

public const string AddComponentReferenceCapture = nameof(AddComponentReferenceCapture);
Expand Down
Loading