From 7539a429c684c5e538b99fc17a39eee861e8989d Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 21 Aug 2019 13:04:30 +0200 Subject: [PATCH 01/62] Port rich text tooltips from SharpDevelop to ILSpy. --- .../Documentation/DocumentationElement.cs | 285 ++++++ .../ICSharpCode.Decompiler.csproj | 27 + ICSharpCode.Decompiler/Xml/AXmlAttribute.cs | 116 +++ ICSharpCode.Decompiler/Xml/AXmlDocument.cs | 55 ++ ICSharpCode.Decompiler/Xml/AXmlElement.cs | 243 +++++ ICSharpCode.Decompiler/Xml/AXmlObject.cs | 245 +++++ ICSharpCode.Decompiler/Xml/AXmlParser.cs | 142 +++ ICSharpCode.Decompiler/Xml/AXmlReader.cs | 477 ++++++++++ ICSharpCode.Decompiler/Xml/AXmlTag.cs | 96 ++ ICSharpCode.Decompiler/Xml/AXmlText.cs | 63 ++ ICSharpCode.Decompiler/Xml/AXmlVisitor.cs | 62 ++ .../Xml/AnchorMovementType.cs | 43 + .../Xml/DocumentationElement.cs | 254 ++++++ ICSharpCode.Decompiler/Xml/ISegment.cs | 69 ++ ICSharpCode.Decompiler/Xml/ITextSource.cs | 218 +++++ .../Xml/IncrementalParserState.cs | 114 +++ .../Xml/InternalDocument.cs | 175 ++++ ICSharpCode.Decompiler/Xml/Log.cs | 88 ++ ICSharpCode.Decompiler/Xml/ObjectIterator.cs | 109 +++ .../Xml/ReuseEqualityComparer.cs | 52 ++ .../Xml/StringTextSource.cs | 160 ++++ ICSharpCode.Decompiler/Xml/SyntaxError.cs | 72 ++ .../Xml/TagMatchingHeuristics.cs | 351 ++++++++ ICSharpCode.Decompiler/Xml/TagReader.cs | 834 ++++++++++++++++++ .../Xml/TextChangeEventArgs.cs | 100 +++ ICSharpCode.Decompiler/Xml/TextLocation.cs | 223 +++++ ICSharpCode.Decompiler/Xml/TextType.cs | 47 + ICSharpCode.Decompiler/Xml/TokenReader.cs | 349 ++++++++ ICSharpCode.Decompiler/Xml/XmlSegment.cs | 47 + ILSpy/ExtensionMethods.cs | 40 + ILSpy/ILSpy.csproj | 2 + ILSpy/Languages/Language.cs | 5 + ILSpy/TextView/DecompilerTextView.cs | 238 ++++- ILSpy/TextView/DecompilerTextView.xaml | 2 +- ILSpy/TextView/DocumentationUIBuilder.cs | 530 +++++++++++ ILSpy/TextView/ReadOnlyDocument.cs | 448 ++++++++++ ILSpy/TextView/XmlDocFormatter.cs | 99 +++ ILSpy/TextView/XmlDocRenderer.cs | 552 ++++++++++-- 38 files changed, 6927 insertions(+), 105 deletions(-) create mode 100644 ICSharpCode.Decompiler/Documentation/DocumentationElement.cs create mode 100644 ICSharpCode.Decompiler/Xml/AXmlAttribute.cs create mode 100644 ICSharpCode.Decompiler/Xml/AXmlDocument.cs create mode 100644 ICSharpCode.Decompiler/Xml/AXmlElement.cs create mode 100644 ICSharpCode.Decompiler/Xml/AXmlObject.cs create mode 100644 ICSharpCode.Decompiler/Xml/AXmlParser.cs create mode 100644 ICSharpCode.Decompiler/Xml/AXmlReader.cs create mode 100644 ICSharpCode.Decompiler/Xml/AXmlTag.cs create mode 100644 ICSharpCode.Decompiler/Xml/AXmlText.cs create mode 100644 ICSharpCode.Decompiler/Xml/AXmlVisitor.cs create mode 100644 ICSharpCode.Decompiler/Xml/AnchorMovementType.cs create mode 100644 ICSharpCode.Decompiler/Xml/DocumentationElement.cs create mode 100644 ICSharpCode.Decompiler/Xml/ISegment.cs create mode 100644 ICSharpCode.Decompiler/Xml/ITextSource.cs create mode 100644 ICSharpCode.Decompiler/Xml/IncrementalParserState.cs create mode 100644 ICSharpCode.Decompiler/Xml/InternalDocument.cs create mode 100644 ICSharpCode.Decompiler/Xml/Log.cs create mode 100644 ICSharpCode.Decompiler/Xml/ObjectIterator.cs create mode 100644 ICSharpCode.Decompiler/Xml/ReuseEqualityComparer.cs create mode 100644 ICSharpCode.Decompiler/Xml/StringTextSource.cs create mode 100644 ICSharpCode.Decompiler/Xml/SyntaxError.cs create mode 100644 ICSharpCode.Decompiler/Xml/TagMatchingHeuristics.cs create mode 100644 ICSharpCode.Decompiler/Xml/TagReader.cs create mode 100644 ICSharpCode.Decompiler/Xml/TextChangeEventArgs.cs create mode 100644 ICSharpCode.Decompiler/Xml/TextLocation.cs create mode 100644 ICSharpCode.Decompiler/Xml/TextType.cs create mode 100644 ICSharpCode.Decompiler/Xml/TokenReader.cs create mode 100644 ICSharpCode.Decompiler/Xml/XmlSegment.cs create mode 100644 ILSpy/TextView/DocumentationUIBuilder.cs create mode 100644 ILSpy/TextView/ReadOnlyDocument.cs create mode 100644 ILSpy/TextView/XmlDocFormatter.cs diff --git a/ICSharpCode.Decompiler/Documentation/DocumentationElement.cs b/ICSharpCode.Decompiler/Documentation/DocumentationElement.cs new file mode 100644 index 000000000..aea2a2d20 --- /dev/null +++ b/ICSharpCode.Decompiler/Documentation/DocumentationElement.cs @@ -0,0 +1,285 @@ +// Copyright (c) 2009-2013 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.Decompiler.Util; +using ICSharpCode.Decompiler.Xml; + +namespace ICSharpCode.Decompiler.Documentation +{ + /// + /// Represents an element in the XML documentation. + /// Any occurrences of "<inheritdoc/>" are replaced with the inherited documentation. + /// + public class XmlDocumentationElement + { + /// + /// Gets the XML documentation element for the specified entity. + /// Returns null if no documentation is found. + /// + public static XmlDocumentationElement Get(IEntity entity, bool inheritDocIfMissing = true) + { + if (entity == null) + return null; + var documentationComment = entity.Documentation; + if (documentationComment != null) { + return Create(documentationComment, entity); + } + + IMember member = entity as IMember; + if (inheritDocIfMissing && member != null) { + if (member.SymbolKind == SymbolKind.Constructor) { + // For constructors, the documentation of the base class ctor + // isn't really suitable as constructors are not inherited. + // We'll use the type's documentation instead: + return Get(entity.DeclaringTypeDefinition, inheritDocIfMissing); + } + foreach (IMember baseMember in InheritanceHelper.GetBaseMembers(member, includeImplementedInterfaces: true)) { + documentationComment = baseMember.Documentation; + if (documentationComment != null) + return Create(documentationComment, baseMember); + } + } + return null; + } + + static XmlDocumentationElement Create(DocumentationComment documentationComment, IEntity declaringEntity) + { + var doc = new AXmlParser().Parse(documentationComment.Xml); + return new XmlDocumentationElement(doc, declaringEntity, documentationComment.ResolveCref); + } + + readonly AXmlObject xmlObject; + readonly AXmlElement element; + readonly IEntity declaringEntity; + readonly Func crefResolver; + volatile string textContent; + + /// + /// Inheritance level; used to prevent cyclic doc inheritance. + /// + int nestingLevel; + + /// + /// Creates a new documentation element. + /// + public XmlDocumentationElement(AXmlElement element, IEntity declaringEntity, Func crefResolver) + { + if (element == null) + throw new ArgumentNullException("element"); + this.element = element; + this.xmlObject = element; + this.declaringEntity = declaringEntity; + this.crefResolver = crefResolver; + } + + /// + /// Creates a new documentation element. + /// + public XmlDocumentationElement(AXmlDocument document, IEntity declaringEntity, Func crefResolver) + { + if (document == null) + throw new ArgumentNullException("document"); + this.xmlObject = document; + this.declaringEntity = declaringEntity; + this.crefResolver = crefResolver; + } + + /// + /// Creates a new documentation element. + /// + public XmlDocumentationElement(string text, IEntity declaringEntity) + { + if (text == null) + throw new ArgumentNullException("text"); + this.declaringEntity = declaringEntity; + this.textContent = text; + } + + /// + /// Gets the entity on which this documentation was originally declared. + /// May return null. + /// + public IEntity DeclaringEntity { + get { return declaringEntity; } + } + + IEntity referencedEntity; + volatile bool referencedEntityInitialized; + + /// + /// Gets the entity referenced by the 'cref' attribute. + /// May return null. + /// + public IEntity ReferencedEntity { + get { + if (!referencedEntityInitialized) { + string cref = GetAttribute("cref"); + if (cref != null && crefResolver != null) + referencedEntity = crefResolver(cref); + referencedEntityInitialized = true; + } + return referencedEntity; + } + } + + /// + /// Gets the element name. + /// + public string Name { + get { + return element != null ? element.Name : string.Empty; + } + } + + /// + /// Gets the attribute value. + /// + public string GetAttribute(string name) + { + return element != null ? element.GetAttributeValue(name) : string.Empty; + } + + /// + /// Gets whether this is a pure text node. + /// + public bool IsTextNode { + get { return xmlObject == null; } + } + + /// + /// Gets the text content. + /// + public string TextContent { + get { + if (textContent == null) { + StringBuilder b = new StringBuilder(); + foreach (var child in this.Children) + b.Append(child.TextContent); + textContent = b.ToString(); + } + return textContent; + } + } + + IList children; + + /// + /// Gets the child elements. + /// + public IList Children { + get { + if (xmlObject == null) + return EmptyList.Instance; + return LazyInitializer.EnsureInitialized( + ref this.children, + () => CreateElements(xmlObject.Children, declaringEntity, crefResolver, nestingLevel)); + } + } + + static readonly string[] doNotInheritIfAlreadyPresent = { + "example", "exclude", "filterpriority", "preliminary", "summary", + "remarks", "returns", "threadsafety", "value" + }; + + static List CreateElements(IEnumerable childObjects, IEntity declaringEntity, Func crefResolver, int nestingLevel) + { + List list = new List(); + foreach (var child in childObjects) { + var childText = child as AXmlText; + var childTag = child as AXmlTag; + var childElement = child as AXmlElement; + if (childText != null) { + list.Add(new XmlDocumentationElement(childText.Value, declaringEntity)); + } else if (childTag != null && childTag.IsCData) { + foreach (var text in childTag.Children.OfType()) + list.Add(new XmlDocumentationElement(text.Value, declaringEntity)); + } else if (childElement != null) { + if (nestingLevel < 5 && childElement.Name == "inheritdoc") { + string cref = childElement.GetAttributeValue("cref"); + IEntity inheritedFrom = null; + DocumentationComment inheritedDocumentation = null; + if (cref != null) { + inheritedFrom = crefResolver(cref); + if (inheritedFrom != null) + inheritedDocumentation = inheritedFrom.Documentation; + } else { + foreach (IMember baseMember in InheritanceHelper.GetBaseMembers((IMember)declaringEntity, includeImplementedInterfaces: true)) { + inheritedDocumentation = baseMember.Documentation; + if (inheritedDocumentation != null) { + inheritedFrom = baseMember; + break; + } + } + } + + if (inheritedDocumentation != null) { + var doc = new AXmlParser().Parse(inheritedDocumentation.Xml); + + // XPath filter not yet implemented + if (childElement.Parent is AXmlDocument && childElement.GetAttributeValue("select") == null) { + // Inheriting documentation at the root level + List doNotInherit = new List(); + doNotInherit.Add("overloads"); + doNotInherit.AddRange(childObjects.OfType().Select(e => e.Name).Intersect( + doNotInheritIfAlreadyPresent)); + + var inheritedChildren = doc.Children.Where( + inheritedObject => { + AXmlElement inheritedElement = inheritedObject as AXmlElement; + return !(inheritedElement != null && doNotInherit.Contains(inheritedElement.Name)); + }); + + list.AddRange(CreateElements(inheritedChildren, inheritedFrom, inheritedDocumentation.ResolveCref, nestingLevel + 1)); + } + } + } else { + list.Add(new XmlDocumentationElement(childElement, declaringEntity, crefResolver) { nestingLevel = nestingLevel }); + } + } + } + if (list.Count > 0 && list[0].IsTextNode) { + if (string.IsNullOrWhiteSpace(list[0].textContent)) + list.RemoveAt(0); + else + list[0].textContent = list[0].textContent.TrimStart(); + } + if (list.Count > 0 && list[list.Count - 1].IsTextNode) { + if (string.IsNullOrWhiteSpace(list[list.Count - 1].textContent)) + list.RemoveAt(list.Count - 1); + else + list[list.Count - 1].textContent = list[list.Count - 1].textContent.TrimEnd(); + } + return list; + } + + /// + public override string ToString() + { + if (element != null) + return "<" + element.Name + ">"; + else + return this.TextContent; + } + } +} diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index 8ab2fbd69..88450d089 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -579,6 +579,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ICSharpCode.Decompiler/Xml/AXmlAttribute.cs b/ICSharpCode.Decompiler/Xml/AXmlAttribute.cs new file mode 100644 index 000000000..52e308efb --- /dev/null +++ b/ICSharpCode.Decompiler/Xml/AXmlAttribute.cs @@ -0,0 +1,116 @@ +// Copyright (c) 2009-2013 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System.Globalization; + +namespace ICSharpCode.Decompiler.Xml +{ + /// + /// Name-value pair in a tag + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")] + public class AXmlAttribute : AXmlObject + { + internal AXmlAttribute(AXmlObject parent, int startOffset, InternalAttribute internalObject) + : base(parent, startOffset, internalObject) + { + } + + internal InternalAttribute InternalAttribute { + get { return (InternalAttribute)internalObject; } + } + + /// Name with namespace prefix - exactly as in source file + public string Name { get { return InternalAttribute.Name; } } + + /// Unquoted and dereferenced value of the attribute + public string Value { get { return InternalAttribute.Value; } } + + /// Gets the segment for the attribute name + public ISegment NameSegment { + get { return new XmlSegment(startOffset, startOffset + Name.Length); } + } + + /// Gets the segment for the attribute value, including the quotes + public ISegment ValueSegment { + get { return new XmlSegment(startOffset + Name.Length + InternalAttribute.EqualsSignLength, this.EndOffset); } + } + /// The element containing this attribute + /// Null if orphaned + public AXmlElement ParentElement { + get { + AXmlTag tag = this.Parent as AXmlTag; + if (tag != null) { + return tag.Parent as AXmlElement; + } + return null; + } + } + + /// The part of name before ":" + /// Empty string if not found + public string Prefix { + get { + return GetNamespacePrefix(this.Name); + } + } + + /// The part of name after ":" + /// Whole name if ":" not found + public string LocalName { + get { + return GetLocalName(this.Name); + } + } + + /// + /// Resolved namespace of the name. Empty string if not found + /// From the specification: "The namespace name for an unprefixed attribute name always has no value." + /// + public string Namespace { + get { + if (string.IsNullOrEmpty(this.Prefix)) return NoNamespace; + + AXmlElement elem = this.ParentElement; + if (elem != null) { + return elem.LookupNamespace(this.Prefix) ?? NoNamespace; + } + return NoNamespace; // Orphaned attribute + } + } + + /// Attribute is declaring namespace ("xmlns" or "xmlns:*") + public bool IsNamespaceDeclaration { + get { + return this.Name == "xmlns" || this.Prefix == "xmlns"; + } + } + + /// + public override void AcceptVisitor(AXmlVisitor visitor) + { + visitor.VisitAttribute(this); + } + + /// + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "[{0} '{1}={2}']", base.ToString(), this.Name, this.Value); + } + } +} diff --git a/ICSharpCode.Decompiler/Xml/AXmlDocument.cs b/ICSharpCode.Decompiler/Xml/AXmlDocument.cs new file mode 100644 index 000000000..55f919649 --- /dev/null +++ b/ICSharpCode.Decompiler/Xml/AXmlDocument.cs @@ -0,0 +1,55 @@ +// Copyright (c) 2009-2013 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System.Globalization; + +namespace ICSharpCode.Decompiler.Xml +{ + /// + /// The root object of the XML document + /// + public class AXmlDocument : AXmlObject + { + internal AXmlDocument(AXmlObject parent, int startOffset, InternalDocument internalObject) + : base(parent, startOffset, internalObject) + { + } + + internal override ObjectIterator CreateIteratorForReader() + { + return new ObjectIterator(internalObject.NestedObjects, startOffset); + } + + /// + public override void AcceptVisitor(AXmlVisitor visitor) + { + visitor.VisitDocument(this); + } + + /// + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "[{0} Chld:{1}]", base.ToString(), this.Children.Count); + } + + /// + /// Represents an empty document. + /// + public readonly static AXmlDocument Empty = new AXmlDocument(null, 0, new InternalDocument()); + } +} diff --git a/ICSharpCode.Decompiler/Xml/AXmlElement.cs b/ICSharpCode.Decompiler/Xml/AXmlElement.cs new file mode 100644 index 000000000..346401e95 --- /dev/null +++ b/ICSharpCode.Decompiler/Xml/AXmlElement.cs @@ -0,0 +1,243 @@ +// Copyright (c) 2009-2013 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Xml; + +namespace ICSharpCode.Decompiler.Xml +{ + /// + /// XML element. + /// + public class AXmlElement : AXmlObject, IXmlNamespaceResolver + { + internal AXmlElement(AXmlObject parent, int startOffset, InternalElement internalObject) + : base(parent, startOffset, internalObject) + { + Log.Assert(internalObject.NestedObjects[0] is InternalTag, "First child of element must be start tag"); + } + + /// No tags are missing anywhere within this element (recursive) + public bool IsProperlyNested { + get { return ((InternalElement)internalObject).IsPropertyNested; } + } + + /// The start or empty-element tag for this element. + public AXmlTag StartTag { + get { return (AXmlTag)this.Children[0]; } + } + + /// Name with namespace prefix - exactly as in source + public string Name { + get { return ((InternalTag)internalObject.NestedObjects[0]).Name; } + } + + /// Gets whether an end tag exists for this node. + public bool HasEndTag { + get { return ((InternalElement)internalObject).HasEndTag; } + } + + /// The end tag, if there is any. Returns null for empty elements "<Element/>" and missing end tags in malformed XML. + public AXmlTag EndTag { + get { + if (HasEndTag) + return (AXmlTag)this.Children[this.Children.Count - 1]; + else + return null; + } + } + + /// + /// Gets the attributes. + /// + public IEnumerable Attributes { + get { + return ((AXmlTag)this.Children[0]).Children.OfType(); + } + } + + /// + /// Gets the content (all children except for the start and end tags) + /// + public IEnumerable Content { + get { + int end = this.Children.Count; + if (HasEndTag) + end--; + for (int i = 1; i < end; i++) { + yield return this.Children[i]; + } + } + } + + /// The part of name before ":" + /// Empty string if not found + public string Prefix { + get { return ((InternalElement)internalObject).Prefix; } + } + + /// The part of name after ":" + /// Empty string if not found + public string LocalName { + get { return ((InternalElement)internalObject).LocalName; } + } + + /// Resolved namespace of the name + /// Empty string if prefix is not found + public string Namespace { + get { + string prefix = this.Prefix; + return LookupNamespace(prefix); + } + } + + /// Find the default namespace for this context + [Obsolete("Use LookupNamespace(string.Empty) instead")] + public string FindDefaultNamespace() + { + return LookupNamespace(string.Empty) ?? NoNamespace; + } + + /// + /// Recursively resolve given prefix in this context. Prefix must have some value. + /// + /// Empty string if prefix is not found + [Obsolete("Use LookupNamespace() instead")] + public string ResolvePrefix(string prefix) + { + return LookupNamespace(prefix) ?? NoNamespace; + } + + /// + /// Recursively resolve given prefix in this context. + /// + /// null if prefix is not found + public string LookupNamespace(string prefix) + { + if (prefix == null) + throw new ArgumentNullException("prefix"); + + // Implicit namespaces + if (prefix == "xml") return XmlNamespace; + if (prefix == "xmlns") return XmlnsNamespace; + + string lookFor = (prefix.Length > 0 ? "xmlns:" + prefix : "xmlns"); + for (AXmlElement current = this; current != null; current = current.Parent as AXmlElement) { + foreach (var attr in current.Attributes) { + if (attr.Name == lookFor) + return attr.Value; + } + } + return null; // Can not find prefix + } + + /// + /// Gets the prefix that is mapped to the specified namespace URI. + /// + /// The prefix that is mapped to the namespace URI; null if the namespace URI is not mapped to a prefix. + public string LookupPrefix(string namespaceName) + { + if (namespaceName == null) + throw new ArgumentNullException("namespaceName"); + + if (namespaceName == XmlNamespace) + return "xml"; + if (namespaceName == XmlnsNamespace) + return "xmlns"; + for (AXmlElement current = this; current != null; current = current.Parent as AXmlElement) { + foreach (var attr in current.Attributes) { + if (attr.Value == namespaceName) { + if (attr.Name.StartsWith("xmlns:", StringComparison.Ordinal)) + return attr.LocalName; + else if (attr.Name == "xmlns") + return string.Empty; + } + } + } + return null; // Can not find prefix + } + + /// + /// Gets a collection of defined prefix-namespace mappings that are currently in scope. + /// + public IDictionary GetNamespacesInScope(XmlNamespaceScope scope) + { + var result = new Dictionary(); + if (scope == XmlNamespaceScope.All) { + result["xml"] = XmlNamespace; + //result["xmlns"] = XmlnsNamespace; xmlns should not be included in GetNamespacesInScope() results + } + for (AXmlElement current = this; current != null; current = current.Parent as AXmlElement) { + foreach (var attr in current.Attributes) { + if (attr.Name.StartsWith("xmlns:", StringComparison.Ordinal)) { + string prefix = attr.LocalName; + if (!result.ContainsKey(prefix)) { + result.Add(prefix, attr.Value); + } + } else if (attr.Name == "xmlns" && !result.ContainsKey(string.Empty)) { + result.Add(string.Empty, attr.Value); + } + } + if (scope == XmlNamespaceScope.Local) + break; + } + return result; + } + + /// + /// Get unquoted value of attribute. + /// It looks in the no namespace (empty string). + /// + /// Null if not found + public string GetAttributeValue(string localName) + { + return GetAttributeValue(string.Empty, localName); + } + + /// + /// Get unquoted value of attribute + /// + /// Namespace. Can be no namepace (empty string), which is the default for attributes. + /// Local name - text after ":" + /// Null if not found + public string GetAttributeValue(string @namespace, string localName) + { + @namespace = @namespace ?? string.Empty; + foreach (AXmlAttribute attr in this.Attributes) { + if (attr.LocalName == localName && attr.Namespace == @namespace) + return attr.Value; + } + return null; + } + + /// + public override void AcceptVisitor(AXmlVisitor visitor) + { + visitor.VisitElement(this); + } + + /// + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "[{0} '{1}' Attr:{2} Chld:{3} Nest:{4}]", base.ToString(), this.Name, this.StartTag.Children.Count, this.Children.Count, this.IsProperlyNested ? "Ok" : "Bad"); + } + } +} diff --git a/ICSharpCode.Decompiler/Xml/AXmlObject.cs b/ICSharpCode.Decompiler/Xml/AXmlObject.cs new file mode 100644 index 000000000..7bc59c108 --- /dev/null +++ b/ICSharpCode.Decompiler/Xml/AXmlObject.cs @@ -0,0 +1,245 @@ +// Copyright (c) 2009-2013 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml; +using ICSharpCode.Decompiler.Util; + +namespace ICSharpCode.Decompiler.Xml +{ + /// + /// XML object. Base class for all nodes in the XML document. + /// + public abstract class AXmlObject : ISegment + { + /// Empty string. The namespace used if there is no "xmlns" specified + internal static readonly string NoNamespace = string.Empty; + + /// Namespace for "xml:" prefix: "http://www.w3.org/XML/1998/namespace" + public static readonly string XmlNamespace = "http://www.w3.org/XML/1998/namespace"; + + /// Namesapce for "xmlns:" prefix: "http://www.w3.org/2000/xmlns/" + public static readonly string XmlnsNamespace = "http://www.w3.org/2000/xmlns/"; + + readonly AXmlObject parent; + internal readonly int startOffset; + internal readonly InternalObject internalObject; + IList children; + + internal AXmlObject(AXmlObject parent, int startOffset, InternalObject internalObject) + { + this.parent = parent; + this.startOffset = startOffset; + this.internalObject = internalObject; + } + + /// + /// Creates an XML reader that reads from this document. + /// + /// + /// The reader will ignore comments and processing instructions; and will not have line information. + /// + public XmlReader CreateReader() + { + return new AXmlReader(CreateIteratorForReader()); + } + + /// + /// Creates an XML reader that reads from this document. + /// + /// Reader settings. + /// Currently, only IgnoreComments is supported. + /// + /// The reader will not have line information. + /// + public XmlReader CreateReader(XmlReaderSettings settings) + { + return new AXmlReader(CreateIteratorForReader(), settings); + } + + /// + /// Creates an XML reader that reads from this document. + /// + /// Reader settings. + /// Currently, only IgnoreComments is supported. + /// + /// The document that was used to parse the XML. It is used to convert offsets to line information. + /// + //public XmlReader CreateReader(XmlReaderSettings settings, IDocument document) + //{ + // if (document == null) + // throw new ArgumentNullException("document"); + // return new AXmlReader(CreateIteratorForReader(), settings, document.GetLocation); + //} + + /// + /// Creates an XML reader that reads from this document. + /// + /// Reader settings. + /// Currently, only IgnoreComments is supported. + /// + /// A function for converting offsets to line information. + /// + public XmlReader CreateReader(XmlReaderSettings settings, Func offsetToTextLocation) + { + return new AXmlReader(CreateIteratorForReader(), settings, offsetToTextLocation); + } + + internal virtual ObjectIterator CreateIteratorForReader() + { + return new ObjectIterator(new[] { internalObject }, startOffset); + } + + /// + /// Gets the parent node. + /// + public AXmlObject Parent { + get { return parent; } + } + + /// + /// Gets the list of child objects. + /// + public IList Children { + get { + var result = LazyInit.VolatileRead(ref this.children); + if (result != null) { + return result; + } else { + if (internalObject.NestedObjects != null) { + var array = new AXmlObject[internalObject.NestedObjects.Length]; + for (int i = 0; i < array.Length; i++) { + array[i] = internalObject.NestedObjects[i].CreatePublicObject(this, startOffset); + } + result = Array.AsReadOnly(array); + } else { + result = EmptyList.Instance; + } + return LazyInit.GetOrSet(ref this.children, result); + } + } + } + + /// + /// Gets a child fully containg the given offset. + /// Goes recursively down the tree. + /// Special case if at the end of attribute or text + /// + public AXmlObject GetChildAtOffset(int offset) + { + foreach(AXmlObject child in this.Children) { + if (offset == child.EndOffset && (child is AXmlAttribute || child is AXmlText)) + return child; + if (child.StartOffset < offset && offset < child.EndOffset) { + return child.GetChildAtOffset(offset); + } + } + return this; // No children at offset + } + + /// + /// The error that occured in the context of this node (excluding nested nodes) + /// + public IEnumerable MySyntaxErrors { + get { + if (internalObject.SyntaxErrors != null) { + return internalObject.SyntaxErrors.Select(e => new SyntaxError(startOffset + e.RelativeStart, startOffset + e.RelativeEnd, e.Description)); + } else { + return EmptyList.Instance; + } + } + } + + /// + /// The error that occured in the context of this node and all nested nodes. + /// It has O(n) cost. + /// + public IEnumerable SyntaxErrors { + get { + return TreeTraversal.PreOrder(this, n => n.Children).SelectMany(obj => obj.MySyntaxErrors); + } + } + + /// Get all ancestors of this node + public IEnumerable Ancestors { + get { + AXmlObject curr = this.Parent; + while(curr != null) { + yield return curr; + curr = curr.Parent; + } + } + } + + #region Helper methods + + /// The part of name before ":" + /// Empty string if not found + internal static string GetNamespacePrefix(string name) + { + if (string.IsNullOrEmpty(name)) return string.Empty; + int colonIndex = name.IndexOf(':'); + if (colonIndex != -1) { + return name.Substring(0, colonIndex); + } else { + return string.Empty; + } + } + + /// The part of name after ":" + /// Whole name if ":" not found + internal static string GetLocalName(string name) + { + if (string.IsNullOrEmpty(name)) return string.Empty; + int colonIndex = name.IndexOf(':'); + if (colonIndex != -1) { + return name.Remove(0, colonIndex + 1); + } else { + return name ?? string.Empty; + } + } + + #endregion + + /// Call appropriate visit method on the given visitor + public abstract void AcceptVisitor(AXmlVisitor visitor); + + /// + /// Gets the start offset of the segment. + /// + public int StartOffset { + get { return startOffset; } + } + + int ISegment.Offset { + get { return startOffset; } + } + + /// + public int Length { + get { return internalObject.Length; } + } + + /// + public int EndOffset { + get { return startOffset + internalObject.Length; } + } + } +} diff --git a/ICSharpCode.Decompiler/Xml/AXmlParser.cs b/ICSharpCode.Decompiler/Xml/AXmlParser.cs new file mode 100644 index 000000000..e1d0ae276 --- /dev/null +++ b/ICSharpCode.Decompiler/Xml/AXmlParser.cs @@ -0,0 +1,142 @@ +// Copyright (c) 2009-2013 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Threading; + +namespace ICSharpCode.Decompiler.Xml +{ + /// + /// XML parser that is error tolerant. + /// + public class AXmlParser + { + /// + /// Generate syntax error when seeing entity reference other then the built-in ones + /// + public bool UnknownEntityReferenceIsError { get; set; } + + IList CreatePublic(IList internalObjects) + { + var publicObjects = new AXmlObject[internalObjects.Count]; + int pos = 0; + for (int i = 0; i < internalObjects.Count; i++) { + publicObjects[i] = internalObjects[i].CreatePublicObject(null, pos); + pos += internalObjects[i].Length; + } + return Array.AsReadOnly(publicObjects); + } + + /// + /// Parses a document into a flat list of tags. + /// + /// Parsed tag soup. + public IList ParseTagSoup(ITextSource textSource, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (textSource == null) + throw new ArgumentNullException("textSource"); + var reader = new TagReader(this, textSource, false); + var internalObjects = reader.ReadAllObjects(cancellationToken); + return CreatePublic(internalObjects); + } + + /// + /// Parses a document incrementally into a flat list of tags. + /// + /// The parser state from a previous call to ParseIncremental(). Use null for the first call. + /// The text source for the new document version. + /// Out: the new parser state, pass this to the next ParseIncremental() call. + /// Optional: cancellation token. + /// Parsed tag soup. + public IList ParseTagSoupIncremental( + IncrementalParserState oldParserState, ITextSource newTextSource, out IncrementalParserState newParserState, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (newTextSource == null) + throw new ArgumentNullException("newTextSource"); + var internalObjects = InternalParseIncremental(oldParserState, newTextSource, out newParserState, false, cancellationToken); + return CreatePublic(internalObjects); + } + + List InternalParseIncremental( + IncrementalParserState oldParserState, ITextSource newTextSource, out IncrementalParserState newParserState, + bool collapseProperlyNestedElements, CancellationToken cancellationToken) + { + var reader = new TagReader(this, newTextSource, collapseProperlyNestedElements); + ITextSourceVersion newVersion = newTextSource.Version; + var reuseMap = oldParserState != null ? oldParserState.GetReuseMapTo(newVersion) : null; + + List internalObjects; + if (reuseMap != null) + internalObjects = reader.ReadAllObjectsIncremental(oldParserState.Objects, reuseMap, cancellationToken); + else + internalObjects = reader.ReadAllObjects(cancellationToken); + + if (newVersion != null) + newParserState = new IncrementalParserState(newTextSource.TextLength, newVersion, internalObjects.ToArray()); + else + newParserState = null; + + return internalObjects; + } + + /// + /// Parses a document. + /// + public AXmlDocument Parse(ITextSource textSource, CancellationToken cancellationToken = default(CancellationToken)) + { + if (textSource == null) + throw new ArgumentNullException("textSource"); + var reader = new TagReader(this, textSource, true); + var internalObjects = reader.ReadAllObjects(cancellationToken); + var heuristic = new TagMatchingHeuristics(textSource); + return new AXmlDocument(null, 0, heuristic.CreateDocument(internalObjects, cancellationToken)); + } + + /// + /// Parses a document incrementally into a flat list of tags. + /// + /// The parser state from a previous call to ParseIncremental(). Use null for the first call. + /// The text source for the new document version. + /// Out: the new parser state, pass this to the next ParseIncremental() call. + /// Optional: cancellation token. + /// Parsed tag soup. + public AXmlDocument ParseIncremental( + IncrementalParserState oldParserState, ITextSource newTextSource, out IncrementalParserState newParserState, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (newTextSource == null) + throw new ArgumentNullException("newTextSource"); + var internalObjects = InternalParseIncremental(oldParserState, newTextSource, out newParserState, true, cancellationToken); + var heuristic = new TagMatchingHeuristics(newTextSource); + return new AXmlDocument(null, 0, heuristic.CreateDocument(internalObjects, cancellationToken)); + } + + /// + /// Checks whether the given name is a valid XML name. + /// + public static bool IsValidXmlName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("The XML name cannot be null, empty or consist solely of white space", "name"); + return TagReader.IsValidName(name); + } + } +} diff --git a/ICSharpCode.Decompiler/Xml/AXmlReader.cs b/ICSharpCode.Decompiler/Xml/AXmlReader.cs new file mode 100644 index 000000000..fb38a23c0 --- /dev/null +++ b/ICSharpCode.Decompiler/Xml/AXmlReader.cs @@ -0,0 +1,477 @@ +// Copyright (c) 2009-2013 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml; + +namespace ICSharpCode.Decompiler.Xml +{ + /// + /// XmlReader implementation that reads from an . + /// + sealed class AXmlReader : XmlReader, IXmlLineInfo + { + readonly ObjectIterator objectIterator; + readonly XmlReaderSettings settings; + Func offsetToTextLocation; + readonly XmlNameTable nameTable; + readonly XmlNamespaceManager nsManager; + ReadState readState = ReadState.Initial; + XmlNodeType elementNodeType = XmlNodeType.None; + IList attributes; + int attributeIndex = -1; + bool inAttributeValue; + + internal AXmlReader(ObjectIterator objectIterator, XmlReaderSettings settings = null, Func offsetToTextLocation = null) + { + this.objectIterator = objectIterator; + this.settings = settings ?? new XmlReaderSettings(); + this.offsetToTextLocation = offsetToTextLocation; + this.nameTable = this.settings.NameTable ?? new NameTable(); + this.nsManager = new XmlNamespaceManager(this.nameTable); + objectIterator.StopAtElementEnd = true; + } + + public override void ResolveEntity() + { + throw new NotSupportedException(); + } + + public override ReadState ReadState { + get { return readState; } + } + + public override XmlReaderSettings Settings { + get { return settings; } + } + + public override bool ReadAttributeValue() + { + if (attributeIndex >= 0 && !inAttributeValue) { + inAttributeValue = true; + return true; + } + return false; + } + + public override bool Read() + { + switch (readState) { + case ReadState.Initial: + readState = ReadState.Interactive; + return ReadCurrentPosition(); + case ReadState.Interactive: + LeaveNode(); + objectIterator.MoveInto(); + return ReadCurrentPosition(); + default: + return false; + } + } + + bool ReadCurrentPosition() + { + attributes = null; + attributeIndex = -1; + inAttributeValue = false; + while (true) { + var obj = objectIterator.CurrentObject; + if (obj == null) { + readState = ReadState.EndOfFile; + elementNodeType = XmlNodeType.None; + return false; + } else if (objectIterator.IsAtElementEnd) { + if (IsEmptyElement) { + // Don't report EndElement for empty elements + nsManager.PopScope(); + } else { + elementNodeType = XmlNodeType.EndElement; + return true; + } + } else if (obj is InternalElement) { + // element start + elementNodeType = XmlNodeType.Element; + InternalTag startTag = ((InternalTag)obj.NestedObjects[0]); + nsManager.PushScope(); + if (startTag.NestedObjects != null) { + attributes = startTag.NestedObjects.OfType().ToList(); + for (int i = 0; i < attributes.Count; i++) { + var attr = attributes[i]; + if (attr.Name.StartsWith("xmlns:", StringComparison.Ordinal)) + nsManager.AddNamespace(AXmlObject.GetLocalName(attr.Name), attr.Value); + else if (attr.Name == "xmlns") + nsManager.AddNamespace(string.Empty, attr.Value); + } + } + return true; + } else if (obj is InternalText) { + InternalText text = (InternalText)obj; + if (text.ContainsOnlyWhitespace) { + elementNodeType = XmlNodeType.Whitespace; + } else { + elementNodeType = XmlNodeType.Text; + } + return true; + } else if (obj is InternalTag) { + InternalTag tag = (InternalTag)obj; + if (tag.IsStartOrEmptyTag || tag.IsEndTag) { + // start/end tags can be skipped as the parent InternalElement already handles them + } else if (tag.IsComment && !settings.IgnoreComments) { + elementNodeType = XmlNodeType.Comment; + return true; + } else if (tag.IsProcessingInstruction && !settings.IgnoreProcessingInstructions) { + if (tag.Name == "xml") { + elementNodeType = XmlNodeType.XmlDeclaration; + attributes = tag.NestedObjects.OfType().ToList(); + } else { + elementNodeType = XmlNodeType.ProcessingInstruction; + } + return true; + } else if (tag.IsCData) { + elementNodeType = XmlNodeType.CDATA; + return true; + } else { + // TODO all other tags + } + } else { + throw new NotSupportedException(); + } + objectIterator.MoveInto(); + } + } + + void LeaveNode() + { + if (elementNodeType == XmlNodeType.EndElement) { + nsManager.PopScope(); + } + } + + public override void Skip() + { + if (readState == ReadState.Interactive) { + MoveToElement(); + LeaveNode(); + objectIterator.MoveNext(); + ReadCurrentPosition(); + } + } + + public override string Prefix { + get { + if (readState != ReadState.Interactive) + return string.Empty; + if (attributeIndex >= 0) { + if (inAttributeValue) + return string.Empty; + return nameTable.Add(AXmlObject.GetNamespacePrefix(attributes[attributeIndex].Name)); + } + InternalElement element = objectIterator.CurrentObject as InternalElement; + return element != null ? nameTable.Add(element.Prefix) : string.Empty; + } + } + + public override string NamespaceURI { + get { + if (readState != ReadState.Interactive) + return string.Empty; + if (attributeIndex >= 0 && !inAttributeValue && attributes[attributeIndex].Name == "xmlns") + return AXmlObject.XmlnsNamespace; + return LookupNamespace(this.Prefix) ?? string.Empty; + } + } + + public override string LocalName { + get { + if (readState != ReadState.Interactive) + return string.Empty; + if (attributeIndex >= 0) { + if (inAttributeValue) + return string.Empty; + return nameTable.Add(AXmlObject.GetLocalName(attributes[attributeIndex].Name)); + } + string result; + switch (elementNodeType) { + case XmlNodeType.Element: + case XmlNodeType.EndElement: + result = ((InternalElement)objectIterator.CurrentObject).LocalName; + break; + case XmlNodeType.XmlDeclaration: + result = "xml"; + break; + default: + return string.Empty; + } + return nameTable.Add(result); + } + } + + public override string Name { + get { + if (readState != ReadState.Interactive) + return string.Empty; + if (attributeIndex >= 0) { + if (inAttributeValue) + return string.Empty; + return nameTable.Add(attributes[attributeIndex].Name); + } + string result; + switch (elementNodeType) { + case XmlNodeType.Element: + case XmlNodeType.EndElement: + result = ((InternalElement)objectIterator.CurrentObject).Name; + break; + case XmlNodeType.XmlDeclaration: + result = "xml"; + break; + default: + return string.Empty; + } + return nameTable.Add(result); + } + } + + public override bool IsEmptyElement { + get { + if (readState != ReadState.Interactive || attributeIndex >= 0) + return false; + InternalElement element = objectIterator.CurrentObject as InternalElement; + return element != null && element.NestedObjects.Length == 1; + } + } + + public override string Value { + get { + if (readState != ReadState.Interactive) + return string.Empty; + if (attributeIndex >= 0) + return attributes[attributeIndex].Value; + switch (elementNodeType) { + case XmlNodeType.Text: + case XmlNodeType.Whitespace: + return ((InternalText)objectIterator.CurrentObject).Value; + case XmlNodeType.Comment: + case XmlNodeType.CDATA: + var nestedObjects = objectIterator.CurrentObject.NestedObjects; + if (nestedObjects.Length == 1) + return ((InternalText)nestedObjects[0]).Value; + else + return string.Empty; + case XmlNodeType.XmlDeclaration: + StringBuilder b = new StringBuilder(); + foreach (var attr in objectIterator.CurrentObject.NestedObjects.OfType()) { + if (b.Length > 0) + b.Append(' '); + b.Append(attr.Name); + b.Append('='); + b.Append('"'); + b.Append(attr.Value); + b.Append('"'); + } + return b.ToString(); + default: + return string.Empty; + } + } + } + + public override bool HasValue { + get { + if (readState != ReadState.Interactive) + return false; + if (attributeIndex >= 0) + return true; + switch (elementNodeType) { + case XmlNodeType.Text: + case XmlNodeType.Whitespace: + case XmlNodeType.Comment: + case XmlNodeType.XmlDeclaration: + case XmlNodeType.CDATA: + return true; + default: + return false; + } + } + } + + public override XmlNodeType NodeType { + get { + if (attributeIndex >= 0) + return inAttributeValue ? XmlNodeType.Text : XmlNodeType.Attribute; + else + return elementNodeType; + } + } + + public override XmlNameTable NameTable { + get { return nameTable; } + } + + public override bool MoveToFirstAttribute() + { + return DoMoveToAttribute(0); + } + + public override bool MoveToNextAttribute() + { + return DoMoveToAttribute(attributeIndex + 1); + } + + public override void MoveToAttribute(int i) + { + if (!DoMoveToAttribute(i)) + throw new ArgumentOutOfRangeException("i"); + } + + bool DoMoveToAttribute(int i) + { + if (i >= 0 && i < this.AttributeCount) { + attributeIndex = i; + inAttributeValue = false; + return true; + } + return false; + } + + public override bool MoveToElement() + { + if (attributeIndex >= 0) { + attributeIndex = -1; + inAttributeValue = false; + return true; + } + return false; + } + + int GetAttributeIndex(string name) + { + if (attributes == null) + return -1; + for (int i = 0; i < attributes.Count; i++) { + if (attributes[i].Name == name) + return i; + } + return -1; + } + + int GetAttributeIndex(string name, string ns) + { + if (attributes == null) + return -1; + for (int i = 0; i < attributes.Count; i++) { + if (AXmlObject.GetLocalName(attributes[i].Name) == name && (LookupNamespace(AXmlObject.GetNamespacePrefix(attributes[i].Name)) ?? string.Empty) == ns) + return i; + } + return -1; + } + + public override bool MoveToAttribute(string name, string ns) + { + return DoMoveToAttribute(GetAttributeIndex(name, ns)); + } + + public override bool MoveToAttribute(string name) + { + return DoMoveToAttribute(GetAttributeIndex(name)); + } + + public override string LookupNamespace(string prefix) + { + return nsManager.LookupNamespace(prefix); + } + + public override string GetAttribute(int i) + { + if (attributes == null || i < 0 || i >= attributes.Count) + return null; + return attributes[i].Value; + } + + public override string GetAttribute(string name, string namespaceURI) + { + return GetAttribute(GetAttributeIndex(name, namespaceURI)); + } + + public override string GetAttribute(string name) + { + return GetAttribute(GetAttributeIndex(name)); + } + + public override bool EOF { + get { return readState == ReadState.EndOfFile; } + } + + public override int Depth { + get { + if (attributeIndex < 0) + return objectIterator.Depth; + else + return objectIterator.Depth + (inAttributeValue ? 2 : 1); + } + } + + public override void Close() + { + readState = ReadState.Closed; + offsetToTextLocation = null; + } + + public override string BaseURI { + get { return string.Empty; } + } + + public override int AttributeCount { + get { return attributes != null ? attributes.Count : 0; } + } + + int CurrentPosition { + get { + if (attributeIndex < 0) + return objectIterator.CurrentPosition; + else + return objectIterator.CurrentPosition + attributes[attributeIndex].StartRelativeToParent; + } + } + + public int LineNumber { + get { + if (offsetToTextLocation != null) + return offsetToTextLocation(CurrentPosition).Line; + else + return 0; + } + } + + public int LinePosition { + get { + if (offsetToTextLocation != null) + return offsetToTextLocation(CurrentPosition).Column - 1; + else + return 0; + } + } + + bool IXmlLineInfo.HasLineInfo() + { + return offsetToTextLocation != null; + } + } +} diff --git a/ICSharpCode.Decompiler/Xml/AXmlTag.cs b/ICSharpCode.Decompiler/Xml/AXmlTag.cs new file mode 100644 index 000000000..922dad8ea --- /dev/null +++ b/ICSharpCode.Decompiler/Xml/AXmlTag.cs @@ -0,0 +1,96 @@ +// Copyright (c) 2009-2013 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System.Collections.ObjectModel; +using System.Globalization; + +namespace ICSharpCode.Decompiler.Xml +{ + /// + /// Represents any markup starting with "<" and (hopefully) ending with ">" + /// + public class AXmlTag : AXmlObject + { + /// These identify the start of DTD elements + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification="ReadOnlyCollection is immutable")] + public static readonly ReadOnlyCollection DtdNames = new ReadOnlyCollection( + new string[] {" Opening bracket - usually "<" + public string OpeningBracket { + get { return internalObject.OpeningBracket; } + } + + /// Name following the opening bracket + public string Name { + get { return internalObject.Name; } + } + + /// Gets the segment containing the tag name + public ISegment NameSegment { + get { + int start = startOffset + internalObject.RelativeNameStart; + return new XmlSegment(start, start + internalObject.Name.Length); + } + } + + /// Closing bracket - usually ">" + public string ClosingBracket { + get { return internalObject.ClosingBracket; } + } + + /// True if tag starts with "<" + public bool IsStartOrEmptyTag { get { return internalObject.IsStartOrEmptyTag; } } + /// True if tag starts with "<" and ends with ">" + public bool IsStartTag { get { return internalObject.IsStartTag; } } + /// True if tag starts with "<" and does not end with ">" + public bool IsEmptyTag { get { return internalObject.IsEmptyTag; } } + /// True if tag starts with "</" + public bool IsEndTag { get { return internalObject.IsEndTag; } } + /// True if tag starts with "<?" + public bool IsProcessingInstruction { get { return internalObject.IsProcessingInstruction; } } + /// True if tag starts with "<!--" + public bool IsComment { get { return internalObject.IsComment; } } + /// True if tag starts with "<![CDATA[" + public bool IsCData { get { return internalObject.IsCData; } } + /// True if tag starts with one of the DTD starts + public bool IsDocumentType { get { return internalObject.IsDocumentType; } } + /// True if tag starts with "<!" + public bool IsUnknownBang { get { return internalObject.IsUnknownBang; } } + + /// + public override void AcceptVisitor(AXmlVisitor visitor) + { + visitor.VisitTag(this); + } + + /// + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "[{0} '{1}{2}{3}' Attr:{4}]", base.ToString(), this.OpeningBracket, this.Name, this.ClosingBracket, this.Children.Count); + } + } +} diff --git a/ICSharpCode.Decompiler/Xml/AXmlText.cs b/ICSharpCode.Decompiler/Xml/AXmlText.cs new file mode 100644 index 000000000..a82d20336 --- /dev/null +++ b/ICSharpCode.Decompiler/Xml/AXmlText.cs @@ -0,0 +1,63 @@ +// Copyright (c) 2009-2013 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Globalization; + +namespace ICSharpCode.Decompiler.Xml +{ + /// + /// Whitespace or character data + /// + public class AXmlText : AXmlObject + { + internal AXmlText(AXmlObject parent, int startOffset, InternalText internalObject) + : base(parent, startOffset, internalObject) + { + } + +// /// The type of the text node +// public TextType Type { +// get { return ((InternalText)internalObject).Type; } +// } + + /// The text with all entity references resloved + public string Value { + get { return ((InternalText)internalObject).Value; } + } + + /// True if the text contains only whitespace characters + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace", + Justification = "System.Xml also uses 'Whitespace'")] + public bool ContainsOnlyWhitespace { + get { return ((InternalText)internalObject).ContainsOnlyWhitespace; } + } + + /// + public override void AcceptVisitor(AXmlVisitor visitor) + { + visitor.VisitText(this); + } + + /// + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "[{0} Text.Length={1}]", base.ToString(), this.Value.Length); + } + } +} diff --git a/ICSharpCode.Decompiler/Xml/AXmlVisitor.cs b/ICSharpCode.Decompiler/Xml/AXmlVisitor.cs new file mode 100644 index 000000000..187e6467f --- /dev/null +++ b/ICSharpCode.Decompiler/Xml/AXmlVisitor.cs @@ -0,0 +1,62 @@ +// Copyright (c) 2009-2013 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Text; + +namespace ICSharpCode.Decompiler.Xml +{ + /// + /// Derive from this class to create visitor for the XML tree + /// + public abstract class AXmlVisitor + { + /// Visit AXmlDocument + public virtual void VisitDocument(AXmlDocument document) + { + foreach (AXmlObject child in document.Children) + child.AcceptVisitor(this); + } + + /// Visit AXmlElement + public virtual void VisitElement(AXmlElement element) + { + foreach (AXmlObject child in element.Children) + child.AcceptVisitor(this); + } + + /// Visit AXmlTag + public virtual void VisitTag(AXmlTag tag) + { + foreach (AXmlObject child in tag.Children) + child.AcceptVisitor(this); + } + + /// Visit AXmlAttribute + public virtual void VisitAttribute(AXmlAttribute attribute) + { + + } + + /// Visit AXmlText + public virtual void VisitText(AXmlText text) + { + + } + } +} \ No newline at end of file diff --git a/ICSharpCode.Decompiler/Xml/AnchorMovementType.cs b/ICSharpCode.Decompiler/Xml/AnchorMovementType.cs new file mode 100644 index 000000000..085389d37 --- /dev/null +++ b/ICSharpCode.Decompiler/Xml/AnchorMovementType.cs @@ -0,0 +1,43 @@ +// Copyright (c) 2009-2013 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +namespace ICSharpCode.Decompiler.Xml +{ + /// + /// Defines how a text anchor moves. + /// + public enum AnchorMovementType + { + /// + /// When text is inserted at the anchor position, the type of the insertion + /// determines where the caret moves to. For normal insertions, the anchor will move + /// after the inserted text. + /// + Default, + /// + /// Behaves like a start marker - when text is inserted at the anchor position, the anchor will stay + /// before the inserted text. + /// + BeforeInsertion, + /// + /// Behave like an end marker - when text is insered at the anchor position, the anchor will move + /// after the inserted text. + /// + AfterInsertion + } +} diff --git a/ICSharpCode.Decompiler/Xml/DocumentationElement.cs b/ICSharpCode.Decompiler/Xml/DocumentationElement.cs new file mode 100644 index 000000000..c4468fd92 --- /dev/null +++ b/ICSharpCode.Decompiler/Xml/DocumentationElement.cs @@ -0,0 +1,254 @@ +// Copyright (c) 2009-2013 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.Decompiler.Util; + +namespace ICSharpCode.Decompiler.Xml +{ + /// + /// Represents an element in the XML documentation. + /// Any occurrences of "<inheritdoc/>" are replaced with the inherited documentation. + /// + public class XmlDocumentationElement + { + static XmlDocumentationElement Create(string documentationComment, IEntity declaringEntity) + { + var doc = new AXmlParser().Parse(new StringTextSource(documentationComment)); + return new XmlDocumentationElement(doc, declaringEntity, null); + } + + readonly AXmlObject xmlObject; + readonly AXmlElement element; + readonly IEntity declaringEntity; + readonly Func crefResolver; + volatile string textContent; + + /// + /// Inheritance level; used to prevent cyclic doc inheritance. + /// + int nestingLevel; + + /// + /// Creates a new documentation element. + /// + public XmlDocumentationElement(AXmlElement element, IEntity declaringEntity, Func crefResolver) + { + if (element == null) + throw new ArgumentNullException("element"); + this.element = element; + this.xmlObject = element; + this.declaringEntity = declaringEntity; + this.crefResolver = crefResolver; + } + + /// + /// Creates a new documentation element. + /// + public XmlDocumentationElement(AXmlDocument document, IEntity declaringEntity, Func crefResolver) + { + if (document == null) + throw new ArgumentNullException("document"); + this.xmlObject = document; + this.declaringEntity = declaringEntity; + this.crefResolver = crefResolver; + } + + /// + /// Creates a new documentation element. + /// + public XmlDocumentationElement(string text, IEntity declaringEntity) + { + if (text == null) + throw new ArgumentNullException("text"); + this.declaringEntity = declaringEntity; + this.textContent = text; + } + + /// + /// Gets the entity on which this documentation was originally declared. + /// May return null. + /// + public IEntity DeclaringEntity { + get { return declaringEntity; } + } + + IEntity referencedEntity; + volatile bool referencedEntityInitialized; + + /// + /// Gets the entity referenced by the 'cref' attribute. + /// May return null. + /// + public IEntity ReferencedEntity { + get { + if (!referencedEntityInitialized) { + string cref = GetAttribute("cref"); + if (cref != null && crefResolver != null) + referencedEntity = crefResolver(cref); + referencedEntityInitialized = true; + } + return referencedEntity; + } + } + + /// + /// Gets the element name. + /// + public string Name { + get { + return element != null ? element.Name : string.Empty; + } + } + + /// + /// Gets the attribute value. + /// + public string GetAttribute(string name) + { + return element != null ? element.GetAttributeValue(name) : string.Empty; + } + + /// + /// Gets whether this is a pure text node. + /// + public bool IsTextNode { + get { return xmlObject == null; } + } + + /// + /// Gets the text content. + /// + public string TextContent { + get { + if (textContent == null) { + StringBuilder b = new StringBuilder(); + foreach (var child in this.Children) + b.Append(child.TextContent); + textContent = b.ToString(); + } + return textContent; + } + } + + IList children; + + /// + /// Gets the child elements. + /// + public IList Children { + get { + if (xmlObject == null) + return EmptyList.Instance; + return LazyInitializer.EnsureInitialized( + ref this.children, + () => CreateElements(xmlObject.Children, declaringEntity, crefResolver, nestingLevel)); + } + } + + static readonly string[] doNotInheritIfAlreadyPresent = { + "example", "exclude", "filterpriority", "preliminary", "summary", + "remarks", "returns", "threadsafety", "value" + }; + + static List CreateElements(IEnumerable childObjects, IEntity declaringEntity, Func crefResolver, int nestingLevel) + { + List list = new List(); + foreach (var child in childObjects) { + var childText = child as AXmlText; + var childTag = child as AXmlTag; + var childElement = child as AXmlElement; + if (childText != null) { + list.Add(new XmlDocumentationElement(childText.Value, declaringEntity)); + } else if (childTag != null && childTag.IsCData) { + foreach (var text in childTag.Children.OfType()) + list.Add(new XmlDocumentationElement(text.Value, declaringEntity)); + } else if (childElement != null) { + if (nestingLevel < 5 && childElement.Name == "inheritdoc") { + /*string cref = childElement.GetAttributeValue("cref"); + IEntity inheritedFrom = null; + DocumentationComment inheritedDocumentation = null; + if (cref != null) { + inheritedFrom = crefResolver(cref); + if (inheritedFrom != null) + inheritedDocumentation = inheritedFrom.Documentation; + } else { + foreach (IMember baseMember in InheritanceHelper.GetBaseMembers((IMember)declaringEntity, includeImplementedInterfaces: true)) { + inheritedDocumentation = baseMember.Documentation; + if (inheritedDocumentation != null) { + inheritedFrom = baseMember; + break; + } + } + } + + if (inheritedDocumentation != null) { + var doc = new AXmlParser().Parse(inheritedDocumentation.Xml); + + // XPath filter not yet implemented + if (childElement.Parent is AXmlDocument && childElement.GetAttributeValue("select") == null) { + // Inheriting documentation at the root level + List doNotInherit = new List(); + doNotInherit.Add("overloads"); + doNotInherit.AddRange(childObjects.OfType().Select(e => e.Name).Intersect( + doNotInheritIfAlreadyPresent)); + + var inheritedChildren = doc.Children.Where( + inheritedObject => { + AXmlElement inheritedElement = inheritedObject as AXmlElement; + return !(inheritedElement != null && doNotInherit.Contains(inheritedElement.Name)); + }); + + list.AddRange(CreateElements(inheritedChildren, inheritedFrom, inheritedDocumentation.ResolveCref, nestingLevel + 1)); + } + }*/ + } else { + list.Add(new XmlDocumentationElement(childElement, declaringEntity, crefResolver) { nestingLevel = nestingLevel }); + } + } + } + if (list.Count > 0 && list[0].IsTextNode) { + if (string.IsNullOrWhiteSpace(list[0].textContent)) + list.RemoveAt(0); + else + list[0].textContent = list[0].textContent.TrimStart(); + } + if (list.Count > 0 && list[list.Count - 1].IsTextNode) { + if (string.IsNullOrWhiteSpace(list[list.Count - 1].textContent)) + list.RemoveAt(list.Count - 1); + else + list[list.Count - 1].textContent = list[list.Count - 1].textContent.TrimEnd(); + } + return list; + } + + /// + public override string ToString() + { + if (element != null) + return "<" + element.Name + ">"; + else + return this.TextContent; + } + } +} diff --git a/ICSharpCode.Decompiler/Xml/ISegment.cs b/ICSharpCode.Decompiler/Xml/ISegment.cs new file mode 100644 index 000000000..ad9cc95e6 --- /dev/null +++ b/ICSharpCode.Decompiler/Xml/ISegment.cs @@ -0,0 +1,69 @@ +// Copyright (c) 2009-2013 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +namespace ICSharpCode.Decompiler.Xml +{ + /// + /// An (Offset,Length)-pair. + /// + public interface ISegment + { + /// + /// Gets the start offset of the segment. + /// + int Offset { get; } + + /// + /// Gets the length of the segment. + /// + /// For line segments (IDocumentLine), the length does not include the line delimeter. + int Length { get; } + + /// + /// Gets the end offset of the segment. + /// + /// EndOffset = Offset + Length; + int EndOffset { get; } + } + + /// + /// Extension methods for . + /// + public static class ISegmentExtensions + { + /// + /// Gets whether fully contains the specified segment. + /// + /// + /// Use segment.Contains(offset, 0) to detect whether a segment (end inclusive) contains offset; + /// use segment.Contains(offset, 1) to detect whether a segment (end exclusive) contains offset. + /// + public static bool Contains(this ISegment segment, int offset, int length) + { + return segment.Offset <= offset && offset + length <= segment.EndOffset; + } + + /// + /// Gets whether fully contains the specified segment. + /// + public static bool Contains(this ISegment thisSegment, ISegment segment) + { + return segment != null && thisSegment.Offset <= segment.Offset && segment.EndOffset <= thisSegment.EndOffset; + } + } +} diff --git a/ICSharpCode.Decompiler/Xml/ITextSource.cs b/ICSharpCode.Decompiler/Xml/ITextSource.cs new file mode 100644 index 000000000..6a2ec9fdd --- /dev/null +++ b/ICSharpCode.Decompiler/Xml/ITextSource.cs @@ -0,0 +1,218 @@ +// Copyright (c) 2009-2013 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.IO; + +namespace ICSharpCode.Decompiler.Xml +{ + /// + /// A read-only view on a (potentially mutable) text source. + /// The IDocument interface derives from this interface. + /// + public interface ITextSource + { + /// + /// Gets a version identifier for this text source. + /// Returns null for unversioned text sources. + /// + ITextSourceVersion Version { get; } + + /// + /// Creates an immutable snapshot of this text source. + /// Unlike all other methods in this interface, this method is thread-safe. + /// + ITextSource CreateSnapshot(); + + /// + /// Creates an immutable snapshot of a part of this text source. + /// Unlike all other methods in this interface, this method is thread-safe. + /// + ITextSource CreateSnapshot(int offset, int length); + + /// + /// Creates a new TextReader to read from this text source. + /// + TextReader CreateReader(); + + /// + /// Creates a new TextReader to read from this text source. + /// + TextReader CreateReader(int offset, int length); + + /// + /// Gets the total text length. + /// + /// The length of the text, in characters. + /// This is the same as Text.Length, but is more efficient because + /// it doesn't require creating a String object. + int TextLength { get; } + + /// + /// Gets the whole text as string. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods")] + string Text { get; } + + /// + /// Gets a character at the specified position in the document. + /// + /// The index of the character to get. + /// Offset is outside the valid range (0 to TextLength-1). + /// The character at the specified position. + /// This is the same as Text[offset], but is more efficient because + /// it doesn't require creating a String object. + char GetCharAt(int offset); + + /// + /// Retrieves the text for a portion of the document. + /// + /// offset or length is outside the valid range. + /// This is the same as Text.Substring, but is more efficient because + /// it doesn't require creating a String object for the whole document. + string GetText(int offset, int length); + + /// + /// Retrieves the text for a portion of the document. + /// + /// offset or length is outside the valid range. + string GetText(ISegment segment); + + /// + /// Writes the text from this document into the TextWriter. + /// + void WriteTextTo(TextWriter writer); + + /// + /// Writes the text from this document into the TextWriter. + /// + void WriteTextTo(TextWriter writer, int offset, int length); + + /// + /// Gets the index of the first occurrence of the character in the specified array. + /// + /// Character to search for + /// Start index of the area to search. + /// Length of the area to search. + /// The first index where the character was found; or -1 if no occurrence was found. + int IndexOf(char c, int startIndex, int count); + + /// + /// Gets the index of the first occurrence of any character in the specified array. + /// + /// Characters to search for + /// Start index of the area to search. + /// Length of the area to search. + /// The first index where any character was found; or -1 if no occurrence was found. + int IndexOfAny(char[] anyOf, int startIndex, int count); + + /// + /// Gets the index of the first occurrence of the specified search text in this text source. + /// + /// The search text + /// Start index of the area to search. + /// Length of the area to search. + /// String comparison to use. + /// The first index where the search term was found; or -1 if no occurrence was found. + int IndexOf(string searchText, int startIndex, int count, StringComparison comparisonType); + + /// + /// Gets the index of the last occurrence of the specified character in this text source. + /// + /// The search character + /// Start index of the area to search. + /// Length of the area to search. + /// The last index where the search term was found; or -1 if no occurrence was found. + /// The search proceeds backwards from (startIndex+count) to startIndex. + /// This is different than the meaning of the parameters on string.LastIndexOf! + int LastIndexOf(char c, int startIndex, int count); + + /// + /// Gets the index of the last occurrence of the specified search text in this text source. + /// + /// The search text + /// Start index of the area to search. + /// Length of the area to search. + /// String comparison to use. + /// The last index where the search term was found; or -1 if no occurrence was found. + /// The search proceeds backwards from (startIndex+count) to startIndex. + /// This is different than the meaning of the parameters on string.LastIndexOf! + int LastIndexOf(string searchText, int startIndex, int count, StringComparison comparisonType); + + /* What about: + void Insert (int offset, string value); + void Remove (int offset, int count); + void Remove (ISegment segment); + + void Replace (int offset, int count, string value); + + Or more search operations: + + IEnumerable SearchForward (string pattern, int startIndex); + IEnumerable SearchForwardIgnoreCase (string pattern, int startIndex); + + IEnumerable SearchBackward (string pattern, int startIndex); + IEnumerable SearchBackwardIgnoreCase (string pattern, int startIndex); + */ + } + + /// + /// Represents a version identifier for a text source. + /// + /// + /// Verions can be used to efficiently detect whether a document has changed and needs reparsing; + /// or even to implement incremental parsers. + /// It is a separate class from ITextSource to allow the GC to collect the text source while + /// the version checkpoint is still in use. + /// + public interface ITextSourceVersion + { + /// + /// Gets whether this checkpoint belongs to the same document as the other checkpoint. + /// + /// + /// Returns false when given null. + /// + bool BelongsToSameDocumentAs(ITextSourceVersion other); + + /// + /// Compares the age of this checkpoint to the other checkpoint. + /// + /// This method is thread-safe. + /// Raised if 'other' belongs to a different document than this version. + /// -1 if this version is older than . + /// 0 if this version instance represents the same version as . + /// 1 if this version is newer than . + int CompareAge(ITextSourceVersion other); + + /// + /// Gets the changes from this checkpoint to the other checkpoint. + /// If 'other' is older than this checkpoint, reverse changes are calculated. + /// + /// This method is thread-safe. + /// Raised if 'other' belongs to a different document than this checkpoint. + IEnumerable GetChangesTo(ITextSourceVersion other); + + /// + /// Calculates where the offset has moved in the other buffer version. + /// + /// Raised if 'other' belongs to a different document than this checkpoint. + int MoveOffsetTo(ITextSourceVersion other, int oldOffset, AnchorMovementType movement = AnchorMovementType.Default); + } +} diff --git a/ICSharpCode.Decompiler/Xml/IncrementalParserState.cs b/ICSharpCode.Decompiler/Xml/IncrementalParserState.cs new file mode 100644 index 000000000..3c62429fb --- /dev/null +++ b/ICSharpCode.Decompiler/Xml/IncrementalParserState.cs @@ -0,0 +1,114 @@ +// Copyright (c) 2009-2013 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace ICSharpCode.Decompiler.Xml +{ + /// + /// Encapsulates the state of the incremental tag soup parser. + /// + public class IncrementalParserState + { + internal readonly int TextLength; + internal readonly ITextSourceVersion Version; + internal readonly InternalObject[] Objects; + + internal IncrementalParserState(int textLength, ITextSourceVersion version, InternalObject[] objects) + { + this.TextLength = textLength; + this.Version = version; + this.Objects = objects; + } + + internal List GetReuseMapTo(ITextSourceVersion newVersion) + { + ITextSourceVersion oldVersion = this.Version; + if (oldVersion == null || newVersion == null) + return null; + if (!oldVersion.BelongsToSameDocumentAs(newVersion)) + return null; + List reuseMap = new List(); + reuseMap.Add(new UnchangedSegment(0, 0, this.TextLength)); + foreach (var change in oldVersion.GetChangesTo(newVersion)) { + bool needsSegmentRemoval = false; + for (int i = 0; i < reuseMap.Count; i++) { + UnchangedSegment segment = reuseMap[i]; + if (segment.NewOffset + segment.Length <= change.Offset) { + // change is completely after this segment + continue; + } + if (change.Offset + change.RemovalLength <= segment.NewOffset) { + // change is completely before this segment + segment.NewOffset += change.InsertionLength - change.RemovalLength; + reuseMap[i] = segment; + continue; + } + // Change is overlapping segment. + // Split segment into two parts: the part before change, and the part after change. + var segmentBefore = new UnchangedSegment(segment.OldOffset, segment.NewOffset, change.Offset - segment.NewOffset); + Debug.Assert(segmentBefore.Length < segment.Length); + + int lengthAtEnd = segment.NewOffset + segment.Length - (change.Offset + change.RemovalLength); + var segmentAfter = new UnchangedSegment( + segment.OldOffset + segment.Length - lengthAtEnd, + change.Offset + change.InsertionLength, + lengthAtEnd); + Debug.Assert(segmentAfter.Length < segment.Length); + Debug.Assert(segmentBefore.Length + segmentAfter.Length <= segment.Length); + Debug.Assert(segmentBefore.NewOffset + segmentBefore.Length <= segmentAfter.NewOffset); + Debug.Assert(segmentBefore.OldOffset + segmentBefore.Length <= segmentAfter.OldOffset); + if (segmentBefore.Length > 0 && segmentAfter.Length > 0) { + reuseMap[i] = segmentBefore; + reuseMap.Insert(++i, segmentAfter); + } else if (segmentBefore.Length > 0) { + reuseMap[i] = segmentBefore; + } else { + reuseMap[i] = segmentAfter; + if (segmentAfter.Length <= 0) + needsSegmentRemoval = true; + } + } + if (needsSegmentRemoval) + reuseMap.RemoveAll(s => s.Length <= 0); + } + return reuseMap; + } + } + + struct UnchangedSegment + { + public int OldOffset; + public int NewOffset; + public int Length; + + public UnchangedSegment(int oldOffset, int newOffset, int length) + { + this.OldOffset = oldOffset; + this.NewOffset = newOffset; + this.Length = length; + } + + public override string ToString() + { + return string.Format("[UnchangedSegment OldOffset={0}, NewOffset={1}, Length={2}]", OldOffset, NewOffset, Length); + } + } +} diff --git a/ICSharpCode.Decompiler/Xml/InternalDocument.cs b/ICSharpCode.Decompiler/Xml/InternalDocument.cs new file mode 100644 index 000000000..f0277a901 --- /dev/null +++ b/ICSharpCode.Decompiler/Xml/InternalDocument.cs @@ -0,0 +1,175 @@ +// Copyright (c) 2009-2013 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; + +namespace ICSharpCode.Decompiler.Xml +{ + internal abstract class InternalObject + { + public int StartRelativeToParent; + public int Length; + /// Length that was touched to parsed this object. + public int LengthTouched; + public InternalSyntaxError[] SyntaxErrors; + public InternalObject[] NestedObjects; + + public InternalObject SetStartRelativeToParent(int newStartRelativeToParent) + { + if (newStartRelativeToParent == StartRelativeToParent) + return this; + InternalObject obj = (InternalObject)MemberwiseClone(); + obj.StartRelativeToParent = newStartRelativeToParent; + return obj; + } + + public abstract AXmlObject CreatePublicObject(AXmlObject parent, int parentStartOffset); + } + + sealed class InternalDocument : InternalObject + { + public override AXmlObject CreatePublicObject(AXmlObject parent, int parentStartOffset) + { + return new AXmlDocument(parent, (parent != null ? parentStartOffset + StartRelativeToParent : parentStartOffset), this); + } + } + + sealed class InternalText : InternalObject + { + public TextType Type; + public bool ContainsOnlyWhitespace; + public string Value; + + public override AXmlObject CreatePublicObject(AXmlObject parent, int parentStartOffset) + { + return new AXmlText(parent, (parent != null ? parentStartOffset + StartRelativeToParent : parentStartOffset), this); + } + + public override string ToString() + { + return "Text: " + this.Value.Replace("\n", "\\n").Replace("\r", "\\r"); + } + } + + sealed class InternalTag : InternalObject + { + public string OpeningBracket; + public int RelativeNameStart; + public string Name; + public string ClosingBracket; + + /// True if tag starts with "<" + public bool IsStartOrEmptyTag { get { return OpeningBracket == "<"; } } + /// True if tag starts with "<" and ends with ">" + public bool IsStartTag { get { return OpeningBracket == "<" && ClosingBracket == ">"; } } + /// True if tag starts with "<" and does not end with ">" + public bool IsEmptyTag { get { return OpeningBracket == "<" && ClosingBracket != ">" ; } } + /// True if tag starts with "</" + public bool IsEndTag { get { return OpeningBracket == " True if tag starts with "<?" + public bool IsProcessingInstruction { get { return OpeningBracket == " True if tag starts with "<!--" + public bool IsComment { get { return OpeningBracket == "") OnSyntaxError(brStart, brEnd, "'-->' expected"); + } else if (tag.IsCData) { + if (tag.ClosingBracket != "]]>") OnSyntaxError(brStart, brEnd, "']]>' expected"); + } else if (tag.IsProcessingInstruction) { + if (tag.ClosingBracket != "?>") OnSyntaxError(brStart, brEnd, "'?>' expected"); + } else if (tag.IsUnknownBang) { + if (tag.ClosingBracket != ">") OnSyntaxError(brStart, brEnd, "'>' expected"); + } else if (tag.IsDocumentType) { + if (tag.ClosingBracket != ">") OnSyntaxError(brStart, brEnd, "'>' expected"); + } else { + throw new InternalException(string.Format(CultureInfo.InvariantCulture, "Unknown opening bracket '{0}'", tag.OpeningBracket)); + } + + // Attribute name may not apper multiple times + if (objects.Count > oldObjectCount) { + // Move nested objects into tag.NestedObjects: + tag.NestedObjects = new InternalObject[objects.Count - oldObjectCount]; + objects.CopyTo(oldObjectCount, tag.NestedObjects, 0, tag.NestedObjects.Length); + objects.RemoveRange(oldObjectCount, objects.Count - oldObjectCount); + + // Look for duplicate attributes: + HashSet attributeNames = new HashSet(); + foreach (var obj in tag.NestedObjects) { + InternalAttribute attr = obj as InternalAttribute; + if (attr != null && !attributeNames.Add(attr.Name)) { + int attrStart = tagStart + attr.StartRelativeToParent; + OnSyntaxError(attrStart, attrStart + attr.Name.Length, "Attribute with name '{0}' already exists", attr.Name); + } + } + } + + EndInternalObject(frame); + } + #endregion + + #region Read DTD + void ReadContentOfDTD() + { + int start = this.CurrentLocation; + while (HasMoreData()) { + TryMoveToNonWhiteSpace(); // Skip whitespace + if (TryRead('\'')) TryMoveTo('\''); // Skip single quoted string TODO: Bug + if (TryRead('\"')) TryMoveTo('\"'); // Skip single quoted string + if (TryRead('[')) { // Start of nested infoset + // Reading infoset + while (HasMoreData()) { + TryMoveToAnyOf('<', ']'); + if (TryPeek('<')) { + if (start != this.CurrentLocation) { // Two following tags + MakeText(start, this.CurrentLocation); + } + ReadTag(); + start = this.CurrentLocation; + } + if (TryPeek(']')) break; + } + } + TryRead(']'); // End of nested infoset + if (TryPeek('>')) break; // Proper closing + if (TryPeek('<')) break; // Malformed XML + TryMoveNext(); // Skip anything else + } + if (start != this.CurrentLocation) { + MakeText(start, this.CurrentLocation); + } + } + + void MakeText(int start, int end) + { + Log.DebugAssert(end > start, "Empty text"); + Log.DebugAssert(end == this.CurrentLocation, "end == current location"); + + InternalText text = new InternalText(); + var frame = BeginInternalObject(text, start); + text.Type = TextType.Other; + text.Value = GetText(start, end); + EndInternalObject(frame); + } + #endregion + + #region Read Brackets + /// + /// Reads any of the know opening brackets. (only full bracket) + /// Context: "<" + /// + string ReadOpeningBracket() + { + // We are using a lot of string literals so that the memory instances are shared + //int start = this.CurrentLocation; + if (TryRead('<')) { + if (TryRead('/')) { + return " + /// Reads any of the know closing brackets. (only full bracket) + /// Context: any + /// + bool TryReadClosingBracket(out string bracket) + { + // We are using a lot of string literals so that the memory instances are shared + if (TryRead('>')) { + bracket = ">"; + } else if (TryRead("/>")) { + bracket = "/>"; + } else if (TryRead("?>")) { + bracket = "?>"; + } else if (TryRead("-->")) { + bracket = "-->"; + } else if (TryRead("]]>")) { + bracket = "]]>"; + } else { + bracket = string.Empty; + return false; + } + return true; + } + #endregion + + #region Attributes + /// + /// Context: name or "=\'\"" + /// + void ReadAttribute() + { + AssertHasMoreData(); + + InternalAttribute attr = new InternalAttribute(); + var frame = BeginInternalObject(attr); + + // Read name + string name; + if (TryReadName(out name)) { + if (!IsValidName(name)) { + OnSyntaxError(this.CurrentLocation - name.Length, this.CurrentLocation, "The name '{0}' is invalid", name); + } + } else { + OnSyntaxError("Attribute name expected"); + } + attr.Name = name; + + // Read equals sign and surrounding whitespace + int checkpoint = this.CurrentLocation; + TryMoveToNonWhiteSpace(); + if (TryRead('=')) { + int chk2 = this.CurrentLocation; + TryMoveToNonWhiteSpace(); + if (!TryPeek('"') && !TryPeek('\'')) { + // Do not read whitespace if quote does not follow + GoBack(chk2); + } + attr.EqualsSignLength = this.CurrentLocation - checkpoint; + } else { + GoBack(checkpoint); + OnSyntaxError("'=' expected"); + attr.EqualsSignLength = 0; + } + + // Read attribute value + int start = this.CurrentLocation; + char quoteChar = TryPeek('"') ? '"' : '\''; + bool startsWithQuote; + if (TryRead(quoteChar)) { + startsWithQuote = true; + int valueStart = this.CurrentLocation; + TryMoveToAnyOf(quoteChar, '<'); + if (TryRead(quoteChar)) { + if (!TryPeekAnyOf(' ', '\t', '\n', '\r', '/', '>', '?')) { + if (TryPeekPrevious('=', 2) || (TryPeekPrevious('=', 3) && TryPeekPrevious(' ', 2))) { + // This actually most likely means that we are in the next attribute value + GoBack(valueStart); + ReadAttributeValue(quoteChar); + if (TryRead(quoteChar)) { + OnSyntaxError("White space or end of tag expected"); + } else { + OnSyntaxError("Quote {0} expected (or add whitespace after the following one)", quoteChar); + } + } else { + OnSyntaxError("White space or end of tag expected"); + } + } + } else { + // '<' or end of file + GoBack(valueStart); + ReadAttributeValue(quoteChar); + OnSyntaxError("Quote {0} expected", quoteChar); + } + } else { + startsWithQuote = false; + int valueStart = this.CurrentLocation; + ReadAttributeValue(null); + TryRead('\"'); + TryRead('\''); + if (valueStart == this.CurrentLocation) { + OnSyntaxError("Attribute value expected"); + } else { + OnSyntaxError(valueStart, this.CurrentLocation, "Attribute value must be quoted"); + } + } + string val = GetText(start, this.CurrentLocation); + val = Unquote(val); + attr.Value = Dereference(val, startsWithQuote ? start + 1 : start); + + EndInternalObject(frame); + } + + /// + /// Read everything up to quote (excluding), opening/closing tag or attribute signature + /// + void ReadAttributeValue(char? quote) + { + while (HasMoreData()) { + // What is next? + int start = this.CurrentLocation; + TryMoveToNonWhiteSpace(); // Read white space (if any) + if (quote.HasValue) { + if (TryPeek(quote.Value)) return; + } else { + if (TryPeek('"') || TryPeek('\'')) return; + } + // Opening/closing tag + string endBr; + if (TryPeek('<') || TryReadClosingBracket(out endBr)) { + GoBack(start); + return; + } + // Try reading attribute signature + if (TryReadName()) { + int nameEnd = this.CurrentLocation; + if (TryMoveToNonWhiteSpace() && TryRead("=") && + TryMoveToNonWhiteSpace() && TryPeekAnyOf('"', '\'')) + { + // Start of attribute. Great + GoBack(start); + return; // Done + } else { + // Just some gargabe - make it part of the value + GoBack(nameEnd); + continue; // Read more + } + } + TryMoveNext(); // Accept everyting else + } + } + + /// Remove quoting from the given string + static string Unquote(string quoted) + { + if (string.IsNullOrEmpty(quoted)) return string.Empty; + char first = quoted[0]; + if (quoted.Length == 1) return (first == '"' || first == '\'') ? string.Empty : quoted; + char last = quoted[quoted.Length - 1]; + if (first == '"' || first == '\'') { + if (first == last) { + // Remove both quotes + return quoted.Substring(1, quoted.Length - 2); + } else { + // Remove first quote + return quoted.Remove(0, 1); + } + } else { + if (last == '"' || last == '\'') { + // Remove last quote + return quoted.Substring(0, quoted.Length - 1); + } else { + // Keep whole string + return quoted; + } + } + } + #endregion + + #region Text + /// + /// Reads text. + /// + void ReadText(TextType type) + { + var text = new InternalText(); + var frame = BeginInternalObject(text); + text.Type = type; + + int start = this.CurrentLocation; + int fragmentEnd = inputLength; + + // Whitespace would be skipped anyway by any operation + TryMoveToNonWhiteSpace(fragmentEnd); + int wsEnd = this.CurrentLocation; + + // Try move to the terminator given by the context + if (type == TextType.WhiteSpace) { + TryMoveToNonWhiteSpace(fragmentEnd); + } else if (type == TextType.CharacterData) { + while(true) { + if (!TryMoveToAnyOf(new char[] {'<', ']'}, fragmentEnd)) break; // End of fragment + if (TryPeek('<')) break; + if (TryPeek(']')) { + if (TryPeek("]]>")) { + OnSyntaxError(this.CurrentLocation, this.CurrentLocation + 3, "']]>' is not allowed in text"); + } + TryMoveNext(); + continue; + } + throw new InternalException("Infinite loop"); + } + } else if (type == TextType.Comment) { + // Do not report too many errors + bool errorReported = false; + while(true) { + if (!TryMoveTo('-', fragmentEnd)) break; // End of fragment + if (TryPeek("-->")) break; + if (TryPeek("--") && !errorReported) { + OnSyntaxError(this.CurrentLocation, this.CurrentLocation + 2, "'--' is not allowed in comment"); + errorReported = true; + } + TryMoveNext(); + } + } else if (type == TextType.CData) { + while(true) { + // We can not use use TryMoveTo("]]>", fragmentEnd) because it may incorectly accept "]" at the end of fragment + if (!TryMoveTo(']', fragmentEnd)) break; // End of fragment + if (TryPeek("]]>")) break; + TryMoveNext(); + } + } else if (type == TextType.ProcessingInstruction) { + while(true) { + if (!TryMoveTo('?', fragmentEnd)) break; // End of fragment + if (TryPeek("?>")) break; + TryMoveNext(); + } + } else if (type == TextType.UnknownBang) { + TryMoveToAnyOf(new char[] {'<', '>'}, fragmentEnd); + } else { + throw new InternalException("Unknown type " + type); + } + + text.ContainsOnlyWhitespace = (wsEnd == this.CurrentLocation); + + string escapedValue = GetText(start, this.CurrentLocation); + if (type == TextType.CharacterData) { + text.Value = Dereference(escapedValue, start); + } else { + text.Value = escapedValue; + } + text.Value = GetCachedString(text.Value); + + EndInternalObject(frame, storeNewObject: this.CurrentLocation > start); + } + #endregion + + #region Dereference + const int maxEntityLength = 16; // The longest built-in one is 10 ("􏿿") + + string Dereference(string text, int textLocation) + { + StringBuilder sb = null; // The dereferenced text so far (all up to 'curr') + int curr = 0; + while(true) { + // Reached end of input + if (curr == text.Length) { + if (sb != null) { + return sb.ToString(); + } else { + return text; + } + } + + // Try to find reference + int start = text.IndexOf('&', curr); + + // No more references found + if (start == -1) { + if (sb != null) { + sb.Append(text, curr, text.Length - curr); // Add rest + return sb.ToString(); + } else { + return text; + } + } + + // Append text before the enitiy reference + if (sb == null) sb = new StringBuilder(text.Length); + sb.Append(text, curr, start - curr); + curr = start; + + // Process the entity + int errorLoc = textLocation + sb.Length; + + // Find entity name + int end = text.IndexOfAny(new char[] {'&', ';'}, start + 1, Math.Min(maxEntityLength, text.Length - (start + 1))); + if (end == -1 || text[end] == '&') { + // Not found + OnSyntaxError(errorLoc, errorLoc + 1, "Entity reference must be terminated with ';'"); + // Keep '&' + sb.Append('&'); + curr++; + continue; // Restart and next character location + } + string name = text.Substring(start + 1, end - (start + 1)); + + // Resolve the name + string replacement; + if (name.Length == 0) { + replacement = null; + OnSyntaxError(errorLoc + 1, errorLoc + 1, "Entity name expected"); + } else if (name == "amp") { + replacement = "&"; + } else if (name == "lt") { + replacement = "<"; + } else if (name == "gt") { + replacement = ">"; + } else if (name == "apos") { + replacement = "'"; + } else if (name == "quot") { + replacement = "\""; + } else if (name.Length > 0 && name[0] == '#') { + int num; + if (name.Length > 1 && name[1] == 'x') { + if (!int.TryParse(name.Substring(2), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture.NumberFormat, out num)) { + num = -1; + OnSyntaxError(errorLoc + 3, errorLoc + 1 + name.Length, "Hexadecimal code of unicode character expected"); + } + } else { + if (!int.TryParse(name.Substring(1), NumberStyles.None, CultureInfo.InvariantCulture.NumberFormat, out num)) { + num = -1; + OnSyntaxError(errorLoc + 2, errorLoc + 1 + name.Length, "Numeric code of unicode character expected"); + } + } + if (num != -1) { + try { + replacement = char.ConvertFromUtf32(num); + } catch (ArgumentOutOfRangeException) { + replacement = null; + OnSyntaxError(errorLoc + 2, errorLoc + 1 + name.Length, "Invalid unicode character U+{0:X} ({0})", num); + } + } else { + replacement = null; + } + } else if (!IsValidName(name)) { + replacement = null; + OnSyntaxError(errorLoc + 1, errorLoc + 1, "Invalid entity name"); + } else { + replacement = null; + if (tagSoupParser.UnknownEntityReferenceIsError) { + OnSyntaxError(errorLoc, errorLoc + 1 + name.Length + 1, "Unknown entity reference '{0}'", name); + } + } + + // Append the replacement to output + if (replacement != null) { + sb.Append(replacement); + } else { + sb.Append('&'); + sb.Append(name); + sb.Append(';'); + } + curr = end + 1; + continue; + } + } + #endregion + + #region Syntax Errors + List syntaxErrors = new List(); + + InternalSyntaxError[] GetSyntaxErrors() + { + if (syntaxErrors.Count > 0) { + var arr = syntaxErrors.ToArray(); + syntaxErrors.Clear(); + return arr; + } else { + return null; + } + } + + void OnSyntaxError(string message, params object[] args) + { + OnSyntaxError(this.CurrentLocation, this.CurrentLocation + 1, message, args); + } + + void OnSyntaxError(int start, int end, string message, params object[] args) + { + if (end <= start) end = start + 1; + string formattedMessage = string.Format(CultureInfo.InvariantCulture, message, args); + Log.WriteLine("Syntax error ({0}-{1}): {2}", start, end, formattedMessage); + syntaxErrors.Add(new InternalSyntaxError(start - internalObjectStartPosition, end - internalObjectStartPosition, formattedMessage)); + } + #endregion + + #region Helper functions + internal static bool IsValidName(string name) + { + try { + System.Xml.XmlConvert.VerifyName(name); + return true; + } catch (System.Xml.XmlException) { + return false; + } + } + #endregion + } +} diff --git a/ICSharpCode.Decompiler/Xml/TextChangeEventArgs.cs b/ICSharpCode.Decompiler/Xml/TextChangeEventArgs.cs new file mode 100644 index 000000000..4ec2e0d08 --- /dev/null +++ b/ICSharpCode.Decompiler/Xml/TextChangeEventArgs.cs @@ -0,0 +1,100 @@ +using System; + +namespace ICSharpCode.Decompiler.Xml +{ + /// + /// Describes a change of the document text. + /// This class is thread-safe. + /// + [Serializable] + public class TextChangeEventArgs : EventArgs + { + readonly int offset; + readonly ITextSource removedText; + readonly ITextSource insertedText; + + /// + /// The offset at which the change occurs. + /// + public int Offset { + get { return offset; } + } + + /// + /// The text that was removed. + /// + public ITextSource RemovedText { + get { return removedText; } + } + + /// + /// The number of characters removed. + /// + public int RemovalLength { + get { return removedText.TextLength; } + } + + /// + /// The text that was inserted. + /// + public ITextSource InsertedText { + get { return insertedText; } + } + + /// + /// The number of characters inserted. + /// + public int InsertionLength { + get { return insertedText.TextLength; } + } + + /// + /// Creates a new TextChangeEventArgs object. + /// + public TextChangeEventArgs(int offset, string removedText, string insertedText) + { + if (offset < 0) + throw new ArgumentOutOfRangeException("offset", offset, "offset must not be negative"); + this.offset = offset; + this.removedText = removedText != null ? new StringTextSource(removedText) : StringTextSource.Empty; + this.insertedText = insertedText != null ? new StringTextSource(insertedText) : StringTextSource.Empty; + } + + /// + /// Creates a new TextChangeEventArgs object. + /// + public TextChangeEventArgs(int offset, ITextSource removedText, ITextSource insertedText) + { + if (offset < 0) + throw new ArgumentOutOfRangeException("offset", offset, "offset must not be negative"); + this.offset = offset; + this.removedText = removedText ?? StringTextSource.Empty; + this.insertedText = insertedText ?? StringTextSource.Empty; + } + + /// + /// Gets the new offset where the specified offset moves after this document change. + /// + public virtual int GetNewOffset(int offset, AnchorMovementType movementType = AnchorMovementType.Default) + { + if (offset >= this.Offset && offset <= this.Offset + this.RemovalLength) { + if (movementType == AnchorMovementType.BeforeInsertion) + return this.Offset; + else + return this.Offset + this.InsertionLength; + } else if (offset > this.Offset) { + return offset + this.InsertionLength - this.RemovalLength; + } else { + return offset; + } + } + + /// + /// Creates TextChangeEventArgs for the reverse change. + /// + public virtual TextChangeEventArgs Invert() + { + return new TextChangeEventArgs(offset, insertedText, removedText); + } + } +} diff --git a/ICSharpCode.Decompiler/Xml/TextLocation.cs b/ICSharpCode.Decompiler/Xml/TextLocation.cs new file mode 100644 index 000000000..d01df467a --- /dev/null +++ b/ICSharpCode.Decompiler/Xml/TextLocation.cs @@ -0,0 +1,223 @@ +// Copyright (c) 2009-2013 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.ComponentModel; +using System.Globalization; + +namespace ICSharpCode.Decompiler.Xml +{ + /// + /// A line/column position. + /// Text editor lines/columns are counted started from one. + /// + /// + /// The document provides the methods and + /// to convert between offsets and TextLocations. + /// + [Serializable] + [TypeConverter(typeof(TextLocationConverter))] + public struct TextLocation : IComparable, IEquatable + { + /// + /// Represents no text location (0, 0). + /// + public static readonly TextLocation Empty = new TextLocation(0, 0); + + /// + /// Constant of the minimum line. + /// + public const int MinLine = 1; + + /// + /// Constant of the minimum column. + /// + public const int MinColumn = 1; + + /// + /// Creates a TextLocation instance. + /// + public TextLocation(int line, int column) + { + this.line = line; + this.column = column; + } + + int column, line; + + /// + /// Gets the line number. + /// + public int Line { + get { return line; } + } + + /// + /// Gets the column number. + /// + public int Column { + get { return column; } + } + + /// + /// Gets whether the TextLocation instance is empty. + /// + public bool IsEmpty { + get { + return column < MinLine && line < MinColumn; + } + } + + /// + /// Gets a string representation for debugging purposes. + /// + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "(Line {1}, Col {0})", this.column, this.line); + } + + /// + /// Gets a hash code. + /// + public override int GetHashCode() + { + return unchecked(191 * column.GetHashCode() ^ line.GetHashCode()); + } + + /// + /// Equality test. + /// + public override bool Equals(object obj) + { + if (!(obj is TextLocation)) return false; + return (TextLocation)obj == this; + } + + /// + /// Equality test. + /// + public bool Equals(TextLocation other) + { + return this == other; + } + + /// + /// Equality test. + /// + public static bool operator ==(TextLocation left, TextLocation right) + { + return left.column == right.column && left.line == right.line; + } + + /// + /// Inequality test. + /// + public static bool operator !=(TextLocation left, TextLocation right) + { + return left.column != right.column || left.line != right.line; + } + + /// + /// Compares two text locations. + /// + public static bool operator <(TextLocation left, TextLocation right) + { + if (left.line < right.line) + return true; + else if (left.line == right.line) + return left.column < right.column; + else + return false; + } + + /// + /// Compares two text locations. + /// + public static bool operator >(TextLocation left, TextLocation right) + { + if (left.line > right.line) + return true; + else if (left.line == right.line) + return left.column > right.column; + else + return false; + } + + /// + /// Compares two text locations. + /// + public static bool operator <=(TextLocation left, TextLocation right) + { + return !(left > right); + } + + /// + /// Compares two text locations. + /// + public static bool operator >=(TextLocation left, TextLocation right) + { + return !(left < right); + } + + /// + /// Compares two text locations. + /// + public int CompareTo(TextLocation other) + { + if (this == other) + return 0; + if (this < other) + return -1; + else + return 1; + } + } + + public class TextLocationConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); + } + + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { + return destinationType == typeof(TextLocation) || base.CanConvertTo(context, destinationType); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + if (value is string) { + string[] parts = ((string)value).Split(';', ','); + if (parts.Length == 2) { + return new TextLocation(int.Parse(parts[0]), int.Parse(parts[1])); + } + } + return base.ConvertFrom(context, culture, value); + } + + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) + { + if (value is TextLocation) { + var loc = (TextLocation)value; + return loc.Line + ";" + loc.Column; + } + return base.ConvertTo(context, culture, value, destinationType); + } + } +} diff --git a/ICSharpCode.Decompiler/Xml/TextType.cs b/ICSharpCode.Decompiler/Xml/TextType.cs new file mode 100644 index 000000000..e46bfd1b9 --- /dev/null +++ b/ICSharpCode.Decompiler/Xml/TextType.cs @@ -0,0 +1,47 @@ +// Copyright (c) 2009-2013 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; + +namespace ICSharpCode.Decompiler.Xml +{ + /// Identifies the context in which the text occured + enum TextType + { + /// Ends with non-whitespace + WhiteSpace, + + /// Ends with "<"; "]]>" is error + CharacterData, + + /// Ends with "-->"; "--" is error + Comment, + + /// Ends with "]]>" + CData, + + /// Ends with "?>" + ProcessingInstruction, + + /// Ends with "<" or ">" + UnknownBang, + + /// Unknown + Other + } +} diff --git a/ICSharpCode.Decompiler/Xml/TokenReader.cs b/ICSharpCode.Decompiler/Xml/TokenReader.cs new file mode 100644 index 000000000..84ea0c171 --- /dev/null +++ b/ICSharpCode.Decompiler/Xml/TokenReader.cs @@ -0,0 +1,349 @@ +// Copyright (c) 2009-2013 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace ICSharpCode.Decompiler.Xml +{ + class TokenReader + { + protected readonly ITextSource input; + protected readonly int inputLength; + int currentLocation; + + // CurrentLocation is assumed to be touched and the fact does not + // have to be recorded in this variable. + // This stores any value bigger than that if applicable. + // Actual value is max(currentLocation, maxTouchedLocation). + int maxTouchedLocation; + + public int InputLength { + get { return inputLength; } + } + + public int CurrentLocation { + get { return currentLocation; } + } + + public int MaxTouchedLocation { + // add 1 to currentLocation because single-char-peek does not increment maxTouchedLocation + get { return Math.Max(currentLocation + 1, maxTouchedLocation); } + } + + public TokenReader(ITextSource input) + { + this.input = input; + this.inputLength = input.TextLength; + } + + protected bool IsEndOfFile() + { + return currentLocation == inputLength; + } + + protected bool HasMoreData() + { + return currentLocation < inputLength; + } + + protected void AssertHasMoreData() + { + Log.Assert(HasMoreData(), "Unexpected end of file"); + } + + protected bool TryMoveNext() + { + if (currentLocation == inputLength) return false; + + currentLocation++; + return true; + } + + protected void Skip(int count) + { + Log.Assert(currentLocation + count <= inputLength, "Skipping after the end of file"); + currentLocation += count; + } + + protected void GoBack(int oldLocation) + { + Log.Assert(oldLocation <= currentLocation, "Trying to move forward"); + // add 1 because single-char-peek does not increment maxTouchedLocation + maxTouchedLocation = Math.Max(maxTouchedLocation, currentLocation + 1); + currentLocation = oldLocation; + } + + protected bool TryRead(char c) + { + if (currentLocation == inputLength) return false; + + if (input.GetCharAt(currentLocation) == c) { + currentLocation++; + return true; + } else { + return false; + } + } + + protected bool TryReadAnyOf(params char[] c) + { + if (currentLocation == inputLength) return false; + + if (c.Contains(input.GetCharAt(currentLocation))) { + currentLocation++; + return true; + } else { + return false; + } + } + + protected bool TryRead(string text) + { + if (TryPeek(text)) { + currentLocation += text.Length; + return true; + } else { + return false; + } + } + + protected bool TryPeekPrevious(char c, int back) + { + if (currentLocation - back == inputLength) return false; + if (currentLocation - back < 0 ) return false; + + return input.GetCharAt(currentLocation - back) == c; + } + + protected bool TryPeek(char c) + { + if (currentLocation == inputLength) return false; + + return input.GetCharAt(currentLocation) == c; + } + + protected bool TryPeekAnyOf(params char[] chars) + { + if (currentLocation == inputLength) return false; + + return chars.Contains(input.GetCharAt(currentLocation)); + } + + protected bool TryPeek(string text) + { + if (!TryPeek(text[0])) return false; // Early exit + + maxTouchedLocation = Math.Max(maxTouchedLocation, currentLocation + text.Length); + // The following comparison 'touches' the end of file - it does depend on the end being there + if (currentLocation + text.Length > inputLength) return false; + + return input.GetText(currentLocation, text.Length) == text; + } + + protected bool TryPeekWhiteSpace() + { + if (currentLocation == inputLength) return false; + + char c = input.GetCharAt(currentLocation); + return ((int)c <= 0x20) && (c == ' ' || c == '\t' || c == '\n' || c == '\r'); + } + + // The move functions do not have to move if already at target + // The move functions allow 'overriding' of the document length + + protected bool TryMoveTo(char c) + { + return TryMoveTo(c, inputLength); + } + + protected bool TryMoveTo(char c, int inputLength) + { + if (currentLocation == inputLength) return false; + int index = input.IndexOf(c, currentLocation, inputLength - currentLocation); + if (index != -1) { + currentLocation = index; + return true; + } else { + currentLocation = inputLength; + return false; + } + } + + protected bool TryMoveToAnyOf(params char[] c) + { + return TryMoveToAnyOf(c, inputLength); + } + + protected bool TryMoveToAnyOf(char[] c, int inputLength) + { + if (currentLocation == inputLength) return false; + int index = input.IndexOfAny(c, currentLocation, inputLength - currentLocation); + if (index != -1) { + currentLocation = index; + return true; + } else { + currentLocation = inputLength; + return false; + } + } + + protected bool TryMoveTo(string text) + { + return TryMoveTo(text, inputLength); + } + + protected bool TryMoveTo(string text, int inputLength) + { + if (currentLocation == inputLength) return false; + int index = input.IndexOf(text, currentLocation, inputLength - currentLocation, StringComparison.Ordinal); + if (index != -1) { + maxTouchedLocation = index + text.Length; + currentLocation = index; + return true; + } else { + currentLocation = inputLength; + return false; + } + } + + protected bool TryMoveToNonWhiteSpace() + { + return TryMoveToNonWhiteSpace(inputLength); + } + + protected bool TryMoveToNonWhiteSpace(int inputLength) + { + while(true) { + if (currentLocation == inputLength) return false; // Reject end of file + char c = input.GetCharAt(currentLocation); + if (((int)c <= 0x20) && (c == ' ' || c == '\t' || c == '\n' || c == '\r')) { + currentLocation++; // Accept white-space + continue; + } else { + return true; // Found non-white-space + } + } + } + + /// + /// Read a name token. + /// The following characters are not allowed: + /// "" End of file + /// " \n\r\t" Whitesapce + /// "=\'\"" Attribute value + /// "<>/?" Tags + /// + /// Returns the length of the name + protected bool TryReadName() + { + int start = currentLocation; + // Keep reading up to invalid character + while (HasMoreData()) { + char c = input.GetCharAt(currentLocation); + if (0x41 <= (int)c) { // Accpet from 'A' onwards + currentLocation++; + continue; + } + if (c == ' ' || c == '\n' || c == '\r' || c == '\t' || // Reject whitesapce + c == '=' || c == '\'' || c == '"' || // Reject attributes + c == '<' || c == '>' || c == '/' || c == '?') { // Reject tags + break; + } else { + currentLocation++; + continue; // Accept other character + } + } + return currentLocation > start; + } + + protected bool TryReadName(out string name) + { + int start = currentLocation; + if (TryReadName()) { + name = GetCachedString(GetText(start, currentLocation)); + return true; + } else { + name = string.Empty; + return false; + } + } + + protected string GetText(int start, int end) + { + Log.Assert(end <= currentLocation, "Reading ahead of current location"); + return input.GetText(start, end - start); + } + + Dictionary stringCache = new Dictionary(); + + #if DEBUG + int stringCacheRequestedCount; + int stringCacheRequestedSize; + int stringCacheStoredCount; + int stringCacheStoredSize; + #endif + + internal void PrintStringCacheStats() + { + #if DEBUG + Log.WriteLine("String cache: Requested {0} ({1} bytes); Actaully stored {2} ({3} bytes); {4}% stored", stringCacheRequestedCount, stringCacheRequestedSize, stringCacheStoredCount, stringCacheStoredSize, stringCacheRequestedSize == 0 ? 0 : stringCacheStoredSize * 100 / stringCacheRequestedSize); + #endif + } + + [Conditional("DEBUG")] + void AddToRequestedSize(string text) + { + #if DEBUG + stringCacheRequestedCount += 1; + stringCacheRequestedSize += 8 + 2 * text.Length; + #endif + } + + [Conditional("DEBUG")] + void AddToStoredSize(string text) + { + #if DEBUG + stringCacheStoredCount += 1; + stringCacheStoredSize += 8 + 2 * text.Length; + #endif + } + + protected string GetCachedString(string cached) + { + AddToRequestedSize(cached); + // Do not bother with long strings + if (cached.Length > 32) { + AddToStoredSize(cached); + return cached; + } + string result; + if (stringCache.TryGetValue(cached, out result)) { + // Get the instance from the cache instead + return result; + } else { + // Add to cache + AddToStoredSize(cached); + stringCache.Add(cached, cached); + return cached; + } + } + } +} diff --git a/ICSharpCode.Decompiler/Xml/XmlSegment.cs b/ICSharpCode.Decompiler/Xml/XmlSegment.cs new file mode 100644 index 000000000..ff168d13e --- /dev/null +++ b/ICSharpCode.Decompiler/Xml/XmlSegment.cs @@ -0,0 +1,47 @@ +// Copyright (c) 2009-2013 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; + +namespace ICSharpCode.Decompiler.Xml +{ + sealed class XmlSegment : ISegment + { + readonly int startOffset, endOffset; + + public XmlSegment(int startOffset, int endOffset) + { + if (endOffset < startOffset) + throw new ArgumentOutOfRangeException(); + this.startOffset = startOffset; + this.endOffset = endOffset; + } + + int ISegment.Offset { + get { return startOffset; } + } + + int ISegment.Length { + get { return endOffset - startOffset; } + } + + int ISegment.EndOffset { + get { return endOffset; } + } + } +} diff --git a/ILSpy/ExtensionMethods.cs b/ILSpy/ExtensionMethods.cs index 1d0815beb..bb7b59040 100644 --- a/ILSpy/ExtensionMethods.cs +++ b/ILSpy/ExtensionMethods.cs @@ -18,6 +18,8 @@ using System; using System.Collections.Generic; +using System.Windows; +using System.Windows.Media; using ICSharpCode.ILSpy.Options; namespace ICSharpCode.ILSpy @@ -161,5 +163,43 @@ namespace ICSharpCode.ILSpy } return result; } + + #region DPI independence + public static Rect TransformToDevice(this Rect rect, Visual visual) + { + Matrix matrix = PresentationSource.FromVisual(visual).CompositionTarget.TransformToDevice; + return Rect.Transform(rect, matrix); + } + + public static Rect TransformFromDevice(this Rect rect, Visual visual) + { + Matrix matrix = PresentationSource.FromVisual(visual).CompositionTarget.TransformFromDevice; + return Rect.Transform(rect, matrix); + } + + public static Size TransformToDevice(this Size size, Visual visual) + { + Matrix matrix = PresentationSource.FromVisual(visual).CompositionTarget.TransformToDevice; + return new Size(size.Width * matrix.M11, size.Height * matrix.M22); + } + + public static Size TransformFromDevice(this Size size, Visual visual) + { + Matrix matrix = PresentationSource.FromVisual(visual).CompositionTarget.TransformFromDevice; + return new Size(size.Width * matrix.M11, size.Height * matrix.M22); + } + + public static Point TransformToDevice(this Point point, Visual visual) + { + Matrix matrix = PresentationSource.FromVisual(visual).CompositionTarget.TransformToDevice; + return matrix.Transform(point); + } + + public static Point TransformFromDevice(this Point point, Visual visual) + { + Matrix matrix = PresentationSource.FromVisual(visual).CompositionTarget.TransformFromDevice; + return matrix.Transform(point); + } + #endregion } } diff --git a/ILSpy/ILSpy.csproj b/ILSpy/ILSpy.csproj index c7f0658d6..20d396e4a 100644 --- a/ILSpy/ILSpy.csproj +++ b/ILSpy/ILSpy.csproj @@ -39,6 +39,7 @@ + @@ -207,6 +208,7 @@ + diff --git a/ILSpy/Languages/Language.cs b/ILSpy/Languages/Language.cs index a1aad8d64..b131c6429 100644 --- a/ILSpy/Languages/Language.cs +++ b/ILSpy/Languages/Language.cs @@ -341,6 +341,11 @@ namespace ICSharpCode.ILSpy return GetDisplayName(entity, true, true, true); } + public virtual object GetFancyTooltip(IEntity entity) + { + return GetTooltip(entity); + } + public virtual string FieldToString(IField field, bool includeDeclaringTypeName, bool includeNamespace, bool includeNamespaceOfDeclaringTypeName) { if (field == null) diff --git a/ILSpy/TextView/DecompilerTextView.cs b/ILSpy/TextView/DecompilerTextView.cs index 8a4f05152..4bc85ca58 100644 --- a/ILSpy/TextView/DecompilerTextView.cs +++ b/ILSpy/TextView/DecompilerTextView.cs @@ -27,6 +27,7 @@ using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; +using System.Windows.Controls.Primitives; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; @@ -44,8 +45,10 @@ using ICSharpCode.AvalonEdit.Rendering; using ICSharpCode.AvalonEdit.Search; using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.CSharp; +using ICSharpCode.Decompiler.CSharp.OutputVisitor; using ICSharpCode.Decompiler.Documentation; using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.Output; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.ILSpy.AvalonEdit; using ICSharpCode.ILSpy.Options; @@ -63,7 +66,7 @@ namespace ICSharpCode.ILSpy.TextView { readonly ReferenceElementGenerator referenceElementGenerator; readonly UIElementGenerator uiElementGenerator; - List activeCustomElementGenerators = new List(); + readonly List activeCustomElementGenerators = new List(); RichTextColorizer activeRichTextColorizer; FoldingManager foldingManager; ILSpyTreeNode[] decompiledNodes; @@ -88,7 +91,7 @@ namespace ICSharpCode.ILSpy.TextView } }); - HighlightingManager.Instance.RegisterHighlighting( + /*HighlightingManager.Instance.RegisterHighlighting( "C#", new string[] { ".cs" }, delegate { using (Stream s = typeof(DecompilerTextView).Assembly.GetManifestResourceStream(typeof(DecompilerTextView), "CSharp-Mode.xshd")) { @@ -96,7 +99,7 @@ namespace ICSharpCode.ILSpy.TextView return HighlightingLoader.Load(reader, HighlightingManager.Instance); } } - }); + });*/ InitializeComponent(); @@ -109,6 +112,8 @@ namespace ICSharpCode.ILSpy.TextView textEditor.TextArea.TextView.MouseHoverStopped += TextViewMouseHoverStopped; textEditor.TextArea.PreviewMouseDown += TextAreaMouseDown; textEditor.TextArea.PreviewMouseUp += TextAreaMouseUp; + textEditor.MouseMove += TextEditorMouseMove; + textEditor.MouseLeave += TextEditorMouseLeave; textEditor.SetBinding(Control.FontFamilyProperty, new Binding { Source = DisplaySettingsPanel.CurrentDisplaySettings, Path = new PropertyPath("SelectedFont") }); textEditor.SetBinding(Control.FontSizeProperty, new Binding { Source = DisplaySettingsPanel.CurrentDisplaySettings, Path = new PropertyPath("SelectedFontSize") }); textEditor.SetBinding(TextEditor.WordWrapProperty, new Binding { Source = DisplaySettingsPanel.CurrentDisplaySettings, Path = new PropertyPath("EnableWordWrap") }); @@ -163,20 +168,18 @@ namespace ICSharpCode.ILSpy.TextView } } } - + #endregion - + #region Tooltip support - ToolTip tooltip; - - void TextViewMouseHoverStopped(object sender, MouseEventArgs e) - { - if (tooltip != null) - tooltip.IsOpen = false; - } + ToolTip toolTip; + Popup popupToolTip; void TextViewMouseHover(object sender, MouseEventArgs e) { + if (!TryCloseExistingPopup(false)) { + return; + } TextViewPosition? position = GetPositionFromMousePosition(); if (position == null) return; @@ -187,35 +190,166 @@ namespace ICSharpCode.ILSpy.TextView if (seg == null) return; object content = GenerateTooltip(seg); - if (tooltip != null) - tooltip.IsOpen = false; - if (content != null) - tooltip = new ToolTip() { Content = content, IsOpen = true }; + + if (content != null) { + popupToolTip = content as Popup; + + if (popupToolTip != null) { + var popupPosition = GetPopupPosition(e); + popupToolTip.Closed += ToolTipClosed; + popupToolTip.HorizontalOffset = popupPosition.X; + popupToolTip.VerticalOffset = popupPosition.Y; + popupToolTip.StaysOpen = true; // We will close it ourselves + + e.Handled = true; + popupToolTip.IsOpen = true; + distanceToPopupLimit = double.PositiveInfinity; // reset limit; we'll re-calculate it on the next mouse movement + } else { + if (toolTip == null) { + toolTip = new ToolTip(); + toolTip.Closed += ToolTipClosed; + } + toolTip.PlacementTarget = this; // required for property inheritance + + if (content is string s) { + toolTip.Content = new TextBlock { + Text = s, + TextWrapping = TextWrapping.Wrap + }; + } else + toolTip.Content = content; + + e.Handled = true; + toolTip.IsOpen = true; + } + } } - + + bool TryCloseExistingPopup(bool mouseClick) + { + if (popupToolTip != null) { + if (popupToolTip.IsOpen && !mouseClick && popupToolTip is FlowDocumentTooltip t && !t.CloseWhenMouseMovesAway) { + return false; // Popup does not want to be closed yet + } + popupToolTip.IsOpen = false; + popupToolTip = null; + } + return true; + } + + /// Returns Popup position based on mouse position, in device independent units + Point GetPopupPosition(MouseEventArgs mouseArgs) + { + Point mousePos = mouseArgs.GetPosition(this); + Point positionInPixels; + // align Popup with line bottom + TextViewPosition? logicalPos = textEditor.GetPositionFromPoint(mousePos); + if (logicalPos.HasValue) { + var textView = textEditor.TextArea.TextView; + positionInPixels = + textView.PointToScreen( + textView.GetVisualPosition(logicalPos.Value, VisualYPosition.LineBottom) - textView.ScrollOffset); + positionInPixels.X -= 4; + } else { + positionInPixels = PointToScreen(mousePos + new Vector(-4, 6)); + } + // use device independent units, because Popup Left/Top are in independent units + return positionInPixels.TransformFromDevice(this); + } + + void TextViewMouseHoverStopped(object sender, MouseEventArgs e) + { + // Non-popup tooltips get closed as soon as the mouse starts moving again + if (toolTip != null) { + toolTip.IsOpen = false; + e.Handled = true; + } + } + + double distanceToPopupLimit; + const double MaxMovementAwayFromPopup = 5; + + void TextEditorMouseMove(object sender, MouseEventArgs e) + { + if (popupToolTip != null) { + double distanceToPopup = GetDistanceToPopup(e); + if (distanceToPopup > distanceToPopupLimit) { + // Close popup if mouse moved away, exceeding the limit + TryCloseExistingPopup(false); + } else { + // reduce distanceToPopupLimit + distanceToPopupLimit = Math.Min(distanceToPopupLimit, distanceToPopup + MaxMovementAwayFromPopup); + } + } + } + + double GetDistanceToPopup(MouseEventArgs e) + { + Point p = popupToolTip.Child.PointFromScreen(PointToScreen(e.GetPosition(this))); + Size size = popupToolTip.Child.RenderSize; + double x = 0; + if (p.X < 0) + x = -p.X; + else if (p.X > size.Width) + x = p.X - size.Width; + double y = 0; + if (p.Y < 0) + y = -p.Y; + else if (p.Y > size.Height) + y = p.Y - size.Height; + return Math.Sqrt(x * x + y * y); + } + + void TextEditorMouseLeave(object sender, MouseEventArgs e) + { + if (popupToolTip != null && !popupToolTip.IsMouseOver) { + // do not close popup if mouse moved from editor to popup + TryCloseExistingPopup(false); + } + } + + void OnUnloaded(object sender, EventArgs e) + { + // Close popup when another document gets selected + // TextEditorMouseLeave is not sufficient for this because the mouse might be over the popup when the document switch happens (e.g. Ctrl+Tab) + TryCloseExistingPopup(true); + } + + void ToolTipClosed(object sender, EventArgs e) + { + if (toolTip == sender) { + toolTip = null; + } + if (popupToolTip == sender) { + // Because popupToolTip instances are created by the tooltip provider, + // they might be reused; so we should detach the event handler + popupToolTip.Closed -= ToolTipClosed; + popupToolTip = null; + } + } + object GenerateTooltip(ReferenceSegment segment) { if (segment.Reference is ICSharpCode.Decompiler.Disassembler.OpCodeInfo code) { XmlDocumentationProvider docProvider = XmlDocLoader.MscorlibDocumentation; - if (docProvider != null){ + DocumentationUIBuilder renderer = new DocumentationUIBuilder(new CSharpAmbience(), MainWindow.Instance.CurrentLanguage.SyntaxHighlighting); + renderer.AddSignatureBlock($"{code.Name} (0x{code.Code:x})"); + if (docProvider != null) { string documentation = docProvider.GetDocumentation("F:System.Reflection.Emit.OpCodes." + code.EncodedName); if (documentation != null) { - XmlDocRenderer renderer = new XmlDocRenderer(); - renderer.AppendText($"{code.Name} (0x{code.Code:x}) - "); renderer.AddXmlDocumentation(documentation); - return renderer.CreateTextBlock(); } } - return $"{code.Name} (0x{code.Code:x})"; + return new FlowDocumentTooltip(renderer.CreateDocument()); } else if (segment.Reference is IEntity entity) { - return CreateTextBlockForEntity(entity); + return new FlowDocumentTooltip(CreateTooltipForEntity(entity)); } else if (segment.Reference is ValueTuple unresolvedEntity) { var typeSystem = new DecompilerTypeSystem(unresolvedEntity.Item1, unresolvedEntity.Item1.GetAssemblyResolver(), TypeSystemOptions.Default | TypeSystemOptions.Uncached); try { IEntity resolved = typeSystem.MainModule.ResolveEntity(unresolvedEntity.Item2); if (resolved == null) return null; - return CreateTextBlockForEntity(resolved); + return new FlowDocumentTooltip(CreateTooltipForEntity(resolved)); } catch (BadImageFormatException) { return null; } @@ -223,10 +357,11 @@ namespace ICSharpCode.ILSpy.TextView return null; } - static TextBlock CreateTextBlockForEntity(IEntity resolved) + static FlowDocument CreateTooltipForEntity(IEntity resolved) { - XmlDocRenderer renderer = new XmlDocRenderer(); - renderer.AppendText(MainWindow.Instance.CurrentLanguage.GetTooltip(resolved)); + Language currentLanguage = MainWindow.Instance.CurrentLanguage; + DocumentationUIBuilder renderer = new DocumentationUIBuilder(new CSharpAmbience(), currentLanguage.SyntaxHighlighting); + renderer.AddSignatureBlock(currentLanguage.GetTooltip(resolved)); try { if (resolved.ParentModule == null || resolved.ParentModule.PEFile == null) return null; @@ -234,14 +369,57 @@ namespace ICSharpCode.ILSpy.TextView if (docProvider != null) { string documentation = docProvider.GetDocumentation(resolved.GetIdString()); if (documentation != null) { - renderer.AppendText(Environment.NewLine); + //renderer.AppendText(Environment.NewLine); renderer.AddXmlDocumentation(documentation); } } } catch (XmlException) { // ignore } - return renderer.CreateTextBlock(); + return renderer.CreateDocument(); + } + + sealed class FlowDocumentTooltip : Popup + { + readonly FlowDocumentScrollViewer viewer; + + public FlowDocumentTooltip(FlowDocument document) + { + TextOptions.SetTextFormattingMode(this, TextFormattingMode.Display); + viewer = new FlowDocumentScrollViewer(); + viewer.Document = document; + Border border = new Border { + Background = SystemColors.InfoBrush, + BorderBrush = SystemColors.InfoTextBrush, + BorderThickness = new Thickness(1), + MaxHeight = 400, + Child = viewer + }; + this.Child = border; + viewer.Foreground = SystemColors.InfoTextBrush; + document.FontSize = DisplaySettingsPanel.CurrentDisplaySettings.SelectedFontSize; + } + + public bool CloseWhenMouseMovesAway { + get { return !this.IsKeyboardFocusWithin; } + } + + protected override void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e) + { + base.OnLostKeyboardFocus(e); + this.IsOpen = false; + } + + protected override void OnMouseLeave(MouseEventArgs e) + { + base.OnMouseLeave(e); + // When the mouse is over the popup, it is possible for ILSpy to be minimized, + // or moved into the background, and yet the popup stays open. + // We don't have a good method here to check whether the mouse moved back into the text area + // or somewhere else, so we'll just close the popup. + if (CloseWhenMouseMovesAway) + this.IsOpen = false; + } } #endregion @@ -321,7 +499,7 @@ namespace ICSharpCode.ILSpy.TextView return tcs.Task; } - void cancelButton_Click(object sender, RoutedEventArgs e) + void CancelButton_Click(object sender, RoutedEventArgs e) { if (currentCancellationTokenSource != null) { currentCancellationTokenSource.Cancel(); diff --git a/ILSpy/TextView/DecompilerTextView.xaml b/ILSpy/TextView/DecompilerTextView.xaml index bc466519e..eed20cda4 100644 --- a/ILSpy/TextView/DecompilerTextView.xaml +++ b/ILSpy/TextView/DecompilerTextView.xaml @@ -21,7 +21,7 @@ -