GO Linter 개발하기

Sangdae Nam·2023년 7월 23일
0
post-thumbnail
  1. 배경
    이번에 회사에서 새롭게 코드 컨밴션을 정하게 되었고 해당 규칙에 따른 linter를 개발하게 되었다. 백엔드팀에서 사용하는 언어는 golang이었기에 golangci-lint를 통해 진행하였다. 하나 짚고 넘어가자면 golangci-lint는 Linter자체는 아니고 Linter의 모음집이라고 보면 된다. https://golangci-lint.run/usage/linters 해당 docs를 보면 여러 linter들이 있어서 이를 활용할 수 있음을 알 수 있다.

  2. Convention 협의
    이번에 협의해야할 내용은 다음과 같았다. 새롭게 개발되는 feature의 이름이 Start, Insert 등 여러 method나 function에서 general하게 사용될 여지가 있었다. 특히나 orm 패키지쪽에서 사용될 수 있는 동사였기에 이를 code-gen을 통해 나오는 것들이 아닌 팀에서 개별적으로 개발하는 method에는 사용할 수 없도록 제한을 걸고자 하였다.

  3. Linter 작성
    기존에 개발되었던 linter를 관리하는 linter 패키지가 있었고 해당 패키지 내에 새로운 모듈을 작성하였다. Private Repo이기에 본 블로그에서는 다음과 같은 상황을 가정한다.

  • 제한하려는 규칙: orm 패키지 내 customized된 모듈들에 대해서는 Start로 시작하는 method 작성 금지
  • Customized orm 모듈은 c_ 라는 이름으로 파일명이 시작한다.
  • 해당 linter 모듈 명: banstartinorm
    이제 새로운 linter를 만들어보자.
    linter => banstartinorm => main.go 파일을 다음과 같이 작성한다.
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
  1. golangci.json or yaml 작성하기
    작성한 linter에서 대해서 json이나 yaml 파일을 통해 더 디테일한 linter 적용이 가능하다. 관련 configuration은 https://golangci-lint.run/usage/configuration/ 여기서 참고 가능하다. 이때 staticcheck 항목에 관련해서는 여러 옵션들이 있다. 해당 정보는 https://staticcheck.dev/docs/checks 여기서 확인 가능하다.
{
  "linters": {
    "enable": [
      "banstartinorm",
    ]
  },
  "linters-settings": {
    "custom": {
      "banstartinorm": {
        "path": "bin/linter/banstartinorm.so"
      }
    },
    "staticcheck": {
      "checks": [
        "all",
        "-SA1029"
      ]
    }
  },
  "run": {
    "modules-download-mode": "mod"
  }
}
  1. Makefile 작성 및 binary 코드 추가
    이제 위의 json파일로 관리되는 linter 코드들을 binary linter 형태로 저장해야하는 차례다.
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 하면 잘 적용될 것이다.

profile
AI+Backend

1개의 댓글

comment-user-thumbnail
2023년 7월 23일

공감하며 읽었습니다. 좋은 글 감사드립니다.

답글 달기