mcpField
F
FSharp.Refactor

FSharp.Refactor

ローカル
github.com/Thorium開発ツール166 日前に更新

FSharp.Refactor について

FSharp.Refactorは、エージェントが生成したコードも対象に、F#開発者向けのオフラインのリファクタリング提案と修正を提供します。ビルドを妨げないヒントを分析器が示し、.NET CLIで修正を一括確認・適用できます。

主な機能

  1. 01オフラインのF#リファクタリング分析器
  2. 02エディターのヒントとクイック修正
  3. 03CLIによる修正の一括適用
  4. 04ドライランとAPI変更オプション
  5. 05CI向けSARIFレポート

活用シーン

  1. 01エージェント生成のF#コードを確認
  2. 02プロジェクト全体に選択した修正を適用
  3. 03CIで分析結果をレポート

MCP 対応

エージェント用途向けの.NETコマンドラインツールがMCPに対応します。入力にはMCPツールの詳細は記載されていません。

設定

ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。

インストール方法は未掲載です

このサーバーのパッケージやリモートエンドポイントはまだ取得できていません。プロジェクトの README の手順に従ってください。

リポジトリ
パッケージ
nugetfsharp-refactor0.8.45

ツール

ツール一覧はまだ取得されていません

ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。

詳細

FSharp.Refactor

インターネットから括弧をなくしましょう

F# 向けの関数型リファクタリングの提案です。
このツールは、(エージェントが生成した、または古い)コードの意図を明確にし、英数字以外のノイズを減らして、組み込みの .NET ライブラリをより効率的に使えるようにします。

  • エディターでの電球アイコンによるクイック修正
  • 一括で適用するコマンドラインツール

提案の重大度は Hint です。これは問題ではなく改善の機会を示すものであり、ビルドを妨げることはありません。

このプロジェクトは CSharp.Refactor の兄弟プロジェクトです。


使用方法

このツールは完全にオフラインで動作します。作成には AI が使われていますが、ツール自体は AI を使用しません。

クイックスタート

設定は不要です。ツールはプロジェクトを読み込み、変更内容を報告します。指示するまで編集は行いません。

dotnet tool install --global fsharp-refactor
fsharp-refactor Your.fsproj --dry-run --api-changes

ファイルと位置を含め、適用される修正をすべて表示します。ファイルへの書き込みは行いません。内容を確認したら、フラグを外して適用します。

fsharp-refactor Your.fsproj --api-changes

すでにビルドに成功しているプロジェクトでなければ実行を拒否し、適用によってエラーが発生した場合は明確に失敗します。

パブリックメソッド/APIを変更したくない場合は、--api-changes を外します。

fsharp-refactor Your.fsproj
入力中に電球アイコンを使う方法については、以下の IDE プラグインの手順をご覧ください:VS Code / Ionide、Visual Studio。
image
エージェントを使うシナリオでは、dotnet ツールが MCP をサポートしています。

ツールの使い方: 対象を指定してください。種類はパスから判別されます。

Your.fsproj1つのプロジェクト
Thing.fs1つのソースファイル。プロジェクトを検出して解析しますが、編集するのはこのファイルだけです
build.fsx1つのスクリプト。MSBuild の手順が一切ないため、すぐに開始します。解決できない参照があるスクリプトも拒否されません。構文ルールはそのまま適用されます(fsi で実行するスクリプトは、実行時にのみ提供されるものを参照している場合があります)
Your.sln、Your.slnxソリューションに含まれるすべての F# プロジェクト
src/そのディレクトリ内のソリューション、または配下のプロジェクト
"src/**/*.fsproj"グロブに一致するすべて

変更内容

例:

変更前変更後
正確性let s = new FileStream(p, m)use s = new FileStream(p, m)
正確性raise (Exception "boom")failwith "boom"
パフォーマンスs.Contains "x"s.Contains 'x'
パフォーマンスxs |> Seq.toList |> List.map fxs |> Seq.map f |> Seq.toList
慣用表現match b with | true -> 1 | false -> 0if b then 1 else 0
冗長性new StringBuilder()StringBuilder()
冗長性[<SerializableAttribute>][<Serializable>]
診断failwith "Error"failwith $"Error, calling f with x: {x}"

150ほどあるルールの変更例です。完全な一覧はリファクタリングをご覧ください。

エディターと CI の設定

アナライザーは FSharp.Refactor.Analyzers として配布されています。
このパッケージは開発時の依存関係です。ヒントとクイック修正を提供するだけで、コンパイル済みの出力に含まれるものはありません。

注意:NUGET 参照を追加した場合でも、このアナライザーは出力先のパス(BIN)に含まれず、プロジェクトの一部にもなりません。

VS Code / Ionide

最も簡単な方法:FSharp.Refactor VS Code 拡張機能をインストールします。

  • アナライザーが同梱されており、同意いただければ Ionide の設定にグローバルに組み込まれるため、プロジェクトごとの設定をしなくても、すべての F# プロジェクトでヒントが表示されます。

または、解析したいプロジェクトからパッケージを参照して、プロジェクトごとに手動で設定します。

<PackageReference Include="FSharp.Refactor.Analyzers" Version="*" PrivateAssets="all" />

次に、復元されたアナライザーを .vscode/settings.json で Ionide に指定します。

{
  "FSharp.enableAnalyzers": true,
  "FSharp.analyzersPath": [
    "~/.nuget/packages/fsharp.refactor.analyzers/<version>/analyzers/dotnet/fs"
  ]
}

<version> は、復元時に実際に選択されたバージョンに置き換えてください(NuGet キャッシュのフォルダー名は常にバージョンをキーにするため、このパスではバージョンを固定する必要があります。ls ~/.nuget/packages/fsharp.refactor.analyzers/ で確認してください)。Windows では、キャッシュは %USERPROFILE%\.nuget\packages にあります。提案は Hint の重大度の診断として表示され、電球アイコンからワンクリックで修正できます。

FsAutoComplete 0.85 以降では、各修正のタイトルに書き込まれる内容が表示されます(Fix FR0136 → Guid.NewGuid())。それ以前のバージョンでは、Fix FR0136 とだけ表示されます。Ionide には独自の FsAutoComplete が同梱されています。より新しいバージョンを実行するには、dotnet tool install -g fsautocomplete を実行し、FSharp.fsac.netCoreDllPath にその fsautocomplete.dll を設定してください(上記の拡張機能を使うと、どちらも実行できます)。

このパッケージには、異なるバージョンの FSharp.Analyzers.SDK に対してビルドされたアナライザーが 2 種類同梱されています。SDK 0.35.0 用の FSharp.Refactor.Analyzers.Ionide.dll(標準の Ionide の FsAutoComplete に同梱されているバージョン。7.31.x 以前)と、SDK 0.37.2 用の FSharp.Refactor.Analyzers.dll(CLI および FsAutoComplete 0.84 以降用)です。SDK は自身のバージョンに一致するアセンブリのみを読み込み、もう一方をスキップしたことをログに記録します。そのため、各ホストが使用できる方を選び、設定を選択する必要はありません。

