배경
이번에 회사에서 새롭게 코드 컨밴션을 정하게 되었고 해당 규칙에 따른 linter를 개발하게 되었다. 백엔드팀에서 사용하는 언어는 golang이었기에 golangci-lint를 통해 진행하였다. 하나 짚고 넘어가자면 golangci-lint는 Linter자체는 아니고 Linter의 모음집이라고 보면 된다. https://golangci-lint.run/usage/linters 해당 docs를 보면 여러 linter들이 있어서 이를 활용할 수 있음을 알 수 있다.
Convention 협의
이번에 협의해야할 내용은 다음과 같았다. 새롭게 개발되는 feature의 이름이 Start, Insert 등 여러 method나 function에서 general하게 사용될 여지가 있었다. 특히나 orm 패키지쪽에서 사용될 수 있는 동사였기에 이를 code-gen을 통해 나오는 것들이 아닌 팀에서 개별적으로 개발하는 method에는 사용할 수 없도록 제한을 걸고자 하였다.
Linter 작성
기존에 개발되었던 linter를 관리하는 linter 패키지가 있었고 해당 패키지 내에 새로운 모듈을 작성하였다. Private Repo이기에 본 블로그에서는 다음과 같은 상황을 가정한다.
package main
import (
"go/ast"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)
var analyzer = &analysis.Analyzer{
Name: "banstartinorm",
Doc: "Checks if orm package with c_ prefix have methods which contains Start",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
}
func run(pass *analysis.Pass) (interface{}, error) {
if !strings.HasPrefix(pass.Pkg.Path(), "base_path/app/orm") {
return nil, nil
}
inspector := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
(*ast.FuncDecl)(nil),
}
inspector.Preorder(nodeFilter, func(n ast.Node) {
funcDecl := n.(*ast.FuncDecl)
if strings.Contains(pass.Fset.Position(funcDecl.Pos()).Filename, "app/orm/c_") {
if strings.HasPrefix(funcDecl.Name.String(), "Start") && funcDecl.Recv != nil {
pass.Reportf(funcDecl.Pos(), "it is not allowed to set method name started with 'Start' in the orm files")
}
}
})
return nil, nil
}
type analyzerPlugin struct{}
func (p *analyzerPlugin) GetAnalyzers() []*analysis.Analyzer {
return []*analysis.Analyzer{
analyzer,
}
}
var AnalyzerPlugin analyzerPlugin
{
"linters": {
"enable": [
"banstartinorm",
]
},
"linters-settings": {
"custom": {
"banstartinorm": {
"path": "bin/linter/banstartinorm.so"
}
},
"staticcheck": {
"checks": [
"all",
"-SA1029"
]
}
},
"run": {
"modules-download-mode": "mod"
}
}
build-customized-linters: bin/linter $(patsubst lint/%,bin/linter/%.so,$(LINTERS)) | .golangci.json
해당 makefile 코드에서는 golangci json 파일에서 linters로 등록해둔 코드들을 binary linter 형태로 빌드해서 저장하게 된다. 여기서 "| .golangci.json" 과 같이 적용하게 되면 해당파일에 업데이트가 있을 때만 동작하기 때문에 효율적으로 관리할 수 있다.
5. Run Linter
이제 linter를 run 해보자. 여기서 우리는 binary linter를 만들어두었기 때문에
@golangci-lint run
다음 명령어를 통해 정적분석을 할 수 있다. 작성해둔 convention에 어긎나는 코드가 있다면 에러를 반환할 것 이다.
6. Tip
golangci-lint의 경우 효율적인 linter 분석을 위해 cache를 사용한다. 다만 이것 때문에 내가 linter 코드를 변경해도 잘 작동하지 않을 때가 있다. 이때는
@golangci-lint cache clean
해당 코드로 캐시를 삭제하고 다시 run 하면 잘 적용될 것이다.
공감하며 읽었습니다. 좋은 글 감사드립니다.