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 -