Extract xml comments for public members only

北战南征 提交于 2019-12-04 22:49:24

SandCastle Help File Builder has an option to recreate the xml files containing only the configured access modes for methods, properties, etc...

The only "downside" is that you'll have to generate a documentation.

EDIT

Since it's been a long time ago I forgot that I added a "component" to SHFB to generate the XML.

The good news is that this component is included in SHFB.

You have to add the "Intellisense Component" to the SHFB project. It will then generate the XML according to the configured SHFB project.

For more info: Intellisense Component in SHFB

There is a tool inside the eazfuscator that can remove non-public documentation. You can see an example here

I've given this some thought and I decided to change the way I go about solving this particular problem. Instead of finding type/member within the assembly trying to parse the XML documentation notation. I decided to simply build a string set (of XML documentation notation) for the public API that can then be used to tes wheter a member isn't public.

It's really simple. Send an assembly to the XmlDocumentationStringSet and it will build a string set of the public API and delete the elements that aren't public.

static void Main(string[] args)
{
    var el = XElement.Load("ConsoleApplication18.XML");

    // obviously, improve this if necessary (might not work like this if DLL isn't already loaded)
    // you can use file paths
    var assemblyName = el.Descendants("assembly").FirstOrDefault();
    var assembly = Assembly.ReflectionOnlyLoad(assemblyName.Value);

    var stringSet = new XmlDocumentationStringSet(assembly);

    foreach (var member in el.Descendants("member").ToList()) // .ToList enables removing while traversing
    {
        var attr = member.Attribute("name");
        if (attr == null)
        {
            continue;
        }
        if (!stringSet.Contains(attr.Value))
        {
            member.Remove();
        }
    }

    el.Save("ConsoleApplication18-public.XML");
}

And here's the class that builds the XML documentation names (it's a bit large but I thought I post the entire source here anyway):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;

namespace ConsoleApplication18
{
    public class XmlDocumentationStringSet : IEnumerable<string>
    {
        private HashSet<string> stringSet = new HashSet<string>(StringComparer.Ordinal);

        public XmlDocumentationStringSet(Assembly assembly)
        {
            AddRange(assembly.GetExportedTypes());
        }

        public bool Contains(string name)
        {
            return stringSet.Contains(name);
        }

        /// <summary>
        /// Heelloasdasdasd
        /// </summary>
        /// <param name="types"></param>
        public void AddRange(IEnumerable<Type> types)
        {
            foreach (var type in types)
            {
                Add(type);
            }
        }

