public void OnError(RazorError error) { EnusreNotTerminated(); AssertOnOwnerTask(); _errorSink.OnError(error); }
private TagHelperRequiredAttributeValueComparison? ParseCssValueComparison(ErrorSink errorSink) { Debug.Assert(!AtEnd); TagHelperRequiredAttributeValueComparison valueComparison; if (CssValueComparisons.TryGetValue(Current, out valueComparison)) { var op = Current; _index++; if (op != '=' && At('=')) { // Two length operator (ex: ^=). Move past the second piece _index++; } else if (op != '=') // We're at an incomplete operator (ex: [foo^] { errorSink.OnError( SourceLocation.Zero, Resources.FormatTagHelperDescriptorFactory_PartialRequiredAttributeOperator(_requiredAttributes, op), length: 0); return null; } } else if (!At(']')) { errorSink.OnError( SourceLocation.Zero, Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeOperator(Current, _requiredAttributes), length: 0); return null; } return valueComparison; }
private static bool TryValidateName( string name, string whitespaceError, Func <char, string> characterErrorBuilder, ErrorSink errorSink) { var validName = true; if (string.IsNullOrWhiteSpace(name)) { errorSink.OnError(SourceLocation.Zero, whitespaceError, length: 0); validName = false; } else { foreach (var character in name) { if (char.IsWhiteSpace(character) || InvalidNonWhitespaceNameCharacters.Contains(character)) { var error = characterErrorBuilder(character); errorSink.OnError(SourceLocation.Zero, error, length: 0); validName = false; } } } return(validName); }
private static bool ValidateName( string name, bool targetingAttributes, ErrorSink errorSink) { if (!targetingAttributes && string.Equals( name, TagHelperDescriptorProvider.ElementCatchAllTarget, StringComparison.OrdinalIgnoreCase)) { // '*' as the entire name is OK in the TargetElement catch-all case. return(true); } else if (targetingAttributes && name.EndsWith( TagHelperDescriptorProvider.RequiredAttributeWildcardSuffix, StringComparison.OrdinalIgnoreCase)) { // A single '*' at the end of a required attribute is valid; everywhere else is invalid. Strip it from // the end so we can validate the rest of the name. name = name.Substring(0, name.Length - 1); } var targetName = targetingAttributes ? Resources.TagHelperDescriptorFactory_Attribute : Resources.TagHelperDescriptorFactory_Tag; var validName = true; if (string.IsNullOrWhiteSpace(name)) { errorSink.OnError( SourceLocation.Zero, Resources.FormatTargetElementAttribute_NameCannotBeNullOrWhitespace(targetName)); validName = false; } else { foreach (var character in name) { if (char.IsWhiteSpace(character) || InvalidNonWhitespaceNameCharacters.Contains(character)) { errorSink.OnError( SourceLocation.Zero, Resources.FormatTargetElementAttribute_InvalidName( targetName.ToLower(), name, character)); validName = false; } } } return(validName); }
private static bool EnsureValidPrefix( string prefix, SourceLocation directiveLocation, ErrorSink errorSink) { foreach (var character in prefix) { // Prefixes are correlated with tag names, tag names cannot have whitespace. if (char.IsWhiteSpace(character) || TagHelperDescriptorFactory.InvalidNonWhitespaceNameCharacters.Contains(character)) { errorSink.OnError( directiveLocation, Resources.FormatTagHelperDescriptorResolver_InvalidTagHelperPrefixValue( SyntaxConstants.CSharp.TagHelperPrefixKeyword, character, prefix), prefix.Length); return(false); } } return(true); }
private static LookupInfo GetLookupInfo( TagHelperDirectiveDescriptor directiveDescriptor, ErrorSink errorSink) { var lookupText = directiveDescriptor.DirectiveText; var lookupStrings = lookupText?.Split(new[] { ',' }); // Ensure that we have valid lookupStrings to work with. The valid format is "typeName, assemblyName" if (lookupStrings == null || lookupStrings.Any(string.IsNullOrWhiteSpace) || lookupStrings.Length != 2) { errorSink.OnError( directiveDescriptor.Location, Resources.FormatTagHelperDescriptorResolver_InvalidTagHelperLookupText(lookupText), GetErrorLength(lookupText)); return(null); } var trimmedAssemblyName = lookupStrings[1].Trim(); // + 1 is for the comma separator in the lookup text. var assemblyNameIndex = lookupStrings[0].Length + 1 + lookupStrings[1].IndexOf(trimmedAssemblyName, StringComparison.Ordinal); var assemblyNamePrefix = directiveDescriptor.DirectiveText.Substring(0, assemblyNameIndex); var assemblyNameLocation = SourceLocation.Advance(directiveDescriptor.Location, assemblyNamePrefix); return(new LookupInfo { TypePattern = lookupStrings[0].Trim(), AssemblyName = trimmedAssemblyName, AssemblyNameLocation = assemblyNameLocation, }); }
public void VisitCallsOnCompleteWhenAllNodesHaveBeenVisited() { // Arrange Mock<ParserVisitor> targetMock = new Mock<ParserVisitor>(); var root = new BlockBuilder() { Type = BlockType.Comment }.Build(); var errorSink = new ErrorSink(); errorSink.OnError(new RazorError("Foo", new SourceLocation(1, 0, 1), length: 3)); errorSink.OnError(new RazorError("Bar", new SourceLocation(2, 0, 2), length: 3)); var results = new ParserResults(root, Enumerable.Empty<TagHelperDescriptor>(), errorSink); // Act targetMock.Object.Visit(results); // Assert targetMock.Verify(v => v.OnComplete()); }
/// <summary> /// Internal for unit testing. /// </summary> internal static bool ValidateParentTagName(string parentTag, ErrorSink errorSink) { if (parentTag == null) { return true; } else if (string.IsNullOrWhiteSpace(parentTag)) { var error = Resources.FormatHtmlTargetElementAttribute_NameCannotBeNullOrWhitespace( Resources.TagHelperDescriptorFactory_ParentTag); errorSink.OnError(SourceLocation.Zero, error, length: 0); return false; } else if (!TryValidateName( parentTag, invalidCharacter => Resources.FormatHtmlTargetElementAttribute_InvalidName( Resources.TagHelperDescriptorFactory_ParentTag.ToLower(), parentTag, invalidCharacter), errorSink)) { return false; } return true; }
private static bool ValidateName(string name, bool targetingAttributes, ErrorSink errorSink) { if (!targetingAttributes && string.Equals( name, TagHelperDescriptorProvider.ElementCatchAllTarget, StringComparison.OrdinalIgnoreCase)) { // '*' as the entire name is OK in the HtmlTargetElement catch-all case. return true; } var targetName = targetingAttributes ? Resources.TagHelperDescriptorFactory_Attribute : Resources.TagHelperDescriptorFactory_Tag; if (string.IsNullOrWhiteSpace(name)) { var error = Resources.FormatHtmlTargetElementAttribute_NameCannotBeNullOrWhitespace(targetName); errorSink.OnError(SourceLocation.Zero, error, length: 0); return false; } else if (!TryValidateName( name, invalidCharacter => Resources.FormatHtmlTargetElementAttribute_InvalidName( targetName.ToLower(), name, invalidCharacter), errorSink)) { return false; } return true; }
public void VisitSendsErrorsToVisitor() { // Arrange var targetMock = new Mock <ParserVisitor>(); var root = new BlockBuilder() { Type = BlockType.Comment }.Build(); var errorSink = new ErrorSink(); var errors = new List <RazorError> { new RazorError("Foo", new SourceLocation(1, 0, 1), length: 3), new RazorError("Bar", new SourceLocation(2, 0, 2), length: 3), }; foreach (var error in errors) { errorSink.OnError(error); } var results = new ParserResults(root, Enumerable.Empty <TagHelperDescriptor>(), errorSink); // Act targetMock.Object.Visit(results); // Assert targetMock.Verify(v => v.VisitError(errors[0])); targetMock.Verify(v => v.VisitError(errors[1])); }
private static LookupInfo GetLookupInfo(TagHelperDirectiveDescriptor directiveDescriptor, ErrorSink errorSink) { var lookupText = directiveDescriptor.DirectiveText; var lookupStrings = lookupText?.Split(new[] { ',' }); // Ensure that we have valid lookupStrings to work with. Valid formats are: // "assemblyName" // "typeName, assemblyName" if (lookupStrings == null || lookupStrings.Any(string.IsNullOrWhiteSpace) || lookupStrings.Length != 2) { errorSink.OnError( directiveDescriptor.Location, Resources.FormatTagHelperDescriptorResolver_InvalidTagHelperLookupText(lookupText)); return(null); } return(new LookupInfo { TypePattern = lookupStrings[0].Trim(), AssemblyName = lookupStrings[1].Trim() }); }
// Internal for testing. internal static bool ValidateTagHelperAttributeDescriptor( TagHelperAttributeDescriptor attributeDescriptor, ITypeInfo parentType, ErrorSink errorSink) { string nameOrPrefix; if (attributeDescriptor.IsIndexer) { nameOrPrefix = Resources.TagHelperDescriptorFactory_Prefix; } else if (string.IsNullOrEmpty(attributeDescriptor.Name)) { errorSink.OnError( SourceLocation.Zero, Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameNullOrEmpty( parentType.FullName, attributeDescriptor.PropertyName), length: 0); return(false); } else { nameOrPrefix = Resources.TagHelperDescriptorFactory_Name; } return(ValidateTagHelperAttributeNameOrPrefix( attributeDescriptor.Name, parentType, attributeDescriptor.PropertyName, errorSink, nameOrPrefix)); }
/// <inheritdoc /> protected override IEnumerable <TagHelperDescriptor> GetTagHelperDescriptors( [NotNull] Block documentRoot, [NotNull] ErrorSink errorSink) { var visitor = new ViewImportsTagHelperDirectiveSpanVisitor( TagHelperDescriptorResolver, _viewImportsDirectiveDescriptors, errorSink); var descriptors = visitor.GetDescriptors(documentRoot); foreach (var descriptor in descriptors) { foreach (var attributeDescriptor in descriptor.Attributes) { if (attributeDescriptor.IsIndexer && string.Equals( attributeDescriptor.TypeName, _modelExpressionTypeName, StringComparison.Ordinal)) { errorSink.OnError(SourceLocation.Undefined, Resources.FormatMvcRazorParser_InvalidPropertyType( descriptor.TypeName, attributeDescriptor.Name, _modelExpressionTypeName)); } } } return(descriptors); }
// Internal for unit testing internal static IEnumerable<string> GetValidAllowedChildren( IEnumerable<string> allowedChildren, string tagHelperName, ErrorSink errorSink) { var validAllowedChildren = new List<string>(); foreach (var name in allowedChildren) { if (string.IsNullOrWhiteSpace(name)) { var whitespaceError = Resources.FormatTagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace( nameof(RestrictChildrenAttribute), tagHelperName); errorSink.OnError(SourceLocation.Zero, whitespaceError, length: 0); } else if (TryValidateName( name, invalidCharacter => Resources.FormatTagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName( nameof(RestrictChildrenAttribute), name, tagHelperName, invalidCharacter), errorSink)) { validAllowedChildren.Add(name); } } return validAllowedChildren; }
private static void ValidateDescriptors( IEnumerable <TagHelperDescriptor> descriptors, string tagName, Block tagBlock, ErrorSink errorSink) { // Ensure that all descriptors associated with this tag have appropriate TagStructures. Cannot have // multiple descriptors that expect different TagStructures (other than TagStructure.Unspecified). TagHelperDescriptor baseDescriptor = null; foreach (var descriptor in descriptors) { if (descriptor.TagStructure != TagStructure.Unspecified) { // Can't have a set of TagHelpers that expect different structures. if (baseDescriptor != null && baseDescriptor.TagStructure != descriptor.TagStructure) { errorSink.OnError( tagBlock.Start, RazorResources.FormatTagHelperParseTreeRewriter_InconsistentTagStructure( baseDescriptor.TypeName, descriptor.TypeName, tagName, nameof(TagHelperDescriptor.TagStructure)), tagBlock.Length); } baseDescriptor = descriptor; } } }
/// <summary> /// Locates valid <see cref="ITagHelper"/> types from the <see cref="Assembly"/> named <paramref name="name"/>. /// </summary> /// <param name="name">The name of an <see cref="Assembly"/> to search.</param> /// <param name="documentLocation">The <see cref="SourceLocation"/> of the associated /// <see cref="Parser.SyntaxTree.SyntaxTreeNode"/> responsible for the current <see cref="Resolve"/> call. /// </param> /// <param name="errorSink">The <see cref="ErrorSink"/> used to record errors found when resolving /// <see cref="ITagHelper"/> types.</param> /// <returns>An <see cref="IEnumerable{ITypeInfo}"/> of valid <see cref="ITagHelper"/> types.</returns> public IEnumerable <ITypeInfo> Resolve( string name, SourceLocation documentLocation, ErrorSink errorSink) { if (errorSink == null) { throw new ArgumentNullException(nameof(errorSink)); } if (string.IsNullOrEmpty(name)) { var errorLength = name == null ? 1 : Math.Max(name.Length, 1); errorSink.OnError( documentLocation, Resources.TagHelperTypeResolver_TagHelperAssemblyNameCannotBeEmptyOrNull, errorLength); return(Enumerable.Empty <ITypeInfo>()); } var assemblyName = new AssemblyName(name); IEnumerable <ITypeInfo> libraryTypes; try { libraryTypes = GetTopLevelExportedTypes(assemblyName); } catch (Exception ex) { errorSink.OnError( documentLocation, Resources.FormatTagHelperTypeResolver_CannotResolveTagHelperAssembly( assemblyName.Name, ex.Message), name.Length); return(Enumerable.Empty <ITypeInfo>()); } return(libraryTypes.Where(IsTagHelper)); }
public static void OnError(this ErrorSink errorSink, MappingLocation mappingLocation, string message) { var location = new SourceLocation( mappingLocation.FilePath, mappingLocation.AbsoluteIndex, mappingLocation.LineIndex, mappingLocation.CharacterIndex); errorSink.OnError(location, message, mappingLocation.ContentLength); }
/// <inheritdoc /> public IEnumerable<Type> Resolve( string name, SourceLocation documentLocation, ErrorSink errorSink) { if (errorSink == null) { throw new ArgumentNullException(nameof(errorSink)); } if (string.IsNullOrEmpty(name)) { var errorLength = name == null ? 1 : Math.Max(name.Length, 1); errorSink.OnError( documentLocation, Resources.TagHelperTypeResolver_TagHelperAssemblyNameCannotBeEmptyOrNull, errorLength); return Type.EmptyTypes; } var assemblyName = new AssemblyName(name); IEnumerable<TypeInfo> libraryTypes; try { libraryTypes = GetExportedTypes(assemblyName); } catch (Exception ex) { errorSink.OnError( documentLocation, Resources.FormatTagHelperTypeResolver_CannotResolveTagHelperAssembly( assemblyName.Name, ex.Message), name.Length); return Type.EmptyTypes; } return libraryTypes.Where(IsTagHelper).Select(t => t.AsType()); }
public void VisitCallsOnCompleteWhenAllNodesHaveBeenVisited() { // Arrange var targetMock = new Mock <ParserVisitor>(); var root = new BlockBuilder() { Type = BlockType.Comment }.Build(); var errorSink = new ErrorSink(); errorSink.OnError(new RazorError("Foo", new SourceLocation(1, 0, 1), length: 3)); errorSink.OnError(new RazorError("Bar", new SourceLocation(2, 0, 2), length: 3)); var results = new ParserResults(root, Enumerable.Empty <TagHelperDescriptor>(), errorSink); // Act targetMock.Object.Visit(results); // Assert targetMock.Verify(v => v.OnComplete()); }
public IEnumerable <Type> Resolve( string name, SourceLocation documentLocation, ErrorSink errorSink) { if (errorSink == null) { throw new ArgumentNullException(nameof(errorSink)); } if (string.IsNullOrEmpty(name)) { var errorLength = name == null ? 1 : Math.Max(name.Length, 1); errorSink.OnError( documentLocation, "Tag Helper Assembly Name Cannot Be Empty Or Null", errorLength); return(Type.EmptyTypes); } IEnumerable <TypeInfo> libraryTypes; try { libraryTypes = GetExportedTypes(name); } catch (Exception ex) { errorSink.OnError( documentLocation, $"Cannot Resolve Tag Helper Assembly: {name}, {ex.Message}", name.Length); return(Type.EmptyTypes); } return(libraryTypes.Select(a => a.AsType())); }
private static void OnAllowedChildrenTagError( TagHelperBlockTracker tracker, Block tagBlock, ErrorSink errorSink) { var tagName = GetTagName(tagBlock); var allowedChildrenString = string.Join(", ", tracker.AllowedChildren); var errorMessage = RazorResources.FormatTagHelperParseTreeRewriter_InvalidNestedTag( tagName, tracker.Builder.TagName, allowedChildrenString); errorSink.OnError(tagBlock.Start, errorMessage, tagBlock.Length); }
private bool EnsureNotAtEnd(ErrorSink errorSink) { if (AtEnd) { errorSink.OnError( SourceLocation.Zero, Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace(_requiredAttributes), length: 0); return false; } return true; }
/// <summary> /// Loads an <see cref="Assembly"/> using the given <paramref name="name"/> and resolves /// all valid <see cref="ITagHelper"/> <see cref="Type"/>s. /// </summary> /// <param name="name">The name of an <see cref="Assembly"/> to search.</param> /// <param name="documentLocation">The <see cref="SourceLocation"/> of the associated /// <see cref="Parser.SyntaxTree.SyntaxTreeNode"/> responsible for the current <see cref="Resolve"/> call. /// </param> /// <param name="errorSink">The <see cref="ErrorSink"/> used to record errors found when resolving /// <see cref="ITagHelper"/> <see cref="Type"/>s.</param> /// <returns>An <see cref="IEnumerable{Type}"/> of valid <see cref="ITagHelper"/> <see cref="Type"/>s. /// </returns> public IEnumerable <Type> Resolve(string name, SourceLocation documentLocation, [NotNull] ErrorSink errorSink) { if (string.IsNullOrEmpty(name)) { errorSink.OnError(documentLocation, Resources.TagHelperTypeResolver_TagHelperAssemblyNameCannotBeEmptyOrNull); return(Type.EmptyTypes); } var assemblyName = new AssemblyName(name); IEnumerable <TypeInfo> libraryTypes; try { libraryTypes = GetExportedTypes(assemblyName); } catch (Exception ex) { errorSink.OnError( documentLocation, Resources.FormatTagHelperTypeResolver_CannotResolveTagHelperAssembly( assemblyName.Name, ex.Message)); return(Type.EmptyTypes); } var validTagHelpers = libraryTypes.Where(IsTagHelper); // Convert from TypeInfo[] to Type[] return(validTagHelpers.Select(type => type.AsType())); }
private static void OnAllowedChildrenEndTagError( TagHelperTracker tracker, string tagName, MarkupEndTagSyntax tagBlock, ErrorSink errorSink, RazorSourceDocument source) { var allowedChildrenString = string.Join(", ", tracker.AllowedChildren); var errorStart = GetEndTagDeclarationErrorStart(tagBlock, source); errorSink.OnError( RazorDiagnosticFactory.CreateTagHelper_InvalidNestedTag( new SourceSpan(errorStart, tagName.Length), tagName, tracker.TagName, allowedChildrenString)); }
public void Compile_ReturnsFailedResultIfParseFails() { // Arrange var errorSink = new ErrorSink(); errorSink.OnError(new RazorError("some message", 1, 1, 1, 1)); var generatorResult = new GeneratorResults( new Block(new BlockBuilder { Type = BlockType.Comment }), Enumerable.Empty <TagHelperDescriptor>(), errorSink, new CodeGeneratorResult("", new LineMapping[0]), new ChunkTree()); var host = new Mock <IMvcRazorHost>(); host.Setup(h => h.GenerateCode(It.IsAny <string>(), It.IsAny <Stream>())) .Returns(generatorResult) .Verifiable(); var fileInfo = new Mock <IFileInfo>(); fileInfo.Setup(f => f.CreateReadStream()) .Returns(Stream.Null); var compiler = new Mock <ICompilationService>(MockBehavior.Strict); var relativeFileInfo = new RelativeFileInfo(fileInfo.Object, @"Views\index\home.cshtml"); var razorService = new RazorCompilationService(compiler.Object, host.Object, GetOptions()); // Act var result = razorService.Compile(relativeFileInfo); // Assert Assert.NotNull(result.CompilationFailures); Assert.Collection(result.CompilationFailures, failure => { var message = Assert.Single(failure.Messages); Assert.Equal("some message", message.Message); }); host.Verify(); }
private void ValidateParentAllowsContent(Span child, ErrorSink errorSink) { if (HasAllowedChildren()) { var content = child.Content; if (!string.IsNullOrWhiteSpace(content)) { var trimmedStart = content.TrimStart(); var whitespace = content.Substring(0, content.Length - trimmedStart.Length); var errorStart = SourceLocation.Advance(child.Start, whitespace); var length = trimmedStart.TrimEnd().Length; var allowedChildren = _currentTagHelperTracker.AllowedChildren; var allowedChildrenString = string.Join(", ", allowedChildren); errorSink.OnError( errorStart, RazorResources.FormatTagHelperParseTreeRewriter_CannotHaveNonTagContent( _currentTagHelperTracker.TagName, allowedChildrenString), length); } } }
private string ParseCssValue(ErrorSink errorSink) { int valueStart; int valueEnd; if (At('\'') || At('"')) { var quote = Current; // Move past the quote _index++; valueStart = _index; valueEnd = _requiredAttributes.IndexOf(quote, _index); if (valueEnd == -1) { errorSink.OnError( SourceLocation.Zero, Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeMismatchedQuotes( _requiredAttributes, quote), length: 0); return(null); } _index = valueEnd + 1; } else { valueStart = _index; var valueEndIndex = _requiredAttributes.IndexOfAny(InvalidCssQuotelessValueCharacters, _index); valueEnd = valueEndIndex == -1 ? _requiredAttributes.Length : valueEndIndex; _index = valueEnd; } var value = _requiredAttributes.Substring(valueStart, valueEnd - valueStart); return(value); }
public void Compile_ReturnsFailedResultIfParseFails() { // Arrange var errorSink = new ErrorSink(); errorSink.OnError(new RazorError("some message", 1, 1, 1, 1)); var generatorResult = new GeneratorResults( new Block(new BlockBuilder { Type = BlockType.Comment }), Enumerable.Empty<TagHelperDescriptor>(), errorSink, new CodeGeneratorResult("", new LineMapping[0]), new ChunkTree()); var host = new Mock<IMvcRazorHost>(); host.Setup(h => h.GenerateCode(It.IsAny<string>(), It.IsAny<Stream>())) .Returns(generatorResult) .Verifiable(); var fileInfo = new Mock<IFileInfo>(); fileInfo.Setup(f => f.CreateReadStream()) .Returns(Stream.Null); var compiler = new Mock<ICompilationService>(MockBehavior.Strict); var relativeFileInfo = new RelativeFileInfo(fileInfo.Object, @"Views\index\home.cshtml"); var razorService = new RazorCompilationService(compiler.Object, host.Object, GetFileProviderAccessor(), NullLoggerFactory.Instance); // Act var result = razorService.Compile(relativeFileInfo); // Assert Assert.NotNull(result.CompilationFailures); Assert.Collection(result.CompilationFailures, failure => { var message = Assert.Single(failure.Messages); Assert.Equal("some message", message.Message); }); host.Verify(); }
public void VisitSendsErrorsToVisitor() { // Arrange Mock<ParserVisitor> targetMock = new Mock<ParserVisitor>(); var root = new BlockBuilder() { Type = BlockType.Comment }.Build(); var errorSink = new ErrorSink(); List<RazorError> errors = new List<RazorError> { new RazorError("Foo", new SourceLocation(1, 0, 1), length: 3), new RazorError("Bar", new SourceLocation(2, 0, 2), length: 3), }; foreach (var error in errors) { errorSink.OnError(error); } var results = new ParserResults(root, Enumerable.Empty<TagHelperDescriptor>(), errorSink); // Act targetMock.Object.Visit(results); // Assert targetMock.Verify(v => v.VisitError(errors[0])); targetMock.Verify(v => v.VisitError(errors[1])); }
private static bool EnsureValidPrefix( string prefix, SourceLocation directiveLocation, ErrorSink errorSink) { foreach (var character in prefix) { // Prefixes are correlated with tag names, tag names cannot have whitespace. if (char.IsWhiteSpace(character) || TagHelperDescriptorFactory.InvalidNonWhitespaceNameCharacters.Contains(character)) { errorSink.OnError( directiveLocation, Resources.FormatTagHelperDescriptorResolver_InvalidTagHelperPrefixValue( SyntaxConstants.CSharp.TagHelperPrefixKeyword, character, prefix), prefix.Length); return false; } } return true; }
private static TryParseResult TryParseBlock( string tagName, Block block, IEnumerable <TagHelperDescriptor> descriptors, ErrorSink errorSink) { // TODO: Accept more than just spans: https://github.com/aspnet/Razor/issues/96. // The first child will only ever NOT be a Span if a user is doing something like: // <input @checked /> var childSpan = block.Children.First() as Span; if (childSpan == null || childSpan.Kind != SpanKind.Markup) { errorSink.OnError( block.Start, RazorResources.FormatTagHelpers_CannotHaveCSharpInTagDeclaration(tagName), block.Length); return(null); } var builder = new BlockBuilder(block); // If there's only 1 child it means that it's plain text inside of the attribute. // i.e. <div class="plain text in attribute"> if (builder.Children.Count == 1) { return(TryParseSpan(childSpan, descriptors, errorSink)); } var nameSymbols = childSpan .Symbols .OfType <HtmlSymbol>() .SkipWhile(symbol => !HtmlMarkupParser.IsValidAttributeNameSymbol(symbol)) // Skip prefix .TakeWhile(nameSymbol => HtmlMarkupParser.IsValidAttributeNameSymbol(nameSymbol)) .Select(nameSymbol => nameSymbol.Content); var name = string.Concat(nameSymbols); if (string.IsNullOrEmpty(name)) { errorSink.OnError( childSpan.Start, RazorResources.FormatTagHelpers_AttributesMustHaveAName(tagName), childSpan.Length); return(null); } // Have a name now. Able to determine correct isBoundNonStringAttribute value. var result = CreateTryParseResult(name, descriptors); var firstChild = builder.Children[0] as Span; if (firstChild != null && firstChild.Symbols[0] is HtmlSymbol) { var htmlSymbol = firstChild.Symbols[firstChild.Symbols.Count - 1] as HtmlSymbol; switch (htmlSymbol.Type) { // Treat NoQuotes and DoubleQuotes equivalently. We purposefully do not persist NoQuotes // ValueStyles at code generation time to protect users from rendering dynamic content with spaces // that can break attributes. // Ex: <tag my-attribute=@value /> where @value results in the test "hello world". // This way, the above code would render <tag my-attribute="hello world" />. case HtmlSymbolType.Equals: case HtmlSymbolType.DoubleQuote: result.AttributeValueStyle = HtmlAttributeValueStyle.DoubleQuotes; break; case HtmlSymbolType.SingleQuote: result.AttributeValueStyle = HtmlAttributeValueStyle.SingleQuotes; break; default: result.AttributeValueStyle = HtmlAttributeValueStyle.Minimized; break; } } // Remove first child i.e. foo=" builder.Children.RemoveAt(0); // Grabbing last child to check if the attribute value is quoted. var endNode = block.Children.Last(); if (!endNode.IsBlock) { var endSpan = (Span)endNode; // In some malformed cases e.g. <p bar="false', the last Span (false' in the ex.) may contain more // than a single HTML symbol. Do not ignore those other symbols. var symbolCount = endSpan.Symbols.Count(); var endSymbol = symbolCount == 1 ? (HtmlSymbol)endSpan.Symbols.First() : null; // Checking to see if it's a quoted attribute, if so we should remove end quote if (endSymbol != null && IsQuote(endSymbol)) { builder.Children.RemoveAt(builder.Children.Count - 1); } } // We need to rebuild the chunk generators of the builder and its children (this is needed to // ensure we don't do special attribute chunk generation since this is a tag helper). block = RebuildChunkGenerators(builder.Build(), result.IsBoundAttribute); // If there's only 1 child at this point its value could be a simple markup span (treated differently than // block level elements for attributes). if (block.Children.Count() == 1) { var child = block.Children.First() as Span; if (child != null) { // After pulling apart the block we just have a value span. var spanBuilder = new SpanBuilder(child); result.AttributeValueNode = CreateMarkupAttribute(spanBuilder, result.IsBoundNonStringAttribute); return(result); } } var isFirstSpan = true; result.AttributeValueNode = ConvertToMarkupAttributeBlock( block, (parentBlock, span) => { // If the attribute was requested by a tag helper but the corresponding property was not a // string, then treat its value as code. A non-string value can be any C# value so we need // to ensure the SyntaxTreeNode reflects that. if (result.IsBoundNonStringAttribute) { // For bound non-string attributes, we'll only allow a transition span to appear at the very // beginning of the attribute expression. All later transitions would appear as code so that // they are part of the generated output. E.g. // key="@value" -> MyTagHelper.key = value // key=" @value" -> MyTagHelper.key = @value // key="1 + @case" -> MyTagHelper.key = 1 + @case // key="@int + @case" -> MyTagHelper.key = int + @case // key="@(a + b) -> MyTagHelper.key = a + b // key="4 + @(a + b)" -> MyTagHelper.key = 4 + @(a + b) if (isFirstSpan && span.Kind == SpanKind.Transition) { // do nothing. } else { var spanBuilder = new SpanBuilder(span); if (parentBlock.Type == BlockType.Expression && (spanBuilder.Kind == SpanKind.Transition || spanBuilder.Kind == SpanKind.MetaCode)) { // Change to a MarkupChunkGenerator so that the '@' \ parenthesis is generated as part of the output. spanBuilder.ChunkGenerator = new MarkupChunkGenerator(); } spanBuilder.Kind = SpanKind.Code; span = spanBuilder.Build(); } } isFirstSpan = false; return(span); }); return(result); }
private static IList <TagHelperAttributeNode> GetTagAttributes( string tagName, bool validStructure, Block tagBlock, IEnumerable <TagHelperDescriptor> descriptors, ErrorSink errorSink) { // Ignore all but one descriptor per type since this method uses the TagHelperDescriptors only to get the // contained TagHelperAttributeDescriptor's. descriptors = descriptors.Distinct(TypeBasedTagHelperDescriptorComparer.Default); var attributes = new List <TagHelperAttributeNode>(); // We skip the first child "<tagname" and take everything up to the ending portion of the tag ">" or "/>". // The -2 accounts for both the start and end tags. If the tag does not have a valid structure then there's // no end tag to ignore. var symbolOffset = validStructure ? 2 : 1; var attributeChildren = tagBlock.Children.Skip(1).Take(tagBlock.Children.Count() - symbolOffset); foreach (var child in attributeChildren) { TryParseResult result; if (child.IsBlock) { result = TryParseBlock(tagName, (Block)child, descriptors, errorSink); } else { result = TryParseSpan((Span)child, descriptors, errorSink); } // Only want to track the attribute if we succeeded in parsing its corresponding Block/Span. if (result != null) { 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 && IsNullOrWhitespaceAttributeValue(result.AttributeValueNode))) { errorLocation = GetAttributeNameStartLocation(child); errorSink.OnError( errorLocation.Value, RazorResources.FormatRewriterError_EmptyTagHelperBoundAttribute( result.AttributeName, tagName, GetPropertyType(result.AttributeName, descriptors)), result.AttributeName.Length); } // Check if the attribute was a prefix match for a tag helper dictionary property but the // dictionary key would be the empty string. if (result.IsMissingDictionaryKey) { if (!errorLocation.HasValue) { errorLocation = GetAttributeNameStartLocation(child); } errorSink.OnError( errorLocation.Value, RazorResources.FormatTagHelperBlockRewriter_IndexerAttributeNameMustIncludeKey( result.AttributeName, tagName), result.AttributeName.Length); } var attributeNode = new TagHelperAttributeNode( result.AttributeName, result.AttributeValueNode, result.AttributeValueStyle); attributes.Add(attributeNode); } else { // Error occured while parsing the attribute. Don't try parsing the rest to avoid misleading errors. break; } } return(attributes); }
/// <inheritdoc /> protected override IEnumerable<TagHelperDescriptor> GetTagHelperDescriptors( Block documentRoot, ErrorSink errorSink) { if (documentRoot == null) { throw new ArgumentNullException(nameof(documentRoot)); } if (errorSink == null) { throw new ArgumentNullException(nameof(errorSink)); } var visitor = new ViewImportsTagHelperDirectiveSpanVisitor( TagHelperDescriptorResolver, _viewImportsDirectiveDescriptors, errorSink); var descriptors = visitor.GetDescriptors(documentRoot); foreach (var descriptor in descriptors) { foreach (var attributeDescriptor in descriptor.Attributes) { if (attributeDescriptor.IsIndexer && string.Equals( attributeDescriptor.TypeName, _modelExpressionTypeName, StringComparison.Ordinal)) { errorSink.OnError( SourceLocation.Undefined, Resources.FormatMvcRazorParser_InvalidPropertyType( descriptor.TypeName, attributeDescriptor.Name, _modelExpressionTypeName), length: 0); } } } return descriptors; }
private static bool ValidateTagHelperAttributeNameOrPrefix( string attributeNameOrPrefix, Type parentType, string propertyName, ErrorSink errorSink, string nameOrPrefix) { if (string.IsNullOrEmpty(attributeNameOrPrefix)) { // ValidateTagHelperAttributeDescriptor validates Name is non-null and non-empty. The empty string is // valid for DictionaryAttributePrefix and null is impossible at this point because it means "don't // create a descriptor". (Empty DictionaryAttributePrefix is a corner case which would bind every // attribute of a target element. Likely not particularly useful but unclear what minimum length // should be required and what scenarios a minimum length would break.) return true; } if (string.IsNullOrWhiteSpace(attributeNameOrPrefix)) { // Provide a single error if the entire name is whitespace, not an error per character. errorSink.OnError( SourceLocation.Zero, Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace( parentType.FullName, propertyName, nameOrPrefix)); return false; } // data-* attributes are explicitly not implemented by user agents and are not intended for use on // the server; therefore it's invalid for TagHelpers to bind to them. if (attributeNameOrPrefix.StartsWith(DataDashPrefix, StringComparison.OrdinalIgnoreCase)) { errorSink.OnError( SourceLocation.Zero, Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixStart( parentType.FullName, propertyName, nameOrPrefix, attributeNameOrPrefix, DataDashPrefix)); return false; } var isValid = true; foreach (var character in attributeNameOrPrefix) { if (char.IsWhiteSpace(character) || InvalidNonWhitespaceNameCharacters.Contains(character)) { errorSink.OnError( SourceLocation.Zero, Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter( parentType.FullName, propertyName, nameOrPrefix, attributeNameOrPrefix, character)); isValid = false; } } return isValid; }
private static IEnumerable <TagHelperAttributeDescriptor> GetAttributeDescriptors( ITypeInfo type, bool designTime, ErrorSink errorSink) { var attributeDescriptors = new List <TagHelperAttributeDescriptor>(); // Keep indexer descriptors separate to avoid sorting the combined list later. var indexerDescriptors = new List <TagHelperAttributeDescriptor>(); var accessibleProperties = type.Properties.Where(IsAccessibleProperty); foreach (var property in accessibleProperties) { if (ShouldSkipDescriptorCreation(designTime, property)) { continue; } var attributeNameAttribute = property .GetCustomAttributes <HtmlAttributeNameAttribute>() .FirstOrDefault(); var hasExplicitName = attributeNameAttribute != null && !string.IsNullOrEmpty(attributeNameAttribute.Name); var attributeName = hasExplicitName ? attributeNameAttribute.Name : ToHtmlCase(property.Name); TagHelperAttributeDescriptor mainDescriptor = null; if (property.HasPublicSetter) { mainDescriptor = ToAttributeDescriptor(property, attributeName, designTime); if (!ValidateTagHelperAttributeDescriptor(mainDescriptor, type, errorSink)) { // HtmlAttributeNameAttribute.Name is invalid. Ignore this property completely. continue; } } else if (hasExplicitName) { // Specified HtmlAttributeNameAttribute.Name though property has no public setter. errorSink.OnError( SourceLocation.Zero, Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameNotNullOrEmpty( type.FullName, property.Name, typeof(HtmlAttributeNameAttribute).FullName, nameof(HtmlAttributeNameAttribute.Name)), length: 0); continue; } bool isInvalid; var indexerDescriptor = ToIndexerAttributeDescriptor( property, attributeNameAttribute, parentType: type, errorSink: errorSink, defaultPrefix: attributeName + "-", designTime: designTime, isInvalid: out isInvalid); if (indexerDescriptor != null && !ValidateTagHelperAttributeDescriptor(indexerDescriptor, type, errorSink)) { isInvalid = true; } if (isInvalid) { // The property type or HtmlAttributeNameAttribute.DictionaryAttributePrefix (or perhaps the // HTML-casing of the property name) is invalid. Ignore this property completely. continue; } if (mainDescriptor != null) { attributeDescriptors.Add(mainDescriptor); } if (indexerDescriptor != null) { indexerDescriptors.Add(indexerDescriptor); } } attributeDescriptors.AddRange(indexerDescriptors); return(attributeDescriptors); }
private static bool ValidateTagHelperAttributeNameOrPrefix( string attributeNameOrPrefix, ITypeInfo parentType, string propertyName, ErrorSink errorSink, string nameOrPrefix) { if (string.IsNullOrEmpty(attributeNameOrPrefix)) { // ValidateTagHelperAttributeDescriptor validates Name is non-null and non-empty. The empty string is // valid for DictionaryAttributePrefix and null is impossible at this point because it means "don't // create a descriptor". (Empty DictionaryAttributePrefix is a corner case which would bind every // attribute of a target element. Likely not particularly useful but unclear what minimum length // should be required and what scenarios a minimum length would break.) return(true); } if (string.IsNullOrWhiteSpace(attributeNameOrPrefix)) { // Provide a single error if the entire name is whitespace, not an error per character. errorSink.OnError( SourceLocation.Zero, Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace( parentType.FullName, propertyName, nameOrPrefix), length: 0); return(false); } // data-* attributes are explicitly not implemented by user agents and are not intended for use on // the server; therefore it's invalid for TagHelpers to bind to them. if (attributeNameOrPrefix.StartsWith(DataDashPrefix, StringComparison.OrdinalIgnoreCase)) { errorSink.OnError( SourceLocation.Zero, Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixStart( parentType.FullName, propertyName, nameOrPrefix, attributeNameOrPrefix, DataDashPrefix), length: 0); return(false); } var isValid = true; foreach (var character in attributeNameOrPrefix) { if (char.IsWhiteSpace(character) || InvalidNonWhitespaceNameCharacters.Contains(character)) { errorSink.OnError( SourceLocation.Zero, Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter( parentType.FullName, propertyName, nameOrPrefix, attributeNameOrPrefix, character), length: 0); isValid = false; } } return(isValid); }
// This method handles cases when the attribute is a simple span attribute such as // class="something moresomething". This does not handle complex attributes such as // class="@myclass". Therefore the span.Content is equivalent to the entire attribute. private static TryParseResult TryParseSpan( Span span, IEnumerable<TagHelperDescriptor> descriptors, ErrorSink errorSink) { var afterEquals = false; var builder = new SpanBuilder { ChunkGenerator = span.ChunkGenerator, EditHandler = span.EditHandler, Kind = span.Kind }; // Will contain symbols that represent a single attribute value: <input| class="btn"| /> var htmlSymbols = span.Symbols.OfType<HtmlSymbol>().ToArray(); var capturedAttributeValueStart = false; var attributeValueStartLocation = span.Start; // The symbolOffset is initialized to 0 to expect worst case: "class=". If a quote is found later on for // the attribute value the symbolOffset is adjusted accordingly. var symbolOffset = 0; string name = null; // Iterate down through the symbols to find the name and the start of the value. // We subtract the symbolOffset so we don't accept an ending quote of a span. for (var i = 0; i < htmlSymbols.Length - symbolOffset; i++) { var symbol = htmlSymbols[i]; if (afterEquals) { // We've captured all leading whitespace, the attribute name, and an equals with an optional // quote/double quote. We're now at: " asp-for='|...'" or " asp-for=|..." // The goal here is to capture all symbols until the end of the attribute. Note this will not // consume an ending quote due to the symbolOffset. // When symbols are accepted into SpanBuilders, their locations get altered to be offset by the // parent which is why we need to mark our start location prior to adding the symbol. // This is needed to know the location of the attribute value start within the document. if (!capturedAttributeValueStart) { capturedAttributeValueStart = true; attributeValueStartLocation = span.Start + symbol.Start; } builder.Accept(symbol); } else if (name == null && HtmlMarkupParser.IsValidAttributeNameSymbol(symbol)) { // We've captured all leading whitespace prior to the attribute name. // We're now at: " |asp-for='...'" or " |asp-for=..." // The goal here is to capture the attribute name. var symbolContents = htmlSymbols .Skip(i) // Skip prefix .TakeWhile(nameSymbol => HtmlMarkupParser.IsValidAttributeNameSymbol(nameSymbol)) .Select(nameSymbol => nameSymbol.Content); // Move the indexer past the attribute name symbols. i += symbolContents.Count() - 1; name = string.Concat(symbolContents); attributeValueStartLocation = SourceLocation.Advance(attributeValueStartLocation, name); } else if (symbol.Type == HtmlSymbolType.Equals) { Debug.Assert( name != null, "Name should never be null here. The parser should guarantee an attribute has a name."); // We've captured all leading whitespace and the attribute name. // We're now at: " asp-for|='...'" or " asp-for|=..." // The goal here is to consume the equal sign and the optional single/double-quote. // The coming symbols will either be a quote or value (in the case that the value is unquoted). SourceLocation symbolStartLocation; // Skip the whitespace preceding the start of the attribute value. do { i++; // Start from the symbol after '='. } while (i < htmlSymbols.Length && (htmlSymbols[i].Type == HtmlSymbolType.WhiteSpace || htmlSymbols[i].Type == HtmlSymbolType.NewLine)); // Check for attribute start values, aka single or double quote if (i < htmlSymbols.Length && IsQuote(htmlSymbols[i])) { symbolStartLocation = htmlSymbols[i].Start; // If there's a start quote then there must be an end quote to be valid, skip it. symbolOffset = 1; } else { // We are at the symbol after equals. Go back to equals to ensure we don't skip past that symbol. i--; symbolStartLocation = symbol.Start; } attributeValueStartLocation = span.Start + symbolStartLocation + new SourceLocation(absoluteIndex: 1, lineIndex: 0, characterIndex: 1); afterEquals = true; } else if (symbol.Type == HtmlSymbolType.WhiteSpace) { // We're at the start of the attribute, this branch may be hit on the first iterations of // the loop since the parser separates attributes with their spaces included as symbols. // We're at: "| asp-for='...'" or "| asp-for=..." // Note: This will not be hit even for situations like asp-for ="..." because the core Razor // parser currently does not know how to handle attributes in that format. This will be addressed // by https://github.com/aspnet/Razor/issues/123. attributeValueStartLocation = SourceLocation.Advance(attributeValueStartLocation, symbol.Content); } } // After all symbols have been added we need to set the builders start position so we do not indirectly // modify each symbol's Start location. builder.Start = attributeValueStartLocation; if (name == null) { // We couldn't find a name, if the original span content was whitespace it ultimately means the tag // that owns this "attribute" is malformed and is expecting a user to type a new attribute. // ex: <myTH class="btn"| | if (!string.IsNullOrWhiteSpace(span.Content)) { errorSink.OnError( span.Start, RazorResources.TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed, span.Content.Length); } return null; } var result = CreateTryParseResult(name, descriptors); // If we're not after an equal then we should treat the value as if it were a minimized attribute. Span attributeValue = null; if (afterEquals) { attributeValue = CreateMarkupAttribute(builder, result.IsBoundNonStringAttribute); } result.AttributeValueNode = attributeValue; return result; }
private static TryParseResult TryParseBlock( string tagName, Block block, IEnumerable<TagHelperDescriptor> descriptors, ErrorSink errorSink) { // TODO: Accept more than just spans: https://github.com/aspnet/Razor/issues/96. // The first child will only ever NOT be a Span if a user is doing something like: // <input @checked /> var childSpan = block.Children.First() as Span; if (childSpan == null || childSpan.Kind != SpanKind.Markup) { errorSink.OnError( block.Start, RazorResources.FormatTagHelpers_CannotHaveCSharpInTagDeclaration(tagName), block.Length); return null; } var builder = new BlockBuilder(block); // If there's only 1 child it means that it's plain text inside of the attribute. // i.e. <div class="plain text in attribute"> if (builder.Children.Count == 1) { return TryParseSpan(childSpan, descriptors, errorSink); } var nameSymbols = childSpan .Symbols .OfType<HtmlSymbol>() .SkipWhile(symbol => !HtmlMarkupParser.IsValidAttributeNameSymbol(symbol)) // Skip prefix .TakeWhile(nameSymbol => HtmlMarkupParser.IsValidAttributeNameSymbol(nameSymbol)) .Select(nameSymbol => nameSymbol.Content); var name = string.Concat(nameSymbols); if (string.IsNullOrEmpty(name)) { errorSink.OnError( childSpan.Start, RazorResources.FormatTagHelpers_AttributesMustHaveAName(tagName), childSpan.Length); return null; } // Have a name now. Able to determine correct isBoundNonStringAttribute value. var result = CreateTryParseResult(name, descriptors); // Remove first child i.e. foo=" builder.Children.RemoveAt(0); // Grabbing last child to check if the attribute value is quoted. var endNode = block.Children.Last(); if (!endNode.IsBlock) { var endSpan = (Span)endNode; // In some malformed cases e.g. <p bar="false', the last Span (false' in the ex.) may contain more // than a single HTML symbol. Do not ignore those other symbols. var symbolCount = endSpan.Symbols.Count(); var endSymbol = symbolCount == 1 ? (HtmlSymbol)endSpan.Symbols.First() : null; // Checking to see if it's a quoted attribute, if so we should remove end quote if (endSymbol != null && IsQuote(endSymbol)) { builder.Children.RemoveAt(builder.Children.Count - 1); } } // We need to rebuild the chunk generators of the builder and its children (this is needed to // ensure we don't do special attribute chunk generation since this is a tag helper). block = RebuildChunkGenerators(builder.Build(), result.IsBoundAttribute); // If there's only 1 child at this point its value could be a simple markup span (treated differently than // block level elements for attributes). if (block.Children.Count() == 1) { var child = block.Children.First() as Span; if (child != null) { // After pulling apart the block we just have a value span. var spanBuilder = new SpanBuilder(child); result.AttributeValueNode = CreateMarkupAttribute(spanBuilder, result.IsBoundNonStringAttribute); return result; } } var isFirstSpan = true; result.AttributeValueNode = ConvertToMarkupAttributeBlock( block, (parentBlock, span) => { // If the attribute was requested by a tag helper but the corresponding property was not a // string, then treat its value as code. A non-string value can be any C# value so we need // to ensure the SyntaxTreeNode reflects that. if (result.IsBoundNonStringAttribute) { // For bound non-string attributes, we'll only allow a transition span to appear at the very // beginning of the attribute expression. All later transitions would appear as code so that // they are part of the generated output. E.g. // key="@value" -> MyTagHelper.key = value // key=" @value" -> MyTagHelper.key = @value // key="1 + @case" -> MyTagHelper.key = 1 + @case // key="@int + @case" -> MyTagHelper.key = int + @case // key="@(a + b) -> MyTagHelper.key = a + b // key="4 + @(a + b)" -> MyTagHelper.key = 4 + @(a + b) if (isFirstSpan && span.Kind == SpanKind.Transition) { // do nothing. } else { var spanBuilder = new SpanBuilder(span); if (parentBlock.Type == BlockType.Expression && (spanBuilder.Kind == SpanKind.Transition || spanBuilder.Kind == SpanKind.MetaCode)) { // Change to a MarkupChunkGenerator so that the '@' \ parenthesis is generated as part of the output. spanBuilder.ChunkGenerator = new MarkupChunkGenerator(); } spanBuilder.Kind = SpanKind.Code; span = spanBuilder.Build(); } } isFirstSpan = false; return span; }); return result; }
private static IEnumerable<TagHelperAttributeDescriptor> GetAttributeDescriptors( Type type, bool designTime, ErrorSink errorSink) { var attributeDescriptors = new List<TagHelperAttributeDescriptor>(); // Keep indexer descriptors separate to avoid sorting the combined list later. var indexerDescriptors = new List<TagHelperAttributeDescriptor>(); var accessibleProperties = type.GetRuntimeProperties().Where(IsAccessibleProperty); foreach (var property in accessibleProperties) { if (ShouldSkipDescriptorCreation(designTime, property)) { continue; } var attributeNameAttribute = property.GetCustomAttribute<HtmlAttributeNameAttribute>(inherit: false); var hasExplicitName = attributeNameAttribute != null && !string.IsNullOrEmpty(attributeNameAttribute.Name); var attributeName = hasExplicitName ? attributeNameAttribute.Name : ToHtmlCase(property.Name); TagHelperAttributeDescriptor mainDescriptor = null; if (property.SetMethod?.IsPublic == true) { mainDescriptor = ToAttributeDescriptor(property, attributeName, designTime); if (!ValidateTagHelperAttributeDescriptor(mainDescriptor, type, errorSink)) { // HtmlAttributeNameAttribute.Name is invalid. Ignore this property completely. continue; } } else if (hasExplicitName) { // Specified HtmlAttributeNameAttribute.Name though property has no public setter. errorSink.OnError( SourceLocation.Zero, Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameNotNullOrEmpty( type.FullName, property.Name, typeof(HtmlAttributeNameAttribute).FullName, nameof(HtmlAttributeNameAttribute.Name))); continue; } bool isInvalid; var indexerDescriptor = ToIndexerAttributeDescriptor( property, attributeNameAttribute, parentType: type, errorSink: errorSink, defaultPrefix: attributeName + "-", designTime: designTime, isInvalid: out isInvalid); if (indexerDescriptor != null && !ValidateTagHelperAttributeDescriptor(indexerDescriptor, type, errorSink)) { isInvalid = true; } if (isInvalid) { // The property type or HtmlAttributeNameAttribute.DictionaryAttributePrefix (or perhaps the // HTML-casing of the property name) is invalid. Ignore this property completely. continue; } if (mainDescriptor != null) { attributeDescriptors.Add(mainDescriptor); } if (indexerDescriptor != null) { indexerDescriptors.Add(indexerDescriptor); } } attributeDescriptors.AddRange(indexerDescriptors); return attributeDescriptors; }
private static TagHelperAttributeDescriptor ToIndexerAttributeDescriptor( PropertyInfo property, HtmlAttributeNameAttribute attributeNameAttribute, Type parentType, ErrorSink errorSink, string defaultPrefix, out bool isInvalid) { isInvalid = false; var dictionaryTypeArguments = ClosedGenericMatcher.ExtractGenericInterface( property.PropertyType, typeof(IDictionary<,>)) ?.GenericTypeArguments; if (dictionaryTypeArguments?[0] != typeof(string)) { if (attributeNameAttribute?.DictionaryAttributePrefix != null) { // DictionaryAttributePrefix is not supported unless associated with an // IDictionary<string, TValue> property. isInvalid = true; errorSink.OnError( SourceLocation.Zero, Resources.FormatTagHelperDescriptorFactory_InvalidAttributePrefix( parentType.FullName, property.Name, nameof(HtmlAttributeNameAttribute), nameof(HtmlAttributeNameAttribute.DictionaryAttributePrefix), "IDictionary<string, TValue>")); } return null; } // Potential prefix case. Use default prefix (based on name)? var useDefault = attributeNameAttribute == null || !attributeNameAttribute.DictionaryAttributePrefixSet; var prefix = useDefault ? defaultPrefix : attributeNameAttribute.DictionaryAttributePrefix; if (prefix == null) { // DictionaryAttributePrefix explicitly set to null. Ignore. return null; } return new TagHelperAttributeDescriptor( name: prefix, propertyName: property.Name, typeName: dictionaryTypeArguments[1].FullName, isIndexer: true); }
private static TryParseResult TryParseBlock( string tagName, Block block, IEnumerable<TagHelperDescriptor> descriptors, ErrorSink errorSink) { // TODO: Accept more than just spans: https://github.com/aspnet/Razor/issues/96. // The first child will only ever NOT be a Span if a user is doing something like: // <input @checked /> var childSpan = block.Children.First() as Span; if (childSpan == null || childSpan.Kind != SpanKind.Markup) { errorSink.OnError(block.Children.First().Start, RazorResources.FormatTagHelpers_CannotHaveCSharpInTagDeclaration(tagName)); return null; } var builder = new BlockBuilder(block); // If there's only 1 child it means that it's plain text inside of the attribute. // i.e. <div class="plain text in attribute"> if (builder.Children.Count == 1) { return TryParseSpan(childSpan, descriptors, errorSink); } var textSymbol = childSpan.Symbols.FirstHtmlSymbolAs(HtmlSymbolType.Text); var name = textSymbol != null ? textSymbol.Content : null; if (name == null) { errorSink.OnError(childSpan.Start, RazorResources.FormatTagHelpers_AttributesMustHaveAName(tagName)); return null; } // Have a name now. Able to determine correct isBoundNonStringAttribute value. var result = CreateTryParseResult(name, descriptors); // Remove first child i.e. foo=" builder.Children.RemoveAt(0); // Grabbing last child to check if the attribute value is quoted. var endNode = block.Children.Last(); if (!endNode.IsBlock) { var endSpan = (Span)endNode; // In some malformed cases e.g. <p bar="false', the last Span (false' in the ex.) may contain more // than a single HTML symbol. Do not ignore those other symbols. var symbolCount = endSpan.Symbols.Count(); var endSymbol = symbolCount == 1 ? (HtmlSymbol)endSpan.Symbols.First() : null; // Checking to see if it's a quoted attribute, if so we should remove end quote if (endSymbol != null && IsQuote(endSymbol)) { builder.Children.RemoveAt(builder.Children.Count - 1); } } // We need to rebuild the chunk generators of the builder and its children (this is needed to // ensure we don't do special attribute chunk generation since this is a tag helper). block = RebuildChunkGenerators(builder.Build()); // If there's only 1 child at this point its value could be a simple markup span (treated differently than // block level elements for attributes). if (block.Children.Count() == 1) { var child = block.Children.First() as Span; if (child != null) { // After pulling apart the block we just have a value span. var spanBuilder = new SpanBuilder(child); result.AttributeValueNode = CreateMarkupAttribute(spanBuilder, result.IsBoundNonStringAttribute); return result; } } result.AttributeValueNode = ConvertToMarkupAttributeBlock(block, result.IsBoundNonStringAttribute); return result; }
private static TagHelperAttributeDescriptor ToIndexerAttributeDescriptor( PropertyInfo property, HtmlAttributeNameAttribute attributeNameAttribute, Type parentType, ErrorSink errorSink, string defaultPrefix, bool designTime, out bool isInvalid) { isInvalid = false; var hasPublicSetter = property.SetMethod?.IsPublic == true; var dictionaryTypeArguments = ClosedGenericMatcher.ExtractGenericInterface( property.PropertyType, typeof(IDictionary<,>)) ?.GenericTypeArguments; if (dictionaryTypeArguments?[0] != typeof(string)) { if (attributeNameAttribute?.DictionaryAttributePrefix != null) { // DictionaryAttributePrefix is not supported unless associated with an // IDictionary<string, TValue> property. isInvalid = true; errorSink.OnError( SourceLocation.Zero, Resources.FormatTagHelperDescriptorFactory_InvalidAttributePrefixNotNull( parentType.FullName, property.Name, nameof(HtmlAttributeNameAttribute), nameof(HtmlAttributeNameAttribute.DictionaryAttributePrefix), "IDictionary<string, TValue>")); } else if (attributeNameAttribute != null && !hasPublicSetter) { // Associated an HtmlAttributeNameAttribute with a non-dictionary property that lacks a public // setter. isInvalid = true; errorSink.OnError( SourceLocation.Zero, Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameAttribute( parentType.FullName, property.Name, nameof(HtmlAttributeNameAttribute), "IDictionary<string, TValue>")); } return null; } else if (!hasPublicSetter && attributeNameAttribute != null && !attributeNameAttribute.DictionaryAttributePrefixSet) { // Must set DictionaryAttributePrefix when using HtmlAttributeNameAttribute with a dictionary property // that lacks a public setter. isInvalid = true; errorSink.OnError( SourceLocation.Zero, Resources.FormatTagHelperDescriptorFactory_InvalidAttributePrefixNull( parentType.FullName, property.Name, nameof(HtmlAttributeNameAttribute), nameof(HtmlAttributeNameAttribute.DictionaryAttributePrefix), "IDictionary<string, TValue>")); return null; } // Potential prefix case. Use default prefix (based on name)? var useDefault = attributeNameAttribute == null || !attributeNameAttribute.DictionaryAttributePrefixSet; var prefix = useDefault ? defaultPrefix : attributeNameAttribute.DictionaryAttributePrefix; if (prefix == null) { // DictionaryAttributePrefix explicitly set to null. Ignore. return null; } return ToAttributeDescriptor( property, attributeName: prefix, typeName: dictionaryTypeArguments[1].FullName, isIndexer: true, designTime: designTime); }
private static LookupInfo GetLookupInfo( TagHelperDirectiveDescriptor directiveDescriptor, ErrorSink errorSink) { var lookupText = directiveDescriptor.DirectiveText; var lookupStrings = lookupText?.Split(new[] { ',' }); // Ensure that we have valid lookupStrings to work with. The valid format is "typeName, assemblyName" if (lookupStrings == null || lookupStrings.Any(string.IsNullOrWhiteSpace) || lookupStrings.Length != 2) { errorSink.OnError( directiveDescriptor.Location, Resources.FormatTagHelperDescriptorResolver_InvalidTagHelperLookupText(lookupText), GetErrorLength(lookupText)); return null; } var trimmedAssemblyName = lookupStrings[1].Trim(); // + 1 is for the comma separator in the lookup text. var assemblyNameIndex = lookupStrings[0].Length + 1 + lookupStrings[1].IndexOf(trimmedAssemblyName); var assemblyNamePrefix = directiveDescriptor.DirectiveText.Substring(0, assemblyNameIndex); var assemblyNameLocation = SourceLocation.Advance(directiveDescriptor.Location, assemblyNamePrefix); return new LookupInfo { TypePattern = lookupStrings[0].Trim(), AssemblyName = trimmedAssemblyName, AssemblyNameLocation = assemblyNameLocation, }; }
private static IList<KeyValuePair<string, SyntaxTreeNode>> GetTagAttributes( string tagName, bool validStructure, Block tagBlock, IEnumerable<TagHelperDescriptor> descriptors, ErrorSink errorSink) { // Ignore all but one descriptor per type since this method uses the TagHelperDescriptors only to get the // contained TagHelperAttributeDescriptor's. descriptors = descriptors.Distinct(TypeBasedTagHelperDescriptorComparer.Default); var attributes = new List<KeyValuePair<string, SyntaxTreeNode>>(); // We skip the first child "<tagname" and take everything up to the ending portion of the tag ">" or "/>". // The -2 accounts for both the start and end tags. If the tag does not have a valid structure then there's // no end tag to ignore. var symbolOffset = validStructure ? 2 : 1; var attributeChildren = tagBlock.Children.Skip(1).Take(tagBlock.Children.Count() - symbolOffset); foreach (var child in attributeChildren) { TryParseResult result; if (child.IsBlock) { result = TryParseBlock(tagName, (Block)child, descriptors, errorSink); } else { result = TryParseSpan((Span)child, descriptors, errorSink); } // Only want to track the attribute if we succeeded in parsing its corresponding Block/Span. if (result != null) { 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 && IsNullOrWhitespaceAttributeValue(result.AttributeValueNode))) { errorLocation = GetAttributeNameStartLocation(child); errorSink.OnError( errorLocation.Value, RazorResources.FormatRewriterError_EmptyTagHelperBoundAttribute( result.AttributeName, tagName, GetPropertyType(result.AttributeName, descriptors)), result.AttributeName.Length); } // Check if the attribute was a prefix match for a tag helper dictionary property but the // dictionary key would be the empty string. if (result.IsMissingDictionaryKey) { if (!errorLocation.HasValue) { errorLocation = GetAttributeNameStartLocation(child); } errorSink.OnError( errorLocation.Value, RazorResources.FormatTagHelperBlockRewriter_IndexerAttributeNameMustIncludeKey( result.AttributeName, tagName), result.AttributeName.Length); } attributes.Add( new KeyValuePair<string, SyntaxTreeNode>(result.AttributeName, result.AttributeValueNode)); } } return attributes; }
private static bool TryValidateName( string name, string whitespaceError, Func<char, string> characterErrorBuilder, ErrorSink errorSink) { var validName = true; if (string.IsNullOrWhiteSpace(name)) { errorSink.OnError(SourceLocation.Zero, whitespaceError); validName = false; } else { foreach (var character in name) { if (char.IsWhiteSpace(character) || InvalidNonWhitespaceNameCharacters.Contains(character)) { var error = characterErrorBuilder(character); errorSink.OnError(SourceLocation.Zero, error); validName = false; } } } return validName; }
private static void OnAllowedChildrenTagError( TagHelperBlockTracker tracker, string tagName, Block tagBlock, ErrorSink errorSink) { var allowedChildrenString = string.Join(", ", tracker.AllowedChildren); var errorMessage = RazorResources.FormatTagHelperParseTreeRewriter_InvalidNestedTag( tagName, tracker.TagName, allowedChildrenString); var errorStart = GetTagDeclarationErrorStart(tagBlock); errorSink.OnError(errorStart, errorMessage, tagName.Length); }
private static LookupInfo GetLookupInfo(TagHelperDirectiveDescriptor directiveDescriptor, ErrorSink errorSink) { var lookupText = directiveDescriptor.DirectiveText; var lookupStrings = lookupText?.Split(new[] { ',' }); // Ensure that we have valid lookupStrings to work with. Valid formats are: // "assemblyName" // "typeName, assemblyName" if (lookupStrings == null || lookupStrings.Any(string.IsNullOrWhiteSpace) || lookupStrings.Length != 2) { errorSink.OnError( directiveDescriptor.Location, Resources.FormatTagHelperDescriptorResolver_InvalidTagHelperLookupText(lookupText)); return null; } return new LookupInfo { TypePattern = lookupStrings[0].Trim(), AssemblyName = lookupStrings[1].Trim() }; }
private static bool ValidateName( string name, bool targetingAttributes, ErrorSink errorSink) { if (!targetingAttributes && string.Equals( name, TagHelperDescriptorProvider.ElementCatchAllTarget, StringComparison.OrdinalIgnoreCase)) { // '*' as the entire name is OK in the TargetElement catch-all case. return true; } else if (targetingAttributes && name.EndsWith( TagHelperDescriptorProvider.RequiredAttributeWildcardSuffix, StringComparison.OrdinalIgnoreCase)) { // A single '*' at the end of a required attribute is valid; everywhere else is invalid. Strip it from // the end so we can validate the rest of the name. name = name.Substring(0, name.Length - 1); } var targetName = targetingAttributes ? Resources.TagHelperDescriptorFactory_Attribute : Resources.TagHelperDescriptorFactory_Tag; var validName = true; if (string.IsNullOrWhiteSpace(name)) { errorSink.OnError( SourceLocation.Zero, Resources.FormatTargetElementAttribute_NameCannotBeNullOrWhitespace(targetName)); validName = false; } else { foreach (var character in name) { if (char.IsWhiteSpace(character) || InvalidNonWhitespaceNameCharacters.Contains(character)) { errorSink.OnError( SourceLocation.Zero, Resources.FormatTargetElementAttribute_InvalidName( targetName.ToLower(), name, character)); validName = false; } } } return validName; }
private static TagHelperAttributeDescriptor ToIndexerAttributeDescriptor( IPropertyInfo property, HtmlAttributeNameAttribute attributeNameAttribute, ITypeInfo parentType, ErrorSink errorSink, string defaultPrefix, bool designTime, out bool isInvalid) { isInvalid = false; var hasPublicSetter = property.HasPublicSetter; var dictionaryTypeArguments = property.PropertyType.GetGenericDictionaryParameters(); if (!StringTypeInfo.Equals(dictionaryTypeArguments?[0])) { if (attributeNameAttribute?.DictionaryAttributePrefix != null) { // DictionaryAttributePrefix is not supported unless associated with an // IDictionary<string, TValue> property. isInvalid = true; errorSink.OnError( SourceLocation.Zero, Resources.FormatTagHelperDescriptorFactory_InvalidAttributePrefixNotNull( parentType.FullName, property.Name, nameof(HtmlAttributeNameAttribute), nameof(HtmlAttributeNameAttribute.DictionaryAttributePrefix), "IDictionary<string, TValue>"), length: 0); } else if (attributeNameAttribute != null && !hasPublicSetter) { // Associated an HtmlAttributeNameAttribute with a non-dictionary property that lacks a public // setter. isInvalid = true; errorSink.OnError( SourceLocation.Zero, Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameAttribute( parentType.FullName, property.Name, nameof(HtmlAttributeNameAttribute), "IDictionary<string, TValue>"), length: 0); } return(null); } else if (!hasPublicSetter && attributeNameAttribute != null && !attributeNameAttribute.DictionaryAttributePrefixSet) { // Must set DictionaryAttributePrefix when using HtmlAttributeNameAttribute with a dictionary property // that lacks a public setter. isInvalid = true; errorSink.OnError( SourceLocation.Zero, Resources.FormatTagHelperDescriptorFactory_InvalidAttributePrefixNull( parentType.FullName, property.Name, nameof(HtmlAttributeNameAttribute), nameof(HtmlAttributeNameAttribute.DictionaryAttributePrefix), "IDictionary<string, TValue>"), length: 0); return(null); } // Potential prefix case. Use default prefix (based on name)? var useDefault = attributeNameAttribute == null || !attributeNameAttribute.DictionaryAttributePrefixSet; var prefix = useDefault ? defaultPrefix : attributeNameAttribute.DictionaryAttributePrefix; if (prefix == null) { // DictionaryAttributePrefix explicitly set to null. Ignore. return(null); } return(ToAttributeDescriptor( property, attributeName: prefix, typeName: dictionaryTypeArguments[1].FullName, isIndexer: true, isStringProperty: StringTypeInfo.Equals(dictionaryTypeArguments[1]), designTime: designTime)); }
private TagHelperAttributeDescriptor ToIndexerAttributeDescriptor( IPropertyInfo property, HtmlAttributeNameAttribute attributeNameAttribute, ITypeInfo parentType, ErrorSink errorSink, string defaultPrefix, out bool isInvalid) { isInvalid = false; var hasPublicSetter = property.HasPublicSetter; var dictionaryTypeArguments = property.PropertyType.GetGenericDictionaryParameters(); if (!StringTypeInfo.Equals(dictionaryTypeArguments?[0])) { if (attributeNameAttribute?.DictionaryAttributePrefix != null) { // DictionaryAttributePrefix is not supported unless associated with an // IDictionary<string, TValue> property. isInvalid = true; errorSink.OnError( SourceLocation.Zero, Resources.FormatTagHelperDescriptorFactory_InvalidAttributePrefixNotNull( parentType.FullName, property.Name, nameof(HtmlAttributeNameAttribute), nameof(HtmlAttributeNameAttribute.DictionaryAttributePrefix), "IDictionary<string, TValue>"), length: 0); } else if (attributeNameAttribute != null && !hasPublicSetter) { // Associated an HtmlAttributeNameAttribute with a non-dictionary property that lacks a public // setter. isInvalid = true; errorSink.OnError( SourceLocation.Zero, Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameAttribute( parentType.FullName, property.Name, nameof(HtmlAttributeNameAttribute), "IDictionary<string, TValue>"), length: 0); } return null; } else if (!hasPublicSetter && attributeNameAttribute != null && !attributeNameAttribute.DictionaryAttributePrefixSet) { // Must set DictionaryAttributePrefix when using HtmlAttributeNameAttribute with a dictionary property // that lacks a public setter. isInvalid = true; errorSink.OnError( SourceLocation.Zero, Resources.FormatTagHelperDescriptorFactory_InvalidAttributePrefixNull( parentType.FullName, property.Name, nameof(HtmlAttributeNameAttribute), nameof(HtmlAttributeNameAttribute.DictionaryAttributePrefix), "IDictionary<string, TValue>"), length: 0); return null; } // Potential prefix case. Use default prefix (based on name)? var useDefault = attributeNameAttribute == null || !attributeNameAttribute.DictionaryAttributePrefixSet; var prefix = useDefault ? defaultPrefix : attributeNameAttribute.DictionaryAttributePrefix; if (prefix == null) { // DictionaryAttributePrefix explicitly set to null. Ignore. return null; } return ToAttributeDescriptor( property, attributeName: prefix, typeName: dictionaryTypeArguments[1].FullName, isIndexer: true, isStringProperty: StringTypeInfo.Equals(dictionaryTypeArguments[1])); }
private static void ValidateDescriptors( IEnumerable<TagHelperDescriptor> descriptors, string tagName, Block tagBlock, ErrorSink errorSink) { // Ensure that all descriptors associated with this tag have appropriate TagStructures. Cannot have // multiple descriptors that expect different TagStructures (other than TagStructure.Unspecified). TagHelperDescriptor baseDescriptor = null; foreach (var descriptor in descriptors) { if (descriptor.TagStructure != TagStructure.Unspecified) { // Can't have a set of TagHelpers that expect different structures. if (baseDescriptor != null && baseDescriptor.TagStructure != descriptor.TagStructure) { errorSink.OnError( tagBlock.Start, RazorResources.FormatTagHelperParseTreeRewriter_InconsistentTagStructure( baseDescriptor.TypeName, descriptor.TypeName, tagName, nameof(TagHelperDescriptor.TagStructure)), tagBlock.Length); } baseDescriptor = descriptor; } } }
// Internal for testing. internal static bool ValidateTagHelperAttributeDescriptor( TagHelperAttributeDescriptor attributeDescriptor, Type parentType, ErrorSink errorSink) { string nameOrPrefix; if (attributeDescriptor.IsIndexer) { nameOrPrefix = Resources.TagHelperDescriptorFactory_Prefix; } else if (string.IsNullOrEmpty(attributeDescriptor.Name)) { errorSink.OnError( SourceLocation.Zero, Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameNullOrEmpty( parentType.FullName, attributeDescriptor.PropertyName)); return false; } else { nameOrPrefix = Resources.TagHelperDescriptorFactory_Name; } return ValidateTagHelperAttributeNameOrPrefix( attributeDescriptor.Name, parentType, attributeDescriptor.PropertyName, errorSink, nameOrPrefix); }