diff --git a/src/Microsoft.AspNetCore.Razor.Language/BoundAttributeDescriptor.cs b/src/Microsoft.AspNetCore.Razor.Language/BoundAttributeDescriptor.cs
index 787ded4336..b416fddd61 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/BoundAttributeDescriptor.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/BoundAttributeDescriptor.cs
@@ -21,10 +21,14 @@ namespace Microsoft.AspNetCore.Razor.Language
public bool IsIndexerStringProperty { get; protected set; }
+ public bool IsIndexerBooleanProperty { get; protected set; }
+
public bool IsEnum { get; protected set; }
public bool IsStringProperty { get; protected set; }
+ public bool IsBooleanProperty { get; protected set; }
+
public string Name { get; protected set; }
public string IndexerNamePrefix { get; protected set; }
diff --git a/src/Microsoft.AspNetCore.Razor.Language/BoundAttributeDescriptorExtensions.cs b/src/Microsoft.AspNetCore.Razor.Language/BoundAttributeDescriptorExtensions.cs
index 616eb3d52e..62b713a8fe 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/BoundAttributeDescriptorExtensions.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/BoundAttributeDescriptorExtensions.cs
@@ -27,5 +27,27 @@ namespace Microsoft.AspNetCore.Razor.Language
return string.Equals(attribute.Kind, TagHelperConventions.DefaultKind, StringComparison.Ordinal);
}
+
+ internal static bool ExpectsStringValue(this BoundAttributeDescriptor attribute, string name)
+ {
+ if (attribute.IsStringProperty)
+ {
+ return true;
+ }
+
+ var isIndexerNameMatch = TagHelperMatchingConventions.SatisfiesBoundAttributeIndexer(name, attribute);
+ return isIndexerNameMatch && attribute.IsIndexerStringProperty;
+ }
+
+ internal static bool ExpectsBooleanValue(this BoundAttributeDescriptor attribute, string name)
+ {
+ if (attribute.IsBooleanProperty)
+ {
+ return true;
+ }
+
+ var isIndexerNameMatch = TagHelperMatchingConventions.SatisfiesBoundAttributeIndexer(name, attribute);
+ return isIndexerNameMatch && attribute.IsIndexerBooleanProperty;
+ }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Razor.Language/DefaultBoundAttributeDescriptor.cs b/src/Microsoft.AspNetCore.Razor.Language/DefaultBoundAttributeDescriptor.cs
index 6389a49310..597d721a96 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/DefaultBoundAttributeDescriptor.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/DefaultBoundAttributeDescriptor.cs
@@ -35,6 +35,9 @@ namespace Microsoft.AspNetCore.Razor.Language
IsIndexerStringProperty = indexerTypeName == typeof(string).FullName || indexerTypeName == "string";
IsStringProperty = typeName == typeof(string).FullName || typeName == "string";
+
+ IsIndexerBooleanProperty = indexerTypeName == typeof(bool).FullName || indexerTypeName == "bool";
+ IsBooleanProperty = typeName == typeof(bool).FullName || typeName == "bool";
}
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorIntermediateNodeLoweringPhase.cs b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorIntermediateNodeLoweringPhase.cs
index 76fa627415..b7047bca1b 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorIntermediateNodeLoweringPhase.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorIntermediateNodeLoweringPhase.cs
@@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.Razor.Language
var imports = codeDocument.GetImportSyntaxTrees();
if (imports != null)
{
- var importsVisitor = new ImportsVisitor(document, builder, namespaces);
+ var importsVisitor = new ImportsVisitor(document, builder, namespaces, syntaxTree.Options.FeatureFlags);
for (var j = 0; j < imports.Count; j++)
{
@@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Razor.Language
}
var tagHelperPrefix = tagHelperContext?.Prefix;
- var visitor = new MainSourceVisitor(document, builder, namespaces, tagHelperPrefix)
+ var visitor = new MainSourceVisitor(document, builder, namespaces, tagHelperPrefix, syntaxTree.Options.FeatureFlags)
{
FilePath = syntaxTree.Source.FilePath,
};
@@ -151,12 +151,14 @@ namespace Microsoft.AspNetCore.Razor.Language
protected readonly IntermediateNodeBuilder _builder;
protected readonly DocumentIntermediateNode _document;
protected readonly Dictionary _namespaces;
+ protected readonly RazorParserFeatureFlags _featureFlags;
- public LoweringVisitor(DocumentIntermediateNode document, IntermediateNodeBuilder builder, Dictionary namespaces)
+ public LoweringVisitor(DocumentIntermediateNode document, IntermediateNodeBuilder builder, Dictionary namespaces, RazorParserFeatureFlags featureFlags)
{
_document = document;
_builder = builder;
_namespaces = namespaces;
+ _featureFlags = featureFlags;
}
public string FilePath { get; set; }
@@ -331,8 +333,7 @@ namespace Microsoft.AspNetCore.Razor.Language
protected SourceSpan? BuildSourceSpanFromNode(SyntaxTreeNode node)
{
- var location = node.Start;
- if (location == SourceLocation.Undefined)
+ if (node == null || node.Start == SourceLocation.Undefined)
{
return null;
}
@@ -351,8 +352,8 @@ namespace Microsoft.AspNetCore.Razor.Language
{
private readonly string _tagHelperPrefix;
- public MainSourceVisitor(DocumentIntermediateNode document, IntermediateNodeBuilder builder, Dictionary namespaces, string tagHelperPrefix)
- : base(document, builder, namespaces)
+ public MainSourceVisitor(DocumentIntermediateNode document, IntermediateNodeBuilder builder, Dictionary namespaces, string tagHelperPrefix, RazorParserFeatureFlags featureFlags)
+ : base(document, builder, namespaces, featureFlags)
{
_tagHelperPrefix = tagHelperPrefix;
}
@@ -670,10 +671,11 @@ namespace Microsoft.AspNetCore.Razor.Language
if (associatedDescriptors.Any() && renderedBoundAttributeNames.Add(attribute.Name))
{
- if (attributeValueNode == null)
+ var isMinimizedAttribute = attributeValueNode == null;
+ if (isMinimizedAttribute && !_featureFlags.AllowMinimizedBooleanTagHelperAttributes)
{
- // Minimized attributes are not valid for bound attributes. TagHelperBlockRewriter has already
- // logged an error if it was a bound attribute; so we can skip.
+ // Minimized attributes are not valid for non-boolean bound attributes. TagHelperBlockRewriter
+ // has already logged an error if it was a non-boolean bound attribute; so we can skip.
continue;
}
@@ -684,6 +686,14 @@ namespace Microsoft.AspNetCore.Razor.Language
return TagHelperMatchingConventions.CanSatisfyBoundAttribute(attribute.Name, a);
});
+ var expectsBooleanValue = associatedAttributeDescriptor.ExpectsBooleanValue(attribute.Name);
+
+ if (isMinimizedAttribute && !expectsBooleanValue)
+ {
+ // We do not allow minimized non-boolean bound attributes.
+ continue;
+ }
+
var setTagHelperProperty = new TagHelperPropertyIntermediateNode()
{
AttributeName = attribute.Name,
@@ -695,7 +705,7 @@ namespace Microsoft.AspNetCore.Razor.Language
};
_builder.Push(setTagHelperProperty);
- attributeValueNode.Accept(this);
+ attributeValueNode?.Accept(this);
_builder.Pop();
}
}
@@ -720,8 +730,8 @@ namespace Microsoft.AspNetCore.Razor.Language
private class ImportsVisitor : LoweringVisitor
{
- public ImportsVisitor(DocumentIntermediateNode document, IntermediateNodeBuilder builder, Dictionary namespaces)
- : base(document, new ImportBuilder(builder), namespaces)
+ public ImportsVisitor(DocumentIntermediateNode document, IntermediateNodeBuilder builder, Dictionary namespaces, RazorParserFeatureFlags featureFlags)
+ : base(document, new ImportBuilder(builder), namespaces, featureFlags)
{
}
diff --git a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParserOptions.cs b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParserOptions.cs
index d824d8d2b3..6bb6d2c284 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParserOptions.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParserOptions.cs
@@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Razor.Language
{
internal class DefaultRazorParserOptions : RazorParserOptions
{
- public DefaultRazorParserOptions(DirectiveDescriptor[] directives, bool designTime, bool parseLeadingDirectives)
+ public DefaultRazorParserOptions(DirectiveDescriptor[] directives, bool designTime, bool parseLeadingDirectives, RazorLanguageVersion version)
{
if (directives == null)
{
@@ -18,6 +18,8 @@ namespace Microsoft.AspNetCore.Razor.Language
Directives = directives;
DesignTime = designTime;
ParseLeadingDirectives = parseLeadingDirectives;
+ Version = version;
+ FeatureFlags = RazorParserFeatureFlags.Create(Version);
}
public override bool DesignTime { get; }
@@ -25,5 +27,9 @@ namespace Microsoft.AspNetCore.Razor.Language
public override IReadOnlyCollection Directives { get; }
public override bool ParseLeadingDirectives { get; }
+
+ public override RazorLanguageVersion Version { get; }
+
+ internal override RazorParserFeatureFlags FeatureFlags { get; }
}
}
diff --git a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParserOptionsBuilder.cs b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParserOptionsBuilder.cs
index 0fa1e56fed..eadc773439 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParserOptionsBuilder.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParserOptionsBuilder.cs
@@ -1,6 +1,7 @@
// 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;
using System.Collections.Generic;
using System.Linq;
@@ -8,9 +9,10 @@ namespace Microsoft.AspNetCore.Razor.Language
{
internal class DefaultRazorParserOptionsBuilder : RazorParserOptionsBuilder
{
- public DefaultRazorParserOptionsBuilder(bool designTime)
+ public DefaultRazorParserOptionsBuilder(bool designTime, RazorLanguageVersion version)
{
DesignTime = designTime;
+ Version = version;
}
public override bool DesignTime { get; }
@@ -19,9 +21,11 @@ namespace Microsoft.AspNetCore.Razor.Language
public override bool ParseLeadingDirectives { get; set; }
+ public override RazorLanguageVersion Version { get; }
+
public override RazorParserOptions Build()
{
- return new DefaultRazorParserOptions(Directives.ToArray(), DesignTime, ParseLeadingDirectives);
+ return new DefaultRazorParserOptions(Directives.ToArray(), DesignTime, ParseLeadingDirectives, Version);
}
}
}
diff --git a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParserOptionsFeature.cs b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParserOptionsFeature.cs
index c5df47dbd6..867e289ee7 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParserOptionsFeature.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorParserOptionsFeature.cs
@@ -8,11 +8,13 @@ namespace Microsoft.AspNetCore.Razor.Language
internal class DefaultRazorParserOptionsFeature : RazorEngineFeatureBase, IRazorParserOptionsFeature
{
private readonly bool _designTime;
+ private readonly RazorLanguageVersion _version;
private IConfigureRazorParserOptionsFeature[] _configureOptions;
- public DefaultRazorParserOptionsFeature(bool designTime)
+ public DefaultRazorParserOptionsFeature(bool designTime, RazorLanguageVersion version)
{
_designTime = designTime;
+ _version = version;
}
protected override void OnInitialized()
@@ -22,7 +24,7 @@ namespace Microsoft.AspNetCore.Razor.Language
public RazorParserOptions GetOptions()
{
- var builder = new DefaultRazorParserOptionsBuilder(_designTime);
+ var builder = new DefaultRazorParserOptionsBuilder(_designTime, _version);
for (var i = 0; i < _configureOptions.Length; i++)
{
_configureOptions[i].Configure(builder);
diff --git a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorTagHelperBinderPhase.cs b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorTagHelperBinderPhase.cs
index fe2d3649ed..b75ab9335c 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorTagHelperBinderPhase.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorTagHelperBinderPhase.cs
@@ -53,7 +53,8 @@ namespace Microsoft.AspNetCore.Razor.Language
}
var errorSink = new ErrorSink();
- var rewriter = new TagHelperParseTreeRewriter(tagHelperPrefix, descriptors);
+ var rewriter = new TagHelperParseTreeRewriter(tagHelperPrefix, descriptors, syntaxTree.Options.FeatureFlags);
+
var root = syntaxTree.Root;
root = rewriter.Rewrite(root, errorSink);
diff --git a/src/Microsoft.AspNetCore.Razor.Language/Extensions/DefaultTagHelperTargetExtension.cs b/src/Microsoft.AspNetCore.Razor.Language/Extensions/DefaultTagHelperTargetExtension.cs
index 0892f7b4d5..3a6247a47e 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/Extensions/DefaultTagHelperTargetExtension.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/Extensions/DefaultTagHelperTargetExtension.cs
@@ -336,7 +336,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions
}
// If we get there, this is the first time seeing this property so we need to evaluate the expression.
- if (node.BoundAttribute.IsStringProperty || (node.IsIndexerNameMatch && node.BoundAttribute.IsIndexerStringProperty))
+ if (node.BoundAttribute.ExpectsStringValue(node.AttributeName))
{
if (DesignTime)
{
@@ -407,7 +407,17 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions
context.CodeWriter.WriteStartAssignment(GetPropertyAccessor(node));
}
- RenderTagHelperAttributeInline(context, node, node.Source);
+ if (node.Children.Count == 0 &&
+ node.AttributeStructure == AttributeStructure.Minimized &&
+ node.BoundAttribute.ExpectsBooleanValue(node.AttributeName))
+ {
+ // If this is a minimized boolean attribute, set the value to true.
+ context.CodeWriter.Write("true");
+ }
+ else
+ {
+ RenderTagHelperAttributeInline(context, node, node.Source);
+ }
context.CodeWriter.WriteLine(";");
}
@@ -429,7 +439,17 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions
.Write(".");
}
- RenderTagHelperAttributeInline(context, node, node.Source);
+ if (node.Children.Count == 0 &&
+ node.AttributeStructure == AttributeStructure.Minimized &&
+ node.BoundAttribute.ExpectsBooleanValue(node.AttributeName))
+ {
+ // If this is a minimized boolean attribute, set the value to true.
+ context.CodeWriter.Write("true");
+ }
+ else
+ {
+ RenderTagHelperAttributeInline(context, node, node.Source);
+ }
context.CodeWriter.WriteLine(";");
}
diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperBlockRewriter.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperBlockRewriter.cs
index 014c4cd21b..af8b1a9c36 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperBlockRewriter.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperBlockRewriter.cs
@@ -16,13 +16,14 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
public static TagHelperBlockBuilder Rewrite(
string tagName,
bool validStructure,
+ RazorParserFeatureFlags featureFlags,
Block tag,
TagHelperBinding bindingResult,
ErrorSink errorSink)
{
// There will always be at least one child for the '<'.
var start = tag.Children.First().Start;
- var attributes = GetTagAttributes(tagName, validStructure, tag, bindingResult, errorSink);
+ var attributes = GetTagAttributes(tagName, validStructure, tag, bindingResult, errorSink, featureFlags);
var tagMode = GetTagMode(tagName, tag, bindingResult, errorSink);
return new TagHelperBlockBuilder(tagName, tagMode, start, attributes, bindingResult);
@@ -33,7 +34,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
bool validStructure,
Block tagBlock,
TagHelperBinding bindingResult,
- ErrorSink errorSink)
+ ErrorSink errorSink,
+ RazorParserFeatureFlags featureFlags)
{
var attributes = new List();
@@ -61,10 +63,15 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
SourceLocation? errorLocation = null;
- // Check if it's a bound attribute that is minimized or if it's a bound non-string attribute that
- // is null or whitespace.
- if ((result.IsBoundAttribute && result.AttributeValueNode == null) ||
- (result.IsBoundNonStringAttribute &&
+ // Check if it's a non-boolean bound attribute that is minimized or if it's a bound
+ // non-string attribute that has null or whitespace content.
+ var isMinimized = result.AttributeValueNode == null;
+ var isValidMinimizedAttribute = featureFlags.AllowMinimizedBooleanTagHelperAttributes && result.IsBoundBooleanAttribute;
+ if ((isMinimized &&
+ result.IsBoundAttribute &&
+ !isValidMinimizedAttribute) ||
+ (!isMinimized &&
+ result.IsBoundNonStringAttribute &&
IsNullOrWhitespaceAttributeValue(result.AttributeValueNode)))
{
errorLocation = GetAttributeNameStartLocation(child);
@@ -690,9 +697,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
var firstBoundAttribute = FindFirstBoundAttribute(name, descriptors);
var isBoundAttribute = firstBoundAttribute != null;
- var isBoundNonStringAttribute = isBoundAttribute &&
- !(firstBoundAttribute.IsStringProperty ||
- (TagHelperMatchingConventions.SatisfiesBoundAttributeIndexer(name, firstBoundAttribute) && firstBoundAttribute.IsIndexerStringProperty));
+ var isBoundNonStringAttribute = isBoundAttribute && !firstBoundAttribute.ExpectsStringValue(name);
+ var isBoundBooleanAttribute = isBoundAttribute && firstBoundAttribute.ExpectsBooleanValue(name);
var isMissingDictionaryKey = isBoundAttribute &&
firstBoundAttribute.IndexerNamePrefix != null &&
name.Length == firstBoundAttribute.IndexerNamePrefix.Length;
@@ -709,6 +715,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
AttributeName = name,
IsBoundAttribute = isBoundAttribute,
IsBoundNonStringAttribute = isBoundNonStringAttribute,
+ IsBoundBooleanAttribute = isBoundBooleanAttribute,
IsMissingDictionaryKey = isMissingDictionaryKey,
IsDuplicateAttribute = isDuplicateAttribute
};
@@ -766,6 +773,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
public bool IsBoundNonStringAttribute { get; set; }
+ public bool IsBoundBooleanAttribute { get; set; }
+
public bool IsMissingDictionaryKey { get; set; }
public bool IsDuplicateAttribute { get; set; }
diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperParseTreeRewriter.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperParseTreeRewriter.cs
index 7d720a365b..00d48921aa 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperParseTreeRewriter.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperParseTreeRewriter.cs
@@ -44,8 +44,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
private readonly Stack _blockStack;
private TagHelperBlockTracker _currentTagHelperTracker;
private BlockBuilder _currentBlock;
+ private RazorParserFeatureFlags _featureFlags;
- public TagHelperParseTreeRewriter(string tagHelperPrefix, IEnumerable descriptors)
+ public TagHelperParseTreeRewriter(
+ string tagHelperPrefix,
+ IEnumerable descriptors,
+ RazorParserFeatureFlags featureFlags)
{
_tagHelperPrefix = tagHelperPrefix;
_tagHelperBinder = new TagHelperBinder(tagHelperPrefix, descriptors);
@@ -53,6 +57,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
_blockStack = new Stack();
_attributeValueBuilder = new StringBuilder();
_htmlAttributeTracker = new List>();
+ _featureFlags = featureFlags;
}
private TagBlockTracker CurrentTracker => _trackerStack.Count > 0 ? _trackerStack.Peek() : null;
@@ -225,6 +230,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
var builder = TagHelperBlockRewriter.Rewrite(
tagName,
validTagStructure,
+ _featureFlags,
tagBlock,
tagHelperBinding,
errorSink);
diff --git a/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs
index f8bd37e267..8b752d0a7e 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs
@@ -682,6 +682,20 @@ namespace Microsoft.AspNetCore.Razor.Language
internal static string FormatTagHelperPrefixDirective_Description()
=> GetString("TagHelperPrefixDirective_Description");
+ ///
+ /// Provided value for razor language version is unsupported or invalid: '{0}'.
+ ///
+ internal static string InvalidRazorLanguageVersion
+ {
+ get => GetString("InvalidRazorLanguageVersion");
+ }
+
+ ///
+ /// Provided value for razor language version is unsupported or invalid: '{0}'.
+ ///
+ internal static string FormatInvalidRazorLanguageVersion(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("InvalidRazorLanguageVersion"), p0);
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorEngine.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorEngine.cs
index 7fec6786d7..2d506da665 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/RazorEngine.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/RazorEngine.cs
@@ -102,7 +102,7 @@ namespace Microsoft.AspNetCore.Razor.Language
internal static void AddRuntimeDefaults(IRazorEngineBuilder builder)
{
// Configure options
- builder.Features.Add(new DefaultRazorParserOptionsFeature(designTime: false));
+ builder.Features.Add(new DefaultRazorParserOptionsFeature(designTime: false, version: RazorParserOptions.LatestRazorLanguageVersion));
builder.Features.Add(new DefaultRazorCodeGenerationOptionsFeature(designTime: false));
// Intermediate Node Passes
@@ -116,7 +116,7 @@ namespace Microsoft.AspNetCore.Razor.Language
internal static void AddDesignTimeDefaults(IRazorEngineBuilder builder)
{
// Configure options
- builder.Features.Add(new DefaultRazorParserOptionsFeature(designTime: true));
+ builder.Features.Add(new DefaultRazorParserOptionsFeature(designTime: true, version: RazorParserOptions.LatestRazorLanguageVersion));
builder.Features.Add(new DefaultRazorCodeGenerationOptionsFeature(designTime: true));
builder.Features.Add(new SuppressChecksumOptionsFeature());
diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorLanguageVersion.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorLanguageVersion.cs
new file mode 100644
index 0000000000..122118d2cb
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Language/RazorLanguageVersion.cs
@@ -0,0 +1,33 @@
+// 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
+{
+ public enum RazorLanguageVersion
+ {
+ Version1_0 = 1,
+
+ Version1_1 = 2,
+
+ Version2_0 = 3,
+
+ Version2_1 = 4,
+ }
+
+ internal static class RazorLanguageVersionExtensions
+ {
+ internal static bool IsValid(this RazorLanguageVersion version)
+ {
+ switch (version)
+ {
+ case RazorLanguageVersion.Version1_0:
+ case RazorLanguageVersion.Version1_1:
+ case RazorLanguageVersion.Version2_0:
+ case RazorLanguageVersion.Version2_1:
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorParserFeatureFlags.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorParserFeatureFlags.cs
new file mode 100644
index 0000000000..fed78efb43
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Language/RazorParserFeatureFlags.cs
@@ -0,0 +1,39 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Razor.Language
+{
+ internal abstract class RazorParserFeatureFlags
+ {
+ public static RazorParserFeatureFlags Create(RazorLanguageVersion version)
+ {
+ if (!version.IsValid())
+ {
+ throw new ArgumentException(Resources.FormatInvalidRazorLanguageVersion(version.ToString()));
+ }
+
+ var allowMinimizedBooleanTagHelperAttributes = false;
+
+ if (version == RazorLanguageVersion.Version2_1)
+ {
+ allowMinimizedBooleanTagHelperAttributes = true;
+ }
+
+ return new DefaultRazorParserFeatureFlags(allowMinimizedBooleanTagHelperAttributes);
+ }
+
+ public abstract bool AllowMinimizedBooleanTagHelperAttributes { get; }
+
+ private class DefaultRazorParserFeatureFlags : RazorParserFeatureFlags
+ {
+ public DefaultRazorParserFeatureFlags(bool allowMinimizedBooleanTagHelperAttributes)
+ {
+ AllowMinimizedBooleanTagHelperAttributes = allowMinimizedBooleanTagHelperAttributes;
+ }
+
+ public override bool AllowMinimizedBooleanTagHelperAttributes { get; }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorParserOptions.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorParserOptions.cs
index c060abef59..47f91c0343 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/RazorParserOptions.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/RazorParserOptions.cs
@@ -8,9 +8,15 @@ namespace Microsoft.AspNetCore.Razor.Language
{
public abstract class RazorParserOptions
{
+ internal static readonly RazorLanguageVersion LatestRazorLanguageVersion = RazorLanguageVersion.Version2_1;
+
public static RazorParserOptions CreateDefault()
{
- return new DefaultRazorParserOptions(Array.Empty(), designTime: false, parseLeadingDirectives: false);
+ return new DefaultRazorParserOptions(
+ Array.Empty(),
+ designTime: false,
+ parseLeadingDirectives: false,
+ version: LatestRazorLanguageVersion);
}
public static RazorParserOptions Create(Action configure)
@@ -20,7 +26,7 @@ namespace Microsoft.AspNetCore.Razor.Language
throw new ArgumentNullException(nameof(configure));
}
- var builder = new DefaultRazorParserOptionsBuilder(designTime: false);
+ var builder = new DefaultRazorParserOptionsBuilder(designTime: false, version: LatestRazorLanguageVersion);
configure(builder);
var options = builder.Build();
@@ -34,7 +40,7 @@ namespace Microsoft.AspNetCore.Razor.Language
throw new ArgumentNullException(nameof(configure));
}
- var builder = new DefaultRazorParserOptionsBuilder(designTime: true);
+ var builder = new DefaultRazorParserOptionsBuilder(designTime: true, version: LatestRazorLanguageVersion);
configure(builder);
var options = builder.Build();
@@ -54,5 +60,9 @@ namespace Microsoft.AspNetCore.Razor.Language
/// In a future release this may be updated to include all leading directive content.
///
public abstract bool ParseLeadingDirectives { get; }
+
+ public virtual RazorLanguageVersion Version { get; } = LatestRazorLanguageVersion;
+
+ internal virtual RazorParserFeatureFlags FeatureFlags { get; }
}
}
diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorParserOptionsBuilder.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorParserOptionsBuilder.cs
index d6a0d46f5c..74b11fcc26 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/RazorParserOptionsBuilder.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/RazorParserOptionsBuilder.cs
@@ -1,6 +1,7 @@
// 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;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Razor.Language
@@ -13,6 +14,8 @@ namespace Microsoft.AspNetCore.Razor.Language
public abstract bool ParseLeadingDirectives { get; set; }
+ public virtual RazorLanguageVersion Version { get; }
+
public abstract RazorParserOptions Build();
}
}
diff --git a/src/Microsoft.AspNetCore.Razor.Language/Resources.resx b/src/Microsoft.AspNetCore.Razor.Language/Resources.resx
index f7c8033553..0d04b0c0b5 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/Resources.resx
+++ b/src/Microsoft.AspNetCore.Razor.Language/Resources.resx
@@ -261,4 +261,7 @@
Specify a prefix that is required in an element name for it to be included in Tag Helper processing.
+
+ Provided value for razor language version is unsupported or invalid: '{0}'.
+
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/BoundAttributeDescriptorExtensionsTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/BoundAttributeDescriptorExtensionsTest.cs
index c3e1c18ecf..8b1c290d3a 100644
--- a/test/Microsoft.AspNetCore.Razor.Language.Test/BoundAttributeDescriptorExtensionsTest.cs
+++ b/test/Microsoft.AspNetCore.Razor.Language.Test/BoundAttributeDescriptorExtensionsTest.cs
@@ -95,5 +95,185 @@ namespace Microsoft.AspNetCore.Razor.Language
// Assert
Assert.False(isDefault);
}
+
+ [Fact]
+ public void ExpectsStringValue_ReturnsTrue_ForStringProperty()
+ {
+ // Arrange
+ var tagHelperBuilder = new DefaultTagHelperDescriptorBuilder(TagHelperConventions.DefaultKind, "TestTagHelper", "Test");
+ tagHelperBuilder.TypeName("TestTagHelper");
+
+ var builder = new DefaultBoundAttributeDescriptorBuilder(tagHelperBuilder, TagHelperConventions.DefaultKind);
+ builder
+ .Name("test")
+ .PropertyName("BoundProp")
+ .TypeName(typeof(string).FullName);
+
+ var descriptor = builder.Build();
+
+ // Act
+ var result = descriptor.ExpectsStringValue("test");
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ExpectsStringValue_ReturnsFalse_ForNonStringProperty()
+ {
+ // Arrange
+ var tagHelperBuilder = new DefaultTagHelperDescriptorBuilder(TagHelperConventions.DefaultKind, "TestTagHelper", "Test");
+ tagHelperBuilder.TypeName("TestTagHelper");
+
+ var builder = new DefaultBoundAttributeDescriptorBuilder(tagHelperBuilder, TagHelperConventions.DefaultKind);
+ builder
+ .Name("test")
+ .PropertyName("BoundProp")
+ .TypeName(typeof(bool).FullName);
+
+ var descriptor = builder.Build();
+
+ // Act
+ var result = descriptor.ExpectsStringValue("test");
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ExpectsStringValue_ReturnsTrue_StringIndexerAndNameMatch()
+ {
+ // Arrange
+ var tagHelperBuilder = new DefaultTagHelperDescriptorBuilder(TagHelperConventions.DefaultKind, "TestTagHelper", "Test");
+ tagHelperBuilder.TypeName("TestTagHelper");
+
+ var builder = new DefaultBoundAttributeDescriptorBuilder(tagHelperBuilder, TagHelperConventions.DefaultKind);
+ builder
+ .Name("test")
+ .PropertyName("BoundProp")
+ .TypeName("System.Collection.Generic.IDictionary")
+ .AsDictionary("prefix-test-", typeof(string).FullName);
+
+ var descriptor = builder.Build();
+
+ // Act
+ var result = descriptor.ExpectsStringValue("prefix-test-key");
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ExpectsStringValue_ReturnsFalse_StringIndexerAndNameMismatch()
+ {
+ // Arrange
+ var tagHelperBuilder = new DefaultTagHelperDescriptorBuilder(TagHelperConventions.DefaultKind, "TestTagHelper", "Test");
+ tagHelperBuilder.TypeName("TestTagHelper");
+
+ var builder = new DefaultBoundAttributeDescriptorBuilder(tagHelperBuilder, TagHelperConventions.DefaultKind);
+ builder
+ .Name("test")
+ .PropertyName("BoundProp")
+ .TypeName("System.Collection.Generic.IDictionary")
+ .AsDictionary("prefix-test-", typeof(string).FullName);
+
+ var descriptor = builder.Build();
+
+ // Act
+ var result = descriptor.ExpectsStringValue("test");
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ExpectsBooleanValue_ReturnsTrue_ForBooleanProperty()
+ {
+ // Arrange
+ var tagHelperBuilder = new DefaultTagHelperDescriptorBuilder(TagHelperConventions.DefaultKind, "TestTagHelper", "Test");
+ tagHelperBuilder.TypeName("TestTagHelper");
+
+ var builder = new DefaultBoundAttributeDescriptorBuilder(tagHelperBuilder, TagHelperConventions.DefaultKind);
+ builder
+ .Name("test")
+ .PropertyName("BoundProp")
+ .TypeName(typeof(bool).FullName);
+
+ var descriptor = builder.Build();
+
+ // Act
+ var result = descriptor.ExpectsBooleanValue("test");
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ExpectsBooleanValue_ReturnsFalse_ForNonBooleanProperty()
+ {
+ // Arrange
+ var tagHelperBuilder = new DefaultTagHelperDescriptorBuilder(TagHelperConventions.DefaultKind, "TestTagHelper", "Test");
+ tagHelperBuilder.TypeName("TestTagHelper");
+
+ var builder = new DefaultBoundAttributeDescriptorBuilder(tagHelperBuilder, TagHelperConventions.DefaultKind);
+ builder
+ .Name("test")
+ .PropertyName("BoundProp")
+ .TypeName(typeof(int).FullName);
+
+ var descriptor = builder.Build();
+
+ // Act
+ var result = descriptor.ExpectsBooleanValue("test");
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ExpectsBooleanValue_ReturnsTrue_BooleanIndexerAndNameMatch()
+ {
+ // Arrange
+ var tagHelperBuilder = new DefaultTagHelperDescriptorBuilder(TagHelperConventions.DefaultKind, "TestTagHelper", "Test");
+ tagHelperBuilder.TypeName("TestTagHelper");
+
+ var builder = new DefaultBoundAttributeDescriptorBuilder(tagHelperBuilder, TagHelperConventions.DefaultKind);
+ builder
+ .Name("test")
+ .PropertyName("BoundProp")
+ .TypeName("System.Collection.Generic.IDictionary")
+ .AsDictionary("prefix-test-", typeof(bool).FullName);
+
+ var descriptor = builder.Build();
+
+ // Act
+ var result = descriptor.ExpectsBooleanValue("prefix-test-key");
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ExpectsBooleanValue_ReturnsFalse_BooleanIndexerAndNameMismatch()
+ {
+ // Arrange
+ var tagHelperBuilder = new DefaultTagHelperDescriptorBuilder(TagHelperConventions.DefaultKind, "TestTagHelper", "Test");
+ tagHelperBuilder.TypeName("TestTagHelper");
+
+ var builder = new DefaultBoundAttributeDescriptorBuilder(tagHelperBuilder, TagHelperConventions.DefaultKind);
+ builder
+ .Name("test")
+ .PropertyName("BoundProp")
+ .TypeName("System.Collection.Generic.IDictionary")
+ .AsDictionary("prefix-test-", typeof(bool).FullName);
+
+ var descriptor = builder.Build();
+
+ // Act
+ var result = descriptor.ExpectsBooleanValue("test");
+
+ // Assert
+ Assert.False(result);
+ }
}
}
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorParsingPhaseTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorParsingPhaseTest.cs
index 0fc887fc06..c495e9ab09 100644
--- a/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorParsingPhaseTest.cs
+++ b/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorParsingPhaseTest.cs
@@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Razor.Language
var engine = RazorEngine.CreateEmpty(builder =>
{
builder.Phases.Add(phase);
- builder.Features.Add(new DefaultRazorParserOptionsFeature(designTime: false));
+ builder.Features.Add(new DefaultRazorParserOptionsFeature(designTime: false, version: RazorParserOptions.LatestRazorLanguageVersion));
});
var codeDocument = TestRazorCodeDocument.CreateEmpty();
@@ -35,7 +35,7 @@ namespace Microsoft.AspNetCore.Razor.Language
var engine = RazorEngine.CreateEmpty((builder) =>
{
builder.Phases.Add(phase);
- builder.Features.Add(new DefaultRazorParserOptionsFeature(designTime: false));
+ builder.Features.Add(new DefaultRazorParserOptionsFeature(designTime: false, version: RazorParserOptions.LatestRazorLanguageVersion));
builder.Features.Add(new MyParserOptionsFeature());
});
@@ -58,9 +58,8 @@ namespace Microsoft.AspNetCore.Razor.Language
var engine = RazorEngine.CreateEmpty((builder) =>
{
builder.Phases.Add(phase);
- builder.Features.Add(new DefaultRazorParserOptionsFeature(designTime: false));
+ builder.Features.Add(new DefaultRazorParserOptionsFeature(designTime: false, version: RazorParserOptions.LatestRazorLanguageVersion));
builder.Features.Add(new MyParserOptionsFeature());
-
});
var imports = new[]
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/IntegrationTests/TestTagHelperDescriptors.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/IntegrationTests/TestTagHelperDescriptors.cs
index 0f6234eb36..d2034b18e0 100644
--- a/test/Microsoft.AspNetCore.Razor.Language.Test/IntegrationTests/TestTagHelperDescriptors.cs
+++ b/test/Microsoft.AspNetCore.Razor.Language.Test/IntegrationTests/TestTagHelperDescriptors.cs
@@ -46,6 +46,43 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
}
}
+ public static IEnumerable MinimizedBooleanTagHelperDescriptors
+ {
+ get
+ {
+ return new[]
+ {
+ CreateTagHelperDescriptor(
+ tagName: "span",
+ typeName: "SpanTagHelper",
+ assemblyName: "TestAssembly"),
+ CreateTagHelperDescriptor(
+ tagName: "div",
+ typeName: "DivTagHelper",
+ assemblyName: "TestAssembly"),
+ CreateTagHelperDescriptor(
+ tagName: "input",
+ typeName: "InputTagHelper",
+ assemblyName: "TestAssembly",
+ attributes: new Action[]
+ {
+ builder => builder
+ .Name("value")
+ .PropertyName("FooProp")
+ .TypeName("System.String"),
+ builder => builder
+ .Name("bound")
+ .PropertyName("BoundProp")
+ .TypeName("System.Boolean"),
+ builder => builder
+ .Name("age")
+ .PropertyName("AgeProp")
+ .TypeName("System.Int32"),
+ })
+ };
+ }
+ }
+
public static IEnumerable CssSelectorTagHelperDescriptors
{
get
@@ -267,6 +304,22 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
.RequireAttributeDescriptor(attribute => attribute.Name("input-bound-required-string"))
.RequireAttributeDescriptor(attribute => attribute.Name("input-unbound-required")),
}),
+ CreateTagHelperDescriptor(
+ tagName: "div",
+ typeName: "DivTagHelper",
+ assemblyName: "TestAssembly",
+ attributes: new Action[]
+ {
+ builder => builder
+ .Name("boundbool")
+ .PropertyName("BoundBoolProp")
+ .TypeName(typeof(bool).FullName),
+ builder => builder
+ .Name("booldict")
+ .PropertyName("BoolDictProp")
+ .TypeName("System.Collections.Generic.IDictionary")
+ .AsDictionaryAttribute("booldict-prefix-", typeof(bool).FullName),
+ }),
};
}
}
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TagHelperBlockRewriterTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TagHelperBlockRewriterTest.cs
index c7cebb1968..cc6b408e3d 100644
--- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TagHelperBlockRewriterTest.cs
+++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TagHelperBlockRewriterTest.cs
@@ -118,11 +118,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
var descriptors = new[]
{
TagHelperDescriptorBuilder.Create("CatchAllTagHelper", "SomeAssembly")
- .TagMatchingRuleDescriptor(rule =>
+ .TagMatchingRuleDescriptor(rule =>
rule
.RequireTagName("*")
.RequireAttributeDescriptor(attribute => attribute.Name("bound")))
- .BoundAttributeDescriptor(attribute =>
+ .BoundAttributeDescriptor(attribute =>
attribute
.Name("[item]")
.PropertyName("ListItems")
@@ -222,7 +222,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
var descriptors = new TagHelperDescriptor[]
{
TagHelperDescriptorBuilder.Create("InputTagHelper", "SomeAssembly")
- .TagMatchingRuleDescriptor(rule =>
+ .TagMatchingRuleDescriptor(rule =>
rule
.RequireTagName("input")
.RequireTagStructure(TagStructure.WithoutEndTag))
@@ -320,7 +320,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
var descriptors = new TagHelperDescriptor[]
{
TagHelperDescriptorBuilder.Create("InputTagHelper1", "SomeAssembly")
- .TagMatchingRuleDescriptor(rule =>
+ .TagMatchingRuleDescriptor(rule =>
rule
.RequireTagName("input")
.RequireTagStructure(structure1))
@@ -2251,7 +2251,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
TagHelperDescriptorBuilder.Create("mythTagHelper", "SomeAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("myth"))
- .BoundAttributeDescriptor(attribute =>
+ .BoundAttributeDescriptor(attribute =>
attribute
.Name("bound")
.PropertyName("Bound")
@@ -3901,7 +3901,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
var descriptors = new TagHelperDescriptor[]
{
TagHelperDescriptorBuilder.Create("InputTagHelper1", "SomeAssembly")
- .TagMatchingRuleDescriptor(rule =>
+ .TagMatchingRuleDescriptor(rule =>
rule
.RequireTagName("input")
.RequireAttributeDescriptor(attribute => attribute.Name("unbound-required")))
@@ -3959,5 +3959,107 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
// Act & Assert
EvaluateData(descriptors, documentContent, (MarkupBlock)expectedOutput, (RazorError[])expectedErrors);
}
+
+ [Fact]
+ public void Rewrite_UnderstandsMinimizedBooleanBoundAttributes()
+ {
+ // Arrange
+ var documentContent = "";
+ var descriptors = new TagHelperDescriptor[]
+ {
+ TagHelperDescriptorBuilder.Create("InputTagHelper", "SomeAssembly")
+ .TagMatchingRuleDescriptor(rule =>
+ rule
+ .RequireTagName("input"))
+ .BoundAttributeDescriptor(attribute =>
+ attribute
+ .Name("boundbool")
+ .PropertyName("BoundBoolProp")
+ .TypeName(typeof(bool).FullName))
+ .BoundAttributeDescriptor(attribute =>
+ attribute
+ .Name("boundbooldict")
+ .PropertyName("BoundBoolDictProp")
+ .TypeName("System.Collections.Generic.IDictionary")
+ .AsDictionary("boundbooldict-", typeof(bool).FullName))
+ .Build(),
+ };
+
+ var expectedOutput = new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("boundbool", null, AttributeStructure.Minimized),
+ new TagHelperAttributeNode("boundbooldict-key", null, AttributeStructure.Minimized),
+ }));
+
+ // Act & Assert
+ EvaluateData(descriptors, documentContent, expectedOutput, new RazorError[] { });
+ }
+
+ [Fact]
+ public void Rewrite_FeatureDisabled_AddsErrorForMinimizedBooleanBoundAttributes()
+ {
+ // Arrange
+ var documentContent = "";
+ var descriptors = new TagHelperDescriptor[]
+ {
+ TagHelperDescriptorBuilder.Create("InputTagHelper", "SomeAssembly")
+ .TagMatchingRuleDescriptor(rule =>
+ rule
+ .RequireTagName("input"))
+ .BoundAttributeDescriptor(attribute =>
+ attribute
+ .Name("boundbool")
+ .PropertyName("BoundBoolProp")
+ .TypeName(typeof(bool).FullName))
+ .BoundAttributeDescriptor(attribute =>
+ attribute
+ .Name("boundbooldict")
+ .PropertyName("BoundBoolDictProp")
+ .TypeName("System.Collections.Generic.IDictionary")
+ .AsDictionary("boundbooldict-", typeof(bool).FullName))
+ .Build(),
+ };
+
+ var featureFlags = new TestRazorParserFeatureFlags(allowMinimizedBooleanTagHelperAttributes: false);
+
+ var expectedOutput = new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "input",
+ TagMode.SelfClosing,
+ attributes: new List()
+ {
+ new TagHelperAttributeNode("boundbool", null, AttributeStructure.Minimized),
+ new TagHelperAttributeNode("boundbooldict-key", null, AttributeStructure.Minimized),
+ }));
+
+ var expectedErrors = new[]
+ {
+ new RazorError(
+ "Attribute 'boundbool' on tag helper element 'input' requires a value. Tag helper bound attributes of type 'System.Boolean' cannot be empty or contain only whitespace.",
+ new SourceLocation(7, 0, 7),
+ length: 9),
+ new RazorError(
+ "Attribute 'boundbooldict-key' on tag helper element 'input' requires a value. Tag helper bound attributes of type 'System.Boolean' cannot be empty or contain only whitespace.",
+ new SourceLocation(17, 0, 17),
+ length: 17),
+ };
+
+ // Act & Assert
+ EvaluateData(descriptors, documentContent, expectedOutput, expectedErrors, featureFlags: featureFlags);
+ }
+
+ private class TestRazorParserFeatureFlags : RazorParserFeatureFlags
+ {
+ public TestRazorParserFeatureFlags(bool allowMinimizedBooleanTagHelperAttributes)
+ {
+ AllowMinimizedBooleanTagHelperAttributes = allowMinimizedBooleanTagHelperAttributes;
+ }
+
+ public override bool AllowMinimizedBooleanTagHelperAttributes { get; }
+ }
}
}
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TagHelperParseTreeRewriterTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TagHelperParseTreeRewriterTest.cs
index 7b027820aa..5c75651f43 100644
--- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TagHelperParseTreeRewriterTest.cs
+++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TagHelperParseTreeRewriterTest.cs
@@ -52,7 +52,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
var errorSink = new ErrorSink();
var parseResult = ParseDocument(documentContent);
var document = parseResult.Root;
- var parseTreeRewriter = new TagHelperParseTreeRewriter(null, Enumerable.Empty());
+ var parseTreeRewriter = new TagHelperParseTreeRewriter(null, Enumerable.Empty(), parseResult.Options.FeatureFlags);
// Assert - Guard
var rootBlock = Assert.IsType(document);
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TagHelperRewritingTestBase.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TagHelperRewritingTestBase.cs
index 6779d73ec7..8471aaf23a 100644
--- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TagHelperRewritingTestBase.cs
+++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TagHelperRewritingTestBase.cs
@@ -51,11 +51,16 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
string documentContent,
MarkupBlock expectedOutput,
IEnumerable expectedErrors,
- string tagHelperPrefix = null)
+ string tagHelperPrefix = null,
+ RazorParserFeatureFlags featureFlags = null)
{
var syntaxTree = ParseDocument(documentContent);
var errorSink = new ErrorSink();
- var parseTreeRewriter = new TagHelperParseTreeRewriter(tagHelperPrefix, descriptors);
+ var parseTreeRewriter = new TagHelperParseTreeRewriter(
+ tagHelperPrefix,
+ descriptors,
+ featureFlags ?? syntaxTree.Options.FeatureFlags);
+
var actualTree = parseTreeRewriter.Rewrite(syntaxTree.Root, errorSink);
var allErrors = syntaxTree.Diagnostics.Concat(errorSink.Errors.Select(error => RazorDiagnostic.Create(error)));
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/RazorParserFeatureFlagsTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/RazorParserFeatureFlagsTest.cs
new file mode 100644
index 0000000000..e0b5c090b0
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Razor.Language.Test/RazorParserFeatureFlagsTest.cs
@@ -0,0 +1,41 @@
+// 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;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Razor.Language
+{
+ public class RazorParserFeatureFlagsTest
+ {
+ [Fact]
+ public void Create_LatestVersion_AllowsMinimizedBooleanTagHelperAttributes()
+ {
+ // Arrange & Act
+ var context = RazorParserFeatureFlags.Create(RazorLanguageVersion.Version2_1);
+
+ // Assert
+ Assert.True(context.AllowMinimizedBooleanTagHelperAttributes);
+ }
+
+ [Fact]
+ public void Create_OlderVersion_DoesNotAllowMinimizedBooleanTagHelperAttributes()
+ {
+ // Arrange & Act
+ var context = RazorParserFeatureFlags.Create(RazorLanguageVersion.Version1_1);
+
+ // Assert
+ Assert.False(context.AllowMinimizedBooleanTagHelperAttributes);
+ }
+
+ [Fact]
+ public void Create_UnknownVersion_Throws()
+ {
+ // Arrange, Act & Assert
+ var exception = Assert.Throws(
+ () => RazorParserFeatureFlags.Create(0));
+
+ Assert.Equal("Provided value for razor language version is unsupported or invalid: '0'.", exception.Message);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers.cshtml b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers.cshtml
index f704834241..c001d4945d 100644
--- a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers.cshtml
+++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers.cshtml
@@ -15,4 +15,6 @@
input-unbound-required="hello2"
catchall-unbound-required
input-bound-required-string="world" />
+
+ Tag helper with unmatched bound boolean attributes.
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers_DesignTime.codegen.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers_DesignTime.codegen.cs
index efeb6306e1..5f2fdedba9 100644
--- a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers_DesignTime.codegen.cs
+++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers_DesignTime.codegen.cs
@@ -7,6 +7,7 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests.TestFiles
{
private global::TestNamespace.CatchAllTagHelper __TestNamespace_CatchAllTagHelper;
private global::TestNamespace.InputTagHelper __TestNamespace_InputTagHelper;
+ private global::DivTagHelper __DivTagHelper;
#pragma warning disable 219
private void __RazorDirectiveTokenHelpers__() {
((System.Action)(() => {
@@ -32,6 +33,10 @@ global::System.Object __typeHelper = "*, TestAssembly";
__TestNamespace_InputTagHelper = CreateTagHelper();
__TestNamespace_CatchAllTagHelper = CreateTagHelper();
__TestNamespace_InputTagHelper.BoundRequiredString = "world";
+ __DivTagHelper = CreateTagHelper();
+ __DivTagHelper.BoundBoolProp = true;
+ __DivTagHelper.BoolDictProp["key"] = true;
+ __DivTagHelper = CreateTagHelper();
__TestNamespace_CatchAllTagHelper = CreateTagHelper();
}
#pragma warning restore 1998
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers_DesignTime.ir.txt b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers_DesignTime.ir.txt
index e6c20c0e02..a16799bfa8 100644
--- a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers_DesignTime.ir.txt
+++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers_DesignTime.ir.txt
@@ -4,6 +4,7 @@ Document -
DefaultTagHelperRuntime -
FieldDeclaration - - private - global::TestNamespace.CatchAllTagHelper - __TestNamespace_CatchAllTagHelper
FieldDeclaration - - private - global::TestNamespace.InputTagHelper - __TestNamespace_InputTagHelper
+ FieldDeclaration - - private - global::DivTagHelper - __DivTagHelper
DesignTimeDirective -
DirectiveToken - (14:0,14 [17] MinimizedTagHelpers.cshtml) - "*, TestAssembly"
CSharpCode -
@@ -15,7 +16,7 @@ Document -
MethodDeclaration - - public async - System.Threading.Tasks.Task - ExecuteAsync
HtmlContent - (31:0,31 [4] MinimizedTagHelpers.cshtml)
IntermediateToken - (31:0,31 [4] MinimizedTagHelpers.cshtml) - Html - \n\n
- TagHelper - (35:2,0 [647] MinimizedTagHelpers.cshtml) - p - TagMode.StartTagAndEndTag
+ TagHelper - (35:2,0 [762] MinimizedTagHelpers.cshtml) - p - TagMode.StartTagAndEndTag
DefaultTagHelperBody -
HtmlContent - (64:2,29 [34] MinimizedTagHelpers.cshtml)
IntermediateToken - (64:2,29 [6] MinimizedTagHelpers.cshtml) - Html - \n
@@ -84,8 +85,24 @@ Document -
HtmlContent - (667:16,40 [5] MinimizedTagHelpers.cshtml)
IntermediateToken - (667:16,40 [5] MinimizedTagHelpers.cshtml) - Html - world
DefaultTagHelperExecute -
- HtmlContent - (676:16,49 [2] MinimizedTagHelpers.cshtml)
- IntermediateToken - (676:16,49 [2] MinimizedTagHelpers.cshtml) - Html - \n
+ HtmlContent - (676:16,49 [6] MinimizedTagHelpers.cshtml)
+ IntermediateToken - (676:16,49 [6] MinimizedTagHelpers.cshtml) - Html - \n
+ TagHelper - (682:17,4 [41] MinimizedTagHelpers.cshtml) - div - TagMode.StartTagAndEndTag
+ DefaultTagHelperBody -
+ DefaultTagHelperCreate - - DivTagHelper
+ DefaultTagHelperProperty - - boundbool - bool DivTagHelper.BoundBoolProp - HtmlAttributeValueStyle.Minimized
+ DefaultTagHelperProperty - - booldict-prefix-key - System.Collections.Generic.IDictionary DivTagHelper.BoolDictProp - HtmlAttributeValueStyle.Minimized
+ DefaultTagHelperExecute -
+ HtmlContent - (723:17,45 [6] MinimizedTagHelpers.cshtml)
+ IntermediateToken - (723:17,45 [6] MinimizedTagHelpers.cshtml) - Html - \n
+ TagHelper - (729:18,4 [62] MinimizedTagHelpers.cshtml) - div - TagMode.StartTagAndEndTag
+ DefaultTagHelperBody -
+ HtmlContent - (734:18,9 [51] MinimizedTagHelpers.cshtml)
+ IntermediateToken - (734:18,9 [51] MinimizedTagHelpers.cshtml) - Html - Tag helper with unmatched bound boolean attributes.
+ DefaultTagHelperCreate - - DivTagHelper
+ DefaultTagHelperExecute -
+ HtmlContent - (791:18,66 [2] MinimizedTagHelpers.cshtml)
+ IntermediateToken - (791:18,66 [2] MinimizedTagHelpers.cshtml) - Html - \n
DefaultTagHelperCreate - - TestNamespace.CatchAllTagHelper
DefaultTagHelperHtmlAttribute - - catchall-unbound-required - HtmlAttributeValueStyle.Minimized
DefaultTagHelperExecute -
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers_DesignTime.mappings.txt b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers_DesignTime.mappings.txt
index 20dc2225f1..cb760ffd40 100644
--- a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers_DesignTime.mappings.txt
+++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers_DesignTime.mappings.txt
@@ -1,5 +1,5 @@
Source Location: (14:0,14 [17] TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers.cshtml)
|"*, TestAssembly"|
-Generated Location: (603:12,37 [17] )
+Generated Location: (657:13,37 [17] )
|"*, TestAssembly"|
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers_Runtime.codegen.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers_Runtime.codegen.cs
index eae84e8b3d..ce9b852bf4 100644
--- a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers_Runtime.codegen.cs
+++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers_Runtime.codegen.cs
@@ -1,4 +1,4 @@
-#pragma checksum "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "cc93b363a32adf077cc406265e403db466e4ae7d"
+#pragma checksum "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "ab8c9a5af38d07138a55ae82942b4a97fe3c9025"
//
#pragma warning disable 1591
namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests.TestFiles
@@ -33,6 +33,7 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests.TestFiles
}
private global::TestNamespace.CatchAllTagHelper __TestNamespace_CatchAllTagHelper;
private global::TestNamespace.InputTagHelper __TestNamespace_InputTagHelper;
+ private global::DivTagHelper __DivTagHelper;
#pragma warning disable 1998
public async System.Threading.Tasks.Task ExecuteAsync()
{
@@ -128,6 +129,41 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests.TestFiles
}
Write(__tagHelperExecutionContext.Output);
__tagHelperExecutionContext = __tagHelperScopeManager.End();
+ WriteLiteral("\r\n ");
+ __tagHelperExecutionContext = __tagHelperScopeManager.Begin("div", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.StartTagAndEndTag, "test", async() => {
+ }
+ );
+ __DivTagHelper = CreateTagHelper();
+ __tagHelperExecutionContext.Add(__DivTagHelper);
+ __DivTagHelper.BoundBoolProp = true;
+ __tagHelperExecutionContext.AddTagHelperAttribute("boundbool", __DivTagHelper.BoundBoolProp, global::Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeValueStyle.Minimized);
+ if (__DivTagHelper.BoolDictProp == null)
+ {
+ throw new InvalidOperationException(InvalidTagHelperIndexerAssignment("booldict-prefix-key", "DivTagHelper", "BoolDictProp"));
+ }
+ __DivTagHelper.BoolDictProp["key"] = true;
+ __tagHelperExecutionContext.AddTagHelperAttribute("booldict-prefix-key", __DivTagHelper.BoolDictProp["key"], global::Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeValueStyle.Minimized);
+ await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
+ if (!__tagHelperExecutionContext.Output.IsContentModified)
+ {
+ await __tagHelperExecutionContext.SetOutputContentAsync();
+ }
+ Write(__tagHelperExecutionContext.Output);
+ __tagHelperExecutionContext = __tagHelperScopeManager.End();
+ WriteLiteral("\r\n ");
+ __tagHelperExecutionContext = __tagHelperScopeManager.Begin("div", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.StartTagAndEndTag, "test", async() => {
+ WriteLiteral("Tag helper with unmatched bound boolean attributes.");
+ }
+ );
+ __DivTagHelper = CreateTagHelper();
+ __tagHelperExecutionContext.Add(__DivTagHelper);
+ await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
+ if (!__tagHelperExecutionContext.Output.IsContentModified)
+ {
+ await __tagHelperExecutionContext.SetOutputContentAsync();
+ }
+ Write(__tagHelperExecutionContext.Output);
+ __tagHelperExecutionContext = __tagHelperScopeManager.End();
WriteLiteral("\r\n");
}
);
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers_Runtime.ir.txt b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers_Runtime.ir.txt
index 4f9ebb2b10..e27c841eb8 100644
--- a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers_Runtime.ir.txt
+++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/MinimizedTagHelpers_Runtime.ir.txt
@@ -11,10 +11,11 @@ Document -
DefaultTagHelperRuntime -
FieldDeclaration - - private - global::TestNamespace.CatchAllTagHelper - __TestNamespace_CatchAllTagHelper
FieldDeclaration - - private - global::TestNamespace.InputTagHelper - __TestNamespace_InputTagHelper
+ FieldDeclaration - - private - global::DivTagHelper - __DivTagHelper
MethodDeclaration - - public async - System.Threading.Tasks.Task - ExecuteAsync
HtmlContent - (33:1,0 [2] MinimizedTagHelpers.cshtml)
IntermediateToken - (33:1,0 [2] MinimizedTagHelpers.cshtml) - Html - \n
- TagHelper - (35:2,0 [647] MinimizedTagHelpers.cshtml) - p - TagMode.StartTagAndEndTag
+ TagHelper - (35:2,0 [762] MinimizedTagHelpers.cshtml) - p - TagMode.StartTagAndEndTag
DefaultTagHelperBody -
HtmlContent - (64:2,29 [34] MinimizedTagHelpers.cshtml)
IntermediateToken - (64:2,29 [6] MinimizedTagHelpers.cshtml) - Html - \n
@@ -63,8 +64,24 @@ Document -
DefaultTagHelperHtmlAttribute - - catchall-unbound-required - HtmlAttributeValueStyle.Minimized
PreallocatedTagHelperProperty - (667:16,40 [5] MinimizedTagHelpers.cshtml) - __tagHelperAttribute_6 - input-bound-required-string - BoundRequiredString
DefaultTagHelperExecute -
- HtmlContent - (676:16,49 [2] MinimizedTagHelpers.cshtml)
- IntermediateToken - (676:16,49 [2] MinimizedTagHelpers.cshtml) - Html - \n
+ HtmlContent - (676:16,49 [6] MinimizedTagHelpers.cshtml)
+ IntermediateToken - (676:16,49 [6] MinimizedTagHelpers.cshtml) - Html - \n
+ TagHelper - (682:17,4 [41] MinimizedTagHelpers.cshtml) - div - TagMode.StartTagAndEndTag
+ DefaultTagHelperBody -
+ DefaultTagHelperCreate - - DivTagHelper
+ DefaultTagHelperProperty - - boundbool - bool DivTagHelper.BoundBoolProp - HtmlAttributeValueStyle.Minimized
+ DefaultTagHelperProperty - - booldict-prefix-key - System.Collections.Generic.IDictionary DivTagHelper.BoolDictProp - HtmlAttributeValueStyle.Minimized
+ DefaultTagHelperExecute -
+ HtmlContent - (723:17,45 [6] MinimizedTagHelpers.cshtml)
+ IntermediateToken - (723:17,45 [6] MinimizedTagHelpers.cshtml) - Html - \n
+ TagHelper - (729:18,4 [62] MinimizedTagHelpers.cshtml) - div - TagMode.StartTagAndEndTag
+ DefaultTagHelperBody -
+ HtmlContent - (734:18,9 [51] MinimizedTagHelpers.cshtml)
+ IntermediateToken - (734:18,9 [51] MinimizedTagHelpers.cshtml) - Html - Tag helper with unmatched bound boolean attributes.
+ DefaultTagHelperCreate - - DivTagHelper
+ DefaultTagHelperExecute -
+ HtmlContent - (791:18,66 [2] MinimizedTagHelpers.cshtml)
+ IntermediateToken - (791:18,66 [2] MinimizedTagHelpers.cshtml) - Html - \n
DefaultTagHelperCreate - - TestNamespace.CatchAllTagHelper
DefaultTagHelperHtmlAttribute - - catchall-unbound-required - HtmlAttributeValueStyle.Minimized
DefaultTagHelperExecute -