Skip to content

Commit

Permalink
Features/rendermode (#9277)
Browse files Browse the repository at this point in the history
Merge rendermode feature to main. Code flow only PR.
  • Loading branch information
chsienki authored Sep 13, 2023
2 parents c01e4d8 + 4258f88 commit d9a1071
Show file tree
Hide file tree
Showing 120 changed files with 3,985 additions and 11 deletions.
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 VisitRenderMode(RenderModeIntermediateNode node)
{
Context.NodeWriter.WriteRenderMode(Context, node);
}

public override void VisitFormName(FormNameIntermediateNode node)
{
Context.NodeWriter.WriteFormName(Context, 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 WriteRenderMode(CodeRenderingContext context, RenderModeIntermediateNode node)
{
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.");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Expand Down Expand Up @@ -225,6 +225,18 @@
<data name="RefTagHelper_Documentation" xml:space="preserve">
<value>Populates the specified field or property with a reference to the element or component.</value>
</data>
<data name="RenderModeDirective_Documentation" xml:space="preserve">
<value>Specifies the render mode for this component.</value>
</data>
<data name="RenderModeDirective_Token_Description" xml:space="preserve">
<value>An identifier or explicit razor expression that resolves to a render mode that should be applied to this component.</value>
</data>
<data name="RenderModeDirective_Token_Name" xml:space="preserve">
<value>Render mode</value>
</data>
<data name="RenderModeTagHelper_Documentation" xml:space="preserve">
<value>Specifies the render mode for a component.</value>
</data>
<data name="SplatTagHelper_Documentation" xml:space="preserve">
<value>Merges a collection of attributes into the current element or component.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ public override void WriteComponent(CodeRenderingContext context, ComponentInter
context.RenderNode(typeArgument);
}

// We need to preserve order for attibutes and attribute splats since the ordering
// We need to preserve order for attributes and attribute splats since the ordering
// has a semantic effect.

foreach (var child in node.Children)
Expand All @@ -401,6 +401,10 @@ public override void WriteComponent(CodeRenderingContext context, ComponentInter
{
context.RenderNode(splat);
}
else if (child is RenderModeIntermediateNode renderMode)
{
context.RenderNode(renderMode);
}
}

if (node.ChildContents.Any())
Expand Down Expand Up @@ -629,6 +633,9 @@ private void WriteTypeInferenceMethodParameterInnards(CodeRenderingContext conte
case TypeInferenceCapturedVariable capturedVariable:
context.CodeWriter.Write(capturedVariable.VariableName);
break;
case RenderModeIntermediateNode renderMode:
WriteCSharpCode(context, new CSharpCodeIntermediateNode() { Source = renderMode.Source, Children = { renderMode.Children[0] } });
break;
default:
throw new InvalidOperationException($"Not implemented: type inference method parameter from source {parameter.Source}");
}
Expand Down Expand Up @@ -1256,6 +1263,30 @@ protected override void WriteReferenceCaptureInnards(CodeRenderingContext contex
}
}

public override void WriteRenderMode(CodeRenderingContext context, RenderModeIntermediateNode node)
{
// Looks like:
// __o = (global::Microsoft.AspNetCore.Components.IComponentRenderMode)(expression);
WriteCSharpCode(context, new CSharpCodeIntermediateNode
{
Source = node.Source,
Children =
{
new IntermediateToken
{
Kind = TokenKind.CSharp,
Content = $"{DesignTimeVariable} = (global::{ComponentsApi.IComponentRenderMode.FullTypeName})("
},
node.Children[0],
new IntermediateToken
{
Kind = TokenKind.CSharp,
Content = ");"
}
}
});
}

private void WriteCSharpToken(CodeRenderingContext context, IntermediateToken token)
{
if (string.IsNullOrWhiteSpace(token.Content))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -571,4 +571,34 @@ public static RazorDiagnostic CreateFormName_NotAForm(SourceSpan? source)
{
return RazorDiagnostic.Create(FormName_NotAForm, source ?? SourceSpan.Undefined);
}

public static readonly RazorDiagnosticDescriptor Attribute_ValidOnlyOnComponent =
new RazorDiagnosticDescriptor(
$"{DiagnosticPrefix}10023",
() => "Attribute '{0}' is only valid when used on a component.",
RazorDiagnosticSeverity.Error);

public static RazorDiagnostic CreateAttribute_ValidOnlyOnComponent(SourceSpan? source, string attribute)
{
var diagnostic = RazorDiagnostic.Create(
Attribute_ValidOnlyOnComponent,
source ?? SourceSpan.Undefined,
attribute);
return diagnostic;
}

public static readonly RazorDiagnosticDescriptor RenderModeAttribute_ComponentDeclaredRenderMode =
new RazorDiagnosticDescriptor(
$"{DiagnosticPrefix}10024",
() => "Cannot override render mode for component '{0}' as it explicitly declares one.",
RazorDiagnosticSeverity.Error);

public static RazorDiagnostic CreateRenderModeAttribute_ComponentDeclaredRenderMode(SourceSpan? source, string component)
{
var diagnostic = RazorDiagnostic.Create(
RenderModeAttribute_ComponentDeclaredRenderMode,
source ?? SourceSpan.Undefined,
component);
return diagnostic;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ public static class Component

public const string NameMatchKey = "Components.NameMatch";

public const string HasRenderModeDirectiveKey = "Components.HasRenderModeDirective";

public const string FullyQualifiedNameMatch = "Components.FullyQualifiedNameMatch";

public const string InitOnlyProperty = "Components.InitOnlyProperty";
Expand Down Expand Up @@ -161,4 +163,11 @@ public static class Ref

public const string RuntimeName = "Components.None";
}

public static class RenderMode
{
public const string TagHelperKind = "Components.RenderMode";

public const string RuntimeName = "Components.None";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ protected void WriteComponentTypeInferenceMethod(CodeRenderingContext context, C
context.CodeWriter.Write(");");
context.CodeWriter.WriteLine();

string renderModeParameterName = null;
foreach (var parameter in parameters)
{
switch (parameter.Source)
Expand Down Expand Up @@ -253,11 +254,20 @@ protected void WriteComponentTypeInferenceMethod(CodeRenderingContext context, C
// We only use the synthetic cascading parameters for type inference
break;

case RenderModeIntermediateNode:
renderModeParameterName = parameter.ParameterName;
break;

default:
throw new InvalidOperationException($"Not implemented: type inference method parameter from source {parameter.Source}");
}
}

if (renderModeParameterName is not null)
{
WriteAddComponentRenderMode(context, ComponentsApi.RenderTreeBuilder.BuilderParameter, renderModeParameterName);
}

context.CodeWriter.WriteInstanceMethodInvocation(ComponentsApi.RenderTreeBuilder.BuilderParameter, ComponentsApi.RenderTreeBuilder.CloseComponent);

if (returnComponentType)
Expand Down Expand Up @@ -366,6 +376,11 @@ protected List<TypeInferenceMethodParameter> GetTypeInferenceMethodParameters(Co
var typeName = ComponentsApi.AddMultipleAttributesTypeFullName;
p.Add(new TypeInferenceMethodParameter($"__seq{p.Count}", typeName, $"__arg{p.Count}", usedForTypeInference: false, splat));
}
else if (child is RenderModeIntermediateNode renderMode)
{
var typeName = ComponentsApi.IComponentRenderMode.FullTypeName;
p.Add(new TypeInferenceMethodParameter($"__seq{p.Count}", typeName, $"__arg{p.Count}", usedForTypeInference: false, renderMode));
}
}

foreach (var childContent in node.Component.ChildContents)
Expand Down Expand Up @@ -429,6 +444,17 @@ protected static bool IsDefaultExpression(string expression)
return expression == "default" || expression.StartsWith("default(", StringComparison.Ordinal);
}

protected static void WriteAddComponentRenderMode(CodeRenderingContext context, string builderName, string variableName)
{
context.CodeWriter.Write(builderName);
context.CodeWriter.Write(".");
context.CodeWriter.Write(ComponentsApi.RenderTreeBuilder.AddComponentRenderMode);
context.CodeWriter.Write("(");
context.CodeWriter.Write(variableName);
context.CodeWriter.Write(");");
context.CodeWriter.WriteLine();
}

protected class TypeInferenceMethodParameter
{
public string SeqName { get; private set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable enable

using System;

namespace Microsoft.AspNetCore.Razor.Language.Components;

internal sealed class ComponentRenderModeDirective
{
public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective(
"rendermode",
DirectiveKind.SingleLine,
builder =>
{
builder.AddIdentifierOrExpression(ComponentResources.RenderModeDirective_Token_Name, ComponentResources.RenderModeDirective_Token_Description);
builder.Usage = DirectiveUsage.FileScopedSinglyOccurring;
builder.Description = ComponentResources.RenderModeDirective_Documentation;
});

public static void Register(RazorProjectEngineBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}

builder.AddDirective(Directive, FileKinds.Component);
builder.Features.Add(new ComponentRenderModeDirectivePass());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable enable

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

namespace Microsoft.AspNetCore.Razor.Language.Components;

internal sealed class ComponentRenderModeDirectivePass : IntermediateNodePassBase, IRazorDirectiveClassifierPass
{
private const string GeneratedRenderModeAttributeName = "__PrivateComponentRenderModeAttribute";

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(ComponentRenderModeDirective.Directive);
if (directives.Count == 0)
{
return;
}

// We don't need to worry about duplicate attributes as we have already replaced any multiples with MalformedDirective
Debug.Assert(directives.Count == 1);

var child = ((DirectiveIntermediateNode)directives[0].Node).Children.FirstOrDefault();
if (child == null)
{
return;
}

// generate the inner attribute class
var classDecl = new ClassDeclarationIntermediateNode()
{
ClassName = GeneratedRenderModeAttributeName,
BaseType = $"global::{ComponentsApi.RenderModeAttribute.FullTypeName}",
};
classDecl.Modifiers.Add("private");
classDecl.Modifiers.Add("sealed");
classDecl.Children.Add(new CSharpCodeIntermediateNode()
{
Children =
{
new IntermediateToken()
{
Kind = TokenKind.CSharp,
Content = $"private static global::{ComponentsApi.IComponentRenderMode.FullTypeName} ModeImpl => "
},
new CSharpCodeIntermediateNode()
{
Source = child.Source,
Children =
{
child is not DirectiveTokenIntermediateNode directiveToken
? child
: new IntermediateToken()
{
Kind = TokenKind.CSharp,
Content = directiveToken.Content
}
}
},
new IntermediateToken()
{
Kind = TokenKind.CSharp,
Content = ";"
}
}
});
classDecl.Children.Add(new CSharpCodeIntermediateNode()
{
Children =
{
new IntermediateToken()
{
Kind = TokenKind.CSharp,
Content = $"public override global::{ComponentsApi.IComponentRenderMode.FullTypeName} Mode => ModeImpl;"
}
}
});
@class.Children.Add(classDecl);

// generate the attribute usage on top of the class
var attributeNode = new CSharpCodeIntermediateNode();
attributeNode.Children.Add(new IntermediateToken()
{
Kind = TokenKind.CSharp,
Content = $"[global::{@namespace.Content}.{@class.ClassName}.{GeneratedRenderModeAttributeName}]",
});

// Insert the new attribute on top of the class
var childCount = @namespace.Children.Count;
for (var i = 0; i < childCount; i++)
{
if (object.ReferenceEquals(@namespace.Children[i], @class))
{
@namespace.Children.Insert(i, attributeNode);
break;
}
}
Debug.Assert(@namespace.Children.Count == childCount + 1);
}
}
Loading

0 comments on commit d9a1071

Please sign in to comment.