以下の内容はhttps://kazuhira-r.hatenablog.com/entry/2026/03/13/001138より取得しました。


ASTベースのパターン検索ができるast-grepで、Javaのソースコードを検索する

これは、なにをしたくて書いたもの?

ast-grepというソースコードをASTベースで扱えるツールがおもしろそうだなと思ったので、ちょっと試してみることにしました。

ast-grep

ast-grepのWebサイトはこちら。

ast-grep | structural search/rewrite tool for many languages

GitHubリポジトリーはこちら。

GitHub - ast-grep/ast-grep: ⚡A CLI tool for code structural search, lint and rewriting. Written in Rust · 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を対象にします。こういうのは実際に試した方がよいと思うので、早速使っていきましょう。

ドキュメントはこちらです。

What is ast-grep? | ast-grep

チートシート。

Rule Cheat Sheet | ast-grep

環境

今回の環境はこちら。

$ 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、それからバイナリーのダウンロードです。

Quick Start / Installation

今回は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に見えますが、そうではありません。たとえばServerResponseWriterServerResponseにすると、
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) {

パターン構文を見ながら、もう少しいろいろ試してみましょう。

Pattern Syntax | ast-grep

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);
193if (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();
196if (defaultInstance instanceof ThreadLocalResteasyProviderFactory) {
197│                ThreadLocalResteasyProviderFactory.push(providerFactory);
198}
199│            ResteasyHttpHeaders headers = null;
200│            ResteasyUriInfo uriInfo = null;
201try {
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);
208return;
209}
210211│            try (HttpResponse theResponse = responseFactory.createResteasyHttpResponse(response, request)) {
212│                HttpRequest in = requestFactory.createResteasyHttpRequest(httpMethod, request, headers, uriInfo, theResponse,
213│                        response);
214215│                ResteasyContext.pushContext(HttpServletRequest.class, request);
216│                ResteasyContext.pushContext(HttpServletResponse.class, response);
217218│                ResteasyContext.pushContext(SecurityContext.class, new ServletSecurityContext(request));
219│                ResteasyContext.pushContext(ServletConfig.class, servletConfig);
220221if (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();
231if (defaultInstance instanceof ThreadLocalResteasyProviderFactory) {
232│                ThreadLocalResteasyProviderFactory.pop();
233}
234235}
236}

なお、このメソッドの場合はthrowsまできっちり書かないとヒットしないことに注意です…。

こんなところでしょうか。

おわりに

ASTベースのパターンで検索できるast-grepで、ソースコードの検索をしてみました。

通常のgrepコマンドと挙動が大きく違う(当たり前ですが)ので、使っていておもしろいですね。

キーワード検索の上位互換のような考え方で捉えると失敗(ヒットしない)こともあると思いますが、慣れるとかなり
強力なツールではないかなと思います。




以上の内容はhttps://kazuhira-r.hatenablog.com/entry/2026/03/13/001138より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14