2013年7月26日金曜日

FxCop カスタムルールの作成(ビルドまで)

ADO.netのDataSetをLINQableに書くために (本題) ~ FxCopカスタムルールを作成する。」の続きです。今度こそカスタムルールを作ります。ただ、手元にVS2012Expressしかないので、これで作ります。

前エントリで書いたとおり、アセンブリを静的分析し、以下のパターンに適合する場合は警告を出すように作ります。

イマイチ好みじゃないずいぶんマシ
foeach構文でDataTable.Rowsプロパティを使う。DataTable.AsEnumerable拡張メソッドまたはDataTable.Selectメソッドを使う。
LINQを使うために、DataTable.RowsプロパティにEnumerable.Cast<DataRow>拡張メソッドを使う。DataTable.AsEnumerable拡張メソッドまたはDataTable.Selectメソッドを使う。
DataRowCollectionクラスのインデクサを使う。DataTable.AsEnumerable拡張メソッドまたはDataTable.Selectメソッドを使う。(インデクサは使わない。)
DataRowクラスのインデクサを使う。DataRow.Field拡張メソッドDataRow.SetField拡張メソッドを使う。

VS2012Express(今回はDesktopを使いました。)を開き、新規プロジェクトを作ります。「クラスライブラリ」を選択し、プロジェクト名は「DataTableRules」とでもしておきましょう。


作成したプロジェクトに、FxCopのアセンブリを参照するようにします。プロジェクトの「参照設定」を右クリックし、「参照の追加」を選択します。

プロジェクト外の、GACにもない外部アセンブリを参照するので、「参照」ボタンをクリックします。
Program FilesのVSのフォルダの、「Team Tools\Static Analysis Tools\FxCop」にある2つのアセンブリファイル、「FxCopSdk.dll」と「Microsoft.Cci.dll」を選択して追加します。


作成したプロジェクトには、警告メッセージなどを管理するためのXMLリソースが必要になります。
ここでは「CustomRules.xml」というファイルをプロジェクトに追加します。プロジェクトを右クリックして「新規作成」-「追加」-「新しい項目」を選び、XMLファイルをプロジェクトに追加します。


  追加したXMLファイルはリソースとして扱います。ソリューションエクスプローラーで追加したXMLファイルを選び、プロパティで「ビルドアクション」を「埋め込まれたリソース」に変更します。


XMLの中身の編集は後でやります。

さて、前述の警告出力パターンですが、どうすれば検知できるかを考えてみます。

まず、「foeach構文でDataTable.Rowsプロパティを使う。」ですが、これは「DataTable.Rows.GetEnumerable」メソッドが使われたときに警告を出す。と考えればよさそう。

「DataTable.RowsプロパティにEnumerable.Cast<DataRow>拡張メソッドを使う。」は、そのまんま「Enumerable.Cast<DataRow>」静的メソッドが使われたときに警告を出すでオケ。

「DataRowCollectionクラスのインデクサを使う。」はインデクサの呼び出しですが、CLI的には「DataRowCollection.get_Item」メソッドが使われた時。という扱いになるようです。

「DataRowクラスのインデクサを使う。」も同様に、「DataRow.get_Item」または「DataRow.set_Item」メソッドが使われた時。となるようです。

これら4つのルールすべてにおいて、メソッドの呼び出しを確認することで検知可能ということになります。そこで、まずは基本となる抽象クラスを作ります。

プロジェクト作成時に作られるプレースホルダのクラス「Class1.cs」を使いましょう。まずはファイル名を「BaseDataTableRule.cs」に修正、クラス名もそれに合わせて変更します。内容は「すべてのメソッドを確認する」的な内容になっている…と思われます。なにせドキュメントが少なく、いろいろ試してうまくいっているだけなので、定かじゃないところも残ってます。

using Microsoft.FxCop.Sdk;

namespace DataTableRules
{
  public abstract class BaseDataTableRule : BaseIntrospectionRule
  {
    protected BaseDataTableRule(string name) : base(
      name, "DataTableRules.CustomRules", typeof(BaseDataTableRule).Assembly) {}

