Skip to content

Commit

Permalink
[Blazor] Adds support for specifying generic type constraints in Razo…
Browse files Browse the repository at this point in the history
…r files (#31800)

Adds support for specifying generic type constraints in the @typeparam directive. 

The constraints are specified after the identifier for the type parameter using the same syntax used in c#. For example:
@typeparam TParameter where TParameter : Base, ISomeInterface, class, new()
  • Loading branch information
javiercn committed Apr 16, 2021
1 parent af66bef commit a4a3668
Show file tree
Hide file tree
Showing 91 changed files with 1,010 additions and 134 deletions.
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)
{
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

0 comments on commit a4a3668

Please sign in to comment.