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());
        }
Exemple #4
0
        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);
        }
Exemple #5
0
        /// <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();
        }
Exemple #7
0
        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);
        }