提案が表示されない場合は、[Output]→[F# Language Service]を開いてください。スキャンしたアナライザー DLL と、読み込まれたアナライザーの数が表示されます。バージョンの不一致によるスキップもここに明記され、エディター上には表示されません。

Visual Studio 2022-2026

FSharp.Refactor Visual Studio 拡張機能(ユーザー向け概要)をインストールすると、FsAutoComplete サイドカーを介して解析が行われ、フル機能の Visual Studio で波線と Ctrl+. のクイック修正を利用できます。前提条件は 1 つです。 dotnet tool install -g fsautocomplete。

CLI / CI
dotnet tool install --global fsharp-analyzers
fsharp-analyzers --project src/YourProject.fsproj --analyzers-path ~/.nuget/packages/fsharp.refactor.analyzers/<version>/analyzers/dotnet/fs --code-root . --report analysis.sarif

fsharp-analyzers はアナライザーのホストであり、インストールする dotnet ツールです。このパッケージ自体はツールではなく、ホストが読み込むアナライザーアセンブリのライブラリなので、インストールするのではなく、ディレクトリとして渡します。アナライザーパッケージには意図的に lib/ フォルダーがなく、開発依存関係としてマークされているため、このディレクトリは NuGet キャッシュ内にあります。そのため、PackageReference を指定しても bin には何も配置されず、復元後はアセンブリがキャッシュ内に置かれます。--analyzers-path にはアセンブリを含む任意のフォルダーを指定でき、その中が再帰的に検索されます。

CLI は結果を報告するだけで、ファイルを編集することはありません。これは CI での利用に適しており、SARIF 出力は GitHub のコードスキャンで使用できます。修正を適用するには、以下の独自ツールを使ってください。リポジトリごとに fsharprefactor.json を使って個別のルールを無効にできます。下記の設定を参照してください。

コマンドラインから修正を適用する

fsharp-refactor dotnet ツールを使うと、ファイルにクイック修正を直接適用できます。

dotnet tool install --global fsharp-refactor
fsharp-refactor Your.fsproj [--dry-run] [--codes FR0002,FR0031] [--api-changes] [--jobs 4] [--max-passes 5]

(または、このリポジトリから実行します。 dotnet run --project src/FSharp.Refactor.Tool -c Release -- Your.fsproj ...)

プロジェクトの場合、MSBuild からコンパイラーの引数をそのまま取得します。スクリプトの場合、FCS が参照を自分で解決するため、MSBuild は実行されません。いずれの場合も、すべてのアナライザーを実行し、重複しない修正を下から上へ適用して、修正が適用されなくなるまで再解析します。修正によって、さらに別の修正が可能になることがあります。すでにエラーがあるコンパイルは拒否し、修正の適用によってエラーが発生した場合は明確に失敗します。 警告をエラーとして扱うかどうかはプロジェクト独自の設定で決まり、ツールがそれを上書きすることはありません。検証には CI で実行するビルドが使われます。メモリ内での型チェックでは、コンパイラー自身の引数が使われるため、--warnaserror と <WarningsAsErrors> を設定すると、修正によって発生した F# の警告はエラーになり、パスはロールバックされます。F# プロジェクトを参照する C# プロジェクトでアナライザーの警告をエラーにする設定(error CA1859、error IDE0005)を使っている場合、検証ビルドはツールのノイズではなく、コードの問題として失敗します。また、修正を適用する場合に変更する設定も実行結果に表示されます(<WarningsNotAsErrors>CA1859</WarningsNotAsErrors>、または dotnet_diagnostic.CA1859.severity = suggestion)。<NoWarn> も使えますが、全員に対してアナライザーが無効になります。

フラグ
--dry-runレポートのみを出力します。実行する修正をファイルと位置とともにすべて一覧表示し、何も書き込みません。書き換えは暗黙に行われることはありません。編集させるには、このフラグを外してください。
--codes FR0002,FR0031実行するルールを指定したものに限定します。他のアナライザーは実行されません。
--categories <list>実行するルールの種類を限定します。correctness、performance、idiom、cosmetic を指定できます。--codes と組み合わせると、指定する順序にかかわらず、さらに絞り込めます。対象から外れたルールのアナライザーは一切実行されません。フィルターをかけた実行では、全ルールでの実行なら通過する箇所で止まることがあります。たとえば FR0157(イディオム)が残す `
--jobs <n>一度に指定した数のファイルを型チェックします(デフォルトは 4、コア数に応じて 2~4 に制限されます)。CPU 使用量を増やして経過時間を短縮します。FCS は増分ビルドのたびに各ファイルのプレフィックスを再利用するため、速度向上は 4 付近で最大になり、それ以上に増やすと逆効果になります。--jobs 1 は逐次処理です。
--framework <tfm>最も狭い対象フレームワークではなく、指定した対象フレームワークに対して解析します。下記を参照してください。
--max-passes <n>修正してから再解析する反復回数です(デフォルトは 5)。最初のパス以降は、前のパスで変更されたファイルと、それらのファイルのうち最初のファイルより後にコンパイルされたファイルだけをスキャンします。
--helpツール自体から同じ一覧を表示します(-h と /? も使用できます)。
--api-changes下記のファイル横断修正も適用します。
--no-if-defsマルチターゲットプロジェクトで、機能対応の修正に #if/#else/#endif のペアを出力しません(下記を参照してください)。修正はそのまま適用され、レガシーフレームワークで拒否された修正は、最終ビルドチェックで元に戻されます。
--report <file>実行で検出された問題をすべてファイルに書き込みます。拡張子によって形式が決まります。.sarif(または .json)は SARIF 2.1.0 です。GitHub のコードスキャンでインラインの PR 注釈として表示されます。.html は、ルール別のグループ化、強調表示されたソース、修正前後の比較、カテゴリーフィルターを備えた自己完結型のページです。.csv は、スプレッドシート向けに問題ごとに 1 行で出力します。CI のリントゲートには --dry-run と組み合わせるのが適しています。下記の CI の設定を参照してください。複数ターゲットの実行では、ターゲットごとにレポートを書き換えます。そのため、1 時間後にクラッシュしたり Ctrl-C で中断したりしても、検出済みの内容は残ります。チェックアウトをまとめたワークスペース、つまり独自のソリューションを持たず、サブディレクトリにソリューションがあるディレクトリ(たとえば C:git)を指定すると、各チェックアウトを個別に解析し(それぞれのソリューションを尊重し、各 fsharprefactor.json を適用し)、ワークスペースからの相対パスを使って 1 つのレポートにまとめます。
--baseline <sarif>検出結果を抑制するための基準です。以前の --report 出力にフィンガープリントが含まれる問題は、報告も修正もされず、新たに発生した問題だけが表示されます。フィンガープリントはルールコード、ファイル名、正規化された周辺ソースからハッシュされるため、行の移動、ファイル内の他の編集、異なるチェックアウトをまたいでも維持されます。一度トリアージすれば、以後は基準として固定できます。
--fail-on-findingsフィルター適用後に問題が 1 つでも残っている場合、終了コード 3 で終了します。CI の厳格なゲートです。終了コードの仕様は次のとおりです。0 は問題なし、1 は解析または適用の失敗、2 は使用方法のエラー、3 は問題あり(このフラグを指定した場合のみ)です。
`--notes [onoff
--notes onlyレビュー用パスです。すべてのルールを実行し、修正を伴わない問題だけをインラインで一覧表示し、何も書き込みません。対象は 33 個の助言ルール(Rules.md の修正列に em ダッシュがあるもの)と、修正ルールでも助言しかできないケースです。fsharp-refactor src/Your.fsproj --notes only --report notes.html を実行すると、ページとして書き出されます。
--format json機械可読な標準出力です。進行状況を示すテキストは標準エラー出力に移り、実行結果の問題は 1 つの JSON ドキュメントとして出力されます(コード、重大度、修正可能かどうか、位置、メッセージ、フィンガープリント、ソース抜粋)。デフォルトの出力は人が読みやすい形式のままです。
--rulesルールカタログを表示します。コード、カテゴリー、デフォルトで有効かどうかが含まれます(--format json に対応)。
--create-configこのビルドのデフォルト設定を記載した fsharprefactor.json を、現在のディレクトリ、またはディレクトリとして指定した場合は <what> に書き込みます。すべてのルールと実行レベルのキーが含まれ、それぞれにコメントが 1 つ付いています。編集するまで設定は何も変更せず、既存の設定ファイルを上書きすることもありません。
--mcpツールを stdio 経由の MCP サーバーとして提供します(改行区切りの JSON-RPC、追加依存関係なし)。analyze(target、codes/categories、parseOnly、apply)と list_rules のツールを提供します。呼び出し間でウォーム状態の型チェッカーを 1 つ保持するため、最初の analyze では参照の解析が必要ですが、それ以降はホットキャッシュから応答します。エージェントのループで必要となる処理効率を実現できます。
--parse-onlyこのマシンではコンパイルできないコードベース向けのオプションです。たとえば、データベースを必要とする型プロバイダーや、復元できない参照がある場合に使用します。MSBuild と参照解決は行いません。ソースは fsproj の <Compile> 項目から直接取得され、型チェッカーを参照しない 119 個中 58 個のアナライザーだけが実行されます(型情報を必要とするルールは、自ら何も検出しないことを期待するのではなく、最初から除外されます)。これは実際の実行の代わりにはならず、残る検出結果にも大きな偏りがあります。コーパス全体で測定すると、正確性ルールとパフォーマンスルールはそれぞれ約 4 分の 1 が引き続き検出される一方、見た目のルールでは 4 分の 3 が検出されます。そのため、--parse-only で問題が見つからなくてもほとんど意味はなく、把握する価値のある内容については特に何も分かりません。型情報に依存するルールの多いコードベースでは検出結果の 38%が失われ、見た目のルールが中心のコードベースでは 10%未満が失われます。それに応じて安全性の基準も変わります。ビルドの代わりに、実行によってコンパイルエラー数がベースラインより増えていないことを確認します。コメントガードや重複範囲の保留といった通常の解析レベルの保護は引き続き適用されます。制限事項:条件付きまたは計算された DefineConstants の背後にある #if 分岐は解析されず、ワイルドカードの <Compile> グロブは拒否され、複数フレームワークのパスは 1 つに統合されます。差分を確認してください。このモードで省かれるのは、全フレームワークを対象にしたビルドによる判定そのものです。
CI の設定(SARIF)

ドライランと --report を組み合わせると、ファイルに触れることなく、CI にすべての検出結果を渡せます。SARIF をアップロードすると、各検出結果がプルリクエスト上のインライン注釈になります。レポート内のパスは、対象を含むリポジトリ(対象の上位にある最も近い .git)からの相対パスなので、ツールはどこからでも実行できます。各結果には、ルールの説明とヘルプリンク、3 行のコンテキストを含む検出結果自体のテキスト、SARIF の fixes エントリとしての修正(コードスキャンでは変更案として表示されます)、安定したフィンガープリント、実行の呼び出し記録が含まれます。

  refactor-lint:
    runs-on: ubuntu-latest
    permissions:
      security-events: write     # required by upload-sarif
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
      - run: dotnet tool install --global fsharp-refactor
      - run: fsharp-refactor src/Your.fsproj --dry-run --report findings.sarif
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: findings.sarif
          category: fsharp-refactor

率直にお伝えしたい点が2つあります。まず、ドライランは何も検出しなかった場合も、検出結果があった場合も終了コード 0 を返します。検出結果はヒントであり、エラーではありません。ステップが失敗するのは、ビルドに失敗した場合か、実行がクラッシュした場合だけです。そのため、注釈は情報を提供しますが、処理をブロックしません。検出結果をジョブの失敗条件にするには、明示的なチェックを追加してください。

jq -e '.runs[0].results | length == 0' findings.sarif

次に、ゲートを有効にする前に対象範囲を絞ってください。共有リポジトリで妥当なシグナルを保つには、--categories correctness,performance を指定します(他者のコードベースを参照)。また、fsharprefactor.json を使えば、チームが不要と判断した項目を無効にできます。設定で説明されている抑制コメントを使うと、ゲートとエディターの両方で、個々の検出結果を該当行で一元的に非表示にできます。

他のアナライザーパッケージと並行して実行する

fsharp-analyzers ホストは、指定されたすべてのアナライザーアセンブリを読み込みます。そのため、同じ SDK を基盤に構築された他のアナライザーとこのパッケージを、1 回の実行で実行できます。高コストな処理である FCS による型チェックも 1 回で済み、すべての検出結果が1つのレポートにまとまります。現在のリリースで、このパッケージと同じ FSharp.Analyzers.SDK(0.37.2)を固定しているG-Research のアナライザーを使う場合は、次のようにします。

fsharp-analyzers --project src/Your.fsproj \
  --analyzers-path ~/.nuget/packages/fsharp.refactor.analyzers/<version>/analyzers/dotnet/fs \
                   ~/.nuget/packages/g-research.fsharp.analyzers/<version>/analyzers/dotnet/fs \
  --code-root . --report findings.sarif

ルールコードは重複しません(こちらは FR*、あちらは GRA*)。また、fsharpanalyzer: ignore-line 抑制コメントは、あちらのコードにも使えます。仕組みは個々のパッケージではなく、共有 SDK に備わっています。

SDK は、ホストと読み込むすべてのアナライザーアセンブリのバージョンが一致していることを厳格に要求します。不一致があると、エラーが明示的に通知されるのではなく、アナライザーの読み込みが黙って失敗します。2つのパッケージが異なる SDK のマイナーバージョンを固定するようになったら、別々の CI ジョブに切り替えてください。それぞれが個別の SARIF を生成し、独自の category: でアップロードします(コードスキャンではストリームが分離されます)。その代わり、ジョブごとにプロジェクトの型チェックが1回ずつ必要です。どちらの場合も、修正の適用にはこのパッケージ専用のツールを使います。fsharp-refactor は独自のルールだけを適用し、レポートのみを出力するアナライザーとは競合しません。

他人のコードベース

各ルールは4種類のいずれかで、Refactorings の最後の列に示されています。

種類件数
correctnessコードが見た目どおりに動作していません。競合状態、握りつぶされた例外、解放されずに漏れる破棄可能オブジェクト、決して成立しない比較などです67
performance正しく動作しますが、不要な処理をしています。不要なメモリ割り当て、繰り返される処理、検索で済むのに行われる走査などです41
idiom同じ動作を、F#らしい書き方で記述します。対応する価値があり、まず合意しておく価値もあります。何よりも、チームのコーディングスタイルの問題です56
cosmeticコードの句読点や綴りです。実際にコードを整理しますが、見知らぬ人からのプルリクエストとして歓迎される類いのものではありません17

各バージョンの更新内容は CHANGELOG.md に記載されています。NuGet パッケージには直近6件分が含まれています。

13個のルールには、カテゴリに加えて priority フラグが付いています。後回しにするにはコストが高すぎる、欠陥やセキュリティ上の問題が起こりやすいものです。該当するのは FR0020、 FR0028、FR0032、FR0046、FR0047、FR0048、FR0061、FR0063、FR0065、FR0066、 FR0122、FR0126、FR0127 です。握りつぶされた例外(FR0055)と公開された可変オブジェクト(FR0062)は、通常の correctness のままです。実際に欠陥となるより、悪い習慣であることのほうが多いためです。これらの内容は --notes を指定しなくても表示され、エディターでは警告として示され、SARIF では warning レベルで記録されます。Rules.md の Priority 列に印が付いています。

リポジトリが自分のものでない場合、これは重要です。自分が保守していないプロジェクト全体にすべてのルールを適用し、その結果をプルリクエストとして出すのは、誰かの午後を無駄にするいい方法です。「空の属性引数リストを削除しました」という変更を200ファイルにわたって望む保守担当者はいませんし、差分が大きすぎて重要な変更が埋もれてしまいます。決して破棄されない破棄可能オブジェクトについては、まったく別の話です。

ですから、ゲストとして参加しているコードベースでは、次のようにします。

fsharp-refactor Their.fsproj --categories correctness,performance --dry-run

レビューに時間をかける価値があるのは、このルール群です。自分のコードなら、すべて実行してください。

カテゴリの主張は、推測ではなく測定に基づいています。

dotnet run -c Release --project benchmarks/PerfClaims

手元のマシンで、実行時間とメモリ割り当ての両方の観点から再確認します(GC への負荷もパフォーマンスの一部です)。条件は次のとおりです。performance ルールの書き換えは、少なくとも一方の観点で改善し、idiom ルールの書き換えは同等の性能を保つ必要があります。かつて FR0050 はリストに対して Seq.sum を出力していました。置き換え前の可変ループより約50%%遅く、列挙子の割り当ても発生していました。これをきっかけに、ベンチマークファイル、ルールのモジュールを解決した出力、そして idiom へのカテゴリ変更が行われました。

マルチターゲットプロジェクト

追加の作業は不要です。マルチターゲットプロジェクトは、フレームワークごとに、より対象範囲の狭いものから順に処理されます。

機能修正は、プロジェクト独自の用語を使って、両方の環境に対応します。プロジェクトがオーバーロードより古いフレームワーク(net4x、netstandard2.0)も対象にしている場合、ツールは fsproj の DefineConstants を読み取り、フレームワーク形式の定数(NETSTANDARD21、NET8 のように数字を含むもの)を探します。この定数は、'$(TargetFramework)' == '...' の条件が最新のフレームワークのみを対象としている必要があります。レガシーフレームワークを示す名前(NET48、NET451、NETSTANDARD2_0)は、条件の内容にかかわらず、最初から対象外です。SDK はレガシーのコンパイル時にまさにそれらの定数を定義しますが、そのとき fsproj の解析ではそれを確認できないためです。同じ条件を共有するフレーバー名(SQLProvider では NETSTANDARD21 のすぐ隣で MICROSOFTSQL が定義されています)は対象外です。これらが示すのはフレーバーであり、同じ共有ファイルをコンパイルする兄弟プロジェクトが、レガシーフレームワークでもその定数を定義する場合があるためです。定数が条件を満たし、ファイルですでに条件付きコンパイルが使われている場合、FR0038 と FR0106 は、レガシー側でコンパイルできない修正の代わりに、次のようなペアを出力します。

#if NETSTANDARD21
let orderNumber (s: string) = Int32.Parse(s.AsSpan(6, 5))
#else
let orderNumber (s: string) = Int32.Parse(s.Substring(6, 5))
#endif

定数を新たに作り出すことは決してありません。そのような定数を定義していないプロジェクトには通常の修正が適用され、最後にすべてのフレームワークをビルドして確認します(レガシー側で拒否された修正は元に戻されます)。ツールが条件を完全に読み取れない DefineConstants 要素に含まれる定数('$(TargetFramework)' == 'X' を Or でつないだもの以外)は、推測せず対象外にします。選択した定数の有効な領域内にすでにある行、または手書きの NET*_OR_GREATER がある行には通常の修正を適用します(レガシー側ではコンパイルされません)。#if が一つもないファイルにはこれらを追加せず、エディターでは常に通常の形式を提案します。--no-if-defs を指定すると、プロジェクトが定義している内容にかかわらず、条件付きコンパイルを追加しない実行のために、ペア化を完全に無効にします。

大規模なソリューションでも、次の3つの方法により実行コストを抑えられます。1つの FCS チェッカーを実行全体で共有します(20個のプロジェクトは参照アセンブリの大部分を共有し、それらは一度だけ解析されます)。条件付きコンパイルの定義が同じ状態でスキャンされた共有ソースファイルは、同じ状態でコンパイルする次のプロジェクトでは再スキャンされません。また、ソースに #if がまったく含まれないマルチターゲットプロジェクトでは、単一フレームワークで一度だけスキャンし、最後にすべてのフレームワークをビルドして残りを検証します。

これは無駄な作業ではありません。ターゲットが解決できる機能に応じて適用されるルールは、フレームワークごとに動作が異なります。オーバーロードが存在する net8.0 では s.Contains 'x' が提案されますが、それを持たない netstandard2.0 ターゲットではコンパイルできません。また、各フレームワークでそれぞれ独自の #if 分岐が有効になるため、別の分岐の内側にあるコードは構文解析ツリーに含まれません。1回のパスでは、コードの一部しか確認できません。

対象範囲の狭いものから順に処理することで、すべての環境で有効な修正を、より広い範囲でのみ適用される修正より先に反映できます。各パスの最後にはすべてのフレームワークをビルドするため、汎用性のない修正は成功として通過せず、明確に失敗します。

これにより、fsharp-refactor Your.fsproj を一度実行するだけで、次の結果が得られます。

let has (s: string) =
#if NETSTANDARD2_0
    s.Contains "x"      // still a string: no char overload here
#else
    s.Contains 'x'      // rewritten under the net8.0 pass
#endif

必要に応じて --framework <tfm> を指定すると、実行対象を1つのフレームワークに限定できます。

条件付きコンパイルシンボル(--define)

#if SOME_SYMBOL の下にあるコードは、何かが SOME_SYMBOL を定義しない限り構文解析ツリーに含まれないため、実行時に解析、修正、またはコンパイル状態の維持ができません。dotnet fsi --define: と同じ方法でシンボルを渡します。

fsharp-refactor Your.sln --api-changes --define LOCAL_BUILD

--define は複数回指定でき、; 区切りのリストを受け取ります(--define:A と -d:A も使用できます)。また、fsharprefactor.json の "defines": ["LOCAL_BUILD"] は、実行のたびにシンボルを追加します。実行時に行うすべてのビルドと、型チェックするすべてのスクリプトでシンボルが定義され、実行の開始時にその旨が一度通知されます。

プロジェクトは独自のシンボルを保持します。ツールは追加のシンボルを DefineConstants 環境変数を介して MSBuild に渡し、SDK とプロジェクトの <DefineConstants>$(DefineConstants);...</DefineConstants> がそれに追加します(-p:DefineConstants=... を使うと置き換えられてしまいます。その場合、DEBUG、TRACE、プロジェクト独自の定数が消え、実行時には dotnet build とは異なるコードがビルドされます)。

よくあるのは、どちらの環境にも対応するように書かれたサンプルスクリプトです。

#if LOCAL_BUILD
#r "../../src/MyLib/bin/Debug/net10.0/MyLib.dll"
#else
#r "nuget: MyLib"
#endif

シンボルがない場合、このようなスクリプトは公開済みパッケージの利用者となるため、呼び出しをリポジトリのソースに照らしてチェックできず、使用する可能性のある宣言も元の形のまま維持されます。このような場合、実行時にはシンボルが通知されます。

  (Example.fsx #r's this project's built assembly and could not be checked against its sources, ...)
    its #r of MyLib.dll did not resolve to a reference this pass can redirect; it sits under
    #if LOCAL_BUILD, which this run does not define - pass --define LOCAL_BUILD ...
公開 API(型など)の変更を許可する

公開型や関数シグネチャの変更は、デフォルトでは行われません。プログラムの効率を高めるために、変更が適切な場合もあります。

たとえば、type Item = { X: Option System.Guid } を type Item = { X: VOption System.Guid } に変更するのは、多くの場合理にかなっています。Guid はすでに構造体なので、ここでは ValueOption のほうが適しているためです。しかし、外部の利用者やシリアル化に影響する可能性があります。

--api-changes を指定すると、内部または公開シグネチャを変更する書き換え(タプル化された関数のカリー化(FR0090)と、パラメーターをデータラストに並べ替える変更(FR0091))が有効になり、プロジェクト内のすべての呼び出し箇所、それを #load するスクリプトやビルド済みアセンブリを #r するスクリプト、さらに参照している同じソリューション内の兄弟プロジェクト(通常はテストプロジェクト)、またはそのソースの一つを直接コンパイルする兄弟プロジェクト(リンクファイル)の呼び出し箇所もすべて書き換えられます(このような修正行には note: linked file と表示されます)。指定しない場合、これらの変更は保留され、件数のみが表示されます。また、内包型のヒント(FR0022、FR0069、FR0070、FR0093)の対象を公開型まで広げます。実行範囲外の利用者がいるため、この指定はオプトインです。外部の呼び出し箇所は書き換えられないため、実行範囲内で参照しているすべてのプロジェクトが型チェックに成功する F# プロジェクトである場合に限り、公開関数の形を変更します。参照元の C# プロジェクト、エラーのある兄弟プロジェクト、ソースに対して型チェックできない #r スクリプト、またはその上にソリューションがない単独のプロジェクトがある場合、公開 API は現状のまま維持され、その旨が実行時に通知されます。また、各ルールは、まだ確認できない呼び出し箇所がコンパイルに失敗する場合にのみ適用され、動作がひそかに変わることはありません。単一のソースファイルを指定した場合、これらの変更はすべてスキップされます。ファイルを一つだけ指定したのに、その呼び出し元まで編集されるのは予想外だからです。

このフラグには、他のファイルを編集する修正と、宣言の形を変更する修正の対象を公開宣言にまで広げる機能という、分離可能な二つの要素があります。要求が必要なのは前者だけです。参照されないアセンブリ(実行可能ファイルやスクリプト)では、後者は単独で適用されます。ライブラリでは、fsharprefactor.json に "publicApi": false を指定すると有効になります。いずれの場合もエディターに変更が反映されますが、エディターでこのフラグを指定する必要はありません。 公開 API の範囲は API ですか?を参照してください。

リファクタリング

各ルールは Rules.md に記載されています。まず、確認しやすいよう、各ルールの適用対象、提供する修正、デフォルトで有効かどうか、修正に --api-changes が必要かどうかをまとめた一行の表があります。続いて各ルールのセクションで、その根拠を詳しく説明しています。テストによって、このファイルとルール一覧の整合性が保たれています。

ロードマップは「F# refactoring possibilities」に基づいています。

設定

ルールは、オプションの fsharprefactor.json を使ってリポジトリごとに無効化できます。ファイルは解析対象の各ファイルから上方向に検索され、リポジトリのルートで停止します(最も近いファイルが優先されます)。キーには、ルールコードまたはアナライザー名を大文字・小文字を区別せずに指定します。ファイルの形式が不正でも無効化を無視するため、エディターが壊れることはありません。コメントと末尾のカンマも使用できます。

fsharp-refactor --create-config を実行すると、設定ファイルが作成されます。このビルドが認識するすべてのルールと、各実行レベルのキーが、それぞれ現在のデフォルト値とともに出力され、各項目にはコメントが付いています。行を編集するまでは、設定ファイルを置いても何も変わりません。納得できない設定だけを切り替え、残りは削除しておけば、デフォルト値の変更に追従できます。既存の設定ファイルがある場合は、上書きを拒否します。

フォーマッターの設定も考慮されます。ルールが実行のたびに設定を元に戻す場合に適用されます。.editorconfig で fsharp_space_before_lowercase_invocation = false(Fantomas がデフォルトの f (x) ではなく f(x) と書き出す設定)にすると、このファイルまたは --codes で名前を指定して有効にしない限り、FR0013 は報告されません。

Fable.Core を参照する Fable プロジェクトでは、一部の判定が変わります。公開値は JavaScript、Python、Dart、または Rust にエクスポートされるため、FR0130 は手を加えません。Fable のすべてのターゲットでは文字列が序数で比較されるため、FR0038 はカルチャに関する注記を省略します。JavaScript にバインドされたプロジェクト(Fable.Browser.*、Fable.Node、Fable.Deno、または Fable.Promise への参照)はシングルスレッドなので、FR0162 は報告されません。ブラウザーにバインドされたプロジェクトはユーザー自身の時計で動作するため、FR0121 は DateTime.Today と DateTime.Now に手を加えません。表現に関するルールは引き続き有効です。Rust ターゲットでは [<Struct>] レコードが値として格納されるためです。

{
  "rules": {
    "FR0003": false,
    "conversionMove": { "enabled": false }
  }
}

しきい値を調整できるルールは、同じオブジェクト値エントリから数値プロパティを読み込みます。FR0114 では thenAtLeast(デフォルトは 20)で、反転を提案するために必要な then ブランチの最小行数を指定します。また、elseAtMost(デフォルトは 3)で、else ブランチの最大行数を指定します。

{
  "rules": {
    "FR0114": { "enabled": true, "thenAtLeast": 30, "elseAtMost": 2 }
  }
}

FR0060 では maxAttributes(デフォルトは 4)で、1 つの [<A; B>] 括弧内にまとめられる属性の数を指定します。また、wrapColumn(デフォルトは 110)で、結合後の行の最大幅を指定します。どちらも正しさではなく、プロジェクトのスタイルに関する設定です。いずれかの上限を超える場合、ルールは結合を行いません。

{
  "rules": {
    "FR0060": { "enabled": true, "maxAttributes": 6, "wrapColumn": 120 }
  }
}

一部の設定はしきい値ではなくオン/オフの切り替えで、JSON の bool と 1 のどちらも受け付けます。FR0029 では tailLines(デフォルトは 40)で、コンパイラーが警告していないタスクにおいて、最後の await の後に何行の await しない処理があれば末尾を抽出するかを指定します。FS3511 がタスクを示している場合は、apply ツールがビルド結果からその情報を読み取り、抽出を常に提案します。また、hoistReturnOnAsync(デフォルトは false)を設定すると、FS3511 の助言は変更せず、戻り値のホイストだけを async { } にも適用します。

{
  "rules": {
    "FR0029": { "tailLines": 25, "hoistReturnOnAsync": true }
  }
}

FR0174 では pipelines(デフォルトは false)を指定します。このルールは、IQueryable 上の C# スタイルの LINQ チェーンからコピーを外に移動します(q.ToList().Where(f) → q.Where(f).ToList())。q |> Seq.toList |> List.filter f のようなパイプラインでは、query { } と同様に、行がメモリに読み込まれる箇所が明確に示されるため、この設定を有効にしない限り変更しません。

{
  "rules": {
    "FR0174": { "pipelines": true }
  }
}

FR0065 では dropLegacyProtocols(デフォルトは false)を指定します。Ssl3/Tls/Tls11 を廃止すると、プロセスがリモートエンドポイントとネゴシエートする内容が変わるため、デフォルトではエディター上での提案にとどめます。この設定を有効にすると、無人実行時に使用されなくなったプロトコルをフラグからコメントアウトできます。--api-changes ではこのルールは意図的に有効になりません。このフラグは再コンパイルが必要な呼び出し側に関するもので、別のリスクだからです。

{
  "rules": {
    "FR0065": { "dropLegacyProtocols": true }
  }
}

FR0015 では perCall(デフォルトは true)を指定します。ループ内の Regex 呼び出しでは、要素ごとにパターンが再解析されるため、常にホイストされます。通常の関数本体内では、呼び出しごとにランタイムの正規表現キャッシュを検索するコストがかかり、再解析が行われるのは、そのキャッシュ(15 個のパターン)が入れ替わるときだけです。それでもデフォルトではホイストされます。関数はモジュールよりも何度も呼び出される可能性がはるかに高く、モジュールの初期化時に束縛が設定されても一度も読み取られないケースがあるためです。また、F# は最初にアクセスされた時点でモジュールの束縛を構築するため、参照されないホイスト済みの Regex は構築されません。false に設定すると、ループ内のホイストだけが残ります。

{
  "rules": {
    "FR0015": { "perCall": false }
  }
}

FR0035 は minElements(デフォルトは 8)を受け取ります。これは、プローブ対象のモジュールレベルのリストまたは配列リテラルで、HashSet の効果が出るまでに必要な、明示的に記述された要素数です。それ未満では、すべてのプローブが見つからない場合でも、線形プローブのほうが高速です。また、setMinElements(デフォルトは 16)も受け取ります。これは、リテラルをその場で F# Set に変換するために必要な要素数です。Set は比較ツリーで、効果が出るまでに時間がかかるため、それより短い場合は代わりに HashSet が使われます。

{
  "rules": {
    "FR0035": { "minElements": 8, "setMinElements": 24 }
  }
}

パスも除外できます。標準の除外対象(paket-files、.paket、node_modules)に追加する形で指定します。これらは、コンパイルに含まれる生成コードやベンダー提供コードを対象としています。

{
  "ignorePaths": [ "generated", "external/imported" ]
}

単独の名前はパスセグメント全体に一致します。スラッシュを含むエントリは正規化されたパスのどこにあっても一致します。* を含むエントリは glob です。* はセグメント内にとどまり、** はセグメントをまたぎます(*.g.fs、src/generated/**)。無視されたファイルは解析されず、適用ツールによる一括処理でも型チェックされません。paket を多用するソリューションでは、誰も「修正」したくないベンダー提供ソースが大量にあります。従来の // <auto-generated> マーカーで始まるファイルは、配置場所にかかわらず自動的にスキップされ、obj/ 以下もすべてスキップされます。

個々の検出結果は、F# analyzer SDK 独自の抑制コメントを使って、その場で抑制できます。エディターも同じコメントを認識するため、1 つのコメントでライトバルブと適用ツールの両方を抑制できます(抑制された検出結果は報告も修正もされません)。

// fsharpanalyzer: ignore-line-next FR0106
let orderNumber (s: string) = Int32.Parse(s.Substring(6, 5))

let inline dodgy (s: string) = s.Substring(0, 3) // fsharpanalyzer: ignore-line FR0106

// fsharpanalyzer: ignore-file FR0031, FR0038
// fsharpanalyzer: ignore-region-start FR0002
// fsharpanalyzer: ignore-region-end

抑制コメントは簡単に使えてしまうため、チームによっては、命名上の細かな指摘と同じように、正確性に関する検出結果を句読点だけの一行で抑制できることを望まない場合があります。「suppressions」ポリシーで、この線引きを行います。

{ "suppressions": "no-correctness" }
  • "all"(デフォルト)- すべての抑制コメントで該当する検出結果を抑制します。
  • "no-correctness" - 正確性カテゴリのルールに対するコメントは、設定されていても報告されます。イディオム、外観、パフォーマンスに関する抑制は引き続き機能します。上書きされた検出結果は自動 FIX されません。明示的なコメントを無視してコードを書き換えることはせず、対処されるか、そのコメントを尊重する価値があると判断されるまで報告されます(また、--fail-on-findings は失敗します)。
  • "none" - すべての抑制コメントが設定されていても報告されます。

コマンドラインの --honor-suppressions は、その実行に限り、ポリシーを "all" に上書きします。開発者のマシンでは抑制を無効にしつつ、パイプラインでは有効にしたいリポジトリは、設定に "no-correctness"(または "none")をコミットし、CI でのみこのフラグを渡します。ポリシーにかかわらず、実行サマリーにはコメントによって抑制された件数が表示されます。抑制がひそかに行われることはありません。このポリシーが適用されるのはこのツールだけです。エディターは SDK のコメントをネイティブに認識するため、ライトバルブは引き続き抑制できます。

公開サーフェスは API ですか?

設定の 2 つのキーによって、--api-changes がフラグなしでどこまで適用されるかが決まります。これらは異なる 2 つのリスクを制御するため、分けて考える価値があります。

{ "publicApi": false }

F# では宣言はデフォルトで公開されるため、構文木における public は通常、明示的な選択ではなく、何も決めていない状態を表します。スコープによって制御されるルール、つまり宣言のコンパイル後の SHAPE をその場で変える修正([<Struct>]、[<Literal>]、名前付きユニオンフィールド、フィールドの option から voption への変更)を行うルールは、別のアセンブリの利用者に変更が見えてしまい、ここではその影響を確認できないため、公開宣言には適用されません。"publicApi": false は、そのような利用者がいないことを示します。アプリケーション、内部ツール、リーフプロジェクトなどが該当します。この場合、これらのルールでは公開宣言を内部宣言として扱います。適用ツールだけでなく、--api-changes を利用できないエディターでも同様です。この設定によって、解析対象のファイル外での編集が許可されることはありません。

設定がない場合、コンパイル設定に基づいて判断します。 OutputType が Exe または WinExe の場合、外部リンカーはありません。つまり、その公開宣言を参照できるものがないため、リーフとして扱われ、ゲートは自動的に開きます。ライブラリはリーフではなく、ゲートは閉じたままです。これを上書きするには "publicApi": true を記述します。公開型を独自にシリアライズする実行ファイルや、リフレクションでプラグインを読み込む実行ファイルでは、保守的な動作に戻す必要があります。 スクリプトは、コンパイル全体としてではなく、ファイルごとに評価されます。 .fsx は最終的なリーフです。何も読み込まず、何からも読み込まれないため、 そのファイル自身の宣言は対象になります。#load するファイルは対象外です。 そのソースは、それを所有するプロジェクトのものであり、おそらくライブラリです。 スクリプトがそれを読み込んでいるからといって、ほかに何がそれをコンパイルするかは わかりません。

評価対象になるのは、実行対象のソリューションまたはプロジェクトの フォルダーとそのサブフォルダーにある .fsx です。ドライブ全体ではなく、 ignorePaths で除外されたパスも対象外です。このリポジトリが無視するよう ツールに指示したパスは外部コードであり、外部コードがこのリポジトリの 宣言の形を決めることはありません。つまり、この実行が対象とするツリーの 内部にあるスクリプトが、移動した名前を呼び出したまま取り残されないことを 保証します。

{ "apiChanges": true }

もう一つのリスクは、ほかのファイルも編集しなければならない修正です。 関数のカリー化(FR0090)やパラメーターの並べ替え(FR0091)を行い、 プロジェクト内のすべての呼び出し箇所を書き換える場合です。これは --api-changes を常時適用する設定で、リポジトリにおいて常に正しい選択となる 場合に使います。この設定はフラグが行うすべての変更を対象とするため、 publicApi: false も意味します。フラグを指定して開始した実行には影響せず、 設定でできるのは対象を広げることだけで、フラグを無効にすることはできません。 この設定を適用すると、実行時にその旨が表示されます。

対応する .fsi がある場合は、どちらよりも優先されます。シグネチャファイルは、 エクスポートされる内容について作者自身が示した宣言です。そこに記述できる val private も含まれるため、シグネチャが宣言する名前については、 隣接する形状変更は適用されません。また、FR0092(定数の failwith メッセージ)は 可視性のルールではありません。リスクはテストや呼び出し元がそのテキストを 読み取ることにあるため、フラグを指定した場合にのみ適用されます。

ゲートで保留された内容は、隠されずに報告されます。 実行の最後に、たとえば次のように表示されます。

  7 finding(s) held back by scope: 4 FR0070, 3 FR0022 - public declarations
  this run may not reshape. Set "publicApi": false in fsharprefactor.json if
  nothing outside this assembly links to them or serializes them.

このため、判断は推測するものではなく、明示的な判断になります。一度回答すれば、 ツールがすべて適用します。ツールが書ける内容を、誰かが手作業で書き直す必要は ありません。

エディターでは、注意書き付きで引き続き提案されます。 電球アイコンは、 その質問に実際に答えられる人が、該当箇所ごとに同意するためのものです。 手作業で編集する代わりにクリックするだけで済むため、エディターでは パブリック宣言にも次の指摘が表示されます。

Union 'Shape' は小さな値型のみを保持しています。[<Struct>] を指定すると、 値ごとのヒープ割り当てを回避できます。パブリックな形状が変わります。 このアセンブリの外部からリンクされず、シリアライズもされない場合に限り安全です (JSON、XML、protobuf — ツールでは判別できません)。

最後の注意書きは、単なる謙遜ではありません。シリアライズは検出できません。 System.Text.Json、Newtonsoft、XmlSerializer、DataContract、protobuf、 MessagePack、そして利用側がリフレクションで組み込んだものはすべて、 コンパイル済みの形状を読み取ります。その一部だけを列挙するガードを設けると、 ほかのものを気づかれないまま壊してしまいます。そのため、ツールは形状変更が シリアライズに対して安全だとは推測しません。変更内容を示し、判断は読者に 委ねます。適用ツールは、尋ねる相手がいないため、該当数を数えるだけです。

無効にしたルールは分析自体をスキップするため、大規模なコードベースでは パフォーマンスを調整する手段にもなります。内部ではすべてのアナライザーが、 ファイルのバージョンごとにメモ化された AST の走査を一度だけ共有します。 そのため、アクティブなルールの数にかかわらず、エディターがキー入力ごとに 行う走査は一度だけです。

次の二つを除き、すべてのルールはデフォルトで有効です。

  • FR0099(行末のセミコロン)は、該当するものを含むすべてのファイルを字句解析しますが、 該当箇所が見つかることはまれです。見た目だけの既定設定としては、コストが見合いません。
  • FR0002(match オプションから Option コンビネーターへの変換)は、実測済みの書き換えのうち、 YOUR code を遅くする唯一のものです。ベンチマークの比較対象では +53% 遅くなり、 呼び出しごとにクロージャの割り当てが発生します。読みやすくはなりますが、実行コストがかかります。 そのトレードオフがコードベースに合う場合に有効にしてください。 "FR0099": true/"FR0002": true のいずれかを有効にするか、明示的に指定してください。apply ツールでは、--codes FR0002 の指定がデフォルトで無効な状態と設定による無効化の両方より優先されます。ルール名の指定は実行を求める指定です。一方、--categories フィルターは意図的にそのようには動作しません。--categories idiom を指定すると、有効になっているイディオムルールが実行され、デフォルトで無効になっているルールがひそかに有効になることはありません。

同じファイルで、FSharpLint のヒント構文を使用したカスタム FR0012 用語書き換えルールを追加できます(1 文字の識別子はメタ変数です)。

{
  "hints": {
    "add": [
      "Option.isSome x |> not ===> Option.isNone x"
    ]
  }
}

カスタムルールにも組み込みルールと同じ安全対策が適用されます。必要に応じて束縛が括弧で囲まれ、右辺でメタ変数が削除または重複されるルールは、純粋なアトムに対してのみ実行されます(副作用を伴う式が破棄されることはありません)。


改善する

貢献を歓迎します。このセクションではアナライザーの開発について説明します。上記はアナライザーの使用方法についてです。

変更を試す

アナライザーをビルドし、いずれかのホストで NuGet キャッシュの代わりにビルド出力を指定します。

エディターでは、対象リポジトリの .vscode/settings.json を使用します。

{
  "FSharp.enableAnalyzers": true,
  "FSharp.analyzersPath": ["<path-to>/FSharp.Refactor.Analyzers/bin/Debug/net8.0"]
}

たとえば、match x with | true -> 1 | false -> 2 を含む F# ファイルを開くと、if x then 1 else 2 を提案するヒントが表示されます。

または、CLI から実行します。

fsharp-analyzers --project YourProject.fsproj --analyzers-path src/FSharp.Refactor.Analyzers/bin/Debug/net8.0 --code-root .

注:アナライザーは、ホストの FsAutoComplete と互換性のある FSharp.Compiler.Service に対してビルドする必要があります。このプロジェクトでは現在、FSharp.Analyzers.SDK 0.37.2(FCS 43.12.201)を固定しています。更新する際は、SDK のバージョン対応表を確認してください。

ビルドとテスト

dotnet build
dotnet test

このプロジェクトは自分自身で使うツールを開発しています。コミットする前に、次を実行します。

dotnet tool restore
dotnet fantomas src tests
dotnet dotnet-fsharplint lint src/FSharp.Refactor.Analyzers/FSharp.Refactor.Analyzers.fsproj
dotnet dotnet-fsharplint lint src/FSharp.Refactor.Tool/FSharp.Refactor.Tool.fsproj
dotnet dotnet-fsharplint lint tests/FSharp.Refactor.Tests/FSharp.Refactor.Tests.fsproj
dotnet dotnet-fsharplint lint tests/FSharp.Refactor.PropertyTests/FSharp.Refactor.PropertyTests.fsproj

さらに、アナライザーを自身のソースコードに対して実行し、検出結果がゼロであることを確認します。 アナライザーだけでなく、両方のプロジェクトが対象です。出荷する F# 製の apply ツールも対象ですが、 長い間チェックされていませんでした。

dotnet tool run fsharp-analyzers --project src/FSharp.Refactor.Analyzers/FSharp.Refactor.Analyzers.fsproj --analyzers-path src/FSharp.Refactor.Analyzers/bin/Debug/net8.0 --code-root .
dotnet tool run fsharp-analyzers --project src/FSharp.Refactor.Tool/FSharp.Refactor.Tool.fsproj --analyzers-path src/FSharp.Refactor.Analyzers/bin/Debug/net8.0 --code-root .

テスト入力は文字列リテラルなので、フォーマッターがテスト対象の意図的に整形されたソース断片に手を加えることはありません。複数行の入力には、 fsharp ヘルパーを使ったインデント付きブロックを使用します(tests/FSharp.Refactor.Tests/TestSource.fs)。 閉じる引用符のインデントは各行から取り除かれるため、ルールには断片が列 0 から始まるように見えます。また、Fantomas がブロックを移動しても内容は変わりません。

let source =
    fsharp
        """
        module Test
        let f x = match x with | true -> 1 | false -> 2
        """

parse は、入力を解析できない場合にテストを失敗させ、parseAndCheck は入力の型チェックに失敗した場合にテストを失敗させます。型付きルールは型エラーのあるファイルでは何も検出しないため、壊れた入力に対する「検出なし」のテストは、誤った理由で成功する可能性があります。壊れた入力そのものがテストの目的である場合(FR0077 と FR0145 はコンパイルエラーを修正します)は、parseAndCheckAllowingErrors を呼び出します。修正後のソースは assertTypechecks "Patched source" patched でチェックします(解析のみを行うルールでは assertParses を使います)。失敗した場合は、typechecksCleanly の真偽値だけではなく、コンパイラーのエラーがソースの上に一覧表示されます。

1 行または 2 行の入力であれば、エスケープされた "...\n..." リテラルのままでかまいません。定数である必要がある場合(属性の引数、[<Literal>]、printf 形式)、また、タブ、\r、テストに必要な末尾の空白が含まれる場合も、エスケープされたリテラルのままにしてください。エディターで削除されてしまう可能性があるためです。

例ベースのテストスイートに加えて、tests/FSharp.Refactor.PropertyTests には、生成されたプログラムを対象とする FsCheck スイートがあります。そこでは、解析のみを行うルール向けにそれぞれ形を整えた宣言のモジュールと、3 つの整数を使う真偽値項を用い、インタープリターをオラクルとします。このスイートのプロパティは、ルールが保証する不変条件です。解析のみを行うルールの修正でファイルの解析可能性が保たれること、パーサーが破損したテキストから復元した構文木に対してルールが例外を投げないこと、編集を 1 つずつ適用すると不動点に達すること、そして真偽値の書き換え(FR0108/FR0109、否定と De Morgan のヒント、FR0010、FR0013)を各段階で適用しても関数の真理値表が保たれることを確認します。カバレッジテストでは、生成器が各ルールを検証できるようにします。ある形状が対象のルールを発動しなくなった場合は、その旨が示されます。

型付きルールにも同じ方法を Families/ で適用します。ルールファミリーごと(async、loops、strings、objects、など)に 1 つのファイルを用意し、各ルールが想定する形状を保持します。名前、リテラル、バリアントはランダムに選ばれ、生成された各モジュールは、ルールが条件として確認する名前空間(テストフレームワークの属性やロガーなど)を宣言するスタブファイルを前置して、スクリプトとして型チェックされます。各ファミリーについて、次の 4 つのプロパティを一度に確認します。生成されたプログラムが型チェックされること、修正を単独で適用しても解析可能性が保たれること、互いに重ならない修正をすべて適用してもプログラムが型チェックされること、そしてキー入力の途中で破損したプログラムに対してルールが例外を投げないことです。各形状は、名前が付けられたルールに対して単独でチェックされます。ただし、!FR0162 と記された形状は例外で、その形状が誤検出防止のために静かに動作すべきルールに対してチェックされます。これは実際のリポジトリから学んだものです。最後のテストでは、カタログ全体を対象に確認します。すべてのルールに形状があるか、到達できる形状がない理由(コンパイルエラーを修正するルール、コンパイラーが拒否するタブ、api パスのプロジェクト間マイグレーション)が明記されている必要があります。

設計原則

  1. ユーザーコードを決して壊しません。 修正を安全に適用できることが確実な場合にのみ提案します。判断が微妙なケースでは、提案を出しません。修正は最小範囲を対象としたテキスト編集で、エディターによって適用されるため、元に戻す操作は常にネイティブの単一ステップで行えます。
  2. 編集は最小限にします。 編集範囲外の元のフォーマットには手を加えません。ファイル全体を再フォーマットすることはありません。
  3. コアは純粋に、アダプターは薄く保ちます。 各リファクタリングは ParsedInput -> ISourceText -> Suggestion list という純粋関数であり、ソース文字列を直接使ってユニットテストします。Analyzers.fs にある SDK アナライザーのエントリーポイントは、すべて1行です。
  4. ヒントは、イディオマティックな F# への誘導に限ります。 各アナライザーは、a -> b の形で、よりイディオマティックな形式である b へ書き換えます。コードをイディオマティックな F# から遠ざけるヒントは提供しません。そのため、提案の重大度は警告ではなく Hint です。これは欠陥ではなく改善の機会を示すものであり、CI を妨げることもありません。どちらの方向もよりイディオマティックとは言えない、真に可逆的な書き換え(if ↔ match、タプル形式 ↔ カリー化形式など)は、ユーザーが呼び出す refactor.rewrite アクションとして FsAutoComplete のコード修正インフラストラクチャに属します。そのため、ここではなく、そちらに追加してください。

私たちのビジョンと同分野の他のプロジェクト

AI エージェントとの互換性:このプロジェクトは、生成された Python に見られる問題と F# コードの問題を区別します。一方、関数の長さや循環的複雑度など、以前からある一部のルールは、今後あまり重視されなくなると考えられます。 このプロジェクトは、イディオマティックな F#、コードのパフォーマンス、ベストプラクティスに重点を置いており、コードの構造、命名、保守性への関心は低めです。

このプロジェクトは他の製品との互換性を目指しているため、提案された変更が互いに行き来して競合することはありません。

ツール共通するルール状況
FxCop と MS Code Analysis多数MinimumRecommendedRules と、F# に関連するパフォーマンスなどのルールをいくつか実装しています。
FSharpLint多数ルールを列挙するだけでなく、クイック修正と自動修正を提供します。ルールはこのプロジェクトと互換性があります。
Resharper F#多数多くの機能が共通していますが、AST はまったく異なるものを使用しています。
Resharper C#一部Resharper はオブジェクト指向に重点を置いていますが、私たちは関数型プログラミングに重点を置いています。クロージャによるキャプチャなど、C# での問題の多くは F# にはありません。
Linq.Expression.Optimizer一部私たちはコンパイル時に最適化しますが、このツールは実行時のコードを最適化します。
SonarQube少数SonarQube のルールの大半は、Java から移植された、企業開発に関する独自の見解に基づくものです。ただし、.NET に関連する共通のルールもいくつかあります。
G-Research FSharp Analyzersあまりありません保守性に重点を置いた優れたルールがあります。重点分野は異なりますが、併用できます。
Fantomasなし重点分野が異なります。Fantomas はコードレイアウトツールです。互換性があるため、両方を使用できます。
FSharp.Analyzers.SDKなし私たちのツールである fsharp-refactor は、FSharp.Analyzers.SDK を基盤に構築されています。
CSharp.Refactorサブセットこのツールの小さな姉妹版で、C# プロジェクトで実行できます。

コメント

MCP は初めてですか?

プロトコルの概要から最初のサーバーの導入まで、よくある疑問にまとめて答えます。

AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。