Add AllowedChildTagDescriptor.

- Changed the `AllowedChildTags` collection on `TagHelperDescriptor` to have a custom object type to represent child tags.
- Created comparers and builders to work with the child tag descriptor.
- Removed the validation methods on `TagHelperDescriptorBuilder` since there's no longer any bits to validate (they're contained within the sub-properties).
- Unit tested the `DisplayName`.

#1493
This commit is contained in:
N. Taylor Mullen 2017-06-30 16:18:33 -07:00
parent a7cc63d6e1
commit 4654997201
17 changed files with 341 additions and 70 deletions

View File

@ -0,0 +1,45 @@
// 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.Diagnostics;
using System.Linq;
namespace Microsoft.AspNetCore.Razor.Language
{
[DebuggerDisplay("{DisplayName,nq}")]
public abstract class AllowedChildTagDescriptor : IEquatable<AllowedChildTagDescriptor>
{
public string Name { get; protected set; }
public string DisplayName { get; protected set; }
public IReadOnlyList<RazorDiagnostic> Diagnostics { get; protected set; }
public bool HasErrors
{
get
{
var errors = Diagnostics.Any(diagnostic => diagnostic.Severity == RazorDiagnosticSeverity.Error);
return errors;
}
}
public bool Equals(AllowedChildTagDescriptor other)
{
return AllowedChildTagDescriptorComparer.Default.Equals(this, other);
}
public override bool Equals(object obj)
{
return Equals(obj as AllowedChildTagDescriptor);
}
public override int GetHashCode()
{
return AllowedChildTagDescriptorComparer.Default.GetHashCode(this);
}
}
}

View File

@ -0,0 +1,17 @@
// 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
{
public abstract class AllowedChildTagDescriptorBuilder
{
public abstract string Name { get; set; }
public abstract string DisplayName { get; set; }
public abstract RazorDiagnosticCollection Diagnostics { get; }
}
}

View File

@ -0,0 +1,73 @@
// 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;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Razor.Language
{
internal class AllowedChildTagDescriptorComparer : IEqualityComparer<AllowedChildTagDescriptor>
{
/// <summary>
/// A default instance of the <see cref="AllowedChildTagDescriptorComparer"/>.
/// </summary>
public static readonly AllowedChildTagDescriptorComparer Default =
new AllowedChildTagDescriptorComparer();
/// <summary>
/// A default instance of the <see cref="AllowedChildTagDescriptorComparer"/> that does case-sensitive comparison.
/// </summary>
internal static readonly AllowedChildTagDescriptorComparer CaseSensitive =
new AllowedChildTagDescriptorComparer(caseSensitive: true);
private readonly StringComparer _stringComparer;
private readonly StringComparison _stringComparison;
private AllowedChildTagDescriptorComparer(bool caseSensitive = false)
{
if (caseSensitive)
{
_stringComparer = StringComparer.Ordinal;
_stringComparison = StringComparison.Ordinal;
}
else
{
_stringComparer = StringComparer.OrdinalIgnoreCase;
_stringComparison = StringComparison.OrdinalIgnoreCase;
}
}
/// <inheritdoc />
public virtual bool Equals(
AllowedChildTagDescriptor descriptorX,
AllowedChildTagDescriptor descriptorY)
{
if (object.ReferenceEquals(descriptorX, descriptorY))
{
return true;
}
if (descriptorX == null ^ descriptorY == null)
{
return false;
}
return descriptorX != null &&
string.Equals(descriptorX.Name, descriptorY.Name, _stringComparison) &&
string.Equals(descriptorX.DisplayName, descriptorY.DisplayName, StringComparison.Ordinal) &&
Enumerable.SequenceEqual(descriptorX.Diagnostics, descriptorY.Diagnostics);
}
/// <inheritdoc />
public virtual int GetHashCode(AllowedChildTagDescriptor descriptor)
{
var hashCodeCombiner = HashCodeCombiner.Start();
hashCodeCombiner.Add(descriptor.Name, _stringComparer);
hashCodeCombiner.Add(descriptor.DisplayName, StringComparer.Ordinal);
return hashCodeCombiner.CombinedHash;
}
}
}

View File

