Jo Shields a575963da9 Imported Upstream version 3.6.0
Former-commit-id: da6be194a6b1221998fc28233f2503bd61dd9d14
2014-08-13 10:39:27 +01:00

586 lines
19 KiB
C#

using System;
using System.Linq;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using System.Collections.Generic;
using Monodoc.Ecma;
namespace Monodoc.Providers
{
public enum EcmaNodeType {
Invalid,
Namespace,
Type,
Member,
Meta, // A node that's here to serve as a header for other node
}
// Common functionality between ecma-provider and ecmauncompiled-provider
internal class EcmaDoc
{
static EcmaUrlParser parser = new EcmaUrlParser ();
public static void PopulateTreeFromIndexFile (string indexFilePath,
string idPrefix,
Tree tree,
IDocStorage storage,
Dictionary<string, XElement> nsSummaries,
Func<XElement, string> indexGenerator = null)
{
var root = tree.RootNode;
int resID = 0;
var asm = Path.GetDirectoryName (indexFilePath);
storage = storage ?? new Storage.NullStorage ();
// nsSummaries is allowed to be null if the user doesn't care about it
nsSummaries = nsSummaries ?? new Dictionary<string, XElement> ();
// default index generator uses a counter
indexGenerator = indexGenerator ?? (_ => resID++.ToString ());
using (var reader = XmlReader.Create (File.OpenRead (indexFilePath))) {
reader.ReadToFollowing ("Types");
var types = XElement.Load (reader.ReadSubtree ());
foreach (var ns in types.Elements ("Namespace")) {
var nsName = (string)ns.Attribute ("Name");
nsName = !string.IsNullOrEmpty (nsName) ? nsName : "global";
var nsNode = root.GetOrCreateNode (nsName, "N:" + nsName);
XElement nsElements;
if (!nsSummaries.TryGetValue (nsName, out nsElements))
nsSummaries[nsName] = nsElements = new XElement ("elements",
new XElement ("summary"),
new XElement ("remarks"));
//Add namespace summary and remarks data from file, if available
var nsFileName = Path.Combine(asm, String.Format("ns-{0}.xml", nsName));
if(File.Exists(nsFileName)){
var nsEl = XElement.Load (nsFileName);
nsElements.Element ("summary").ReplaceWith (nsEl.Descendants ("summary").First ());
nsElements.Element ("remarks").ReplaceWith (nsEl.Descendants ("remarks").First ());
}else{
Console.WriteLine ("Error reading namespace XML for " + nsName);
}
foreach (var type in ns.Elements ("Type")) {
// Add the XML file corresponding to the type to our storage
var id = indexGenerator (type);
string typeFilePath;
var typeDocument = EcmaDoc.LoadTypeDocument (asm, nsName, type.Attribute ("Name").Value, out typeFilePath);
if (typeDocument == null)
continue;
using (var file = File.OpenRead (typeFilePath))
storage.Store (id, file);
nsElements.Add (ExtractClassSummary (typeFilePath));
var typeCaption = EcmaDoc.GetTypeCaptionFromIndex (type);
var url = idPrefix + id + '#' + typeCaption + '/';
typeCaption = EcmaDoc.GetTypeCaptionFromIndex (type, true);
var typeNode = nsNode.CreateNode (typeCaption, url);
// Add meta "Members" node
typeNode.CreateNode ("Members", "*");
var membersNode = typeDocument.Root.Element ("Members");
if (membersNode == null || !membersNode.Elements ().Any ())
continue;
var members = membersNode
.Elements ("Member")
.ToLookup (EcmaDoc.GetMemberType);
foreach (var memberType in members) {
// We pluralize the member type to get the caption and take the first letter as URL
var node = typeNode.CreateNode (EcmaDoc.PluralizeMemberType (memberType.Key), memberType.Key[0].ToString ());
var memberIndex = 0;
var isCtors = memberType.Key[0] == 'C';
// We do not escape much member name here
foreach (var memberGroup in memberType.GroupBy (m => MakeMemberCaption (m, isCtors))) {
if (memberGroup.Count () > 1) {
// Generate overload
var overloadCaption = MakeMemberCaption (memberGroup.First (), false);
var overloadNode = node.CreateNode (overloadCaption, overloadCaption);
foreach (var member in memberGroup)
overloadNode.CreateNode (MakeMemberCaption (member, true), (memberIndex++).ToString ());
overloadNode.Sort ();
} else {
// We treat constructor differently by showing their argument list in all cases
node.CreateNode (MakeMemberCaption (memberGroup.First (), isCtors), (memberIndex++).ToString ());
}
}
node.Sort ();
}
}
nsNode.Sort ();
}
root.Sort ();
}
}
// Utility methods
public static XDocument LoadTypeDocument (string basePath, string nsName, string typeName)
{
string dummy;
return LoadTypeDocument (basePath, nsName, typeName, out dummy);
}
public static XDocument LoadTypeDocument (string basePath, string nsName, string typeName, out string finalPath)
{
finalPath = Path.Combine (basePath, nsName, Path.ChangeExtension (typeName, ".xml"));
if (!File.Exists (finalPath)) {
Console.Error.WriteLine ("Warning: couldn't process type file `{0}' as it doesn't exist", finalPath);
return null;
}
XDocument doc = null;
try {
doc = XDocument.Load (finalPath);
} catch (Exception e) {
Console.WriteLine ("Document `{0}' is unparsable, {1}", finalPath, e.ToString ());
}
return doc;
}
public static string GetTypeCaptionFromIndex (XElement typeNodeFromIndex, bool full = false)
{
var t = typeNodeFromIndex;
var c = ((string)(t.Attribute ("DisplayName") ?? t.Attribute ("Name"))).Replace ('+', '.');
if (full)
c += " " + (string)t.Attribute ("Kind");
return c;
}
public static string PluralizeMemberType (string memberType)
{
switch (memberType) {
case "Property":
return "Properties";
default:
return memberType + "s";
}
}
public static string GetMemberType (XElement m)
{
return m.Attribute ("MemberName").Value.StartsWith ("op_") ? "Operator" : m.Element ("MemberType").Value;
}
public static string MakeMemberCaption (XElement member, bool withArguments)
{
var caption = (string)member.Attribute ("MemberName");
// Use type name instead of .ctor for cosmetic sake
if (caption == ".ctor") {
caption = (string)member.Ancestors ("Type").First ().Attribute ("Name");
// If this is an inner type ctor, strip the parent type reference
var plusIndex = caption.LastIndexOf ('+');
if (plusIndex != -1)
caption = caption.Substring (plusIndex + 1);
}
if (caption.StartsWith ("op_")) {
string sig;
caption = MakeOperatorSignature (member, out sig);
caption = withArguments ? sig : caption;
return caption;
}
if (withArguments) {
var args = member.Element ("Parameters");
caption += '(';
if (args != null && args.Elements ("Parameter").Any ()) {
caption += args.Elements ("Parameter")
.Select (p => (string)p.Attribute ("Type"))
.Aggregate ((p1, p2) => p1 + "," + p2);
}
caption += ')';
}
return caption;
}
public static Node MatchNodeWithEcmaUrl (string url, Tree tree)
{
Node result = null;
EcmaDesc desc;
if (!parser.TryParse (url, out desc))
return null;
// Namespace search
Node currentNode = tree.RootNode;
Node searchNode = new Node () { Caption = desc.Namespace };
int index = currentNode.ChildNodes.BinarySearch (searchNode, EcmaGenericNodeComparer.Instance);
if (index >= 0)
result = currentNode.ChildNodes[index];
if (desc.DescKind == EcmaDesc.Kind.Namespace || index < 0)
return result;
// Type search
currentNode = result;
result = null;
searchNode.Caption = desc.ToCompleteTypeName ();
if (!desc.GenericTypeArgumentsIsNumeric)
index = currentNode.ChildNodes.BinarySearch (searchNode, EcmaTypeNodeComparer.Instance);
else
index = GenericTypeBacktickSearch (currentNode.ChildNodes, desc);
if (index >= 0)
result = currentNode.ChildNodes[index];
if ((desc.DescKind == EcmaDesc.Kind.Type && !desc.IsEtc) || index < 0)
return result;
// Member selection
currentNode = result;
result = null;
var caption = desc.IsEtc ? EtcKindToCaption (desc.Etc) : MemberKindToCaption (desc.DescKind);
currentNode = FindNodeForCaption (currentNode.ChildNodes, caption);
if (currentNode == null
|| (desc.IsEtc && desc.DescKind == EcmaDesc.Kind.Type && string.IsNullOrEmpty (desc.EtcFilter)))
return currentNode;
// Member search
result = null;
var format = desc.DescKind == EcmaDesc.Kind.Constructor ? EcmaDesc.Format.WithArgs : EcmaDesc.Format.WithoutArgs;
searchNode.Caption = desc.ToCompleteMemberName (format);
index = currentNode.ChildNodes.BinarySearch (searchNode, EcmaGenericNodeComparer.Instance);
if (index < 0)
return null;
result = currentNode.ChildNodes[index];
if (result.ChildNodes.Count == 0 || desc.IsEtc)
return result;
// Overloads search
currentNode = result;
searchNode.Caption = desc.ToCompleteMemberName (EcmaDesc.Format.WithArgs);
index = currentNode.ChildNodes.BinarySearch (searchNode, EcmaGenericNodeComparer.Instance);
if (index < 0)
return result;
result = result.ChildNodes[index];
return result;
}
static int GenericTypeBacktickSearch (IList<Node> childNodes, EcmaDesc desc)
{
/* Our strategy is to search for the non-generic variant of the type
* (which in most case should fail) and then use the closest index
* to linearily search for the generic variant with the right generic arg number
*/
var searchNode = new Node () { Caption = desc.TypeName };
int index = childNodes.BinarySearch (searchNode, EcmaTypeNodeComparer.Instance);
// Place the index in the right start position
if (index < 0)
index = ~index;
for (int i = index; i < childNodes.Count; i++) {
var currentNode = childNodes[i];
// Find the index of the generic argument list
int genericIndex = currentNode.Caption.IndexOf ('<');
// If we are not on the same base type name anymore, there is no point
int captionSlice = genericIndex != -1 ? genericIndex : currentNode.Caption.LastIndexOf (' ');
if (string.Compare (searchNode.Caption, 0,
currentNode.Caption, 0,
Math.Max (captionSlice, searchNode.Caption.Length),
StringComparison.Ordinal) != 0)
break;
var numGenerics = CountTypeGenericArguments (currentNode.Caption, genericIndex);
if (numGenerics == desc.GenericTypeArguments.Count) {
// Simple comparison if we are not looking for an inner type
if (desc.NestedType == null)
return i;
// If more complicated, we fallback to using EcmaUrlParser
var caption = currentNode.Caption;
caption = "T:" + caption.Substring (0, caption.LastIndexOf (' ')).Replace ('.', '+');
EcmaDesc otherDesc;
var parser = new EcmaUrlParser ();
if (parser.TryParse (caption, out otherDesc) && desc.NestedType.Equals (otherDesc.NestedType))
return i;
}
}
return -1;
}
// This comparer returns the answer straight from caption comparison
class EcmaGenericNodeComparer : IComparer<Node>
{
public static readonly EcmaGenericNodeComparer Instance = new EcmaGenericNodeComparer ();
public int Compare (Node n1, Node n2)
{
return string.Compare (n1.Caption, n2.Caption, StringComparison.Ordinal);
}
}
// This comparer take into account the space in the caption
class EcmaTypeNodeComparer : IComparer<Node>
{
public static readonly EcmaTypeNodeComparer Instance = new EcmaTypeNodeComparer ();
public int Compare (Node n1, Node n2)
{
int length1 = CaptionLength (n1.Caption);
int length2 = CaptionLength (n2.Caption);
return string.Compare (n1.Caption, 0, n2.Caption, 0, Math.Max (length1, length2), StringComparison.Ordinal);
}
int CaptionLength (string caption)
{
var length = caption.LastIndexOf (' ');
return length == -1 ? caption.Length : length;
}
}
public static Dictionary<string, string> GetContextForEcmaNode (string hash, string sourceID, Node node)
{
var args = new Dictionary<string, string> ();
args["source-id"] = sourceID;
if (node != null) {
var nodeType = GetNodeType (node);
switch (nodeType) {
case EcmaNodeType.Namespace:
args["show"] = "namespace";
args["namespace"] = node.Element.Substring ("N:".Length);
break;
case EcmaNodeType.Type:
args["show"] = "typeoverview";
break;
case EcmaNodeType.Member:
case EcmaNodeType.Meta:
switch (GetNodeMemberTypeChar (node)){
case 'C':
args["membertype"] = "Constructor";
break;
case 'M':
args["membertype"] = "Method";
break;
case 'P':
args["membertype"] = "Property";
break;
case 'F':
args["membertype"] = "Field";
break;
case 'E':
args["membertype"] = "Event";
break;
case 'O':
args["membertype"] = "Operator";
break;
case 'X':
args["membertype"] = "ExtensionMethod";
break;
case '*':
args["membertype"] = "All";
break;
}
if (nodeType == EcmaNodeType.Meta) {
args["show"] = "members";
args["index"] = "all";
} else {
args["show"] = "member";
args["index"] = node.Element;
}
break;
}
}
if (!string.IsNullOrEmpty (hash))
args["hash"] = hash;
return args;
}
public static EcmaNodeType GetNodeType (Node node)
{
// We guess the node type by checking the depth level it's at in the tree
int level = GetNodeLevel (node);
switch (level) {
case 0:
return EcmaNodeType.Namespace;
case 1:
return EcmaNodeType.Type;
case 2:
return EcmaNodeType.Meta;
case 3: // Here it's either a member or, in case of overload, a meta
return node.IsLeaf ? EcmaNodeType.Member : EcmaNodeType.Meta;
case 4: // At this level, everything is necessarily a member
return EcmaNodeType.Member;
default:
return EcmaNodeType.Invalid;
}
}
public static char GetNodeMemberTypeChar (Node node)
{
int level = GetNodeLevel (node);
// We try to reach the member group node depending on node nested level
switch (level) {
case 2:
return node.Element[0];
case 3:
return node.Parent.Element[0];
case 4:
return node.Parent.Parent.Element[0];
default:
throw new ArgumentException ("node", "Couldn't determine member type of node `" + node.Caption + "'");
}
}
public static int GetNodeLevel (Node node)
{
int i = 0;
for (; !node.Element.StartsWith ("root:/", StringComparison.OrdinalIgnoreCase); i++) {
node = node.Parent;
if (node == null)
return i - 1;
}
return i - 1;
}
public static string EtcKindToCaption (char etc)
{
switch (etc) {
case 'M':
return "Methods";
case 'P':
return "Properties";
case 'C':
return "Constructors";
case 'F':
return "Fields";
case 'E':
return "Events";
case 'O':
return "Operators";
case '*':
return "Members";
default:
return null;
}
}
public static string MemberKindToCaption (EcmaDesc.Kind kind)
{
switch (kind) {
case EcmaDesc.Kind.Method:
return "Methods";
case EcmaDesc.Kind.Property:
return "Properties";
case EcmaDesc.Kind.Constructor:
return "Constructors";
case EcmaDesc.Kind.Field:
return "Fields";
case EcmaDesc.Kind.Event:
return "Events";
case EcmaDesc.Kind.Operator:
return "Operators";
default:
return null;
}
}
public static Node FindNodeForCaption (IList<Node> nodes, string caption)
{
foreach (var node in nodes)
if (node.Caption.Equals (caption, StringComparison.OrdinalIgnoreCase))
return node;
return null;
}
public static int CountTypeGenericArguments (string typeDefinition, int startIndex = 0)
{
int nestedLevel = 0;
int count = 0;
bool started = false;
foreach (char c in typeDefinition.Skip (startIndex)) {
switch (c) {
case '<':
if (!started)
count = 1;
started = true;
nestedLevel++;
break;
case ',':
if (started && nestedLevel == 1)
count++;
break;
case '>':
nestedLevel--;
break;
}
}
return count;
}
internal static string MakeOperatorSignature (XElement member, out string memberSignature)
{
string name = (string)member.Attribute ("MemberName");
var nicename = name.Substring(3);
memberSignature = null;
switch (name) {
// unary operators: no overloading possible [ECMA-335 §10.3.1]
case "op_UnaryPlus": // static R operator+ (T)
case "op_UnaryNegation": // static R operator- (T)
case "op_LogicalNot": // static R operator! (T)
case "op_OnesComplement": // static R operator~ (T)
case "op_Increment": // static R operator++ (T)
case "op_Decrement": // static R operator-- (T)
case "op_True": // static bool operator true (T)
case "op_False": // static bool operator false (T)
case "op_AddressOf": // static R operator& (T)
case "op_PointerDereference": // static R operator* (T)
memberSignature = nicename;
break;
// conversion operators: overloading based on parameter and return type [ECMA-335 §10.3.3]
case "op_Implicit": // static implicit operator R (T)
case "op_Explicit": // static explicit operator R (T)
nicename = name.EndsWith ("Implicit") ? "ImplicitConversion" : "ExplicitConversion";
string arg = (string)member.Element ("Parameters").Element ("Parameter").Attribute ("Type");
string ret = (string)member.Element ("ReturnValue").Element ("ReturnType");
memberSignature = arg + " to " + ret;
break;
// binary operators: overloading is possible [ECMA-335 §10.3.2]
default:
if (member.Element ("Parameters") != null)
memberSignature =
nicename + "("
+ string.Join (",", member.Element ("Parameters").Elements ("Parameter").Select (p => (string)p.Attribute ("Type")))
+ ")";
break;
}
return nicename;
}
static XElement ExtractClassSummary (string typeFilePath)
{
using (var reader = XmlReader.Create (typeFilePath)) {
reader.ReadToFollowing ("Type");
var name = reader.GetAttribute ("Name");
var fullName = reader.GetAttribute ("FullName");
reader.ReadToFollowing ("AssemblyName");
var assemblyName = reader.ReadElementString ();
var summary = reader.ReadToFollowing ("summary") ? XElement.Load (reader.ReadSubtree ()) : new XElement ("summary");
var remarks = reader.ReadToFollowing ("remarks") ? XElement.Load (reader.ReadSubtree ()) : new XElement ("remarks");
return new XElement ("class",
new XAttribute ("name", name ?? string.Empty),
new XAttribute ("fullname", fullName ?? string.Empty),
new XAttribute ("assembly", assemblyName ?? string.Empty),
summary,
remarks);
}
}
}
}