Skip to content

[Blazor] Adds support for specifying generic type constraints in Razor files #31800

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

Merged
merged 11 commits into from
Apr 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
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 @@ -297,7 +297,7 @@ public static CodeWriter WriteMethodInvocation(this CodeWriter writer, string me

public static CodeWriter WriteMethodInvocation(this CodeWriter writer, string methodName, bool endLine, params string[] parameters)
{
return
return
WriteStartMethodInvocation(writer, methodName)
.Write(string.Join(", ", parameters))
.WriteEndMethodInvocation(endLine);
Expand Down Expand Up @@ -377,7 +377,7 @@ public static CSharpCodeWritingScope BuildClassDeclaration(
string name,
string baseType,
IList<string> interfaces,
IList<string> typeParameters)
IList<(string name, string constraint)> typeParameters)
{
for (var i = 0; i < modifiers.Count; i++)
{
Expand All @@ -391,7 +391,7 @@ public static CSharpCodeWritingScope BuildClassDeclaration(
if (typeParameters != null && typeParameters.Count > 0)
{
writer.Write("<");
writer.Write(string.Join(", ", typeParameters));
writer.Write(string.Join(", ", typeParameters.Select(tp => tp.name)));
writer.Write(">");
}

Expand Down Expand Up @@ -419,6 +419,18 @@ public static CSharpCodeWritingScope BuildClassDeclaration(
}

writer.WriteLine();
if (typeParameters != null)
{
for (var i = 0; i < typeParameters.Count; i++)
{
var constraint = typeParameters[i].constraint;
if (constraint != null)
{
writer.Write(constraint);
writer.WriteLine();
}
}
}

return new CSharpCodeWritingScope(writer);
}
Expand Down Expand Up @@ -605,7 +617,7 @@ private class LinePragmaWriter : IDisposable
private readonly string _sourceFilePath;

public LinePragmaWriter(
CodeWriter writer,
CodeWriter writer,
SourceSpan span,
CodeRenderingContext context)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node
node.ClassName,
node.BaseType,
node.Interfaces,
node.TypeParameters.Select(p => p.ParameterName).ToArray()))
node.TypeParameters.Select(p => (p.ParameterName, p.Constraints)).ToArray()))
{
VisitDefault(node);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,12 @@
<data name="SplatTagHelper_Documentation" xml:space="preserve">
<value>Merges a collection of attributes into the current element or component.</value>
</data>
<data name="TypeParamDirective_Constraint_Description" xml:space="preserve">
<value>The constraints applied to the type parameter.</value>
</data>
<data name="TypeParamDirective_Constraint_Name" xml:space="preserve">
<value>type parameter constraint</value>
</data>
<data name="TypeParamDirective_Description" xml:space="preserve">
<value>Declares a generic type parameter for the generated component class.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,11 @@ protected override void OnDocumentStructureCreated(
continue;
}

@class.TypeParameters.Add(new TypeParameter() { ParameterName = typeParamNode.Tokens.First().Content, });
@class.TypeParameters.Add(new TypeParameter()
{
ParameterName = typeParamNode.Tokens.First().Content,
Constraints = typeParamNode.Tokens.Skip(1).FirstOrDefault()?.Content
});
}

method.ReturnType = "void";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,45 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
{
internal class ComponentTypeParamDirective
{
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 DirectiveDescriptor Directive = null;

public static RazorProjectEngineBuilder Register(RazorProjectEngineBuilder builder)
public static RazorProjectEngineBuilder Register(RazorProjectEngineBuilder builder, bool supportConstraints)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of passing this boolean through, should we just create a new ComponentTypeParamWithGeneric directive that we then conditionally register in the RazorEngineBuilder.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if I'm understanding correctly.

The directives map 1 to 1 to language concepts, so it's not clear to me that is desirable to have two of them to represent the same concept. The problem with it is that we would need to litter the codebase with additional checks for the newly introduced directive, when the only difference is what is allowed per language version.

I think it makes more sense for all that to be configured one time in one place since it only varies by language version.

{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}

if (Directive == null)
{
// Do nothing and assume the first registration wins. In real life this directive is only ever registered once.
if (supportConstraints)
{
Directive = DirectiveDescriptor.CreateDirective(
"typeparam",
DirectiveKind.SingleLine,
builder =>
{
builder.AddMemberToken(ComponentResources.TypeParamDirective_Token_Name, ComponentResources.TypeParamDirective_Token_Description);
builder.AddOptionalGenericTypeConstraintToken(ComponentResources.TypeParamDirective_Constraint_Name, ComponentResources.TypeParamDirective_Constraint_Description);
builder.Usage = DirectiveUsage.FileScopedMultipleOccurring;
builder.Description = ComponentResources.TypeParamDirective_Description;
});
}
else
{
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;
});
}
}

