private static TryParseResult TryParseMinimizedAttribute( string tagName, MarkupMinimizedAttributeBlockSyntax attributeBlock, IEnumerable <TagHelperDescriptor> descriptors, ErrorSink errorSink, HashSet <string> processedBoundAttributeNames) { // Have a name now. Able to determine correct isBoundNonStringAttribute value. var result = CreateTryParseResult(attributeBlock.Name.GetContent(), descriptors, processedBoundAttributeNames); result.AttributeStructure = AttributeStructure.Minimized; var rewritten = SyntaxFactory.MarkupMinimizedTagHelperAttribute( attributeBlock.NamePrefix, attributeBlock.Name); rewritten = rewritten.WithTagHelperAttributeInfo( new TagHelperAttributeInfo(result.AttributeName, result.AttributeStructure, result.IsBoundAttribute)); result.RewrittenAttribute = rewritten; return(result); }
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 = SourceLocationTracker.Advance(child.Start, whitespace); var length = trimmedStart.TrimEnd().Length; var allowedChildren = _currentTagHelperTracker.AllowedChildren; var allowedChildrenString = string.Join(", ", allowedChildren); errorSink.OnError( errorStart, LegacyResources.FormatTagHelperParseTreeRewriter_CannotHaveNonTagContent( _currentTagHelperTracker.TagName, allowedChildrenString), length); } } }
private void BuildMalformedTagHelpers(int count, ErrorSink errorSink) { for (var i = 0; i < count; i++) { var tracker = _trackerStack.Peek(); // Skip all non-TagHelper entries. Non TagHelper trackers do not need to represent well-formed HTML. if (!tracker.IsTagHelper) { PopTrackerStack(); continue; } var malformedTagHelper = ((TagHelperBlockTracker)tracker).Builder; errorSink.OnError( RazorDiagnosticFactory.CreateParsing_TagHelperFoundMalformedTagHelper( new SourceSpan(SourceLocationTracker.Advance(malformedTagHelper.Start, "<"), malformedTagHelper.TagName.Length), malformedTagHelper.TagName)); BuildCurrentlyTrackedTagHelperBlock(endTag: null); } }
private static void ValidateBinding( TagHelperBinding bindingResult, 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; TagStructure? baseStructure = null; foreach (var descriptor in bindingResult.Descriptors) { var boundRules = bindingResult.GetBoundRules(descriptor); foreach (var rule in boundRules) { if (rule.TagStructure != TagStructure.Unspecified) { // Can't have a set of TagHelpers that expect different structures. if (baseStructure.HasValue && baseStructure != rule.TagStructure) { errorSink.OnError( tagBlock.Start, LegacyResources.FormatTagHelperParseTreeRewriter_InconsistentTagStructure( baseDescriptor.DisplayName, descriptor.DisplayName, tagName, nameof(TagMatchingRuleDescriptor.TagStructure)), tagBlock.Length); } baseDescriptor = descriptor; baseStructure = rule.TagStructure; } } } }
public static RazorSyntaxTree Rewrite(RazorSyntaxTree syntaxTree, string tagHelperPrefix, IEnumerable <TagHelperDescriptor> descriptors) { var errorSink = new ErrorSink(); var rewriter = new Rewriter( syntaxTree.Source, tagHelperPrefix, descriptors, syntaxTree.Options.FeatureFlags, errorSink); var rewritten = rewriter.Visit(syntaxTree.Root); var errorList = new List <RazorDiagnostic>(); errorList.AddRange(errorSink.Errors); errorList.AddRange(descriptors.SelectMany(d => d.GetAllDiagnostics())); var diagnostics = CombineErrors(syntaxTree.Diagnostics, errorList).OrderBy(error => error.Span.AbsoluteIndex); var newSyntaxTree = RazorSyntaxTree.Create(rewritten, syntaxTree.Source, diagnostics, syntaxTree.Options); return(newSyntaxTree); }
private void BuildMalformedTagHelpers(int count, ErrorSink errorSink) { for (var i = 0; i < count; i++) { var tracker = _trackerStack.Peek(); // Skip all non-TagHelper entries. Non TagHelper trackers do not need to represent well-formed HTML. if (!tracker.IsTagHelper) { PopTrackerStack(); continue; } var malformedTagHelper = ((TagHelperBlockTracker)tracker).Builder; errorSink.OnError( SourceLocationTracker.Advance(malformedTagHelper.Start, "<"), LegacyResources.FormatTagHelpersParseTreeRewriter_FoundMalformedTagHelper( malformedTagHelper.TagName), malformedTagHelper.TagName.Length); BuildCurrentlyTrackedTagHelperBlock(endTag: null); } }
internal void EvaluateData( IEnumerable <TagHelperDescriptor> descriptors, string documentContent, MarkupBlock expectedOutput, IEnumerable <RazorDiagnostic> expectedErrors, string tagHelperPrefix = null, RazorParserFeatureFlags featureFlags = null) { var syntaxTree = ParseDocument(documentContent); var errorSink = new ErrorSink(); var parseTreeRewriter = new TagHelperParseTreeRewriter( tagHelperPrefix, descriptors, featureFlags ?? syntaxTree.Options.FeatureFlags); var actualTree = parseTreeRewriter.Rewrite(syntaxTree.Root, errorSink); var allErrors = syntaxTree.Diagnostics.Concat(errorSink.Errors); var actualErrors = allErrors .OrderBy(error => error.Span.AbsoluteIndex) .ToList(); BaselineTest(actualTree, verifySyntaxTree: false, actualErrors.ToArray()); }
private static TryParseResult TryParseBlock( string tagName, Block block, IEnumerable <TagHelperDescriptor> descriptors, ErrorSink errorSink, HashSet <string> processedBoundAttributeNames) { // 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 != SpanKindInternal.Markup) { errorSink.OnError( block.Start, LegacyResources.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, processedBoundAttributeNames)); } 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, LegacyResources.FormatTagHelpers_AttributesMustHaveAName(tagName), childSpan.Length); return(null); } // Have a name now. Able to determine correct isBoundNonStringAttribute value. var result = CreateTryParseResult(name, descriptors, processedBoundAttributeNames); 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) { case HtmlSymbolType.Equals: if (builder.Children.Count == 2 && builder.Children[1] is Span value && value.Kind == SpanKindInternal.Markup) { // Attribute value is a string literal. Eg: <tag my-attribute=foo />. result.AttributeStructure = AttributeStructure.NoQuotes; } else { // Could be an expression, 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" />. result.AttributeStructure = AttributeStructure.DoubleQuotes; } break;
private static IList <TagHelperAttributeNode> GetTagAttributes( string tagName, bool validStructure, Block tagBlock, TagHelperBinding bindingResult, ErrorSink errorSink) { 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); var processedBoundAttributeNames = new HashSet <string>(StringComparer.OrdinalIgnoreCase); foreach (var child in attributeChildren) { TryParseResult result; if (child.IsBlock) { result = TryParseBlock(tagName, (Block)child, bindingResult.Descriptors, errorSink, processedBoundAttributeNames); } else { result = TryParseSpan((Span)child, bindingResult.Descriptors, errorSink, processedBoundAttributeNames); } // 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, LegacyResources.FormatRewriterError_EmptyTagHelperBoundAttribute( result.AttributeName, tagName, GetPropertyType(result.AttributeName, bindingResult.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, LegacyResources.FormatTagHelperBlockRewriter_IndexerAttributeNameMustIncludeKey( result.AttributeName, tagName), result.AttributeName.Length); } var attributeNode = new TagHelperAttributeNode( result.AttributeName, result.AttributeValueNode, result.AttributeStructure); attributes.Add(attributeNode); } else { // Error occured while parsing the attribute. Don't try parsing the rest to avoid misleading errors. break; } } return(attributes); }
// 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, HashSet <string> processedBoundAttributeNames) { var afterEquals = false; var builder = new SpanBuilder(span.Start) { 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; // Default to DoubleQuotes. We purposefully do not persist NoQuotes ValueStyle to stay consistent with the // TryParseBlock() variation of attribute parsing. var attributeValueStyle = AttributeStructure.DoubleQuotes; // 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 = 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 nameBuilder = new StringBuilder(); // Move the indexer past the attribute name symbols. for (var j = i; j < htmlSymbols.Length; j++) { var nameSymbol = htmlSymbols[j]; if (!HtmlMarkupParser.IsValidAttributeNameSymbol(nameSymbol)) { break; } nameBuilder.Append(nameSymbol.Content); i++; } i--; name = nameBuilder.ToString(); attributeValueStartLocation = SourceLocationTracker.Advance(attributeValueStartLocation, name); } else if (symbol.Type == HtmlSymbolType.Equals) { // 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])) { if (htmlSymbols[i].Type == HtmlSymbolType.SingleQuote) { attributeValueStyle = AttributeStructure.SingleQuotes; } 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 = new SourceLocation( symbolStartLocation.FilePath, symbolStartLocation.AbsoluteIndex + 1, symbolStartLocation.LineIndex, symbolStartLocation.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 = SourceLocationTracker.Advance(attributeValueStartLocation, symbol.Content); } } // After all symbols have been added we need to set the builders start position so we do not indirectly // modify the span'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, LegacyResources.TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed, span.Content.Length); } return(null); } var result = CreateTryParseResult(name, descriptors, processedBoundAttributeNames); // 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); } else { attributeValueStyle = AttributeStructure.Minimized; } result.AttributeValueNode = attributeValue; result.AttributeStructure = attributeValueStyle; return(result); }
private static SyntaxList <RazorSyntaxNode> GetRewrittenChildren( string tagName, bool validStructure, MarkupTagBlockSyntax tagBlock, TagHelperBinding bindingResult, RazorParserFeatureFlags featureFlags, ErrorSink errorSink, RazorSourceDocument source) { var tagHelperBuilder = SyntaxListBuilder <RazorSyntaxNode> .Create(); var processedBoundAttributeNames = new HashSet <string>(StringComparer.OrdinalIgnoreCase); if (tagBlock.Children.Count == 1) { // Tag with no attributes. We have nothing to rewrite here. return(tagBlock.Children); } // Add the tag start tagHelperBuilder.Add(tagBlock.Children.First()); // We skip the first child "<tagname" and take everything up to the ending portion of the tag ">" or "/>". // If the tag does not have a valid structure then there's no close angle to ignore. var tokenOffset = validStructure ? 1 : 0; for (var i = 1; i < tagBlock.Children.Count - tokenOffset; i++) { var isMinimized = false; var attributeNameLocation = SourceLocation.Undefined; var child = tagBlock.Children[i]; TryParseResult result; if (child is MarkupAttributeBlockSyntax attributeBlock) { attributeNameLocation = attributeBlock.Name.GetSourceLocation(source); result = TryParseAttribute( tagName, attributeBlock, bindingResult.Descriptors, errorSink, processedBoundAttributeNames); tagHelperBuilder.Add(result.RewrittenAttribute); } else if (child is MarkupMinimizedAttributeBlockSyntax minimizedAttributeBlock) { isMinimized = true; attributeNameLocation = minimizedAttributeBlock.Name.GetSourceLocation(source); result = TryParseMinimizedAttribute( tagName, minimizedAttributeBlock, bindingResult.Descriptors, errorSink, processedBoundAttributeNames); tagHelperBuilder.Add(result.RewrittenAttribute); } else if (child is CSharpCodeBlockSyntax) { // TODO: Accept more than just Markup attributes: https://github.com/aspnet/Razor/issues/96. // Something like: // <input @checked /> var location = new SourceSpan(child.GetSourceLocation(source), child.FullWidth); var diagnostic = RazorDiagnosticFactory.CreateParsing_TagHelpersCannotHaveCSharpInTagDeclaration(location, tagName); errorSink.OnError(diagnostic); result = null; } else if (child is MarkupTextLiteralSyntax) { // 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"| | var literalContent = child.GetContent(); if (!string.IsNullOrWhiteSpace(literalContent)) { var location = child.GetSourceSpan(source); var diagnostic = RazorDiagnosticFactory.CreateParsing_TagHelperAttributeListMustBeWellFormed(location); errorSink.OnError(diagnostic); } result = null; } else { result = null; } // Only want to track the attribute if we succeeded in parsing its corresponding Block/Span. if (result == null) { // Error occurred while parsing the attribute. Don't try parsing the rest to avoid misleading errors. for (var j = i; j < tagBlock.Children.Count; j++) { tagHelperBuilder.Add(tagBlock.Children[j]); } return(tagHelperBuilder.ToList()); } // Check if it's a non-boolean bound attribute that is minimized or if it's a bound // non-string attribute that has null or whitespace content. var isValidMinimizedAttribute = featureFlags.AllowMinimizedBooleanTagHelperAttributes && result.IsBoundBooleanAttribute; if ((isMinimized && result.IsBoundAttribute && !isValidMinimizedAttribute) || (!isMinimized && result.IsBoundNonStringAttribute && string.IsNullOrWhiteSpace(GetAttributeValueContent(result.RewrittenAttribute)))) { var errorLocation = new SourceSpan(attributeNameLocation, result.AttributeName.Length); var propertyTypeName = GetPropertyType(result.AttributeName, bindingResult.Descriptors); var diagnostic = RazorDiagnosticFactory.CreateTagHelper_EmptyBoundAttribute(errorLocation, result.AttributeName, tagName, propertyTypeName); errorSink.OnError(diagnostic); } // 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) { var errorLocation = new SourceSpan(attributeNameLocation, result.AttributeName.Length); var diagnostic = RazorDiagnosticFactory.CreateParsing_TagHelperIndexerAttributeNameMustIncludeKey(errorLocation, result.AttributeName, tagName); errorSink.OnError(diagnostic); } } if (validStructure) { // Add the tag end. tagHelperBuilder.Add(tagBlock.Children[tagBlock.Children.Count - 1]); } return(tagHelperBuilder.ToList()); }
private static TryParseResult TryParseAttribute( string tagName, MarkupAttributeBlockSyntax attributeBlock, IEnumerable <TagHelperDescriptor> descriptors, ErrorSink errorSink, HashSet <string> processedBoundAttributeNames) { // Have a name now. Able to determine correct isBoundNonStringAttribute value. var result = CreateTryParseResult(attributeBlock.Name.GetContent(), descriptors, processedBoundAttributeNames); if (attributeBlock.ValuePrefix == null) { // We are purposefully not persisting NoQuotes even for unbound attributes because it is still possible to // rewrite the values that introduces a space like in UrlResolutionTagHelper. // The other case is it could be an expression, 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" />. result.AttributeStructure = AttributeStructure.DoubleQuotes; } else { var lastToken = attributeBlock.ValuePrefix.GetLastToken(); switch (lastToken.Kind) { case SyntaxKind.DoubleQuote: result.AttributeStructure = AttributeStructure.DoubleQuotes; break; case SyntaxKind.SingleQuote: result.AttributeStructure = AttributeStructure.SingleQuotes; break; default: result.AttributeStructure = AttributeStructure.Minimized; break; } } var attributeValue = attributeBlock.Value; if (attributeValue == null) { var builder = SyntaxListBuilder <RazorSyntaxNode> .Create(); // Add a marker for attribute value when there are no quotes like, <p class= > builder.Add(SyntaxFactory.MarkupTextLiteral(new SyntaxList <SyntaxToken>())); attributeValue = SyntaxFactory.GenericBlock(builder.ToList()); } var rewrittenValue = RewriteAttributeValue(result, attributeValue); var rewritten = SyntaxFactory.MarkupTagHelperAttribute( attributeBlock.NamePrefix, attributeBlock.Name, attributeBlock.NameSuffix, attributeBlock.EqualsToken, attributeBlock.ValuePrefix, rewrittenValue, attributeBlock.ValueSuffix); rewritten = rewritten.WithTagHelperAttributeInfo( new TagHelperAttributeInfo(result.AttributeName, result.AttributeStructure, result.IsBoundAttribute)); result.RewrittenAttribute = rewritten; return(result); }
private void RewriteTags(Block input, ErrorSink errorSink, int depth) { // We want to start a new block without the children from existing (we rebuild them). TrackBlock(new BlockBuilder { Type = input.Type, ChunkGenerator = input.ChunkGenerator }); var activeTrackers = _trackerStack.Count; foreach (var child in input.Children) { if (child.IsBlock) { var childBlock = (Block)child; if (childBlock.Type == BlockKindInternal.Tag) { if (TryRewriteTagHelper(childBlock, errorSink)) { continue; } else { // Non-TagHelper tag. ValidateParentAllowsPlainTag(childBlock, errorSink); TrackTagBlock(childBlock, depth); } // If we get to here it means that we're a normal html tag. No need to iterate any deeper into // the children of it because they wont be tag helpers. } else { // We're not an Html tag so iterate through children recursively. RewriteTags(childBlock, errorSink, depth + 1); continue; } } else { ValidateParentAllowsContent((Span)child, errorSink); } // At this point the child is a Span or Block with Type BlockType.Tag that doesn't happen to be a // tag helper. // Add the child to current block. _currentBlock.Children.Add(child); } // We captured the number of active tag helpers at the start of our logic, it should be the same. If not // it means that there are malformed tag helpers at the top of our stack. if (activeTrackers != _trackerStack.Count) { // Malformed tag helpers built here will be tag helpers that do not have end tags in the current block // scope. Block scopes are special cases in Razor such as @<p> would cause an error because there's no // matching end </p> tag in the template block scope and therefore doesn't make sense as a tag helper. BuildMalformedTagHelpers(_trackerStack.Count - activeTrackers, errorSink); Debug.Assert(activeTrackers == _trackerStack.Count); } BuildCurrentlyTrackedBlock(); }
private bool TryRewriteTagHelper(Block tagBlock, ErrorSink errorSink) { // Get tag name of the current block (doesn't matter if it's an end or start tag) var tagName = GetTagName(tagBlock); // Could not determine tag name, it can't be a TagHelper, continue on and track the element. if (tagName == null) { return(false); } TagHelperBinding tagHelperBinding; if (!IsPotentialTagHelper(tagName, tagBlock)) { return(false); } var tracker = _currentTagHelperTracker; var tagNameScope = tracker?.TagName ?? string.Empty; if (!IsEndTag(tagBlock)) { // We're now in a start tag block, we first need to see if the tag block is a tag helper. var elementAttributes = GetAttributeNameValuePairs(tagBlock); tagHelperBinding = _tagHelperBinder.GetBinding( tagName, elementAttributes, CurrentParentTagName, CurrentParentIsTagHelper); // If there aren't any TagHelperDescriptors registered then we aren't a TagHelper if (tagHelperBinding == null) { // If the current tag matches the current TagHelper scope it means the parent TagHelper matched // all the required attributes but the current one did not; therefore, we need to increment the // OpenMatchingTags counter for current the TagHelperBlock so we don't end it too early. // ex: <myth req="..."><myth></myth></myth> We don't want the first myth to close on the inside // tag. if (string.Equals(tagNameScope, tagName, StringComparison.OrdinalIgnoreCase)) { tracker.OpenMatchingTags++; } return(false); } ValidateParentAllowsTagHelper(tagName, tagBlock, errorSink); ValidateBinding(tagHelperBinding, tagName, tagBlock, errorSink); // We're in a start TagHelper block. var validTagStructure = ValidateTagSyntax(tagName, tagBlock, errorSink); var builder = TagHelperBlockRewriter.Rewrite( tagName, validTagStructure, _featureFlags, tagBlock, tagHelperBinding, errorSink); // Track the original start tag so the editor knows where each piece of the TagHelperBlock lies // for formatting. builder.SourceStartTag = tagBlock; // Found a new tag helper block TrackTagHelperBlock(builder); // If it's a non-content expecting block then we don't have to worry about nested children within the // tag. Complete it. if (builder.TagMode == TagMode.SelfClosing || builder.TagMode == TagMode.StartTagOnly) { BuildCurrentlyTrackedTagHelperBlock(endTag: null); } } else { // Validate that our end tag matches the currently scoped tag, if not we may need to error. if (tagNameScope.Equals(tagName, StringComparison.OrdinalIgnoreCase)) { // If there are additional end tags required before we can build our block it means we're in a // situation like this: <myth req="..."><myth></myth></myth> where we're at the inside </myth>. if (tracker.OpenMatchingTags > 0) { tracker.OpenMatchingTags--; return(false); } ValidateTagSyntax(tagName, tagBlock, errorSink); BuildCurrentlyTrackedTagHelperBlock(tagBlock); } else { tagHelperBinding = _tagHelperBinder.GetBinding( tagName, attributes: Array.Empty <KeyValuePair <string, string> >(), parentTagName: CurrentParentTagName, parentIsTagHelper: CurrentParentIsTagHelper); // If there are not TagHelperDescriptors associated with the end tag block that also have no // required attributes then it means we can't be a TagHelper, bail out. if (tagHelperBinding == null) { return(false); } foreach (var descriptor in tagHelperBinding.Descriptors) { var boundRules = tagHelperBinding.GetBoundRules(descriptor); var invalidRule = boundRules.FirstOrDefault(rule => rule.TagStructure == TagStructure.WithoutEndTag); if (invalidRule != null) { // End tag TagHelper that states it shouldn't have an end tag. errorSink.OnError( RazorDiagnosticFactory.CreateParsing_TagHelperMustNotHaveAnEndTag( new SourceSpan(SourceLocationTracker.Advance(tagBlock.Start, "</"), tagName.Length), tagName, descriptor.DisplayName, invalidRule.TagStructure)); return(false); } } // Current tag helper scope does not match the end tag. Attempt to recover the tag // helper by looking up the previous tag helper scopes for a matching tag. If we // can't recover it means there was no corresponding tag helper start tag. if (TryRecoverTagHelper(tagName, tagBlock, errorSink)) { ValidateParentAllowsTagHelper(tagName, tagBlock, errorSink); ValidateTagSyntax(tagName, tagBlock, errorSink); // Successfully recovered, move onto the next element. } else { // Could not recover, the end tag helper has no corresponding start tag, create // an error based on the current childBlock. errorSink.OnError( RazorDiagnosticFactory.CreateParsing_TagHelperFoundMalformedTagHelper( new SourceSpan(SourceLocationTracker.Advance(tagBlock.Start, "</"), tagName.Length), tagName)); return(false); } } } return(true); }
private static IList <TagHelperAttributeNode> GetTagAttributes( string tagName, bool validStructure, Block tagBlock, TagHelperBinding bindingResult, ErrorSink errorSink, RazorParserFeatureFlags featureFlags) { 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); var processedBoundAttributeNames = new HashSet <string>(StringComparer.OrdinalIgnoreCase); foreach (var child in attributeChildren) { TryParseResult result; if (child.IsBlock) { result = TryParseBlock(tagName, (Block)child, bindingResult.Descriptors, errorSink, processedBoundAttributeNames); } else { result = TryParseSpan((Span)child, bindingResult.Descriptors, errorSink, processedBoundAttributeNames); } // Only want to track the attribute if we succeeded in parsing its corresponding Block/Span. if (result != null) { // Check if it's a non-boolean bound attribute that is minimized or if it's a bound // non-string attribute that has null or whitespace content. var isMinimized = result.AttributeValueNode == null; var isValidMinimizedAttribute = featureFlags.AllowMinimizedBooleanTagHelperAttributes && result.IsBoundBooleanAttribute; if ((isMinimized && result.IsBoundAttribute && !isValidMinimizedAttribute) || (!isMinimized && result.IsBoundNonStringAttribute && IsNullOrWhitespaceAttributeValue(result.AttributeValueNode))) { var errorLocation = GetAttributeNameLocation(child, result.AttributeName); var propertyTypeName = GetPropertyType(result.AttributeName, bindingResult.Descriptors); var diagnostic = RazorDiagnosticFactory.CreateTagHelper_EmptyBoundAttribute(errorLocation, result.AttributeName, tagName, propertyTypeName); errorSink.OnError(diagnostic); } // 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) { var errorLocation = GetAttributeNameLocation(child, result.AttributeName); var diagnostic = RazorDiagnosticFactory.CreateParsing_TagHelperIndexerAttributeNameMustIncludeKey(errorLocation, result.AttributeName, tagName); errorSink.OnError(diagnostic); } var attributeNode = new TagHelperAttributeNode( result.AttributeName, result.AttributeValueNode, result.AttributeStructure); attributes.Add(attributeNode); } else { // Error occured while parsing the attribute. Don't try parsing the rest to avoid misleading errors. break; } } return(attributes); }
public static MarkupTagHelperStartTagSyntax Rewrite( string tagName, RazorParserFeatureFlags featureFlags, MarkupStartTagSyntax startTag, TagHelperBinding bindingResult, ErrorSink errorSink, RazorSourceDocument source) { var processedBoundAttributeNames = new HashSet <string>(StringComparer.OrdinalIgnoreCase); var attributes = startTag.Attributes; var attributeBuilder = SyntaxListBuilder <RazorSyntaxNode> .Create(); for (var i = 0; i < startTag.Attributes.Count; i++) { var isMinimized = false; var attributeNameLocation = SourceLocation.Undefined; var child = startTag.Attributes[i]; TryParseResult result; if (child is MarkupAttributeBlockSyntax attributeBlock) { attributeNameLocation = attributeBlock.Name.GetSourceLocation(source); result = TryParseAttribute( tagName, attributeBlock, bindingResult.Descriptors, errorSink, processedBoundAttributeNames); attributeBuilder.Add(result.RewrittenAttribute); } else if (child is MarkupMinimizedAttributeBlockSyntax minimizedAttributeBlock) { isMinimized = true; attributeNameLocation = minimizedAttributeBlock.Name.GetSourceLocation(source); result = TryParseMinimizedAttribute( tagName, minimizedAttributeBlock, bindingResult.Descriptors, errorSink, processedBoundAttributeNames); attributeBuilder.Add(result.RewrittenAttribute); } else if (child is MarkupMiscAttributeContentSyntax miscContent) { foreach (var contentChild in miscContent.Children) { if (contentChild is CSharpCodeBlockSyntax codeBlock) { // TODO: Accept more than just Markup attributes: https://github.com/aspnet/Razor/issues/96. // Something like: // <input @checked /> var location = new SourceSpan(codeBlock.GetSourceLocation(source), codeBlock.FullWidth); var diagnostic = RazorDiagnosticFactory.CreateParsing_TagHelpersCannotHaveCSharpInTagDeclaration(location, tagName); errorSink.OnError(diagnostic); break; } else { // 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"| | var literalContent = contentChild.GetContent(); if (!string.IsNullOrWhiteSpace(literalContent)) { var location = contentChild.GetSourceSpan(source); var diagnostic = RazorDiagnosticFactory.CreateParsing_TagHelperAttributeListMustBeWellFormed(location); errorSink.OnError(diagnostic); break; } } } result = null; } else { result = null; } // Only want to track the attribute if we succeeded in parsing its corresponding Block/Span. if (result == null) { // Error occurred while parsing the attribute. Don't try parsing the rest to avoid misleading errors. for (var j = i; j < startTag.Attributes.Count; j++) { attributeBuilder.Add(startTag.Attributes[j]); } break; } // Check if it's a non-boolean bound attribute that is minimized or if it's a bound // non-string attribute that has null or whitespace content. var isValidMinimizedAttribute = featureFlags.AllowMinimizedBooleanTagHelperAttributes && result.IsBoundBooleanAttribute; if ((isMinimized && result.IsBoundAttribute && !isValidMinimizedAttribute) || (!isMinimized && result.IsBoundNonStringAttribute && string.IsNullOrWhiteSpace(GetAttributeValueContent(result.RewrittenAttribute)))) { var errorLocation = new SourceSpan(attributeNameLocation, result.AttributeName.Length); var propertyTypeName = GetPropertyType(result.AttributeName, bindingResult.Descriptors); var diagnostic = RazorDiagnosticFactory.CreateTagHelper_EmptyBoundAttribute(errorLocation, result.AttributeName, tagName, propertyTypeName); errorSink.OnError(diagnostic); } // 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) { var errorLocation = new SourceSpan(attributeNameLocation, result.AttributeName.Length); var diagnostic = RazorDiagnosticFactory.CreateParsing_TagHelperIndexerAttributeNameMustIncludeKey(errorLocation, result.AttributeName, tagName); errorSink.OnError(diagnostic); } } if (attributeBuilder.Count > 0) { // This means we rewrote something. Use the new set of attributes. attributes = attributeBuilder.ToList(); } var tagHelperStartTag = SyntaxFactory.MarkupTagHelperStartTag( startTag.OpenAngle, startTag.Bang, startTag.Name, attributes, startTag.ForwardSlash, startTag.CloseAngle); return(tagHelperStartTag.WithSpanContext(startTag.GetSpanContext())); }