[Mono-list] A documentation coverage tool

Peter Williams peter@newton.cx
30 Jan 2003 00:38:27 -0500


--=-iFvuyoeqqm8g3xeNaXIp
Content-Type: text/plain
Content-Transfer-Encoding: 7bit

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     peter@newton.cx / peterw@ximian.com

"[Ninjas] are cool; and by cool, I mean totally sweet."
                              -- REAL Ultimate Power

--=-iFvuyoeqqm8g3xeNaXIp
Content-Disposition: attachment; filename=ClassChecker.cs
Content-Type: text/plain; name=ClassChecker.cs; charset=ISO-8859-1
Content-Transfer-Encoding: 7bit

// Author: Peter Williams <peterw@ximian.com>

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();
	}
}

}

--=-iFvuyoeqqm8g3xeNaXIp--