Here is the code for the UnusedClassesRule.cs
class:
using Microsoft.FxCop.Sdk;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FxCopDemo {
[Flags]
public enum CodeLocation {
StaticInitializer = 4096,
ConstructorParameter = 1,
ConstructorBody = 2,
//---
MethodParameter = 4,
MethodBody = 8,
MethodReturnType = 16,
StaticMethodBody = 2048,
MethodDef = 8192,
ClassDef = 16384,
//---
PropertyReturnType = 32,
PropertyGetterBody = 64,
PropertySetterBody = 128,
//---
EventDelegate = 256,
//---
MemberVariable = 512,
StaticVariable = 1024
}
public class RefItem {
///<summary>All the locations in the source code where the full name is referenced.</summary>
public CodeLocation Locations = (CodeLocation) 0;
///<summary>The full name of the class, struct, or enum.</summary>
public readonly String FullName;
///<summary>The total number of times the FullName is referenced by the ClassTypes.</summary>
public int Count;
public RefItem(String fullName) {
this.FullName = fullName;
}
}
// for each custom class, maintain a collection of all types that the class references
public class Class2 : IComparable<Class2> {
///<summary>List of other class types that have a direct reference to this class. This list is populated at the very end.</summary>
public List<Class2> Nbors = new List<Class2>();
///<summary>The full name of this class, struct, or enum type.</summary>
public readonly String FullName;
///<summary>If this class is a nested class, the DeclaringType is the full name of the immediate host.</summary>
public readonly String DeclaringType;
///<summary>The full name of the immediate parent.</summary>
public readonly String BaseType;
///<summary>True if this class contains the Main(...) entry point method.</summary>
public readonly bool IsEntryPoint;
///<summary>The type of node, typically Class, Interface, DelegateNode, EnumNode. Used for reporting purposes.</summary>
public readonly NodeType NodeType;
private Dictionary<String, RefItem> dict = new Dictionary<String, RefItem>();
public Class2(String fullName, bool isEntryPoint, String declaringType, String baseType, NodeType nodeType) {
this.FullName = fullName;
this.IsEntryPoint = isEntryPoint;
this.DeclaringType = declaringType;
this.BaseType = baseType;
this.NodeType = nodeType;
}
///<summary>
///Adds a reference to a class that this class directly references.
///<param name="fullName">The full name of the class that this class references.</param>
///<param name="loc">The location in the code where the reference occurs. CodeLocation is a [Flags] enum, so multiple locations can exist.</param>
///</summary>
public void Add(String fullName, CodeLocation loc) {
// variables passed with 'ref' or 'out' as method parameters have an '@' as the last character of the fullName
fullName = fullName.TrimEnd('@');
lock(dict) {
RefItem ri = null;
if (!dict.TryGetValue(fullName, out ri)) {
ri = new RefItem(fullName);
dict[fullName] = ri;
}
ri.Locations = (ri.Locations | loc);
ri.Count++;
}
}
public RefItem GetRef(String fullName) {
RefItem ri = null;
dict.TryGetValue(fullName, out ri);
return ri;
}
public List<RefItem> GetRefs() {
return dict.Values.OrderBy(r => r.FullName).ToList();
}
public override String ToString() {
return FullName;
}
public int CompareTo(Class2 other) {
return String.Compare(this.FullName, other.FullName);
}
}
/*
For each type node, detect all other types it references.
Then for each node, determine the other nodes that point to it.
Report:
1. Any nodes that aren't pointed to, and
2. Any groups of nodes that are not connected to the main entry point.
Limitations:
1. Seems like when an enum is cast to a int value, the original enum type is not recoverable
e.g: int x = (int) MyEnum.SomeValue; // seems like the compiler replaces the MyEnum.SomeValue reference with the literal value
This means that some enums may be reported as unused, but they are actually used.
2. Unreachable code is reported as unused. Not sure how to access unreachable code using the FxCop api.
3. Does not account for reflection that uses hard coded string names.
The following are too granular and are not reported in this rule:
1. Methods (instance or static) that are never called.
2. Variables (properties or fields, instance or static) in classes not used.
3. Variables in method arguments not used.
*/
public class UnusedClassesRule : BaseIntrospectionRule {
private static Dictionary<String, Class2> dictTypes = new Dictionary<String, Class2>();
// The multi-threads in FxCop make it hard to debug. The messages are grouped by thread when debugging of this rule is needed.
private static Dictionary<int, StringBuilder> dictOutput = new Dictionary<int, StringBuilder>();
private Class2 currentC2 = null;
private CodeLocation currentLoc = (CodeLocation) 0;
private Member currentMember = null;
private bool writeCheckMemberError = true;
private bool writeVisitExpressionError = true;
public UnusedClassesRule() : base("UnusedClassesRule", "FxCopDemo.UnusedCodeXml", typeof(UnusedClassesRule).Assembly) {
}
public override TargetVisibilities TargetVisibility {
get {
return TargetVisibilities.All;
}
}
// Note: BeforeAnalysis() and AfterAnalysis() are called multiple times, one for each rule instance.
// According to a forum post on MSDN, one rule instance is created for each CPU core.
//public override void BeforeAnalysis() {
//base.BeforeAnalysis();
//MethodCollection list = CallGraph.CallersFor((Method) null);
//}
public override void AfterAnalysis() {
base.AfterAnalysis();
lock(dictTypes) {
if (dictTypes.Count == 0)
return;
try {
FinalAnalysis();
} catch (Exception ex) {
writeCheckMemberError = false;
Write("Error: " + ex.GetType().FullName + ": " + ex.Message, null, true);
Write(ex.StackTrace, null, true);
}
dictTypes.Clear();
}
}
public override ProblemCollection Check(Member member) {
try {
CheckInternal(member);
} catch (Exception ex) {
if (writeCheckMemberError) {
writeCheckMemberError = false;
Write("Error: " + ex.GetType().FullName + ": " + ex.Message);
Write(ex.StackTrace);
}
}
return this.Problems;
}
private ProblemCollection CheckInternal(Member member) {
Class2 ct = currentC2;
AttributeNodeCollection attribs = member.Attributes;
for (int i = 0; i < attribs.Count; i++) {
AttributeNode a = attribs[i];
Add(ct, a.Type, CodeLocation.MethodDef);
}
if (member is Method) {
currentMember = member;
var m = (Method) member;
if (m.ReturnType != FrameworkTypes.Void)
Add(ct, m.ReturnType, CodeLocation.MethodReturnType);
if (m.NodeType == NodeType.InstanceInitializer)
currentLoc = CodeLocation.ConstructorBody;
else if (m.NodeType == NodeType.StaticInitializer)
currentLoc = CodeLocation.StaticInitializer;
else
currentLoc = (m.IsStatic ? CodeLocation.StaticMethodBody : CodeLocation.MethodBody);
for (int i = 0; i < m.Parameters.Count; i++) {
Parameter p = m.Parameters[i];
TypeNode ty = p.Type;
Add(ct, ty, CodeLocation.MethodParameter);
}
// the DeclaringType is needed when an extension method is called
Add(ct, m.DeclaringType, currentLoc);
VisitMethod(m);
//VisitBlock(m.Body);
//VisitStatements(m.Body.Statements);
}
else if (member is PropertyNode) {
var pn = (PropertyNode) member;
if (pn.Setter != null) {
currentMember = member;
currentLoc = CodeLocation.PropertySetterBody;
VisitStatements(pn.Setter.Body.Statements);
}
if (pn.Getter != null) {
Add(ct, pn.Getter.ReturnType, CodeLocation.PropertyReturnType);
currentMember = member;
currentLoc = CodeLocation.PropertyGetterBody;
VisitStatements(pn.Getter.Body.Statements);
}
}
else if (member is Field) {
var f = (Field) member;
var loc = (f.IsStatic ? CodeLocation.StaticVariable : CodeLocation.MemberVariable);
Add(ct, f.Type, loc);
}
else if (member is EventNode) {
var evt = (EventNode) member;
Add(ct, evt.HandlerType, CodeLocation.EventDelegate);
}
else {
Write("UNKNOWN CHECK MEMBER: " + member.GetType().FullName + " full name: " + member.FullName);
}
return this.Problems;
}
private static void Add(Class2 ct, TypeNode ty, CodeLocation loc) {
// Given a TypeNode, it may have to be drilled down into determine it's actual type
// It's also possible that a single TypeNode results in multiple references. For example,
// public class MyList<T> { ... }
// public static void Foo<U>(MyList<U> list) where U : Bar {}
// which should result in adding namespace.MyList, and namespace.Bar
Stack<TypeNode> stack = new Stack<TypeNode>();
stack.Push(ty);
while (stack.Count > 0) {
ty = stack.Pop();
if (ty == null)
continue;
if (ty.IsGeneric) {
// otherwise Nullable`1 shows up in the list of references
if (ty.Template != null && ty.Template != FrameworkTypes.GenericNullable)
ct.Add(ty.Template.FullName, loc);
if (ty.TemplateArguments != null) {
foreach (TypeNode ta in ty.TemplateArguments)
stack.Push(ta);
}
if (ty.TemplateParameters != null) {
foreach (TypeNode tp in ty.TemplateParameters)
stack.Push(tp);
}
}
else if (ty.IsTemplateParameter) {
// this is the case when a constraint is placed on a generic type, e.g. public static void Foo<T>(U input) where U : Bar { ... }
// ty.FullName will be something like "parameter.T"
// The BaseType == "Bar". If no constraint is specified, then BaseType == "System.Object".
// The reason it's pushed on the stack is because it could be a constraint like where : U : MyList<Bar> { ... }
stack.Push(ty.BaseType);
}
else if (ty is ArrayType) {
ArrayType ty2 = (ArrayType) ty;
TypeNode ty3 = ty2.ElementType;
ct.Add(ty3.FullName, loc);
}
else {
ct.Add(ty.FullName, loc);
}
}
}
public override void VisitExpression(Expression exp) {
try {
VisitExpressionInternal(exp);
} catch (Exception ex) {
if (writeVisitExpressionError) {
writeVisitExpressionError = false; // only record the first error
Write("Error: " + ex.GetType().FullName + ": " + ex.Message, null, true);
Write(ex.StackTrace);
}
}
}
private void VisitExpressionInternal(Expression exp) {
if (exp == null)
return;
Stack<Expression> stack = new Stack<Expression>();
stack.Push(exp);
while (stack.Count > 0) {
exp = stack.Pop();
if (exp == null)
continue;
if (exp is NaryExpression) {
// this handles Construct, ConstructArray, MethodCall, Indexer
var ne = (NaryExpression) exp;
foreach (Expression e in ne.Operands) {
stack.Push(e); // the actual arguments
}
}
TypeNode ty = exp.Type;
bool alert = false;
if (exp is Variable || exp is Local) { // Note: Local extends Variable
Add(currentC2, ty, currentLoc);
//Write("VARIABLE NAME: " + ((Variable) exp).Name.Name + " " + exp);
}
else if (exp is Literal) {
var lit = (Literal) exp;
Add(currentC2, ty, currentLoc);
Object val = lit.Value;
if (val is TypeNode)
Add(currentC2, (TypeNode) val, currentLoc);
else if (val != null) {
// val == null when there is an assignment like, MyClass c = null;
Type ty2 = val.GetType();
bool isKnown = (ty2.IsPrimitive || ty2 == typeof(String));
if (isKnown) {
currentC2.Add(ty2.FullName, currentLoc);
}
else {
Write("val.GetType(): " + ty2.FullName);
alert = true;
}
}
//if (val is EnumNode) {
// var en = (EnumNode) val;
// en.UnderlyingType; // this is the primitive type that the Enum extends, typically int32
//}
}
else if (exp is MethodCall) {
var mc = (MethodCall) exp;
stack.Push(mc.Callee);
}
else if (exp is Construct) {
Add(currentC2, ty, currentLoc);
var c = (Construct) exp;
stack.Push(c.Constructor); // c.Constuctor is MemberBinding
}
else if (exp is Indexer) {
var ix = (Indexer) exp;
Add(currentC2, ix.ElementType, currentLoc);
stack.Push(ix.Object);
}
else if (exp is AddressDereference) {
var ad = (AddressDereference) exp; // nullable type
TypeNode ty2 = ad.Type;
Add(currentC2, ty2, currentLoc);
stack.Push(ad.Address);
}
else if (exp is MemberBinding) {
var mb = (MemberBinding) exp;
stack.Push(mb.TargetObject);
if (mb.BoundMember is Method) { // Note: InstanceInitializer is a Method
var m = (Method) mb.BoundMember;
Add(currentC2, m.DeclaringType, currentLoc);
CodeLocation loc = (mb.BoundMember is InstanceInitializer ? CodeLocation.ConstructorParameter : currentLoc);
for (int i = 0; i < m.Parameters.Count; i++) {
Parameter p = m.Parameters[i];
TypeNode ty2 = p.Type;
Add(currentC2, ty2, loc);
}
}
else if (mb.BoundMember is Field) {
// this occurs for a class level static variable
var f = (Field) mb.BoundMember;
var loc = (f.IsStatic ? CodeLocation.StaticVariable : CodeLocation.MemberVariable);
Add(currentC2, f.Type, loc);
Add(currentC2, f.DeclaringType, loc);
}
//else if (mb.BoundMember is PropertyNode) { // can this happen?
//}
else {
alert = true;
}
}
else if (exp is ConstructArray) {
var ca = (ConstructArray) exp;
Add(currentC2, ca.ElementType, currentLoc);
}
else if (exp is UnaryExpression) {
// e.g. a method call
var ue = (UnaryExpression) exp;
stack.Push(ue.Operand);
}
else if (exp is BinaryExpression) {
var be = (BinaryExpression) exp;
stack.Push(be.Operand1);
stack.Push(be.Operand2);
}
else if (exp is TernaryExpression) {
var te = (TernaryExpression) exp;
stack.Push(te.Operand1);
stack.Push(te.Operand2);
stack.Push(te.Operand3);
}
else if (exp is NamedArgument) {
var na = (NamedArgument) exp;
stack.Push(na.Value);
}
else {
NodeType nt = exp.NodeType;
bool ignore = (nt == NodeType.Pop || nt == NodeType.Dup);
alert = !ignore;
}
if (alert)
Write("x x " + currentC2.FullName + " " + currentMember.FullName + " " + exp.ToString() + " NodeType:" + exp.NodeType + " Type:" + exp.Type);
}
}
public override ProblemCollection Check(ModuleNode module) {
//Write("Check_ModuleNode: " + module.Name);
return this.Problems;
}
public override ProblemCollection Check(Parameter parameter) {
//Write("Check_Parameter: " + parameter.Name);
return this.Problems;
}
public override ProblemCollection Check(Resource resource) {
//Write("Check_Resource: " + resource.Name);
return this.Problems;
}
public override ProblemCollection Check(TypeNode type) {
//Write("Check_TypeNode: " + type.FullName + " declaring type: " + (type.DeclaringType == null ? "null" : type.DeclaringType.FullName) + " " + type.Interfaces);
currentC2 = GetClass2(type.FullName, type);
base.Check(type);
AttributeNodeCollection attribs = type.Attributes;
//Write("attributs.COunt: " + attribs.Count);
for (int i = 0; i < attribs.Count; i++) {
AttributeNode a = attribs[i];
//Write("a.Type: " + a.Type);
Add(currentC2, a.Type, CodeLocation.ClassDef);
}
InterfaceCollection iList = type.Interfaces;
for (int i = 0; i < iList.Count; i++) {
InterfaceNode n = iList[i];
Add(currentC2, n, CodeLocation.ClassDef);
}
return this.Problems;
}
private static Class2 GetClass2(String fullName, TypeNode ty) {
lock(dictTypes) {
Class2 ct = null;
if (!dictTypes.TryGetValue(fullName, out ct)) {
AssemblyNode a = ty.ContainingAssembly();
bool isEntryPoint = false;
Method ep = a.EntryPoint;
if (ep != null)
isEntryPoint = (String.Compare(fullName, ep.DeclaringType.FullName) == 0);
TypeNode dt = ty.DeclaringType;
TypeNode bt = ty.BaseType;
String dt_ = (dt == null ? "" : dt.FullName);
String bt_ = (bt == null ? "" : bt.FullName);
//Write("type: " + ty.FullName + " nt:" + ty.NodeType + " dt:" + dt_ + " bt:" + bt_);
ct = new Class2(fullName, isEntryPoint, dt_, bt_, ty.NodeType);
dictTypes[fullName] = ct;
}
return ct;
}
}
public override ProblemCollection Check(String namespaceName, TypeNodeCollection types) {
//Write("Check_namespaceName_types: " + namespaceName + ", " + types.Count);
return this.Problems;
}
protected override String GetLocalizedString(String name, params Object[] arguments) {
String s = base.GetLocalizedString(name, arguments);
//Write("GetLocalizedString: " + name);
return s;
}
protected override Resolution GetNamedResolution(String name, params Object[] arguments) {
Resolution r = base.GetNamedResolution(name, arguments);
//Write("GetNamedResolution: " + name);
return r;
}
private static void Write(String message, StreamWriter sw = null, bool console = false) {
if (dictOutput == null) {
if (console)
Console.WriteLine(message);
if (sw != null)
sw.WriteLine(message);
return;
}
lock(dictOutput) {
int id = System.Threading.Thread.CurrentThread.ManagedThreadId;
StringBuilder sb = null;
if (!dictOutput.TryGetValue(id, out sb)) {
sb = new StringBuilder();
dictOutput[id] = sb;
}
sb.AppendLine(message);
}
}
// chops off "Node" at the end, e.g: EnumNode > Node, DelegateNode > Delegate
private static String GetName(NodeType nt) {
String s = nt.ToString();
if (s.EndsWith("Node"))
s = s.Substring(0, s.Length - 4);
return s;
}
private static void FinalAnalysis() {
foreach (KeyValuePair<int, StringBuilder> pair in dictOutput) {
Console.WriteLine();
Console.WriteLine("Thread Id: " + pair.Key);
Console.Write(pair.Value);
}
dictOutput = null;
bool writeRefs = false;
bool console = false;
String outputFilename = null;
StreamWriter sw2 = null;
String[] args = Environment.GetCommandLineArgs(); // look at the args to determine what to output
foreach (String arg in args) {
if (arg.StartsWith("/out:")) {
outputFilename = arg.Substring(5);
String folder = Path.GetDirectoryName(outputFilename);
if (!Directory.Exists(folder))
Directory.CreateDirectory(folder);
System.Threading.Thread.Sleep(2000);
sw2 = new StreamWriter(new FileStream(outputFilename, FileMode.Create, FileAccess.Write));
}
else if (arg == "/c" || arg == "/console")
console = true;
else if (arg == "/v" || arg == "/verbose")
writeRefs = true;
}
Write("", sw2, console);
List<Class2> ctList = dictTypes.Values.OrderBy(t => t.FullName).ToList();
Hashtable htCluster = new Hashtable();
Hashtable ht = new Hashtable();
foreach (Class2 ct in ctList)
ht[ct.FullName] = ct;
foreach (Class2 ct in ctList) {
if (ct.DeclaringType.Length == 0) {
List<Class2> list = new List<Class2>();
list.Add(ct);
htCluster[ct] = list;
}
}
foreach (Class2 ct in ctList) {
if (ct.DeclaringType.Length == 0)
continue;
var ct2 = ct;
while (ct2.DeclaringType.Length > 0)
ct2 = (Class2) ht[ct2.DeclaringType];
List<Class2> list = (List<Class2>) htCluster[ct2];
ct2 = ct;
while (ct2.DeclaringType.Length > 0) {
list.Add(ct2);
ct2 = (Class2) ht[ct2.DeclaringType];
}
htCluster[ct] = list;
}
if (writeRefs) {
Write("ClassName,RefersTo,Locations", sw2, console);
foreach (Class2 ct in ctList) {
List<RefItem> refList = ct.GetRefs();
foreach (RefItem ri in refList)
Write(ct.FullName + "," + ri.FullName + "," + ri.Locations.ToString().Replace(',', ' '), sw2, console);
}
}
// if a nested class points to a parent class, don't count that as a reference
// if the only reference to class A is the construtor body of class B and class B extends class A, then don't count that as a reference
int n = ctList.Count;
for (int i = 0; i < n; i++) {
// check for other classes (ct2) that refer to ct
for (int j = 0; j < n; j++) {
if (i == j)
continue;
Class2 ct = ctList[i];
Class2 ct2 = ctList[j];
bool isNested = false;
String dt = ct2.DeclaringType;
while (dt.Length > 0) {
var p = (Class2) ht[dt];
if (p == ct) {
isNested = true;
break;
}
dt = p.DeclaringType;
}
if (isNested) {
//Write("Skipping: " + ct2.FullName + " because it is a nested class of: " + ct.FullName);
continue;
}
RefItem ri = ct2.GetRef(ct.FullName);
if (ri == null)
continue; // no references exist
if (ri.Locations == CodeLocation.ConstructorBody) {
bool isDescendant = false;
// ConstructorBody is the only location typically because of the implicit "base()" call.
// Check if ct2 is a descendant class of ct. If so, don't count the reference.
String bt = ct2.BaseType;
while (bt.Length > 0) {
var p = (Class2) ht[bt];
if (p == null)
break;
if (p == ct) {
isDescendant = true;
break;
}
bt = p.BaseType;
}
if (isDescendant)
continue; // skip, does not count as a reference
}
var list1 = (List<Class2>) htCluster[ct];
var list2 = (List<Class2>) htCluster[ct2];
if (list1 != list2) {
// group the nodes as part of the same cluster
List<Class2> list3 = new List<Class2>();
list3.AddRange(list1);
list3.AddRange(list2);
foreach (Class2 c2 in list3)
htCluster[c2] = list3;
//Write("Merged: [" + String.Join(", ", list1.Select(t => t.FullName)) + "] and [" + String.Join(", ", list2.Select(t => t.FullName)) + "]", sw2, console);
}
while (ct != null) {
if (ct2 != ct)
ct.Nbors.Add(ct2);
//Console.WriteLine(ct.FullName + ".Nbors.Add(" + ct2.FullName + ")");
ct = (Class2) ht[ct.DeclaringType];
}
}
}
// for each type that has a non-ConstructorBody reference, the ancestor types are counted as references.
// for example, if there were 5 classes A to E, where each one extends the one before it, i.e: A < B < C < D < E
// Suppose only class C is referenced somewhere in the code, then classes A & B are considered to be used,
// but classes D & E are considered unused.
foreach (Class2 ct in ctList) {
if (ct.Nbors.Count == 0)
continue;
Class2 ct2 = ct;
String bt = ct2.BaseType;
while (bt.Length > 0) {
var p = (Class2) ht[bt];
if (p == null)
break;
if (p.Nbors.Count == 0)
p.Nbors.Add(ct2);
bt = p.BaseType;
ct2 = p;
}
}
List<List<Class2>> clusters = htCluster.Values.Cast<List<Class2>>().Distinct().ToList();
List<List<Class2>> clusters2 = new List<List<Class2>>();
foreach (List<Class2> list in clusters) {
if (list.FirstOrDefault(t => t.IsEntryPoint) != null)
continue; // the cluster that contains the Main() method is not reported as an isolated cluster
// a cluster must have two or more non-related, non-nested classes
var list2 = list.Where(t => t.DeclaringType.Length == 0).ToList();
if (list2.Count > 1)
clusters2.Add(list2);
}
if (clusters2.Count > 0) {
Write(clusters2.Count + " isolated code clusters detected:", sw2, console);
for (int i = 0; i < clusters2.Count; i++) {
List<Class2> list = clusters2[i];
list.Sort();
Write("Cluster " + (i+1) + " has " + list.Count + " classes:", sw2, console);
for (int j = 0; j < list.Count; j++) {
Write(" " + (j+1) + ". " + list[j].FullName + " (" + GetName(list[j].NodeType) + ")", sw2, console);
}
Write("", sw2, console);
}
}
// the class that contains the entry point is not reported as unused
// Don't report anonymous classes (classes that have "<>" in the name)
List<Class2> ctUnused = ctList.Where(t => t.Nbors.Count == 0 && t.FullName.IndexOf("<>") < 0 && !t.IsEntryPoint).ToList();
if (ctUnused.Count > 0) {
// group by NodeType
Dictionary<NodeType, List<Class2>> dict = new Dictionary<NodeType, List<Class2>>();
foreach (Class2 ct in ctUnused) {
Class2 ct2 = ct;
bool include = true;
while (ct2.DeclaringType.Length > 0) {
var dt = (Class2) ht[ct2.DeclaringType];
if (dt.Nbors.Count == 0) {
// don't report a nested class if a parent class will be reported
include = false;
break;
}
ct2 = dt;
}
if (!include)
continue;
List<Class2> list = null;
if (!dict.TryGetValue(ct.NodeType, out list)) {
list = new List<Class2>();
dict[ct.NodeType] = list;
}
list.Add(ct);
}
List<NodeType> types = dict.Keys.ToList();
types.Sort();
foreach (NodeType key in types) {
String keyName = GetName(key);
List<Class2> list = dict[key];
Write("", sw2, console);
if (list.Count > 1)
Write("The following " + list.Count + " " + keyName + "s are not used by other code:", sw2, console);
else
Write("The following " + keyName + " is not used by other code:", sw2, console);
for (int i = 0; i < list.Count; i++) {
Write((i + 1) + ": " + list[i].FullName, sw2, console);
}
}
}
if (sw2 != null) {
sw2.Flush();
sw2.Dispose();
}
}
}
}