VCL(Varnish Configuration Language) を記述する際、nvimでシンタックスハイライトが効いてない状態でずっと書いていた。
正規表現ベースのプラグインを作っている方はいたのだが、tree-sitterのパーサーが存在しなかったため作ってみた。
https://github.com/ntsk/tree-sitter-vcl
tree-sitterについて
https://tree-sitter.github.io/tree-sitter/
tree-sitterは、プログラミング言語の構文解析を行うためのパーサージェネレーター。
特徴として、
- 高速な構文解析
- エラー耐性(不完全なコードでも解析可能)
- インクリメンタルパース(変更箇所のみ再解析)
- 言語非依存(C言語で実装されており、各言語のバインディングが提供されている)
等がある。
tree-sitterでは、JavaScriptで文法規則を記述すると、C言語のパーサーコードが生成される仕組みとなっている。
パーサーはソースコードを解析してASTを生成し、エディタはこのASTを利用してシンタックスハイライトやコード補完などの機能を提供している。
nvim-treesitterについて
https://github.com/nvim-treesitter/nvim-treesitter
nvim-treesitterは、nvimでtree-sitterを利用するためのプラグイン。
Neovim 0.5以降では、tree-sitterのサポートが組み込まれており、nvim-treesitterを利用することで、
- シンタックスハイライト
- コード折りたたみ
- インデント
- インクリメンタルセレクション
等の機能が利用できる。
各言語のパーサーは、nvim-treesitterから:TSInstall <言語名>でインストールできる。
公式にサポートされていない言語でも、カスタムパーサーのリポジトリを指定することで利用できる。
実装
開発フロー
プロジェクトの初期化
tree-sitter init コマンドで、新しいパーサープロジェクトを初期化する。
tree-sitter initこのコマンドを実行すると、対話形式でプロジェクト名などが要求されて下記のテンプレートが生成される。
- grammar.js: 文法定義のテンプレート
- package.json: Node.jsプロジェクトの設定
- binding.gyp: ネイティブアドオンのビルド設定
- bindings/: 各言語用のバインディング
- src/: 生成されたパーサーコードの出力先
- test/corpus/: テストケースの配置先
パーサーの生成
grammar.js を編集した後、tree-sitter generate コマンドでパーサーコードを生成する。
tree-sitter generateこのコマンドにより、grammar.js の定義から以下のファイルが src/ ディレクトリに生成される。
- parser.c: C言語のパーサー実装
- grammar.json: 文法のJSON表現
- node-types.json: ASTノードの型定義
テストの実行
test/corpus/ ディレクトリにテストケースを配置し、tree-sitter test コマンドでテストを実行できる。
tree-sitter testテストケースは以下のような形式で記述する。
==================
Backend definition
==================
backend default {
.host = "127.0.0.1";
.port = "8080";
}
---
(source_file
(backend_declaration
(identifier)
(backend_properties
(backend_property
(property_name)
(string))
(backend_property
(property_name)
(string)))))上部にVCLのコード、下部に期待されるASTを記述することで、パーサーが正しく動作しているか確認できる。
動作確認
tree-sitter parse コマンドで、任意のファイルを解析してASTを確認できる。
tree-sitter parse example.vclまた、tree-sitter playground コマンドでWebベースのプレイグラウンドを起動し、リアルタイムでパースの結果を確認しながら開発できる。事前に、tree-sitter build --wasm でビルドしておく必要がある。
tree-sitter playground文法
tree-sitterでは、grammar.jsにJavaScriptで文法規則を記述する。
VCLの文法は、公式ドキュメントを参考に、以下のような構造で定義した。
- vcl_declaration: VCLバージョン宣言 (
vcl 4.1;等) - import_statement: モジュールのインポート (
import std;等) - backend_declaration: バックエンドサーバーの定義
- acl_declaration: アクセス制御リストの定義
- subroutine_declaration: サブルーチンの定義 (
sub vcl_recv { ... }等)
それぞれの定義内で利用される式や文として、
- 二項演算子 (
==,!=,~,!~,&&,||等) - 単項演算子 (
!) - if文、set文、unset文、return文、call文
- メンバー式 (
req.url,beresp.ttl等) - リテラル (string, integer, duration, boolean)
等を定義している。
例えば、backend_declarationは以下のような定義となる。
backend_declaration: $ => seq(
'backend',
$.identifier,
'{',
field('properties', $.backend_properties),
'}'
),
backend_properties: $ => repeat1($.backend_property),
backend_property: $ => choice(
seq(
field('name', $.property_name),
'=',
field('value', $.probe_properties)
),
seq(
field('name', $.property_name),
'=',
field('value', choice(
$.string,
$.integer,
$.duration,
)),
';'
)
),
property_name: $ => /\.[a-zA-Z_][a-zA-Z0-9_]*/,これにより、以下のようなbackend定義が解析できる。
backend default {
.host = "127.0.0.1";
.port = "8080";
.probe = {
.url = "/health";
.timeout = 1s;
}
}tree-sitter-vclのシンタックスハイライト定義
tree-sitterでは、queries/highlights.scmにScheme記法でハイライトルールを記述する。
各構文要素に対して、どのハイライトグループを適用するかを指定できる。
;; Keywords
[
"vcl"
"backend"
"acl"
"sub"
"import"
"if"
"elsif"
"else"
"return"
"set"
"unset"
"call"
"new"
] @keyword
;; Literals
(string) @string
(integer) @number
(duration) @number
(boolean) @boolean
;; Comments
(comment) @comment
;; Built-in subroutines and variables
((identifier) @function.builtin
(#match? @function.builtin "^vcl_"))
((identifier) @variable.builtin
(#match? @variable.builtin "^(req|bereq|beresp|resp|obj|client|server|storage|now)$"))これにより、VCLの組み込みサブルーチン (vcl_recv, vcl_backend_response 等)や
組み込み変数 (req, beresp, resp 等)が適切にハイライトされる。
使い方
Neovimでの利用
nvim-treesitterを利用している場合、以下の設定を追加する。
local parser_config = require("nvim-treesitter.parsers").get_parser_configs()
parser_config.vcl = {
install_info = {
url = "https://github.com/ntsk/tree-sitter-vcl",
files = {"src/parser.c"},
branch = "main",
},
filetype = "vcl",
}また、.vcl はデフォルトではfiletypeを解釈してくれないのと、カスタムパーサーだとTSBufEnable highlight を明示的に実行しないと効かないので下記を設定に記述しておく。
autocmd BufNewFile,BufRead *.vcl set filetype=vcl
autocmd FileType vcl TSBufEnable highlight設定後、:TSInstall vclでパーサーをインストールすると、.vclファイルでシンタックスハイライトが有効になる。
まとめ
tree-sitterは、文法定義からパーサーを自動生成する仕組みであるため、エディタごとにシンタックスハイライトの実装を個別に行う必要がなく、一度実装すれば複数のエディタで利用できる点が便利。
VCLは、他のプログラミング言語と比較して文法がシンプルなので、tree-sitterのパーサー実装の入門として良かった。