        public void Add(Type type)
        {
            // Public API only
            if (!type.IsVisible)
            {
                return;
            }
            var members = type.GetMembers(BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
            foreach (var member in members)
            {
                Add(type, member);
            }
        }

        StringBuilder sb = new StringBuilder();

        private void Add(Type type, MemberInfo member)
        {
            Type nestedType = null;

            sb.Length = 0;

            switch (member.MemberType)
            {
                case MemberTypes.Constructor:
                    sb.Append("M:");
                    AppendConstructor(sb, (ConstructorInfo)member);
                    break;
                case MemberTypes.Event:
                    sb.Append("E:");
                    AppendEvent(sb, (EventInfo)member);
                    break;
                case MemberTypes.Field:
                    sb.Append("F:");
                    AppendField(sb, (FieldInfo)member);
                    break;
                case MemberTypes.Method:
                    sb.Append("M:");
                    AppendMethod(sb, (MethodInfo)member);
                    break;
                case MemberTypes.NestedType:
                    nestedType = (Type)member;
                    if (IsVisible(nestedType))
                    {
                        sb.Append("T:");
                        AppendNestedType(sb, (Type)member);
                    }
                    break;
                case MemberTypes.Property:
                    sb.Append("P:");
                    AppendProperty(sb, (PropertyInfo)member);
                    break;
            }

            if (sb.Length > 0)
            {
                stringSet.Add(sb.ToString());
            }

            if (nestedType != null)
            {
                Add(nestedType);
            }
        }

        private bool IsVisible(Type nestedType)
        {
            return nestedType.IsVisible;
        }

        private void AppendProperty(StringBuilder sb, PropertyInfo propertyInfo)
        {
            if (!IsVisible(propertyInfo))
            {
                sb.Length = 0;
                return;
            }
            AppendType(sb, propertyInfo.DeclaringType);
            sb.Append('.').Append(propertyInfo.Name);
        }

        private bool IsVisible(PropertyInfo propertyInfo)
        {
            var getter = propertyInfo.GetGetMethod();
            var setter = propertyInfo.GetSetMethod();
            return (getter != null && IsVisible(getter)) || (setter != null && IsVisible(setter));
        }

        private void AppendNestedType(StringBuilder sb, Type type)
        {
            AppendType(sb, type.DeclaringType);
        }

        private void AppendMethod(StringBuilder sb, MethodInfo methodInfo)
        {
            if (!IsVisible(methodInfo) || (methodInfo.IsHideBySig && methodInfo.IsSpecialName))
            {
                sb.Length = 0;
                return;
            }
            AppendType(sb, methodInfo.DeclaringType);
            sb.Append('.').Append(methodInfo.Name);
            AppendParameters(sb, methodInfo.GetParameters());
        }

        private bool IsVisible(MethodInfo methodInfo)
        {
            return methodInfo.IsFamily || methodInfo.IsPublic;
        }

        private void AppendParameters(StringBuilder sb, ParameterInfo[] parameterInfo)
        {
            if (parameterInfo.Length == 0)
            {
                return;
            }
            sb.Append('(');
            for (int i = 0; i < parameterInfo.Length; i++)
            {
                if (i > 0)
                {
                    sb.Append(',');
                }
                var p = parameterInfo[i];
                AppendType(sb, p.ParameterType);
            }
            sb.Append(')');
        }

        private void AppendField(StringBuilder sb, FieldInfo fieldInfo)
        {
            if (!IsVisible(fieldInfo))
            {
                sb.Length = 0;
                return;
            }
            AppendType(sb, fieldInfo.DeclaringType);
            sb.Append('.').Append(fieldInfo.Name);
        }

        private bool IsVisible(FieldInfo fieldInfo)
        {
            return fieldInfo.IsFamily || fieldInfo.IsPublic;
        }

        private void AppendEvent(StringBuilder sb, EventInfo eventInfo)
        {
            if (!IsVisible(eventInfo))
            {
                sb.Length = 0;
                return;
            }
            AppendType(sb, eventInfo.DeclaringType);
            sb.Append('.').Append(eventInfo.Name);
        }

        private bool IsVisible(EventInfo eventInfo)
        {
            return true; // hu?
        }

        private void AppendConstructor(StringBuilder sb, ConstructorInfo constructorInfo)
        {
            if (!IsVisible(constructorInfo))
            {
                sb.Length = 0;
                return;
            }
            AppendType(sb, constructorInfo.DeclaringType);
            sb.Append('.').Append("#ctor");
            AppendParameters(sb, constructorInfo.GetParameters());
        }

        private bool IsVisible(ConstructorInfo constructorInfo)
        {
            return constructorInfo.IsFamily || constructorInfo.IsPublic;
        }

        private void AppendType(StringBuilder sb, Type type)
        {
            if (type.DeclaringType != null)
            {
                AppendType(sb, type.DeclaringType);
                sb.Append('.');
            }
            else if (!string.IsNullOrEmpty(type.Namespace))
            {
                sb.Append(type.Namespace);
                sb.Append('.');
            }
            sb.Append(type.Name);
            if (type.IsGenericType && !type.IsGenericTypeDefinition)
            {
                // Remove "`1" suffix from type name
                while (char.IsDigit(sb[sb.Length - 1]))
                    sb.Length--;
                sb.Length--;
                {
                    var args = type.GetGenericArguments();
                    sb.Append('{');
                    for (int i = 0; i < args.Length; i++)
                    {
                        if (i > 0)
                        {
                            sb.Append(',');
                        }
                        AppendType(sb, args[i]);
                    }
                    sb.Append('}');
                }
            }
        }

        public IEnumerator<string> GetEnumerator()
        {
            return stringSet.GetEnumerator();
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
}

Oh, and I haven't figured out how to handle events yet, they are always visible in this example.

@John Leidegren

I've got the same requirement, and I've found the answer to the missing bit of your code. An Event has 2 methods, Add and Remove, and is considered public if either of them is public. So it would be something like:

private bool IsVisible(EventInfo eventInfo)
{
    return eventInfo.GetAddMethod(false) != null
        || eventInfo.GetRemoveMethod(false) != null;
}

although I can't think of any reason why one would be public and not the other.

What tool are you using to generate the documentation? I use Sandcastle and that gives you the option to select members to include by accessability.

In general it seems expected that the XML would contain all info that could be needed and its up to the processing tool to select from it what is needed.

I was facing the same problem. SHFB is slow as hell and since we have another documentation code-base we did not need it to generate the documentation for us.

I ended up using XMLStarlet plus a separate namespace for internal classes. For example, all my internal classes would reside in MyCompany.MyProduct.Internal. Then I can use one simple command

xml ed -L -d "//member[contains(@name, 'MyCompany.MyProduct.Internal')]" MyProduct.xml

to cleanse the XML. This of course is not bullet-proof -- it does not cover internal members in public classes, and it does require some discipline to remember to put internal classes into internal. But this is the cleanest and least intrusive method which works for me. It is also a standalone EXE file easily checked into build server, no sweat.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!