ReSharperでC#のコードを静的解析するアクションを作りました

muno-92.hatenablog.com

の記事で紹介したReSharper Command Line Toolsを使った静的解析について、再利用しやすいようにアクションを作成しました。

github.com

on: [push]

jobs:
  inspection:
    runs-on: ubuntu-latest # or macos-latest, windows-latest
    name: Inspection
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Setup .NET
        uses: actions/setup-dotnet@v1
        with:
          dotnet-version: '5.0.x' # or 3.1.x
      - name: Restore
        run: dotnet restore
      - name: Inspect code
        uses: muno92/resharper_inspectcode@1.0.0
        with:
          solutionPath: ./解析対象のソリューション.sln

のように指定して頂ければ、解析結果がアノテーションとして差し込まれます。

f:id:muno_92:20210511222430p:plain

良かったら使ってみて下さい。

これより下はどのようにアクションを作ったかについての内容になります。

TypeScriptでのアクション作成

アクションは「Docker」「JavaScript」「複合実行ステップ」の3種類がありますが、今回アクションを作成するに当たって以下の点を満たしたいと考えました。

  • 解析結果のXML(サンプル)でメッセージとは別のタグに記載されている重要度を取得してアノテーションに反映させたい
  • 複数のOS・.NETのバージョンで動くようにしたい
    • 複数の.NETでの動作は最低限可能にする(LTSの.NET Core3.1 + 最新の.NET5)
    • 複数OSでの動作は出来たら良いかな、という位

それに加え、普段使っていないTypeScriptを触ってみたかったので、公式から提供されているTypeScriptでJavaScriptアクションを作成するテンプレートを使ってアクションを作成しました。

アクションの中身

処理としてはhtmlparser2でXMLをパースして取り出した指摘事項を公式ツールキットのIssueCommand関数に渡してアノテーションとして表示されるようにしています。

www.npmjs.com

XMLのパースにどのライブラリを使ったら良いのか分かりませんでしたが、ダウンロード数やTypeScriptでの使いやすさ、Issue/PRが溜まっていないかといった点をチェックしてhtmlparser2を選びました。

このライブラリの方が良いよ、というのがあったら教えて頂けると嬉しいです。

