Documentation
¶
개요 (Overview) ¶
패키지 분석(Package analysis)은 모듈형 정적 분석과 분석 드라이버 프로그램 간의 인터페이스를 정의합니다.
배경 (Background) ¶
정적 분석(static analysis)이란 Go 코드 패키지를 검사하고 일련의 진단(diagnostics)(일반적으로 코드상의 오류)을 보고하며, 또한 제안된 리팩토링이나 기타 사실과 같은 다른 결과를 생성할 수 있는 함수입니다. 오류를 보고하는 분석은 비공식적으로 "체커(checker)"라고 불립니다. 예를 들어, printf 체커는 fmt.Printf 형식 문자열의 오류를 보고합니다.
"모듈형(modular)" 분석이란 한 번에 하나의 패키지를 검사하지만, 하위 레벨 패키지로부터 정보를 저장하고 이를 상위 레벨 패키지를 검사할 때 사용할 수 있는 분석입니다. 이는 툴체인 내의 별도 컴파일과 유사합니다. printf 체커는 모듈형입니다: log.Fatalf와 같은 함수가 fmt.Printf를 위임하는 것을 발견하면, 이 사실을 기록하고 다른 패키지에서 이루어진 호출을 포함하여 해당 함수의 호출도 검사합니다.
공통 인터페이스를 구현함으로써, 다양한 출처의 체커들은 명령줄 도구(예: vet), 텍스트 편집기 및 IDE, 빌드 및 테스트 시스템(예: go build, Bazel, 또는 Buck), 테스트 프레임워크, 코드 리뷰 도구, 코드베이스 인덱서(예: SourceGraph), 문서 보기 도구(예: godoc), 대규모 코드베이스를 위한 배치 파이프라인 등 광범위한 드라이버 프로그램에서 쉽게 선택, 통합 및 재사용될 수 있습니다.
Analyzer ¶
API의 주요 타입은 Analyzer입니다. Analyzer는 분석 함수를 정적으로 설명합니다: 그 이름, 문서화, 플래그, 다른 분석과의 관계, 그리고 물론 로직을 포함합니다.
분석을 정의하려면 사용자는 Analyzer 타입의 (논리적으로 상수인) 변수를 선언합니다. 다음은 go/analysis/passes 서브디렉터리의 분석기 중 하나에서 가져온 전형적인 예입니다:
분석 드라이버(analysis driver)는 vet과 같은 프로그램으로, 여러 분석을 실행하고 해당 분석들이 보고하는 진단 메시지를 출력합니다. 이 드라이버 프로그램은 필요한 Analyzer 목록을 가져와야 합니다. 일반적으로 각 Analyzer는 별도의 패키지에 존재합니다. 기존 드라이버에 새로운 Analyzer를 추가하려면 목록에 항목을 하나 더 추가하면 됩니다:
import ( "unusedresult"; "nilness"; "printf" ) var analyses = []*analysis.Analyzer{ unusedresult.Analyzer, nilness.Analyzer, printf.Analyzer, }
드라이버는 이름(name), 플래그(flags), 그리고 문서화(documentation)를 사용하여 수행하는 분석에 대한 온라인 도움말을 제공할 수 있습니다. doc 주석에는 간략한 한 줄 요약이 포함되며, 선택적으로 설명 단락이 뒤따릅니다.
Analyzer 타입은 위에 표시된 필드 외에도 더 많은 필드를 가집니다:
type Analyzer struct { Name string Doc string Flags flag.FlagSet Run func(*Pass) (interface{}, error) RunDespiteErrors bool ResultType reflect.Type Requires []*Analyzer FactTypes []Fact }
Flags 필드는 분석 동작을 제어하는 이름이 지정된(전역) 플래그 변수 세트를 선언합니다. vet과 달리, 분석 플래그는 명령줄 FlagSet에 직접 선언되지 않습니다. 이는 드라이버가 플래그 변수를 설정할 책임이 있습니다. 단일 분석 a를 위한 드라이버는 해당 플래그 f를 명령어줄에서 -f로 직접 노출할 수 있지만, 여러 분석을 위한 드라이버는 모호성을 피하기 위해 플래그 이름 앞에 분석 이름(-a.f)을 접두사로 붙일 수 있습니다. IDE는 그래픽 인터페이스를 통해 플래그를 노출할 수 있으며, 배치 파이프라인은 구성 파일에서 이를 설정할 수 있습니다.
RunDespiteErrors 플래그는 분석이 잘못된 타입의 코드도 처리할 수 있는지 여부를 나타냅니다. 그렇지 않다면, 드라이버는 구문 오류나 타입 오류가 있을 경우 분석을 건너뜁니다. 선택적 ResultType 필드는 이 분석에 의해 계산되어 다른 분석에서 사용할 수 있도록 제공되는 결과 값의 타입을 지정합니다. Requires 필드는 이 분석이 의존하는 분석 목록과 그 결과에 접근할 수 있는 곳을 지정하며, 드라이버가 분석을 실행할 순서를 제약합니다. FactTypes 필드는 모듈성(Modularity) 섹션에서 논의됩니다. analysis 패키지는 Analyzer에 대한 기본적인 건전성 검사(sanity checks)를 수행하는 Validate 함수를 제공하는데, 예를 들어 그 Requires 그래프가 비순환적(acyclic)인지, 그 fact 및 result 타입이 고유한지 등을 확인합니다.
마지막으로, Run 필드는 드라이버가 단일 패키지에 대해 분석을 실행하기 위해 호출할 함수를 포함합니다. 드라이버는 이 함수에 Pass 타입의 인스턴스를 전달합니다.
Pass ¶
Pass는 단일 작업 단위: 특정 Analyzer를 특정 Go 코드 패키지에 적용하는 것을 설명합니다. Pass는 분석되는 패키지에 대한 정보를 Analyzer의 Run 함수에 제공하며, 진단(diagnostics) 및 기타 정보를 드라이버로 보고하기 위한 작업을 Run 함수에 제공합니다.
type Pass struct { Fset *token.FileSet Files []*ast.File OtherFiles []string IgnoredFiles []string Pkg *types.Package TypesInfo *types.Info ResultOf map[*Analyzer]interface{} Report func(Diagnostic) ... }
The Fset, Files, Pkg, 그리고 TypesInfo 필드는 단일 Go 코드 패키지에 대한 구문 트리(syntax trees), 타입 정보, 및 소스 위치를 제공합니다.
OtherFiles 필드는 이 패키지의 일부인 assembly와 같은 비(非)Go 파일들의 이름을 제공합니다. 유사하게, IgnoredFiles 필드는 현재 빌드 구성의 일부는 아니지만 다른 빌드 구성에 포함될 수 있는 Go 및 비(非)Go 소스 파일들의 이름을 제공합니다. 이러한 파일들의 내용은 Pass.ReadFile을 사용하여 읽을 수 있습니다. 비(非)Go 파일을 로드하고 그에 대해 진단(diagnostics)을 보고하는 예시로는 "asmdecl" 또는 "buildtags" 분석기(analyzers)를 참조하십시오.
ResultOf 필드는 이 분석기가 필요로 하는, Analyzer.Requires 필드에 표현된 결과들을 제공합니다. 드라이버는 필요한 분석기들을 먼저 실행하고 그 결과를 이 맵에 사용 가능하게 만듭니다. 각 분석기는 자신의 Analyzer.ResultType 필드에 설명된 타입의 값을 반환해야 합니다. 예를 들어, "ctrlflow" 분석기는 패키지 내 각 함수에 대한 제어 흐름 그래프(control-flow graph)를 제공하는 *ctrlflow.CFGs를 반환합니다 (golang.org/x/tools/go/cfg 참조); "inspect" 분석기는 다른 분석기들이 패키지의 구문 트리(syntax trees)를 더 효율적으로 순회할 수 있게 하는 값을 반환하며; 그리고 "buildssa" 분석기는 SSA-형 중간 표현(intermediate representation)을 구성합니다. 이들 각 분석기는 핵심 API에 의존성을 추가하지 않으면서 후속 분석기들의 기능을 확장하므로, 분석 도구는 자신이 필요한 확장 기능에 대해서만 비용을 지불하게 됩니다.
Report 함수는 소스 위치와 관련된 메시지인 진단(diagnostic)을 방출합니다. 대부분의 분석에서 진단 자체가 주요 결과물입니다. 편의를 위해 Pass는 문자열을 포맷하여 새로운 진단을 보고하는 헬퍼 메서드인 Reportf를 제공합니다. 진단은 다음과 같이 정의됩니다:
type Diagnostic struct { Pos token.Pos Category string // optional Message string }
선택적인 Category 필드는 분석이 여러 종류의 진단을 생성할 때 메시지의 종류를 분류하는 짧은 식별자입니다.
Diagnostic 구조체는 Analyzer와 그 진단(diagnostic)의 상대적 중요성에 대한 의견이 사용자들 사이에서 매우 다양하기 때문에 심각도(severity)를 나타내는 필드를 가지고 있지 않습니다. 이 프레임워크의 설계는 각 Analyzer가 자신의 진단에 대한 심각도를 식별할 책임이 있다고 여기지 않습니다. 대신, 우리는 드라이버(driver)가 사용자가 선호하는 바에 따라 생성하는 Analyzer와 선택적 카테고리(Category)를 기반으로 진단의 필터링 및 우선순위 지정 기능을 제공할 것이라고 기대합니다.
대부분의 Analyzer는 타입이 지정된 Go 구문 트리(typed Go syntax trees)를 검사하지만, asmdecl이나 buildtag처럼 몇몇은 Go 소스 파일의 원시 텍스트 또는 어셈블리 같은 비-Go 파일을 검사합니다. 원시 텍스트 파일의 특정 라인에 대해 진단을 보고하려면 다음 순서를 사용하십시오:
content, err := pass.ReadFile(filename) if `err != nil { ... } tf := fset.AddFile(filename, -1, len(content)) tf.SetLinesForContent(content) ... pass.Reportf(tf.LineStart(line),
컴파일러의 타입 체커는 모듈형 정적 분석의 한 예입니다. Go 프로그램에 적용하고 싶은 다른 많은 체커들은 대체적이거나 비표준적인 타입 시스템으로 이해할 수 있습니다. 예를 들어, vet의 printf 체커는 함수가 'printf 래퍼' 타입을 가지고 있는지 추론하고, 그러한 함수의 호출에는 더 엄격한 검사를 적용합니다. 또한, 나중에 분석 패스에서 다른 printf 래퍼를 귀납적으로 식별하는 데 사용하기 위해 어떤 함수들이 printf 래퍼인지 기록합니다. “f는 printf 래퍼이다”와 같은 결과는 그 자체로는 흥미롭지 않지만 (진단과 같은) 흥미로운 결과를 위한 발판 역할을 하는 것을 Fact라고 부릅니다.
분석 API를 사용하면 분석이 새로운 종류의 Fact를 정의하고, 이러한 타입의 Fact를 현재 패키지 내에 선언된 객체(명명된 엔티티) 또는 전체 패키지에 연관시키거나, 특정 객체나 패키지와 연관된 기존 Fact를 쿼리할 수 있도록 합니다.
Fact를 사용하는 Analyzer는 그 타입들을 선언해야 합니다:
var Analyzer = &analysis.Analyzer{ Name: "printf", FactTypes: []analysis.Fact{new(isWrapper)}, ... } type isWrapper struct{} // => *types.Func f “is a printf wrapper”
드라이버 프로그램은 패스(pass)의 의존성에 대한 팩트(fact)가 패키지를 분석하기 전에 생성되도록 보장하며, 여러 주소 공간을 가로질러 한 패키지에서 다른 패키지로 팩트를 전파하는 역할을 담당합니다. 결과적으로, Fact는 직렬화 가능해야 합니다. 이 API는 드라이버가 효율적이고 견고하며 자체 설명적인 바이너리 프로토콜인 gob 인코딩을 사용하도록 요구합니다. 팩트 타입은 기본 인코딩이 부적절한 경우 GobEncoder/GobDecoder 인터페이스를 구현할 수 있습니다. Fact는 상태가 없어야 합니다(stateless). 직렬화된 팩트는 빌드 출력물 내에 나타날 수 있으므로, 콘텐츠 주소 지정 가능 캐시를 사용하는 빌드 시스템에서 스퓨리어스(spurious) 캐시 미스를 방지하기 위해 팩트의 gob 인코딩은 결정적이어야 합니다. 드라이버는 주어진 분석 패스가 내보내는 모든 팩트에 대해 gob 인코더에 단일 호출을 수행하여, 여러 팩트에 의해 참조되는 공유 데이터 구조의 토폴로지를 보존합니다.
Pass 타입은 객체 또는 패키지와 관련된 팩트를 가져오고(import) 내보내는(export) 함수를 가지고 있습니다:
type Pass struct { ... ExportObjectFact func(types.Object, Fact) ImportObjectFact func(types.Object, Fact) bool ExportPackageFact func(fact Fact) ImportPackageFact func(*types.Package, Fact) bool }
Analyzer는 현재 패키지 또는 그 객체와 관련된 팩트만 내보낼 수 있지만, 현재 패키지의 import 의존성인 모든 패키지나 객체로부터 팩트를 가져올 수는 있습니다.
개념적으로, ExportObjectFact(obj, fact)는 (obj, TypeOf(fact)) 쌍의 키로 팩트를 숨겨진 맵에 삽입하고, ImportObjectFact 함수는 이 맵에서 항목을 검색하여 그 값을 fact가 가리키는 변수에 복사합니다. 이 방식은 fact의 구체적인 타입이 포인터라는 것을 가정하며, 이 가정은 Validate 함수에 의해 확인됩니다. 동작하는 객체 팩트의 예시로 "printf" analyzer를 참조하십시오.
일부 드라이버 구현(Bazel 및 Blaze 기반 등)은 현재 표준 라이브러리 패키지에 분석기(analyzer)를 적용하지 않습니다. 따라서 최적의 결과를 얻으려면, 분석기 작성자는 표준 패키지에 대한 분석 사실(analysis facts)이 사용 가능할 것이라고 의존해서는 안 됩니다. 예를 들어, printf 검사기는 log 패키지 분석 중 log.Printf가 printf 래퍼(wrapper)임을 추론할 수 있지만, 이 사실은 분석기에 내장되어 있어 드라이버가 표준 패키지에 적용하지 않을 때에도 log.Printf 호출을 올바르게 확인합니다. 저희는 향후에 이러한 제한 사항을 제거하고 싶습니다.
분석기 테스트하기 ¶
analysistest 하위 패키지는 분석기를 테스트하기 위한 유틸리티를 제공합니다. 몇 줄의 코드로 testdata 파일 패키지에 대해 분석기를 실행하고, 예상되는 모든 진단(diagnostics)과 사실(facts)이 보고되었는지(그리고 그 이상은 없는지) 확인할 수 있습니다. 기대치는 입력 코드 내의 "// want ..." 주석을 사용하여 표현합니다.
독립형 명령어 ¶