Make Razor codegen use invariant culture

- #557
- use `int.ToString(CultureInfo.InvariantCulture)` consistently in `CSharpCodeWriter` / `CSharpCodeVisitor`
- correct `string` operations to use explicit `StringComparison.Ordinal`
- improve `RazorEditorParser` doc comments

nits:
- remove one-off use of `CurrentUICulture` in `LineMapping` debug code (`ToString()` implementation)
- clean a bit of #YOLO wrapping and long lines
This commit is contained in:
Doug Bunting 2016-01-03 12:13:54 -08:00
parent 37eca627dd
commit fe5e1b8c71
10 changed files with 174 additions and 96 deletions

View File

@ -276,7 +276,8 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
var trimmedAssemblyName = lookupStrings[1].Trim(); var trimmedAssemblyName = lookupStrings[1].Trim();
// + 1 is for the comma separator in the lookup text. // + 1 is for the comma separator in the lookup text.
var assemblyNameIndex = lookupStrings[0].Length + 1 + lookupStrings[1].IndexOf(trimmedAssemblyName); var assemblyNameIndex =
lookupStrings[0].Length + 1 + lookupStrings[1].IndexOf(trimmedAssemblyName, StringComparison.Ordinal);
var assemblyNamePrefix = directiveDescriptor.DirectiveText.Substring(0, assemblyNameIndex); var assemblyNamePrefix = directiveDescriptor.DirectiveText.Substring(0, assemblyNameIndex);
var assemblyNameLocation = SourceLocation.Advance(directiveDescriptor.Location, assemblyNamePrefix); var assemblyNameLocation = SourceLocation.Advance(directiveDescriptor.Location, assemblyNamePrefix);

View File

