본문으로 건너뛰기

AST 재작성

AST 재작성은 대체 템플릿을 사용하는 AST 검색입니다. Greph은 AST 검색과 동일한 매처를 실행한 다음, 매치된 바이트 범위의 소스에 재작성된 템플릿을 삽입합니다. 주변 공백, 들여쓰기, 주석은 보존되며 매치된 코드 자체는 다시 출력됩니다 (매치 내부 참고).

빠른 예제

셸
# Convert array() literals to []
./vendor/bin/greph -p 'array($$$ITEMS)' -r '[$$$ITEMS]' --dry-run src
./vendor/bin/greph -p 'array($$$ITEMS)' -r '[$$$ITEMS]' src

# isset ternary -> null coalesce
./vendor/bin/greph -p 'isset($X) ? $X : $Y' -r '$X ?? $Y' --dry-run src

# Rename a method call
./vendor/bin/greph -p '$obj->oldName($$$ARGS)' -r '$obj->newName($$$ARGS)' src

# Confirm each file before writing
./vendor/bin/greph -p 'array($$$ITEMS)' -r '[$$$ITEMS]' --interactive src

대체 템플릿

대체 템플릿은 검색 패턴과 동일한 메타변수 구문을 사용합니다. 캡처된 노드는 이름으로 다시 삽입됩니다:

패턴에서 템플릿에서 효과
$X $X 캡처된 노드 삽입
$$$ARGS $$$ARGS 캡처된 노드 시퀀스 삽입
$NAME (식별자) $NAME 캡처된 식별자 삽입

