提交 539cfa82 编写于 作者: P Pharring

PERF: Reuse the XmlReader used to parse Xml doc comments (changeset 1342552)

上级 751847f2
......@@ -67,9 +67,9 @@ internal sealed class DocumentationComment
private DocumentationComment()
{
ParameterNames = ImmutableArray.Create<string>();
TypeParameterNames = ImmutableArray.Create<string>();
ExceptionTypes = ImmutableArray.Create<string>();
ParameterNames = ImmutableArray<string>.Empty;
TypeParameterNames = ImmutableArray<string>.Empty;
ExceptionTypes = ImmutableArray<string>.Empty;
}
/// <summary>
......@@ -79,126 +79,150 @@ private DocumentationComment()
/// <returns>A DocumentationComment instance.</returns>
public static DocumentationComment FromXmlFragment(string xml)
{
try
return CommentBuilder.Parse(xml);
}
/// <summary>
/// Helper class for parsing XML doc comments. Encapsulates the state required during parsing.
/// </summary>
private class CommentBuilder
{
private readonly DocumentationComment comment;
private ImmutableArray<string>.Builder parameterNamesBuilder = null;
private ImmutableArray<string>.Builder typeParameterNamesBuilder = null;
private ImmutableArray<string>.Builder exceptionTypesBuilder = null;
private Dictionary<string, ImmutableArray<string>.Builder> exceptionTextBuilders = null;
/// <summary>
/// Parse and construct a <see cref="DocumentationComment" /> from the given fragment of XML.
/// </summary>
/// <param name="xml">The fragment of XML to parse.</param>
/// <returns>A DocumentationComment instance.</returns>
public static DocumentationComment Parse(string xml)
{
// TODO: probably want to preserve whitespace (DevDiv #13045).
XmlReader reader = XmlReader.Create(new StringReader(xml), new XmlReaderSettings { ConformanceLevel = ConformanceLevel.Fragment });
try
{
return new CommentBuilder(xml).ParseInternal(xml);
}
catch (Exception)
{
// It would be nice if we only had to catch XmlException to handle invalid XML
// while parsing doc comments. Unfortunately, other exceptions can also occur,
// so we just catch them all. See Dev12 Bug 612456 for an example.
return new DocumentationComment { FullXmlFragment = xml, HadXmlParseError = true };
}
}
DocumentationComment comment = new DocumentationComment();
comment.FullXmlFragment = xml;
private CommentBuilder(string xml)
{
comment = new DocumentationComment() { FullXmlFragment = xml };
}
List<string> parameterNamesBuilder = new List<string>();
List<string> typeParameterNamesBuilder = new List<string>();
List<string> exceptionTypesBuilder = new List<string>();
private DocumentationComment ParseInternal(string xml)
{
XmlFragmentParser.ParseFragment(xml, ParseCallback, this);
try
if (exceptionTextBuilders != null)
{
Dictionary<string, List<string>> exceptionTextBuilders = new Dictionary<string, List<string>>();
foreach (var typeAndBuilderPair in exceptionTextBuilders)
{
comment.exceptionTexts.Add(typeAndBuilderPair.Key, typeAndBuilderPair.Value.AsImmutable());
}
}
comment.ParameterNames = parameterNamesBuilder == null ? ImmutableArray<string>.Empty : parameterNamesBuilder.ToImmutable();
comment.TypeParameterNames = typeParameterNamesBuilder == null ? ImmutableArray<string>.Empty : typeParameterNamesBuilder.ToImmutable();
comment.ExceptionTypes = exceptionTypesBuilder == null ? ImmutableArray<string>.Empty : exceptionTypesBuilder.ToImmutable();
return comment;
}
while (!reader.EOF)
private static void ParseCallback(XmlReader reader, CommentBuilder builder)
{
builder.ParseCallback(reader);
}
private void ParseCallback(XmlReader reader)
{
if (reader.NodeType == XmlNodeType.Element)
{
string localName = reader.LocalName;
if (XmlNames.ElementEquals(localName, XmlNames.ExampleElementName) && comment.ExampleText == null)
{
comment.ExampleText = reader.ReadInnerXml().Trim(); // TODO: trim each line
}
else if (XmlNames.ElementEquals(localName, XmlNames.SummaryElementName) && comment.SummaryText == null)
{
comment.SummaryText = reader.ReadInnerXml().Trim(); // TODO: trim each line
}
else if (XmlNames.ElementEquals(localName, XmlNames.ReturnsElementName) && comment.ReturnsText == null)
{
comment.ReturnsText = reader.ReadInnerXml().Trim(); // TODO: trim each line
}
else if (XmlNames.ElementEquals(localName, XmlNames.RemarksElementName) && comment.RemarksText == null)
{
comment.RemarksText = reader.ReadInnerXml().Trim(); // TODO: trim each line
}
else if (XmlNames.ElementEquals(localName, XmlNames.ParameterElementName))
{
if (reader.IsStartElement())
string name = reader.GetAttribute(XmlNames.NameAttributeName);
string paramText = reader.ReadInnerXml();
if (!string.IsNullOrWhiteSpace(name) && !comment.parameterTexts.ContainsKey(name))
{
string localName = reader.LocalName;
if (XmlNames.ElementEquals(localName, XmlNames.ExampleElementName) && comment.ExampleText == null)
{
comment.ExampleText = reader.ReadInnerXml().Trim(); // TODO: trim each line
}
else if (XmlNames.ElementEquals(localName, XmlNames.SummaryElementName) && comment.SummaryText == null)
{
comment.SummaryText = reader.ReadInnerXml().Trim(); // TODO: trim each line
}
else if (XmlNames.ElementEquals(localName, XmlNames.ReturnsElementName) && comment.ReturnsText == null)
{
comment.ReturnsText = reader.ReadInnerXml().Trim(); // TODO: trim each line
}
else if (XmlNames.ElementEquals(localName, XmlNames.RemarksElementName) && comment.RemarksText == null)
{
comment.RemarksText = reader.ReadInnerXml().Trim(); // TODO: trim each line
}
else if (XmlNames.ElementEquals(localName, XmlNames.ParameterElementName))
{
string name = reader.GetAttribute(XmlNames.NameAttributeName);
string paramText = reader.ReadInnerXml();
if (!string.IsNullOrWhiteSpace(name) && !comment.parameterTexts.ContainsKey(name))
{
parameterNamesBuilder.Add(name);
comment.parameterTexts.Add(name, paramText.Trim()); // TODO: trim each line
}
}
else if (XmlNames.ElementEquals(localName, XmlNames.TypeParameterElementName))
{
string name = reader.GetAttribute(XmlNames.NameAttributeName);
string typeParamText = reader.ReadInnerXml();
if (!string.IsNullOrWhiteSpace(name) && !comment.typeParameterTexts.ContainsKey(name))
{
typeParameterNamesBuilder.Add(name);
comment.typeParameterTexts.Add(name, typeParamText.Trim()); // TODO: trim each line
}
}
else if (XmlNames.ElementEquals(localName, XmlNames.ExceptionElementName))
{
string type = reader.GetAttribute(XmlNames.CrefAttributeName);
string exceptionText = reader.ReadInnerXml();
if (!string.IsNullOrWhiteSpace(type))
{
if (!exceptionTextBuilders.ContainsKey(type))
{
exceptionTypesBuilder.Add(type);
exceptionTextBuilders.Add(type, new List<string>());
}
exceptionTextBuilders[type].Add(exceptionText);
}
}
else if (XmlNames.ElementEquals(localName, XmlNames.CompletionListElementName))
{
string cref = reader.GetAttribute(XmlNames.CrefAttributeName);
if (!string.IsNullOrWhiteSpace(cref))
{
comment.CompletionListCref = cref;
}
(parameterNamesBuilder ?? (parameterNamesBuilder = ImmutableArray.CreateBuilder<string>())).Add(name);
comment.parameterTexts.Add(name, paramText.Trim()); // TODO: trim each line
}
}
else if (XmlNames.ElementEquals(localName, XmlNames.TypeParameterElementName))
{
string name = reader.GetAttribute(XmlNames.NameAttributeName);
string typeParamText = reader.ReadInnerXml();
reader.ReadInnerXml();
}
else
if (!string.IsNullOrWhiteSpace(name) && !comment.typeParameterTexts.ContainsKey(name))
{
(typeParameterNamesBuilder ?? (typeParameterNamesBuilder = ImmutableArray.CreateBuilder<string>())).Add(name);
comment.typeParameterTexts.Add(name, typeParamText.Trim()); // TODO: trim each line
}
}
else if (XmlNames.ElementEquals(localName, XmlNames.ExceptionElementName))
{
string type = reader.GetAttribute(XmlNames.CrefAttributeName);
string exceptionText = reader.ReadInnerXml();
if (!string.IsNullOrWhiteSpace(type))
{
if (exceptionTextBuilders == null || !exceptionTextBuilders.ContainsKey(type))
{
// This is an element we don't handle. Skip it.
reader.Read();
(exceptionTypesBuilder ?? (exceptionTypesBuilder = ImmutableArray.CreateBuilder<string>())).Add(type);
(exceptionTextBuilders ?? (exceptionTextBuilders = new Dictionary<string, ImmutableArray<string>.Builder>())).Add(type, ImmutableArray.CreateBuilder<string>());
}
exceptionTextBuilders[type].Add(exceptionText);
}
else
}
else if (XmlNames.ElementEquals(localName, XmlNames.CompletionListElementName))
{
string cref = reader.GetAttribute(XmlNames.CrefAttributeName);
if (!string.IsNullOrWhiteSpace(cref))
{
// We came across something that isn't a start element, like a block of text.
// Skip it.
reader.Read();
comment.CompletionListCref = cref;
}
}
foreach (var typeAndBuilderPair in exceptionTextBuilders)
reader.ReadInnerXml();
}
else
{
comment.exceptionTexts.Add(typeAndBuilderPair.Key,
typeAndBuilderPair.Value.AsImmutable());
// This is an element we don't handle. Skip it.
reader.Read();
}
}
finally
else
{
comment.ParameterNames = parameterNamesBuilder.AsImmutable();
comment.TypeParameterNames = typeParameterNamesBuilder.AsImmutable();
comment.ExceptionTypes = exceptionTypesBuilder.AsImmutable();
// We came across something that isn't a start element, like a block of text.
// Skip it.
reader.Read();
}
return comment;
}
catch (Exception)
{
// It would be nice if we only had to catch XmlException to handle invalid XML
// while parsing doc comments. Unfortunately, other exceptions can also occur,
// so we just catch them all. See Dev12 Bug 612456 for an example.
return new DocumentationComment { FullXmlFragment = xml, HadXmlParseError = true };
}
}
......
using System;
using System.Diagnostics;
using System.IO;
using System.Xml;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Shared.Utilities
{
/// <summary>
/// An XML parser that is designed to parse small fragments of XML such as those that appear in documentation comments.
/// PERF: We try to re-use the same underlying <see cref="XmlReader"/> to reduce the allocation costs of multiple parses.
/// </summary>
internal sealed class XmlFragmentParser
{
private XmlReader xmlReader;
private readonly Reader textReader = new Reader();
private static readonly ObjectPool<XmlFragmentParser> pool =
new ObjectPool<XmlFragmentParser>(() => new XmlFragmentParser(), size: 2);
/// <summary>
/// Parse the given XML fragment. The given callback is executed until either the end of the fragment
/// is reached or an exception occurs.
/// </summary>
/// <typeparam name="TArg">Type of an additional argument passed to the <paramref name="callback"/> delegate.</typeparam>
/// <param name="xmlFragment">The fragment to parse.</param>
/// <param name="callback">Action to execute while there is still more to read.</param>
/// <param name="arg">Additional argument passed to the callback.</param>
/// <remarks>
/// It is important that the <paramref name="callback"/> action advances the <see cref="XmlReader"/>,
/// otherwise parsing will never complete.
/// </remarks>
public static void ParseFragment<TArg>(string xmlFragment, Action<XmlReader, TArg> callback, TArg arg)
{
var instance = pool.Allocate();
try
{
instance.ParseInternal(xmlFragment, callback, arg);
}
finally
{
pool.Free(instance);
}
}
private void ParseInternal<TArg>(string text, Action<XmlReader, TArg> callback, TArg arg)
{
textReader.SetText(text);
if (xmlReader == null)
{
xmlReader = XmlReader.Create(textReader);
}
try
{
while (!ReachedEnd)
{
if (BeforeStart)
{
// Skip over the synthetic root element and first node
xmlReader.Read();
}
else
{
callback(xmlReader, arg);
}
}
// Read the final EndElement to reset things for the next user.
xmlReader.ReadEndElement();
}
catch
{
// The reader is in a bad state, so dispose of it and recreate a new one next time we get called.
xmlReader.Dispose();
xmlReader = null;
textReader.Reset();
throw;
}
}
private bool BeforeStart
{
get
{
// Depth 0 = Document root
// Depth 1 = Synthetic wrapper, "CurrentElement"
// Depth 2 = Start of user's fragment.
return xmlReader.Depth < 2;
}
}
private bool ReachedEnd
{
get
{
return xmlReader.Depth == 1
&& xmlReader.NodeType == XmlNodeType.EndElement
&& xmlReader.LocalName == Reader.CurrentElementName;
}
}
/// <summary>
/// A text reader over a synthesized XML stream consisting of a single root element followed by a potentially
/// infinite stream of fragments. Each time "SetText" is called the stream is rewound to the element immediately
/// following the synthetic root node.
/// </summary>
private sealed class Reader : TextReader
{
/// <summary>
/// Current text to validate.
/// </summary>
private string text;
private int position;
// Base the root element name on a GUID to avoid accidental (or intentional) collisions. An underscore is
// prefixed because element names must not start with a number.
private static readonly string RootElementName = "_" + Guid.NewGuid().ToString("N");
// We insert an extra synthetic element name to allow for raw text at the root
internal static readonly string CurrentElementName = "_" + Guid.NewGuid().ToString("N");
private static readonly string RootStart = "<" + RootElementName + ">";
private static readonly string CurrentStart = "<" + CurrentElementName + ">";
private static readonly string CurrentEnd = "</" + CurrentElementName + ">";
public void Reset()
{
this.text = null;
this.position = 0;
}
public void SetText(string text)
{
this.text = text;
// The first read shall read the <root>,
// the subsequents reads shall start with <current> element
if (this.position > 0)
{
this.position = RootStart.Length;
}
}
public override int Read(char[] buffer, int index, int count)
{
if (count == 0)
{
return 0;
}
// The stream synthesizes an XML document with:
// 1. A root element start tag
// 2. Current element start tag
// 3. The user text (xml fragments)
// 4. Current element end tag
int initialCount = count;
// <root>
position += EncodeAndAdvance(RootStart, position, buffer, ref index, ref count);
// <current>
position += EncodeAndAdvance(CurrentStart, position - RootStart.Length, buffer, ref index, ref count);
// text
position += EncodeAndAdvance(text, position - RootStart.Length - CurrentStart.Length, buffer, ref index, ref count);
// </current>
position += EncodeAndAdvance(CurrentEnd, position - RootStart.Length - CurrentStart.Length - text.Length, buffer, ref index, ref count);
// Pretend that the stream is infinite, i.e. never return 0 characters read.
if (initialCount == count)
{
buffer[index++] = ' ';
count--;
}
return initialCount - count;
}
private static int EncodeAndAdvance(string src, int srcIndex, char[] dest, ref int destIndex, ref int destCount)
{
if (destCount == 0 || srcIndex < 0 || srcIndex >= src.Length)
{
return 0;
}
int charCount = Math.Min(src.Length - srcIndex, destCount);
Debug.Assert(charCount > 0);
src.CopyTo(srcIndex, dest, destIndex, charCount);
destIndex += charCount;
destCount -= charCount;
Debug.Assert(destCount >= 0);
return charCount;
}
public override int Read()
{
// XmlReader does not call this API
throw ExceptionUtilities.Unreachable;
}
public override int Peek()
{
// XmlReader does not call this API
throw ExceptionUtilities.Unreachable;
}
}
}
}
......@@ -355,6 +355,7 @@
<Compile Include="Log\Logger.LogBlock.cs" />
<Compile Include="Log\LogMessage.cs" />
<Compile Include="Shared\Extensions\ITypeSymbolExtensions.ReplaceTypeParameterBasedOnTypeConstraintVisitor.cs" />
<Compile Include="Shared\Utilities\XmlFragmentParser.cs" />
<Compile Include="Simplification\SimplifyTypeNameCodeAction.cs" />
<Compile Include="Utilities\WeakEventHandler.cs" />
<Compile Include="Versions\Extensions.cs" />
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册