内部のPVS-Studiofor Java:診断開発



変更点として、本日、PVS-StudioJavaの診断ルールを開発および確定するプロセスについて少し説明します。古いアナライザーのトリガーがリリースごとにあまり変動せず、新しいものがあまりクレイジーではない理由を見てみましょう。また、「ジャビストの計画は何か」を少し台無しにし、次のリリースの診断を使用して見つかったいくつかの美しい(そうではない)エラーを示します。



診断およびSelfTester開発プロセス



当然、新しい診断ルールはそれぞれアイデアから始まります。また、JavaアナライザーはPVS-Studioの開発における最年少の方向性であるため、基本的にこれらのアイデアをC / C ++およびC#部門から盗みます。しかし、すべてがそれほど悪いわけではありません。私たちは、後で同じ部門が私たちからそれらを盗むように、私たち自身によって発明されたルールも追加します(ユーザーによるものを含む-ありがとう!)。彼らが言うように、サイクル。



ほとんどの場合、コードでのルールの実装そのものが、かなりのパイプラインタスクであることがわかります。いくつかの合成例を使用してファイルを作成し、エラーが発生する場所を手でマークし、準備ができたらデバッガーを使用して、飽きて発明されたすべてのケースをカバーするまで構文ツリーを実行します。ルールがとてつもなく単純であることが判明する場合があります(たとえば、V6063文字通り数行で構成されています)、時にはロジックについて十分に長く考える必要があります。



しかし、これはほんの始まりに過ぎません。ご存知のように、実際のプロジェクトでのアナライザートリガーのタイプをほとんど反映していないため、合成例は特に好きではありません。ちなみに、ユニットテストのこれらの例の大部分は実際のプロジェクトから引用されています-考えられるすべてのケースを自分で発明することはほとんど不可能です。また、ユニットテストでは、ドキュメントの例のトリガーを失うことはありません。前例がありました、はい、shhだけです。



したがって、実際のプロジェクトのポジティブな点は、最初に何らかの方法で見つける必要があります。また、どういうわけかそれを確認する必要があります:



  • 「興味深い」解決策が一般的であるオープンソースの狂気には、このルールは当てはまりません。
  • ( - , );
  • data-flow ( ) - ;
  • open-source ;
  • over 9000%;
  • "" , ;
  • .


一般的に、ここでは、馬に乗った騎士のように(少し足を引きずっていますが、私たちはそれに取り組んでいます)、SelfTesterが前面に出てきます。その主で唯一のタスクは、一連のプロジェクトを自動的にチェックし、バージョン制御システムの「参照」に関連して追加、非表示、または変更されたトリガーを表示することです。アナライザーレポートの差分を提供し、プロジェクト内の対応するコードを簡単に表示します。現在、SelfTester for Javaは、ひげを生やしたバージョンの62のオープンソースプロジェクトをテストしています。その中には、たとえばDBeaver、Hibernate、Springなどがあります。すべてのプロジェクトを1回完全に実行するには、2〜2.5時間かかります。これは間違いなく苦痛ですが、何もできません。





上のスクリーンショットでは、「グリーン」プロジェクトは何も変更されていないプロジェクトです。 「赤」のプロジェクトの各差異は手動で確認され、正しい場合は、同じ「承認」ボタンで確認されます。ちなみに、アナライザー配布キットは、SelfTesterが純粋な緑色の結果を出した場合にのみ構築されます。一般に、これは、異なるバージョン間で結果の一貫性を維持する方法です。



結果の一貫性を維持することに加えて、SelfTesterを使用すると、診断がリリースされる前であっても、膨大な数の誤検知を取り除くことができます。典型的な開発パターンは次のようになります。



  • , . , " double-checked locking" ;
  • SelfTester-, ;
  • , -;
  • SelfTester- , ;
  • 3-4, ;
  • , , ( , );
  • , master.


幸い、SelfTesterが完全に実行されることは十分にまれであり、「2〜2.5時間」待つ必要はあまりありません。時々、運が迂回し、堺やアパッチハイブのような大規模なプロジェクトにトリガーが現れます-それはコーヒーを飲み、コーヒーを飲み、そしてコーヒーを飲む時です。ドキュメントを調べることもできますが、これはすべての人に適しているわけではありません。



