SonarQubeでフロントエンドのCognitive Complexityを計測する
フロントエンドのコードスメルのあたりのつけ方
SonarQubeで継続的インスペクション
プログラムの読みやすさを数値化する方法として、循環的複雑度やCognitive Complexity(認知的複雑度)があります。どちらかというとCognitive Complexityの方が人間の感覚にあっているとされています。
TypeScriptでCognitive Complexityを計測する方法としてcognitive-complexity-tsを使う方法などもあるのですが、ts以外の拡張子だと上手く認識しなかったので、SonarQubeを使った方法を紹介します。
SonarQubeは継続的インスペクションのプラットフォームです。コードをGitHubなどにpushした際に自動でテストを実行するフローを継続的インテグレーション(CI: Continuous Integration)と言いますが、同様にコードpush時に静的解析を行うことで脆弱性やコードスメルを計測してフィードバックするフローを継続的インスペクションと呼んでいるようです。
SonarQubeはCode Climateなどと同様にSaaS版のSonarCloudもあるのですが、セルフホストバージョンであるSonarQubeはオープンソースで公開されており、今回試してみます。ライセンスによってサポートされている言語に違いあり。
- Community Edition: Java, C#, JavaScript, TypeScript, CloudFormation, Terraform, Docker, Kubernetes, Kotlin, Ruby, Go, Scala, Flex, Python, PHP, HTML, CSS, XML, VB.NET.
- Developer Edition: Community Editionの言語 + C, C++, Obj-C, Swift, ABAP, T-SQL, PL/SQL.
- Enterprise Edition: Developer Editionの言語 + Apex, COBOL, PL/I, RPG, VB6.
SonarQubeの環境構築
SonarQubeは素のDockerで動かすとH2というJava製のDBを使用するのですが、試験用とされており他のDBの使用が推奨されています。いくつか選択肢があるのですが、今回はpostgresqlを使います。
先人がdocker composeの設定を公開しているのでそれを使用します。後々SQLを実行したいのでDBのポートだけホスト側に晒しておきます。
mkdir sonarqube_postgres
cd sonarqube_postgres
touch docker-compose.yml
docker-compose.ymlを記述します。
# docker-compose.yml
version: "3"
services:
sonarqube:
image: sonarqube:community
hostname: sonarqube
container_name: sonarqube
depends_on:
- db
environment:
SONAR_JDBC_URL: jdbc:postgresql://db:5432/sonar
SONAR_JDBC_USERNAME: sonar
SONAR_JDBC_PASSWORD: sonar
volumes:
- sonarqube_data:/opt/sonarqube/data
- sonarqube_extensions:/opt/sonarqube/extensions
- sonarqube_logs:/opt/sonarqube/logs
ports:
- "9000:9000"
db:
image: postgres:13
hostname: postgresql
container_name: postgresql
environment:
POSTGRES_USER: sonar
POSTGRES_PASSWORD: sonar
POSTGRES_DB: sonar
volumes:
- postgresql:/var/lib/postgresql
- postgresql_data:/var/lib/postgresql/data
ports:
- "5432:5432"
volumes:
sonarqube_data:
sonarqube_extensions:
sonarqube_logs:
postgresql:
postgresql_data:
docker-compose up -d --build
http://localhost:9000/
にアクセスしてログインします。初期アカウントはユーザー名: admin、パスワード: admin。パスワードの変更を要求されるので適当に入れます。
プロジェクトを作成します。今回はdiscourseのコードを使います。
- Project display name: discourse
- Project key: discourse
- Main branch name: main
sonar-scannerで静的解析の結果を送信する
SonarQubeの静的解析はWebにzip等をアップロードすると実行されるというわけではなく、sonar-scannerというcliでコードを解析し、tokenを使ってサーバーのレポート用のAPIに情報を送信、その後WebのUIで結果を確認という流れになっています。
brew install sonar-scanner
Analysis MethodでLocally。その後言語とOSを選びます。
基本的に出力されたコマンドをコピーして解析したいリポジトリの解析したいブランチで実行すればいいのですが、UIの言語フィルター機能が少し弱く、バックエンドとフロントエンドの言語が違う場合ノイズになってしまうので今回はフロントエンドのディレクトリに移動してコマンドを打ちます。
SonarCloudだとmonorepoサポートもある模様。
cd discourse
cd app/assets/javascripts/
sonar-scanner \
-Dsonar.projectKey=discourse \
-Dsonar.sources=. \
-Dsonar.host.url=http://localhost:9000 \
-Dsonar.token=<出力されたトークン>
WebのUIでCognitive Complexityを確認
プロジェクトページに行ってMeasuresタブに移動。サイドバーからComplexity => Cognitive Complexity。View asでListを選ぶと降順で結果が見れます。
RubyのRuboCopだとAbcSize緩めの設定で100くらいにしている会社が多い印象。AbcSizeはCognitive Complexityとはちょっと違うのですが、同じ感覚で言えば100超えてたら流石にやばいといった感じでしょうか。
JSON APIとSQLで結果を出力してみる
この結果をCSVエクスポートしたいところですが、軽く触った感じなさそうですね。1ページ目ならコピペでいけそうですが。
SonarQubeはWeb APIも使えるのでそれを使うと情報は抜けます。
$ curl -s -u admin:<パスワード> -G -d "component=discourse" -d "metricKeys=cognitive_complexity" http://localhost:9000/api/measures/component_tree |jq . |head -n 50
{
"paging": {
"pageIndex": 1,
"pageSize": 100,
"total": 2454
},
"baseComponent": {
"key": "discourse",
"name": "discourse",
"qualifier": "TRK",
"measures": [
{
"metric": "cognitive_complexity",
"value": "12319",
"bestValue": false
}
]
},
"components": [
{
"key": "discourse:discourse/app/components/about-page-users.js",
"name": "about-page-users.js",
"qualifier": "FIL",
"path": "discourse/app/components/about-page-users.js",
"language": "js",
"measures": [
{
"metric": "cognitive_complexity",
"value": "2",
"bestValue": false
}
]
},
{
"key": "discourse:discourse/app/lib/sidebar/common/community-section/about-section-link.js",
"name": "about-section-link.js",
"qualifier": "FIL",
"path": "discourse/app/lib/sidebar/common/community-section/about-section-link.js",
"language": "js",
"measures": [
{
"metric": "cognitive_complexity",
"value": "0",
"bestValue": true
}
]
},
{
"key": "discourse:discourse/tests/acceptance/about-test.js",
"name": "about-test.js",
APIの仕様を見るとページングが必要そうなのとJSONをフラットに変換するのが面倒ですね。
ここら辺は、バージョンアップ後に壊れやすい変更耐性0ゾーンですが、スキーマふんふん読んだらSQLである程度同じものが出力できそうです。
SELECT c.long_name,
lm.value
FROM live_measures lm
JOIN project_branches pb ON lm.project_uuid = pb.uuid
JOIN projects p ON pb.project_uuid = p.uuid
JOIN metrics m ON lm.metric_uuid = m.uuid
JOIN components c ON lm.component_uuid = c.uuid
WHERE p.kee = 'discourse'
AND m.name = 'cognitive_complexity'
AND c.scope = 'FIL'
ORDER BY lm.value DESC ;
結果
long_name | value |
---|---|
discourse/app/lib/autocomplete.js | 366 |
discourse/app/services/composer.js | 310 |
select-kit/addon/components/select-kit.js | 212 |
discourse/app/widgets/post-menu.js | 190 |
admin/addon/models/report.js | 154 |
pretty-text/engines/discourse-markdown/watched-words.js | 149 |
discourse/app/lib/to-markdown.js | 148 |
discourse/app/widgets/search-menu-results.js | 146 |
discourse/app/widgets/post.js | 135 |
最後に
以上です。
他のリポジトリでも試したところTypeScript/JavaScript関係なくReactやVueのファイルでも認識する模様。一つのツールでカバー範囲が広いのは嬉しいですね。
SonarQubeはCognitive Complexity計測以外にも使える便利ツールなのでもっと流行るのを期待したいです。
Discussion