패턴에는 있지만 템플릿에는 없는 메타변수는 재작성된 코드에서 제거됩니다. 템플릿에는 있지만 패턴에는 없는 메타변수는 현재 오류로 처리되지 않습니다. 문자 그대로 출력되므로 -r '$o->$m($$$A, $EXTRA)'은 $EXTRA 변수를 코드에 씁니다 (#4). 먼저 --dry-run로 템플릿을 확인하세요.

모드

플래그 동작
(없음) 매치되는 모든 파일에 재작성 적용
--dry-run 재작성된 파일 콘텐츠를 표준 출력으로 출력하고 쓰지 않음
--interactive 각 파일을 쓰기 전에 Rewrite <file>? [y/N]에 확인 요청

기본 동작은 "변경 사항 모두 쓰기"입니다. 패턴을 다듬는 동안에는 먼저 --dry-run을 사용하고, 변경 내용이 만족스러우면 플래그를 제거하세요.

형식 보존

Greph은 파일 형식을 다시 지정하지 않습니다. 매처는 원본 소스에서 모든 매치의 바이트 범위를 기록하고, 재작성기는 렌더링된 템플릿을 해당 범위에 직접 삽입합니다. 건드리지 않은 코드는 다음 항목을 포함해 원래 그대로 유지됩니다:

  • 들여쓰기와 뒤따르는 공백
  • 인라인 및 독립 주석
  • 빈 줄 배치
  • 문자열 따옴표 형식 및 연결 방식
  • PHP 열기/닫기 태그 형식

이는 전체 형식 재지정과 의도적으로 절충한 방식입니다. Greph은 매치하지 않은 코드를 "정리"하지 않지만, 관련 없는 줄에 불필요한 변경을 만들지도 않습니다.

매치 내부

매치된 범위 자체는 PHP-Parser의 예쁜 출력기로 렌더링한 템플릿으로 대체되며, 캡처된 노드는 소스 텍스트에서 복사하지 않고 다시 출력됩니다. 현재 동작은 다음과 같습니다 (#2):

  • 매치 내부에서 뒤따르는 노드에 연결되지 않은 주석은 삭제됩니다. 예를 들어 배열의 마지막 항목 뒤에 있는 후행 주석이나 비어 있는 배열 안의 주석이 해당합니다.
  • 캡처된 노드에 연결된 주석은 유지되지만 별도 줄로 이동할 수 있어 한 줄 표현식이 여러 줄로 바뀔 수 있습니다.
  • 매치 내부의 줄바꿈은 유지되지 않습니다. 이러한 주석이 없는 여러 줄 array(...)는 한 줄 [...]가 됩니다.
  • 리터럴 표기는 정규화됩니다. 0x1F는 0x1f가 되고, 1_000는 1000가 됩니다. "\x41" 같은 이스케이프 시퀀스는 문자로 출력되며 ("A"), 유연한 heredoc은 들여쓰기를 잃습니다.
PHP
// before
$d = array(
    "a" => 1,  // trailing comment
);

// after -p 'array($$$ITEMS)' -r '[$$$ITEMS]'
$d = ["a" => 1];

주석이나 직접 형식을 지정한 리터럴이 포함된 매치는 --dry-run 출력을 검토하세요.

여러 줄 및 중첩 매치

여러 줄 패턴도 한 줄 패턴과 동일하게 작동합니다. 대체 코드는 메타변수를 삽입한 뒤 예쁜 출력기로 렌더링되므로, 대체된 코드의 배치는 원래 매치가 아니라 출력기에서 정해집니다.

Greph은 각 파일을 한 번에 재작성하며 서로 겹치는 매치를 함께 적용하지 않습니다. 파일 끝에서부터 역순으로 매치를 순회하고, 이미 재작성한 범위와 겹치는 매치는 건너뜁니다. 서로 다른 오프셋에서 시작하는 중첩 매치에서는 내부 매치만 재작성되고 외부 매치는 그대로 유지됩니다:

PHP
// before
$b = array(1, array(2));

// after one run of -p 'array($$$ITEMS)' -r '[$$$ITEMS]'
$b = array(1, [2]);

// after a second run
$b = [1, [2]];

두 매치가 같은 오프셋에서 시작하면 (예를 들어 메서드 체인에 $obj->$method($$$ARGS)를 매치한 경우) 외부 매치가 대신 재작성됩니다. 패턴이 중첩될 수 있다면 --dry-run에서 아무것도 출력되지 않을 때까지 재작성을 반복하세요 (#3). ast-grep는 중첩된 두 매치 중 바깥쪽을 재작성하므로 여기서는 vendor/bin/sg와 업스트림 sg의 결과가 다릅니다.

프로그래밍 방식 사용

PHP
use Greph\Greph;
use Greph\Ast\AstSearchOptions;

$rewrites = Greph::rewriteAst(
    'array($$$ITEMS)',
    '[$$$ITEMS]',
    'src',
    new AstSearchOptions(jobs: 4),
);

foreach ($rewrites as $result) {
    if (!$result->changed()) {
        continue;
    }

    echo "{$result->file}: {$result->replacementCount} replacement(s)\n";
    file_put_contents($result->file, $result->rewrittenContents);
}

Greph::rewriteAst()은 파일을 직접 쓰지 않습니다. 변경 사항을 커밋할지 (또는 --dry-run 및 거부된 --interactive 사례에서 건너뛸지)는 CLI이 결정합니다. 따라서 차이를 먼저 확인하고 적용하려는 자동화 에이전트에서도 파사드를 안전하게 호출할 수 있습니다.

RewriteResult에서 제공하는 항목:

  • file: 절대 경로
  • originalContents: 재작성 전 파일 내용
  • rewrittenContents: 재작성 후 파일 내용
  • replacementCount: 구조적 대체 횟수
  • changed(): originalContents !== rewrittenContents일 때 true를 반환하는 헬퍼

rewrite를 사용할 때

다음과 같은 경우에는 Rewrite가 적합합니다:

  • 변환이 국소적이고 구조적일 때(파일 전체를 재구성하는 작업이 아닐 때)
  • 반복 실행해 변경 사항이 없어질 때까지 다시 적용할 수 있는 결정적이고 반복 가능한 리팩터링을 원할 때
  • 적용 전에 변경 사항을 미리 보고 싶을 때
  • 외부 바이너리를 실행하지 않고 스크립트나 CI 작업에 리팩터링을 통합하고 싶을 때

다음과 같은 경우에는 Rewrite가 적합하지 않습니다:

  • 변환에 PHP-Parser가 단일 파일에서 확인할 수 없는 형식 정보가 필요한 경우
  • 파일 경계를 넘어 가져오기를 추가하거나 제거해야 하는 경우
  • 리팩터링이 아니라 포맷터를 원하는 경우(PHP-CS-Fixer 사용)