「そのような魔法のツールがあるのに、なぜユニットテストが必要なのですか?」



そして、テストは大幅に高速化されます。数分-そしてすでに結果があります。また、ルールのどの部分が脱落したかを正確に確認することもできます。また、SelfTesterプロジェクトでは、ルールのすべての許容トリガーが常にキャッチされるわけではありませんが、それらの操作性もチェックする必要があります。



古い知人の新しい問題



当初、記事のこのセクションは、「SelfTesterのプロジェクトのバージョンはかなり古いため、表示されるエラーのほとんどは修正されている可能性が高い」という言葉で始まりました。しかし、これを確かめようと決心したとき、私は驚きました。すべての間違いはそのままでした。すべて。これは2つのことを意味します:



  • これらのエラーは、アプリケーションが機能するためにそれほど重要ではありません。ちなみに、それらの多くはテストコードに含まれており、誤ったテストは一貫性があるとは言えません。
  • これらのエラーは、開発者がほとんどアクセスしない大規模なプロジェクトの使用頻度の低いファイルに見られます。このため、誤ったコードは非常に長い間そこに存在する運命にあります。おそらく、それが原因で重大なバグが発生するまでです。


より深く掘り下げたい人のために、私たちがチェックしている特定のバージョンへのリンクがあります。



PS上記は、静的分析が未使用のコードの無害なエラーのみをキャッチすることを意味するものではありません。プロジェクトのリリース(およびほぼリリース)バージョンを確認します。開発者とテスター(場合によっては、残念ながらユーザー)が最も関連性の高いバグを手作業で見つけました。これは長く、費用がかかり、苦痛です。これについて詳しくは、記事「静的コード分析が使用されていないために検出できないエラー」を参照してください



ApacheDubboと空白のメニュー



GitHub



Diagnostics「V6080ミスプリントのチェックを検討してください。割り当てられた変数を次の条件でチェックする必要がある可能性があります」はバージョン7.08ですでにリリースされていますが、まだ記事に掲載されていないため、修正する必要があります。



Menu.java:40



public class Menu
{
  private Map<String, List<String>> menus = new HashMap<String, List<String>>();

  public void putMenuItem(String menu, String item)
  {
    List<String> items = menus.get(menu);
    if (item == null)                      // <=
    {
      items = new ArrayList<String>();
      menus.put(menu, items);
    }
    items.add(item);
  }
  ....
}


「キーコレクション」辞書の典型的な例と同様に古典的なタイプミス。開発者は、キーに対応するコレクションがまだ存在しない場合は作成したいと考えていましたが、変数の名前を混同し、メソッドの誤った操作だけでなく、最後の行にNullPointerException発生しました。Java 8以降の場合、このような辞書を実装するには、computeIfAbsentメソッドを使用します



public class Menu
{
  private Map<String, List<String>> menus = new HashMap<String, List<String>>();

  public void putMenuItem(String menu, String item)
  {
    List<String> items = menus.computeIfAbsent(menu, key -> new ArrayList<>());
    items.add(item);
  }
  ....
}


Glassfishとダブルチェックロック



GitHub



次のリリースに含まれる診断の1つは、「ダブルチェックロック」パターンの正しい実装をチェックすることです。Glassfishは、SelfTesterプロジェクトからの検出の記録保持者であることが判明しました。合計で、PVS-Studioは、このルールを使用してプロジェクト内の10の問題領域を検出しました。読者に楽しんでもらい、以下のコードスニペットでそのうちの2つを探すことをお勧めします。ヘルプについては、ドキュメント「V6082安全でないダブルチェックロック」を参照してくださいまあ、または、まったくしたくない場合は、記事の最後に。



EjbComponentAnnotationScanner.java



public class EjbComponentAnnotationScanner
{
  private Set<String> annotations = null;

  public boolean isAnnotation(String value)
  {
    if (annotations == null)
    {
      synchronized (EjbComponentAnnotationScanner.class)
      {
        if (annotations == null)
        {
          init();
        }
      }
    }
    return annotations.contains(value);
  }