@ -0,0 +1,15 @@
// 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
{
internal class DefaultAllowedChildTagDescriptor : AllowedChildTagDescriptor
{
public DefaultAllowedChildTagDescriptor(string name, string displayName, RazorDiagnostic[] diagnostics)
{
Name = name;
DisplayName = displayName;
Diagnostics = diagnostics;
}
}
}

View File

@ -0,0 +1,77 @@
// 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;
namespace Microsoft.AspNetCore.Razor.Language
{
internal class DefaultAllowedChildTagDescriptorBuilder : AllowedChildTagDescriptorBuilder
{
private readonly DefaultTagHelperDescriptorBuilder _parent;
private DefaultRazorDiagnosticCollection _diagnostics;
public DefaultAllowedChildTagDescriptorBuilder(DefaultTagHelperDescriptorBuilder parent)
{
_parent = parent;
}
public override string Name { get; set; }
public override string DisplayName { get; set; }
public override RazorDiagnosticCollection Diagnostics
{
get
{
if (_diagnostics == null)
{
_diagnostics = new DefaultRazorDiagnosticCollection();
}
return _diagnostics;
}
}
public AllowedChildTagDescriptor Build()
{
var validationDiagnostics = Validate();
var diagnostics = new HashSet<RazorDiagnostic>(validationDiagnostics);
if (_diagnostics != null)
{
diagnostics.UnionWith(_diagnostics);
}
var displayName = DisplayName ?? Name;
var descriptor = new DefaultAllowedChildTagDescriptor(
Name,
displayName,
diagnostics?.ToArray() ?? Array.Empty<RazorDiagnostic>());
return descriptor;
}
private IEnumerable<RazorDiagnostic> Validate()
{
if (string.IsNullOrWhiteSpace(Name))
{
var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidRestrictedChildNullOrWhitespace(_parent.GetDisplayName());
yield return diagnostic;
}
else if (Name != TagHelperMatchingConventions.ElementCatchAllName)
{
foreach (var character in Name)
{
if (char.IsWhiteSpace(character) || HtmlConventions.InvalidNonWhitespaceHtmlCharacters.Contains(character))
{
var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidRestrictedChild(_parent.GetDisplayName(), Name, character);
yield return diagnostic;
}
}
}
}
}
}

View File

@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Razor.Language
string tagOutputHint,
TagMatchingRuleDescriptor[] tagMatchingRules,
BoundAttributeDescriptor[] attributeDescriptors,
string[] allowedChildTags,
AllowedChildTagDescriptor[] allowedChildTags,
Dictionary<string, string> metadata,
RazorDiagnostic[] diagnostics)
: base(kind)

View File

