1
// Copyright (c) AlphaSierraPapa for the SharpDevelop Team
3
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
4
// software and associated documentation files (the "Software"), to deal in the Software
5
// without restriction, including without limitation the rights to use, copy, modify, merge,
6
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
7
// to whom the Software is furnished to do so, subject to the following conditions:
9
// The above copyright notice and this permission notice shall be included in all copies or
10
// substantial portions of the Software.
12
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
13
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
14
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
15
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
17
// DEALINGS IN THE SOFTWARE.
20
using System.Collections.Generic;
21
using System.Diagnostics;
23
using System.Runtime.Serialization;
25
using ICSharpCode.NRefactory.Editor;
26
using ICSharpCode.NRefactory.TypeSystem;
27
using ICSharpCode.NRefactory.TypeSystem.Implementation;
29
namespace ICSharpCode.NRefactory.Documentation
32
/// Provides documentation from an .xml file (as generated by the Microsoft C# compiler).
35
/// This class first creates an in-memory index of the .xml file, and then uses that to read only the requested members.
36
/// This way, we avoid keeping all the documentation in memory.
37
/// The .xml file is only opened when necessary, the file handle is not kept open all the time.
38
/// If the .xml file is changed, the index will automatically be recreated.
41
public class XmlDocumentationProvider : IDocumentationProvider, IDeserializationCallback
44
sealed class XmlDocumentationCache
46
readonly KeyValuePair<string, string>[] entries;
49
public XmlDocumentationCache(int size = 50)
52
throw new ArgumentOutOfRangeException("size", size, "Value must be positive");
53
this.entries = new KeyValuePair<string, string>[size];
56
internal string Get(string key)
58
foreach (var pair in entries) {
65
internal void Add(string key, string value)
67
entries[pos++] = new KeyValuePair<string, string>(key, value);
68
if (pos == entries.Length)
75
struct IndexEntry : IComparable<IndexEntry>
78
/// Hash code of the documentation tag
80
internal readonly int HashCode;
83
/// Position in the .xml file where the documentation starts
85
internal readonly int PositionInFile;
87
internal IndexEntry(int hashCode, int positionInFile)
89
this.HashCode = hashCode;
90
this.PositionInFile = positionInFile;
93
public int CompareTo(IndexEntry other)
95
return this.HashCode.CompareTo(other.HashCode);
100
XmlDocumentationCache cache = new XmlDocumentationCache();
102
readonly string fileName;
103
DateTime lastWriteDate;
104
IndexEntry[] index; // SORTED array of index entries
106
#region Constructor / Redirection support
108
/// Creates a new XmlDocumentationProvider.
110
/// <param name="fileName">Name of the .xml file.</param>
111
public XmlDocumentationProvider(string fileName)
113
if (fileName == null)
114
throw new ArgumentNullException("fileName");
116
using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete)) {
117
using (XmlTextReader xmlReader = new XmlTextReader(fs)) {
118
xmlReader.XmlResolver = null; // no DTD resolving
119
xmlReader.MoveToContent();
120
if (string.IsNullOrEmpty(xmlReader.GetAttribute("redirect"))) {
121
this.fileName = fileName;
122
ReadXmlDoc(xmlReader);
124
string redirectionTarget = GetRedirectionTarget(xmlReader.GetAttribute("redirect"));
125
if (redirectionTarget != null) {
126
Debug.WriteLine("XmlDoc " + fileName + " is redirecting to " + redirectionTarget);
127
using (FileStream redirectedFs = new FileStream(redirectionTarget, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete)) {
128
using (XmlTextReader redirectedXmlReader = new XmlTextReader(redirectedFs)) {
129
this.fileName = redirectionTarget;
130
ReadXmlDoc(redirectedXmlReader);
134
throw new XmlException("XmlDoc " + fileName + " is redirecting to " + xmlReader.GetAttribute("redirect") + ", but that file was not found.");
141
// ProgramFilesX86 is broken on 32-bit WinXP, this is a workaround
142
static string GetProgramFilesX86()
144
return Environment.GetFolderPath(IntPtr.Size == 8?
145
Environment.SpecialFolder.ProgramFilesX86 : Environment.SpecialFolder.ProgramFiles);
148
static string GetRedirectionTarget(string target)
150
string programFilesDir = AppendDirectorySeparator(GetProgramFilesX86());
152
string corSysDir = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory();
153
corSysDir = AppendDirectorySeparator(corSysDir);
155
return LookupLocalizedXmlDoc(target.Replace("%PROGRAMFILESDIR%", programFilesDir)
156
.Replace("%CORSYSDIR%", corSysDir));
159
static string AppendDirectorySeparator(string dir)
161
if (dir.EndsWith("\\", StringComparison.Ordinal) || dir.EndsWith("/", StringComparison.Ordinal))
164
return dir + Path.DirectorySeparatorChar;
168
/// Given the assembly file name, looks up the XML documentation file name.
169
/// Returns null if no XML documentation file is found.
171
public static string LookupLocalizedXmlDoc(string fileName)
173
string xmlFileName = Path.ChangeExtension(fileName, ".xml");
174
string currentCulture = System.Threading.Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName;
175
string localizedXmlDocFile = GetLocalizedName(xmlFileName, currentCulture);
177
Debug.WriteLine("Try find XMLDoc @" + localizedXmlDocFile);
178
if (File.Exists(localizedXmlDocFile)) {
179
return localizedXmlDocFile;
181
Debug.WriteLine("Try find XMLDoc @" + xmlFileName);
182
if (File.Exists(xmlFileName)) {
185
if (currentCulture != "en") {
186
string englishXmlDocFile = GetLocalizedName(xmlFileName, "en");
187
Debug.WriteLine("Try find XMLDoc @" + englishXmlDocFile);
188
if (File.Exists(englishXmlDocFile)) {
189
return englishXmlDocFile;
195
static string GetLocalizedName(string fileName, string language)
197
string localizedXmlDocFile = Path.GetDirectoryName(fileName);
198
localizedXmlDocFile = Path.Combine(localizedXmlDocFile, language);
199
localizedXmlDocFile = Path.Combine(localizedXmlDocFile, Path.GetFileName(fileName));
200
return localizedXmlDocFile;
204
#region Load / Create Index
205
void ReadXmlDoc(XmlTextReader reader)
207
lastWriteDate = File.GetLastWriteTimeUtc(fileName);
208
// Open up a second file stream for the line<->position mapping
209
using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete)) {
210
LinePositionMapper linePosMapper = new LinePositionMapper(fs);
211
List<IndexEntry> indexList = new List<IndexEntry>();
212
while (reader.Read()) {
213
if (reader.IsStartElement()) {
214
switch (reader.LocalName) {
216
ReadMembersSection(reader, linePosMapper, indexList);
222
this.index = indexList.ToArray();
226
sealed class LinePositionMapper
228
readonly FileStream fs;
231
public LinePositionMapper(FileStream fs)
236
public int GetPositionForLine(int line)
238
Debug.Assert(line >= currentLine);
239
while (line > currentLine) {
240
int b = fs.ReadByte();
242
throw new EndOfStreamException();
247
return checked((int)fs.Position);
251
static void ReadMembersSection(XmlTextReader reader, LinePositionMapper linePosMapper, List<IndexEntry> indexList)
253
while (reader.Read()) {
254
switch (reader.NodeType) {
255
case XmlNodeType.EndElement:
256
if (reader.LocalName == "members") {
260
case XmlNodeType.Element:
261
if (reader.LocalName == "member") {
262
int pos = linePosMapper.GetPositionForLine(reader.LineNumber) + Math.Max(reader.LinePosition - 2, 0);
263
string memberAttr = reader.GetAttribute("name");
264
if (memberAttr != null)
265
indexList.Add(new IndexEntry(memberAttr.GetHashCode(), pos));
274
#region GetDocumentation
276
/// Get the documentation for the member with the specified documentation key.
278
public string GetDocumentation(string key)
281
throw new ArgumentNullException("key");
283
int hashcode = key.GetHashCode();
284
// index is sorted, so we can use binary search
285
int m = Array.BinarySearch(index, new IndexEntry(hashcode, 0));
288
// correct hash code found.
289
// possibly there are multiple items with the same hash, so go to the first.
290
while (--m >= 0 && index[m].HashCode == hashcode);
291
// m is now 1 before the first item with the correct hash
293
XmlDocumentationCache cache = this.cache;
295
string val = cache.Get(key);
297
// go through all items that have the correct hash
298
while (++m < index.Length && index[m].HashCode == hashcode) {
299
val = LoadDocumentation(key, index[m].PositionInFile);
303
// cache the result (even if it is null)
311
#region GetDocumentation for entity
313
public DocumentationComment GetDocumentation(IEntity entity)
315
string xmlDoc = GetDocumentation(IdStringProvider.GetIdString(entity));
316
if (xmlDoc != null) {
317
return new DocumentationComment(new StringTextSource(xmlDoc), new SimpleTypeResolveContext(entity));
324
#region Load / Read XML
325
string LoadDocumentation(string key, int positionInFile)
327
using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete)) {
328
fs.Position = positionInFile;
329
using (XmlTextReader r = new XmlTextReader(fs, XmlNodeType.Element, null)) {
330
r.XmlResolver = null; // no DTD resolving
332
if (r.NodeType == XmlNodeType.Element) {
333
string memberAttr = r.GetAttribute("name");
334
if (memberAttr == key) {
335
return r.ReadInnerXml();
347
public virtual void OnDeserialization(object sender)
349
cache = new XmlDocumentationCache();