👩‍⚕️

SonarQubeでフロントエンドのCognitive Complexityを計測する

2023/10/25に公開

フロントエンドのコードスメルのあたりのつけ方

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