C++ 커스텀 컴파일러 패스 | Clang 플러그인·AST 변환·커스텀 진단 [#55-6]
이 글의 핵심
C++ 컴파일러 확장: Clang 플러그인 작성, AST 탐색, 코드 변환, 커스텀 경고·에러. 실무 코드 분석·DSL·정적 검사 도구 구현. 시나리오 1: 매직 넘버 산재 팀 코드베이스에 if (retrycount > 3), sleep(5000) 같은 하드코딩된 숫자가 수백 개 있습니다. 리팩터링 시 상수로 추출해야 하는데, 수동 검색은 놓치는 경우가 많습니다.
들어가며: “코드 규칙을 자동으로 검사하고 싶어요”
실제 겪는 문제 시나리오
시나리오 1: 매직 넘버 산재
팀 코드베이스에 if (retry_count > 3), sleep(5000) 같은 하드코딩된 숫자가 수백 개 있습니다. 리팩터링 시 상수로 추출해야 하는데, 수동 검색은 놓치는 경우가 많습니다. 컴파일 시점에 자동으로 경고를 내고 싶습니다.
시나리오 2: 함수 재선언 시 매개변수 이름 불일치
헤더에는 int divide(int numerator, int denominator);로 선언했는데, 구현부에서 int divide(int denominator, int numerator)로 바꿔 써서 버그가 발생했습니다. 재선언 시 매개변수 이름이 다르면 경고를 받고 싶습니다.
시나리오 3: 금지된 API 사용 탐지
레거시 malloc/free 대신 std::unique_ptr를 쓰도록 팀 규칙을 정했는데, 신규 코드가 여전히 malloc을 사용합니다. 특정 함수 호출을 금지하고 컴파일 에러를 내고 싶습니다.
시나리오 4: 도메인 특화 검사
게임 엔진에서 Update() 함수는 반드시 float delta_time을 첫 번째 인자로 받아야 합니다. 커스텀 규칙을 컴파일러 수준에서 검사하고 싶습니다.
시나리오 5: 코드 메트릭스 자동 수집
함수당 복잡도, 중첩 깊이, 호출 그래프 등을 빌드 시점에 자동으로 수집해 대시보드에 표시하고 싶습니다.
시나리오 6: 보안 취약점 패턴 탐지
strcpy, sprintf 같은 위험 함수 사용을 컴파일 시점에 차단하고, 대안(strncpy, snprintf)을 제안하고 싶습니다.
이런 요구를 충족하려면 컴파일러 확장이 필요합니다. C++에서는 Clang 플러그인을 통해 AST(Abstract Syntax Tree)를 탐색하고, 커스텀 경고·에러를 발생시키거나, 코드를 변환할 수 있습니다.
이 글을 읽으면:
- Clang 플러그인의 구조와 동작 원리를 이해할 수 있습니다.
- AST Visitor를 이용해 코드를 분석하는 방법을 배울 수 있습니다.
- 커스텀 진단(경고·에러)을 구현할 수 있습니다.
- 실무에서 활용할 수 있는 프로덕션 패턴을 익힐 수 있습니다.
요구 환경: C++17 이상, Clang/LLVM (권장: LLVM 16+)
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
목차
- 기본 개념: AST와 컴파일러 파이프라인
- Clang 플러그인 구조
- 완전한 Clang 플러그인 예제
- AST Visitor로 코드 분석
- 커스텀 진단(경고·에러)
- 자주 발생하는 에러와 해결법
- 모범 사례와 주의점
- 프로덕션 패턴
- 빌드·실행 가이드
1. 기본 개념: AST와 컴파일러 파이프라인
AST란?
AST(Abstract Syntax Tree)는 소스 코드를 구문 분석한 뒤 만드는 트리 구조입니다. 컴파일러는 소스 → 토큰 → AST → IR → 기계어 순으로 변환합니다. 플러그인은 AST 단계에 개입해 분석·변환을 수행합니다.
flowchart LR
subgraph compile[컴파일 파이프라인]
A[소스 코드] --> B[전처리]
B --> C[토큰화]
C --> D[AST 생성]
D --> E["플러그인 (여기서 개입)"]
E --> F[IR 생성]
F --> G[기계어]
end
비유: AST는 “문장의 문법 구조를 나무처럼 펼쳐 놓은 것”입니다. int x = 3 + 5;라면, 루트는 선언(DeclStmt), 자식은 변수 선언과 이니셜라이저(정수 리터럴 3, 5와 이항 연산자 +)로 구성됩니다.
Clang 플러그인의 역할
Clang 플러그인은 FrontendAction을 구현해, 컴파일 중 AST가 완성된 시점에 추가 작업을 수행합니다. 코드 생성 전에 실행되므로, AST를 수정하지 않는 한 기존 컴파일 결과에 영향을 주지 않습니다.
| 항목 | 설명 |
|---|---|
| 분석 | AST 순회로 패턴 탐지, 통계 수집 |
| 진단 | 커스텀 경고·에러 발생 |
| 변환 | AST 수정 (Rewriter 사용, 주의 필요) |
| 생성 | AST 기반 코드 생성 |
2. Clang 플러그인 구조
세 가지 핵심 컴포넌트
flowchart TB
subgraph plugin[Clang 플러그인]
PA[PluginASTAction]
AC[ASTConsumer]
RV[RecursiveASTVisitor]
end
PA -->|CreateASTConsumer| AC
AC -->|HandleTranslationUnit| RV
RV -->|VisitXxxDecl/VisitXxxStmt| AST[AST 노드 방문]
- PluginASTAction: 플러그인 진입점.
CreateASTConsumer()로 Consumer 생성,ParseArgs()로 옵션 처리 - ASTConsumer: 번역 단위(Translation Unit) 단위로 AST 처리.
HandleTranslationUnit()에서 Visitor 호출 - RecursiveASTVisitor: AST를 재귀적으로 순회.
VisitCXXRecordDecl,VisitCallExpr등으로 특정 노드 처리
PluginASTAction vs ASTFrontendAction
- PluginASTAction: 동적 라이브러리로 로드되는 플러그인.
-fplugin=xxx.so로 사용 - ASTFrontendAction: LibTooling 기반 독립 실행 도구.
clang-tool처럼 단독 실행
플러그인은 기존 빌드 시스템에 -fplugin만 추가하면 되므로, CI/CD 통합이 쉽습니다.
3. 완전한 Clang 플러그인 예제
예제 1: 클래스/함수 이름 출력 플러그인
가장 단순한 플러그인으로, 모든 최상위 함수와 클래스 이름을 출력합니다.
// PrintNamesPlugin.cpp
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
#include "clang/Frontend/FrontendAction.h"
#include "llvm/Support/raw_ostream.h"
using namespace clang;
namespace {
class PrintNamesVisitor : public RecursiveASTVisitor<PrintNamesVisitor> {
public:
explicit PrintNamesVisitor(ASTContext *Ctx) : Context(Ctx) {}
bool VisitFunctionDecl(FunctionDecl *D) {
if (D->isTopLevelDecl()) {
llvm::outs() << "Function: " << D->getQualifiedNameAsString() << "\n";
}
return true; // 계속 순회
}
bool VisitCXXRecordDecl(CXXRecordDecl *D) {
if (D->isTopLevelDecl()) {
llvm::outs() << "Class: " << D->getQualifiedNameAsString() << "\n";
}
return true;
}
private:
ASTContext *Context;
};
class PrintNamesConsumer : public ASTConsumer {
public:
explicit PrintNamesConsumer(ASTContext *Ctx) : Visitor(Ctx) {}
void HandleTranslationUnit(ASTContext &Ctx) override {
Visitor.TraverseDecl(Ctx.getTranslationUnitDecl());
}
private:
PrintNamesVisitor Visitor;
};
class PrintNamesAction : public PluginASTAction {
public:
std::unique_ptr<ASTConsumer> CreateASTConsumer(
CompilerInstance &CI, llvm::StringRef) override {
return std::make_unique<PrintNamesConsumer>(&CI.getASTContext());
}
bool ParseArgs(const CompilerInstance &,
const std::vector<std::string> &) override {
return true;
}
};
} // namespace
static FrontendPluginRegistry::Add<PrintNamesAction> X(
"print-names", "Print top-level function and class names");
빌드 및 실행:
# LLVM/Clang 빌드 디렉터리에서
clang++ -std=c++17 -fPIC -shared -o PrintNamesPlugin.so PrintNamesPlugin.cpp \
-I$LLVM/include -I$LLVM/build/include \
-I$LLVM/clang/include -I$LLVM/build/tools/clang/include \
-L$LLVM/build/lib -lclangAST -lclangBasic -lclangFrontend -lclangSerialization
# 사용
clang++ -fplugin=./PrintNamesPlugin.so -fsyntax-only test.cpp
4. AST Visitor로 코드 분석
RecursiveASTVisitor 활용
RecursiveASTVisitor는 선행 순회(pre-order)로 AST를 방문합니다. 관심 있는 노드 타입에 대해 VisitXxx 메서드를 오버라이드합니다.
| 노드 타입 | Visit 메서드 | 용도 |
|---|---|---|
| 함수 선언 | VisitFunctionDecl | 함수 시그니처 분석 |
| 클래스 선언 | VisitCXXRecordDecl | 클래스/구조체 분석 |
| 함수 호출 | VisitCallExpr | 특정 함수 호출 탐지 |
| 이항 연산 | VisitBinaryOperator | 연산자 사용 분석 |
| 정수 리터럴 | VisitIntegerLiteral | 매직 넘버 탐지 |
| 변수 선언 | VisitVarDecl | 변수 사용 패턴 |
| 멤버 접근 | VisitMemberExpr | 필드 접근 분석 |
| If/For/While | VisitIfStmt, VisitForStmt | 제어 흐름 분석 |
예제 2: 매직 넘버 탐지 Visitor
리터럴 0, 1, -1을 제외한 정수 리터럴에 경고를 냅니다.
// MagicNumberVisitor - RecursiveASTVisitor 상속
class MagicNumberVisitor : public RecursiveASTVisitor<MagicNumberVisitor> {
public:
explicit MagicNumberVisitor(ASTContext *Ctx, DiagnosticsEngine &Diag)
: Context(Ctx), DiagEngine(Diag) {}
bool VisitIntegerLiteral(IntegerLiteral *Lit) {
llvm::APInt Val = Lit->getValue();
if (Val.getBitWidth() <= 64) {
int64_t V = Val.getSExtValue();
if (V != 0 && V != 1 && V != -1) {
unsigned ID = DiagEngine.getCustomDiagID(
DiagnosticsEngine::Warning,
"magic number %0; consider using a named constant");
DiagEngine.Report(Lit->getBeginLoc(), ID) << Val.toString(10, true);
}
}
return true;
}
private:
ASTContext *Context;
DiagnosticsEngine &DiagEngine;
};
예제 3: 특정 함수 호출 금지
malloc, free 직접 사용을 금지하는 Visitor입니다.
class ForbiddenCallVisitor : public RecursiveASTVisitor<ForbiddenCallVisitor> {
public:
explicit ForbiddenCallVisitor(DiagnosticsEngine &Diag) : DiagEngine(Diag) {}
bool VisitCallExpr(CallExpr *E) {
const FunctionDecl *Callee = E->getDirectCallee();
if (!Callee) return true;
llvm::StringRef Name = Callee->getName();
if (Name == "malloc" || Name == "free") {
unsigned ID = DiagEngine.getCustomDiagID(
DiagnosticsEngine::Error,
"use of %0 is forbidden; use std::unique_ptr or std::vector instead");
DiagEngine.Report(E->getBeginLoc(), ID) << Name;
}
return true;
}
private:
DiagnosticsEngine &DiagEngine;
};
5. 커스텀 진단(경고·에러)
DiagnosticsEngine 사용
CompilerInstance::getDiagnostics()로 DiagnosticsEngine를 얻고, getCustomDiagID()로 진단 ID를 등록한 뒤 Report()로 출력합니다.
DiagnosticsEngine &Diag = CI.getDiagnostics();
// 진단 ID 생성: (레벨, 메시지)
unsigned WarnID = Diag.getCustomDiagID(
DiagnosticsEngine::Warning,
"custom warning message");
unsigned ErrID = Diag.getCustomDiagID(
DiagnosticsEngine::Error,
"custom error: %0");
// 보고
Diag.Report(SourceLocation::getFromRawEncoding(0), WarnID);
Diag.Report(Loc, ErrID) << "additional info";
예제 4: 매개변수 이름 불일치 검사 (완전한 플러그인)
함수 재선언 시 매개변수 이름이 다르면 경고를 냅니다. 실무에서 자주 쓰이는 패턴입니다.
// ParameterNameChecker.cpp
#include "clang/AST/AST.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
#include "llvm/Support/raw_ostream.h"
using namespace clang;
namespace {
class ParamNameVisitor : public RecursiveASTVisitor<ParamNameVisitor> {
public:
explicit ParamNameVisitor(DiagnosticsEngine &Diag) : DiagEngine(Diag) {}
bool VisitFunctionDecl(const FunctionDecl *FD) {
const FunctionDecl *Prev = FD->getPreviousDecl();
if (!Prev || !FD->hasPrototype() || !Prev->hasPrototype())
return true;
if (FD->getNumParams() != Prev->getNumParams())
return true;
for (unsigned i = 0, N = FD->getNumParams(); i < N; ++i) {
const ParmVarDecl *Param = FD->getParamDecl(i);
const ParmVarDecl *PrevParam = Prev->getParamDecl(i);
if (Param->getName().empty() || PrevParam->getName().empty())
continue;
if (Param->getIdentifier() != PrevParam->getIdentifier()) {
unsigned WarnID = DiagEngine.getCustomDiagID(
DiagnosticsEngine::Warning,
"parameter name mismatch with previous declaration");
DiagEngine.Report(Param->getLocation(), WarnID);
unsigned NoteID = DiagEngine.getCustomDiagID(
DiagnosticsEngine::Note,
"previous declaration was here");
DiagEngine.Report(PrevParam->getLocation(), NoteID);
}
}
return true;
}
private:
DiagnosticsEngine &DiagEngine;
};
class ParamNameConsumer : public ASTConsumer {
public:
explicit ParamNameConsumer(DiagnosticsEngine &Diag) : Visitor(Diag) {}
void HandleTranslationUnit(ASTContext &Ctx) override {
Visitor.TraverseDecl(Ctx.getTranslationUnitDecl());
}
private:
ParamNameVisitor Visitor;
};
class ParamNameChecker : public PluginASTAction {
protected:
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
llvm::StringRef) override {
return std::make_unique<ParamNameConsumer>(CI.getDiagnostics());
}
bool ParseArgs(const CompilerInstance &,
const std::vector<std::string> &) override {
return true;
}
};
} // namespace
static FrontendPluginRegistry::Add<ParamNameChecker> X(
"check-param-names", "Check parameter name consistency across redeclarations");
테스트 코드 (test.cpp):
// test.cpp
int divide(int numerator, int denominator);
int main() {
return divide(10, 2);
}
int divide(int denominator, int numerator) { // 매개변수 순서/이름 바뀜
return numerator / denominator;
}
실행:
clang++ -c -Xclang -load -Xclang ./ParamNameChecker.so \
-Xclang -add-plugin -Xclang check-param-names test.cpp
출력 예시:
test.cpp:9:16: warning: parameter name mismatch with previous declaration
int divide(int denominator, int numerator) {
^
test.cpp:1:16: note: previous declaration was here
int divide(int numerator, int denominator);
^
진단 레벨
| 레벨 | 의미 |
|---|---|
DiagnosticsEngine::Warning | 경고 (빌드 계속) |
DiagnosticsEngine::Error | 에러 (빌드 실패) |
DiagnosticsEngine::Note | 참고 (선행 진단에 대한 보조 정보) |
DiagnosticsEngine::Remark | 최적화 관련 메시지 |
6. 자주 발생하는 에러와 해결법
문제 1: “undefined symbol” 링크 에러
증상: 플러그인 .so 빌드 시 undefined reference to 'clang::...' 발생
원인: Clang/LLVM 라이브러리와 ABI가 맞지 않거나, 링크 순서/누락
해결법:
# ❌ 잘못된 예: 단일 라이브러리만 링크
clang++ -shared -o plugin.so plugin.cpp -lclangAST
# ✅ 올바른 예: CMakeLists.txt 사용 권장
add_library(MyPlugin MODULE MyPlugin.cpp)
target_link_libraries(MyPlugin PRIVATE
clangAST clangBasic clangFrontend clangSerialization
clangTooling clangParse clangSema clangAnalysis
LLVMSupport
)
문제 2: 플러그인 로드 실패 “cannot load plugin”
증상: -fplugin=./plugin.so 시 “Unable to load plugin ’./plugin.so’” 에러
원인:
- 플러그인과 Clang 버전 불일치
FrontendPluginRegistry::Add미등록- 동적 라이브러리 경로 문제
해결법:
# 플러그인과 동일한 Clang으로 컴파일할 소스 빌드
$CLANG_BUILD/bin/clang++ -fplugin=$PWD/plugin.so -fsyntax-only test.cpp
# macOS: 설치 이름 확인
otool -L plugin.dylib
문제 3: Visit 메서드에서 AST 수정
증상: Visitor 내부에서 AST 노드를 수정하면 크래시 또는 비정상 동작
원인: RecursiveASTVisitor는 순회 중 구조 변경을 가정하지 않음
해결법:
- 분석만 할 경우: 수정 금지
- 수정이 필요하면:
Rewriter+ 별도 패스, 또는TreeTransform사용
// ❌ 위험: 순회 중 노드 수정
bool VisitFunctionDecl(FunctionDecl *D) {
D->setXYZ(...); // 위험!
return true;
}
// ✅ 안전: 정보만 수집
bool VisitFunctionDecl(FunctionDecl *D) {
Names.push_back(D->getNameAsString());
return true;
}
문제 4: getQualifiedNameAsString()이 빈 문자열 반환
증상: 익명 네임스페이스, 템플릿 특수화 등에서 빈 문자열
원인: 일부 선언은 qualified name이 없음
해결법:
// ❌ 빈 문자열 가능
llvm::StringRef Name = D->getQualifiedNameAsString();
// ✅ 여러 fallback 사용
std::string getName(const NamedDecl *D) {
if (D->getIdentifier())
return D->getName().str();
if (const auto *RD = dyn_cast<CXXRecordDecl>(D))
return RD->getQualifiedNameAsString();
return "(anonymous)";
}
문제 5: 시스템 헤더까지 분석되어 경고 폭증
증상: <iostream>, <vector> 등에서도 커스텀 경고 발생
원인: Location이 시스템 헤더인지 미확인
해결법:
bool VisitCallExpr(CallExpr *E) {
SourceManager &SM = Context->getSourceManager();
if (SM.isInSystemHeader(E->getBeginLoc()))
return true; // 시스템 헤더는 무시
// ...
}
문제 6: 플러그인 빌드 시 “incompatible target” 에러
증상: error: the clang compiler does not support '-march=native' 또는 아키텍처 불일치
원인: 플러그인을 빌드한 컴파일러와 실행 시 사용하는 Clang이 다름
해결법:
# 플러그인과 동일한 clang++로 빌드
$LLVM_BUILD/bin/clang++ -shared -o plugin.so plugin.cpp ...
# 실행 시에도 동일한 clang 사용
$LLVM_BUILD/bin/clang++ -fplugin=./plugin.so -fsyntax-only test.cpp
문제 7: Visit 메서드 반환값 오해
증상: return false를 썼는데 의도와 다르게 동작
원인: true = 자식 노드 순회 계속, false = 해당 서브트리 순회 중단
해결법:
// true: 자식들도 방문 (대부분의 경우)
// false: 이 노드의 자식은 방문하지 않음 (조기 종료용)
bool VisitFunctionDecl(FunctionDecl *D) {
if (shouldSkip(D)) return false; // 이 함수 내부는 순회 안 함
process(D);
return true;
}
문제 8: SourceLocation이 invalid
증상: Loc.isValid()이 false여서 진단을 못 냄
원인: 매크로 확장, 컴파일러 생성 코드, Builtin 선언 등
해결법:
void reportIfValid(SourceLocation Loc, unsigned DiagID) {
if (!Loc.isValid()) return;
FullSourceLoc FSL = Context->getFullLoc(Loc);
if (!FSL.isInMainFile()) return; // 메인 파일이 아니면 선택적으로 스킵
DiagEngine.Report(Loc, DiagID);
}
7. 모범 사례와 주의점
1. 시스템 헤더 제외
사용자 코드에만 진단을 적용하려면 SourceManager::isInSystemHeader()로 필터링합니다.
bool isUserCode(SourceLocation Loc) {
return Loc.isValid() &&
!Context->getSourceManager().isInSystemHeader(Loc);
}
2. 매크로 확장 위치 처리
매크로에서 온 코드는 getSpellingLoc()과 getExpansionLoc()을 구분해 사용합니다.
// 매크로 정의 위치가 아닌, 확장된 소스 위치
SourceLocation Loc = E->getBeginLoc();
FullSourceLoc FSL = Context->getFullLoc(Loc);
if (FSL.isValid())
llvm::outs() << "at " << FSL.getSpellingLineNumber() << ":"
<< FSL.getSpellingColumnNumber() << "\n";
3. 플러그인 옵션 파싱
ParseArgs에서 -plugin-arg-<name>-<key>=<value> 형태로 옵션을 받습니다.
bool ParseArgs(const CompilerInstance &CI,
const std::vector<std::string> &Args) override {
for (size_t i = 0; i < Args.size(); ++i) {
if (Args[i] == "-warn-magic-numbers") {
WarnMagicNumbers = true;
} else if (Args[i].startswith("-max-magic=")) {
MaxMagic = std::stoi(Args[i].substr(11).str());
}
}
return true;
}
4. 성능: 필요한 노드만 방문
VisitStmt만 오버라이드하면 모든 문을 방문하므로 비효율적입니다. 관심 노드에 맞는 VisitXxx만 구현합니다.
// ❌ 비효율: 모든 문 방문
bool VisitStmt(Stmt *S) {
if (auto *CE = dyn_cast<CallExpr>(S)) { /* ... */ }
return true;
}
// ✅ 효율: CallExpr만 방문
bool VisitCallExpr(CallExpr *E) {
// ...
return true;
}
5. getActionType으로 실행 시점 제어
메모리 사용을 줄이려면 AddBeforeMainAction으로 코드 생성 전에 실행합니다.
PluginASTAction::ActionType getActionType() override {
return AddBeforeMainAction; // 코드 생성 전 실행
}
6. 템플릿 인스턴스화 처리
템플릿은 인스턴스화된 선언만 방문하거나, 명시적 특수화도 함께 처리할 수 있습니다.
bool VisitFunctionDecl(FunctionDecl *FD) {
// 템플릿 선언 자체는 건너뛰고, 인스턴스화된 것만 처리
if (FD->isTemplateInstantiation())
return true; // 또는 여기서 분석
if (FD->getTemplatedKind() != FunctionDecl::TK_NonTemplate)
return true; // 템플릿 선언 자체는 건너뛰기
// ...
}
7. 의존성 정보 활용
DeclContext로 부모 스코프(네임스페이스, 클래스)를 확인할 수 있습니다.
bool VisitCXXRecordDecl(CXXRecordDecl *D) {
const DeclContext *Ctx = D->getDeclContext();
if (const NamespaceDecl *NS = dyn_cast<NamespaceDecl>(Ctx)) {
llvm::StringRef NSName = NS->getName();
if (NSName == "internal") // internal 네임스페이스는 무시
return true;
}
// ...
}
8. 프로덕션 패턴
패턴 1: CI 통합
CMake에서 플러그인을 빌드하고, 대상 타겟에 자동으로 -fplugin을 붙입니다.
# CMakeLists.txt
add_library(OurChecker MODULE
ParamNameChecker.cpp
MagicNumberChecker.cpp
)
target_link_libraries(OurChecker PRIVATE
clangAST clangBasic clangFrontend clangSerialization
)
# 기존 타겟에 플러그인 추가
add_executable(MyApp main.cpp)
target_compile_options(MyApp PRIVATE
-fplugin=${CMAKE_BINARY_DIR}/libOurChecker.so
-fplugin-arg-our-checker-warn-all
)
패턴 2: 플러그인 조합
여러 검사기를 하나의 플러그인으로 묶거나, -fplugin을 여러 번 지정합니다.
clang++ -fplugin=./Checker1.so -fplugin=./Checker2.so -fsyntax-only app.cpp
패턴 3: JSON/XML 리포트 출력
CI에서 파싱하기 쉽도록 진단을 JSON으로 출력합니다.
void reportAsJson(SourceLocation Loc, llvm::StringRef Msg) {
FullSourceLoc FSL = Context->getFullLoc(Loc);
llvm::outs() << "{\"file\":\"" << getFilename(Loc)
<< "\",\"line\":" << FSL.getSpellingLineNumber()
<< ",\"msg\":\"" << Msg << "\"}\n";
}
패턴 4: 팀별 규칙 설정
ParseArgs에서 프로젝트/팀별 규칙을 켜고 끕니다.
// -fplugin-arg-our-checker-strict
// -fplugin-arg-our-checker-relaxed
// -fplugin-arg-our-checker-config=/path/to/rules.json
패턴 5: AST Context 캐싱
큰 TU에서 반복 접근이 많다면, 자주 쓰는 정보를 캐시합니다.
class CachedVisitor : public RecursiveASTVisitor<CachedVisitor> {
llvm::DenseMap<const Decl *, std::string> NameCache;
std::string getCachedName(const NamedDecl *D) {
auto It = NameCache.find(D);
if (It != NameCache.end()) return It->second;
std::string N = D->getQualifiedNameAsString();
NameCache[D] = N;
return N;
}
};
패턴 6: 도메인 특화 시그니처 검사 (Update 예제)
게임 엔진처럼 Update(float delta_time) 시그니처를 강제하는 예제입니다.
bool VisitCXXRecordDecl(CXXRecordDecl *D) {
for (CXXMethodDecl *M : D->methods()) {
if (M->getName() != "Update") continue;
if (M->getNumParams() < 1) {
DiagEngine.Report(M->getLocation(), ErrID)
<< "Update() must have (float delta_time) as first parameter";
continue;
}
QualType FirstParam = M->getParamDecl(0)->getType();
if (!FirstParam->isFloatingType()) {
DiagEngine.Report(M->getParamDecl(0)->getLocation(), ErrID)
<< "first parameter must be float";
}
}
return true;
}
패턴 7: LibTooling vs 플러그인 선택
| 방식 | 장점 | 단점 |
|---|---|---|
| 플러그인 | 기존 빌드에 -fplugin만 추가, CI 통합 쉬움 | Clang 버전에 강하게 종속 |
| LibTooling | 독립 실행, compile_commands.json 활용 | 빌드 시스템과 별도 실행 필요 |
단일 파일/디렉터리 분석, 리팩터링 도구는 LibTooling이 적합하고, 팀 전체 빌드에 규칙을 적용할 때는 플러그인이 적합합니다.
9. 빌드·실행 가이드
LLVM/Clang 소스 빌드
# LLVM 18 기준
git clone --depth 1 -b release/18.x https://github.com/llvm/llvm-project.git
cd llvm-project
cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=Release \
-DLLVM_ENABLE_PROJECTS=clang -DLLVM_TARGETS_TO_BUILD=host \
llvm
ninja -C build
플러그인 CMake 예제
# plugins/OurChecker/CMakeLists.txt
set(LLVM_DIR "${LLVM_BUILD}/lib/cmake/llvm")
set(Clang_DIR "${LLVM_BUILD}/lib/cmake/clang")
find_package(LLVM REQUIRED CONFIG)
find_package(Clang REQUIRED CONFIG)
add_library(OurChecker MODULE OurChecker.cpp)
target_include_directories(OurChecker PRIVATE
${LLVM_INCLUDE_DIRS}
${CLANG_INCLUDE_DIRS}
)
target_link_libraries(OurChecker PRIVATE
clangAST clangBasic clangFrontend clangSerialization
clangTooling clangParse clangSema
LLVMSupport
)
# 플러그인은 -fno-rtti -fno-exceptions 사용 가능
set_target_properties(OurChecker PROPERTIES
PREFIX ""
OUTPUT_NAME "OurChecker"
)
구현 체크리스트
- LLVM/Clang 버전과 플러그인 ABI 일치
-
FrontendPluginRegistry::Add등록 - 시스템 헤더 제외
-
ParseArgs에서 옵션 처리 - CI에서
-fplugin경로 및 인자 설정 - 대용량 TU에서 성능 테스트
정리
| 항목 | 설명 |
|---|---|
| 구조 | PluginASTAction → ASTConsumer → RecursiveASTVisitor |
| 분석 | VisitXxxDecl, VisitXxxStmt로 관심 노드 처리 |
| 진단 | DiagnosticsEngine::getCustomDiagID + Report |
| 주의 | 시스템 헤더 제외, AST 수정 금지, 버전 일치 |
핵심 원칙:
- 필요한 노드 타입에 맞는 Visit 메서드만 구현
- 사용자 코드에만 진단 적용
- 플러그인과 Clang 버전을 맞출 것
- CI에서
-fplugin으로 자동 검사
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 커스텀 코드 분석, 자동 코드 생성, 도메인 특화 언어(DSL), 코딩 규칙 검사, 매직 넘버·금지 API 탐지 등에 활용합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. Clang Plugins 공식 문서, RAVFrontendAction 튜토리얼, LLVM/Clang 소스의 clang/examples/ 디렉터리를 참고하세요.
Q. GCC에서도 비슷한 걸 할 수 있나요?
A. GCC는 플러그인 API가 다르고, GIMPLE 단위로 작업합니다. AST 기반 도구가 필요하면 Clang이 더 적합합니다. GCC 플러그인은 GCC Plugin API를 참고하세요.
Q. 프로덕션에서 주의할 점은?
A. (1) 플러그인과 Clang 버전을 반드시 맞출 것, (2) 대용량 TU에서 컴파일 시간 증가를 측정할 것, (3) 시스템 헤더를 제외해 경고 폭증을 막을 것, (4) CI에서 플러그인 빌드 실패 시 메인 빌드가 깨지지 않도록 분리할 것.
한 줄 요약: Clang 플러그인으로 AST를 분석하고 커스텀 진단을 구현해, 팀 규칙을 컴파일 시점에 자동 검사할 수 있습니다.
관련 글
- C++ 코드 생성 완벽 가이드 | 템플릿·매크로·Clang·외부 도구 [#55-6]
- C++ 메타프로그래밍 고급 | SFINAE·Concepts·constexpr·타입 트레이트 가이드