Customization

Overview

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.

Writing Custom Rules

Overview

Example

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>

Writing Custom Formatters

Overview

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).

Example

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>