@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Razor.Language
// Required values
private readonly Dictionary<string, string> _metadata;
private HashSet<string> _allowedChildTags;
private List<DefaultAllowedChildTagDescriptorBuilder> _allowedChildTags;
private List<DefaultBoundAttributeDescriptorBuilder> _attributeBuilders;
private List<DefaultTagMatchingRuleDescriptorBuilder> _tagMatchingRuleBuilders;
private DefaultRazorDiagnosticCollection _diagnostics;
@ -38,19 +38,6 @@ namespace Microsoft.AspNetCore.Razor.Language
public override string DisplayName { get; set; }
public override ICollection<string> AllowedChildTags
{
get
{
if (_allowedChildTags == null)
{
_allowedChildTags = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
}
return _allowedChildTags;
}
}
public override string TagOutputHint { get; set; }
public override string Documentation { get; set; }
@ -70,6 +57,16 @@ namespace Microsoft.AspNetCore.Razor.Language
}
}
public override IReadOnlyList<AllowedChildTagDescriptorBuilder> AllowedChildTags
{
get
{
EnsureAllowedChildTags();
return _allowedChildTags;
}
}
public override IReadOnlyList<BoundAttributeDescriptorBuilder> BoundAttributes
{
get
@ -90,6 +87,20 @@ namespace Microsoft.AspNetCore.Razor.Language
}
}
public override void AllowChildTag(Action<AllowedChildTagDescriptorBuilder> configure)
{
if (configure == null)
{
throw new ArgumentNullException(nameof(configure));
}
EnsureAllowedChildTags();
var builder = new DefaultAllowedChildTagDescriptorBuilder(this);
configure(builder);
_allowedChildTags.Add(builder);
}
public override void BindAttribute(Action<BoundAttributeDescriptorBuilder> configure)
{
if (configure == null)
@ -120,13 +131,24 @@ namespace Microsoft.AspNetCore.Razor.Language
public override TagHelperDescriptor Build()
{
var validationDiagnostics = Validate();
var diagnostics = new HashSet<RazorDiagnostic>(validationDiagnostics);
var diagnostics = new HashSet<RazorDiagnostic>();
if (_diagnostics != null)
{
diagnostics.UnionWith(_diagnostics);
}
var allowedChildTags = Array.Empty<AllowedChildTagDescriptor>();
if (_allowedChildTags != null)
{
var allowedChildTagsSet = new HashSet<AllowedChildTagDescriptor>(AllowedChildTagDescriptorComparer.Default);
for (var i = 0; i < _allowedChildTags.Count; i++)
{
allowedChildTagsSet.Add(_allowedChildTags[i].Build());
}
allowedChildTags = allowedChildTagsSet.ToArray();
}
var tagMatchingRules = Array.Empty<TagMatchingRuleDescriptor>();
if (_tagMatchingRuleBuilders != null)
{
@ -160,7 +182,7 @@ namespace Microsoft.AspNetCore.Razor.Language
TagOutputHint,
tagMatchingRules,
attributes,
_allowedChildTags?.ToArray() ?? Array.Empty<string>(),
allowedChildTags,
new Dictionary<string, string>(_metadata),
diagnostics.ToArray());
@ -188,31 +210,11 @@ namespace Microsoft.AspNetCore.Razor.Language
return this.GetTypeName() ?? Name;
}
private IEnumerable<RazorDiagnostic> Validate()
private void EnsureAllowedChildTags()
{
if (_allowedChildTags != null)
if (_allowedChildTags == null)
{
foreach (var name in _allowedChildTags)
{
if (string.IsNullOrWhiteSpace(name))
{
var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidRestrictedChildNullOrWhitespace(GetDisplayName());
yield return diagnostic;
}
else if (name != TagHelperMatchingConventions.ElementCatchAllName)
{
foreach (var character in name)
{
if (char.IsWhiteSpace(character) || HtmlConventions.InvalidNonWhitespaceHtmlCharacters.Contains(character))
{
var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidRestrictedChild(GetDisplayName(), name, character);
yield return diagnostic;
}
}
}
}
_allowedChildTags = new List<DefaultAllowedChildTagDescriptorBuilder>();
}
}

View File

@ -855,7 +855,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
AllowedChildren = Builder.BindingResult.Descriptors
.Where(descriptor => descriptor.AllowedChildTags != null)
.SelectMany(descriptor => descriptor.AllowedChildTags)
.SelectMany(descriptor => descriptor.AllowedChildTags.Select(childTag => childTag.Name))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}

View File

