public void UpdateLocationDoesNotAdvanceLineIndexOnNonNewlineCharacter() { // Arrange var tracker = new SourceLocationTracker(TestStartLocation); // Act tracker.UpdateLocation('f', 'o'); // Assert Assert.Equal(42, tracker.CurrentLocation.LineIndex); }
public void UpdateLocationAdvancesCharacterIndexOnSlashRFollowedBySlashN() { // Arrange var tracker = new SourceLocationTracker(TestStartLocation); // Act tracker.UpdateLocation('\r', '\n'); // Assert Assert.Equal(46, tracker.CurrentLocation.CharacterIndex); }
public void UpdateLocationResetsCharacterIndexOnSlashRFollowedByNonNewlineCharacter() { // Arrange var tracker = new SourceLocationTracker(TestStartLocation); // Act tracker.UpdateLocation('\r', 'o'); // Assert Assert.Equal(0, tracker.CurrentLocation.CharacterIndex); }
public void UpdateLocationAdvancesAbsoluteIndexOnSlashN() { // Arrange var tracker = new SourceLocationTracker(TestStartLocation); // Act tracker.UpdateLocation('\n', 'o'); // Assert Assert.Equal(11, tracker.CurrentLocation.AbsoluteIndex); }
internal void CalculateStart(Span prev) { if (prev == null) { Start = SourceLocation.Zero; } else { Start = new SourceLocationTracker(prev.Start).UpdateLocation(prev.Content).CurrentLocation; } }
public static SourceLocation Advance(SourceLocation location, string text) { if (text == null) { throw new ArgumentNullException(nameof(text)); } var tracker = new SourceLocationTracker(location); tracker.UpdateLocation(text); return(tracker.CurrentLocation); }
public void ChangeStart(SourceLocation newStart) { _start = newStart; var current = this; var tracker = new SourceLocationTracker(newStart); tracker.UpdateLocation(Content); while ((current = current.Next) != null) { current._start = tracker.CurrentLocation; tracker.UpdateLocation(current.Content); } }
public void UpdateLocationAdvancesCorrectlyForMultiLineString() { // Arrange var tracker = new SourceLocationTracker(TestStartLocation); // Act tracker.UpdateLocation("foo\nbar\rbaz\r\nbox"); // Assert Assert.Equal(26, tracker.CurrentLocation.AbsoluteIndex); Assert.Equal(45, tracker.CurrentLocation.LineIndex); Assert.Equal(3, tracker.CurrentLocation.CharacterIndex); }
public void UpdateLocationAdvancesCorrectlyForMultiLineString() { // Arrange var location = TestStartLocation; // Act var currentLocation = SourceLocationTracker.Advance(location, "foo\nbar\rbaz\r\nbox"); // Assert Assert.Equal(26, currentLocation.AbsoluteIndex); Assert.Equal(45, currentLocation.LineIndex); Assert.Equal(3, currentLocation.CharacterIndex); }
public void Advance_PreservesSourceLocationFilePath(string path) { // Arrange var sourceLocation = new SourceLocation(path, 15, 2, 8); // Act var result = SourceLocationTracker.Advance(sourceLocation, "Hello world"); // Assert Assert.Equal(path, result.FilePath); Assert.Equal(26, result.AbsoluteIndex); Assert.Equal(2, result.LineIndex); Assert.Equal(19, result.CharacterIndex); }
protected virtual SpanBuilder UpdateSpan(Span target, SourceChange change) { var newContent = change.GetEditedContent(target); var newSpan = new SpanBuilder(target); newSpan.ClearTokens(); foreach (var token in Tokenizer(newContent)) { newSpan.Accept(token); } if (target.Next != null) { var newEnd = SourceLocationTracker.CalculateNewLocation(target.Start, newContent); target.Next.ChangeStart(newEnd); } return(newSpan); }
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( RazorDiagnosticFactory.CreateTagHelper_CannotHaveNonTagContent( new SourceSpan(errorStart, length), _currentTagHelperTracker.TagName, allowedChildrenString)); } } }
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 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); } }
// 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); }
public override SyntaxNode VisitMarkupElement(MarkupElementSyntax node) { if (IsPartOfStartTag(node)) { // If this element is inside a start tag, it is some sort of malformed case like // <p @do { someattribute=\"btn\"></p>, where the end "p" tag is inside the start "p" tag. // We don't want to do tag helper parsing for this tag. return(base.VisitMarkupElement(node)); } MarkupTagHelperStartTagSyntax tagHelperStart = null; MarkupTagHelperEndTagSyntax tagHelperEnd = null; TagHelperInfo tagHelperInfo = null; // Visit the start tag. var startTag = (MarkupStartTagSyntax)Visit(node.StartTag); if (startTag != null) { var tagName = startTag.GetTagNameWithOptionalBang(); if (TryRewriteTagHelperStart(startTag, node.EndTag, out tagHelperStart, out tagHelperInfo)) { // This is a tag helper. if (tagHelperInfo.TagMode == TagMode.SelfClosing || tagHelperInfo.TagMode == TagMode.StartTagOnly) { var tagHelperElement = SyntaxFactory.MarkupTagHelperElement(tagHelperStart, body: new SyntaxList <RazorSyntaxNode>(), endTag: null); var rewrittenTagHelper = tagHelperElement.WithTagHelperInfo(tagHelperInfo); if (node.Body.Count == 0 && node.EndTag == null) { return(rewrittenTagHelper); } // This tag contains a body and/or an end tag which needs to be moved to the parent. var rewrittenNodes = SyntaxListBuilder <RazorSyntaxNode> .Create(); rewrittenNodes.Add(rewrittenTagHelper); var rewrittenBody = VisitList(node.Body); rewrittenNodes.AddRange(rewrittenBody); return(SyntaxFactory.MarkupElement(startTag: null, body: rewrittenNodes.ToList(), endTag: node.EndTag)); } else if (node.EndTag == null) { // Start tag helper with no corresponding end tag. _errorSink.OnError( RazorDiagnosticFactory.CreateParsing_TagHelperFoundMalformedTagHelper( new SourceSpan(SourceLocationTracker.Advance(startTag.GetSourceLocation(_source), "<"), tagName.Length), tagName)); } else { // Tag helper start tag. Keep track. var tracker = new TagHelperTracker(_tagHelperPrefix, tagHelperInfo); _trackerStack.Push(tracker); } } else { // Non-TagHelper tag. ValidateParentAllowsPlainStartTag(startTag); if (node.EndTag != null || (!startTag.IsSelfClosing() && !startTag.IsVoidElement())) { // Ideally we don't want to keep track of self-closing or void tags. // But if a matching end tag exists, keep track of the start tag no matter what. // We will just assume the parser had a good reason to do this. var tracker = new TagTracker(tagName, isTagHelper: false); _trackerStack.Push(tracker); } } } // Visit body between start and end tags. var body = VisitList(node.Body); // Visit end tag. var endTag = (MarkupEndTagSyntax)Visit(node.EndTag); if (endTag != null) { var tagName = endTag.GetTagNameWithOptionalBang(); if (TryRewriteTagHelperEnd(startTag, endTag, out tagHelperEnd)) { // This is a tag helper if (startTag == null) { // The end tag helper has no corresponding start tag, create an error. _errorSink.OnError( RazorDiagnosticFactory.CreateParsing_TagHelperFoundMalformedTagHelper( new SourceSpan(SourceLocationTracker.Advance(endTag.GetSourceLocation(_source), "</"), tagName.Length), tagName)); } } else { // Non tag helper end tag. if (startTag == null) { // Standalone end tag. We may need to error if it is not supposed to be here. // If there was a corresponding start tag, we would have already added this error. ValidateParentAllowsPlainEndTag(endTag); } else { // Since a start tag exists, we must already be tracking it. // Pop the stack as we're done with the end tag. _trackerStack.Pop(); } } } if (tagHelperInfo != null) { // If we get here it means this element was rewritten as a tag helper. var tagHelperElement = SyntaxFactory.MarkupTagHelperElement(tagHelperStart, body, tagHelperEnd); return(tagHelperElement.WithTagHelperInfo(tagHelperInfo)); } // There was no matching tag helper for this element. Return. return(node.Update(startTag, body, endTag)); }
private static SourceLocation GetEndTagDeclarationErrorStart(MarkupEndTagSyntax tagBlock, RazorSourceDocument source) { return(SourceLocationTracker.Advance(tagBlock.GetSourceLocation(source), "</")); }
private bool TryRewriteTagHelperEnd(MarkupStartTagSyntax startTag, MarkupEndTagSyntax endTag, out MarkupTagHelperEndTagSyntax rewritten) { rewritten = null; var tagName = endTag.GetTagNameWithOptionalBang(); // Could not determine tag name, it can't be a TagHelper, continue on and track the element. if (string.IsNullOrEmpty(tagName) || tagName.StartsWith("!", StringComparison.Ordinal)) { return(false); } var tracker = CurrentTagHelperTracker; var tagNameScope = tracker?.TagName ?? string.Empty; if (!IsPotentialTagHelperEnd(tagName, endTag)) { return(false); } // Validate that our end tag matches the currently scoped tag, if not we may need to error. if (startTag != null && 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); } ValidateEndTagSyntax(tagName, endTag); _trackerStack.Pop(); } else { var 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.Mappings[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(endTag.GetSourceLocation(_source), "</"), tagName.Length), tagName, descriptor.DisplayName, invalidRule.TagStructure)); return(false); } } } rewritten = SyntaxFactory.MarkupTagHelperEndTag( endTag.OpenAngle, endTag.ForwardSlash, endTag.Bang, endTag.Name, endTag.MiscAttributeContent, endTag.CloseAngle); return(true); }
private static SourceLocation GetTagDeclarationErrorStart(Block tagBlock) { var advanceBy = IsEndTag(tagBlock) ? "</" : "<"; return(SourceLocationTracker.Advance(tagBlock.Start, advanceBy)); }
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 SourceLocation GetTagDeclarationErrorStart(MarkupTagBlockSyntax tagBlock, RazorSourceDocument source) { var advanceBy = IsEndTag(tagBlock) ? "</" : "<"; return(SourceLocationTracker.Advance(tagBlock.GetSourceLocation(source), advanceBy)); }