アノテーションの表示については最初はツールキットのcore.warning()`やcore.error()で出力したメッセージのパターンをProblem Matchersで指定しようとしたのですが、この方法ではアノテーションとして表示されませんでした。

blog.utgw.net

によるとProblem Matchersは標準出力/標準エラー出力を読み取っているようですが、この記事を参考にRunnerのコードを追ってみた所、

と違いがあったため、それが原因ではないかと思います。

上記記事やActionCommandManagerのコードを見て初めてwarningやerrorのメッセージを出力する際にファイル情報をプロパティとして指定出来ると分かったため、その方法を採りました。
(この記事を書いている今になってドキュメントに記載されていると気づきました・・・)

ツールキットを使うと以下のような形になります。

import {issueCommand} from '@actions/core/lib/command'

issueCommand('warning', {file: 'ファイルパス', line: 行番号, col: 列番号}, 'メッセージ')

Problem Matchersを使った方法では正規表現を読み解かないとどのようなメッセージ形式なのか分かりませんが、こちらは明示的ですね。

感想

今回TypeScriptでアクションを作成しましたが、公式でテンプレートやツールキットが用意されており、作成し易かったです。

TypeScript自体についても、不慣れなライブラリを使う際に使い方が合っているか型で確認出来るので、やっぱり型があるのは良いなと思いました。

GitHub Actionsに限定せず、機会を見つけてTypeScriptを使っていこうと思います。

PHPerKaigi2021に参加しました

3/26(金)〜28(日)に開催されたPHPのカンファレンス、PHPerKaigi 2021に一般参加したのでその感想記事です。

開催前

開催日の10日前にノベルティが届きました。

タイムテーブルなどに加えて技術記事も掲載されているパンフレットやマスク、ペーパーナイフ、入浴剤など盛りだくさん。
(GMOさんのカレーはクオリティが高いパッケージで笑いましたw)

更にノベルティが入ったボックスはPHPerKaigiやスポンサー企業さんのロゴが印刷されており、それ単体で取っておきたくなるデザイン。

これだけあってアーリーバードのチケット代2000円は安っ!と感じるボリュームでした。

また、オンラインのカンファレンスだと開催日が近づいている実感が沸きづらいような気がしていましたが、事前にノベルティを受け取るともう少しでカンファレンスが始まると実感出来て良かったと思います。

事前録画形式での配信

本編のセッションはLTを除き、スピーカーの方が事前に収録した動画をニコ生で見るスタイルでした。

スピーカーの方が自分のセッションについてリアルタイムで補足されている内容を見ながらセッションを視聴出来、理解が深まるだけでなく双方向にコミュニケーションを取れる感じがあって良かったです。

またAsk the SpeakerはDiscordのボイスチャンネル上で行われていたのですが、オフラインでスピーカーの方に質問しに行くのと比べ、「ちょっとボイスチャンネルに入ってみよう」位の気軽な気持ちでチャンネルに入れたので、個人的には参加しやすかったです。

一方、20人参加しているようなボイスチャンネルでミュートを解除して声を出す勇気を持てず、チャットにテキストで書き込むだけになってしまったのは反省かなと思います。

次の機会があったら勇気を出して声を出してみたいと思います。

アンカンファレンス

PHPerKaigiではzoomを使ったアンカンファレンスも行われました。

タイムテーブル上から枠を予約するとその時間に参加可能なzoomのミーティングルームが自動で作成される仕組みで(当日になってそれを用意出来るfortee凄い)、技術的な雑談やCI/CDの話、懇親会など様々なアンカンファレンスが開かれていました。

特に、day0のアンカンファレンスでは突発的にforteeのライブデバッグが始まり、画面共有したコードを見ながら話しているのが、スクリーンに映されたコードを見てわいわいするオフラインのアンカンファレンスみたいでとても楽しかったです。

(余談ですが、ニコ生のタイムシフトで見返せるセッションよりもアンカンファレンスに参加した方が楽しそう、とは思いつつも見たいセッションもあるのでどうしよう、と悩み、オフラインカンファレンスもこんな感じだったな〜と少し懐かしくなりました。)

感想

今年はオンライン開催になったPHPerKaigiでしたがスタッフの方が色々と工夫して下さったおかげで、一方的にセッションを聞くだけにならずとても楽しめました。

改めてPHPerKaigiはこの参加者全員で作り上げ、一緒に楽しむ感じが好きなんだな〜と感じました。
(と言いつつほとんどROMになってしまったので次はもっとコミュニケーションを取っていきたい)

そして、オンラインでも楽しかったですがオフラインはもっと楽しいので次回はオフライン開催出来る情勢になっていると良いな〜と思います。

また来年もPHPerKaigiに参加したいです!

GitHub ActionsでReSharperのコード解析を実行する

こんにちは。

最近、ReSharperコマンドライン版が無料で公開されている事を知り、ちょうどC#で書かれたコードの静的解析をCIで実行したいと考えていた所だったので、GitHub Actionsで動かしてみました。

結論

リポジトリの.githubディレクトリに静的解析結果の書式を指定するjsonファイルを置き、

{
    "problemMatcher": [
        {
            "owner": "resharper_xml_output",
            "pattern": [
                {
                    "regexp": "^\\s+<Issue TypeId=\"[^\"]+\" File=\"([^\"]+)\" Offset=\"(\\d+)-\\d+\" (Line=\"(\\d+)\" )?Message=\"([^\"]+)\"",
                    "file": 1,
                    "column": 2,
                    "line": 4,
                    "message": 5
                }
            ]
        }
    ]
}

以下のジョブを定義するだけで

name: Incpection

on: [push,pull_request]

jobs:
  inspection:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-dotnet@v1
        with:
          dotnet-version: '3.1.x'
      - name: Install ReSharper Command Line Tool
        run: dotnet tool install -g JetBrains.ReSharper.GlobalTools
      - name: Run Inspection
        run: jb inspectcode -o=result.xml -a ソリューション名.sln
      - name: Set Output Format
        run: echo "::add-matcher::.github/上記JSONのファイル名.json"
      - name: Output Inspection Result
        run: cat result.xml
      - name: Exit on failure if issue is exist
        run: if grep -Eq '<Issue [^>]+>' result.xml; then exit 1; fi

静的解析結果がアノテーションとして認識され、PRのFile Changedタブに差し込まれるようになります。

f:id:muno_92:20210130211428p:plain

バージョン情報

  • .NET Core SDK 3.1系
  • JetBrains.ReSharper.CommandLineTools 2020.3.2

ReSharper コマンドラインツール

ReShaperはコマンドライン版が無料で公開されています。

https://www.jetbrains.com/help/resharper/ReSharper_Command_Line_Tools.html

.NETツールとして公開されているため、.NET Core SDKがインストールされている環境さえ用意すれば気軽に利用可能です。

注意点として、現在の最新バージョン(2020.3.2)は.NET5で動作しません。
実行環境には.NET Core SDK3.1系がインストールされている必要があります。
(NuGetのページには依存無と記載されていましたが、nupkgファイルをダウンロード・展開したところruntimeconfig.jsonのtfmにnetcoreapp3.1と指定されていました。)

2021/4/11 追記
バージョン2021.1.0から.NET5でも動作するようになりました。
inspectcode.unix.runtimeconfig.jsonrollForward: "Major"が追記されていたため、ロールフォワードするようになったものと思われます。

ReSharperコマンドラインツールは

  • コードのクリーンアップ (CleanupCode)
  • 重複コードの検出 (dupFinder)
  • コードの検査 (InspectCode)

を機能として備えており、コードの検査は以下のコマンドで実行出来ます。

jb inspectcode [option] ソリューション名.sln

今回使用したオプションも含め、いくつかのオプションについて説明します。

  • --output (-o)
    解析結果はファイルに書き出されるため、出力先の指定が必要です。
  • --format (-f)
    デフォルト: Xml
    出力形式をHtml、Text、Xmlの中から選択出来ます。
    今回はパースし易いXMLで出力させるため、未指定としてデフォルト値をそのまま使っています。
  • --absolute-paths (-a)
    解析結果に記載する各ファイルのパスを絶対パスで出力します。
    デフォルトでは相対パスで出力されますが、MacLinux上で実行していたとしてもファイルパスがバックスラッシュ区切りで出力されてしまいます。
    絶対パスの場合はスラッシュ区切りになるため、このオプションを有効にして回避しています。

また、解析でコードに問題が発見されたとしても、コマンドは正常終了します。

そのため、ジョブを失敗させたい場合は実行結果次第でexit 1するステップを追加しましょう。

Problem Matchers

GitHub Actions で .NETプロジェクトの静的コード解析を行うを読んで知ったのですが、GitHub Actionsはjsonで指定したパターンに一致した出力をアノテーションとして認識してくれます。

アノテーションとして認識されるとPRのFile Changedタブに差し込まれるため、出力メッセージのファイル名・行番号を元に自分の目で問題になったコードを探さなくて済むようになります。
(とても便利)

指定方法の詳細については以下のドキュメントをご参照下さい。

https://github.com/actions/toolkit/blob/master/docs/problem-matchers.md

まとめ

ReSharper コマンドラインツールが.NETツールとして公開されているおかげで、気軽に/どのOSでも高度なコード解析機能を利用出来る事が分かりました。

また、今回初めてGitHub Actionsを使ってみましたが、公式から様々なワークフローが提供されていたり、Problem Matchersを使ってコマンドの実行結果をアノテーションとして認識させられたりと、中々便利だなと感じました。

今後も色々と試していきたいと思います。

2020年の振り返り

1月

1月は年始に放送していた24時間バンドリ!TV 2020を見てからバンドリ!にどはまりして、そっちに時間を割いていた気がします。

その中でも、なんとなく使っていたVSCodeについて徹底解説Visual Studio Codeを読んで学習していました。

2月

新型コロナウイルスの感染が広がる中、オフラインイベントに参加出来た最後の月。

PHPerKaigiは当日スタッフとしての参加も、3日間フルでの参加も今年が初めてでした。
色々な人と交流でき、3日間とても楽しかったです。
ぎりぎりのタイミングではありましたが参加できて本当に良かったと思います。

(オフラインイベントが恋しい・・・)

3月

【初参加歓迎】バーチャル空間勉強会 #0ではclusterを使ったイベントの楽しさを実感しました。

また、3月にリリースされたTechFeed Proを使い始めました。
英語記事も含め、自分が気になるトピックでどんな記事があるかざっと目を通せ、情報収集がし易くなりました。

4月

この月から在宅勤務に移行。

オフィスで仕事をしていた時と比べ体に負担が掛かったため、(4月以降も含め)色々と買い揃えました。

5月

muno-92.hatenablog.com
ブログ初投稿。

C#で3つあるテスティングフレームワークについて調べている中で「これ記事に出来るんじゃね?」と思いガッと書きました。

6月

中途半端に手をつけて放置していたスクレイピングのアプリを実際に動くように調整し始めました。

6/20に開催されたチャリティカンファレンス沖縄2020 Vol.1 Frontend編にて、名前を聞いたことがあるだけだったJamstack周りのセッションを視聴。

裏番組だったmicroCMSのワークショップも資料が公開され、Jamstackの理解が進みました。

7月

muno-92.hatenablog.com

2記事目を投稿。

オンラインカンファレンス/勉強会で長時間イヤホンをつけていると耳が痛くなるのが悩みだったので、AfterShokz Aeropexを購入。

オンラインカンファレンス/勉強会への参加以外にもポッドキャスト視聴などでフル回転していて、もうAfterShokz無しの生活には戻れません。

8月

8/7に発売されたEngineers in Voyageの感想記事を書こうとするもまとまらず・・・

9月

CloudNative Days Tokyo 2020に参加。

k8sDocker/Kubernetes 実践コンテナ開発入門を読んだ際にDocker Desktop上で動かした事はありましたが、セッションで話されている内容を聞いていると「知らないこと多過ぎ・・・」と感じました。

AWS Summitは基調講演だけ見て、「アーカイブはいつでも見れるから」と思うと見るモチベーションが沸かず他のセッションは全く見ないまま終わってしまいました・・・

10月

ドメイン駆動設計 モデリング/実装ガイドドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本を読んでDDDの学習を始めました。

他には、6月に作成したスクレイピングのアプリをコンテナ化して開発環境での再現を容易にしたいと考えてECS/ECRのドキュメントを読んで手を動かしてみていました。

11月

.NET 5、PHP8がリリースされ、お祭り気分で過ごした月でした。

また、前月のECS/ECRの学習に続き、クラウドに移すならIaCで構築したいと考えて実践Terraform AWSにおけるシステム設計とベストプラクティスを読みました。

作成したリソースを数日放置してただけで月の費用が10ドルに届き、「NAT Gateway高っ」と感じました。

結局、料金を考えるとHerokuの無料は強いな〜と思ってスクレイピングのアプリはそのままにしています。

12月

PHPカンファレンス2020に参加。

PHPカンファレンスは初めて参加したカンファレンスであり、毎年参加していることもあって参加出来ると何だかほっとし、そして毎年刺激を受けています。

オンラインと形を変えつつ、今年も開催されて良かったです。

まとめ

今年は初めての記事を書き、アウトプットの第一歩を踏み出せた点は良かったと思います。
アウトプットへのハードルが以前より低くなっただけでなく、記事を書く上でソース・ドキュメントを確認し、理解が深まりました。

一方、2記事しか書けなかったり、自分の意見・感想を書かなくて済む内容に逃げてしまったりした点は反省点かなと思います。

また、k8sの知識に課題を感じたり、クラウドやDDDの学習はまだ入門したばかりだったりします。

来年はk8sクラウド、DDDなどの学習を進め、アウトプットとしては

  • 最低でも年6記事(2ヶ月に1記事)
  • 読んだ本の感想も書く

を目標に取り組んでいこうと思います。

Scrapyで発生した例外をキャッチする方法

最近、Heroku上で定期実行し、Scrapyで取得した新刊情報をSlackに通知するプログラムを作成しました。

その際、スクレイピングで発生した例外についても手動で確認せずに済むよう、Slack通知を設定したためその方法を記載します。
(Heroku上で動作させ、Slack通知する方法については参考記事参照)

↓作成したプログラムはこちら↓

github.com

Scrapyとは

ScrapyはPythonでクローリング・スクレイピングを行うためのフレームワークです。

クローリング・スクレイピングを行う際には

  • robot.txtで拒否されているページのクロールを行わない
  • あるページのリンク(ニュース一覧に記載されている各ニュースヘのURL等)を辿り、遷移先のページから情報を取得する
  • サイト内のリンクを辿る際にはサイトに負荷を掛けないよう、アクセス間隔を空ける

等、毎回行う処理や注意しなければいけない点があります。

Scrapyにはこういった点を考慮した機能が備わっており、少ないコードでクローリング・スクレイピングを行うプログラムを記述できます。

例外をキャッチする方法

Scrapyは

  • WEBページを取得するDownloader
  • 取得したWEBページをパースし、情報を取得したり次のページに遷移するためのリクエストを発生させたりするSpider
  • 取得した情報に対する処理を行うItem Pipeline
  • これらを管理するScrapy Engine

といったアーキテクチャで動作します。
(詳しくは下記ページをご覧下さい)

doc-ja-scrapy.readthedocs.io

そして、Scrapy EngineとSpiderの間で入出力をフックするSpider Middlewareを使う事でSpiderで発生した例外をキャッチできます。

doc-ja-scrapy.readthedocs.io

Spiderで発生した例外をキャッチするためにはprocess_spider_exceptionを実装したクラスをミドルウェアとして追加すればOKです。

setting.py

SPIDER_MIDDLEWARES = {
    'notification.middlewares.NotificationErrorMiddleware': 543,
}

独自定義したミドルウェア

class NotificationErrorMiddleware(object):
    def process_spider_exception(self, response, exception, spider):
        webhook_url = environ.get('SINKAN_TUUTI_SLACK_URL')
        if not webhook_url:
            raise DropItem('webhook url is not defined.')

        message = 'url: ' + response.url + '\n'
        message += traceback.format_exc()

        # 例外が発生したページのURL・スタックトレースをSlackに通知
        slack = slackweb.Slack(webhook_url)
        slack.notify(text=message)

        return None

これでいちいちログを目視で確認しなくて済みますね!

まとめ

失敗するクローリング・スクレイピングのプログラムをそのままにしておくとクロール先のサイトに迷惑を掛けてしまう恐れがあります。

発生した例外を自動通知し、スクレイピングの失敗にいち早く気付く事でプログラムを改善していきましょう!

参考記事

Scrapyでイベントサイトの更新情報を定期ツイート on Heroku
Python3でslackに投稿する

MSTest・NUnit・xUnitにおけるリスト比較の違い

C#にはMSTest・NUnit・xUnitという3種類のテスティングフレームワークがあります。

違いが良くわからないままMSTestを使っていたのですが、MSTestはリスト比較のアサーションに失敗した際のメッセージが分かりにくく、リスト比較がしやすいテスティングフレームワークはどれなのか検証してみました。

バージョン

  • .NET Core : 3.1.201
  • MSTest : 2.1.0
  • NUnit : 3.12.0
  • xUnit : 2.4.0

MSTest

MSTestはCollectionAssert.AreEquivalentで順不同の比較、CollectionAssert.AreEqualで要素の順番まで考慮した比較が行えます。

using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace MSTestSample
{
    [TestClass]
    public class CollectionTests
    {
        [TestMethod]
        public void TestDifferentValue()
        {
            var list1 = new List<string>() {"aaa", "bbb"};
            var list2 = new List<string>() {"aaa", "ccc"};

            // The expected collection contains 1 occurrence(s) of <bbb>.
            // The actual collection contains 0 occurrence(s).
            CollectionAssert.AreEquivalent(list1, list2);
        }

        [TestMethod]
        public void TestDifferentOrder()
        {
            var list1 = new List<string>() {"aaa", "bbb"};
            var list2 = new List<string>() {"bbb", "aaa"};

            // Element at index 0 do not match.
            CollectionAssert.AreEqual(list1, list2);
        }
    }
}

このメッセージだけでは情報が少なく、アサーションに失敗した原因を確認するためにはデバッガを使用するかprintデバッグする必要がありますね。

NUnit

NUnitはAssert.That〜Is.EquivalentToで順不同の比較、Is.EqualToで要素の順番まで考慮した比較が行えます。
NUnitにもMSTestと同様のCollectionAssertクラスがありますが、内部的にはIs.EquivalentToやIs.EqualToを実行しています。)

using NUnit.Framework;
using System.Collections.Generic;

namespace NUnitSample
{
    public class CollectionTests
    {
        [Test]
        public void TestDifferentValue()
        {
            var list1 = new List<string>() {"aaa", "bbb"};
            var list2 = new List<string>() {"aaa", "ccc"};

            // Expected and actual are both <System.Collections.Generic.List`1[System.String]> with 2 elements
            //     Values differ at index [1]
            // String lengths are both 3. Strings differ at index 0.
            //     Expected: "ccc"
            // But was:  "bbb"
            // Assert.That(list1, Is.EqualTo(list2));

            // Expected: equivalent to < "aaa", "ccc" >
            //     But was:  < "aaa", "bbb" >
            //     Missing (1): < "ccc" >
            //     Extra (1): < "bbb" >
            Assert.That(list1, Is.EquivalentTo(list2));
        }

        [Test]
        public void TestDifferentOrder()
        {
            var list1 = new List<string>() {"aaa", "bbb"};
            var list2 = new List<string>() {"bbb", "aaa"};

            // Expected and actual are both <System.Collections.Generic.List`1[System.String]> with 2 elements
            //     Values differ at index [0]
            // String lengths are both 3. Strings differ at index 0.
            //     Expected: "bbb"
            // But was:  "aaa"
            Assert.That(list1, Is.EqualTo(list2));
        }
    }
}

