On Wed, 2003-01-29 at 20:29, Peter Williams wrote:
> Hi Duncan et al,

Ok, I sort of ran with this and wrote a tool to compare the ECMA
documentation file for a class and compare it against the master.xml
file. Again, it almost surely has some issues, but I thought it was
really nifty.

        Peter

-- 
Peter Williams     [EMAIL PROTECTED] / [EMAIL PROTECTED]

"[Ninjas] are cool; and by cool, I mean totally sweet."
                              -- REAL Ultimate Power
// Author: Peter Williams <[EMAIL PROTECTED]>

using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections;
using System.IO;
using System.Xml;

//
// Known bugs:
//
// * Implementations of interface functions might
//   be handled wrong. The doc file records
//   the name as something like 
//
//   <Member MemberName="System.Collections.IEnumerable.GetEnumerator">
//
//   but the master XML file only has 
//
//   <method name="GetEnumerator()" argnames="" returntype="System.CharEnumerator" />
//
//   So there isn't enough info to tie the two together. Well, maybe we could chop
//   off everything before the last period? Seems risky.
//
// * Probably there are more bugs when the XML isn't in the expected format.
//   I must admit that XmlTextReader is not winning my heart.
//

namespace MonoDoc {

public enum MemberType {
        Class,
        Interface,
        Struct,
        Delegate,
        Enum,
        Constructor,
        Field,
        Prop,
        Method,
        Operator,
        Event
};

public enum DocType {
        Unknown,
        Docs,
        NoDocs
};
        
public class Member {
        public MemberType type;
        public DocType    docs;
        public string     name;
        public string     return_type; // or type of self if property
        public string[]   param_names;
        public string[]   param_types;

        private int param_size;

        private void AddParam (string name, string type)
        {
                if (param_size == 0) {
                        param_names = new string[2];
                        param_types = new string[2];
                } else if (param_size == param_names.Length) {
                        string[] new_names = new string [param_size * 2];
                        string[] new_types = new string [param_size * 2];

                        param_names.CopyTo (new_names, 0);
                        param_types.CopyTo (new_types, 0);

                        param_names = new_names;
                        param_types = new_types;
                }

                param_names[param_size] = name;
                param_types[param_size] = type;
                param_size++;
        }

        private void EnumFromMaster (XmlTextReader r)
        {
                int depth = r.Depth;

                while (r.Read () && r.Depth > depth) {
                        if (r.Name == "field" && r["name"] != "")
                                AddParam (r["name"], null);
                }
        }

        private void ParamsFromMaster (XmlTextReader r)
        {
                string name = r["name"];
                string args = r["argnames"];

                if (args == "")
                        return;

                string[] namelist = Regex.Split (args, ", ");
                string types = Regex.Match (name, 
"[^(]*\\(([^)]*)\\)").Groups[1].Value;
                string[] typelist = Regex.Split (types, ", ");

                for (int i = 0; i < namelist.Length; i++)
                        AddParam (namelist[i], typelist[i]);
        }

