algonote(en)

There's More Than One Way To Do It

Measuring Cognitive Complexity of the frontend with SonarQube

How to find the frontend code smells

Continuous Inspection with SonarQube

There are some ways to quantify the readability of a program. For example, Cyclomatic Complexity and Cognitive Complexity are used. Cognitive Complexity is considered to be more similar to human senses.

There are some methods to measure Cognitive Complexity in TypeScript, such as using cognitive-complexity-ts. However, it was not recognized well with extensions other than ts, so I will introduce a method using SonarQube.

SonarQube is a continuous inspection platform. The flow of automatically executing tests when code is pushed to GitHub, etc. is called Continuous Integration (CI), and similarly, the flow of measuring vulnerabilities and code smells by performing static analysis when code is pushed is called Continuous Inspection.

SonarQube, like Code Climate, has a SaaS version, SonarCloud. Self-hosted version, SonarQube, is open source, so I will try it out this time. There are differences in supported languages depending on the license.

  • 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 Languages + C, C++, Obj-C, Swift, ABAP, T-SQL, PL/SQL.
  • Enterprise Edition: Developer Edition Languages + Apex, COBOL, PL/I, RPG, VB6.

Building the SonarQube Environment

SonarQube uses a Java-made DB called H2 when run on plain Docker, but it is recommended to use other DBs since it is considered for testing purposes. There are several options, but this time we will use postgresql.

Our predecessor has published a docker compose configuration. I add the expose of the DB port because I want to execute SQL later from host side.

mkdir sonarqube_postgres
cd sonarqube_postgres
touch docker-compose.yml

Write 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

Go to http://localhost:9000/ and log in. Initial account username is admin and a password is admin. You will be asked to change your password, so enter it appropriately.

Create a project. I use the code from discourse.

  • Project display name: discourse
  • Project key: discourse
  • Main branch name: main

Sending static analysis results with sonar-scanner

The static analysis of SonarQube is not executed by uploading a zip file to the web. Instead, we need to use a cli called sonar-scanner to analyze the code. It sends the information to the server's reporting API using a token, and then we check the results in the web UI.

brew install sonar-scanner

Select "Locally" in "Analysis Method", then select your language and OS.

Basically, you can copy the output command and execute it in the branch of the repository you want to analyze, but the language filtering function of the UI is a little weak. So if the language of the backend and the frontend are different, you will get noise. In this case, you need to move to the front-end directory and type the command.

SonarCloud seems to have monorepo support.

cd discourse
cd app/assets/javascripts/

sonar-scanner \
  -Dsonar.projectKey=discourse \
  -Dsonar.sources=. \
  -Dsonar.host.url=http://localhost:9000 \
  -Dsonar.token=<gained token>

See Cognitive Complexity in the Web UI

Go to the project page and move to the Measures tab. In the sidebar, select Complexity => Cognitive Complexity. In View as, select List to see the results in descending order.

I have the impression that many Ruby RuboCop companies set the AbcSize loosely to 100 or so. AbcSize is a little different from Cognitive Complexity, but in the same sense, if it exceeds 100, it would be a bad status.

Output results via JSON API and SQL.

I'd like to export the results to CSV, but it doesn't look like there's any way to do that, although the first page could be copied and pasted.

SonarQube also has a Web API, so I can use that to get the information out.

$ curl -s  -u admin:<password> -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",

Looking at the API specifications, it looks like paging is required and converting JSON to flat is a pain.

This area is a zero tolerance zone for changes that are likely to break after version upgrades, but if you read the schema, you should be able to output some of the same things in 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 ;

Result

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

Finally

That's all.

I have tried other repositories and it seems to recognize React and Vue files as well, regardless of TypeScript/JavaScript. It is nice to have a wide range of coverage in one tool.

SonarQube is a useful tool that can be used for more than just Cognitive Complexity measurement, so I hope it will become more popular.