Is.EqualToで出力されるメッセージでも一致しなかった部分が確認出来ますが、Is.EquivalentToだとより見やすく出力されるため要素の順番まで一致していて欲しい場合以外はIs.EquivalentToを使った方が良さそうです。

xUnit

xUnitはAssert.Equalでリストや配列を比較出来ます。
(順不同での比較はありません)

using Xunit;
using System.Collections.Generic;

namespace xUnitSample
{
    public class CollectionTests
    {
        [Fact]
        public void TestDifferentValue()
        {
            var list1 = new List<string>() {"aaa", "bbb"};
            var list2 = new List<string>() {"aaa", "ccc"};

            // Expected: List<String> ["aaa", "bbb"]
            // Actual:   List<String> ["aaa", "ccc"]
            Assert.Equal(list1, list2);
        }

        [Fact]
        public void TestDifferentOrder()
        {
            var list1 = new List<string>() {"aaa", "bbb"};
            var list2 = new List<string>() {"bbb", "aaa"};
            
            // Expected: List<String> ["aaa", "bbb"]
            // Actual:   List<String> ["bbb", "aaa"]
            Assert.Equal(list1, list2);
        }
    }
}

シンプルなメッセージで、個人的にはxUnitの出力内容が好み。

まとめ

分かりやすいメッセージ出力・順不同での比較の両方を備えたNUnitが配列やリストを比較するケースには向いているようです。

