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.