텍스트가 아닌 AST 기반으로 코드를 매칭·치환하는 ast-grep을 활용해 Elixir 테스트 코드의 반복 패턴을 일관되게 정리하는 방법을 소개한다. any, precedes, expandEnd, 멀티 메타변수, constraints 같은 규칙 작성 기법과 예제, CI 연동까지 다룬다.
코드를 찾아 바꾸는 ast-grep이라는 유용한 도구가 있다. sed 같은 다른 도구와의 차이점은 텍스트에 단순 매칭하는 대신, ast-grep은 추상 구문 트리(abstract syntax tree), 즉 AST를 기준으로 매칭한다는 점이다. 이 덕분에 ast-grep은 프로그램 소스 코드를 찾고 수정하는 데 sed보다 훨씬 표현력이 높으며, 특정 규칙에 따라 대규모 코드베이스를 일괄 정리하는 같은 작업의 가능성을 열어 준다.
Elixir로 작성된 소스 코드에 ast-grep을 사용하는 예시 몇 가지를 살펴보자.
내 앞에는 수년간의 개발을 거친 제법 큰 Elixir 코드베이스가 있다. 여러 사람이 다양한 디테일 감각으로 작업해 온 결과, 여기저기 자잘한 불일치가 곳곳에 남아 있다.
테스트 스위트를 조금 다듬으려던 참에, 아래 같은 코드가 눈에 띄었다:
test "returns 401 for unauthenticated request", %{conn: conn} do
conn = get(conn, category_path(conn, :index))
assert json_response(conn, 401)
end
이 코드에는 내 취향에 그다지 맞지 않는 작은 스타일 문제들이 몇 가지 있다:
이상적으로는 위 코드를 다음과 같이 바꾸고 싶다:
test "returns 401 for unauthenticated request", %{conn: conn} do
conn
|> get(category_path(conn, :index))
|> json_response(401)
end
코드베이스에는 이런 곳이 아주 많고, 전부 같은 모양으로 표준화하면 좋겠다. sed를 이렇게 쓸 수도 있다:
rg -F -l "conn = get(conn, category_path(conn, :index))" | xargs sed -i -e '/conn = get(conn, category_path(conn, :index))/{N;s/.*conn = get(conn, category_path(conn, :index))\n.*assert json_response(conn, 401)/ conn\n |> get(category_path(conn, :index))\n |> json_response(401)/;}'
하지만 단점이 있다:
이제 ast-grep이 sed의 단점을 해결하면서 같은 결과를 낼 수 있는지 보자.
먼저 프로젝트 루트 폴더에 rules.yaml이라는 파일을 만들고 다음 내용을 넣는다:
id: conn-pipe-syntax-1
language: elixir
rule:
pattern: |
conn = get(conn, category_path(conn, :index))
assert json_response(conn, 401)
fix:
template: |
conn
|> get(category_path(conn, :index))
|> json_response(conn, 401)
그런 다음 프로젝트 루트에서 ast-grep을 호출해 이 "규칙"을 적용해 본다:
ast-grep scan --rule rules.yaml --update-all
아쉽게도 다음과 같은 출력과 함께 실패한다:
Error: Cannot parse rule rules.yaml
Help: The file is not a valid ast-grep rule. Please refer to doc and fix the error.
See also: https://ast-grep.github.io/guide/rule-config.html
✖ Caused by
╰▻ Fail to parse yaml as Rule.
╰▻ `rule` is not configured correctly.
╰▻ Rule contains invalid pattern matcher.
╰▻ Multiple AST nodes are detected. Please check the pattern source `conn = get(conn, category_path(conn, :index))
assert json_response(conn, 401)
`.
에러 메시지에 "여러 AST 노드" 얘기가 나온다. 한 번에 여러 AST 노드를 지정하고 있는 셈이다. ast-grep은 tree-sitter로 코드를 파싱하는데, 내가 tree-sitter 전문가인 것은 아니지만 여기서는 각 줄이 별도의 AST 노드로 파싱된다고 가정하자.
대신 여러 노드를 함께 다루려면, 서로 어떻게 배치되어 있는지를 표현해야 한다. 예를 들어, 목표 코드 조각이 다른 코드 조각보다 먼저 나온다(precedes)고 표현할 수 있다:
id: conn-pipe-syntax-1
language: elixir
rule:
pattern: conn = get(conn, category_path(conn, :index))
precedes:
pattern: assert json_response(conn, 401)
fix:
template: |
conn
|> get(category_path(conn, :index))
|> json_response(conn, 401)
precedes를 사용하면 우리 규칙이 관계형 규칙이 된다.
명령을 다시 실행하면 더 이상 에러가 나지 않는다:
ast-grep scan --rule rules.yaml --update-all .
# Applied 1 changes
멋지다, 뭔가 작동했다! 무엇을 했는지 보자:
diff --git a/test/api/controllers/category_controller_test.exs b/test/api/controllers/category_controller_test.exs
index 7f3809922..6511b39f7 100644
--- a/test/api/controllers/category_controller_test.exs
+++ b/test/api/controllers/category_controller_test.exs
@@ -53,7 +53,10 @@ defmodule Api.CategoryControllerTest do
end
test "returns 401 for unauthenticated request", %{conn: conn} do
- conn = get(conn, category_path(conn, :index))
+ conn
+ |> get(category_path(conn, :index))
+ |> json_response(conn, 401)
+
assert json_response(conn, 401)
end
end
꽤 괜찮다!
넘어가기 전에 ast-grep은 온라인 플레이그라운드를 제공한다는 점을 언급하고 싶다. 여기서 다양한 코드 스니펫과 매칭 규칙을 실험해 볼 수 있다. 게다가 플레이그라운드는 코드 스니펫의 AST도 보여 준다. 규칙을 만들고 디버깅하는 데 없어서는 안 될 도구다. 위 코드 스니펫과 규칙을 적용한 플레이그라운드는 대략 이렇게 보인다:
직접 만져보고 싶다면 다음 링크를 사용해 보자: playground. 왼쪽 상단의 "Diff" 탭을 눌러 결과를 확인해 보자!
이제 또 다른 문제가 보인다. assert json_response(conn, 401) 줄이 제거되지 않았다. 명백히 pattern과 일치하는 코드만 제거되는 모양이다. 그렇다면 ast-grep에 assert json_response(conn, 401)까지 함께 지우라고 어떻게 알려 줄까? 이를 위해 fix 섹션에서 extendEnd라는 옵션을 다음과 같이 사용할 수 있다:
diff --git a/rules.yaml b/rules.yaml
index 23a11fb42..0cfef889a 100644
--- a/rules.yaml
+++ b/rules.yaml
@@ -10,3 +10,6 @@ fix:
conn
|> get(category_path(conn, :index))
|> json_response(conn, 401)
+ expandEnd:
+ pattern: |
+ assert json_response(conn, 401)
이렇게 하면 수정 범위를 확장(expand)하여 assert json_response(conn, 401)까지 잡아서 교체한다. 첫 시도에서 수정된 파일을 되돌린 뒤 ast-grep 명령을 다시 실행해 보자. 다음과 같은 결과가 보일 것이다:
diff --git a/test/api/controllers/category_controller_test.exs b/test/api/controllers/category_controller_test.exs
index 7f3809922..dc24111df 100644
--- a/test/api/controllers/category_controller_test.exs
+++ b/test/api/controllers/category_controller_test.exs
@@ -53,8 +53,10 @@ defmodule Api.CategoryControllerTest do
end
test "returns 401 for unauthenticated request", %{conn: conn} do
- conn = get(conn, category_path(conn, :index))
- assert json_response(conn, 401)
+ conn
+ |> get(category_path(conn, :index))
+ |> json_response(conn, 401)
+
end
end
end
훨씬 낫다! 하지만 또 다른 문제가 있다. 교체 결과에 불필요한 개행이 하나 생겼다. Elixir 코드에서는 이런 개행을 없앨 때 흔히 mix format 명령을 실행한다. 다만, ast-grep에게 처음부터 개행이 생기지 않게 할 방법이 있지 않을까?
방법이 있다. 이번에 rules.yaml에서 바꿔야 할 것은 ast-grep 자체가 아니라 yaml 문법이다. 블록 chomping 인디케이터라고 불리는 기능을 사용해 줄바꿈 문자를 제거할 수 있다:
diff --git a/rules.yaml b/rules.yaml
index 0cfef889a..84b0a5381 100644
--- a/rules.yaml
+++ b/rules.yaml
@@ -6,7 +6,7 @@ rule:
precedes:
pattern: assert json_response(conn, 401)
fix:
- template: |
+ template: |-
conn
|> get(category_path(conn, :index))
|> json_response(conn, 401)
파일 변경을 되돌린 뒤 ast-grep을 다시 실행하면 다음과 같은 결과가 나온다:
diff --git a/test/api/controllers/category_controller_test.exs b/test/api/controllers/category_controller_test.exs
index 7f3809922..4c9d11d93 100644
--- a/test/api/controllers/category_controller_test.exs
+++ b/test/api/controllers/category_controller_test.exs
@@ -53,8 +53,9 @@ defmodule Api.CategoryControllerTest do
end
test "returns 401 for unauthenticated request", %{conn: conn} do
- conn = get(conn, category_path(conn, :index))
- assert json_response(conn, 401)
+ conn
+ |> get(category_path(conn, :index))
+ |> json_response(conn, 401)
end
end
end
드디어 보기 좋고 깔끔한 변경이다!
하지만 문제가 하나 더 있다. 지금은 단 하나의 테스트 파일에서 한 군데만 고쳤다. 테스트 곳곳에 위 패턴과 매우 비슷한 코드가 훨씬 많이 있다. 예를 들어:
test "returns 404 for unauthenticated request", %{conn: conn} do
contract = insert(:contract)
conn = get(conn, contract_path(conn, :show, contract.id))
assert json_response(conn, 404)
end
요청 메서드, 경로, 응답 코드에 상관없이 찾아 바꾸기를 수행하라고 ast-grep에 설명할 수 있으면 좋겠다.
이를 위해 패턴의 일부를 파라미터화하면 된다. rules.yaml을 다음과 같이 수정해 보자:
diff --git a/rules.yaml b/rules.yaml
index 0cfef889a..f6531e9c0 100644
--- a/rules.yaml
+++ b/rules.yaml
@@ -2,14 +2,14 @@ id: conn-pipe-syntax-1
language: elixir
rule:
- pattern: conn = get(conn, category_path(conn, :index))
+ pattern: conn = $METHOD(conn, $PATH)
precedes:
- pattern: assert json_response(conn, 401)
+ pattern: assert json_response(conn, $STATUS)
fix:
- template: |
+ template: |-
conn
- |> get(category_path(conn, :index))
- |> json_response(conn, 401)
+ |> $METHOD($STATUS)
+ |> json_response(conn, $STATUS)
expandEnd:
pattern: |
- assert json_response(conn, 401)
+ assert json_response(conn, $STATUS)
ast-grep 명령을 다시 실행하면 다음과 같은 출력이 보일 것이다:
Applied 37 changes
이제 조금 진척이 있다! 코드를 검토해 보면 다음과 같은 다양한 수정이 보인다:
diff --git a/test/api/controllers/contract_controller_test.exs b/test/api/controllers/contract_controller_test.exs
index cfabef8f2..0996690d3 100644
--- a/test/api/controllers/contract_controller_test.exs
+++ b/test/api/controllers/contract_controller_test.exs
@@ -658,9 +658,9 @@ defmodule Api.ContractControllerTest do
test "returns 401 for unauthenticated request", %{conn: conn} do
contract = insert(:contract)
- conn = get(conn, contract_path(conn, :show, contract.id))
-
- assert json_response(conn, 401)
+ conn
+ |> get(contract_path(conn, :show, contract.id))
+ |> json_response(conn, 401)
end
test "validates UUID", %{conn: conn} do
@@ -791,9 +791,9 @@ defmodule Api.ContractControllerTest do
test "returns 401 for unauthenticated request", %{conn: conn} do
contract = insert(:contract)
- conn = get(conn, contract_path(conn, :preview, contract.id))
-
- assert json_response(conn, 401)
+ conn
+ |> get(contract_path(conn, :preview, contract.id))
+ |> json_response(conn, 401)
end
end
@@ -1055,8 +1055,9 @@ defmodule Api.ContractControllerTest do
end
test "returns 401 for unauthenticated request", %{conn: conn} do
- conn = get(conn, contract_path(conn, :index))
- assert json_response(conn, 401)
+ conn
+ |> get(contract_path(conn, :index))
+ |> json_response(conn, 401)
end
하지만 또 다른 문제가 있다. 코드를 살펴보니 이런 패턴도 있다:
conn =
conn
|> authenticate_conn(account)
|> patch(contract_path(conn, :update, contract.id))
assert json_response(conn, 403)
이 코드와 지금까지 우리가 겨냥한 패턴의 차이는 다음 한 줄이 존재한다는 점뿐이다:
|> authenticate_conn(account)
이 경우까지 고려하려면 규칙을 하나 더 추가해 보자:
diff --git a/rules.yaml b/rules.yaml
index b711ffbc3..309c15d7a 100644
--- a/rules.yaml
+++ b/rules.yaml
@@ -13,3 +13,23 @@ fix:
expandEnd:
pattern: |
assert json_response(conn, $STATUS)
+---
+id: conn-pipe-syntax-2
+language: elixir
+rule:
+ pattern: |
+ conn =
+ conn
+ |> authenticate_conn($ACCOUNT)
+ |> $METHOD($PATH)
+ precedes:
+ pattern: assert json_response(conn, $STATUS)
+fix:
+ template: |-
+ conn
+ |> authenticate_conn($ACCOUNT)
+ |> $METHOD($PATH)
+ |> json_response($STATUS)
+ expandEnd:
+ pattern: |
+ assert json_response(conn, $STATUS)
ast-grep을 다시 실행하면 다음과 같은 변경들이 생긴다:
- conn =
- conn
- |> authenticate_conn(account)
- |> patch(contract_path(conn, :update, contract.id))
-
- assert json_response(conn, 403)
+ conn
+ |> authenticate_conn(account)
+ |> patch(contract_path(conn, :update, contract.id))
+ |> json_response(403)
end
좋다, 또 하나의 흐트러진 패턴을 정리했다. 그런데 이번에는 이런 코드는 바뀌지 않았다:
conn =
conn
|> get(user_path(conn, :return_url))
assert json_response(conn, 401)
그럴 만도 하다. 우리가 본 이전 패턴과 아주 약간 다르지만, 결국 변형일 뿐이다:
# 이전
conn = get(conn, some_path(conn, :return_url))
# 새 버전
conn = conn |> get(some_path(conn, :return_url))
이 경우도 커버해 보자. 이번에는 새 규칙을 추가하는 대신, 첫 번째 규칙을 복합(composite) 규칙으로 바꿔 보겠다:
diff --git a/rules.yaml b/rules.yaml
index 309c15d7a..5b31bdaa1 100644
--- a/rules.yaml
+++ b/rules.yaml
@@ -1,8 +1,9 @@
id: conn-pipe-syntax-1
language: elixir
rule:
- pattern: |
- conn = $METHOD(conn, $PATH)
+ any:
+ - pattern: conn = conn |> $METHOD($PATH)
+ - pattern: conn = $METHOD(conn, $PATH)
precedes:
pattern: assert json_response(conn, $STATUS)
fix:
여기서 패턴을 꼭 코드와 똑같은 서식으로 쓸 필요는 없다는 점에 주목하자. 이 맥락에서 패턴은 여분의 공백이나 줄바꿈을 그다지 신경 쓰지 않는다. 바로 AST 매칭의 힘이다. 계속 진행하자.
테스트를 더 살펴보니 아직 커버되지 않은 또 다른 반복 패턴이 보인다. 예를 들면:
conn =
conn
|> post(contract_signature_path(conn, :request_changes, contract.id), %{
title: "My draft",
content: "This is the content"
})
assert json_response(conn, 403)
지금까지 다룬 것과 거의 비슷하지만 중요한 차이가 있다. 경로 헬퍼 함수 호출 뒤에 맵 파라미터가 하나 더 붙는다. 지금까지의 패턴은 이 경우를 매칭하지 못한다.
문제 패턴을 커버하려면 규칙 1과 2의 변형을 두 개 더 추가해, 이번에는 $PARAMS를 넣으면 된다. 규칙은 대략 다음과 같을 것이다:
id: conn-pipe-syntax-3
language: elixir
rule:
any:
- pattern: conn = conn |> $METHOD($PATH, $PARAMS)
- pattern: conn = $METHOD(conn, $PATH, $PARAMS)
precedes:
pattern: assert json_response(conn, $STATUS)
fix:
template: |-
conn
|> $METHOD($PATH, $PARAMS)
|> json_response(conn, $STATUS)
expandEnd:
pattern: |
assert json_response(conn, $STATUS)
---
id: conn-pipe-syntax-4
language: elixir
rule:
pattern: |
conn =
conn
|> authenticate_conn($ACCOUNT)
|> $METHOD($PATH, $PARAMS)
precedes:
pattern: assert json_response(conn, $STATUS)
fix:
template: |-
conn
|> authenticate_conn($ACCOUNT)
|> $METHOD($PATH, $PARAMS)
|> json_response($STATUS)
expandEnd:
pattern: |
assert json_response(conn, $STATUS)
하지만 이렇게 하면 유지해야 할 규칙 발자국(footprint)이 정확히 두 배로 늘어난다. ast-grep에서 "모든 파라미터"를 표현할 수 있는 방법만 있다면 $PATH, $PARAMS는 물론, 다른 선택적 파라미터까지 한 번에 커버할 수 있을 텐데. 다행히 멀티 메타변수(multi meta variable)라는 기능이 있다. 직접 써 보자. $PATH를 $$$ALL_PARAMS라는 새 변수로 바꿔 보겠다:
diff --git a/rules.yaml b/rules.yaml
index 5b31bdaa1..e495b5e58 100644
--- a/rules.yaml
+++ b/rules.yaml
@@ -2,14 +2,14 @@ id: conn-pipe-syntax-1
language: elixir
rule:
any:
- - pattern: conn = conn |> $METHOD($PATH)
- - pattern: conn = $METHOD(conn, $PATH)
+ - pattern: conn = conn |> $METHOD($$$ALL_PARAMS)
+ - pattern: conn = $METHOD(conn, $$$ALL_PARAMS)
precedes:
pattern: assert json_response(conn, $STATUS)
fix:
template: |-
conn
- |> $METHOD($PATH)
+ |> $METHOD($$$ALL_PARAMS)
|> json_response(conn, $STATUS)
expandEnd:
pattern: |
@@ -22,14 +22,14 @@ rule:
conn =
conn
|> authenticate_conn($ACCOUNT)
- |> $METHOD($PATH)
+ |> $METHOD($$$ALL_PARAMS)
precedes:
pattern: assert json_response(conn, $STATUS)
fix:
template: |-
conn
|> authenticate_conn($ACCOUNT)
- |> $METHOD($PATH)
+ |> $METHOD($$$ALL_PARAMS)
|> json_response($STATUS)
expandEnd:
pattern: |
표현력을 높여 규칙을 더 정밀하게 쓰는 방법을 활용해, 유지해야 할 규칙 코드를 늘리지 않고 문제를 해결했다.
코드를 더 보니 다음과 같은 테스트도 있다:
conn = put(conn, contract_path(conn, :move, contract.id, %{target_folder_id: folder.id}))
assert response(conn, 401)
지금까지 다룬 것과 거의 똑같지만, json_response 대신 response라는 헬퍼를 쓴다. 이번에도 파라미터화를 활용하자. 헬퍼 함수 이름을 치환 가능한 변수로 바꾼다:
diff --git a/rules.yaml b/rules.yaml
index e495b5e58..b5e08456b 100644
--- a/rules.yaml
+++ b/rules.yaml
@@ -5,15 +5,15 @@ rule:
- pattern: conn = conn |> $METHOD($$$ALL_PARAMS)
- pattern: conn = $METHOD(conn, $$$ALL_PARAMS)
precedes:
- pattern: assert json_response(conn, $STATUS)
+ pattern: assert $RESPONSE_HELPER(conn, $STATUS)
fix:
template: |-
conn
|> $METHOD($$$ALL_PARAMS)
- |> json_response(conn, $STATUS)
+ |> $RESPONSE_HELPER(conn, $STATUS)
expandEnd:
pattern: |
- assert json_response(conn, $STATUS)
+ assert $RESPONSE_HELPER(conn, $STATUS)
---
id: conn-pipe-syntax-2
language: elixir
@@ -24,13 +24,13 @@ rule:
|> authenticate_conn($ACCOUNT)
|> $METHOD($$$ALL_PARAMS)
precedes:
- pattern: assert json_response(conn, $STATUS)
+ pattern: assert $RESPONSE_HELPER(conn, $STATUS)
fix:
template: |-
conn
|> authenticate_conn($ACCOUNT)
|> $METHOD($$$ALL_PARAMS)
- |> json_response($STATUS)
+ |> $RESPONSE_HELPER($STATUS)
expandEnd:
pattern: |
- assert json_response(conn, $STATUS)
+ assert $RESPONSE_HELPER(conn, $STATUS)
좋다. ast-grep을 다시 실행하면 더 많은 패턴 인스턴스를 찾아 전역적으로 일관되게 맞춰 준다.
마지막으로 더 살펴보니 이런 코드도 보인다:
assert response =
conn
|> authenticate_conn(account)
|> post(document_path(conn, :create, draft.id), %{
title: "My draft",
content: "This is the content"
})
|> json_response(201)
assert response == %{"document" => %{"id" => "3ec51098-d00c-496b-bfbf-f90cb4e1a4ba"}}
여기서의 문제는 앞서 본 것과 같다. 응답 본문에 대해 굳이 assert할 필요가 없다. 지금까지 다룬 패턴과는 조금 다르지만, ast-grep의 또 다른 멋진 기능을 보여 주기엔 충분히 비슷하다.
먼저 지금까지의 지식으로 고쳐 보자. 규칙을 하나 더 도입한다:
diff --git a/rules.yaml b/rules.yaml
index b5e08456b..365714b45 100644
--- a/rules.yaml
+++ b/rules.yaml
@@ -34,3 +34,20 @@ fix:
expandEnd:
pattern: |
assert $RESPONSE_FUNCTION(conn, $STATUS)
+---
+id: conn-pipe-syntax-6
+language: elixir
+rule:
+ pattern: |
+ assert $VARIABLE =
+ conn
+ |> authenticate_conn($ACCOUNT)
+ |> $METHOD($$$ALL_PARAMS)
+ |> $RESPONSE_FUNCTION($STATUS)
+fix:
+ template: |-
+ $VARIABLE =
+ conn
+ |> authenticate_conn($ACCOUNT)
+ |> $METHOD($$$ALL_PARAMS)
+ |> $RESPONSE_FUNCTION($STATUS)
이 규칙은 위 문제를 잘 고쳐 준다. 하지만 뜻하지 않게 다음과 같은 코드도 "고쳐" 버린다:
-assert %{"drafts" => draft} =
- conn
- |> authenticate_conn(owner)
- |> get(draft_path(conn, :index))
- |> json_response(200)
+%{"drafts" => draft} =
+ conn
+ |> authenticate_conn(owner)
+ |> get(draft_path(conn, :index))
+ |> json_response(200)
여기에는 미묘한 차이가 있다. 테스트의 의미론은 변하지 않았지만, 나중에 코드 변경으로 테스트가 실패했을 때 ExUnit의 assert 매크로가 예쁘게 포맷한 diff 대신 MatchError가 날 수도 있다.
그렇다면 변수 패턴이 아니라 "식별자 변수"일 때만 매칭하라고 ast-grep에 어떻게 알려 줄까? constraints 필터를 사용해 변수의 종류를 지정할 수 있다. 변수는 identifier여야 한다:
diff --git a/rules.yaml b/rules.yaml
index f49c23a57..0c5fd1c6e 100644
--- a/rules.yaml
+++ b/rules.yaml
@@ -44,6 +44,9 @@ rule:
|> authenticate_conn($ACCOUNT)
|> $METHOD($$$ALL_PARAMS)
|> $RESPONSE_FUNCTION($STATUS)
+constraints:
+ VARIABLE:
+ kind: identifier
fix:
template: |-
$VARIABLE =
수정된 테스트 파일을 되돌린 뒤 ast-grep을 다시 실행하면, 불필요한 변경은 더 이상 보이지 않는다. 훌륭하다 :)
결국 최종 rules.yaml은 다음과 같은 모습이 되었다:
id: conn-pipe-syntax-1
language: elixir
rule:
any:
- pattern: conn = conn |> $METHOD($$$ALL_PARAMS)
- pattern: conn = $METHOD(conn, $$$ALL_PARAMS)
precedes:
pattern: assert $RESPONSE_FUNCTION(conn, $STATUS)
fix:
template: |-
conn
|> $METHOD($$$ALL_PARAMS)
|> $RESPONSE_FUNCTION($STATUS)
expandEnd:
pattern: |
assert $RESPONSE_FUNCTION(conn, $STATUS)
---
id: conn-pipe-syntax-2
language: elixir
rule:
pattern: |
conn =
conn
|> authenticate_conn($ACCOUNT)
|> $METHOD($$$ALL_PARAMS)
precedes:
pattern: assert $RESPONSE_FUNCTION(conn, $STATUS)
fix:
template: |-
conn
|> authenticate_conn($ACCOUNT)
|> $METHOD($$$ALL_PARAMS)
|> $RESPONSE_FUNCTION($STATUS)
expandEnd:
pattern: |
assert $RESPONSE_FUNCTION(conn, $STATUS)
---
id: conn-pipe-syntax-3
language: elixir
rule:
pattern: |
assert $VARIABLE =
conn
|> authenticate_conn($ACCOUNT)
|> $METHOD($$$ALL_PARAMS)
|> $RESPONSE_FUNCTION($STATUS)
constraints:
VARIABLE:
kind: identifier
fix:
template: |-
$VARIABLE =
conn
|> authenticate_conn($ACCOUNT)
|> $METHOD($$$ALL_PARAMS)
|> $RESPONSE_FUNCTION($STATUS)
이 규칙 묶음은 이제 CI 스크립트에 추가할 수 있다:
ast-grep scan --rule rule.yaml
규칙 위반이 있으면 다음과 같은 출력이 나온다:
test/api/controllers/category_controller_test.exs
help[conn-pipe-syntax-1]:
@@ -52,8 +52,9 @@
53 53│ end
54 54│
55 55│ test "returns 401 for unauthenticated request", %{conn: conn} do
56 │- conn = get(conn, category_path(conn, :index))
57 │- assert json_response(conn, 401)
56│+ conn
57│+ |> get(category_path(conn, :index))
58│+ |> json_response(401)
58 59│ end
59 60│ end
60 61│ end
test/api/controllers/category_controller_test.exs
help[conn-pipe-syntax-5]:
@@ -10,12 +10,11 @@
11 11│ insert(:category, owner: account_1)
12 12│ insert(:category, owner: account_2)
13 13│
14 │- conn =
14│+ assert %{"categories" => categories} =
15 15│ conn
16 16│ |> authenticate_conn(account_1)
17 17│ |> get(category_path(conn, :index))
18 │-
19 │- assert %{"categories" => categories} = json_response(conn, 200)
18│+ |> json_response(200)
20 19│
21 20│ assert Enum.count(categories) == 2
22 21│ end
짧은 grep-ast 여정을 여기서 마치자. 이런 치환을 손으로 다 했다면, 말 그대로 고역이었을 것이다.
우리는 any, precedes, expandEnd, 멀티 메타변수, constraints(kind 사용) 등을 살펴보았다. 하지만 이는 빙산의 일각일 뿐이며, ast-grep은 훨씬 더 다양한 방법으로, 원하는 깊이만큼 규칙을 구성할 수 있다.
공식 웹사이트에는 다양한 예시와 함께 잘 정리된 문서가 있다: https://ast-grep.github.io/. 저자도 매우 친절하고, 공식 Discord 채널에서 활발히 활동 중이다: https://discord.com/invite/4YZjf6htSQ.
즐거운 찾기와 치환 작업 되세요!