大規模コードベース向けASTツールのast-grepについて
大規模なコードベース向けの高速なASTツールとしてast-grepというものがある。これについての調査メモ。
主に以下の公式ドキュメントを読み進めて、利用方法、パターンやルールの詳細などについて理解を深める。
ast-grepとは
公式ドキュメントでは、コードの検索からLint、codemodに至るまでASTツールとして網羅的にカバーできるツールであることが以下のように表現されている。
結果に精度が求められる場面では、高速であるものの精密さに欠けるテキストベースではなく、ASTベースでの正確な解析の方が望ましいだろうけど、ASTでの記述は非常に面倒になりがちという側面がある。
その点においてast-grep
は、テキストベースでのgrep
ではなく、ASTベースでのgrep
というように理解すると良さそう。
ASTベースでの正確さ(厳密には依存ライブラリであるtree-sitterによるCSTベースと考えるべきかもしれない)をもってgrep
のような容易さでコードを検索したり、チェックにかけたり、置換処理などを行えるるツールと考えられる。
なお、CSTとはConcrete Syntax Treeのことで、ASTに比べてよりコードの詳細を含んだツリー表現となるもの[1]。
公式ドキュメントにおいて、同様のツールと比較してもast-grep
の特徴的な点としてあげられているのは以下の点。
- パフォーマンス: Rust製でマルチコアを活用していて、非常に高速
- 漸進性: 置換する簡易なワンライナーから始めて、YAMLで記述されたLintルールとしたり、コードを書き換えるツールを作ることにも利用できる
- 実用性: インタラクティブなコード修正が可能なインタラクティブモード、Linter、言語サーバーなどをCLIに同梱している
ast-grep
自体は多くの言語をサポートしているけど、この記事では主にJavaScriptやTypeScriptに対して利用するケースをフォーカスする。
インストール
ast-grep
のCLIは、以下の通りnpmでインストールできる。cargoやhomebrewなどでもインストール可能。[2]
❯ npm i @ast-grep/cli -g
インストールできたらsg
かast-grep
でCLIが利用可能になる。--help
オプションをつけて出力して確認すると以下のようになる。
❯ ast-grep --help
Search and Rewrite code at large scale using AST pattern.
__
____ ______/ /_ ____ _________ ____
/ __ `/ ___/ __/_____/ __ `/ ___/ _ \/ __ \
/ /_/ (__ ) /_/_____/ /_/ / / / __/ /_/ /
\__,_/____/\__/ \__, /_/ \___/ .___/
/____/ /_/
Usage: ast-grep <COMMAND>
Commands:
run Run one time search or rewrite in command line. (default command)
scan Scan and rewrite code by configuration
test Test ast-grep rule
new Create new ast-grep project or items like rules or tests
lsp Starts language server
docs Generate rule docs for current configuration
help Print this message or the help of the given subcommand(s)
Options:
-h, --help
Print help (see a summary with '-h')
-V, --version
Print version
ここで利用しているast-grep
のバージョンは0.6.6
。
❯ ast-grep --version
ast-grep 0.6.6
試す
まず、簡易的な例で各機能を試すため、公式のクイックスタートを参考にして、TypeScriptで&&
を?.
に書き換えることを試してみる。
ASTで扱う対象としてTypeScriptのリポジトリをクローンしておく。
❯ git clone git@github.com:microsoft/TypeScript.git --depth 1
JavaScriptにおいては、someFunc && someFunc()
のような論理積をsomeFunc?()
のようなオプショナルチェーンに書き換えることが可能だけども、これをast-grep
で行うとどのようになるか。
ast-grep
においては、--pattern
で検索して--rewrite
で置換という構成になる。
検索
まず対象のsomeFunc && someFunc()
のように論理積を用いたコードを検索するには以下のようにする。
❯ ast-grep --pattern '$PROP && $PROP()' --lang ts TypeScript/src
--pattern
オプションで対象のコードを指定して、--lang
オプションで対象の言語を指定している。なお末尾のTypeScript/src
はクローンしたTypeScriptのリポジトリのソースコードへのパスで、コマンド実行対象のコードベースのパスとなる。
実際に実行した結果は、以下のように該当したファイルパスおよび該当行が出力される。
❯ ast-grep --pattern '$PROP && $PROP()' --lang ts TypeScript/src
TypeScript/src/services/services.ts
1552│ documentRegistry: DocumentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(), host.getCurrentDirectory()),
2204│ const customTransformers = host.getCustomTransformers && host.getCustomTransformers();
TypeScript/src/services/stringCompletions.ts
643│ const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames());
708│ const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames());
TypeScript/src/services/shims.ts
1381│ this.documentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(), host.getCurrentDirectory());
TypeScript/src/compiler/moduleSpecifiers.ts
940│ const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation();
TypeScript/src/harness/harnessIO.ts
174│ tryEnableSourceMapsForHost: () => ts.sys.tryEnableSourceMapsForHost && ts.sys.tryEnableSourceMapsForHost(),
175│ getMemoryUsage: () => ts.sys.getMemoryUsage && ts.sys.getMemoryUsage(),
TypeScript/src/tsserver/nodeServer.ts
938│ (os.homedir && os.homedir()) ||
963│ const homePath = (os.homedir && os.homedir()) ||
留意するべき点としては、ASTベースでの検索なので以下のような改行や空白を含んだコード等でも上記のパターン指定にマッチするということ。
someFunc
&& someFunc();
置換
検索結果を置換するにはどうするかというと--rewrite
オプションを追加する。
❯ ast-grep --pattern '$PROP && $PROP()' --rewrite '$PROP?.()' --lang ts TypeScript/src
実行結果
❯ ast-grep --pattern '$PROP && $PROP()' --rewrite '$PROP?.()' --lang ts TypeScript/src
TypeScript/src/compiler/moduleSpecifiers.ts
@@ -936,7 +936,7 @@
937 937│ return undefined;
938 938│ }
939 939│
940 │- const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation();
940│+ const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation?.();
941 941│ // Get a path that's relative to node_modules or the importing file's path
942 942│ // if node_modules folder is in this folder or any of its parent folders, no need to keep it.
943 943│ const pathToTopLevelNodeModules = getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex));
TypeScript/src/services/services.ts
@@ -1548,7 +1548,7 @@
1549 1549│ ];
1550 1550│ export function createLanguageService(
1551 1551│ host: LanguageServiceHost,
1552 │- documentRegistry: DocumentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(), host.getCurrentDirectory()),
1552│+ documentRegistry: DocumentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames?.(), host.getCurrentDirectory()),
1553 1553│ syntaxOnlyOrLanguageServiceMode?: boolean | LanguageServiceMode,
1554 1554│ ): LanguageService {
1555 1555│ let languageServiceMode: LanguageServiceMode;
@@ -2200,7 +2200,7 @@
2201 2201│ synchronizeHostData();
2202 2202│
2203 2203│ const sourceFile = getValidSourceFile(fileName);
2204 │- const customTransformers = host.getCustomTransformers && host.getCustomTransformers();
2204│+ const customTransformers = host.getCustomTransformers?.();
2205 2205│ return getFileEmitOutput(program, sourceFile, !!emitOnlyDtsFiles, cancellationToken, customTransformers, forceDtsEmit);
2206 2206│ }
2207 2207│
TypeScript/src/services/stringCompletions.ts
@@ -639,7 +639,7 @@
640 640│
641 641│ function getCompletionEntriesForDirectoryFragmentWithRootDirs(rootDirs: string[], fragment: string, scriptDirectory: string, extensionOptions: ExtensionOptions, compilerOptions: CompilerOptions, host: LanguageServiceHost, exclude: string): readonly NameAndKind[] {
642 642│ const basePath = compilerOptions.project || host.getCurrentDirectory();
643 │- const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames());
643│+ const ignoreCase = !(host.useCaseSensitiveFileNames?.());
644 644│ const baseDirectories = getBaseDirectoriesFromRootDirs(rootDirs, basePath, scriptDirectory, ignoreCase);
645 645│ return flatMap(baseDirectories, baseDirectory => arrayFrom(getCompletionEntriesForDirectoryFragment(fragment, baseDirectory, extensionOptions, host, /*moduleSpecifierIsRelative*/ true, exclude).values()));
646 646│ }
@@ -704,7 +704,7 @@
705 705│ }
706 706│ }
707 707│
708 │- const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames());
708│+ const ignoreCase = !(host.useCaseSensitiveFileNames?.());
709 709│ if (!tryDirectoryExists(host, baseDirectory)) return result;
710 710│
711 711│ // Enumerate the available files if possible
TypeScript/src/services/shims.ts
@@ -1377,7 +1377,7 @@
1378 1378│ public createLanguageServiceShim(host: LanguageServiceShimHost): LanguageServiceShim {
1379 1379│ try {
1380 1380│ if (this.documentRegistry === undefined) {
1381 │- this.documentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(), host.getCurrentDirectory());
1381│+ this.documentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames?.(), host.getCurrentDirectory());
1382 1382│ }
1383 1383│ const hostAdapter = new LanguageServiceShimHostAdapter(host);
1384 1384│ const languageService = createLanguageService(hostAdapter, this.documentRegistry, /*syntaxOnlyOrLanguageServiceMode*/ false);
TypeScript/src/harness/harnessIO.ts
@@ -170,8 +170,8 @@
171 171│ exit: exitCode => ts.sys.exit(exitCode),
172 172│ readDirectory: (path, extension, exclude, include, depth) => ts.sys.readDirectory(path, extension, exclude, include, depth),
173 173│ getAccessibleFileSystemEntries,
174 │- tryEnableSourceMapsForHost: () => ts.sys.tryEnableSourceMapsForHost && ts.sys.tryEnableSourceMapsForHost(),
175 │- getMemoryUsage: () => ts.sys.getMemoryUsage && ts.sys.getMemoryUsage(),
174│+ tryEnableSourceMapsForHost: () => ts.sys.tryEnableSourceMapsForHost?.(),
175│+ getMemoryUsage: () => ts.sys.getMemoryUsage?.(),
176 176│ getEnvironmentVariable: name => ts.sys.getEnvironmentVariable(name),
177 177│ joinPath
178 178│ };
TypeScript/src/tsserver/nodeServer.ts
@@ -934,7 +934,7 @@
935 935│ case "win32": {
936 936│ const basePath = process.env.LOCALAPPDATA ||
937 937│ process.env.APPDATA ||
938 │- (os.homedir && os.homedir()) ||
938│+ (os.homedir?.()) ||
939 939│ process.env.USERPROFILE ||
940 940│ (process.env.HOMEDRIVE && process.env.HOMEPATH && normalizeSlashes(process.env.HOMEDRIVE + process.env.HOMEPATH)) ||
941 941│ os.tmpdir();
@@ -959,7 +959,7 @@
960 960│ return process.env.XDG_CACHE_HOME;
961 961│ }
962 962│ const usersDir = platformIsDarwin ? "Users" : "home";
963 │- const homePath = (os.homedir && os.homedir()) ||
963│+ const homePath = (os.homedir?.()) ||
964 964│ process.env.HOME ||
965 965│ ((process.env.LOGNAME || process.env.USER) && `/${usersDir}/${process.env.LOGNAME || process.env.USER}`) ||
966 966│ os.tmpdir();
--interactive
--intertactive
オプションを付与することで、以下のように変更を各箇所で適用するか選択しながら置換できる。
❯ ast-grep --pattern '$PROP && $PROP()' --rewrite '$PROP?.()' --interactive --lang ts TypeScript/src
TypeScript/src/compiler/moduleSpecifiers.ts
@@ -936,7 +936,7 @@
937 937│ return undefined;
938 938│ }
939 939│
940 │- const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation();
940│+ const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation?.();
941 941│ // Get a path that's relative to node_modules or the importing file's path
942 942│ // if node_modules folder is in this folder or any of its parent folders, no need to keep it.
943 943│ const pathToTopLevelNodeModules = getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex));
Accept change? (Yes[y], No[n], Accept All[a], Quit[q], Edit[e])
パターン
上述の$PROP && $PROP()
のような--pattern
オプションに指定するパターンはどのようなものがあるか。
メタ変数
動的なパターンマッチをさせたいとき、利用するのはメタ変数。メタ変数は、$
で始まるA-Z
と_
と1-9
との組み合わせからなる変数。具体的には$SOME_META_VAR
や$SOME_META_1
のような形になる。
正規表現における.
がテキストではなくASTノードに対して利用できるイメージになる。
例えばconsole.log($SOME_META)
とすれば以下のようなTypeScriptのコードのconsole.log
にはマッチすることになる。
function hello() {
console.log("Hello.");
}
const log = console.log("log");
これは1つのASTノードとマッチさせるための変数で、複数のASTノードとマッチさせることに利用はできない。そのため以下のようなconsole.log
にはマッチしない。
const logString = "console.log(\"ok\")";
console.log();
function hello(name: string) {
console.log("hello", name);
}
以下のPlaygroundでマッチするケースとマッチしないケースとをそれぞれ確認できる。
マルチメタ変数
0個以上のASTノードにマッチさせたい場合はマルチメタ変数を使う。関数の引数が0以上のケースを考慮したい場合などに有用なものになる。
$$$
始まりであること以外はメタ変数の名前付けと同じルール。
console.log($$$)
とすれば、以下のようなconsole.log
の引数が1つではないもの全てにマッチすることとなる。
console.log();
console.log('Hello', name)
console.log(...args)
const $VAR = ($$$) => $$$
であれば以下のようなアロー関数にマッチする。
const foo = (bar) => bar;
const noop = () => {};
const add = (a, b, c) => {
return a + b + c;
};
メタ変数のキャプチャー
メタ変数は正規表現におけるキャプチャーと同じように、マッチしたASTノードを再利用して参照することができる。
例えば[$META, $META]
であれば以下のような配列にマッチさせることができる。
const ary = [1, 1];
_
から始まる名前のメタ変数(例えば$_META
)にすると、逆にキャプチャーしないことができるとなっているが、パターンマッチ速度の最適化で利用すると便利なもののよう。
匿名ノード
メタ変数はデフォルトだと、匿名ノードと呼ばれるノードを含まず、名前付きノードを対象としてマッチングする。匿名ノードを含むには、$$SOME_META
の形で$
を2つ先頭につける形とする必要がある。
匿名ノードとは、文法上どちらかといえば重要ではないものが該当するようで、以下の例における;
がそれに該当する。
return 123
return;
名前付きノードとマッチするケースのPlaygroundと匿名ノード含めてマッチするケースのPlaygroundで比較してみるとreturn;
の部分が異なる結果であることを確認できる。
スキャン
コマンドライン引数を幾度も変更して実行していては手間だけども、ast-grep scan
コマンドを使うと複数のルールをまとめて実行することができる。プロジェクトのLinterとして利用するようなイメージになると思う。
セットアップ
スキャン実施には事前のセットアップが必要になる。ast-grep new
コマンドをプロジェクルートディレクトリで実行して、対話形式で質問に回答してセットアップする。
❯ ast-grep new
No sgconfig.yml found. Creating a new ast-grep project...
> Where do you want to have your rules? rules
> Do you want to create rule tests? Yes
> Where do you want to have your tests? rule-test
> Do you want to create folder for utility rules? Yes
> Where do you want to have your utilities? utils
Your new ast-grep project has been created!
各質問に答えたら、回答に応じてディレクトリが作られていることが確認できる。
❯ tree
.
├── rule-test
├── rules
├── sgconfig.yml
└── utils
4 directories, 1 file
sgconfig.yml
の中身は以下の通り。
ruleDirs:
- ./rules
testConfigs:
- testDir: ./rule-test
utilDirs:
- ./utils
ルールの追加
スキャンする時に利用するLintのルールを追加するには、ast-grep new rule
コマンドを実行する。
ここではESLintにおけるno-new-symbolと同じルールを追加してみる。Symbol
はnew
をつけて実行するとTypeError
が発生するので、それを静的解析の段階で未然に防止するルールとなる。
❯ ast-grep new rule
> What is your rule's name? no-new-symbol
> Choose rule's language JavaScript
Created rules at ././rules/no-new-symbol.yml
> Do you also need to create a test for the rule? Yes
Created test at ./rule-test/no-new-symbol-test.yml
rules
ディレクトリにルールのYAMLファイルno-new-symbole.yml
が作られるので、message
やpattern
の値をルールに合わせて書き換える。
TypeScriptのSymbol
の型定義から参照するにdescription
の値はマルチメタ変数で表現する方が適切と思われるので$$$
にしておく。
id: no-new-symbol
message: Symbol cannot be called as a constructor.
severity: error
language: JavaScript
rule:
pattern: new Symbol($$$)
ESLintでの同じルールに対するASTと比較すると、記述がかなり簡潔に済むことが分かる。
適当なJavaScriptのファイルを用意してast-grep scan
コマンドを実行すると、追加したルールでnew
をつけてSymbol
をコンストラクタとして実行することをエラーとして検知できている。
❯ ast-grep scan
error[no-new-symbol]: Symbol cannot be called as a constructor.
┌─ ./index.js:1:13
│
1 │ const sym = new Symbol('sym')
│ ^^^^^^^^^^^^^^^^^
Error: 1 error(s) found in code.
Help: Scan succeeded and found error level diagnostics in the codebase.
ルール
Lintルールは、YAMLで記述するこものになっていて、sgconfig.yml
のruleDirs
でルールのファイルのディレクトリを指定する形となる。ruleDirs
に指定されているディレクトリ配下の全てのYAMLファイルがルールのファイルとして扱われるようになる。
ruleDirs:
- ./rules
ルールのファイル
ルール自体のYAMLファイルは以下のような内容になる。
id: 一意なルールID
message: スキャンして問題が見つかったときに使われる説明文
severity: 重要度レベル
language: ASTの対象のプログラミング言語
rule:
pattern: ASTのパターン
files:
- "ルールを適用する対象ファイルのglobパターン"
ignores:
- "ルールの適用外とするファイルのglobパターン"
fix: 自動で書き換えるコード
constraints: メタ変数のマッチに対する制約条件
ルールの詳細
ast-grep
のルールは以下3つに分類される。いずれのルールもcomposeしてより複雑なルールを作ることができる。
- Atomicルール: 構文ノードがルールにマッチするかどうかを決定する、最も基本的なルール
- Relationalルール: 周辺のノードをベースにして対象のノードをフィルターできるルール
- Compositeルール: 他のルールやルールのリストを再帰的に受け入れて、アトミックなルールをより複雑なものに合成するルール
以下にルール種別毎のkey
一覧を列挙する。
ルールの種別 | ルールのkey
|
概要 |
---|---|---|
Atomic | pattern |
パターン構文に応じてノードにマッチ |
Atomic | kind |
ノードの種別に応じてマッチ |
Atomic | regex |
ノードのテキストにRustの正規表現でマッチ |
Relational | inside |
サブルールにマッチするノードでフィルター |
Relational | has |
サブルールで指定されている子ノードを持つノードでフィルター |
Relational | follows |
サブルールで指定されたノードにつづくノードでフィルター |
Relational | precedes |
サブルールで指定されたノードの前にあるノードでフィルター |
Composite | all |
全てのルールを満たすノードにマッチ |
Composite | any |
1つでもルールを満たしたノードにマッチ |
Composite | not |
ルールを満たさないノードにマッチ |
Composite | matches |
ユーティリティルールを参照してそのルールにマッチ |
これだけだと個々のルールの理解が難しいので、より詳細にast-grep/eslintにおけるルールやPlaygroundを例としてあわせて見つつ、各ルールについてながめてみる。
Atomicルール
Atomicルールは最も基本的なルールで、pattern
、kind
、regex
の3種類がある。
pattern
pattern
はパターン構文に応じてノードにマッチする。
pattern: console.log($META)
オブジェクトスタイルでコンテキストを絞り込んでselector
で該当のノードを指定することもできる。例えばTypeScriptでクラスのプロパティのASTノードをパターン指定する場合は以下のようになる。
pattern:
selector: public_field_definition
context: class { $F }
selector
に指定する値はtree-sitterのノード名だと思う。Playgroundで確認してみたりすると分かるけど、JavaScriptとTypeScriptでも微妙に異なるノード名だったりする。
kind
kind
はselector
に似ている感じがするけど、ASTノードの種別で検索できるルール。有効な構文をpattern
だけで指定するには複雑度が高いとき、ASTノード種別を指定できるものとなる。
kind: public_field_definition
regex
Rustの正規表現でASTノードのテキストにマッチさせるもの。
例えば、ast-grep/eslintのarray-callback-returnルールにおいては、return
を忘れないようArray
のメソッドとマッチさせるのに利用している。
大抵の場合、regex
はパフォーマンスが出ないので単独ではなく他のAtomicルールと併用するべきものとされている。
Relationalルール
周辺のノードの状況をノードの絞り込み条件として加えられる強力なルールがRelationalルール。例えば以下のようなルールだとfor in
やfor
のループの中でawait
のパターンにマッチするノードを探す。
all:
- pattern: await $PROMISE
- inside:
any:
- kind: for_in_statement
- kind: for_statement
Relationalルールでサポートされているのは以下の4つ。
inside
サブルールにマッチするノードの中を対象のノードとする。
ast-grep/eslintのno-dupe-args
においては、以下のようなinside
でのマッチによってパラメーターの重複を検知している。
has
サブルールで指定されている子ノードを持つノードを対象とする。上述の例ではメタ変数$NAME
とマッチする子ノードを持っているノードと該当するようになる。
has:
pattern: $NAME
follows
サブルールで指定されたノードにつづくノードを対象とする。例としては以下のように// comment
のようなコメントのノードの後のconsole.log('foo');
のノードにのみマッチさせたいというケースで、
console.log("foo");
console.log("foo");
// comment
console.log("foo");
console.log("foo");
follows
でコメントの後にあるノードを範囲として指定するルールを記述できる。
rule:
all:
- pattern: console.log('foo');
- follows:
pattern: // comment
precedes
サブルールで指定されたノードの前にあるノードを対象とする。follows
の例とは逆に、// comment
のようなコメントのノードの前のconsole.log('foo');
のノードにのみマッチさせたいというケースで、
console.log("foo");
console.log("foo");
// comment
console.log("foo");
console.log("foo");
precedes
でコメントの前にあるノードを範囲として指定するルールを記述できる。
rule:
all:
- pattern: console.log('foo');
- precedes:
pattern: // comment
オプション
また、Relationalルールのオプションとして以下のものがある。
-
stopBy
: デフォルトだとRelationalルールは1階層だけノードを辿ってマッチさせるけど、end
をstopBy
に指定すると周囲のノード(何を周囲とするかはRelationalルール次第だとは思う)を最後まで辿って探す -
field
: 特定のフィールドによってノードを指定したい場合はfield
を利用する
なお、follows
とprecedes
にはfield
オプションがない。
Compositeルール
個別のルールを組み合わせてより複雑なルールを構成できるのがCompositeルール。
all
リストで受け取ったすべてのルールとマッチするときに該当する。
例えば以下のpattern
とinside
との組み合わせだと関数宣言とアロー関数とのいずれかにおけるreturn
文のノードにマッチするケースが該当する。pattern
とinside
とのいずれのルールも満たすものだけが該当する形になる。
all:
- pattern: return $
- inside:
stopBy: end
any:
- kind: function_declaration
- kind: arrow_function
any
リストで受け取ったいずれかのルールとマッチさせる。上述のall
での例にあるように、関数宣言function_declaration
とアロー関数arrow_function
、いずれかのkind
であれば該当する形となる。
not
受け取ったルールにマッチさせない。受け取るルールは単一のもの。例えば以下のようなnot
のルールを含むルールであれば、マルチメタ変数を利用した引数の数を問わず全てのconsole.log
にマッチするパターンから引数が1つのconsole.log
をnot
で除外することになる。
rule:
all:
- pattern: console.log($$$)
- not:
pattern: console.log($)
matches
参照するユーティリティルールのidをもとにルールとマッチさせる。従って以下のように該当のユーティリティルールのidをmatches
へ指定する。
matches: util-unique-id
ユーティリティルール
ユーティリティルールとは、他のルールから再利用可能な形にしておくルールのこと。ユーティリティルールには、ローカルルールとグローバルルールとが存在する。
ローカルルール
utils
というセクションで識別子を持ってローカルのルールオブジェクトを用意することで、matches
からその識別子を指定してルールを再利用することができる。
例えば、以下のようにfoo
という識別子のローカルルールを用意してmatches
によって裏要することが可能になっている。
utils:
foo:
any:
- pattern: console.log($$$)
rule:
matches: foo
なお、ローカルルールの識別子の重複はできないけどもグローバルルールが同名であった場合ローカルルールで上書きされる動きになるらしい。
グローバルルール
グローバルルールはsgconfig.yml
のutilDirs
のディレクトリに配置されるルールで、プロジェクト内でグローバルに再利用できるルールとなる。
例えば以下のようなグローバルルールをutils/foo.yml
として配置すると、
id: foo
language: JavaScript
rule:
any:
- pattern: console.log($$$)
ルールのファイルでmatches
を利用してグローバルルールを再利用することが可能となる。
rule:
matches: foo
まとめ
@ast-grep/cli
で検索、置換、スキャンなどを試しながら、パターンやルールについて確認してみた。@ast-grep/napi
の方は今回試していないけども、プログラムから利用する形であればよりさまざまな目的に利用できそうな感じがするので、そういうことも試してみれると良さそう。
参考
-
ASTとCSTの比較についてはAST vs CSTのセクションで言及されている。 ↩︎
-
プログラムから利用できるAPIを提供する
@ast-grep/napi
もあるけど、この記事では扱わない。 ↩︎
Discussion
ast-grepの日本語記事をいただき、光栄に思います。ありがとうございます!