@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Razor.Language
public IEnumerable<BoundAttributeDescriptor> BoundAttributes { get; protected set; }
public IEnumerable<string> AllowedChildTags { get; protected set; }
public IEnumerable<AllowedChildTagDescriptor> AllowedChildTags { get; protected set; }
public string Documentation { get; protected set; }
@ -55,9 +55,13 @@ namespace Microsoft.AspNetCore.Razor.Language
{
if (_allDiagnostics == null)
{
var allowedChildTagDiagnostics = AllowedChildTags.SelectMany(childTag => childTag.Diagnostics);
var attributeDiagnostics = BoundAttributes.SelectMany(attribute => attribute.Diagnostics);
var ruleDiagnostics = TagMatchingRules.SelectMany(rule => rule.GetAllDiagnostics());
var combinedDiagnostics = attributeDiagnostics.Concat(ruleDiagnostics).Concat(Diagnostics);
var combinedDiagnostics = allowedChildTagDiagnostics
.Concat(attributeDiagnostics)
.Concat(ruleDiagnostics)
.Concat(Diagnostics);
_allDiagnostics = combinedDiagnostics.ToArray();
}

View File

@ -54,16 +54,18 @@ namespace Microsoft.AspNetCore.Razor.Language
public abstract string Documentation { get; set; }
public abstract ICollection<string> AllowedChildTags { get; }
public abstract IDictionary<string, string> Metadata { get; }
public abstract RazorDiagnosticCollection Diagnostics { get; }
public abstract IReadOnlyList<AllowedChildTagDescriptorBuilder> AllowedChildTags { get; }
public abstract IReadOnlyList<BoundAttributeDescriptorBuilder> BoundAttributes { get; }
public abstract IReadOnlyList<TagMatchingRuleDescriptorBuilder> TagMatchingRules { get; }
public abstract void AllowChildTag(Action<AllowedChildTagDescriptorBuilder> configure);
public abstract void BindAttribute(Action<BoundAttributeDescriptorBuilder> configure);
public abstract void TagMatchingRule(Action<TagMatchingRuleDescriptorBuilder> configure);

View File

@ -23,6 +23,7 @@ namespace Microsoft.AspNetCore.Razor.Language
private readonly StringComparer _stringComparer;
private readonly StringComparison _stringComparison;
private readonly AllowedChildTagDescriptorComparer _AllowedChildTagDescriptorComparer;
private readonly BoundAttributeDescriptorComparer _boundAttributeComparer;
private readonly TagMatchingRuleDescriptorComparer _tagMatchingRuleComparer;
@ -32,6 +33,7 @@ namespace Microsoft.AspNetCore.Razor.Language
{
_stringComparer = StringComparer.Ordinal;
_stringComparison = StringComparison.Ordinal;
_AllowedChildTagDescriptorComparer = AllowedChildTagDescriptorComparer.CaseSensitive;
_boundAttributeComparer = BoundAttributeDescriptorComparer.CaseSensitive;
_tagMatchingRuleComparer = TagMatchingRuleDescriptorComparer.CaseSensitive;
}
@ -39,6 +41,7 @@ namespace Microsoft.AspNetCore.Razor.Language
{
_stringComparer = StringComparer.OrdinalIgnoreCase;
_stringComparison = StringComparison.OrdinalIgnoreCase;
_AllowedChildTagDescriptorComparer = AllowedChildTagDescriptorComparer.Default;
_boundAttributeComparer = BoundAttributeDescriptorComparer.Default;
_tagMatchingRuleComparer = TagMatchingRuleDescriptorComparer.Default;
}
@ -71,9 +74,9 @@ namespace Microsoft.AspNetCore.Razor.Language
(descriptorX.AllowedChildTags != null &&
descriptorY.AllowedChildTags != null &&
Enumerable.SequenceEqual(
descriptorX.AllowedChildTags.OrderBy(child => child, _stringComparer),
descriptorY.AllowedChildTags.OrderBy(child => child, _stringComparer),
_stringComparer))) &&
descriptorX.AllowedChildTags.OrderBy(childTag => childTag.Name, _stringComparer),
descriptorY.AllowedChildTags.OrderBy(childTag => childTag.Name, _stringComparer),
_AllowedChildTagDescriptorComparer))) &&
string.Equals(descriptorX.Documentation, descriptorY.Documentation, StringComparison.Ordinal) &&
string.Equals(descriptorX.DisplayName, descriptorY.DisplayName, StringComparison.Ordinal) &&
string.Equals(descriptorX.TagOutputHint, descriptorY.TagOutputHint, _stringComparison) &&
@ -95,6 +98,12 @@ namespace Microsoft.AspNetCore.Razor.Language
hashCodeCombiner.Add(descriptor.Kind);
hashCodeCombiner.Add(descriptor.AssemblyName, StringComparer.Ordinal);
var childTags = descriptor.AllowedChildTags.OrderBy(childTag => childTag.Name, _stringComparer);
foreach (var childTag in childTags)
{
hashCodeCombiner.Add(_AllowedChildTagDescriptorComparer.GetHashCode(childTag));
}
var boundAttributes = descriptor.BoundAttributes.OrderBy(attribute => attribute.Name, _stringComparer);
foreach (var attribute in boundAttributes)
{
@ -111,15 +120,6 @@ namespace Microsoft.AspNetCore.Razor.Language
hashCodeCombiner.Add(descriptor.DisplayName, StringComparer.Ordinal);
hashCodeCombiner.Add(descriptor.TagOutputHint, _stringComparer);
if (descriptor.AllowedChildTags != null)
{
var allowedChildren = descriptor.AllowedChildTags.OrderBy(child => child, _stringComparer);
foreach (var child in allowedChildren)
{
hashCodeCombiner.Add(child, _stringComparer);
}
}
return hashCodeCombiner.CombinedHash;
}
}