    public override ProblemCollection Check(Member member)
    {
      var m = member as Method;
      if (m != null)
        Visit(m);

      return Problems;
    }
  }
}

抽象クラスを作ったら、1ルールにつき1クラスとなるように、4つのクラスを作り抽象クラスから派生させます。まず「foeach構文でDataTable.Rowsプロパティを使う。」を検出するルール。「NoUseGetEnumeratorMethodToDataTableRows」としました。長いけどこのクラスを誰か人に使ってもらうわけではないので、今回は気にしない。名前がわかりやすいほうが重要。

using System;
using Microsoft.FxCop.Sdk;

namespace DataTableRules
{
  public class NoUseGetEnumeratorMethodToDataTableRows : BaseDataTableRule
  {
    public NoUseGetEnumeratorMethodToDataTableRows()
      : base("NoUseGetEnumeratorMethodToDataTableRows") {}

    public override void VisitMethodCall(MethodCall call)
    {
      var binding = call.Callee as MemberBinding;
      var target = binding.TargetObject;
      var member = binding.BoundMember;

      if (member.Name.Name == "GetEnumerator" && 
          target.Type.FullName == "System.Data.DataRowCollection")
      {
        Problems.Add(new Problem(GetResolution()));
      }

      base.VisitMethodCall(call);
    }
  }
}

2つめのクラスは「DataTable.RowsプロパティにEnumerable.Cast<DataRow>拡張メソッドを使う。」これを「NoUseCastMethodToDataTableRows」クラスとして実装します。(18行目と19行目に分けているのは、Web上で表示させるのに横長になりすぎるためで、本来分ける必要はありません。)

using System;
using Microsoft.FxCop.Sdk;

namespace DataTableRules
{
  public class NoUseCastMethodToDataTableRows : BaseDataTableRule
  {
    public NoUseCastMethodToDataTableRows()
      : base("NoUseCastMethodToDataTableRows") {}

    public override void VisitMethodCall(MethodCall call)
    {
      var binding = call.Callee as MemberBinding;
      var member = binding.BoundMember;

      if (member.FullName == 
        "System.Linq.Enumerable.Cast<System.Data.DataRow>"
        + "(System.Collections.IEnumerable)")
      {
        Problems.Add(new Problem(GetResolution()));
      }

      base.VisitMethodCall(call);
    }
  }
}

そして3つめのクラスは「DataRowCollectionクラスのインデクサを使う。」を「NoUseIndexerToDataTableRows」クラスとして実装します。

using System;
using Microsoft.FxCop.Sdk;

namespace DataTableRules
{
  public class NoUseIndexerToDataTableRows : BaseDataTableRule
  {
    public NoUseIndexerToDataTableRows()
      : base("NoUseIndexerToDataTableRows") {}

    public override void VisitMethodCall(MethodCall call)
    {
      var binding = call.Callee as MemberBinding;
      var member = binding.BoundMember;

      if (member.FullName.StartsWith(
         "System.Data.DataRowCollection.get_Item(", StringComparison.Ordinal))
      {
        Problems.Add(new Problem(GetResolution()));
      }

      base.VisitMethodCall(call);
    }
  }
}

そして4つめ。「DataRowクラスのインデクサを使う。」を「NoUseIndexerToDataRow」として実装します。

using System;
using Microsoft.FxCop.Sdk;

namespace DataTableRules
{
  public class NoUseIndexerToDataRow : BaseDataTableRule
  {
    public NoUseIndexerToDataRow() : base("NoUseIndexerToDataRow") {}

    public override void VisitMethodCall(MethodCall call)
    {
      var binding = call.Callee as MemberBinding;
      var member = binding.BoundMember;

      if (member.FullName.StartsWith(
            "System.Data.DataRow.get_Item(", StringComparison.Ordinal) ||
          member.FullName.StartsWith(
            "System.Data.DataRow.set_Item(", StringComparison.Ordinal))
      {
        Problems.Add(new Problem(GetResolution()));
      }

      base.VisitMethodCall(call);
    }
  }
}