        private void TypeFromDoc (XmlTextReader r)
        {
                // this probably needs robustification

                r.Read();
                string t = r.Value;

                switch (t) {
                case "Class":
                case "Interface":
                case "Structure":
                        throw new Exception (String.Format ("Can't handle a nested {0} 
yet", t));
                case "Delegate":
                        type = MemberType.Delegate;
                        break;
                case "Enumeration":
                        type = MemberType.Enum;
                        break;
                case "Constructor":
                        type = MemberType.Constructor;
                        break;
                case "Field":
                        type = MemberType.Field;
                        break;
                case "Property":
                        type = MemberType.Prop;
                        break;
                case "Method":
                        type = MemberType.Method;
                        break;
                // This doesn't happen
                // case "Operator":
                //      type = MemberType.Operator;
                //      break;
                case "Event":
                        type = MemberType.Event;
                        break;
                default:
                        throw new Exception (String.Format (
                                "Unknown member type {0} in doc xml file", t));
                }
        }

        private void RVFromDoc (XmlTextReader r)
        {
                int depth = r.Depth;

                while (r.Read() && r.Depth > depth) {
                        if (r.Name == "ReturnType") {
                                r.Read();
                                return_type = r.Value;

                                // bail out before the close tag screws us up.
                                return;
                        }
                }

                // We can have this element be empty,
                // if it's an event or something, so
                // don't complain about that.
        }

        private void ParamsFromDoc (XmlTextReader r)
        {
                int depth = r.Depth;

                while (r.Read() & r.Depth > depth) {
                        if (r.Name != "Parameter" || r["Name"] == "")
                                continue;

                        AddParam (r["Name"], r["Type"]);
                }
        }

        // Public

        public Member ()
        {
                docs = DocType.Unknown;
                param_size = 0;
        }

        public static string MemberTypeName (MemberType type)
        {
                string t = "[unknown type]";

                switch (type) {
                case MemberType.Class:
                        t = "class";
                        break;
                case MemberType.Interface:
                        t = "interface";
                        break;
                case MemberType.Struct:
                        t = "structure";
                        break;
                case MemberType.Delegate:
                        t = "delegate";
                        break;
                case MemberType.Enum:
                        t = "enumeration";
                        break;
                case MemberType.Constructor:
                        t = "constructor";
                        break;
                case MemberType.Field:
                        t = "field";
                        break;
                case MemberType.Prop:
                        t = "property";
                        break;
                case MemberType.Method:
                        t = "method";
                        break;
                case MemberType.Operator:
                        t = "operator";
                        break;
                case MemberType.Event:
                        t = "event";
                        break;
                }

                return t;
        }

        public string ParamsList (bool with_names)
        {
                StringBuilder sb = new StringBuilder();

                if (with_names) {
                        for (int i = 0; i < param_size; i++) {
                                if (i != 0)
                                        sb.Append (", ");
                                sb.Append (String.Format ("{0} {1}", param_types[i], 
param_names[i]));
                        }
                } else {
                        for (int i = 0; i < param_size; i++) {
                                if (i != 0)
                                        sb.Append (", ");
                                sb.Append (param_types[i]);
                        }
                }

                return sb.ToString ();
        }

        public void Print()
        {
                Console.WriteLine ("{0} {1}:", MemberTypeName (type), name);

                if (return_type != null)
                        Console.WriteLine ("   return or self type: {0}", return_type);
                if (param_names != null)
                        Console.WriteLine ("   params: {0}", ParamsList (true));
        }

        // Read in a member from a master.xml-style file

        public static Member FromMasterFile (XmlTextReader r)
        {
                Member m = new Member ();

                m.name = r["name"];

                switch (r.Name) {
                case "class":
                case "interface":
                case "structure":
                case "delegate":
                        throw new Exception (String.Format (
                                "Can't handle a nested {0} yet.", r.Name));
                case "enum":
                        m.type = MemberType.Enum;
                        m.EnumFromMaster (r);
                        break;
                case "constructor":
                        m.type = MemberType.Constructor;
                        m.ParamsFromMaster (r);
                        break;
                case "method":
                        m.type = MemberType.Method;
                        m.ParamsFromMaster (r);
                        break;
                case "operator":
                        // The ECMA doc files list these as methods,
                        // and I'd rather lose this info and use the
                        // names, which agree, than include a table of
                        // all special method names and change matching
                        // methods to operators.

                        //m.type = MemberType.Operator;
                        m.type = MemberType.Method;
                        m.ParamsFromMaster (r);
                        break;
                case "property":
                        m.type = MemberType.Prop;
                        m.return_type = r["propertytype"];
                        break;
                case "event":
                        m.type = MemberType.Event;
                        break;
                case "field":
                        m.type = MemberType.Field;
                        break;
                }

                // Let's canonicalize for fun and profit
                if ((m.type == MemberType.Constructor || m.type == MemberType.Method)
                    && m.param_size == 0) {
                        if (Regex.Match (m.name, "\\(\\)$") == null)
                                m.name = m.name + "()";
                }

                return m;
        }

        // read in a member from a doc file

        public static Member FromDocFile (XmlTextReader r, string class_name)
        {
                Member m = new Member ();
                int depth;

                if (r.Name != "Member")
                        throw new Exception ("Not on <Member> when reading doc xml 
file");

                m.name = r["MemberName"];
                depth = r.Depth;

                // I can't figure out a way to tell start tags
                // from end tags. This is ridiculous.

                bool got_m_type = false;
                bool got_rv = false;
                bool got_params = false;

                while (r.Read() && r.Depth > depth) {
                        switch (r.Name) {
                        case "MemberType":
                                if (!got_m_type) {
                                        m.TypeFromDoc (r);
                                        got_m_type = true;
                                }
                                break;
                        case "ReturnValue":
                                if (!got_rv) {
                                        m.RVFromDoc (r);
                                        got_rv = true;
                                }
                                break;
                        case "Parameters":
                                if (!got_params) {
                                        m.ParamsFromDoc (r);
                                        got_params = true;
                                }
                                break;
                        case "Docs":
                                m.docs = DocType.Docs;
                                break;
                        }
                }

                if (m.docs == DocType.Unknown)
                        m.docs = DocType.NoDocs;

                if (m.type == MemberType.Method) {
                        // munge the name so that we can check
                        // overloadings

                        StringBuilder sb = new StringBuilder (m.name);

                        sb.Append ('(');
                        sb.Append (m.ParamsList (false));
                        sb.Append (')');
                        m.name = sb.ToString();
                }

                if (m.type == MemberType.Constructor) {
                        // whee, a different kind of munging

                        StringBuilder sb = new StringBuilder (class_name);
                        sb.Append ('(');
                        sb.Append (m.ParamsList (false));
                        sb.Append (')');
                        m.name = sb.ToString();
                }

                return m;
        }

        public bool CheckAgainst (Member canon)
        {
                bool ok = true;

                if (canon == null) {
                        Console.WriteLine ("{0}: This member is documented but doesn't 
exist", name);
                        return false;
                }

                if (name != canon.name)
                        throw new Exception (String.Format (
                                "Checking member {0} against {1}", name, canon.name));

                if (docs != DocType.Docs) {
                        Console.WriteLine ("{0}: No <Docs> tag.", name);
                        ok = false;
                }

                if (type != canon.type) {
                        Console.WriteLine ("{0}: Type is {2} but is documented as 
{1}.",
                                name, MemberTypeName (type), MemberTypeName 
(canon.type));
                        ok = false;
                }

                if (canon.return_type != null && return_type != canon.return_type) {
                        if (canon.type == MemberType.Prop)
                                Console.WriteLine ("{0}: Property type should be {1} 
but is documented as {2}.",
                                        name, canon.return_type, return_type);
                        else
                                Console.WriteLine ("{0}: Return type should be {1} but 
is documented as {2}.",
                                        name, canon.return_type, return_type);
                        ok = false;
                }

                if (canon.param_size > 0) {
                        if (param_size != canon.param_size) {
                                Console.WriteLine ("{0}: Method has {1} parameters but 
{2} are documented.",
                                        name, canon.param_size, param_size);

                                Console.WriteLine ("   {0}", ParamsList (true));
                                ok = false;
                        } else {
                                for (int i = 0; i < canon.param_size; i++) {
                                        if (param_names[i] != canon.param_names[i]) {
                                                Console.WriteLine ("{0}: parameter {1} 
is named {2} but is documented as {3}",
                                                        name, i, canon.param_names[i], 
param_names[i]);
                                                ok = false;
                                        }

                                        if (param_types[i] != canon.param_types[i]) {
                                                Console.WriteLine ("{0}: parameter {1} 
is of type {2} but is documented as {3}",
                                                        name, i, canon.param_types[i], 
param_types[i]);
                                                ok = false;
                                        }
                                }
                        }
                }

                return ok;
        }
}

class ClassChecker {

        string[] args;
        Hashtable cov;

        FileStream doc_stream;
        XmlTextReader dr;

        string class_name;
        string full_name;

        void Go ()
        {
                if (args.Length != 2) {
                        Usage ();
                        Environment.Exit (1);
                }

                string master_file = args[0];
                string doc_file = args[1];

                try {
                        // Open up the doc and see what we're dealing with
                        InitDoc (doc_file);

                        // Parse the master xml file; see what we should document.
                        ReadMaster (master_file);

                        // See what' actually documented
                        CompareDocs ();
                } catch (Exception e) {
                        Console.WriteLine ("Error checking doc file {0}: {1}",
                                doc_file, e.Message);
                }

                // Cleanup
                DoneDoc ();
        }

        void InitDoc (string doc_file)
        {
                doc_stream = File.OpenRead (doc_file);
                dr = new XmlTextReader (doc_stream);

                while (dr.Read()) {
                        if (dr.Name == "Type") {
                                class_name = dr["Name"];
                                full_name = dr["FullName"];

                                Console.WriteLine ("Checking documentation of {0}:", 
full_name);
                                Console.WriteLine ();
                                return;
                        }
                }

                throw new Exception (String.Format ("Didn't find <Type> node in {0}", 
doc_file));
        }

        void DoneDoc ()
        {
                dr.Close ();
                doc_stream.Close ();
        }

        void ReadMaster (string master_file)
        {
                FileStream master_stream = File.OpenRead (master_file);
                XmlTextReader mr = new XmlTextReader (master_stream);

                bool yay = false;

                while (mr.Read()) {
                        if ((mr.Name != "class" && mr.Name != "interface") || 
                            mr["name"] != class_name || 
                            mr["namespace"] + "." + class_name != full_name) 
                                continue;

                        yay = true;

                        int depth = mr.Depth;
                        while (mr.Read() && mr.Depth > depth) {
                                // we get closing tags too or something
                                if (mr["name"] != "") {
                                        Member m = Member.FromMasterFile (mr);
                                        //m.Print();
                                        cov[m.name] = m;
                                }
                        }

                        break;
                }

                mr.Close ();
                master_stream.Close ();

                if (yay == false)
                        throw new Exception (String.Format ("Didn't find <class> node 
for {0} in {1}",
                                full_name, master_file));
        }

        void CompareDocs ()
        {
                int ok = 0;
                int total = 0;

                while (dr.Read ()) {
                        if (dr.Name == "Members") {
                                int depth = dr.Depth;

                                while (dr.Read() && dr.Depth > depth) {
                                        if (dr.Name == "Member") {
                                                Member m = Member.FromDocFile (dr, 
class_name);
                                                Member canon = (Member) cov[m.name];
        
                                                total++;
                                                if (m.CheckAgainst (canon))
                                                        ok++;

                                                if (canon != null)
                                                        cov[m.name] = null;
                                        }
                                }
                        }
                }

                foreach (string s in cov.Keys) {
                        Member canon = cov[s] as Member;

                        if (canon != null) {
                                Console.WriteLine ("{0}: not documented", canon.name);
                                total++;
                        }
                }

                if (total == 0)
                        throw new Exception ("No <Members> tag in documentation file");

                Console.WriteLine ();
                Console.WriteLine ("Summary: {0} / {1} members properly documented 
({2:F2}%)",
                        ok, total, (float) 100.0 * ok / total);
        }

        ///////////////////////////////////////////
        // Housekeeping

        void Usage ()
        {
                Console.WriteLine ("ClassChecker.exe [path to master xml file] [path 
to class documentation xml file");
        }

        ClassChecker (string[] args)
        {
                this.args = new string[args.Length];
                args.CopyTo (this.args, 0);

                cov = new Hashtable ();
        }

        static void Main (string[] args)
        {
                ClassChecker cc = new ClassChecker (args);
                cc.Go();
        }
}

}

Reply via email to