構文を理解する構造的差分解析ツール Difftastic
(difftastic.wilfred.me.uk)- Difftastic は行単位ではなく、コードの 構文構造 を基準にファイルを比較する CLI diff ツール
- 内部では tree-sitter でコードをパースし、フォーマット変更で行が変わっても実際に異なる式をより適切に見分けられる
- コードフォーマッタが項目を複数行に分割したりラッパーが追加された場合でも、変更された構造と維持された内部内容を分けて表示できる
- Git 設定ガイドとインストール文書が提供されており、複数のプログラミング言語と JSON・YAML のような構造化テキスト形式をサポートする
- MIT ライセンス で公開されており、ダウンロード、修正、共有が可能で、既存の diff フローに構造的比較を加える用途に適している
行ではなく構文構造を比較する
- Difftastic はファイルを構文ベースで比較する CLI diff ツール
- 一般的な行単位 diff よりも、コード構造を理解した結果を示すことに重点を置く
- コードは tree-sitter でパースする
- 行中心の diff では変更されたように見える内部式についても、実際の変更有無を見分けられる
- コードフォーマッタがコードを複数行に分割する場合でも 実際の変更点 を示せる
- ラッパーが追加されると区切り記号を正確にマッチングする
- 内部内容も同時に変わっていても 追加されたラッパー を表示できる
@@ -5,6 +5,7 @@のような diff hunk 構文の代わりに、変更前後ファイルの 実際の行番号 を表示する
対応言語と Git 連携
- プロジェクト資料は GitHub、Manual、Install で提供されている
- 対応プログラミング言語の例は次のとおり
- C++, C#, Clojure, Dart, Erlang, Go, Haskell, Java, JavaScript, Kotlin, Lisp, Lua, OCaml, PHP, Python, R, Ruby, Rust, Scala, TypeScript
- 全一覧はマニュアルの 対応言語一覧 で確認できる
- 対応ファイル形式の例は次のとおり
- HCL, HTML, JSON, YAML
- 全一覧はマニュアルの 構造化テキスト形式一覧 で確認できる
- Git とあわせて利用でき、マニュアルに Git 設定 の案内がある
- Difftastic は MIT ライセンス で公開されており、ダウンロード、修正、共有が可能
1件のコメント
Hacker News のコメント
すでにご存じの方もいるかもしれませんが、このツールは tree-sitter(https://tree-sitter.github.io/tree-sitter/) の上に作られており、LSP が解析に対して行ったことを、パースに対して行っているようなものです
コードを抽象構文木(AST)に変換し、その AST をエディタや diff ツールのようなクライアントが利用できる標準インターフェースを提供します
こうしたツールは数十の言語を直接サポートする必要がなく、tree-sitter だけをサポートすれば、tree-sitter が対応するすべての言語で自動的に動作できます
新しい言語を開発するなら tree-sitter パーサを作ればよく、そうすれば tree-sitter を扱うすべてのツールがその言語をサポートできるようになります
この 2 つの大きな革新のおかげで、この種のツール改善が爆発的に増えています。今やエディタや diff ツールなどは、互いに同じ作業を繰り返さなくても数十〜数百の言語をサポートできるので、本当にすごいことです
ちなみに私たちのチームメンバーの何人かは OCaml tree-sitter バインディングを保守しており、Semgrep の作業の一環として文法にも頻繁に貢献しています。Semgrep はコード検索と、コード片そのものであるクエリを AST マッチャとしてパースするために tree-sitter を使っています
リンターを作るときは、対象言語のランタイムも一緒に持ってこなければならないことがよくあります。たとえば Python で組み込みの
astモジュールを使ってパーサを作ると、言語バージョンや機能を合わせる必要があるため、Python 2.7 で実行される Pylint では Python 3 のコードをパースできません複数の言語を対象にすると、最初に思うよりずっと面倒になります
tree-sitter 以前は、最新の構文に追随している保証があるため、言語組み込みの AST ツールを使うのが最善である場合が多くありました
個人的には、tree-sitter の天才的な点は、従来の文法方式よりも言語パーサを最新状態に保つことをはるかに簡単にしたところにあります
tree-sitter の設計上の選択についてもっと知りたいなら、Max Brunsfield の Strange Loop での発表を強くおすすめします: https://www.youtube.com/watch?v=Jes3bD6P0To
その結果、difftastic 以外にも neovim、Zed、Semgrep、GitHub コード検索など、いくつもの新しいツールが tree-sitter の上に作られました
VHDL 用の tree-sitter 文法とハイライト用クエリ一式を書いてみましたが、VHDL の文法を tree-sitter で表現するのは困難が多かったです
LSP はクエリ指向コンパイラのための標準インターフェースを使うことで、M 個のエディタと N 個の言語の間に必要になる M×N の統合問題を実際に解決します
tree-sitter はその問題を解決するというより、エディタやツールで N 個の統合を書く作業をずっと簡単にしてくれるものです
Common Lisp は完全な Lisp 実装なしにはパースできず、Haskell の文法は十分に複雑なため文法が不完全で、C/C++ はプリプロセッサだけを見ても正確にパースするのが難しく、Perl のパースはチューリング完全です
他のところで出ていた提案のように、新しいエコシステムでパーサを書き直させるのではなく、既存のパーサが追加出力として生成できる形式を定義するほうがよさそうです
そうすれば difftastic で、見た目上の行ではなく文単位の変更を表示できそうです
エッセイでは見た目上の行を基準にした diff は変です。たいてい変更された文は、見た目上の行の途中から始まるからです
cargo でインストールした場合、difftastic と関連ツールを更新する方法は次のとおりです
cargo install cargo-updatecargo install-update --listcargo install-update --allcargo でインストールできる面白い Rust プロジェクトもあります
https://mise.jdx.dev/ mise-en-place は https://asdf-vm.com/ の高速で柔軟な代替品です
https://github.com/ajeetdsouza/zoxide は優れた
cd代替ツールで、移動した場所を保存しておき、"z hel"のような部分一致だけで"~/projects/helloworld"に移動できますhttps://github.com/bootandy/dust は
duを補完するツールで、どのディレクトリがディスク容量を多く使っているかを示しますls、https://github.com/ClementTsang/bottom はグラフ付きのhtop、https://github.com/sharkdp/bat は構文ハイライト付きのcatですlsですdu系の中では dua-cli が一番気に入っています。ncduのクローンで、対話型のターミナル UI ですncduにライトモードがなかったので探していたところ見つけましたncduが断然最高のdu代替ツールですgit のようなツールが、なぜこういうものを標準で提供していないのか、ほとんど理解できない。
difft を1年ほど使ってきたが、最大の不満は difft がない環境で他の diff ツールに戻りにくくなることだ。
意味ベースの diff ツールも研究されているのか気になる。たとえば、構文は変わっているが意味は同じ場合を扱うツールのことだ。
一般的な問題としては難解に見えるが、小さな DSL や一部言語のサブセットでは可能だったり有用だったりするかもしれない。
diffを使うのが妥当だと思う。git を実行しようとするほぼすべてのシステムにすでにあり、速く、小さく、誰もが基本的な使い方を知っている。
それでも、そのデフォルトを簡単に変更できるのは良いことだ。
コマンドラインユーティリティではなく、VS Code 拡張と GitHub App だ。
仕組みや difftastic との違いが気になるなら、https://semanticdiff.com/blog/semanticdiff-vs-difftastic/ を見るとよい。
それでもデフォルトにすべきではないと思う理由の1つは、空白の差分を隠す点だ。設定できるかどうかはまだ確認していない。
@@ -5,6 +5,7 @@という構文を読めるかと尋ね、Difftastic は変更前後のファイルの実際の行番号を表示する、という説明には完全に同意する。すぐに導入して
git diffを試してみたが、とてもよく動く。GitHub がこういうものを出していないのは、プロダクト革新の大きな空白のように見える。
インデントだけが変わっても助けてくれる機能がなく、たいてい巨大な削除と追加として表示される。
GitHub の diff ビューアはもっと賢くなれるし、そうあるべきだ。
実際、
foo.cpp.inとfoo.mk.inが違うことすら区別できない。どんな
foo.tでも Perl として宣言され、修正する方法もない。10年前からあるチケットも存在する。実際に TS の主要開発者たちも、文法の正確性にあまり気を使いすぎないよう勧めている[1]。
GitHub や同じような立場のサービスには、この制約が致命的になり得るように見える。
[1] https://github.com/tree-sitter/tree-sitter/issues/130#issuec...
このツールに乗り換えようとしたが、変わっていない部分にも妙な書式を使うのでうるさすぎると感じた。
結局 icdiff[0] に戻った。
[0]: https://github.com/jeffkaufman/icdiff
以前 Semantic Merge[1] を見つけたが、オープンソースではなかった。
これはマージはせず diff だけだが、少なくともオープンソースで、diff がはるかに見やすいので、すでにデフォルトに変更した。
マージ機能へ拡張する計画があるのか気になる。
[1] https://docs.plasticscm.com/semanticmerge
AST マージは難しい問題であり、difftastic は扱わないとのこと。
また AST diff は、テキスト diff の観点では情報を失うプロセスでもある。difftastic は構文上意味のない空白を無視するが、マージには空白の追跡が必要だ。
大きなチームで C# プロジェクトの厄介なリファクタリングをしていた時期には、本当に救世主のようなツールだった。
以前の議論:
https://news.ycombinator.com/item?id=27768861 (297 points | 3 years ago | 61 comments)
https://news.ycombinator.com/item?id=32746258 (698 points | 2 years ago | 90 comments)
https://news.ycombinator.com/item?id=30841244 (983 points | 2 years ago | 219 comments)
素晴らしいツールのようだが、大きな XML ファイルをいくつか確認してみると、変更された行は通常の緑、変更された属性は太字の緑で表示され、視覚的に区別しにくかった。
ドキュメントでは diff スタイルを変えたり、太字の場合に別の色を指定したりする方法を見つけられなかった。
方法を知っている人がいるか気になる。
この機能を支持すると、この issue[1] にコメントした。
望んでいる機能が同じカテゴリなら、そこに意見を添えてもよいし、十分に別の機能だと思うなら新しい issue を開いてもよさそうだ。
[1]: https://github.com/Wilfred/difftastic/issues/611
Arch Linux のパッケージは 7MB なのに、インストール後は 80MB に展開され、
difftバイナリだけでも 78MB ある点が興味深いLZ4 圧縮を有効にした ZFS データセットでは、
duは 17MB と表示されるとのことバイナリ内でこれほどよく圧縮できる何かを、なぜそのまま圧縮しておかないのか気になる。RAM 上で展開すれば、もっと速くロードできる可能性もありそう
Processed 1 file, 614 regular extents (614 refs), 0 inline.Type Perc Disk Usage Uncompressed ReferencedTOTAL 14% 10M 77M 77Mnone 100% 1.1M 1.1M 1.1Mzstd 12% 9.8M 76M 76MTree-sitter は非常に大量のコードを必要とするようで、そのかなりの部分は重複しているか、圧縮可能に見える
メモリマッピングされ、実際に使われる部分だけが必要になったときにロードされる
そのため大きなバイナリを圧縮すると、デマンドページングが機能せず、かえって遅くなる
数年前に 280MB の Windows EXE で試したところ、70MB 程度まで圧縮はされたが、元のものより起動に数秒余計にかかった
ネットワーク越しにバイナリを実行する場合のような一部のシナリオでは理にかなうかもしれないが、ほとんどの場合は圧縮していないバイナリのほうが起動が速い