View File

@ -141,13 +141,13 @@ namespace Microsoft.CodeAnalysis.Razor
return;
}
builder.AllowedChildTags.Add((string)restrictChildrenAttribute.ConstructorArguments[0].Value);
builder.AllowChildTag(childTagBuilder => childTagBuilder.Name = (string)restrictChildrenAttribute.ConstructorArguments[0].Value);
if (restrictChildrenAttribute.ConstructorArguments.Length == 2)
{
foreach (var value in restrictChildrenAttribute.ConstructorArguments[1].Values)
{
builder.AllowedChildTags.Add((string)value.Value);
builder.AllowChildTag(childTagBuilder => childTagBuilder.Name = (string)value.Value);
}
}
}

View File

@ -244,14 +244,9 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
foreach (var descriptor in binding.Descriptors)
{
if (descriptor.AllowedChildTags == null)
{
continue;
}
foreach (var childTag in descriptor.AllowedChildTags)
{
var prefixedName = string.Concat(prefix, childTag);
var prefixedName = string.Concat(prefix, childTag.Name);
var descriptors = _tagHelperFactsService.GetTagHelpersGivenTag(
completionContext.DocumentContext,
prefixedName,

View File

@ -58,8 +58,8 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
foreach (var childTag in childTags)
{
var tagValue = childTag.Value<string>();
builder.AllowedChildTags.Add(tagValue);
var tag = childTag.Value<JObject>();
builder.AllowChildTag(childTagBuilder => ReadAllowedChildTag(childTagBuilder, tag, serializer));
}
foreach (var diagnostic in diagnostics)
@ -133,6 +133,23 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
}
}
private void ReadAllowedChildTag(AllowedChildTagDescriptorBuilder builder, JObject childTag, JsonSerializer serializer)
{
var name = childTag[nameof(AllowedChildTagDescriptor.Name)].Value<string>();
var displayName = childTag[nameof(AllowedChildTagDescriptor.DisplayName)].Value<string>();
var diagnostics = childTag[nameof(AllowedChildTagDescriptor.Diagnostics)].Value<JArray>();
builder.Name = name;
builder.DisplayName = displayName;
foreach (var diagnostic in diagnostics)
{
var diagnosticReader = diagnostic.CreateReader();
var diagnosticObject = serializer.Deserialize<RazorDiagnostic>(diagnosticReader);
builder.Diagnostics.Add(diagnosticObject);
}
}
private void ReadBoundAttribute(BoundAttributeDescriptorBuilder builder, JObject attribute, JsonSerializer serializer)
{
var descriptorKind = attribute[nameof(BoundAttributeDescriptor.Kind)].Value<string>();

View File

@ -0,0 +1,24 @@
// 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 Xunit;
namespace Microsoft.AspNetCore.Razor.Language
{
public class DefaultAllowedChildTagDescriptorBuilderTest
{
[Fact]
public void Build_DisplayNameIsName()
{
// Arrange
var builder = new DefaultAllowedChildTagDescriptorBuilder(null);
builder.Name = "foo";
// Act
var descriptor = builder.Build();
// Assert
Assert.Equal("foo", descriptor.DisplayName);
}
}
}

View File

@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Razor.Language
throw new ArgumentNullException(nameof(builder));
}
builder.AllowedChildTags.Add(allowedChild);
builder.AllowChildTag(childTagBuilder => childTagBuilder.Name = allowedChild);
return builder;
}

View File

@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Razor.Language
throw new ArgumentNullException(nameof(builder));
}
builder.AllowedChildTags.Add(allowedChild);
builder.AllowChildTag(childTagBuilder => childTagBuilder.Name = allowedChild);
return builder;
}