[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--