builder.AddDirective(Directive, FileKinds.Component, FileKinds.ComponentImport);
return builder;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -248,5 +248,22 @@ public static IDirectiveDescriptorBuilder AddOptionalAttributeToken(this IDirect

return builder;
}

public static IDirectiveDescriptorBuilder AddOptionalGenericTypeConstraintToken(this IDirectiveDescriptorBuilder builder, string name, string description)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}

builder.Tokens.Add(
DirectiveTokenDescriptor.CreateToken(
DirectiveTokenKind.GenericTypeConstraint,
optional: true,
name: name,
description: description));

return builder;
}
}
}
Original file line number Diff line number Diff line change
@@ -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.

namespace Microsoft.AspNetCore.Razor.Language
Expand All @@ -11,5 +11,6 @@ public enum DirectiveTokenKind
String,
Attribute,
Boolean,
GenericTypeConstraint,
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -12,17 +12,17 @@ internal class DesignTimeDirectiveTargetExtension : IDesignTimeDirectiveTargetEx
private const string DirectiveTokenHelperMethodName = "__RazorDirectiveTokenHelpers__";
private const string TypeHelper = "__typeHelper";

public void WriteDesignTimeDirective(CodeRenderingContext context, DesignTimeDirectiveIntermediateNode node)
public void WriteDesignTimeDirective(CodeRenderingContext context, DesignTimeDirectiveIntermediateNode directiveNode)
{
context.CodeWriter
.WriteLine("#pragma warning disable 219")
.WriteLine($"private void {DirectiveTokenHelperMethodName}() {{");

for (var i = 0; i < node.Children.Count; i++)
for (var i = 0; i < directiveNode.Children.Count; i++)
{
if (node.Children[i] is DirectiveTokenIntermediateNode n)
if (directiveNode.Children[i] is DirectiveTokenIntermediateNode directiveTokenNode)
{
WriteDesignTimeDirectiveToken(context, n);
WriteDesignTimeDirectiveToken(context, directiveNode, directiveTokenNode, currentIndex: i);
}
}

Expand All @@ -31,7 +31,7 @@ public void WriteDesignTimeDirective(CodeRenderingContext context, DesignTimeDir
.WriteLine("#pragma warning restore 219");
}

private void WriteDesignTimeDirectiveToken(CodeRenderingContext context, DirectiveTokenIntermediateNode node)
private void WriteDesignTimeDirectiveToken(CodeRenderingContext context, DesignTimeDirectiveIntermediateNode parent, DirectiveTokenIntermediateNode node, int currentIndex)
{
var tokenKind = node.DirectiveToken.Kind;
if (!node.Source.HasValue ||
Expand Down Expand Up @@ -192,6 +192,35 @@ private void WriteDesignTimeDirectiveToken(CodeRenderingContext context, Directi
context.CodeWriter.WriteLine(";");
}
break;
case DirectiveTokenKind.GenericTypeConstraint:
// We generate a generic local function with a generic parameter using the
// same name and apply the constraints, like below.
// The two warnings that we disable are:
// * Hiding the class type parameter with the parameter on the method
// * The function is defined but not used.
// static void TypeConstraints_TParamName<TParamName>() where TParamName ...;
context.CodeWriter.WriteLine("#pragma warning disable CS0693");
context.CodeWriter.WriteLine("#pragma warning disable CS8321");
using (context.CodeWriter.BuildLinePragma(node.Source, context))
{
// It's OK to do this since a GenericTypeParameterConstraint token is always preceded by a member token.
var genericTypeParamName = (DirectiveTokenIntermediateNode)parent.Children[currentIndex - 1];
context.CodeWriter
.Write("void __TypeConstraints_")
.Write(genericTypeParamName.Content)
.Write("<")
.Write(genericTypeParamName.Content)
.Write(">() ");

context.AddSourceMappingFor(node);
context.CodeWriter.Write(node.Content);
context.CodeWriter.WriteLine();
context.CodeWriter.WriteLine("{");
context.CodeWriter.WriteLine("}");
context.CodeWriter.WriteLine("#pragma warning restore CS0693");
context.CodeWriter.WriteLine("#pragma warning restore CS8321");
}
break;
}
context.CodeWriter.CurrentIndent = originalIndent;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// 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.

namespace Microsoft.AspNetCore.Razor.Language.Intermediate
{
public sealed class TypeParameter
{
public string ParameterName { get; set; }
public string Constraints { get; set; }
}
}
Loading