diff options
author | Sebastien Pouliot <sebastien@ximian.com> | 2008-02-15 22:51:21 +0300 |
---|---|---|
committer | Sebastien Pouliot <sebastien@ximian.com> | 2008-02-15 22:51:21 +0300 |
commit | 7714ce1c5622ad329f110fffe754eb0a4cda27b7 (patch) | |
tree | d713ac070f0a463b196145397fa4d9b286048514 /gendarme/console | |
parent | 2282e366e0e10c29b45c81a8d86f3badc5a9014e (diff) | |
parent | b502282bb675feba4f68136a9f1e49f9b8a008e7 (diff) |
2008-02-15 Sebastien Pouliot <sebastien@ximian.com>
* ConsoleRunner.cs: Updated runner for the new framework (still a
work in progress).
* gendarme.xsl: Updated XSLT to match new XML format.
* HtmlResultWriter.cs: Produce HTML reports using gendarme.xsl and
the XML writer.
* Makefile.am: Updated with changes.
* Options.cs: Jonathan Pryor Getopt::Long-inspired option parsing
library for C#
* ResultWriter.cs: Renamed from IResultWriter. Now an abstract class.
* TextResultWriter.cs: Updated for display more information.
* XmlResultWriter.cs: Updated xml format for results.
svn path=/trunk/mono-tools/; revision=95808
Diffstat (limited to 'gendarme/console')
-rw-r--r-- | gendarme/console/ChangeLog | 14 | ||||
-rw-r--r-- | gendarme/console/ConsoleRunner.cs | 533 | ||||
-rw-r--r-- | gendarme/console/HtmlResultWriter.cs | 60 | ||||
-rw-r--r-- | gendarme/console/Makefile.am | 6 | ||||
-rw-r--r-- | gendarme/console/Options.cs | 1032 | ||||
-rw-r--r-- | gendarme/console/ResultWriter.cs (renamed from gendarme/console/IResultWriter.cs) | 49 | ||||
-rw-r--r-- | gendarme/console/TextResultWriter.cs | 79 | ||||
-rw-r--r-- | gendarme/console/XmlResultWriter.cs | 151 | ||||
-rw-r--r-- | gendarme/console/gendarme.xsl | 307 |
9 files changed, 1643 insertions, 588 deletions
diff --git a/gendarme/console/ChangeLog b/gendarme/console/ChangeLog index bd48921a..237a30c2 100644 --- a/gendarme/console/ChangeLog +++ b/gendarme/console/ChangeLog @@ -1,3 +1,17 @@ +2008-02-15 Sebastien Pouliot <sebastien@ximian.com> + + * ConsoleRunner.cs: Updated runner for the new framework (still a + work in progress). + * gendarme.xsl: Updated XSLT to match new XML format. + * HtmlResultWriter.cs: Produce HTML reports using gendarme.xsl and + the XML writer. + * Makefile.am: Updated with changes. + * Options.cs: Jonathan Pryor Getopt::Long-inspired option parsing + library for C# + * ResultWriter.cs: Renamed from IResultWriter. Now an abstract class. + * TextResultWriter.cs: Updated for display more information. + * XmlResultWriter.cs: Updated xml format for results. + 2008-02-03 Sebastien Pouliot <sebastien@ximian.com> * gendarme.xsl: Display the correct message (not the first one) for diff --git a/gendarme/console/ConsoleRunner.cs b/gendarme/console/ConsoleRunner.cs index 3c9da508..c27b41fd 100644 --- a/gendarme/console/ConsoleRunner.cs +++ b/gendarme/console/ConsoleRunner.cs @@ -27,363 +27,300 @@ // using System; -using System.Collections; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; using System.Text; using System.Xml; using Mono.Cecil; + using Gendarme.Framework; -using Gendarme.Console.Writers; -class ConsoleRunner : Runner { +using NDesk.Options; - private const string defaultConfiguration = "rules.xml"; - private const string defaultRuleSet = "default"; +namespace Gendarme { - private string config; - private string set; - private Hashtable assemblies; - private string format; - private string output; + public class ConsoleRunner : Runner { - private static Assembly assembly; - private bool quiet; + private string config_file = "rules.xml"; + private string rule_set = "*"; + private string html_file; + private string log_file; + private string xml_file; + private bool help; + private bool quiet; + private List<string> assembly_names; - static Assembly Assembly { - get { - if (assembly == null) - assembly = Assembly.GetExecutingAssembly (); - return assembly; + int Parse (string [] args) + { + var p = new OptionSet () { + { "config=", v => config_file = v }, + { "set=", v => rule_set = v }, + { "log=", v => log_file = v }, + { "xml=", v => xml_file = v }, + { "html=", v => html_file = v }, + { "v|verbose", v => ++VerbosityLevel }, + { "quiet", v => quiet = v != null }, + { "h|?|help", v => help = v != null }, + }; + assembly_names = p.Parse (args); + return (assembly_names.Count > 0) ? 0 : 1; } - } - static string GetFullPath (string filename) - { - if (Path.GetDirectoryName (filename) != String.Empty) - return filename; - return Path.Combine (Path.GetDirectoryName (Assembly.Location), filename); - } + // name can be + // - a filename (a single assembly) + // - a mask (*, ?) for multiple assemblies + // - a special file (@) containing a list of assemblies + int AddFiles (string name) + { + if (String.IsNullOrEmpty (name)) + return 0; - static string GetNext (string[] args, int index, string defaultValue) - { - if ((args == null) || (index < 0) || (index >= args.Length)) - return defaultValue; - return args [index]; - } - - // name can be - // - a filename (a single assembly) - // - a mask (*, ?) for multiple assemblies - // - a special file (@) containing a list of assemblies - void AddFiles (string name) - { - if ((name == null) || (name.Length == 0)) - return; - - if (name.StartsWith ("@")) { - // note: recursive (can contains @, masks and filenames) - using (StreamReader sr = File.OpenText (name.Substring (1))) { - while (sr.Peek () >= 0) { - AddFiles (sr.ReadLine ()); + if (name.StartsWith ("@", StringComparison.OrdinalIgnoreCase)) { + // note: recursive (can contains @, masks and filenames) + using (StreamReader sr = File.OpenText (name.Substring (1))) { + while (sr.Peek () >= 0) { + AddFiles (sr.ReadLine ()); + } } + } else if (name.IndexOfAny (new char [] { '*', '?' }) >= 0) { + string dirname = Path.GetDirectoryName (name); + if (dirname.Length == 0) + dirname = "."; // assume current directory + string [] files = Directory.GetFiles (dirname, Path.GetFileName (name)); + foreach (string file in files) { + AddAssembly (file); + } + } else { + AddAssembly (name); } - } else if (name.IndexOfAny (new char[] { '*', '?' }) >= 0) { - string dirname = Path.GetDirectoryName (name); - if (dirname.Length == 0) - dirname = "."; // assume current directory - string [] files = Directory.GetFiles (dirname, Path.GetFileName (name)); - foreach (string file in files) { - assemblies.Add (Path.GetFullPath (file), null); - } - } else { - assemblies.Add (Path.GetFullPath (name), null); + return 0; } - } - bool ParseOptions (string[] args) - { - // defaults - config = GetFullPath (defaultConfiguration); - set = defaultRuleSet; - assemblies = new Hashtable (); - - // TODO - we probably want (i.e. later) the possibility to - // include/exclude certain rules from executing - for (int i=0; i < args.Length; i++) { - switch (args [i]) { - case "--config": - config = GetNext (args, ++i, defaultConfiguration); - break; - case "--set": - set = GetNext (args, ++i, defaultRuleSet); - break; - case "--debug": - debug = true; - break; - case "--quiet": - quiet = true; - break; - case "--help": - return false; - case "--log": - format = "text"; - output = GetNext (args, ++i, String.Empty); - break; - case "--xml": - format = "xml"; - output = GetNext (args, ++i, String.Empty); - break; - case "--html": - format = "html"; - output = GetNext (args, ++i, String.Empty); - break; - default: - AddFiles (args[i]); - break; - } + void AddAssembly (string filename) + { + string assembly_name = Path.GetFullPath (filename); + AssemblyDefinition ad = AssemblyFactory.GetAssembly (assembly_name); + (ad as IAnnotationProvider).Annotations.Add ("filename", assembly_name); + Assemblies.Add (ad); } - return (assemblies.Count > 0); - } - bool LoadCustomParameters (XmlElement ruleset) { - foreach (XmlElement parameter in ruleset.SelectNodes ("parameter")) { - try { - if (!parameter.HasAttribute ("name")) - throw new XmlException ("The attribute name can't be found"); - if (!parameter.HasAttribute ("value")) - throw new XmlException ("The attribute value can't be found"); - if (!parameter.HasAttribute ("rule")) - throw new XmlException ("The attribute rule can't be found"); - - string name = GetAttribute (parameter, "name", String.Empty); - int value = 0; - try { - value = Int32.Parse (GetAttribute (parameter, "value", String.Empty)); - } - catch (Exception exception) { - throw new XmlException ("The value for the value field should be an integer.", exception); - } - string ruleName = GetAttribute (parameter, "rule", String.Empty); + static string GetFullPath (string filename) + { + if (Path.GetDirectoryName (filename).Length > 0) + return filename; + return Path.Combine (Path.GetDirectoryName (Assembly.Location), filename); + } + + static string GetAttribute (XmlElement xel, string name, string defaultValue) + { + XmlAttribute xa = xel.Attributes [name]; + if (xa == null) + return defaultValue; + return xa.Value; + } - ApplyCustomParameterToRule (ruleName, name, value); + private static bool IsContainedInRuleSet (string rule, string mask) + { + string [] ruleSet = mask.Split ('|'); + foreach (string entry in ruleSet) { + if (String.Compare (rule, entry.Trim ()) == 0) + return true; } - catch (Exception e) { - Console.WriteLine ("Error reading parameters{0}Details: {1}", Environment.NewLine, e); - return false; + return false; + } + + public static bool RuleFilter (Type type, object interfaceName) + { + return (type.ToString () == (interfaceName as string)); + } + + public int LoadRulesFromAssembly (string assembly, string includeMask, string excludeMask) + { + int total = 0; + Assembly a = Assembly.LoadFile (Path.GetFullPath (assembly)); + foreach (Type t in a.GetTypes ()) { + if (t.IsAbstract || t.IsInterface) + continue; + + if (includeMask != "*") + if (!IsContainedInRuleSet (t.Name, includeMask)) + continue; + + if ((excludeMask != null) && (excludeMask.Length > 0)) + if (IsContainedInRuleSet (t.Name, excludeMask)) + continue; + + if (t.FindInterfaces (new TypeFilter (RuleFilter), "Gendarme.Framework.IRule").Length > 0) { + Rules.Add ((IRule) Activator.CreateInstance (t)); + total++; + } } + return total; } - return true; - } - static string GetAttribute (XmlElement xel, string name, string defaultValue) - { - XmlAttribute xa = xel.Attributes [name]; - if (xa == null) - return defaultValue; - return xa.Value; - } + bool LoadConfiguration () + { + XmlDocument doc = new XmlDocument (); + doc.Load (config_file); + if (doc.DocumentElement.Name != "gendarme") + return false; - bool LoadConfiguration () - { - XmlDocument doc = new XmlDocument (); - doc.Load (config); - if (doc.DocumentElement.Name != "gendarme") - return false; + bool result = false; + foreach (XmlElement ruleset in doc.DocumentElement.SelectNodes ("ruleset")) { + if (ruleset.Attributes ["name"].Value != rule_set) + continue; + foreach (XmlElement assembly in ruleset.SelectNodes ("rules")) { + string include = GetAttribute (assembly, "include", "*"); + string exclude = GetAttribute (assembly, "exclude", String.Empty); + string from = GetFullPath (GetAttribute (assembly, "from", String.Empty)); - bool result = false; - foreach (XmlElement ruleset in doc.DocumentElement.SelectNodes("ruleset")) { - if (ruleset.Attributes["name"].Value != set) - continue; - foreach (XmlElement assembly in ruleset.SelectNodes("rules")) { - string include = GetAttribute (assembly, "include", "*"); - string exclude = GetAttribute (assembly, "exclude", String.Empty); - string from = GetFullPath (GetAttribute (assembly, "from", String.Empty)); - try { int n = LoadRulesFromAssembly (from, include, exclude); result = (result || (n > 0)); } - catch (Exception e) { - Console.WriteLine ("Error reading rules{1}Details: {0}", e, Environment.NewLine); - return false; - } } - if (!LoadCustomParameters (ruleset)) - return false; + return result; } - return result; - } - void ApplyCustomParameterToRule (string ruleName, string name, int value) - { - IRule rule = GetRule (ruleName); - if (rule == null) - throw new ArgumentException (String.Format ("The rule name {0} can't be found in the rules collection", ruleName), "rule"); - PropertyInfo property = rule.GetType ().GetProperty (name); - if (property == null) - throw new ArgumentException (String.Format ("The property {0} can't be found in the rule {1}", name, ruleName), "name"); - if (!property.CanWrite) - throw new ArgumentException (String.Format ("The property {0} can't be written in the rule {1}", name, ruleName), "name"); - object result = property.GetSetMethod ().Invoke (rule, new object[] {value}); - } + int Report () + { + // generate text report (default, to console, if xml and html aren't specified) + if ((log_file != null) || ((xml_file == null) && (html_file == null))) { + using (TextResultWriter writer = new TextResultWriter (this, log_file)) { + writer.Report (); + } + } - IRule GetRule (string name) - { - IRule result; - result = GetRuleFromSet (name, Rules.Assembly); - if (result == null) { - result = GetRuleFromSet (name, Rules.Module); - if (result == null) { - result = GetRuleFromSet (name, Rules.Type); - if (result == null) { - result = GetRuleFromSet (name, Rules.Method); + // generate XML report + if (xml_file != null) { + using (XmlResultWriter writer = new XmlResultWriter (this, xml_file)) { + writer.Report (); } } - } - return result; - } - static IRule GetRuleFromSet (string name, RuleCollection rules) - { - foreach (IRule rule in rules) { - if (String.Compare (name, rule.GetType ().FullName) == 0) - return rule; + // generate HTML report + if (html_file != null) { + using (HtmlResultWriter writer = new HtmlResultWriter (this, html_file)) { + writer.Report (); + } + } + return 0; } - return null; - } - void Header () - { - if (quiet) - return; - - Assembly a = Assembly.GetExecutingAssembly(); - Version v = a.GetName ().Version; - if (v.ToString () != "0.0.0.0") { - Console.WriteLine ("Gendarme v{0}", v); - object[] attr = a.GetCustomAttributes (typeof (AssemblyCopyrightAttribute), false); - if (attr.Length > 0) - Console.WriteLine (((AssemblyCopyrightAttribute) attr [0]).Copyright); - } else { - Console.WriteLine ("Gendarme - Development Snapshot"); - } - Console.WriteLine (); - } + int Execute (string [] args) + { + try { + int result = Parse (args); + if (result != 0) { + if (help) { + Help (); + } + return result; + } - static void Help () - { - Console.WriteLine ("Usage: gendarme [--config file] [--set ruleset] [--{log|xml|html} file] assembly"); - Console.WriteLine ("Where"); - Console.WriteLine (" --config file\t\tSpecify the configuration file. Default is 'rules.xml'."); - Console.WriteLine (" --set ruleset\t\tSpecify the set of rules to verify. Default is '*'."); - Console.WriteLine (" --log file\t\tSave the text output to the specified file."); - Console.WriteLine (" --xml file\t\tSave the output, as XML, to the specified file."); - Console.WriteLine (" --html file\t\tSave the output, as HTML, to the specified file."); - Console.WriteLine (" --quiet\t\tDisplay minimal output (results) from the runner."); - Console.WriteLine (" --debug\t\tEnable debugging output."); - Console.WriteLine (" assembly\t\tSpecify the assembly to verify."); - Console.WriteLine (); - } + Header (); - void Write (string text) - { - if (!quiet) - Console.Write (text); - } + // load configuration, including rules, and continue if + // there's at least one rule to execute + if (!LoadConfiguration () || (Rules.Count < 1)) + return 3; - void WriteLine (string text) - { - if (!quiet) - Console.WriteLine (text); - } - - void WriteLine (string text, params object[] args) - { - if (!quiet) - Console.WriteLine (text, args); - } - - static void ProcessRules (ConsoleRunner runner) - { - runner.Header (); - string[] assemblies = new string [runner.assemblies.Count]; - runner.assemblies.Keys.CopyTo (assemblies, 0); - DateTime total = DateTime.UtcNow; - foreach (string assembly in assemblies) { - DateTime start = DateTime.UtcNow; - runner.Write (assembly); - try { - AssemblyDefinition ad = AssemblyFactory.GetAssembly (assembly); - try { - runner.Process (ad); - runner.assemblies [assembly] = ad; - runner.WriteLine (" - completed ({0} seconds).", (DateTime.UtcNow - start).TotalSeconds); - } - catch (Exception e) { - runner.WriteLine (" - error executing rules{0}Details: {1}", Environment.NewLine, e); + foreach (string name in assembly_names) { + result = AddFiles (name); + if (result != 0) + return result; } + + // now that all rules and assemblies are know, time to initialize + Initialize (); + // before analizing the assemblies with the rules + Run (); + + return Report (); } catch (Exception e) { - runner.WriteLine (" - error processing{0}\tDetails: {1}", Environment.NewLine, e); + Console.WriteLine ("Uncatched exception occured. Please fill a bug report."); + Console.WriteLine ("Rule:\t{0}", CurrentRule); + Console.WriteLine ("Target:\t{0}", CurrentTarget); + Console.WriteLine ("Stack trace: {0}", e); + return 4; } } - runner. WriteLine ("{0}{1} assemblies processed in {2} seconds.{0}", Environment.NewLine, runner.assemblies.Count, - (DateTime.UtcNow - total).TotalSeconds); - } - static void Report (ConsoleRunner runner) - { - IResultWriter writer; - switch (runner.format) { - case "xml": - writer = new XmlResultWriter (runner.output); - break; - case "html": - writer = new HtmlResultWriter (runner.output); - break; - default: - writer = new TextResultWriter (runner.output); - break; + public override void Run () + { + DateTime start = DateTime.UtcNow; + base.Run (); + Console.WriteLine ("{0}{1} assemblies processed in {2} seconds.{0}", + Environment.NewLine, Assemblies.Count, (DateTime.UtcNow - start).TotalSeconds); } - writer.Start (); - writer.Write (runner.assemblies); - writer.Write (runner.Rules); - foreach (Violation v in runner.Violations) { - writer.Write (v); + protected override void OnAssembly (RunnerEventArgs e) + { + DateTime start = DateTime.UtcNow; + base.OnAssembly (e); + Console.WriteLine ("{0}: {1} seconds.", + (e.CurrentAssembly as IAnnotationProvider).Annotations ["filename"], + (DateTime.UtcNow - start).TotalSeconds); } - writer.End (); - } - static int Main (string[] args) - { - ConsoleRunner runner = new ConsoleRunner (); + void Header () + { + if (quiet) + return; - // runner options and configuration - - try { - if (!runner.ParseOptions (args)) { - Help (); - return 1; + Assembly a = Assembly.GetExecutingAssembly (); + Version v = a.GetName ().Version; + if (v.ToString () != "0.0.0.0") { + Console.WriteLine ("Gendarme v{0}", v); + object [] attr = a.GetCustomAttributes (typeof (AssemblyCopyrightAttribute), false); + if (attr.Length > 0) + Console.WriteLine (((AssemblyCopyrightAttribute) attr [0]).Copyright); + } else { + Console.WriteLine ("Gendarme - Development Snapshot"); } - if (!runner.LoadConfiguration ()) { - Console.WriteLine ("No assembly file were specified."); - return 1; + Console.WriteLine (); + } + + private static Assembly assembly; + + static Assembly Assembly { + get { + if (assembly == null) + assembly = Assembly.GetExecutingAssembly (); + return assembly; } } - catch (Exception e) { - Console.WriteLine (e); - return 1; + + static void Help () + { + Console.WriteLine ("Usage: gendarme [--config file] [--set ruleset] [--{log|xml|html} file] assembly"); + Console.WriteLine ("Where"); + Console.WriteLine (" --config file\t\tSpecify the configuration file. Default is 'rules.xml'."); + Console.WriteLine (" --set ruleset\t\tSpecify the set of rules to verify. Default is '*'."); + Console.WriteLine (" --log file\t\tSave the text output to the specified file."); + Console.WriteLine (" --xml file\t\tSave the output, as XML, to the specified file."); + Console.WriteLine (" --html file\t\tSave the output, as HTML, to the specified file."); + Console.WriteLine (" --quiet\t\tDisplay minimal output (results) from the runner."); + Console.WriteLine (" --v\t\tEnable debugging output (use multiple time to augment verbosity)."); + Console.WriteLine (" assembly\t\tSpecify the assembly to verify."); + Console.WriteLine (); } - ProcessRules (runner); - Report (runner); - - if (runner.Violations.Count == 0) { - runner.WriteLine ("No rule's violation were found."); - return 0; + /// <summary> + /// + /// </summary> + /// <param name="args"></param> + /// <returns>0 for success, + /// 1 if some defects are found, + /// 2 if some parameters are bad, + /// 3 if a problem is related to the xml configuration file + /// 4 if an uncatched exception occured</returns> + static int Main (string [] args) + { + return new ConsoleRunner ().Execute (args); } - return 1; } } diff --git a/gendarme/console/HtmlResultWriter.cs b/gendarme/console/HtmlResultWriter.cs index 9b844971..35e0c764 100644 --- a/gendarme/console/HtmlResultWriter.cs +++ b/gendarme/console/HtmlResultWriter.cs @@ -27,7 +27,6 @@ // using System; -using System.Collections; using System.IO; using System.Reflection; using System.Xml; @@ -35,56 +34,53 @@ using System.Xml.Xsl; using Gendarme.Framework; -namespace Gendarme.Console.Writers { +namespace Gendarme { - public class HtmlResultWriter : IResultWriter { + public class HtmlResultWriter : ResultWriter, IDisposable { - private XmlResultWriter writer; private string temp_filename; - private string final_filename; - public HtmlResultWriter (string output) + public HtmlResultWriter (IRunner runner, string fileName) + : base (runner, fileName) { - final_filename = output; temp_filename = Path.GetTempFileName (); - writer = new XmlResultWriter (temp_filename); } - public void Start () + protected override void Write() { - writer.Start (); - } - - public void End () - { - try { - writer.End (); - // load XSL file from embedded resource - using (Stream s = Assembly.GetExecutingAssembly ().GetManifestResourceStream ("gendarme.xsl")) { - // process the XML result with the XSL file - XslCompiledTransform xslt = new XslCompiledTransform (); - xslt.Load (new XmlTextReader (s)); - xslt.Transform (temp_filename, final_filename); - } - } - finally { - File.Delete (temp_filename); + using (XmlResultWriter writer = new XmlResultWriter (Runner, temp_filename)) { + writer.Report (); } } - public void Write (IDictionary assemblies) + protected override void Finish () { - writer.Write (assemblies); + // load XSL file from embedded resource + Assembly a = Assembly.GetExecutingAssembly (); + string[] resources = a.GetManifestResourceNames (); + if (resources.Length != 1) + throw new InvalidDataException ("Could not locate XSL style sheet"); + + using (Stream s = a.GetManifestResourceStream (resources [0])) { + // process the XML result with the XSL file + XslCompiledTransform xslt = new XslCompiledTransform (); + xslt.Load (new XmlTextReader (s)); + xslt.Transform (temp_filename, FileName); + } } - public void Write (Rules rules) + public void Dispose () { - writer.Write (rules); + Dispose (true); + GC.SuppressFinalize (this); } - public void Write (Violation v) + protected virtual void Dispose (bool disposing) { - writer.Write (v); + if (disposing) { + if (File.Exists (temp_filename)) + File.Delete (temp_filename); + } } } } diff --git a/gendarme/console/Makefile.am b/gendarme/console/Makefile.am index 7c612a67..da90479a 100644 --- a/gendarme/console/Makefile.am +++ b/gendarme/console/Makefile.am @@ -6,15 +6,15 @@ DISTCLEANFILES = Makefile.in gendarme_sources_in = ../AssemblyInfo.cs.in gendarme_generated_sources = $(gendarme_sources_in:.in=) -gendarme_sources = ConsoleRunner.cs IResultWriter.cs TextResultWriter.cs XmlResultWriter.cs HtmlResultWriter.cs +gendarme_sources = ConsoleRunner.cs ResultWriter.cs TextResultWriter.cs XmlResultWriter.cs HtmlResultWriter.cs Options.cs gendarme_resources = gendarme.xsl gendarme_build_sources = $(addprefix $(srcdir)/, $(gendarme_sources)) gendarme_build_sources += $(gendarme_generated_sources) ../bin/gendarme.exe: $(gendarme_build_sources) $(gendarme_resources) - $(GMCS) -debug -r:$(top_builddir)/gendarme/bin/Mono.Cecil.dll -r:../bin/Gendarme.Framework.dll -r:System.Xml.dll -out:$@ $(gendarme_build_sources) \ - -resource:gendarme.xsl + $(GMCS) -debug -d:LINQ -r:$(top_builddir)/gendarme/bin/Mono.Cecil.dll -r:../bin/Gendarme.Framework.dll -r:System.Xml.dll \ + -out:$@ $(gendarme_build_sources) -resource:gendarme.xsl self-test: ../bin/gendarme.exe mono --debug ../bin/gendarme.exe ../bin/gendarme.exe diff --git a/gendarme/console/Options.cs b/gendarme/console/Options.cs new file mode 100644 index 00000000..540ac51f --- /dev/null +++ b/gendarme/console/Options.cs @@ -0,0 +1,1032 @@ +// +// Options.cs +// +// Authors: +// Jonathan Pryor <jpryor@novell.com> +// +// Copyright (C) 2008 Novell (http://www.novell.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +// Compile With: +// gmcs -debug+ -d:TEST -r:System.Core Options.cs +// gmcs -debug+ -d:LINQ -d:TEST -r:System.Core Options.cs + +// +// A Getopt::Long-inspired option parsing library for C#. +// +// NDesk.Options.OptionSet is built upon a key/value table, where the +// key is a option format string and the value is an Action<string> +// delegate that is invoked when the format string is matched. +// +// Option format strings: +// BNF Grammar: ( name [=:]? ) ( '|' name [=:]? )+ +// +// Each '|'-delimited name is an alias for the associated action. If the +// format string ends in a '=', it has a required value. If the format +// string ends in a ':', it has an optional value. If neither '=' or ':' +// is present, no value is supported. +// +// Options are extracted either from the current option by looking for +// the option name followed by an '=' or ':', or is taken from the +// following option IFF: +// - The current option does not contain a '=' or a ':' +// - The following option is not a registered named option +// +// The `name' used in the option format string does NOT include any leading +// option indicator, such as '-', '--', or '/'. All three of these are +// permitted/required on any named option. +// +// Option bundling is permitted so long as: +// - '-' is used to start the option group +// - all of the bundled options do not require values +// - all of the bundled options are a single character +// +// This allows specifying '-a -b -c' as '-abc'. +// +// Option processing is disabled by specifying "--". All options after "--" +// are returned by OptionSet.Parse() unchanged and unprocessed. +// +// Unprocessed options are returned from OptionSet.Parse(). +// +// Examples: +// int verbose = 0; +// OptionSet p = new OptionSet () +// .Add ("v", v => ++verbose) +// .Add ("name=|value=", v => Console.WriteLine (v)); +// p.Parse (new string[]{"-v", "--v", "/v", "-name=A", "/name", "B", "extra"}); +// +// The above would parse the argument string array, and would invoke the +// lambda expression three times, setting `verbose' to 3 when complete. +// It would also print out "A" and "B" to standard output. +// The returned array would contain the string "extra". +// +// C# 3.0 collection initializers are supported: +// var p = new OptionSet () { +// { "h|?|help", v => ShowHelp () }, +// }; +// +// System.ComponentModel.TypeConverter is also supported, allowing the use of +// custom data types in the callback type; TypeConverter.ConvertFromString() +// is used to convert the value option to an instance of the specified +// type: +// +// var p = new OptionSet () { +// { "foo=", (Foo f) => Console.WriteLine (f.ToString ()) }, +// }; +// +// Random other tidbits: +// - Boolean options (those w/o '=' or ':' in the option format string) +// are explicitly enabled if they are followed with '+', and explicitly +// disabled if they are followed with '-': +// string a = null; +// var p = new OptionSet () { +// { "a", s => a = s }, +// }; +// p.Parse (new string[]{"-a"}); // sets v != null +// p.Parse (new string[]{"-a+"}); // sets v != null +// p.Parse (new string[]{"-a-"}); // sets v == null +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Runtime.Serialization; +using System.Text.RegularExpressions; + +#if LINQ +using System.Linq; +#endif + +#if TEST +using NDesk.Options; +#endif + +#if !LINQ +namespace System { + public delegate void Action<T1, T2> (T1 a, T2 b); +} +#endif + +namespace NDesk.Options { + + public enum OptionValueType { + None, + Optional, + Required, + } + + public class OptionContext { + public OptionContext () + { + } + + public Option Option { get; set; } + public string OptionName { get; set; } + public int OptionIndex { get; set; } + public string OptionValue { get; set; } + } + + public abstract class Option { + string prototype, description; + string [] names; + OptionValueType type; + + public Option (string prototype, string description) + { + if (prototype == null) + throw new ArgumentNullException ("prototype"); + if (prototype.Length == 0) + throw new ArgumentException ("Cannot be the empty string.", "prototype"); + + this.prototype = prototype; + this.names = prototype.Split ('|'); + this.description = description; + this.type = ValidateNames (); + } + + public string Prototype { get { return prototype; } } + public string Description { get { return description; } } + public OptionValueType OptionValueType { get { return type; } } + + public string [] GetNames () + { + return (string []) names.Clone (); + } + + internal string [] Names { get { return names; } } + + static readonly char [] NameTerminator = new char [] { '=', ':' }; + private OptionValueType ValidateNames () + { + char type = '\0'; + for (int i = 0; i < names.Length; ++i) { + string name = names [i]; + if (name.Length == 0) + throw new ArgumentException ("Empty option names are not supported.", "prototype"); + + int end = name.IndexOfAny (NameTerminator); + if (end > 0) { + names [i] = name.Substring (0, end); + if (type == '\0' || type == name [end]) + type = name [end]; + else + throw new ArgumentException ( + string.Format ("Conflicting option types: '{0}' vs. '{1}'.", type, name [end]), + "prototype"); + } + } + if (type == '\0') + return OptionValueType.None; + return type == '=' ? OptionValueType.Required : OptionValueType.Optional; + } + + public void Invoke (OptionContext c) + { + OnParseComplete (c); + c.OptionName = null; + c.OptionValue = null; + c.Option = null; + } + + protected abstract void OnParseComplete (OptionContext c); + + public override string ToString () + { + return Prototype; + } + } + + [Serializable] + public class OptionException : Exception { + private string option; + + public OptionException (string message, string optionName) + : base (message) + { + this.option = optionName; + } + + public OptionException (string message, string optionName, Exception innerException) + : base (message, innerException) + { + this.option = optionName; + } + + protected OptionException (SerializationInfo info, StreamingContext context) + : base (info, context) + { + this.option = info.GetString ("OptionName"); + } + + public string OptionName + { + get { return this.option; } + } + + public override void GetObjectData (SerializationInfo info, StreamingContext context) + { + base.GetObjectData (info, context); + info.AddValue ("OptionName", option); + } + } + + public class OptionSet : Collection<Option> { + public OptionSet () + : this (f => f) + { + } + + public OptionSet (Converter<string, string> localizer) + { + this.localizer = localizer; + } + + Dictionary<string, Option> options = new Dictionary<string, Option> (); + Converter<string, string> localizer; + + protected Option GetOptionForName (string option) + { + if (option == null) + throw new ArgumentNullException ("option"); + Option v; + if (options.TryGetValue (option, out v)) + return v; + return null; + } + + protected override void ClearItems () + { + this.options.Clear (); + } + + protected override void InsertItem (int index, Option item) + { + Add (item); + base.InsertItem (index, item); + } + + protected override void RemoveItem (int index) + { + Option p = Items [index]; + foreach (string name in p.Names) { + this.options.Remove (name); + } + base.RemoveItem (index); + } + + protected override void SetItem (int index, Option item) + { + RemoveItem (index); + Add (item); + base.SetItem (index, item); + } + + class ActionOption : Option { + Action<string, OptionContext> action; + + public ActionOption (string prototype, string description, Action<string, OptionContext> action) + : base (prototype, description) + { + if (action == null) + throw new ArgumentNullException ("action"); + this.action = action; + } + + protected override void OnParseComplete (OptionContext c) + { + action (c.OptionValue, c); + } + } + + public new OptionSet Add (Option option) + { + if (option == null) + throw new ArgumentNullException ("option"); + List<string> added = new List<string> (); + try { + foreach (string name in option.Names) { + this.options.Add (name, option); + } + } + catch (Exception e) { + foreach (string name in added) + this.options.Remove (name); + throw; + } + return this; + } + + public OptionSet Add (string options, Action<string> action) + { + return Add (options, null, action); + } + + public OptionSet Add (string options, Action<string, OptionContext> action) + { + return Add (options, null, action); + } + + public OptionSet Add (string options, string description, Action<string> action) + { + if (action == null) + throw new ArgumentNullException ("action"); + return Add (options, description, (v, c) => { action (v); }); + } + + public OptionSet Add (string options, string description, Action<string, OptionContext> action) + { + Option p = new ActionOption (options, description, action); + base.Add (p); + return this; + } + + public OptionSet Add<T> (string options, Action<T> action) + { + return Add (options, null, action); + } + + public OptionSet Add<T> (string options, Action<T, OptionContext> action) + { + return Add (options, null, action); + } + + public OptionSet Add<T> (string options, string description, Action<T> action) + { + return Add (options, description, (T v, OptionContext c) => { action (v); }); + } + + public OptionSet Add<T> (string options, string description, Action<T, OptionContext> action) + { + TypeConverter conv = TypeDescriptor.GetConverter (typeof (T)); + Action<string, OptionContext> a = delegate (string s, OptionContext c) { + T t = default (T); + try { + if (s != null) + t = (T) conv.ConvertFromString (s); + } + catch (Exception e) { + throw new OptionException ( + string.Format ( + localizer ("Could not convert string `{0}' to type {1} for option `{2}'."), + s, typeof (T).Name, c.OptionName), + c.OptionName, e); + } + action (t, c); + }; + return Add (options, description, a); + } + + protected virtual OptionContext CreateOptionContext () + { + return new OptionContext (); + } + +#if LINQ + public List<string> Parse (IEnumerable<string> options) + { + bool process = true; + OptionContext c = CreateOptionContext (); + c.OptionIndex = -1; + var unprocessed = + from option in options + where ++c.OptionIndex >= 0 && process + ? option == "--" + ? (process = false) + : !Parse (option, c) + : true + select option; + List<string> r = unprocessed.ToList (); + if (c.Option != null) + NoValue (c); + return r; + } +#else + public List<string> Parse (IEnumerable<string> options) + { + OptionContext c = CreateOptionContext (); + c.OptionIndex = -1; + bool process = true; + List<string> unprocessed = new List<string> (); + foreach (string option in options) { + ++c.OptionIndex; + if (option == "--") { + process = false; + continue; + } + if (!process) { + unprocessed.Add (option); + continue; + } + if (!Parse (option, c)) + unprocessed.Add (option); + } + if (c.Option != null) + NoValue (c); + return unprocessed; + } +#endif + + private readonly Regex ValueOption = new Regex ( + @"^(?<flag>--|-|/)(?<name>[^:=]+)([:=](?<value>.*))?$"); + + protected bool GetOptionParts (string option, out string flag, out string name, out string value) + { + Match m = ValueOption.Match (option); + if (!m.Success) { + flag = name = value = null; + return false; + } + flag = m.Groups ["flag"].Value; + name = m.Groups ["name"].Value; + value = !m.Groups ["value"].Success ? null : m.Groups ["value"].Value; + return true; + } + + protected virtual bool Parse (string option, OptionContext c) + { + if (c.Option != null) { + c.OptionValue = option; + c.Option.Invoke (c); + return true; + } + + string f, n, v; + if (!GetOptionParts (option, out f, out n, out v)) + return false; + + Option p; + if (this.options.TryGetValue (n, out p)) { + c.OptionName = f + n; + c.Option = p; + switch (p.OptionValueType) { + case OptionValueType.None: + c.OptionValue = n; + c.Option.Invoke (c); + break; + case OptionValueType.Optional: + case OptionValueType.Required: + if (v != null) { + c.OptionValue = v; + c.Option.Invoke (c); + } + break; + } + return true; + } + // no match; is it a bool option? + if (ParseBool (option, n, c)) + return true; + // is it a bundled option? + if (ParseBundled (f, n, c)) + return true; + + return false; + } + + private bool ParseBool (string option, string n, OptionContext c) + { + Option p; + if (n.Length >= 1 && (n [n.Length - 1] == '+' || n [n.Length - 1] == '-') && + this.options.TryGetValue (n.Substring (0, n.Length - 1), out p)) { + string v = n [n.Length - 1] == '+' ? option : null; + c.OptionName = option; + c.OptionValue = v; + c.Option = p; + p.Invoke (c); + return true; + } + return false; + } + + private bool ParseBundled (string f, string n, OptionContext c) + { + Option p; + if (f == "-" && this.options.TryGetValue (n [0].ToString (), out p)) { + int i = 0; + do { + string opt = "-" + n [i].ToString (); + if (p.OptionValueType != OptionValueType.None) { + throw new OptionException (string.Format ( + localizer ("Cannot bundle option '{0}' that requires a value."), opt), + opt); + } + c.OptionName = opt; + c.OptionValue = n; + c.Option = p; + p.Invoke (c); + } while (++i < n.Length && this.options.TryGetValue (n [i].ToString (), out p)); + return true; + } + return false; + } + + private void NoValue (OptionContext c) + { + c.OptionValue = null; + Option p = c.Option; + if (p != null && p.OptionValueType == OptionValueType.Optional) { + p.Invoke (c); + } else if (p != null && p.OptionValueType == OptionValueType.Required) { + throw new OptionException (string.Format ( + localizer ("Missing required value for option '{0}'."), c.OptionName), + c.OptionName); + } + } + + private const int OptionWidth = 29; + + public void WriteOptionDescriptions (TextWriter o) + { + foreach (Option p in this) { + List<string> names = new List<string> (p.Names); + + int written = 0; + if (names [0].Length == 1) { + Write (o, ref written, " -"); + Write (o, ref written, names [0]); + } else { + Write (o, ref written, " --"); + Write (o, ref written, names [0]); + } + + for (int i = 1; i < names.Count; ++i) { + Write (o, ref written, ", "); + Write (o, ref written, names [i].Length == 1 ? "-" : "--"); + Write (o, ref written, names [i]); + } + + if (p.OptionValueType == OptionValueType.Optional) + Write (o, ref written, localizer ("[=VALUE]")); + else if (p.OptionValueType == OptionValueType.Required) + Write (o, ref written, localizer ("=VALUE")); + + if (written < OptionWidth) + o.Write (new string (' ', OptionWidth - written)); + else { + o.WriteLine (); + o.Write (new string (' ', OptionWidth)); + } + + o.WriteLine (localizer (p.Description)); + } + } + + static void Write (TextWriter o, ref int n, string s) + { + n += s.Length; + o.Write (s); + } + } +} + +#if TEST +namespace Tests.NDesk.Options { + + using System.Linq; + + class FooConverter : TypeConverter { + public override bool CanConvertFrom (ITypeDescriptorContext context, Type sourceType) + { + if (sourceType == typeof (string)) + return true; + return base.CanConvertFrom (context, sourceType); + } + + public override object ConvertFrom (ITypeDescriptorContext context, + CultureInfo culture, object value) + { + string v = value as string; + if (v != null) { + switch (v) { + case "A": return Foo.A; + case "B": return Foo.B; + } + } + + return base.ConvertFrom (context, culture, value); + } + } + + [TypeConverter (typeof(FooConverter))] + class Foo { + public static readonly Foo A = new Foo ("A"); + public static readonly Foo B = new Foo ("B"); + string s; + Foo (string s) { this.s = s; } + public override string ToString () {return s;} + } + + class Test { + public static void Main (string[] args) + { + var tests = new Dictionary<string, Action> () { + { "boolean", () => CheckBoolean () }, + { "bundling", () => CheckOptionBundling () }, + { "context", () => CheckOptionContext () }, + { "descriptions", () => CheckWriteOptionDescriptions () }, + { "exceptions", () => CheckExceptions () }, + { "halt", () => CheckHaltProcessing () }, + { "localization", () => CheckLocalization () }, + { "many", () => CheckMany () }, + { "optional", () => CheckOptional () }, + { "required", () => CheckRequired () }, + { "derived-type", () => CheckDerivedType () }, + }; + bool run = true; + bool help = false; + var p = new OptionSet () { + { "t|test=", + "Run the specified test. Valid tests:\n" + new string (' ', 32) + + string.Join ("\n" + new string (' ', 32), tests.Keys.OrderBy (s => s).ToArray ()), + v => { run = false; Console.WriteLine (v); tests [v] (); } }, + { "h|?|help", "Show this message and exit", (v) => help = v != null }, + }; + p.Parse (args); + if (help) { + Console.WriteLine ("usage: Options.exe [OPTION]+\n"); + Console.WriteLine ("Options unit test program."); + Console.WriteLine ("Valid options include:"); + p.WriteOptionDescriptions (Console.Out); + } else if (run) { + foreach (Action a in tests.Values) + a (); + } + } + + static IEnumerable<string> _ (params string[] a) + { + return a; + } + + static void CheckRequired () + { + string a = null; + int n = 0; + OptionSet p = new OptionSet () { + { "a=", v => a = v }, + { "n=", (int v) => n = v }, + }; + List<string> extra = p.Parse (_("a", "-a", "s", "-n=42", "n")); + Assert (extra.Count, 2); + Assert (extra [0], "a"); + Assert (extra [1], "n"); + Assert (a, "s"); + Assert (n, 42); + + extra = p.Parse (_("-a=")); + Assert (extra.Count, 0); + Assert (a, ""); + } + + static void CheckOptional () + { + string a = null; + int n = -1; + Foo f = null; + OptionSet p = new OptionSet () { + { "a:", v => a = v }, + { "n:", (int v) => n = v }, + { "f:", (Foo v) => f = v }, + }; + p.Parse (_("-a=s")); + Assert (a, "s"); + p.Parse (_("-a")); + Assert (a, null); + p.Parse (_("-a=")); + Assert (a, ""); + + p.Parse (_("-f", "A")); + Assert (f, Foo.A); + p.Parse (_("-f")); + Assert (f, null); + + p.Parse (_("-n", "42")); + Assert (n, 42); + p.Parse (_("-n")); + Assert (n, 0); + } + + static void CheckBoolean () + { + bool a = false; + OptionSet p = new OptionSet () { + { "a", v => a = v != null }, + }; + p.Parse (_("-a")); + Assert (a, true); + p.Parse (_("-a+")); + Assert (a, true); + p.Parse (_("-a-")); + Assert (a, false); + } + + static void CheckMany () + { + int a = -1, b = -1; + string av = null, bv = null; + Foo f = null; + int help = 0; + int verbose = 0; + OptionSet p = new OptionSet () { + { "a=", v => { a = 1; av = v; } }, + { "b", "desc", v => {b = 2; bv = v;} }, + { "f=", (Foo v) => f = v }, + { "v", v => { ++verbose; } }, + { "h|?|help", (v) => { switch (v) { + case "h": help |= 0x1; break; + case "?": help |= 0x2; break; + case "help": help |= 0x4; break; + } } }, + }; + List<string> e = p.Parse (new string[]{"foo", "-v", "-a=42", "/b-", + "-a", "64", "bar", "--f", "B", "/h", "-?", "--help", "-v"}); + + Assert (e.Count, 2); + Assert (e[0], "foo"); + Assert (e[1], "bar"); + Assert (a, 1); + Assert (av, "64"); + Assert (b, 2); + Assert (bv, null); + Assert (verbose, 2); + Assert (help, 0x7); + Assert (f, Foo.B); + } + + static void Assert<T>(T actual, T expected) + { + if (!object.Equals (actual, expected)) + throw new InvalidOperationException ( + string.Format ("Assertion failed: {0} != {1}", actual, expected)); + } + + class DefaultOption : Option { + public DefaultOption (string prototypes, string description) + : base (prototypes, description) + { + } + + protected override void OnParseComplete (OptionContext c) + { + throw new NotImplementedException (); + } + } + + static void CheckExceptions () + { + string a = null; + var p = new OptionSet () { + { "a=", v => a = v }, + { "c", v => { } }, + { "n=", (int v) => { } }, + { "f=", (Foo v) => { } }, + }; + // missing argument + AssertException (typeof(OptionException), + "Missing required value for option '-a'.", + p, v => { v.Parse (_("-a")); }); + // another named option while expecting one -- follow Getopt::Long + AssertException (null, null, + p, v => { v.Parse (_("-a", "-a")); }); + Assert (a, "-a"); + // no exception when an unregistered named option follows. + AssertException (null, null, + p, v => { v.Parse (_("-a", "-b")); }); + Assert (a, "-b"); + AssertException (typeof(ArgumentNullException), + "Argument cannot be null.\nParameter name: option", + p, v => { v.Add (null); }); + + // bad type + AssertException (typeof(OptionException), + "Could not convert string `value' to type Int32 for option `-n'.", + p, v => { v.Parse (_("-n", "value")); }); + AssertException (typeof(OptionException), + "Could not convert string `invalid' to type Foo for option `--f'.", + p, v => { v.Parse (_("--f", "invalid")); }); + + // try to bundle with an option requiring a value + AssertException (typeof(OptionException), + "Cannot bundle option '-a' that requires a value.", + p, v => { v.Parse (_("-ca", "value")); }); + + AssertException (typeof(ArgumentNullException), + "Argument cannot be null.\nParameter name: prototype", + p, v => { new DefaultOption (null, null); }); + AssertException (typeof(ArgumentException), + "Cannot be the empty string.\nParameter name: prototype", + p, v => { new DefaultOption ("", null); }); + AssertException (typeof(ArgumentException), + "Empty option names are not supported.\nParameter name: prototype", + p, v => { new DefaultOption ("a|b||c=", null); }); + AssertException (typeof(ArgumentException), + "Conflicting option types: '=' vs. ':'.\nParameter name: prototype", + p, v => { new DefaultOption ("a=|b:", null); }); + AssertException (typeof(ArgumentNullException), + "Argument cannot be null.\nParameter name: action", + p, v => { v.Add ("foo", (Action<string>) null); }); + AssertException (typeof(ArgumentNullException), + "Argument cannot be null.\nParameter name: action", + p, v => { v.Add ("foo", (Action<string, OptionContext>) null); }); + } + + static void AssertException<T> (Type exception, string message, T a, Action<T> action) + { + Type actualType = null; + string stack = null; + string actualMessage = null; + try { + action (a); + } + catch (Exception e) { + actualType = e.GetType (); + actualMessage = e.Message; + if (!object.Equals (actualType, exception)) + stack = e.ToString (); + } + if (!object.Equals (actualType, exception)) { + throw new InvalidOperationException ( + string.Format ("Assertion failed: Expected Exception Type {0}, got {1}.\n" + + "Actual Exception: {2}", exception, actualType, stack)); + } + if (!object.Equals (actualMessage, message)) + throw new InvalidOperationException ( + string.Format ("Assertion failed:\n\tExpected: {0}\n\t Actual: {1}", + message, actualMessage)); + } + + static void CheckWriteOptionDescriptions () + { + var p = new OptionSet () { + { "p|indicator-style=", "append / indicator to directories", v => {} }, + { "color:", "controls color info", v => {} }, + { "h|?|help", "show help text", v => {} }, + { "version", "output version information and exit", v => {} }, + }; + + StringWriter expected = new StringWriter (); + expected.WriteLine (" -p, --indicator-style=VALUE"); + expected.WriteLine (" append / indicator to directories"); + expected.WriteLine (" --color[=VALUE] controls color info"); + expected.WriteLine (" -h, -?, --help show help text"); + expected.WriteLine (" --version output version information and exit"); + + StringWriter actual = new StringWriter (); + p.WriteOptionDescriptions (actual); + + Assert (actual.ToString (), expected.ToString ()); + } + + static void CheckOptionBundling () + { + string a, b, c; + a = b = c = null; + var p = new OptionSet () { + { "a", v => a = "a" }, + { "b", v => b = "b" }, + { "c", v => c = "c" }, + }; + p.Parse (_ ("-abc")); + Assert (a, "a"); + Assert (b, "b"); + Assert (c, "c"); + } + + static void CheckHaltProcessing () + { + var p = new OptionSet () { + { "a", v => {} }, + { "b", v => {} }, + }; + List<string> e = p.Parse (_ ("-a", "-b", "--", "-a", "-b")); + Assert (e.Count, 2); + Assert (e [0], "-a"); + Assert (e [1], "-b"); + } + + static void CheckLocalization () + { + var p = new OptionSet (f => "hello!") { + { "n=", (int v) => { } }, + }; + AssertException (typeof(OptionException), "hello!", + p, v => { v.Parse (_("-n=value")); }); + + StringWriter expected = new StringWriter (); + expected.WriteLine (" -nhello! hello!"); + + StringWriter actual = new StringWriter (); + p.WriteOptionDescriptions (actual); + + Assert (actual.ToString (), expected.ToString ()); + } + + class CiOptionSet : OptionSet { + protected override void InsertItem (int index, Option item) + { + if (item.Prototype.ToLower () != item.Prototype) + throw new ArgumentException ("prototypes must be null!"); + base.InsertItem (index, item); + } + + protected override bool Parse (string option, OptionContext c) + { + if (c.Option != null) + return base.Parse (option, c); + string f, n, v; + if (!GetOptionParts (option, out f, out n, out v)) { + return base.Parse (option, c); + } + return base.Parse (f + n.ToLower () + (v != null ? "=" + v : ""), c); + } + + public new Option GetOptionForName (string n) + { + return base.GetOptionForName (n); + } + } + + static void CheckDerivedType () + { + bool help = false; + var p = new CiOptionSet () { + { "h|help", v => help = v != null }, + }; + p.Parse (_("-H")); + Assert (help, true); + help = false; + p.Parse (_("-HELP")); + Assert (help, true); + + Assert (p.GetOptionForName ("h"), p [0]); + Assert (p.GetOptionForName ("help"), p [0]); + Assert (p.GetOptionForName ("invalid"), null); + + AssertException (typeof(ArgumentException), "prototypes must be null!", + p, v => { v.Add ("N|NUM=", (int n) => {}); }); + AssertException (typeof(ArgumentNullException), + "Argument cannot be null.\nParameter name: option", + p, v => { v.GetOptionForName (null); }); + } + + static void CheckOptionContext () + { + var p = new OptionSet () { + { "a=", "a desc", (v,c) => { + Assert (v, "a-val"); + Assert (c.Option.Description, "a desc"); + Assert (c.OptionName, "/a"); + Assert (c.OptionIndex, 1); + Assert (c.OptionValue, v); + } }, + { "b", "b desc", (v, c) => { + Assert (v, "--b+"); + Assert (c.Option.Description, "b desc"); + Assert (c.OptionName, "--b+"); + Assert (c.OptionIndex, 2); + Assert (c.OptionValue, v); + } }, + { "c=", "c desc", (v, c) => { + Assert (v, "C"); + Assert (c.Option.Description, "c desc"); + Assert (c.OptionName, "--c"); + Assert (c.OptionIndex, 3); + Assert (c.OptionValue, v); + } }, + { "d", "d desc", (v, c) => { + Assert (v, null); + Assert (c.Option.Description, "d desc"); + Assert (c.OptionName, "/d-"); + Assert (c.OptionIndex, 4); + Assert (c.OptionValue, v); + } }, + }; + p.Parse (_("/a", "a-val", "--b+", "--c=C", "/d-")); + } + } +} +#endif + diff --git a/gendarme/console/IResultWriter.cs b/gendarme/console/ResultWriter.cs index 281990de..70124aa7 100644 --- a/gendarme/console/IResultWriter.cs +++ b/gendarme/console/ResultWriter.cs @@ -1,5 +1,5 @@ // -// IResultWriter interface +// ResultWriter base class // // Authors: // Christian Birkl <christian.birkl@gmail.com> @@ -28,20 +28,47 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -using System; -using System.Collections; - using Gendarme.Framework; -namespace Gendarme.Console.Writers { +namespace Gendarme { + + abstract public class ResultWriter { + + private IRunner runner; + private string filename; + + protected ResultWriter (IRunner runner, string fileName) + { + this.runner = runner; + this.filename = fileName; + } + + protected IRunner Runner { + get { return runner; } + } + + protected string FileName { + get { return filename; } + set { filename = value; } + } + + protected virtual void Start () + { + } - public interface IResultWriter { + protected virtual void Write () + { + } - void Start (); - void End (); + protected virtual void Finish () + { + } - void Write (IDictionary assemblies); - void Write (Rules rules); - void Write (Violation v); + public void Report () + { + Start (); + Write (); + Finish (); + } } } diff --git a/gendarme/console/TextResultWriter.cs b/gendarme/console/TextResultWriter.cs index 67dc8d55..4c22c1d9 100644 --- a/gendarme/console/TextResultWriter.cs +++ b/gendarme/console/TextResultWriter.cs @@ -29,70 +29,75 @@ // using System; -using System.Collections; +using System.Collections.Generic; using System.IO; +using System.Linq; + +using Mono.Cecil; using Gendarme.Framework; -namespace Gendarme.Console.Writers { +namespace Gendarme { - public class TextResultWriter : IResultWriter { + public class TextResultWriter : ResultWriter, IDisposable { private TextWriter writer; private bool need_closing; - private int index; - public TextResultWriter (string output) + public TextResultWriter (IRunner runner, string fileName) + : base (runner, fileName) { - if ((output == null) || (output.Length == 0)) + if ((fileName == null) || (fileName.Length == 0)) writer = System.Console.Out; else { - writer = new StreamWriter (output); + writer = new StreamWriter (fileName); need_closing = true; } } - public void Start () + protected override void Write () { - index = 0; - } + int index = 0; - public void End () - { - if (need_closing) - writer.Close (); - } + var query = from n in Runner.Defects + orderby n.Severity + select n; - public void Write (IDictionary assemblies) - { + foreach (Defect defect in query) { + IRule rule = defect.Rule; + + writer.WriteLine ("{0}. {1}", ++index, rule.Name); + writer.WriteLine (); + writer.WriteLine ("Problem: {0}", rule.Problem); + writer.WriteLine (); + writer.WriteLine ("Details [Severity: {0}, Confidence: {1}]", defect.Severity, defect.Confidence); + writer.WriteLine ("* Target: {0}", defect.Target); + writer.WriteLine ("* Location: {0}", defect.Location); + if (!String.IsNullOrEmpty (defect.Text)) + writer.WriteLine ("* {0}", defect.Text); + writer.WriteLine (); + writer.WriteLine ("Solution: {0}", rule.Solution); + writer.WriteLine (); + writer.WriteLine ("More info available at: {0}", rule.Uri.ToString ()); + writer.WriteLine (); + writer.WriteLine (); + } } - public void Write (Rules rules) + public void Dispose () { + Dispose (true); + GC.SuppressFinalize (this); } - public void Write (Violation v) + protected virtual void Dispose (bool disposing) { - RuleInformation ri = RuleInformationManager.GetRuleInformation (v.Rule); - writer.WriteLine ("{0}. {1}", ++index, ri.Name); - writer.WriteLine (); - writer.WriteLine ("Problem: {0}", String.Format (ri.Problem, v.Violator)); - writer.WriteLine (); - if (v.Messages != null && v.Messages.Count > 0) { - writer.WriteLine ("Details:"); - foreach (Message message in v.Messages) { - writer.WriteLine (" {0}", message); + if (disposing) { + if (need_closing) { + writer.Close (); + writer.Dispose (); } - writer.WriteLine (); - } - writer.WriteLine ("Solution: {0}", String.Format (ri.Solution, v.Violator)); - writer.WriteLine (); - string url = ri.Uri; - if (url.Length > 0) { - writer.WriteLine ("More info available at: {0}", url); - writer.WriteLine (); } - writer.WriteLine (); } } } diff --git a/gendarme/console/XmlResultWriter.cs b/gendarme/console/XmlResultWriter.cs index be34fb25..0c5788bc 100644 --- a/gendarme/console/XmlResultWriter.cs +++ b/gendarme/console/XmlResultWriter.cs @@ -1,5 +1,5 @@ // -// ResultWriter +// XmlResultWriter // // Authors: // Christian Birkl <christian.birkl@gmail.com> @@ -29,29 +29,31 @@ // using System; -using System.Collections; +using System.Linq; using System.Text; using System.Xml; -using System.Xml.Serialization; -using Gendarme.Framework; using Mono.Cecil; -namespace Gendarme.Console.Writers { +using Gendarme.Framework; +using Gendarme.Framework.Rocks; + +namespace Gendarme { - public class XmlResultWriter : IResultWriter { + public class XmlResultWriter : ResultWriter, IDisposable { private XmlTextWriter writer; - public XmlResultWriter (string output) + public XmlResultWriter (IRunner runner, string fileName) + : base (runner, fileName) { - if ((output == null) || (output.Length == 0)) + if ((fileName == null) || (fileName.Length == 0)) writer = new XmlTextWriter (System.Console.Out); else - writer = new XmlTextWriter (output, Encoding.UTF8); + writer = new XmlTextWriter (fileName, Encoding.UTF8); } - public void Start () + protected override void Start () { writer.Formatting = Formatting.Indented; writer.WriteProcessingInstruction ("xml", "version='1.0'"); @@ -59,74 +61,101 @@ namespace Gendarme.Console.Writers { writer.WriteAttributeString ("date", DateTime.UtcNow.ToString ()); } - public void End () + protected override void Write () { + writer.WriteStartElement ("files"); + foreach (AssemblyDefinition assembly in Runner.Assemblies) { + writer.WriteStartElement ("file"); + writer.WriteAttributeString ("Name", assembly.Name.ToString ()); + IAnnotationProvider provider = (assembly as IAnnotationProvider); + if (provider.Annotations.Contains ("filename")) { + writer.WriteString (provider.Annotations ["filename"] as string); + } + writer.WriteEndElement (); + } writer.WriteEndElement (); - writer.Flush (); - writer.Close (); - writer = null; - } - public void Write (IDictionary assemblies) - { - foreach (DictionaryEntry de in assemblies) { - writer.WriteStartElement ("input"); - AssemblyDefinition ad = (de.Value as AssemblyDefinition); - if (ad != null) - writer.WriteAttributeString ("Name", ad.Name.ToString ()); - writer.WriteString ((string) de.Key); + writer.WriteStartElement ("rules"); + foreach (IRule rule in Runner.Rules) { + if (rule is IAssemblyRule) + WriteRule (rule, "Assembly"); + if (rule is ITypeRule) + WriteRule (rule, "Type"); + if (rule is IMethodRule) + WriteRule (rule, "Method"); + } + writer.WriteEndElement (); + + var query = from n in Runner.Defects + orderby n.Assembly.Name.FullName, n.Rule.Name + group n by n.Rule into a + select new { + Rule = a.Key, + Value = from o in a + group o by o.Target into r + select new { + Target = r.Key, + Value = r + } + }; + + writer.WriteStartElement ("results"); + foreach (var value in query) { + writer.WriteStartElement ("rule"); + writer.WriteAttributeString ("Name", value.Rule.Name); + writer.WriteAttributeString ("Uri", value.Rule.Uri.ToString ()); + writer.WriteElementString ("problem", value.Rule.Problem); + writer.WriteElementString ("solution", value.Rule.Solution); + foreach (var v2 in value.Value) { + writer.WriteStartElement ("target"); + writer.WriteAttributeString ("Name", v2.Target.ToString ()); + writer.WriteAttributeString ("Assembly", v2.Target.GetAssembly ().Name.FullName); + foreach (var v3 in v2.Value) { + writer.WriteStartElement ("defect"); + writer.WriteAttributeString ("Severity", v3.Severity.ToString ()); + writer.WriteAttributeString ("Confidence", v3.Confidence.ToString ()); + writer.WriteAttributeString ("Location", v3.Location.ToString ()); + writer.WriteAttributeString ("Source", v3.Source); + writer.WriteString (v3.Text); + writer.WriteEndElement (); + } + writer.WriteEndElement (); + } writer.WriteEndElement (); } + writer.WriteEndElement (); } - public void Write (Rules rules) + protected override void Finish () { - writer.WriteStartElement ("rules"); - Rules ("Assembly", rules.Assembly); - Rules ("Module", rules.Module); - Rules ("Type", rules.Type); - Rules ("Method", rules.Method); writer.WriteEndElement (); + writer.Flush (); } - private void Rules (string type, RuleCollection rules) + private void WriteRule (IRule rule, string type) { - foreach (IRule rule in rules) { - RuleInformation info = RuleInformationManager.GetRuleInformation (rule); - writer.WriteStartElement ("rule"); - writer.WriteAttributeString ("Name", info.Name); - writer.WriteAttributeString ("Type", type); - writer.WriteAttributeString ("Uri", info.Uri); - writer.WriteString (rule.GetType ().FullName); - writer.WriteEndElement (); - } + writer.WriteStartElement ("rule"); + writer.WriteAttributeString ("Name", rule.Name); + writer.WriteAttributeString ("Type", type); + writer.WriteAttributeString ("Uri", rule.Uri.ToString ()); + writer.WriteString (rule.GetType ().FullName); + writer.WriteEndElement (); } - public void Write (Violation v) + public void Dispose () { - RuleInformation ri = RuleInformationManager.GetRuleInformation (v.Rule); - - writer.WriteStartElement ("violation"); - writer.WriteAttributeString ("Assembly", v.Assembly.ToString ()); - writer.WriteAttributeString ("Name", ri.Name); - writer.WriteAttributeString ("Uri", ri.Uri); - writer.WriteElementString ("problem", String.Format (ri.Problem, v.Violator)); - writer.WriteElementString ("solution", String.Format (ri.Solution, v.Violator)); - - if ((v.Messages != null) && (v.Messages.Count > 0)) { - writer.WriteStartElement ("messages"); - foreach (Message message in v.Messages) { - writer.WriteStartElement ("message"); - if (message.Location != null) - writer.WriteAttributeString ("Location", message.Location.ToString ()); - writer.WriteAttributeString ("Type", message.Type.ToString ()); - writer.WriteString (message.Text); - writer.WriteEndElement (); + Dispose (true); + GC.SuppressFinalize (this); + } + + protected virtual void Dispose (bool disposing) + { + if (disposing) { + if (writer != null) { + writer.Close (); + writer = null; } - writer.WriteEndElement (); } - - writer.WriteEndElement (); } } } diff --git a/gendarme/console/gendarme.xsl b/gendarme/console/gendarme.xsl index ac7cfc7f..1c328128 100644 --- a/gendarme/console/gendarme.xsl +++ b/gendarme/console/gendarme.xsl @@ -1,166 +1,181 @@ -<?xml version="1.0" encoding="iso-8859-1" ?>
-<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
- <xsl:output method="html" encoding="iso-8859-1" />
- <xsl:template name="print-defect-rules">
+<?xml version="1.0" encoding="iso-8859-1" ?> +<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> + <xsl:output method="html" encoding="iso-8859-1" /> + <xsl:template name="print-defect-rules"> <xsl:param name="name" /> - : <xsl:value-of select="count(//violation[@Name = $name])" /> defects
- </xsl:template>
- <xsl:template name="print-rules">
- <xsl:param name="type" />
- <p>
- <b><xsl:value-of select="$type" /></b>:
- <xsl:choose>
- <xsl:when test="count(rules/rule[@Type = $type]) = 0">
- <ul>
- <li>None</li>
- </ul>
- </xsl:when>
- <xsl:otherwise>
- <ul>
- <xsl:for-each select="rules/rule[@Type = $type]">
+ : <xsl:value-of select="count(//rule[@Name = $name]/target/defect)" /> defects + </xsl:template> + <xsl:template name="print-rules"> + <xsl:param name="type" /> + <p> + <b><xsl:value-of select="$type" /></b>: + <xsl:choose> + <xsl:when test="count(rules/rule[@Type = $type]) = 0"> + <ul> + <li>None</li> + </ul> + </xsl:when> + <xsl:otherwise> + <ul> + <xsl:for-each select="rules/rule[@Type = $type]"> <li> <a href="{@Uri}" target="{@Name}"><xsl:value-of select="text()" /></a> - <xsl:call-template name="print-defect-rules">
+ <xsl:call-template name="print-defect-rules"> <xsl:with-param name="name"> <xsl:value-of select="@Name" /> - </xsl:with-param>
- </xsl:call-template>
- </li>
- </xsl:for-each>
- </ul>
- </xsl:otherwise>
- </xsl:choose>
- </p>
- </xsl:template>
- <xsl:template match="/">
- <xsl:for-each select="gendarme-output">
- <html>
- <head>
- <title>Gendarme Report</title>
- </head>
- <style type="text/css">
- h1, h2, h3 {
- font-family: Verdana;
- color: #68892F;
- }
- h2 {
- font-size: 14pt;
- }
-
- p, li, b {
- font-family: Verdana;
- font-size: 11pt;
- }
- p.where, p.problem, p.found, p.solution {
- background-color: #F6F6F6;
- border: 1px solid #DDDDDD;
- padding: 10px;
+ </xsl:with-param> + </xsl:call-template> + </li> + </xsl:for-each> + </ul> + </xsl:otherwise> + </xsl:choose> + </p> + </xsl:template> + <xsl:template match="/"> + <xsl:for-each select="gendarme-output"> + <html> + <head> + <title>Gendarme Report</title> + </head> + <style type="text/css"> + h1, h2, h3 { + font-family: Verdana; + color: #68892F; + } + h2 { + font-size: 14pt; + } + + p, li, b { + font-family: Verdana; + font-size: 11pt; + } + p.where, p.problem, p.found, p.solution { + background-color: #F6F6F6; + border: 1px solid #DDDDDD; + padding: 10px; } span.found { - padding: 10px;
- }
- div.toc {
- background-color: #F6F6F6;
- border: 1px solid #DDDDDD;
- padding: 10px;
- float: right;
- width: 300px;
- }
- a:link, a:active, a:hover, a:visited {
- color: #9F75AD;
- font-weight: bold;
- text-decoration: none;
- }
- </style>
- <body>
-
- <h1>Gendarme Report</h1>
+ margin-left: 10px; + } + div.toc { + background-color: #F6F6F6; + border: 1px solid #DDDDDD; + padding: 10px; + float: right; + width: 300px; + } + a:link, a:active, a:hover, a:visited { + color: #9F75AD; + font-weight: bold; + text-decoration: none; + } + </style> + <body> + + <h1>Gendarme Report</h1> <p>Produced on <xsl:value-of select="@date" /> UTC.</p> -
- <div class="toc">
- <div align="center">
- <b style="font-size: 10pt;">Table of contents</b>
- </div>
- <p style="font-size: 10pt;">
- <a href="#s1">1  Summary</a><br />
- <a href="#s1_1">  1.1  List of assemblies searched</a><br />
- <a href="#s1_2">  1.2  List of rules used</a><br />
- <a href="#s2">2  Reported defects</a><br />
- </p>
- </div>
- <h1><a name="s1">Summary</a></h1>
-
- <p>
- <h2>List of assemblies analyzed</h2>
- <ul>
- <xsl:for-each select="input">
- <xsl:variable name="assembly"><xsl:value-of select="@Name" /></xsl:variable>
-
- <li><xsl:value-of select="text()" />: <xsl:value-of select="count(//violation[@Assembly = $assembly])" /> defects</li>
- </xsl:for-each>
- </ul>
- </p>
-
- <p>
- <h2>List of rules used</h2>
-
- <xsl:call-template name="print-rules">
- <xsl:with-param name="type">Assembly</xsl:with-param>
- </xsl:call-template>
-
- <xsl:call-template name="print-rules">
- <xsl:with-param name="type">Module</xsl:with-param>
- </xsl:call-template>
-
- <xsl:call-template name="print-rules">
- <xsl:with-param name="type">Type</xsl:with-param>
- </xsl:call-template>
-
- <xsl:call-template name="print-rules">
- <xsl:with-param name="type">Method</xsl:with-param>
- </xsl:call-template>
- </p>
-
- <h1><a name="s2">Reported Defects</a></h1>
-
- <p>
- <xsl:for-each select="violation"> + + <div class="toc"> + <div align="center"> + <b style="font-size: 10pt;">Table of contents</b> + </div> + <p style="font-size: 10pt;"> + <a href="#s1">1  Summary</a><br /> + <a href="#s1_1">  1.1  List of assemblies searched</a><br /> + <a href="#s1_2">  1.2  List of rules used</a><br /> + <a href="#s2">2  Reported defects</a><br /> + <xsl:for-each select="results/rule"> + <a href="#{@Name}">  2.<xsl:value-of select="position()" /> <xsl:value-of select="@Name" /><br /> + </a> + </xsl:for-each> + </p> + </div> + <h1><a name="s1">Summary</a></h1> + <p> + <a href="http://www.mono-project.com/Gendarme">Gendarme</a> found <xsl:value-of select="count(//rule/target/defect)" /> defects using <xsl:value-of select="count(//rules/rule)" /> rules. + </p> + <p> + <h2>List of assemblies analyzed</h2> + <ul> + <xsl:for-each select="files/file"> + <xsl:variable name="file"> + <xsl:value-of select="@Name" /> + </xsl:variable> + + <li><xsl:value-of select="text()" />: <xsl:value-of select="count(//target[@Assembly = $file])" /> defects</li> + </xsl:for-each> + </ul> + </p> + + <p> + <h2>List of rules used</h2> + + <xsl:call-template name="print-rules"> + <xsl:with-param name="type">Assembly</xsl:with-param> + </xsl:call-template> + + <xsl:call-template name="print-rules"> + <xsl:with-param name="type">Type</xsl:with-param> + </xsl:call-template> + + <xsl:call-template name="print-rules"> + <xsl:with-param name="type">Method</xsl:with-param> + </xsl:call-template> + </p> + + <h1><a name="s2">Reported Defects</a></h1> + + <p> + <xsl:for-each select="results/rule"> <h3><xsl:value-of select="position()" />  + <a name="{@Name}" /> <a href="{@Uri}" target="{@Name}"> <xsl:value-of select="@Name" /> </a> </h3> - <b>Problem:</b>
- <p class="problem">
- <xsl:value-of select="problem" />
+ <b>Problem:</b> + <p class="problem"> + <xsl:value-of select="problem" /> </p> <b>Found in:</b> - <p class="found"> - Assembly Qualified Name: <i><xsl:value-of select="@Assembly" /></i><br/> - <xsl:if test="count(messages/message) != 0">
- <xsl:for-each select="messages/message"> - <br/> + <xsl:if test="count(target) != 0"> + <xsl:for-each select="target"> + <p class="found"> + <b>Target:</b> <xsl:value-of select="@Name" /><br/> + <b>Assembly:</b> <xsl:value-of select="@Assembly" /><br/> + <xsl:for-each select="defect"> <!-- FIXME: use different color/style for warnings versus errors --> - <b>Location:</b> <xsl:value-of select="@Location" /> - <br/>
- <span class="found"> - <xsl:value-of select="." /> - </span>
- <br/> - </xsl:for-each> - </xsl:if>
- </p> + <span class="found"> + <br/> + <b>Severity:</b> <xsl:value-of select="@Severity" />  + <b>Confidence:</b> <xsl:value-of select="@Confidence" /><br/> + <xsl:if test="@Location != (../@Name)"> + <b>Location:</b> <xsl:value-of select="@Location" /><br/> + </xsl:if> + <xsl:if test="string-length(@Source) > 0"> + <b>Source:</b> <xsl:value-of select="@Source" /><br/> + </xsl:if> + <xsl:if test="string-length(.) > 0"> + <b>Details:</b> <xsl:value-of select="." /><br/> + </xsl:if> + </span> + </xsl:for-each> + </p> + </xsl:for-each> + </xsl:if> - <b>Solution:</b>
- <p class="solution">
- <xsl:value-of select="solution" />
- </p>
- </xsl:for-each>
- </p>
- </body>
- </html>
- </xsl:for-each>
- </xsl:template>
+ <b>Solution:</b> + <p class="solution"> + <xsl:value-of select="solution" /> + </p> + </xsl:for-each> + </p> + </body> + </html> + </xsl:for-each> + </xsl:template> </xsl:stylesheet> |