  private void init()
  {
    annotations = new HashSet();
    annotations.add("Ljavax/ejb/Stateless;");
    annotations.add("Ljavax/ejb/Stateful;");
    annotations.add("Ljavax/ejb/MessageDriven;");
    annotations.add("Ljavax/ejb/Singleton;");
  }

  ....
}


SonarQubeとデータフロー



GitHub



診断の改善は、より疑わしい場所を見つけたり、誤検知を削除したりするために、コードを直接変更することだけではありません。データフローのメソッドの手動マーキングも、アナライザーの開発において重要な役割を果たします。たとえば、そのようなライブラリメソッドは常にnull以外を返すように記述できます。新しい診断を書いているときに、Map#clear()メソッドがマークアップされていないことを誤って発見しました。 「V6009コレクションが空です。「clear」関数の呼び出しは無意味ですという明らかにばかげたコードは別として、診断がキャッチし始めましたが、大きなタイプミスを見つけることができました。



MetricRepositoryRule.java:90



protected void after()
{
  this.metricsById.clear();
  this.metricsById.clear();
}


一見すると、辞書を再度クリアすることは間違いではありません。そして、私たちの視線が少し下がっていなければ、これはランダムに複製された行であるとさえ考えるでしょう-文字通り次の方法に。



protected void after()
{
  this.metricsById.clear();
  this.metricsById.clear();
}
public Metric getByKey(String key)
{
  Metric res = metricsByKey.get(key);
  ....
}


丁度。このクラスには、metricsByIdmetricsByKeyという名前が似ている2つのフィールドがありますafterメソッドでは、開発者は両方の辞書をクリアしたいと思っていたと思いますが、自動完了が失敗したか、不活性に同じ名前を入力しました。したがって、関連データを保持する2つの辞書は、afterの呼び出し後に同期がとれなくなります。



酒井と空のコレクション



GitHub



次のリリースに含まれるもう1つの新しい診断は、「V6084常に空のコレクションの疑わしいリターン」です。特に各アイテムを最初に初期化する必要がある場合は、コレクションにアイテムを追加するのを忘れるのは簡単です。個人的な経験から、このようなエラーはほとんどの場合、アプリケーションのクラッシュではなく、奇妙な動作や機能の欠如につながります。



DateModel.java:361



public List getDaySelectItems()
{
  List selectDays = new ArrayList();
  Integer[] d = this.getDays();
  for (int i = 0; i < d.length; i++)
  {
    SelectItem selectDay = new SelectItem(d[i], d[i].toString());
  }
  return selectDays;
}


ちなみに、同じクラスには、同じエラーなしで非常に類似したメソッドが含まれています。例えば:



public List getMonthSelectItems()
{
  List selectMonths = new ArrayList();
  Integer[] m = this.getMonths();
  for (int i = 0; i < m.length; i++)
  {
    SelectItem selectMonth = new SelectItem(m[i], m[i].toString());
    selectMonths.add(selectMonth);
  }
  return selectMonths;
}


今後の計画



あまり興味のないさまざまな内部的なものとは別に、SpringFrameworkの診断をJavaアナライザーに追加することを検討しています。それはジャビストの主なパンであるだけでなく、つまずく可能性のある多くの非自明な瞬間も含まれています。これらの診断が最終的にどのような形で表示されるのか、いつ発生するのか、また発生するのかどうかはまだよくわかりません。しかし、Spring forSelfTesterを使用したそれらのアイデアとオープンソースプロジェクトが必要であると確信しています。ですから、何か考えていることがあれば、それを提案してください(コメントやプライベートメッセージで、あなたもそうすることができます)!そして、私たちが収集するこの良さが多ければ多いほど、優先順位はそれに移ります。



そして最後に、Glassfishのダブルチェックロックの実装にエラーがあります。



  • フィールドは「揮発性」と宣言されていません。
  • オブジェクトは最初に公開されてから初期化されます。


なぜこれがすべて悪いのか-繰り返しになりますが、ドキュメントで確認できます





この記事を英語を話す聴衆と共有したい場合は、翻訳リンクを使用してください:NikitaLazeba。PVS-Studio for Javaの内部:診断の開発方法



All Articles