@ -108,7 +108,7 @@ namespace Microsoft.AspNet.Razor.CodeGenerators
{ {
WriteStringLiteral(value.Value); WriteStringLiteral(value.Value);
WriteParameterSeparator(); WriteParameterSeparator();
Write(value.Location.AbsoluteIndex.ToString(CultureInfo.CurrentCulture)); Write(value.Location.AbsoluteIndex.ToString(CultureInfo.InvariantCulture));
return this; return this;
} }
@ -144,7 +144,8 @@ namespace Microsoft.AspNet.Razor.CodeGenerators
public CSharpCodeWriter WriteUsing(string name, bool endLine) public CSharpCodeWriter WriteUsing(string name, bool endLine)
{ {
Write(string.Format("using {0}", name)); Write("using ");
Write(name);
if (endLine) if (endLine)
{ {
@ -225,6 +226,7 @@ namespace Microsoft.AspNet.Razor.CodeGenerators
{ {
return WriteEndMethodInvocation(endLine: true); return WriteEndMethodInvocation(endLine: true);
} }
public CSharpCodeWriter WriteEndMethodInvocation(bool endLine) public CSharpCodeWriter WriteEndMethodInvocation(bool endLine)
{ {
Write(")"); Write(")");
@ -300,7 +302,9 @@ namespace Microsoft.AspNet.Razor.CodeGenerators
public CSharpCodeWriter WriteMethodInvocation(string methodName, bool endLine, params string[] parameters) public CSharpCodeWriter WriteMethodInvocation(string methodName, bool endLine, params string[] parameters)
{ {
return WriteStartMethodInvocation(methodName).Write(string.Join(", ", parameters)).WriteEndMethodInvocation(endLine); return WriteStartMethodInvocation(methodName)
.Write(string.Join(", ", parameters))
.WriteEndMethodInvocation(endLine);
} }
public CSharpDisableWarningScope BuildDisableWarningScope(int warning) public CSharpDisableWarningScope BuildDisableWarningScope(int warning)
@ -363,7 +367,10 @@ namespace Microsoft.AspNet.Razor.CodeGenerators
return BuildClassDeclaration(accessibility, name, new string[] { baseType }); return BuildClassDeclaration(accessibility, name, new string[] { baseType });
} }
public CSharpCodeWritingScope BuildClassDeclaration(string accessibility, string name, IEnumerable<string> baseTypes) public CSharpCodeWritingScope BuildClassDeclaration(
string accessibility,
string name,
IEnumerable<string> baseTypes)
{ {
Write(accessibility).Write(" class ").Write(name); Write(accessibility).Write(" class ").Write(name);
@ -388,9 +395,17 @@ namespace Microsoft.AspNet.Razor.CodeGenerators
return BuildConstructor(accessibility, name, Enumerable.Empty<KeyValuePair<string, string>>()); return BuildConstructor(accessibility, name, Enumerable.Empty<KeyValuePair<string, string>>());
} }
public CSharpCodeWritingScope BuildConstructor(string accessibility, string name, IEnumerable<KeyValuePair<string, string>> parameters) public CSharpCodeWritingScope BuildConstructor(
string accessibility,
string name,
IEnumerable<KeyValuePair<string, string>> parameters)
{ {
Write(accessibility).Write(" ").Write(name).Write("(").Write(string.Join(", ", parameters.Select(p => p.Key + " " + p.Value))).WriteLine(")"); Write(accessibility)
.Write(" ")
.Write(name)
.Write("(")
.Write(string.Join(", ", parameters.Select(p => p.Key + " " + p.Value)))
.WriteLine(")");
return new CSharpCodeWritingScope(this); return new CSharpCodeWritingScope(this);
} }
@ -400,15 +415,28 @@ namespace Microsoft.AspNet.Razor.CodeGenerators
return BuildMethodDeclaration(accessibility, returnType, name, Enumerable.Empty<KeyValuePair<string, string>>()); return BuildMethodDeclaration(accessibility, returnType, name, Enumerable.Empty<KeyValuePair<string, string>>());
} }
public CSharpCodeWritingScope BuildMethodDeclaration(string accessibility, string returnType, string name, IEnumerable<KeyValuePair<string, string>> parameters) public CSharpCodeWritingScope BuildMethodDeclaration(
string accessibility,
string returnType,
string name,
IEnumerable<KeyValuePair<string, string>> parameters)
{ {
Write(accessibility).Write(" ").Write(returnType).Write(" ").Write(name).Write("(").Write(string.Join(", ", parameters.Select(p => p.Key + " " + p.Value))).WriteLine(")"); Write(accessibility)
.Write(" ")
.Write(returnType)
.Write(" ")
.Write(name)
.Write("(")
.Write(string.Join(", ", parameters.Select(p => p.Key + " " + p.Value)))
.WriteLine(")");
return new CSharpCodeWritingScope(this); return new CSharpCodeWritingScope(this);
} }
// TODO: Do I need to look at the document content to determine its mapping length? public CSharpLineMappingWriter BuildLineMapping(
public CSharpLineMappingWriter BuildLineMapping(SourceLocation documentLocation, int contentLength, string sourceFilename) SourceLocation documentLocation,
int contentLength,
string sourceFilename)
{ {
return new CSharpLineMappingWriter(this, documentLocation, contentLength, sourceFilename); return new CSharpLineMappingWriter(this, documentLocation, contentLength, sourceFilename);
} }

View File

@ -73,7 +73,7 @@ namespace Microsoft.AspNet.Razor.CodeGenerators
public override string ToString() public override string ToString()
{ {
return string.Format(CultureInfo.CurrentUICulture, "{0} -> {1}", DocumentLocation, GeneratedLocation); return string.Format(CultureInfo.CurrentCulture, "{0} -> {1}", DocumentLocation, GeneratedLocation);
} }
} }
} }

View File

