코드 일관성과 가이드라인을 제공할 수 있는 Elixir Credo 라이브러리
Elixir가 아닌 다른 프로그래밍 언어를 사용해 협업하다 보면 그리운 라이브러리 중 하나가 Credo이다. code review에서 정적 분석기가 거를 수 있는 것들을 수정 요청할 때 그립다. 잔소리는 기계가 해야 한다. 개인 프로젝트를 할 때는 코드 일관성이 제대로 안 지켜지고 이랬다저랬다 하는 규칙들을 볼 때 그립다. 이런 건 다 Credo가 잡아주는데 말이지.
다른 정적 분석기 다른 credo의 특징은?
Credo is a static code analysis tool for the Elixir language with a focus on teaching and code consistency.
It can show you refactoring opportunities in your code, complex code fragments, warn you about common mistakes, show inconsistencies in your naming scheme and - if needed - help you enforce a desired coding style.
Credo는 교육 및 코드 일관성에 중점을 둔 Elixir 언어용 정적 코드 분석 도구입니다.
코드의 리팩토링 기회, 복잡한 코드 조각을 보여주고, 일반적인 실수에 대해 경고하고, 명명 체계의 불일치를 보여주며, 필요한 경우 원하는 코딩 스타일을 적용하는 데 도움을 줄 수 있습니다.
Elixir 언어의 정적 분석기라고 하면 Dialyzer가 빠질 수 없다. 정적 분석기라고 하면 연상되는 코드의 모순을 잡는 프로그램이다. 반면 Credo는 코드의 일관성과 교육에 중점을 둔 정적 분석기이다. 예를 들어 Dialyzer는 Pattern matching이 무조건 실패해서 실행될 수 없는 코드를 진단해 준다. 반면 Credo는 length(some_list) == 0
코드는 쓸데없이 비싸니깐 Enum.empty?(some_list)
코드를 사용하라고 추천해준다. 겹치는 영역이 없어서 Dialyzer와 Credo를 둘 다 사용하는 걸 추천한다.
Credo 체크 항목 몇 개를 더 예를 든다면 아래와 같다.
Enum.map/2
리턴 값을 사용하지 않아서Enum.each/2
함수로 교체length(some_list) == 0
은 비싸니깐Enum.empty?(some_list)
함수로 교체- 프로덕션 코드에서
IO.inspect
를 사용하지 말라 - 중복 코드가 50줄이 넘는 코드가 있으니 리팩터링을 하라
설치
의존 라이브러리로 credo
를 추가한다. 글을 쓰는 시점에는 credo 최신 버전은 1.7.7이다.
defp deps do
[
{:credo, "~> 1.7", only: [:dev, :test], runtime: false}
]
end
의존 라이브러리를 라이브러리 매니저로 내려받고 credo
mix task를 실행한다.
$ mix deps.get
$ mix credo
mix credo --all
- 사이드 프로젝트에 돌린 결과
Tbot-800.ex에 추가해서 돌려봤다.
$ mix credo --all
Checking 31 source files ...
Software Design
┃ [D] → Found a TODO tag in a comment: # TODO 한글, 영문에 따라 트윗 캐릭터 계산이 다르다. url도 다름
┃ apps/builder/lib/builder/default_impl/tweet_item_builder.ex:11 #(Builder.DefaultImpl.TweetItemBuilder.build)
...
Code Readability
┃ [R] → Modules should have a @moduledoc tag.
┃ apps/tbot800/lib/tbot800/default_impl/twitter_process.ex:1:11 #(Tbot800.DefaultImpl.TwitterProcess)
┃ apps/tbot800/lib/tbot800.ex:1:11 #(Tbot800)
┃ [R] → Modules should have a @moduledoc tag.
┃ apps/builder/lib/builder/default_impl/html_builder.ex:1:11 #(Builder.DefaultImpl.HtmlBuilder)
...
Please report incorrect results: https://github.com/rrrene/credo/issues
Analysis took 0.08 seconds (0.04s to load, 0.04s running 55 checks on 31 files)
78 mods/funs, found 20 code readability issues, 2 software design suggestions.
Showing priority issues: ↑ ↗ → (use `mix credo explain` to explain issues, `mix credo --help` for options).
우선순위(priority)를 ↑ ↗ →
심볼로 표시하는 게 재미있고 가시성도 좋다. credo 제안(suggestion)에 대해 자세한 설명을 보려면 어떻게 하면 될까?
mix credo explain
- 자세한 설명을 보고 싶을 때
설명을 보려면 mix credo explain
뒤에 위치 포맷 문자열을 인자로 넘기면 된다.
$ mix credo explain apps/builder/lib/builder/default_impl/html_builder.ex:1:11
Builder.DefaultImpl.HtmlBuilder
┃ [R] Category: readability
┃ → Priority: normal
┃
┃ Modules should have a @moduledoc tag.
┃ apps/builder/lib/builder/default_impl/html_builder.ex:1:11 (Builder.DefaultImpl.HtmlBuilder)
┃
┃ __ CODE IN QUESTION
┃
┃ 1 defmodule Builder.DefaultImpl.HtmlBuilder do
┃ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
┃ 2 @template """
┃ 3 <!DOCTYPE html>
┃
┃ __ WHY IT MATTERS
┃
┃ Every module should contain comprehensive documentation.
┃
┃ # preferred
┃
┃ defmodule MyApp.Web.Search do
┃ @moduledoc """
┃ This module provides a public API for all search queries originating
┃ in the web layer.
┃ """
┃ end
┃
┃ # also okay: explicitly say there is no documentation
┃
┃ defmodule MyApp.Web.Search do
┃ @moduledoc false
┃ end
┃
┃ Many times a sentence or two in plain english, explaining why the module
┃ exists, will suffice. Documenting your train of thought this way will help
┃ both your co-workers and your future-self.
┃
┃ Other times you will want to elaborate even further and show some
┃ examples of how the module's functions can and should be used.
┃
┃ In some cases however, you might not want to document things about a module,
┃ e.g. it is part of a private API inside your project. Since Elixir prefers
┃ explicitness over implicit behaviour, you should "tag" these modules with
┃
┃ @moduledoc false
┃
┃ to make it clear that there is no intention in documenting it.
┃
┃ Like all `Readability` issues, this one is not a technical concern.
┃ But you can improve the odds of others reading and liking your code by making
┃ it easier to follow.
┃
┃ __ CONFIGURATION OPTIONS
┃
┃ To configure this check, use this tuple
┃
┃ {Credo.Check.Readability.ModuleDoc, <params>}
┃
┃ with <params> being false or any combination of these keywords:
┃
┃ ignore_names: All modules matching this regex (or list of regexes) will be ignored.
┃ (defaults to [~r/(\.\w+Controller|\.Endpoint|\.\w+Live(\.\w+)?|\.Repo|\.Router|\.\w+Socket|\.\w+View|\.\w+HTML|\.\w+JSON|\.Telemetry|\.Layouts|\.Mailer)$/])
┃
설명이 감동이다.
credo 체크를 억제하는 방법
오탐 혹은 고치기엔 너무 복잡한 코드를 만날 수 있다. 다행히 주석으로 credo 체크를 억제할 수 있다. 사실 정적 분석기에서 필수적으로 제공해야 하는 기능이다.
# credo:disable-for-this-file - to disable for the entire file
# credo:disable-for-next-line - to disable for the next line
# credo:disable-for-previous-line - to disable for the previous line
# credo:disable-for-lines:<count> - to disable for the given number of lines (negative for previous lines)
credo:disable-begin
, credo:disable-end
쌍을 왜 추가하지 않았을까? 이런 생각이 들었다. 쌍으로 항상 동작해야 하니 휴먼 에러가 더 많이 발생한다. 또한 쌍이 맞지 않으면 경고를 해줘야 해서 구현도 번거롭다. 그것보다는 독립적으로 사용해도 동작하는 주석 세트를 제공한다. 좋은 판단이다.
어떻게 credo 실행하고 check 실패를 다뤄야 할까?
credo를 가끔 돌려서 고치는 건 의미가 없다. 말만 그렇게 하고 잊을 수 있다. 가끔이 아니라 한 달에 한 번 혹은 일주일에 한 번처럼 주기를 정하면 그래도 의미가 있다. 난 주기를 정해놓는 것보다 반드시 지켜야 하는 걸 고르고 그걸 지키지 않으면 CI에서 테스트 실패가 나는 걸 선호한다. 외울 게 없고 간단한 규칙으로 돌아가기 때문이다.
실패하면 테스트 스크립트가 멈춰야 한다. 종료 상태(exit status)가 세팅되는지 확인해야 한다.
하나라도 credo 제안이 있으면 종료 상태가 0이 아니다.
$ mix credo
...
215 mods/funs, found 3 refactoring opportunities, 69 code readability issues, 7 software design suggestions.
$ echo $?
14
무조건 종료 상태를 0으로 만드는 --mute-exit-status
옵션도 제공한다. 고쳐야 할 목록은 다 출력하지만 성공 상태로 만들 때 쓸 수 있겠다. 이번에는 쓸 일은 없다.
$ mix credo --mute-exit-status
...
215 mods/funs, found 3 refactoring opportunities, 69 code readability issues, 7 software design suggestions.
$ echo $?
0
이제 반드시 지킬 것을 어떻게 표시할지 결정해야 한다. warning 카테고리가 보여서 반드시 지킬 걸을 warning 카테고리로 모으는 건 어떨지 생각했다. 모은 다음 mix credo suggest --checks warning
과 같이 warning 카테고리만 실행하는 방법이다. 이 방법으로 내가 원하는 체크만 할 수 있지만 Software Design, Code Readability 등과 같은 카테고리를 뭉개야 한다. 정 방법이 없으면 선택해야겠다.
우선순위(priority)가 눈에 들어온다. --min-priority high
옵션을 사용하면 high
우선순위 이상만 실행한다.
$ mix credo suggest --min-priority high
...
$ echo $?
4
내가 원하는 대로 우선순위를 덮어쓸 수 있을까?
# .credo.exs
[
{Credo.Check.Readability.AliasOrder, [priority: :high]},
]
check가 공통으로 지원하는 옵션이다. .credo.exs
파일에서 정의할 수 있다. 좋다. 이걸로 하면 되겠다.
테스트 스크립트에는 mix credo suggest --min-priority high
프로그램 실행을 넣어두고 반드시 지켜야 할 check를 선별해서 우선순위를 high로 올린다.
우선순위를 high로 설정한 check 목록
credo 설정 파일에 기본 check 목록이 모두 있으니 생성한다.
$ mix credo gen.config
* creating .credo.exs
설정 파일 생성 Mix 태스크를 제공하니 정말 편하다. 아래는 1.7.8 디폴트 설정값에서 내가 수정한 check 목록이다. 우선순위만 바꿨다. 디폴트 우선순위가 high인 check 목록에 아래 목록을 더해서 검사한다.
[
# Readability Checks
{Credo.Check.Readability.AliasOrder, [priority: :high]},
{Credo.Check.Readability.MaxLineLength, [priority: :high, max_length: 120]},
{Credo.Check.Readability.ModuleDoc, [priority: :high]},
{Credo.Check.Readability.StringSigils, [priority: :high]},
{Credo.Check.Readability.WithSingleClause, [priority: :high]},
{Credo.Check.Readability.MultiAlias, [priority: :high]},
{Credo.Check.Readability.SeparateAliasRequire, [priority: :high]},
{Credo.Check.Readability.Specs, [priority: :high]},
{Credo.Check.Readability.StrictModuleLayout, [priority: :high]},
{Credo.Check.Readability.WithCustomTaggedTuple, [priority: :high]},
## Refactoring Opportunities
{Credo.Check.Refactor.Apply, [priority: :high]},
{Credo.Check.Refactor.CondStatements, [priority: :high]},
{Credo.Check.Refactor.FilterFilter, [priority: :high]},
{Credo.Check.Refactor.MatchInCondition, [priority: :high]},
{Credo.Check.Refactor.Nesting, [priority: :high]},
{Credo.Check.Refactor.RejectReject, [priority: :high]},
{Credo.Check.Refactor.AppendSingleItem, [priority: :high]},
{Credo.Check.Refactor.MapMap, [priority: :high]},
{Credo.Check.Refactor.PassAsyncInTestCases, [priority: :high]},
# Warnings
{Credo.Check.Warning.RaiseInsideRescue, [priority: :high]},
{Credo.Check.Warning.SpecWithStruct, [priority: :high]},
]
아래는 활성화한 목록에 대한 간단한 설명이다.
- Readability.AliasOrder
- alias 소팅
- Readability.MaxLineLength
- 이건 formatter가 잘 잘라줘서 지키기 쉽다
- Readability.ModuleDoc
- 인간이 아니라 Copilot에 힌트를 주기 위해서라도 잘 써야 한다
- Readability.StringSigils
- Sigil을 잘 쓰고 싶어서 활성화
- Readability.WithSingleClause
- with 남발을 하지 않으려고
- Readability.MultiAlias
- 찾기 힘들어서 alias를 튜플로 줄여 쓰지 않게 함
- Readability.SeparateAliasRequire
- require와 alias 각각 그룹짓자
- Readability.Specs
- public 함수는 typespecs 적자
- Readability.StrictModuleLayout
- 모듈에 정의하는 behaviour, use, import 등의 순서 검사
- Readability.WithCustomTaggedTuple
- Refactor.Apply
- 함수를 바로 쓸 수 있을 때는 apply 쓰지 말자
- Refactor.CondStatements
- if else 문 하나로 대체 가능하면 cond 대신 if 사용
- Refactor.FilterFilter
- 연달아 filter 두 개 쓰지 말고 하나로
- Refactor.RejectReject
- 연달아 reject 두 개 쓰지 말고 하나로
- Refactor.MapMap
- 연달아 map 두 개 쓰지 말고 하나로
- Refactor.MatchInCondition
- 복잡한 if 문을 case 문으로 리팩터링
- Refactor.Nesting
- function body nesting 제한
- Refactor.AppendSingleItem
- list 구현 방법 때문에 아이템 하나를 append 하는 것보다는 prepend 후 reverse가 빠르다
- Refactor.PassAsyncInTestCases
- 테스트에서 async 디폴트가 false인 걸 모를 수 있으니 async를 항상 정의해야 함
- Warning.RaiseInsideRescue
- rescue 문 안에서는 reraise를 써서 stacktrace 보존
- Warning.SpecWithStruct
- struct는 모듈 사이에 컴파일타임 의존성을 만들어내니 typespec에는
%MyModule{}
처럼 바로 쓰지 말고MyModule.t()
처럼 사용
- struct는 모듈 사이에 컴파일타임 의존성을 만들어내니 typespec에는
GitHub workflow에 추가
#!/bin/sh
set -e
echo "==> Running credo..."
mix credo suggest --min-priority high
GitHub Actions workflow에 credo를 돌리는 step을 추가하지 않았다. 대신 테스트 step에 호출하는 script/test 스크립트에 credo 프로그램 실행을 추가했다. 될 수 있으면 CI는 when만 결정해야 한다. 로컬에서도 편하게 호출할 수 있다.
마치며
코드 일관성은 협업할 때는 필수적이다. 다른 사람은 코드를 볼 일이 없는 개인 프로젝트에도 유용하다. 누가 짰건 코드를 읽는 시간이 많으니깐. 그래서 credo는 개인 프로젝트에도 유용하다. 많은 credo check를 살펴보며 몰랐던 지식과 습관도 배우게 된다.
링크
- About workflows - GitHub Docs - docs.github.com
- lucasvegi/Elixir-Code-Smells - github.com
- ohyecloudy/template-elixir/blob/main/script/test - github.com
- rrrene/credo - github.com
- rrrene/credo/tree/v1.7.8 - github.com
- credo - Hex - hex.pm
- Inline Config Comments — Credo v1.7.10 - hexdocs.pm
- Mix — Mix v1.18.0 - hexdocs.pm
- #NDCOslo2014 #review beautiful builds - roy osherove - ohyecloudy’s pnotes - …
- 자동화를 하는 이유가 하나 더 생겼다 - ohyecloudy’s pnotes - ohyecloudy.com
- 10년 전에 Clojure로 짠 트위터 인용봇을 Elixir로 재작성한 후기 - ohyecloudy’s pnotes - ohyecloudy…
- Erlang – dialyzer - erlang.org