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

  • Create a class that derives from JelleDruyts.BuildCop.Rules.BaseRule.
    • Re-implement the base class constructor (simply pass through to the base class constructor).
    • Override the Check method to analyze an MSBuild project file and return log entries.
    • If your rule has configuration information, retrieve the strongly typed configuration instance using the generic GetTypedConfiguration method.
    • When creating log entries, you should pass the Name property of your rule as the rule argument for the log entry's constructor.
  • If your rule requires configuration:
    • Create a root configuration element that inherits from JelleDruyts.BuildCop.Configuration.RuleConfigurationElement.
    • Re-implement the two base class constructors (simply pass through to the base class constructors).
    • Inside the class, use the standard .NET 2.0 Configuration infrastructure (i.e. ConfigurationElement and ConfigurationElementCollection classes) to define the actual configuration information.
    • Decorate your custom rule class with [BuildCopRule(ConfigurationType = typeof(your configuration root type))] to associate the rule with the configuration type.
  • Copy the assembly containing your rule to the BuildCop directory.
  • Register your rule in the configuration file using its fully qualified name, and provide the necessary configuration information.

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

  • Create a class that derives from JelleDruyts.BuildCop.Formatters.BaseFormatter.
    • Re-implement the base class constructor (simply pass through to the base class constructor).
    • Override the WriteReport method to write the given report in whatever way you want.
    • If your formatter has configuration information, retrieve the strongly typed configuration instance using the generic GetTypedConfiguration method.
    • To retrieve the log entries to write (taking the minimum log level into account), call the FindLogEntries method of the given BuildCopReport instance.
  • If your formatter requires configuration:
    • Create a root configuration element that inherits from JelleDruyts.BuildCop.Configuration.FormatterConfigurationElement.
    • Re-implement the two base class constructors (simply pass through to the base class constructors).
    • Inside the class, use the standard .NET 2.0 Configuration infrastructure (i.e. ConfigurationElement and ConfigurationElementCollection classes) to define the actual configuration information.
    • Decorate your custom formatter class with [BuildCopFormatter(ConfigurationType = typeof(your configuration root type))] to associate the rule with the configuration type.
  • Copy the assembly containing your formatter to the BuildCop directory.
  • Register your formatter in the configuration file using its fully qualified name, and provide the necessary configuration information.

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>

Last edited Feb 5, 2008 at 8:11 PM by jelled, version 1

Comments

No comments yet.