@ -5,7 +5,6 @@ using System;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Text;
using Microsoft.AspNet.Razor.Chunks; using Microsoft.AspNet.Razor.Chunks;
using Microsoft.AspNet.Razor.Parser.SyntaxTree; using Microsoft.AspNet.Razor.Parser.SyntaxTree;
@ -257,9 +256,9 @@ namespace Microsoft.AspNet.Razor.CodeGenerators.Visitors
Writer Writer
.WriteParameterSeparator() .WriteParameterSeparator()
.Write(chunk.Start.AbsoluteIndex.ToString(CultureInfo.CurrentCulture)) .Write(chunk.Start.AbsoluteIndex.ToString(CultureInfo.InvariantCulture))
.WriteParameterSeparator() .WriteParameterSeparator()
.Write(chunk.Association.Length.ToString(CultureInfo.CurrentCulture)) .Write(chunk.Association.Length.ToString(CultureInfo.InvariantCulture))
.WriteParameterSeparator() .WriteParameterSeparator()
.WriteBooleanLiteral(value: false) .WriteBooleanLiteral(value: false)
.WriteEndMethodInvocation(); .WriteEndMethodInvocation();
@ -281,9 +280,9 @@ namespace Microsoft.AspNet.Razor.CodeGenerators.Visitors
Writer Writer
.WriteEndMethodInvocation(false) .WriteEndMethodInvocation(false)
.WriteParameterSeparator() .WriteParameterSeparator()
.Write(chunk.Start.AbsoluteIndex.ToString(CultureInfo.CurrentCulture)) .Write(chunk.Start.AbsoluteIndex.ToString(CultureInfo.InvariantCulture))
.WriteParameterSeparator() .WriteParameterSeparator()
.Write(chunk.Association.Length.ToString(CultureInfo.CurrentCulture)) .Write(chunk.Association.Length.ToString(CultureInfo.InvariantCulture))
.WriteParameterSeparator() .WriteParameterSeparator()
.WriteBooleanLiteral(false) .WriteBooleanLiteral(false)
.WriteEndMethodInvocation(); .WriteEndMethodInvocation();
@ -334,9 +333,9 @@ namespace Microsoft.AspNet.Razor.CodeGenerators.Visitors
Writer Writer
.WriteParameterSeparator() .WriteParameterSeparator()
.Write(chunk.ValueLocation.AbsoluteIndex.ToString(CultureInfo.CurrentCulture)) .Write(chunk.ValueLocation.AbsoluteIndex.ToString(CultureInfo.InvariantCulture))
.WriteParameterSeparator() .WriteParameterSeparator()
.Write(chunk.Association.Length.ToString(CultureInfo.CurrentCulture)) .Write(chunk.Association.Length.ToString(CultureInfo.InvariantCulture))
.WriteParameterSeparator() .WriteParameterSeparator()
.WriteBooleanLiteral(false) .WriteBooleanLiteral(false)
.WriteEndMethodInvocation(); .WriteEndMethodInvocation();
@ -346,7 +345,7 @@ namespace Microsoft.AspNet.Razor.CodeGenerators.Visitors
Writer Writer
.WriteLocationTaggedString(chunk.Value) .WriteLocationTaggedString(chunk.Value)
.WriteParameterSeparator() .WriteParameterSeparator()
.Write(chunk.Association.Length.ToString(CultureInfo.CurrentCulture)) .Write(chunk.Association.Length.ToString(CultureInfo.InvariantCulture))
.WriteParameterSeparator() .WriteParameterSeparator()
.WriteBooleanLiteral(true) .WriteBooleanLiteral(true)
.WriteEndMethodInvocation(); .WriteEndMethodInvocation();
@ -382,7 +381,7 @@ namespace Microsoft.AspNet.Razor.CodeGenerators.Visitors
.WriteParameterSeparator() .WriteParameterSeparator()
.WriteLocationTaggedString(chunk.Suffix) .WriteLocationTaggedString(chunk.Suffix)
.WriteParameterSeparator() .WriteParameterSeparator()
.Write(attributeCount.ToString(CultureInfo.CurrentCulture)) .Write(attributeCount.ToString(CultureInfo.InvariantCulture))
.WriteEndMethodInvocation(); .WriteEndMethodInvocation();
Accept(chunk.Children); Accept(chunk.Children);

View File

