구조적으로 코드 리팩터
이 가이드에서는 구조적 검색과 재작성으로 에이전트가 광범위한 기계적 변경 — 코드모드, 대규모 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()는 찾지 못합니다 — 그런데 보통 여러분이 찾고 있던 버그가 바로 이것입니다. 텍스트 검색으로는 이를 표현할 수 없습니다.
패턴이 아무것도 찾지 못할 때
섹션 제목: “패턴이 아무것도 찾지 못할 때”순서대로 시도할 두 가지가 있습니다:
- 더 작게 만드십시오.
if (a) { foo($X); }보다foo($X)를 먼저 시도합니다. 그 자체로는 유효한 코드가 아닌 패턴은 의도한 구문이 아닌 경우가 많습니다. - 언어를 지정하십시오.
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 |
관련 항목
섹션 제목: “관련 항목”- 코드 인텔리전스 — 구조적 매칭이 단계만 몇 개 더 많은 정규식이 아닌 이유
- 내장 에이전트 도구 — 전체 인수 목록
- 코드 그래프로 코드 검색 — 심볼과 영향 측면