public void VisitSendsErrorsToVisitor() { // Arrange Mock <ParserVisitor> targetMock = new Mock <ParserVisitor>(); var root = new BlockBuilder() { Type = BlockType.Comment }.Build(); var errorSink = new ParserErrorSink(); List <RazorError> errors = new List <RazorError> { new RazorError("Foo", 1, 0, 1), new RazorError("Bar", 2, 0, 2), }; 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, ParserErrorSink 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() }); }
public void VisitCallsOnCompleteWhenAllNodesHaveBeenVisited() { // Arrange Mock <ParserVisitor> targetMock = new Mock <ParserVisitor>(); var root = new BlockBuilder() { Type = BlockType.Comment }.Build(); var errorSink = new ParserErrorSink(); errorSink.OnError(new RazorError("Foo", 1, 0, 1)); errorSink.OnError(new RazorError("Bar", 2, 0, 2)); var results = new ParserResults(root, Enumerable.Empty <TagHelperDescriptor>(), errorSink); // Act targetMock.Object.Visit(results); // Assert targetMock.Verify(v => v.OnComplete()); }
private static bool ValidateName( string name, bool targetingAttributes, ParserErrorSink errorSink) { 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); }
/// <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="ParserErrorSink"/> 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] ParserErrorSink 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())); }
public void Compile_ReturnsFailedResultIfParseFails() { // Arrange var errorSink = new ParserErrorSink(); 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 CodeBuilderResult("", new LineMapping[0]), new CodeTree()); 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); // Act var result = razorService.Compile(relativeFileInfo); // Assert var ex = Assert.Throws <CompilationFailedException>(() => result.CompiledType); var failure = Assert.Single(ex.CompilationFailures); var message = Assert.Single(failure.Messages); Assert.Equal("some message", message.Message); host.Verify(); }
private static LookupInfo GetLookupInfo(TagHelperDirectiveDescriptor directiveDescriptor, ParserErrorSink errorSink) { var lookupText = directiveDescriptor.LookupText; 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 != 1 && lookupStrings.Length != 2)) { errorSink.OnError( directiveDescriptor.Location, Resources.FormatTagHelperDescriptorResolver_InvalidTagHelperLookupText(lookupText)); return(null); } // Grab the assembly name from the lookup text strings. Due to our supported lookupText formats it will // always be the last element provided. var assemblyName = lookupStrings.Last().Trim(); string typeName = null; // Check if the lookupText specifies a type to search for. if (lookupStrings.Length == 2) { // The user provided a type name. Retrieve it so we can prune our descriptors. typeName = lookupStrings[0].Trim(); } return(new LookupInfo { AssemblyName = assemblyName, TypeName = typeName }); }
private static bool EnsureValidPrefix( string prefix, SourceLocation directiveLocation, ParserErrorSink 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)); return(false); } } return(true); }
private static IDictionary <string, SyntaxTreeNode> GetTagAttributes( string tagName, bool validStructure, Block tagBlock, IEnumerable <TagHelperDescriptor> descriptors, ParserErrorSink errorSink) { var attributes = new Dictionary <string, SyntaxTreeNode>(StringComparer.OrdinalIgnoreCase); // Build a dictionary so we can easily lookup expected attribute value lookups IReadOnlyDictionary <string, string> attributeValueTypes = descriptors.SelectMany(descriptor => descriptor.Attributes) .Distinct(TagHelperAttributeDescriptorComparer.Default) .ToDictionary(descriptor => descriptor.Name, descriptor => descriptor.TypeName, StringComparer.OrdinalIgnoreCase); // 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) { KeyValuePair <string, SyntaxTreeNode> attribute; bool succeeded = true; if (child.IsBlock) { succeeded = TryParseBlock(tagName, (Block)child, attributeValueTypes, errorSink, out attribute); } else { succeeded = TryParseSpan((Span)child, attributeValueTypes, errorSink, out attribute); } // Only want to track the attribute if we succeeded in parsing its corresponding Block/Span. if (succeeded) { // Check if it's a bound attribute that is not of type string and happens to be null or whitespace. string attributeValueType; if (attributeValueTypes.TryGetValue(attribute.Key, out attributeValueType) && !IsStringAttribute(attributeValueType) && IsNullOrWhitespaceAttributeValue(attribute.Value)) { var errorLocation = GetAttributeNameStartLocation(child); errorSink.OnError( errorLocation, RazorResources.FormatRewriterError_EmptyTagHelperBoundAttribute( attribute.Key, tagName, attributeValueType), attribute.Key.Length); } attributes[attribute.Key] = attribute.Value; } } return(attributes); }
private static bool TryParseBlock( string tagName, Block block, IReadOnlyDictionary <string, string> attributeValueTypes, ParserErrorSink errorSink, out KeyValuePair <string, SyntaxTreeNode> attribute) { // 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)); attribute = default(KeyValuePair <string, SyntaxTreeNode>); return(false); } 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, attributeValueTypes, errorSink, out attribute)); } 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)); attribute = default(KeyValuePair <string, SyntaxTreeNode>); return(false); } // TODO: Support no attribute values: https://github.com/aspnet/Razor/issues/220 // 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; var endSymbol = (HtmlSymbol)endSpan.Symbols.Last(); // Checking to see if it's a quoted attribute, if so we should remove end quote if (IsQuote(endSymbol)) { builder.Children.RemoveAt(builder.Children.Count - 1); } } // We need to rebuild the code generators of the builder and its children (this is needed to // ensure we don't do special attribute code generation since this is a tag helper). block = RebuildCodeGenerators(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); attribute = CreateMarkupAttribute(name, spanBuilder, attributeValueTypes); return(true); } } attribute = new KeyValuePair <string, SyntaxTreeNode>(name, block); return(true); }
// 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 bool TryParseSpan( Span span, IReadOnlyDictionary <string, string> attributeValueTypes, ParserErrorSink errorSink, out KeyValuePair <string, SyntaxTreeNode> attribute) { var afterEquals = false; var builder = new SpanBuilder { CodeGenerator = span.CodeGenerator, 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 && symbol.Type == HtmlSymbolType.Text) { // 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. name = symbol.Content; attributeValueStartLocation = SourceLocation.Advance(attributeValueStartLocation, name); } else if (symbol.Type == HtmlSymbolType.Equals) { Debug.Assert( name != null, "Name should never be null here. The parser should guaruntee 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). // Spaces after/before the equal symbol are not yet supported: // https://github.com/aspnet/Razor/issues/123 // TODO: Handle malformed tags, if there's an '=' then there MUST be a value. // https://github.com/aspnet/Razor/issues/104 SourceLocation symbolStartLocation; // Check for attribute start values, aka single or double quote if ((i + 1) < htmlSymbols.Length && IsQuote(htmlSymbols[i + 1])) { // Move past the attribute start so we can accept the true value. 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 { 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_TagHelperAttributesMustBeWelformed, span.Content.Length); } attribute = default(KeyValuePair <string, SyntaxTreeNode>); return(false); } attribute = CreateMarkupAttribute(name, builder, attributeValueTypes); return(true); }
// 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 bool TryParseSpan( Span span, IReadOnlyDictionary <string, string> attributeValueTypes, ParserErrorSink errorSink, out KeyValuePair <string, SyntaxTreeNode> attribute) { var afterEquals = false; var builder = new SpanBuilder { CodeGenerator = span.CodeGenerator, EditHandler = span.EditHandler, Kind = span.Kind }; var htmlSymbols = span.Symbols.OfType <HtmlSymbol>().ToArray(); var capturedAttributeValueStart = false; var attributeValueStartLocation = span.Start; var symbolOffset = 1; 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) { // 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 && symbol.Type == HtmlSymbolType.Text) { name = symbol.Content; attributeValueStartLocation = SourceLocation.Advance(span.Start, name); } else if (symbol.Type == HtmlSymbolType.Equals) { // We've found an '=' symbol, this means that the coming symbols will either be a quote // or value (in the case that the value is unquoted). // Spaces after/before the equal symbol are not yet supported: // https://github.com/aspnet/Razor/issues/123 // TODO: Handle malformed tags, if there's an '=' then there MUST be a value. // https://github.com/aspnet/Razor/issues/104 SourceLocation symbolStartLocation; // Check for attribute start values, aka single or double quote if (IsQuote(htmlSymbols[i + 1])) { // Move past the attribute start so we can accept the true value. i++; symbolStartLocation = htmlSymbols[i + 1].Start; } else { symbolStartLocation = symbol.Start; // Set the symbol offset to 0 so we don't attempt to skip an end quote that doesn't exist. symbolOffset = 0; } attributeValueStartLocation = symbolStartLocation + span.Start + new SourceLocation(absoluteIndex: 1, lineIndex: 0, characterIndex: 1); afterEquals = true; } } // 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) { errorSink.OnError(span.Start, RazorResources.TagHelperBlockRewriter_TagHelperAttributesMustBeWelformed, span.Content.Length); attribute = default(KeyValuePair <string, SyntaxTreeNode>); return(false); } attribute = CreateMarkupAttribute(name, builder, attributeValueTypes); return(true); }