@ -18,7 +18,13 @@ namespace Microsoft.AspNet.Razor.Parser
private void SetUpKeywords() private void SetUpKeywords()
{ {
MapKeywords(ConditionalBlock, CSharpKeyword.For, CSharpKeyword.Foreach, CSharpKeyword.While, CSharpKeyword.Switch, CSharpKeyword.Lock); MapKeywords(
ConditionalBlock,
CSharpKeyword.For,
CSharpKeyword.Foreach,
CSharpKeyword.While,
CSharpKeyword.Switch,
CSharpKeyword.Lock);
MapKeywords(CaseStatement, false, CSharpKeyword.Case, CSharpKeyword.Default); MapKeywords(CaseStatement, false, CSharpKeyword.Case, CSharpKeyword.Default);
MapKeywords(IfStatement, CSharpKeyword.If); MapKeywords(IfStatement, CSharpKeyword.If);
MapKeywords(TryStatement, CSharpKeyword.Try); MapKeywords(TryStatement, CSharpKeyword.Try);
@ -443,7 +449,8 @@ namespace Microsoft.AspNet.Razor.Parser
// Accept whitespace but always keep the last whitespace node so we can put it back if necessary // Accept whitespace but always keep the last whitespace node so we can put it back if necessary
var lastWhitespace = AcceptWhiteSpaceInLines(); var lastWhitespace = AcceptWhiteSpaceInLines();
Debug.Assert(lastWhitespace == null || (lastWhitespace.Start.AbsoluteIndex + lastWhitespace.Content.Length == CurrentLocation.AbsoluteIndex)); Debug.Assert(lastWhitespace == null ||
(lastWhitespace.Start.AbsoluteIndex + lastWhitespace.Content.Length == CurrentLocation.AbsoluteIndex));
if (EndOfFile) if (EndOfFile)
{ {
@ -459,8 +466,8 @@ namespace Microsoft.AspNet.Razor.Parser
var isSingleLineMarkup = type == CSharpSymbolType.Transition && NextIs(CSharpSymbolType.Colon); var isSingleLineMarkup = type == CSharpSymbolType.Transition && NextIs(CSharpSymbolType.Colon);
var isMarkup = isSingleLineMarkup || var isMarkup = isSingleLineMarkup ||
type == CSharpSymbolType.LessThan || type == CSharpSymbolType.LessThan ||
(type == CSharpSymbolType.Transition && NextIs(CSharpSymbolType.LessThan)); (type == CSharpSymbolType.Transition && NextIs(CSharpSymbolType.LessThan));
if (Context.DesignTimeMode || !isMarkup) if (Context.DesignTimeMode || !isMarkup)
{ {
@ -477,8 +484,9 @@ namespace Microsoft.AspNet.Razor.Parser
// MARKUP owns whitespace EXCEPT in DesignTimeMode. // MARKUP owns whitespace EXCEPT in DesignTimeMode.
PutCurrentBack(); PutCurrentBack();
// Don't putback the whitespace if it precedes a '<text>' tag. // Put back the whitespace unless it precedes a '<text>' tag.
if (nextSymbol != null && !nextSymbol.Content.Equals(SyntaxConstants.TextTagName)) if (nextSymbol != null &&
!string.Equals(nextSymbol.Content, SyntaxConstants.TextTagName, StringComparison.Ordinal))
{ {
PutBack(lastWhitespace); PutBack(lastWhitespace);
} }
@ -496,7 +504,8 @@ namespace Microsoft.AspNet.Razor.Parser
// Markup block // Markup block
Output(SpanKind.Code); Output(SpanKind.Code);
if (Context.DesignTimeMode && CurrentSymbol != null && (CurrentSymbol.Type == CSharpSymbolType.LessThan || CurrentSymbol.Type == CSharpSymbolType.Transition)) if (Context.DesignTimeMode && CurrentSymbol != null &&
(CurrentSymbol.Type == CSharpSymbolType.LessThan || CurrentSymbol.Type == CSharpSymbolType.Transition))
{ {
PutCurrentBack(); PutCurrentBack();
} }
@ -600,7 +609,9 @@ namespace Microsoft.AspNet.Razor.Parser
sym.Type != CSharpSymbolType.LeftParenthesis && sym.Type != CSharpSymbolType.LeftParenthesis &&
sym.Type != CSharpSymbolType.LeftBracket && sym.Type != CSharpSymbolType.LeftBracket &&
sym.Type != CSharpSymbolType.RightBrace); sym.Type != CSharpSymbolType.RightBrace);
if (At(CSharpSymbolType.LeftBrace) || At(CSharpSymbolType.LeftParenthesis) || At(CSharpSymbolType.LeftBracket)) if (At(CSharpSymbolType.LeftBrace) ||
At(CSharpSymbolType.LeftParenthesis) ||
At(CSharpSymbolType.LeftBracket))
{ {
Accept(read); Accept(read);
if (Balance(BalancingModes.AllowCommentsAndTemplates | BalancingModes.BacktrackOnFailure)) if (Balance(BalancingModes.AllowCommentsAndTemplates | BalancingModes.BacktrackOnFailure))

View File

@ -4,8 +4,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using Microsoft.AspNet.Razor.Editor;
using Microsoft.AspNet.Razor.Chunks.Generators; using Microsoft.AspNet.Razor.Chunks.Generators;
using Microsoft.AspNet.Razor.Editor;
using Microsoft.AspNet.Razor.Parser.SyntaxTree; using Microsoft.AspNet.Razor.Parser.SyntaxTree;
using Microsoft.AspNet.Razor.Tokenizer; using Microsoft.AspNet.Razor.Tokenizer;
using Microsoft.AspNet.Razor.Tokenizer.Symbols; using Microsoft.AspNet.Razor.Tokenizer.Symbols;
@ -103,12 +103,16 @@ namespace Microsoft.AspNet.Razor.Parser
[Conditional("DEBUG")] [Conditional("DEBUG")]
internal void Assert(CSharpKeyword expectedKeyword) internal void Assert(CSharpKeyword expectedKeyword)
{ {
Debug.Assert(CurrentSymbol.Type == CSharpSymbolType.Keyword && CurrentSymbol.Keyword.HasValue && CurrentSymbol.Keyword.Value == expectedKeyword); Debug.Assert(CurrentSymbol.Type == CSharpSymbolType.Keyword &&
CurrentSymbol.Keyword.HasValue &&
CurrentSymbol.Keyword.Value == expectedKeyword);
} }
protected internal bool At(CSharpKeyword keyword) protected internal bool At(CSharpKeyword keyword)
{ {
return At(CSharpSymbolType.Keyword) && CurrentSymbol.Keyword.HasValue && CurrentSymbol.Keyword.Value == keyword; return At(CSharpSymbolType.Keyword) &&
CurrentSymbol.Keyword.HasValue &&
CurrentSymbol.Keyword.Value == keyword;
} }
protected internal bool AcceptIf(CSharpKeyword keyword) protected internal bool AcceptIf(CSharpKeyword keyword)
@ -145,7 +149,9 @@ namespace Microsoft.AspNet.Razor.Parser
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
var current = CurrentSymbol; var current = CurrentSymbol;
if (At(CSharpSymbolType.StringLiteral) && CurrentSymbol.Content.Length > 0 && CurrentSymbol.Content[0] == SyntaxConstants.TransitionCharacter) if (At(CSharpSymbolType.StringLiteral) &&
CurrentSymbol.Content.Length > 0 &&
CurrentSymbol.Content[0] == SyntaxConstants.TransitionCharacter)
{ {
var split = Language.SplitSymbol(CurrentSymbol, 1, CSharpSymbolType.Transition); var split = Language.SplitSymbol(CurrentSymbol, 1, CSharpSymbolType.Transition);
current = split.Item1; current = split.Item1;
@ -222,11 +228,15 @@ namespace Microsoft.AspNet.Razor.Parser
} }
else else
{ {
if (CurrentSymbol.Content.Equals(SyntaxConstants.CSharp.HelperKeyword)) if (string.Equals(
CurrentSymbol.Content,
SyntaxConstants.CSharp.HelperKeyword,
StringComparison.Ordinal))
{ {
Context.OnError( Context.OnError(
CurrentLocation, CurrentLocation,
RazorResources.FormatParseError_HelperDirectiveNotAvailable(SyntaxConstants.CSharp.HelperKeyword), RazorResources.FormatParseError_HelperDirectiveNotAvailable(
SyntaxConstants.CSharp.HelperKeyword),
CurrentSymbol.Content.Length); CurrentSymbol.Content.Length);
} }
@ -380,7 +390,8 @@ namespace Microsoft.AspNet.Razor.Parser
{ {
if (!EndOfFile) if (!EndOfFile)
{ {
if (CurrentSymbol.Type == CSharpSymbolType.LeftParenthesis || CurrentSymbol.Type == CSharpSymbolType.LeftBracket) if (CurrentSymbol.Type == CSharpSymbolType.LeftParenthesis ||
CurrentSymbol.Type == CSharpSymbolType.LeftBracket)
{ {
// If we end within "(", whitespace is fine // If we end within "(", whitespace is fine
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any;
@ -541,7 +552,9 @@ namespace Microsoft.AspNet.Razor.Parser
using (PushSpanConfig(ConfigureExplicitExpressionSpan)) using (PushSpanConfig(ConfigureExplicitExpressionSpan))
{ {
var success = Balance( var success = Balance(
BalancingModes.BacktrackOnFailure | BalancingModes.NoErrorOnFailure | BalancingModes.AllowCommentsAndTemplates, BalancingModes.BacktrackOnFailure |
BalancingModes.NoErrorOnFailure |
BalancingModes.AllowCommentsAndTemplates,
CSharpSymbolType.LeftParenthesis, CSharpSymbolType.LeftParenthesis,
CSharpSymbolType.RightParenthesis, CSharpSymbolType.RightParenthesis,
block.Start); block.Start);

View File

@ -6,9 +6,9 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using Microsoft.AspNet.Razor.Chunks.Generators; using Microsoft.AspNet.Razor.Chunks.Generators;
using Microsoft.AspNet.Razor.Compilation.TagHelpers;
using Microsoft.AspNet.Razor.Parser.SyntaxTree; using Microsoft.AspNet.Razor.Parser.SyntaxTree;
using Microsoft.AspNet.Razor.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers;
using Microsoft.AspNet.Razor.Compilation.TagHelpers;
using Microsoft.AspNet.Razor.Tokenizer.Symbols; using Microsoft.AspNet.Razor.Tokenizer.Symbols;
namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
@ -119,7 +119,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
var childSpan = beginTagBlock.FindLastDescendentSpan(); var childSpan = beginTagBlock.FindLastDescendentSpan();
// Self-closing tags are always valid despite descriptors[X].TagStructure. // Self-closing tags are always valid despite descriptors[X].TagStructure.
if (childSpan?.Content.EndsWith("/>") ?? false) if (childSpan?.Content.EndsWith("/>", StringComparison.Ordinal) ?? false)
{ {
return TagMode.SelfClosing; return TagMode.SelfClosing;
} }

View File

@ -107,7 +107,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers
} }
var directiveText = span.Content.Trim(); var directiveText = span.Content.Trim();
var startOffset = span.Content.IndexOf(directiveText); var startOffset = span.Content.IndexOf(directiveText, StringComparison.Ordinal);
var offsetContent = span.Content.Substring(0, startOffset); var offsetContent = span.Content.Substring(0, startOffset);
var offsetTextLocation = SourceLocation.Advance(span.Start, offsetContent); var offsetTextLocation = SourceLocation.Advance(span.Start, offsetContent);
var directiveDescriptor = new TagHelperDirectiveDescriptor var directiveDescriptor = new TagHelperDirectiveDescriptor

View File

@ -5,9 +5,9 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using Microsoft.AspNet.Razor.Compilation.TagHelpers;
using Microsoft.AspNet.Razor.Parser.SyntaxTree; using Microsoft.AspNet.Razor.Parser.SyntaxTree;
using Microsoft.AspNet.Razor.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers;
using Microsoft.AspNet.Razor.Compilation.TagHelpers;
using Microsoft.AspNet.Razor.Tokenizer.Symbols; using Microsoft.AspNet.Razor.Tokenizer.Symbols;
namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
@ -663,7 +663,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
{ {
var childSpan = childBlock.FindLastDescendentSpan(); var childSpan = childBlock.FindLastDescendentSpan();
return childSpan?.Content.EndsWith("/>") ?? false; return childSpan?.Content.EndsWith("/>", StringComparison.Ordinal) ?? false;
} }
private void PushTrackerStack(TagBlockTracker tracker) private void PushTrackerStack(TagBlockTracker tracker)

View File

@ -12,45 +12,65 @@ using Microsoft.AspNet.Razor.Text;
namespace Microsoft.AspNet.Razor namespace Microsoft.AspNet.Razor
{ {
/// <summary> /// <summary>
/// Parser used by editors to avoid reparsing the entire document on each text change /// Parser used by editors to avoid reparsing the entire document on each text change.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// <para>
/// This parser is designed to allow editors to avoid having to worry about incremental parsing. /// This parser is designed to allow editors to avoid having to worry about incremental parsing.
/// The CheckForStructureChanges method can be called with every change made by a user in an editor and /// The <see cref="CheckForStructureChanges"/> method can be called with every change made by a user in an editor
/// the parser will provide a result indicating if it was able to incrementally reparse the document. /// and the parser will provide a result indicating if it was able to incrementally apply the change.
/// /// </para>
/// <para>
/// The general workflow for editors with this parser is: /// The general workflow for editors with this parser is:
/// 0. User edits document /// <list type="number">
/// 1. Editor builds TextChange structure describing the edit and providing a reference to the _updated_ text buffer /// <item><description>User edits document.</description></item>
/// 2. Editor calls CheckForStructureChanges passing in that change. /// <item><description>Editor builds a <see cref="TextChange"/> structure describing the edit and providing a
/// 3. Parser determines if the change can be simply applied to an existing parse tree node /// reference to the <em>updated</em> text buffer.</description></item>
/// a. If it can, the Parser updates its parse tree and returns PartialParseResult.Accepted /// <item><description>Editor calls <see cref="CheckForStructureChanges"/> passing in that change.
/// b. If it can not, the Parser starts a background parse task and return PartialParseResult.Rejected /// </description></item>
/// NOTE: Additional flags can be applied to the PartialParseResult, see that enum for more details. However, /// <item><description>Parser determines if the change can be simply applied to an existing parse tree node.
/// the Accepted or Rejected flags will ALWAYS be present /// </description></item>
/// /// <list type="number">
/// A change can only be incrementally parsed if a single, unique, Span (see Microsoft.AspNet.Razor.Parser.SyntaxTree) in the syntax tree can /// <item><description>If it can, the Parser updates its parse tree and returns
/// be identified as owning the entire change. For example, if a change overlaps with multiple spans, the change cannot be /// <see cref="PartialParseResult.Accepted"/>.</description></item>
/// parsed incrementally and a full reparse is necessary. A Span "owns" a change if the change occurs either a) entirely /// <item><description>If it cannot, the Parser starts a background parse task and returns
/// within it's boundaries or b) it is a pure insertion (see TextChange) at the end of a Span whose CanGrow flag (see Span) is /// <see cref="PartialParseResult.Rejected"/>.</description></item>
/// true. /// </list>
/// /// </list>
/// Even if a single unique Span owner can be identified, it's possible the edit will cause the Span to split or merge with other /// NOTE: Additional flags can be applied to the <see cref="PartialParseResult"/>, see that <c>enum</c> for more
/// Spans, in which case, a full reparse is necessary to identify the extent of the changes to the tree. /// details. However, the <see cref="PartialParseResult.Accepted"/> or <see cref="PartialParseResult.Rejected"/>
/// /// flags will ALWAYS be present.
/// When the RazorEditorParser returns Accepted, it updates CurrentParseTree immediately. However, the editor is expected to /// </para>
/// update it's own data structures independently. It can use CurrentParseTree to do this, as soon as the editor returns from /// <para>
/// CheckForStructureChanges, but it should (ideally) have logic for doing so without needing the new tree. /// A change can only be incrementally parsed if a single, unique, <see cref="Span"/> (see
/// /// <see cref="Parser.SyntaxTree"/>) in the syntax tree can be identified as owning the entire change.
/// When Rejected is returned by CheckForStructureChanges, a background parse task has _already_ been started. When that task /// For example, if a change overlaps with multiple <see cref="Span"/>s, the change cannot be parsed incrementally
/// finishes, the DocumentStructureChanged event will be fired containing the new generated code, parse tree and a reference to /// and a full reparse is necessary. A <see cref="Span"/> "owns" a change if the change occurs either a) entirely
/// the original TextChange that caused the reparse, to allow the editor to resolve the new tree against any changes made since /// within it's boundaries or b) it is a pure insertion (see <see cref="TextChange"/>) at the end of a
/// calling CheckForStructureChanges. /// <see cref="Span"/> whose <see cref="Span.EditHandler"/> can accept the change (see
/// /// <see cref="SpanEditHandler.CanAcceptChange"/>).
/// If a call to CheckForStructureChanges occurs while a reparse is already in-progress, the reparse is cancelled IMMEDIATELY /// </para>
/// and Rejected is returned without attempting to reparse. This means that if a conusmer calls CheckForStructureChanges, which /// <para>
/// returns Rejected, then calls it again before DocumentParseComplete is fired, it will only recieve one DocumentParseComplete /// When the <see cref="RazorEditorParser"/> returns <see cref="PartialParseResult.Accepted"/>, it updates
/// event, for the second change. /// <see cref="CurrentParseTree"/> immediately. However, the editor is expected to update it's own data structures
/// independently. It can use <see cref="CurrentParseTree"/> to do this, as soon as the editor returns from
/// <see cref="CheckForStructureChanges"/>, but it should (ideally) have logic for doing so without needing the new
/// tree.
/// </para>
/// <para>
/// When <see cref="PartialParseResult.Rejected"/> is returned by <see cref="CheckForStructureChanges"/>, a
/// background parse task has <em>already</em> been started. When that task finishes, the
/// <see cref="DocumentParseComplete"/> event will be fired containing the new generated code, parse tree and a
/// reference to the original <see cref="TextChange"/> that caused the reparse, to allow the editor to resolve the
/// new tree against any changes made since calling <see cref="CheckForStructureChanges"/>.
/// </para>
/// <para>
/// If a call to <see cref="CheckForStructureChanges"/> occurs while a reparse is already in-progress, the reparse
/// is canceled IMMEDIATELY and <see cref="PartialParseResult.Rejected"/> is returned without attempting to
/// reparse. This means that if a consumer calls <see cref="CheckForStructureChanges"/>, which returns
/// <see cref="PartialParseResult.Rejected"/>, then calls it again before <see cref="DocumentParseComplete"/> is
/// fired, it will only receive one <see cref="DocumentParseComplete"/> event, for the second change.
/// </para>
/// </remarks> /// </remarks>
public class RazorEditorParser : IDisposable public class RazorEditorParser : IDisposable
{ {
@ -61,11 +81,13 @@ namespace Microsoft.AspNet.Razor
private Block _currentParseTree; private Block _currentParseTree;
/// <summary> /// <summary>
/// Constructs the editor parser. One instance should be used per active editor. This /// Constructs the editor parser. One instance should be used per active editor. This
/// instance _can_ be shared among reparses, but should _never_ be shared between documents. /// instance <em>can</em> be shared among reparses, but should <em>never</em> be shared between documents.
/// </summary> /// </summary>
/// <param name="host">The <see cref="RazorEngineHost"/> which defines the environment in which the generated code will live. <see cref="F:RazorEngineHost.DesignTimeMode"/> should be set if design-time code mappings are desired</param> /// <param name="host">The <see cref="RazorEngineHost"/> which defines the environment in which the generated
/// <param name="sourceFileName">The physical path to use in line pragmas</param> /// code will live. <see cref="RazorEngineHost.DesignTimeMode"/> should be set if design-time behavior is
/// desired.</param>
/// <param name="sourceFileName">The physical path to use in line pragmas.</param>
public RazorEditorParser(RazorEngineHost host, string sourceFileName) public RazorEditorParser(RazorEngineHost host, string sourceFileName)
{ {
if (host == null) if (host == null)
@ -86,7 +108,7 @@ namespace Microsoft.AspNet.Razor
} }
/// <summary> /// <summary>
/// Event fired when a full reparse of the document completes /// Event fired when a full reparse of the document completes.
/// </summary> /// </summary>
public event EventHandler<DocumentParseCompleteEventArgs> DocumentParseComplete; public event EventHandler<DocumentParseCompleteEventArgs> DocumentParseComplete;
@ -112,19 +134,20 @@ namespace Microsoft.AspNet.Razor
} }
/// <summary> /// <summary>
/// Determines if a change will cause a structural change to the document and if not, applies it to the existing tree. /// Determines if a change will cause a structural change to the document and if not, applies it to the
/// If a structural change would occur, automatically starts a reparse /// existing tree. If a structural change would occur, automatically starts a reparse.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// NOTE: The initial incremental parsing check and actual incremental parsing (if possible) occurs /// NOTE: The initial incremental parsing check and actual incremental parsing (if possible) occurs
/// on the callers thread. However, if a full reparse is needed, this occurs on a background thread. /// on the caller's thread. However, if a full reparse is needed, this occurs on a background thread.
/// </remarks> /// </remarks>
/// <param name="change">The change to apply to the parse tree</param> /// <param name="change">The change to apply to the parse tree.</param>
/// <returns>A PartialParseResult value indicating the result of the incremental parse</returns> /// <returns>A <see cref="PartialParseResult"/> value indicating the result of the incremental parse.</returns>
public virtual PartialParseResult CheckForStructureChanges(TextChange change) public virtual PartialParseResult CheckForStructureChanges(TextChange change)
{ {
// Validate the change // Validate the change
long? elapsedMs = null; long? elapsedMs = null;
#if EDITOR_TRACING #if EDITOR_TRACING
var sw = new Stopwatch(); var sw = new Stopwatch();
sw.Start(); sw.Start();
@ -132,9 +155,9 @@ namespace Microsoft.AspNet.Razor
RazorEditorTrace.TraceLine(RazorResources.FormatTrace_EditorReceivedChange(Path.GetFileName(FileName), change)); RazorEditorTrace.TraceLine(RazorResources.FormatTrace_EditorReceivedChange(Path.GetFileName(FileName), change));
if (change.NewBuffer == null) if (change.NewBuffer == null)
{ {
throw new ArgumentException(RazorResources.FormatStructure_Member_CannotBeNull( throw new ArgumentException(
nameof(change.NewBuffer), RazorResources.FormatStructure_Member_CannotBeNull(nameof(change.NewBuffer), nameof(TextChange)),
nameof(TextChange)), nameof(change)); nameof(change));
} }
var result = PartialParseResult.Rejected; var result = PartialParseResult.Rejected;
@ -168,16 +191,18 @@ namespace Microsoft.AspNet.Razor
elapsedMs = sw.ElapsedMilliseconds; elapsedMs = sw.ElapsedMilliseconds;
sw.Reset(); sw.Reset();
#endif #endif
RazorEditorTrace.TraceLine(
RazorResources.FormatTrace_EditorProcessedChange( RazorEditorTrace.TraceLine(RazorResources.FormatTrace_EditorProcessedChange(
Path.GetFileName(FileName), Path.GetFileName(FileName),
changeString, elapsedMs.HasValue ? elapsedMs.Value.ToString(CultureInfo.InvariantCulture) : "?", changeString,
result.ToString())); elapsedMs.HasValue ? elapsedMs.Value.ToString(CultureInfo.InvariantCulture) : "?",
result.ToString()));
return result; return result;
} }
/// <summary> /// <summary>
/// Disposes of this parser. Should be called when the editor window is closed and the document is unloaded. /// Disposes of this parser. Should be called when the editor window is closed and the document is unloaded.
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {
@ -235,6 +260,7 @@ namespace Microsoft.AspNet.Razor
_lastAutoCompleteSpan = null; _lastAutoCompleteSpan = null;
} }
} }
return result; return result;
} }