콘텐츠로 이동

구조적으로 코드 리팩터

이 가이드에서는 구조적 검색과 재작성으로 에이전트가 광범위한 기계적 변경 — 코드모드, 대규모 API 마이그레이션, 없애고 싶은 패턴 — 을 수행하도록 하는 방법을 보여 줍니다. 변경은 아무것도 기록되기 전에 검토할 수 있도록 스테이징됩니다.

ast_grep은 코드를 형태로 매칭합니다. ast_edit은 그것을 재작성합니다. resolve는 커밋하거나 버립니다.

구조적 도구는 코드 그래프가 파싱하는 언어를 다룹니다: Dart, JavaScript, TypeScript, TSX, PHP, Python, Rust, Zig, C, C++, Go, Java, Ruby, C#, Swift, Kotlin, R, Assembly, MATLAB, Ada. 이 도구들은 tree-sitter 문법이 스테이징되어 있어야 하는데, 배포 빌드에서는 항상 그렇습니다. 문법이 하나라도 빠지면 검색 품질이 떨어지는 것이 아니라 서버가 멈춥니다.

보지 않은 것을 재작성하지 마십시오. 에이전트에게 검색을 요청하십시오:

방금 null 검사한 nullable에서 dispose()를 호출하는 모든 곳을 찾아 주십시오.

ast_grep(pattern: "if ($X != null) $X.dispose()", language: "dart")

출력은 각 지점의 file:line과 매칭된 소스입니다.

토큰 매칭 대상
$NAME 캡처되는 임의의 단일 노드
$$$NAME 임의의 연속 형제 노드 — 인수, 문장
$_ 무엇이든, 이름 없이 캡처됨

중요한 것은 하나입니다: 반복된 $NAME은 같은 텍스트를 캡처해야 합니다.

if ($X != null) $X.dispose()

이 패턴은 자신이 검사한 대상을 dispose하는 가드를 찾아냅니다. if (a != null) b.dispose()는 찾지 못합니다 — 그런데 보통 여러분이 찾고 있던 버그가 바로 이것입니다. 텍스트 검색으로는 이를 표현할 수 없습니다.

순서대로 시도할 두 가지가 있습니다:

  1. 더 작게 만드십시오. if (a) { foo($X); }보다 foo($X)를 먼저 시도합니다. 그 자체로는 유효한 코드가 아닌 패턴은 의도한 구문이 아닌 경우가 많습니다.
  2. 언어를 지정하십시오. language:는 path가 단일 파일을 가리킬 때만 추론됩니다.

어떤 파싱 컨텍스트도 받아들이지 않는 패턴은 파싱할 수 없음으로 보고됩니다 — 겉모습대로 읽힌 적이 없는 대상에 대해 “일치 항목 없음”이라고 보고하는 대신, 도구가 그렇게 알려 줍니다.

재작성하고, 반영되기 전에 확인하십시오

섹션 제목: “재작성하고, 반영되기 전에 확인하십시오”
ast_edit(
pattern: "if ($X != null) $X.dispose()",
rewrite: "$X?.dispose()",
language: "dart",
)

결과는 제안입니다:

(proposed) ast_edit: if ($X != null) $X.dispose() → $X?.dispose()
12 replacements in 4 files:
lib/features/a/x.dart (3)
lib/features/b/y.dart (5)
…
Nothing has been written. Call resolve with edit_id "edit_1" and action
"accept" to apply it, or "discard" to drop it.

아직 디스크에는 아무것도 없습니다. 목록을 읽고, 검색이 보고한 지점 몇 곳을 확인한 다음:

resolve(edit_id: "edit_1", action: "accept")

또는

resolve(edit_id: "edit_1", action: "discard")

컴파일러로 작업을 확인하십시오

섹션 제목: “컴파일러로 작업을 확인하십시오”

이 작업을 구조적으로 하는 이유는 그것이 광범위하기 때문입니다. 광범위한 변경은 그럴듯해 보이는 재작성이 타입을 깨뜨리는 바로 그 지점입니다. resolve 후에 진단을 요청하십시오:

lsp(action: "diagnostics", file: "*")

file: "*"는 파일별 조회가 아니라 프로젝트 자체의 검사기(dart analyze, tsc --noEmit, cargo check, go build)를 실행합니다 — 파일 마흔 개를 건드린 변경 뒤에 던질 올바른 형태의 질문입니다.

log(msg, level)이 log(level: level, message: msg)로 바뀐다고 해 보겠습니다.

ast_grep(pattern: "log($MSG, $LEVEL)", language: "dart")

매칭된 지점을 보십시오. 그중 일부가 다른 두 인수 log라면, 패턴 안에서 그 차이를 표현하려 하지 말고 path를 의도한 대상이 있는 디렉터리로 좁히십시오.

ast_edit(
pattern: "log($MSG, $LEVEL)",
rewrite: "log(level: $LEVEL, message: $MSG)",
path: "lib/features/telemetry",
language: "dart",
)

그다음 resolve, 그다음 lsp(action: "diagnostics", file: "*")를 실행합니다.

상황 더 나은 도구
심볼 이름 바꾸기 lsp_rename — 형태가 아니라 참조를 따라갑니다
문자열, 주석 또는 파일 이름 찾기 search(내용) / search_files(이름)
“이걸 바꾸면 무엇이 깨지나?” 코드 그래프
문법이 지원하지 않는 언어 search와 edit