mirror of https://github.com/icsharpcode/ILSpy.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
432 lines
14 KiB
432 lines
14 KiB
// Copyright (c) 2010-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.IO; |
|
using System.Runtime.Serialization; |
|
using System.Text; |
|
using System.Xml; |
|
using ICSharpCode.Decompiler.TypeSystem; |
|
using ICSharpCode.Decompiler.Util; |
|
|
|
namespace ICSharpCode.Decompiler.Documentation |
|
{ |
|
/// <summary> |
|
/// Provides XML documentation for type and member definitions in source code. |
|
/// </summary> |
|
public interface IDocumentationProvider |
|
{ |
|
/// <summary> |
|
/// Returns the XML documentation for the given <paramref name="entity"/>. |
|
/// May return null, if no documentation is present for the entity. |
|
/// </summary> |
|
/// <exception cref="ArgumentNullException"><paramref name="entity"/> is null.</exception> |
|
string GetDocumentation(IEntity entity); |
|
} |
|
|
|
/// <summary> |
|
/// Provides documentation from an .xml file (as generated by the Microsoft C# compiler). |
|
/// </summary> |
|
/// <remarks> |
|
/// This class first creates an in-memory index of the .xml file, and then uses that to read only the requested members. |
|
/// This way, we avoid keeping all the documentation in memory. |
|
/// The .xml file is only opened when necessary, the file handle is not kept open all the time. |
|
/// If the .xml file is changed, the index will automatically be recreated. |
|
/// </remarks> |
|
[Serializable] |
|
public class XmlDocumentationProvider : IDeserializationCallback, IDocumentationProvider |
|
{ |
|
#region Cache |
|
sealed class XmlDocumentationCache |
|
{ |
|
readonly KeyValuePair<string, string>[] entries; |
|
int pos; |
|
|
|
public XmlDocumentationCache(int size = 50) |
|
{ |
|
if (size <= 0) |
|
throw new ArgumentOutOfRangeException(nameof(size), size, "Value must be positive"); |
|
this.entries = new KeyValuePair<string, string>[size]; |
|
} |
|
|
|
internal bool TryGet(string key, out string value) |
|
{ |
|
foreach (var pair in entries) { |
|
if (pair.Key == key) { |
|
value = pair.Value; |
|
return true; |
|
} |
|
} |
|
value = null; |
|
return false; |
|
} |
|
|
|
internal void Add(string key, string value) |
|
{ |
|
entries[pos++] = new KeyValuePair<string, string>(key, value); |
|
if (pos == entries.Length) |
|
pos = 0; |
|
} |
|
} |
|
#endregion |
|
|
|
[Serializable] |
|
struct IndexEntry : IComparable<IndexEntry> |
|
{ |
|
/// <summary> |
|
/// Hash code of the documentation tag |
|
/// </summary> |
|
internal readonly int HashCode; |
|
|
|
/// <summary> |
|
/// Position in the .xml file where the documentation starts |
|
/// </summary> |
|
internal readonly int PositionInFile; |
|
|
|
internal IndexEntry(int hashCode, int positionInFile) |
|
{ |
|
this.HashCode = hashCode; |
|
this.PositionInFile = positionInFile; |
|
} |
|
|
|
public int CompareTo(IndexEntry other) |
|
{ |
|
return this.HashCode.CompareTo(other.HashCode); |
|
} |
|
} |
|
|
|
[NonSerialized] |
|
XmlDocumentationCache cache = new XmlDocumentationCache(); |
|
|
|
readonly string fileName; |
|
readonly Encoding encoding; |
|
volatile IndexEntry[] index; // SORTED array of index entries |
|
|
|
#region Constructor / Redirection support |
|
/// <summary> |
|
/// Creates a new XmlDocumentationProvider. |
|
/// </summary> |
|
/// <param name="fileName">Name of the .xml file.</param> |
|
/// <exception cref="IOException">Error reading from XML file (or from redirected file)</exception> |
|
/// <exception cref="XmlException">Invalid XML file</exception> |
|
public XmlDocumentationProvider(string fileName) |
|
{ |
|
if (fileName == null) |
|
throw new ArgumentNullException(nameof(fileName)); |
|
|
|
using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete)) { |
|
using (XmlTextReader xmlReader = new XmlTextReader(fs)) { |
|
xmlReader.XmlResolver = null; // no DTD resolving |
|
xmlReader.MoveToContent(); |
|
if (string.IsNullOrEmpty(xmlReader.GetAttribute("redirect"))) { |
|
this.fileName = fileName; |
|
this.encoding = xmlReader.Encoding; |
|
ReadXmlDoc(xmlReader); |
|
} else { |
|
string redirectionTarget = GetRedirectionTarget(fileName, xmlReader.GetAttribute("redirect")); |
|
if (redirectionTarget != null) { |
|
Debug.WriteLine("XmlDoc " + fileName + " is redirecting to " + redirectionTarget); |
|
using (FileStream redirectedFs = new FileStream(redirectionTarget, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete)) { |
|
using (XmlTextReader redirectedXmlReader = new XmlTextReader(redirectedFs)) { |
|
redirectedXmlReader.XmlResolver = null; // no DTD resolving |
|
redirectedXmlReader.MoveToContent(); |
|
this.fileName = redirectionTarget; |
|
this.encoding = redirectedXmlReader.Encoding; |
|
ReadXmlDoc(redirectedXmlReader); |
|
} |
|
} |
|
} else { |
|
throw new XmlException("XmlDoc " + fileName + " is redirecting to " + xmlReader.GetAttribute("redirect") + ", but that file was not found."); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
static string GetRedirectionTarget(string xmlFileName, string target) |
|
{ |
|
string programFilesDir = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); |
|
programFilesDir = AppendDirectorySeparator(programFilesDir); |
|
|
|
string corSysDir = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory(); |
|
corSysDir = AppendDirectorySeparator(corSysDir); |
|
|
|
var fileName = target.Replace ("%PROGRAMFILESDIR%", programFilesDir) |
|
.Replace ("%CORSYSDIR%", corSysDir); |
|
if (!Path.IsPathRooted (fileName)) |
|
fileName = Path.Combine (Path.GetDirectoryName (xmlFileName), fileName); |
|
return LookupLocalizedXmlDoc(fileName); |
|
} |
|
|
|
static string AppendDirectorySeparator(string dir) |
|
{ |
|
if (dir.EndsWith("\\", StringComparison.Ordinal) || dir.EndsWith("/", StringComparison.Ordinal)) |
|
return dir; |
|
else |
|
return dir + Path.DirectorySeparatorChar; |
|
} |
|
|
|
/// <summary> |
|
/// Given the assembly file name, looks up the XML documentation file name. |
|
/// Returns null if no XML documentation file is found. |
|
/// </summary> |
|
public static string LookupLocalizedXmlDoc(string fileName) |
|
{ |
|
string xmlFileName = Path.ChangeExtension(fileName, ".xml"); |
|
string currentCulture = System.Threading.Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName; |
|
string localizedXmlDocFile = GetLocalizedName(xmlFileName, currentCulture); |
|
|
|
Debug.WriteLine("Try find XMLDoc @" + localizedXmlDocFile); |
|
if (File.Exists(localizedXmlDocFile)) { |
|
return localizedXmlDocFile; |
|
} |
|
Debug.WriteLine("Try find XMLDoc @" + xmlFileName); |
|
if (File.Exists(xmlFileName)) { |
|
return xmlFileName; |
|
} |
|
if (currentCulture != "en") { |
|
string englishXmlDocFile = GetLocalizedName(xmlFileName, "en"); |
|
Debug.WriteLine("Try find XMLDoc @" + englishXmlDocFile); |
|
if (File.Exists(englishXmlDocFile)) { |
|
return englishXmlDocFile; |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
static string GetLocalizedName(string fileName, string language) |
|
{ |
|
string localizedXmlDocFile = Path.GetDirectoryName(fileName); |
|
localizedXmlDocFile = Path.Combine(localizedXmlDocFile, language); |
|
localizedXmlDocFile = Path.Combine(localizedXmlDocFile, Path.GetFileName(fileName)); |
|
return localizedXmlDocFile; |
|
} |
|
#endregion |
|
|
|
#region Load / Create Index |
|
void ReadXmlDoc(XmlTextReader reader) |
|
{ |
|
//lastWriteDate = File.GetLastWriteTimeUtc(fileName); |
|
// Open up a second file stream for the line<->position mapping |
|
using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete)) { |
|
LinePositionMapper linePosMapper = new LinePositionMapper(fs, encoding); |
|
List<IndexEntry> indexList = new List<IndexEntry>(); |
|
while (reader.Read()) { |
|
if (reader.IsStartElement()) { |
|
switch (reader.LocalName) { |
|
case "members": |
|
ReadMembersSection(reader, linePosMapper, indexList); |
|
break; |
|
} |
|
} |
|
} |
|
indexList.Sort(); |
|
this.index = indexList.ToArray(); // volatile write |
|
} |
|
} |
|
|
|
sealed class LinePositionMapper |
|
{ |
|
readonly FileStream fs; |
|
readonly Decoder decoder; |
|
int currentLine = 1; |
|
char prevChar = '\0'; |
|
|
|
// buffers for use with Decoder: |
|
byte[] input = new byte[1]; |
|
char[] output = new char[1]; |
|
|
|
public LinePositionMapper(FileStream fs, Encoding encoding) |
|
{ |
|
this.decoder = encoding.GetDecoder(); |
|
this.fs = fs; |
|
} |
|
|
|
public int GetPositionForLine(int line) |
|
{ |
|
Debug.Assert(line >= currentLine); |
|
while (line > currentLine) { |
|
int b = fs.ReadByte(); |
|
if (b < 0) |
|
throw new EndOfStreamException(); |
|
input[0] = (byte)b; |
|
decoder.Convert(input, 0, 1, output, 0, 1, false, out int bytesUsed, out int charsUsed, out _); |
|
Debug.Assert(bytesUsed == 1); |
|
if (charsUsed == 1) { |
|
if ((prevChar != '\r' && output[0] == '\n') || output[0] == '\r') |
|
currentLine++; |
|
prevChar = output[0]; |
|
} |
|
} |
|
return checked((int)fs.Position); |
|
} |
|
} |
|
|
|
static void ReadMembersSection(XmlTextReader reader, LinePositionMapper linePosMapper, List<IndexEntry> indexList) |
|
{ |
|
while (reader.Read()) { |
|
switch (reader.NodeType) { |
|
case XmlNodeType.EndElement: |
|
if (reader.LocalName == "members") { |
|
return; |
|
} |
|
break; |
|
case XmlNodeType.Element: |
|
if (reader.LocalName == "member") { |
|
int pos = linePosMapper.GetPositionForLine(reader.LineNumber) + Math.Max(reader.LinePosition - 2, 0); |
|
string memberAttr = reader.GetAttribute("name"); |
|
if (memberAttr != null) |
|
indexList.Add(new IndexEntry(GetHashCode(memberAttr), pos)); |
|
reader.Skip(); |
|
} |
|
break; |
|
} |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Hash algorithm used for the index. |
|
/// This is a custom implementation so that old index files work correctly |
|
/// even when the .NET string.GetHashCode implementation changes |
|
/// (e.g. due to .NET 4.5 hash randomization) |
|
/// </summary> |
|
static int GetHashCode(string key) |
|
{ |
|
unchecked { |
|
int h = 0; |
|
foreach (char c in key) { |
|
h = (h << 5) - h + c; |
|
} |
|
return h; |
|
} |
|
} |
|
#endregion |
|
|
|
#region GetDocumentation |
|
/// <summary> |
|
/// Get the documentation for the member with the specified documentation key. |
|
/// </summary> |
|
public string GetDocumentation(string key) |
|
{ |
|
if (key == null) |
|
throw new ArgumentNullException(nameof(key)); |
|
return GetDocumentation(key, true); |
|
} |
|
|
|
/// <summary> |
|
/// Get the documentation for the specified member. |
|
/// </summary> |
|
public string GetDocumentation(IEntity entity) |
|
{ |
|
if (entity == null) |
|
throw new ArgumentNullException(nameof(entity)); |
|
return GetDocumentation(entity.GetIdString()); |
|
} |
|
|
|
string GetDocumentation(string key, bool allowReload) |
|
{ |
|
int hashcode = GetHashCode(key); |
|
var index = this.index; // read volatile field |
|
// index is sorted, so we can use binary search |
|
int m = Array.BinarySearch(index, new IndexEntry(hashcode, 0)); |
|
if (m < 0) |
|
return null; |
|
// correct hash code found. |
|
// possibly there are multiple items with the same hash, so go to the first. |
|
while (--m >= 0 && index[m].HashCode == hashcode); |
|
// m is now 1 before the first item with the correct hash |
|
|
|
XmlDocumentationCache cache = this.cache; |
|
lock (cache) { |
|
if (!cache.TryGet(key, out string val)) { |
|
try { |
|
// go through all items that have the correct hash |
|
while (++m < index.Length && index[m].HashCode == hashcode) { |
|
val = LoadDocumentation(key, index[m].PositionInFile); |
|
if (val != null) |
|
break; |
|
} |
|
// cache the result (even if it is null) |
|
cache.Add(key, val); |
|
} catch (IOException) { |
|
// may happen if the documentation file was deleted/is inaccessible/changed (EndOfStreamException) |
|
return allowReload ? ReloadAndGetDocumentation(key) : null; |
|
} catch (XmlException) { |
|
// may happen if the documentation file was changed so that the file position no longer starts on a valid XML element |
|
return allowReload ? ReloadAndGetDocumentation(key) : null; |
|
} |
|
} |
|
return val; |
|
} |
|
} |
|
|
|
string ReloadAndGetDocumentation(string key) |
|
{ |
|
try { |
|
// Reload the index |
|
using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete)) { |
|
using (XmlTextReader xmlReader = new XmlTextReader(fs)) { |
|
xmlReader.XmlResolver = null; // no DTD resolving |
|
xmlReader.MoveToContent(); |
|
ReadXmlDoc(xmlReader); |
|
} |
|
} |
|
} catch (IOException) { |
|
// Ignore errors on reload; IEntity.Documentation callers aren't prepared to handle exceptions |
|
this.index = Empty<IndexEntry>.Array; // clear index to avoid future load attempts |
|
return null; |
|
} catch (XmlException) { |
|
this.index = Empty<IndexEntry>.Array; // clear index to avoid future load attempts |
|
return null; |
|
} |
|
return GetDocumentation(key, allowReload: false); // prevent infinite reload loops |
|
} |
|
#endregion |
|
|
|
#region Load / Read XML |
|
string LoadDocumentation(string key, int positionInFile) |
|
{ |
|
using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete)) { |
|
fs.Position = positionInFile; |
|
var context = new XmlParserContext(null, null, null, XmlSpace.None) { Encoding = encoding }; |
|
using (XmlTextReader r = new XmlTextReader(fs, XmlNodeType.Element, context)) { |
|
r.XmlResolver = null; // no DTD resolving |
|
while (r.Read()) { |
|
if (r.NodeType == XmlNodeType.Element) { |
|
string memberAttr = r.GetAttribute("name"); |
|
if (memberAttr == key) { |
|
return r.ReadInnerXml(); |
|
} else { |
|
return null; |
|
} |
|
} |
|
} |
|
return null; |
|
} |
|
} |
|
} |
|
#endregion |
|
|
|
public virtual void OnDeserialization(object sender) |
|
{ |
|
cache = new XmlDocumentationCache(); |
|
} |
|
} |
|
}
|
|
|