これは、なにをしたくて書いたもの?
ast-grepというソースコードをASTベースで扱えるツールがおもしろそうだなと思ったので、ちょっと試してみることにしました。
ast-grep
ast-grepのWebサイトはこちら。
ast-grep | structural search/rewrite tool for many languages
GitHubリポジトリーはこちら。
ast-grepは、ASTを使ってソースコードを検索、リント、書き換えができるツールです。
Rustで実装されていて、高速なことが売りのようです。導入は様々なパッケージで行えます。
内部的にはtree-sitterを使っているようです。なので、構文もtree-sitterのものが現れるようです。
同じようなツールとしてsemgrepがありますが、リンターとしてはsemgrepの方が有名ではないでしょうか。個人的には
ast-grepには検索に興味があります。
サポートしている言語はこちらです。
- Bash
- C
- C++
- C#
- CSS
- Elixir
- Go
- Haskell
- HCL
- HTML
- Java
- JavaScript
- JSON
- Kotlin
- Lua
- Nix
- PHP
- Python
- Ruby
- Rust
- Scala
- Solidity
- TypeScript
- TSX
- YAML
List of Languages with Built-in Support | ast-grep
今回はJavaを対象にします。こういうのは実際に試した方がよいと思うので、早速使っていきましょう。
ドキュメントはこちらです。
チートシート。
環境
今回の環境はこちら。
$ python3 --version Python 3.12.3 $ pip3 --version pip 24.0 from /usr/lib/python3/dist-packages/pip (python 3.12)
ast-grepをインストールする
ast-grepをインストールする方法は、Homebrew、npm、pip、cargo、macPorts、それからバイナリーのダウンロードです。
今回はpipでインストールすることにします。
$ pip3 install --break-system-packages ast-grep-cli
バージョン。
$ ast-grep --version ast-grep 0.41.1
ヘルプ。
$ ast-grep --help Search and Rewrite code at large scale using AST pattern. __ ____ ______/ /_ ____ _________ ____ / __ `/ ___/ __/_____/ __ `/ ___/ _ \/ __ \ / /_/ (__ ) /_/_____/ /_/ / / / __/ /_/ / \__,_/____/\__/ \__, /_/ \___/ .___/ /____/ /_/ Usage: ast-grep [OPTIONS] <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 rules new Create new ast-grep project or items like rules/tests lsp Start language server completions Generate shell completion script help Print this message or the help of the given subcommand(s) Options: -c, --config <CONFIG_FILE> Path to ast-grep root config, default is sgconfig.yml -h, --help Print help (see a summary with '-h') -V, --version Print version
ちなみに、sgコマンドとしても使えます。
$ sg --version ast-grep 0.41.1
今回はast-grepコマンドとして使うことにします。
ast-grepでJavaソースコードを検索する
ast-grepを使ってみようということでなにかお題が必要なのですが、今回はRESTEasyで試すことにします。
リポジトリーをclone。
$ git clone https://github.com/resteasy/resteasy
$ cd resteasy
検索はast-grep runサブコマンドで行うのですが、runサブコマンドは省略できます。
$ ast-grep run ...
今回は全部省略して書くことにします。
クラス名を指定してみます。
$ ast-grep --pattern 'ServerResponseWriter' resteasy-core/src/main/java/org/jboss/resteasy/core/AsyncResponseConsumer.java 135│ ServerResponseWriter.writeNomapResponse(builtResponse, httpRequest, httpResponse, dispatcher.getProviderFactory(), 309│ ServerResponseWriter.setResponseMediaType(builtResponse, httpRequest, httpResponse, dispatcher.getProviderFactory(), 470│ ServerResponseWriter.setResponseMediaType(builtResponse, httpRequest, httpResponse, dispatcher.getProviderFactory(), resteasy-core/src/main/java/org/jboss/resteasy/core/SynchronousDispatcher.java 212│ ServerResponseWriter.writeNomapResponse(((BuiltResponse) handledResponse), request, response, providerFactory, 474│ ServerResponseWriter.writeNomapResponse((BuiltResponse) jaxrsResponse, request, response, providerFactory, 518│ ServerResponseWriter.writeNomapResponse((BuiltResponse) jaxrsResponse, request, response, providerFactory, resteasy-core/src/main/java/org/jboss/resteasy/core/interception/jaxrs/ContainerResponseContextImpl.java 26│import org.jboss.resteasy.core.ServerResponseWriter.RunnableWithIOException; resteasy-core/src/main/java/org/jboss/resteasy/core/ServerResponseWriter.java 52│public class ServerResponseWriter { resteasy-core/src/main/java/org/jboss/resteasy/plugins/providers/sse/SseEventOutputImpl.java 33│import org.jboss.resteasy.core.ServerResponseWriter; 144│ ServerResponseWriter.writeNomapResponse(jaxrsResponse, request, response, 349│ MediaType elementType = ServerResponseWriter.getResponseMediaType(jaxrsResponse, request, response, 425│ ServerResponseWriter.writeNomapResponse(jaxrsResponse, request, response, $ ast-grep -p 'ServerResponseWriter' resteasy-core/src/main/java/org/jboss/resteasy/core/AsyncResponseConsumer.java 135│ ServerResponseWriter.writeNomapResponse(builtResponse, httpRequest, httpResponse, dispatcher.getProviderFactory(), 309│ ServerResponseWriter.setResponseMediaType(builtResponse, httpRequest, httpResponse, dispatcher.getProviderFactory(), 470│ ServerResponseWriter.setResponseMediaType(builtResponse, httpRequest, httpResponse, dispatcher.getProviderFactory(), resteasy-core/src/main/java/org/jboss/resteasy/core/SynchronousDispatcher.java 212│ ServerResponseWriter.writeNomapResponse(((BuiltResponse) handledResponse), request, response, providerFactory, 474│ ServerResponseWriter.writeNomapResponse((BuiltResponse) jaxrsResponse, request, response, providerFactory, 518│ ServerResponseWriter.writeNomapResponse((BuiltResponse) jaxrsResponse, request, response, providerFactory, resteasy-core/src/main/java/org/jboss/resteasy/core/interception/jaxrs/ContainerResponseContextImpl.java 26│import org.jboss.resteasy.core.ServerResponseWriter.RunnableWithIOException; resteasy-core/src/main/java/org/jboss/resteasy/plugins/providers/sse/SseEventOutputImpl.java 33│import org.jboss.resteasy.core.ServerResponseWriter; 144│ ServerResponseWriter.writeNomapResponse(jaxrsResponse, request, response, 349│ MediaType elementType = ServerResponseWriter.getResponseMediaType(jaxrsResponse, request, response, 425│ ServerResponseWriter.writeNomapResponse(jaxrsResponse, request, response, resteasy-core/src/main/java/org/jboss/resteasy/core/ServerResponseWriter.java 52│public class ServerResponseWriter {
パターンは-pまたは--patternオプションで指定します。
言語は-lまたは--langオプションで指定するのですが、指定しない場合はファイルの拡張子で判断しているようです。
$ ast-grep --lang java -p 'ServerResponseWriter' $ ast-grep -l java -p 'ServerResponseWriter'
Extension specifies the file extensions that ast-grep will look for when scanning the file system. By default, ast-grep uses the file extensions to determine the language.
List of Languages with Built-in Support | ast-grep
これだとただのgrepに見えますが、そうではありません。たとえばServerResponseWriterをServerResponseにすると、
ServerResponseWriterはヒットしなくなります。
$ ast-grep -p 'ServerResponse' resteasy-core/src/main/java/org/jboss/resteasy/core/ServerResponse.java 13│public class ServerResponse extends BuiltResponse { 14│ public ServerResponse() { 17│ public ServerResponse(final Object entity, final int status, final Headers<Object> metadata) { 23│ public ServerResponse(final BuiltResponse response) {
パターン構文を見ながら、もう少しいろいろ試してみましょう。
setResponseMediaTypeメソッドの呼び出し。ここで$VARはメタ変数で、$で始まりアルファベット大文字、アンダースコア、
数字を使うことができます。なんにでもマッチします。
$ ast-grep -p '$VAR.setResponseMediaType($$$)' resteasy-core/src/main/java/org/jboss/resteasy/core/AsyncResponseConsumer.java 309│ ServerResponseWriter.setResponseMediaType(builtResponse, httpRequest, httpResponse, dispatcher.getProviderFactory(), 310│ method); 470│ ServerResponseWriter.setResponseMediaType(builtResponse, httpRequest, httpResponse, dispatcher.getProviderFactory(), 471│ method);
$$$というのは、0個以上のASTノードにヒットさせるものです。
つまり$VAR.setResponseMediaType($$$)は、なにかの変数なりを経由して引数はなんでもいいので
setResponseMediaTypeメソッドを呼び出している箇所を検索していることになります。
引数は0個でもヒットしますが、レシーバーの変数は必須でこれがないメソッド呼び出しはヒットしません。
レシーバーの変数も任意にしたかったらこうですね。
$ ast-grep -p '$$$.setResponseMediaType($$$)' resteasy-core/src/main/java/org/jboss/resteasy/core/AsyncResponseConsumer.java 309│ ServerResponseWriter.setResponseMediaType(builtResponse, httpRequest, httpResponse, dispatcher.getProviderFactory(), 310│ method); 470│ ServerResponseWriter.setResponseMediaType(builtResponse, httpRequest, httpResponse, dispatcher.getProviderFactory(), 471│ method); resteasy-core/src/main/java/org/jboss/resteasy/core/ServerResponseWriter.java 98│ setResponseMediaType(jaxrsResponse, request, response, providerFactory, method); vagrant@server:~/resteasy$ ast-grep -p '$$$.setResponseMediaType($$$)' resteasy-core/src/main/java/org/jboss/resteasy/core/AsyncResponseConsumer.java 309│ ServerResponseWriter.setResponseMediaType(builtResponse, httpRequest, httpResponse, dispatcher.getProviderFactory(), 310│ method); 470│ ServerResponseWriter.setResponseMediaType(builtResponse, httpRequest, httpResponse, dispatcher.getProviderFactory(), 471│ method); resteasy-core/src/main/java/org/jboss/resteasy/core/ServerResponseWriter.java 98│ setResponseMediaType(jaxrsResponse, request, response, providerFactory, method);
{、}を使うことで、メソッドの中身も抽出できます。
$ ast-grep -p '$$$ setResponseMediaType($$$) { $$$ }' resteasy-core/src/main/java/org/jboss/resteasy/core/ServerResponseWriter.java 190│ public static void setResponseMediaType(BuiltResponse jaxrsResponse, HttpRequest request, HttpResponse response, 191│ ResteasyProviderFactory providerFactory, ResourceMethodInvoker method) { 192│ MediaType mt = getResponseMediaType(jaxrsResponse, request, response, providerFactory, method); 193│ if (mt != null) { 194│ jaxrsResponse.getHeaders().putSingle(HttpHeaders.CONTENT_TYPE, mt); 195│ } 196│ }
別にメタ変数を使わないといけないわけではありません。
logger変数でlogメソッドを呼び出している箇所を検索する場合。
$ ast-grep -p 'logger.log($$$)' resteasy-core/src/main/java/org/jboss/resteasy/tracing/RESTEasyTracingLoggerImpl.java 137│ logger.log(loggingLevel, 138│ new StringBuilder() 139│ .append(requestId) 140│ .append(' ') 141│ .append(event.name()) 142│ .append(' ') 143│ .append(message.toString()) 144│ .append(" [") 145│ .append(tracingInfo.formatDuration(duration)) 146│ .append(" ms]") 147│ .toString()); resteasy-core/src/main/java/org/jboss/resteasy/core/registry/ClassNode.java 55│ logger.log("MATCH_RUNTIME_RESOURCE", 56│ expression, 57│ expression.getRegex(), 58│ expression.getRoot().root, 59│ expression.getPathExpression()); resteasy-core/src/main/java/org/jboss/resteasy/core/registry/SegmentNode.java 75│ logger.log("MATCH_PATH_FIND", path); 88│ logger.log("MATCH_PATH_SKIPPED", expression.getRegex()); 128│ logger.log("MATCH_LOCATOR", invoker.getMethod()); 137│ logger.log("MATCH_PATH_NOT_MATCHED", expression.getRegex()); 148│ logger.log("MATCH_PATH_SELECTED", match.match.expression.getRegex()); resteasy-core/src/main/java/org/jboss/resteasy/core/SynchronousDispatcher.java 298│ logger.log("MATCH_RESOURCE", invoker); 299│ logger.log("MATCH_RESOURCE_METHOD", invoker.getMethod()); 386│ logger.log("DISPATCH_RESPONSE", jaxrsResponse);
指定の引数を使っているものにマッチさせる場合。
$ ast-grep -p 'logger.log("MATCH_RESOURCE", $$$)' resteasy-core/src/main/java/org/jboss/resteasy/core/SynchronousDispatcher.java 298│ logger.log("MATCH_RESOURCE", invoker);
このようにリテラルにマッチさせることも可能です。
$ ast-grep -p 'logger.info("POST data to : " + $VAR)' testsuite/integration-tests/src/test/java/org/jboss/resteasy/test/providers/jackson2/whitelist/WhiteListPolymorphicTypeValidatorTest.java 127│ logger.info("POST data to : " + urlString); testsuite/integration-tests/src/test/java/org/jboss/resteasy/test/providers/jackson2/whitelist/WhiteListPolymorphicTypeValidatorCatchAllTest.java 114│ logger.info("POST data to : " + urlString); testsuite/integration-tests/src/test/java/org/jboss/resteasy/test/providers/jackson2/whitelist/WhiteListPolymorphicTypeValidatorManualOverrideTest.java 120│ logger.info("POST data to : " + urlString);
代入も考慮してみます。
$ ast-grep -p '$$$ = $$$.getResourceInvoker($$$)' resteasy-core/src/main/java/org/jboss/resteasy/plugins/providers/AbstractPatchMethodFilter.java 160│ resourceInovker = methodRegistry.getResourceInvoker(request); testsuite/unit-tests/src/test/java/org/jboss/resteasy/test/resource/SegmentTest.java 56│ invoker = registry.getResourceInvoker(MockHttpRequest.put("/resource/sub"));
ちなみに、あるクラスの中身を取り出すことはできないようです。
たとえばこういうパターンを書くと、検索範囲に含まれるserviceメソッドがすべて抽出されます。
$ ast-grep -p '$$$ service($$$) throws $$$ { $$$ }'
この場合は対象のファイル単体で指定しましょう。
$ ast-grep -p '$$$ service($$$) throws $$$ { $$$ }' resteasy-core/src/main/java/org/jboss/resteasy/plugins/server/servlet/ServletContainerDispatcher.java resteasy-core/src/main/java/org/jboss/resteasy/plugins/server/servlet/ServletContainerDispatcher.java 188│ public void service(String httpMethod, HttpServletRequest request, HttpServletResponse response, boolean handleNotFound) 189│ throws IOException, NotFoundException { 190│ try { 191│ //logger.info(httpMethod + " " + request.getRequestURL().toString()); 192│ //logger.info("***PATH: " + request.getRequestURL()); 193│ // classloader/deployment aware RestasyProviderFactory. Used to have request specific 194│ // ResteasyProviderFactory.getInstance() 195│ ResteasyProviderFactory defaultInstance = ResteasyProviderFactory.getInstance(); 196│ if (defaultInstance instanceof ThreadLocalResteasyProviderFactory) { 197│ ThreadLocalResteasyProviderFactory.push(providerFactory); 198│ } 199│ ResteasyHttpHeaders headers = null; 200│ ResteasyUriInfo uriInfo = null; 201│ try { 202│ headers = ServletUtil.extractHttpHeaders(request); 203│ uriInfo = ServletUtil.extractUriInfo(request, servletMappingPrefix); 204│ } catch (Exception e) { 205│ response.sendError(HttpServletResponse.SC_BAD_REQUEST); 206│ // made it warn so that people can filter this. 207│ LogMessages.LOGGER.failedToParseRequest(e); 208│ return; 209│ } 210│ 211│ try (HttpResponse theResponse = responseFactory.createResteasyHttpResponse(response, request)) { 212│ HttpRequest in = requestFactory.createResteasyHttpRequest(httpMethod, request, headers, uriInfo, theResponse, 213│ response); 214│ 215│ ResteasyContext.pushContext(HttpServletRequest.class, request); 216│ ResteasyContext.pushContext(HttpServletResponse.class, response); 217│ 218│ ResteasyContext.pushContext(SecurityContext.class, new ServletSecurityContext(request)); 219│ ResteasyContext.pushContext(ServletConfig.class, servletConfig); 220│ 221│ if (handleNotFound) { 222│ dispatcher.invoke(in, theResponse); 223│ } else { 224│ ((SynchronousDispatcher) dispatcher).invokePropagateNotFound(in, theResponse); 225│ } 226│ } finally { 227│ ResteasyContext.clearContextData(); 228│ } 229│ } finally { 230│ ResteasyProviderFactory defaultInstance = ResteasyProviderFactory.getInstance(); 231│ if (defaultInstance instanceof ThreadLocalResteasyProviderFactory) { 232│ ThreadLocalResteasyProviderFactory.pop(); 233│ } 234│ 235│ } 236│ }
なお、このメソッドの場合はthrowsまできっちり書かないとヒットしないことに注意です…。
こんなところでしょうか。
おわりに
ASTベースのパターンで検索できるast-grepで、ソースコードの検索をしてみました。
通常のgrepコマンドと挙動が大きく違う(当たり前ですが)ので、使っていておもしろいですね。
キーワード検索の上位互換のような考え方で捉えると失敗(ヒットしない)こともあると思いますが、慣れるとかなり
強力なツールではないかなと思います。