BuildCop is designed with extensibility in mind: its use would be very restricted if it weren't possible to write custom rules or formatters. Luckily, customization is quite straight-forward if you are a .NET developer, so feel free to extend the tool and send me any custom classes you have created!
If the documentation would not suffice, you can always look at the source code for the existing rules (they have no special tricks or privileges) to get more insight in the way it works. If you would still get stuck in any way, though, please let me know and I'll clear up whatever isn't obvious right now.
As for the technical details: BuildCop is written on .NET 2.0 so all you need is
a compiler and a reference to the JelleDruyts.BuildCop.dll
assembly,
which contains all the required classes needed for customization.
JelleDruyts.BuildCop.Rules.BaseRule
.
Check
method to analyze an MSBuild project file and return
log entries.GetTypedConfiguration
method.Name
property of your
rule as the rule
argument for the log entry's constructor.JelleDruyts.BuildCop.Configuration.RuleConfigurationElement
.
ConfigurationElement
and ConfigurationElementCollection
classes)
to define the actual configuration information.[BuildCopRule(ConfigurationType = typeof(your
configuration root type))]
to associate the rule with the configuration
type.Below is a sample implementation of a custom rule that raises errors if the project's assembly name contains forbidden words:
[BuildCopRule(ConfigurationType = typeof(ForbiddenWordsRuleElement))] public class ForbiddenWordsRule : BaseRule { public ForbiddenWordsRule(RuleConfigurationElement configuration) : base(configuration) { } public override IList<LogEntry> Check(BuildFile project) { ForbiddenWordsRuleElement config = this.GetTypedConfiguration<ForbiddenWordsRuleElement>(); List<LogEntry> entries = new List<LogEntry>(); foreach (string forbiddenWord in config.Words.ForbiddenWords.Split(';')) { if (project.AssemblyName.IndexOf(forbiddenWord, StringComparison.OrdinalIgnoreCase) >= 0) { string message = "The assembly name contains a forbidden word."; string detail = string.Format("The assembly name \"{0}\" contains the forbidden word \"{1}\".", project.AssemblyName, forbiddenWord); entries.Add(new LogEntry(this.Name, "ForbiddenWord", LogLevel.Error, message, detail)); } } return entries; } }
The configuration classes for this rule would be defined as follows:
public class ForbiddenWordsRuleElement : RuleConfigurationElement { public ForbiddenWordsRuleElement() { } public ForbiddenWordsRuleElement(XmlReader reader) : base(reader) { } [ConfigurationProperty("words", IsRequired = true)] public WordsElement Words { get { return (WordsElement)base["words"]; } } } public class WordsElement : ConfigurationElement { [ConfigurationProperty("forbidden", IsRequired = true)] public string ForbiddenWords { get { return (string)base["forbidden"]; } set { base["forbidden"] = value; } } }
The configuration file would then contain a rule definition such as the following:
<rule name="ForbiddenWordsRule" type="CustomRules.ForbiddenWordsRule, CustomRules"> <words forbidden="hack;crack;dummy" /> </rule>
JelleDruyts.BuildCop.Formatters.BaseFormatter
.
WriteReport
method to write the given report in whatever
way you want.GetTypedConfiguration
method.FindLogEntries
method of the given BuildCopReport
instance.JelleDruyts.BuildCop.Configuration.FormatterConfigurationElement
.
ConfigurationElement
and ConfigurationElementCollection
classes)
to define the actual configuration information.[BuildCopFormatter(ConfigurationType
= typeof(your configuration root type))]
to associate the rule with
the configuration type.Note that you can build formatter and configuration classes from scratch as shown
above, but in the very common case that you want to write to a file, it is also
possible to use the existing FilebasedFormatter
base class with its
associated FilebasedFormatterElement
configuration base class, which
already provide you with a file name and the code that launches the file after the
report has been written (if so desired by the user). In that case, you only have
to worry about writing the actual file as in the example below. Likewise, if you
require file-based output that involves an XSLT, you can use the XsltFilebasedFormatter
and XsltFilebasedFormatterElement
base classes, which adds a stylesheet
attribute to the configuration. In both cases, you must now override the WriteReportCore
method instead of WriteReport
(to allow the base class to launch the
file after it is written).
Below is a sample implementation of a custom formatter that writes to a text file.
[BuildCopFormatter(ConfigurationType = typeof(FilebasedFormatterElement))] public class TextFileFormatter : FilebasedFormatter { public TextFileFormatter(FormatterConfigurationElement configuration) : base(configuration) { } protected override void WriteReportCore(BuildCopReport report, LogLevel minimumLogLevel) { FilebasedFormatterElement configuration = this.GetTypedConfiguration<FilebasedFormatterElement>(); string fileName = configuration.Output.FileName; using (FileStream outputStream = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.Read)) using (StreamWriter writer = new StreamWriter(outputStream)) { foreach (BuildGroupReport groupReport in report.BuildGroupReports) { foreach (BuildFileReport fileReport in groupReport.BuildFileReports) { foreach (LogEntry entry in fileReport.FindLogEntries(minimumLogLevel)) { writer.WriteLine("{0} - {1} - {2} - {3} - {4} - {5} - {6}", groupReport.BuildGroupName, fileReport.FileName, entry.Level.ToString(), entry.Rule, entry.Code, entry.Message, entry.Detail); } } } } } }
The configuration file would then contain a formatter definition such as the following:
<formatter name="Text" type="CustomFormatters.TextFormatter, CustomFormatters"> <output fileName="out.txt" launch="true" /> </formatter>