メッセージ出力についてはデバッガを使えば済む問題であり、テスティングフレームワークを選ぶ基準は他にもあるとは思いますが、この記事がMSTest・NUnit・xUnitのどれを使えばいいのか迷った自分のような人の助けになれば嬉しいです。

余談

実際のテストコードで書く事はないでしょうが、興味本位で
List<string> { "10", "20" }

List<int> { 30, 40 }
を比較した場合の挙動も確認してみました。

xUnit

CollectionTests.cs(14,26): error CS1503: Argument 1: cannot convert from 'System.Collections.Generic.List<string>' to 'string'
CollectionTests.cs(14,33): error CS1503: Argument 2: cannot convert from 'System.Collections.Generic.List<int>' to 'string'

リストの比較に使われるのはEqual<T>(T expected, T actual)なので期待値と比較対象の型が違う場合は実行されず、Equal(string expected, string actual)を実行しようとしてコンパイルエラーになります。

NUnit

Expected: equivalent to < 30, 40 >
But was:  < "10", "20" >
Missing (2): < 30, 40 >
Extra (2): < "10", "20" >

ダブルクオートの有無で見分けなければいけませんが、まあ型が違う事は分かります。

MSTest

The expected collection contains 1 occurrence(s) of <10>.
The actual collection contains 0 occurrence(s).

ええ・・・