あとはXMLリソースの中身の編集です。長いですが、こんな感じ。(適宜改行してます。)
<?xml version="1.0" encoding="utf-8" ?>
<Rules FriendlyName="データテーブルの使い方の規則">
<Rule TypeName="NoUseCastMethodToDataTableRows"
      Category="My.DataTable" CheckId="DT0001">
<Name>DataTable.RowsにCast&lt;DataRow&gt;()拡張メソッドを使用しません</Name>
<Description>可読性の向上のため、DataTable.RowsにCast&lt;DataRow&gt;拡張メソッド
を使用しません。</Description>
<Url />
<Resolution>DataTable.RowsにCast&lt;DataRow&gt;拡張メソッドが使用されています。
さらなる条件の絞り込みや、ソートが必要であればDataTable.Select()メソッドを、
そうでなければDataTable.AsEnumerable()拡張メソッドを使用してください。
</Resolution>
<Email />
<MessageLevel Certainty="80">Warning</MessageLevel>
<FixCategories>NonBreaking</FixCategories>
<Owner />
</Rule>
<Rule TypeName="NoUseGetEnumeratorMethodToDataTableRows"
      Category="My.DataTable" CheckId="DT0002">
<Name>foreach構文でDataTable.Rowsを使用した列挙を行いません</Name>
<Description>LINQとの親和性向上のため、DataTable.Rows.GetEnumerable()メソッドを
使用しません。</Description>
<Url />
<Resolution>DataTable.Rows.GetEnumerable()が使用されています。さらなる条件の
絞り込みや、ソートが必要であればDataTable.Select()メソッドを、そうでなければ
DataTable.AsEnumerable()拡張メソッドを使用してください。</Resolution>
<Email />
<MessageLevel Certainty="80">Warning</MessageLevel>
<FixCategories>NonBreaking</FixCategories>
<Owner />
</Rule>
<Rule TypeName="NoUseIndexerToDataTableRows"
      Category="My.DataTable" CheckId="DT0003">
<Name>DataTable.Rowsのインデクサを使用しません</Name>
<Description>可読性の向上のため、DataTable.Rowsのインデクサを使用しません。
</Description>
<Url />
<Resolution>DataTable.Rowsのインデクサが使用されています。テーブル内の各行を列挙
する場合には、DataTable.Select()メソッド、またはDataTable.AsEnumerable()拡張
メソッドを使用してforeach構文で列挙してください。 先頭行のみ取り出す場合には、
DataTable.AsEnumerable()拡張メソッドと、Enumerable.First()を使用して一時変数に
代入後、使用してください。
      例:var firstRow = dt.AsEnumerable().First();</Resolution>
<Email />
<MessageLevel Certainty="80">Warning</MessageLevel>
<FixCategories>NonBreaking</FixCategories>
<Owner />
</Rule>
<Rule TypeName="NoUseIndexerToDataRow"
      Category="My.DataTable" CheckId="DT0004">
<Name>DataRowのインデクサを使用しません</Name>
<Description>可読性の向上のため、DataRowのインデクサを使用しません。
</Description>
<Url />
<Resolution>DataRowのインデクサが使用されています。テーブル内の各行のカラムに値
を設定、または取得する場合は、DataRow.SetField&lt;T&gt;()拡張メソッド、および
DataRow.Field&lt;T&gt;()拡張メソッドを使用してください。</Resolution>
<Email />
<MessageLevel Certainty="80">Warning</MessageLevel>
<FixCategories>NonBreaking</FixCategories>
<Owner />
</Rule>
</Rules>

ふぅ。これで準備完了。のはず。後はビルド~配置~デバッグ。です。

が、もう限界。眠い。またまた続きにします。しくしく。いい加減終わらせたかったけどなぁ。

0 件のコメント:

コメントを投稿