В своей работе мы активно используем платформу SonarQube для поддержания качества кода на высоком уровне. При интеграции одного из проектов, написанном на VueJs+Typescript, возникли проблемы. Поэтому хотел бы рассказать подробней о том, как удалось их решить.
В данной статье речь пойдет, как писал выше, о платформе SonarQube. Немного теории — что это такое вообще, для тех, кто слышит о ней впервые:
SonarQube (бывший Sonar) — платформа с открытым исходным кодом для непрерывного анализа (англ. continuous inspection) и измерения качества кода.
Поддерживает анализ кода и поиск ошибок согласно правилам стандартов программирования MISRA C, MISRA C++, MITRE/CWE и CERT Secure Coding Standards. А также умеет распознавать ошибки из списков OWASP Топ-10 и CWE/SANS Топ-25 ошибок программирования.
Несмотря на то, что платформа использует различные готовые инструменты, SonarQube сводит результаты к единой информационной панели (англ. dashboard), ведя историю прогонов и позволяя тем самым увидеть общую тенденцию изменения качества программного обеспечения в ходе разработки.
Более подробно можно узнать на
Поддерживается большое количество языков программирования. Судя по информации из ссылки выше — это более 25 языков. Для поддержки конкретного языка необходимо установить соответствующий плагин. В community-версию входит плагин для работы с Javascript (в том числе typesсript), хотя в wiki написано обратное. За Javascript отвечает плагин SonarJS, за Typescript SonarTS соответственно.
Для отправки информации о покрытии используется официальный клиент sonarqube-scanner, который, используя настройки из config-файла, отправляет эти данные на сервер SonarQube для дальнейшей консолидации и агрегирования.
Для Javascript есть
Для развертывания сервера SonarQube воспользуемся docker-compose.
sonar.yaml:
version: '1'
services:
simplesample-sonar:
image: sonarqube:lts
ports:
- 9001:9000
- 9092:9092
network_mode: bridge
Запуск:
docker-compose -f sonar.yml up
После этого SonarQube будет доступен по адресу –
Пока в нем нет проектов и это справедливо. Будем исправлять данную ситуацию. За основу я взял официальный проект-пример для VueJS+TS+Jest. Склонируем его к себе:
git clone https://github.com/vuejs/vue-test-utils-typescript-example.git
Сначала нам нужно установить клиент SonarQube, который называется sonar-scanner, для npm есть обертка:
yarn add sonarqube-scanner
И сразу же добавим команду в scripts для работы с ним.
package.json:
{
…
scripts: {
...
"sonar": "sonar-scanner"
...
},
…
}
Далее, для работы сканера, нужно задать настройки проекта в специальном файле. Начнем с базовых.
sonar-project.properties:
sonar.host.url=http://localhost:9001
sonar.projectKey=test-project-vuejs-ts
sonar.projectName=Test Application (VueJS+TS)
sonar.sources=src
# sonar.tests=
sonar.test.inclusions=src/**/*tests*/**
sonar.sourceEncoding=UTF-8
- sonar.host.url – адрес Sonar’а;
- sonar.projectKey – уникальный идентификатор проекта на сервере Sonar’а;
- sonar.projectName – его наименование, оно может быть изменено в любой момент, так как идентификация проекта производится по projectKey;
- sonar.sources – папка с исходниками, обычно это src, но может быть любым. Эта папка задается относительно рутовой папки, которой является папка откуда запущен сканер;
- sonar.tests – параметр, который идет в паре с предыдущим. Это папка, где находятся тесты. В данном проекте, нет такой папки, а тест находится рядом с тестируемым компонентом в папке ‘test‘, поэтому мы его пока проигнорируем и воспользуемся следующим параметром;
- sonar.test.inclusions – путь для тестов с использованием маски, может быть несколько элементов перечисленных через запятую;
- sonar.sourceEncoding – кодировка для исходных файлов.
Для первого запуска сканера все готово, кроме основного предшествующего действия: запуск самого тестового движка, для формирования им информации о покрытии, которую и будет в последствии использовать сканер.
Но для этого нужно настроить тестовый движок на формирование данной информации. В данном проекте тестовый движок — это Jest. И его настройки находятся в соответствующем разделе файла package.json.
Добавим эти настройки:
"collectCoverage": true,
"collectCoverageFrom": [
"src/**/*",
"!src/main.ts",
"!src/App.vue",
"!src/**/*.d.*",
"!src/**/*__tests__*"
],
То есть задаем сам флаг необходимости вычисления покрытия и источник (вместе с исключениями), на основе которых оно будет формироваться.
Теперь запустим тест:
yarn test
Увидим следующее:
Причина в том, что в самом компоненте, как такового, кода нет. Исправим это.
HelloWorld.vue:
...
methods: {
calc(n) {
return n + 1;
}
},
mounted() {
this.msg1 = this.msg + this.calc(1);
},
...
Этого будет достаточно для расчета покрытия.
После перезапуска теста убедимся в этом:
На экране мы должны увидеть информацию о покрытии, а в папке проекта будет создана папка coverage с информацией о покрытии тестами в универсальном формате LCOV (LTP GCOV extension).
Gcov — свободно распространяемая утилита для исследования покрытия кода. Gcov генерирует точное количество исполнений для каждого оператора в программе и позволяет добавить аннотации к исходному коду. Gcov поставляется как стандартная утилита в составе пакета GCC.
Lcov — графический интерфейс для gcov. Он собирает файлы gcov для нескольких файлов с исходниками и создает комплект HTML страниц с кодом и сведениями о покрытии. Также генерируются страницы для упрощения навигации. Lcov поддерживает покрытие строк, функций, ветвлений.
После выполнения тестов информация о покрытии будет находится в coverage/lcov.info.
Нам надо сказать Sonar’у откуда ее взять. Поэтому добавим следующие строчки в его файл конфигурации. Но есть один момент: проекты могут быть мультиязычные, то есть в папке src находятся исходники для нескольких языков программирования и принадлежность к тому или иному, и в свою очередь использование того или иного плагина, определяется по его расширению. И информация о покрытии может хранится в разных местах для разных языков программирования, поэтому для каждого ЯП есть свой раздел для настройки этого. У нас проект использует Typescript, поэтому нам необходим раздел настроек именно для него:
sonar-project.properties:
sonar.typescript.coveragePlugin=lcov
sonar.typescript.lcov.reportPaths=coverage/lcov.info
Все готово к первому запуску сканера. Хочу заметить, что проект в Sonar’е создается автоматически при первом запуске сканера для данного проекта. В последующие разы информация уже будет аккумулироваться, чтобы видеть динамику изменения параметров проекта во времени.
Итак, воспользуемся командой, созданной ранее в package.json:
yarn run sonar
Примечание: можно также воспользоваться параметром -X для более детального логирования.
Если запуск сканера был впервые, то сначала скачается бинарник самого сканера. После этого он запускается и начинает сканировать сервер Sonar’а на предмет установленных плагинов, вычисляя тем самым поддерживаемые ЯП. Также загружаются другие различные параметры для его работы: quality profiles, active rules, metrics repository, server rules.
Примечание: подробно на них мы останавливаться не будем в рамках данной статьи, но всегда можно обратиться в официальные источники.
Далее начинается анализ папки src на предмет наличия исходных файлов для всех (если не задан явно какой-то конкретный) поддерживаемых ЯП, с последующей их индексацией.
Далее идут другие различные анализы, на которых мы не заостряем внимание в данной статье (например, такие как: линтинг, определение дублирования кода и тд).
В самом конце работы сканера происходит агрегирование всей собранной информации, архивирование и отправка ее на сервер.
После этого мы можем уже посмотреть что получилось в вэб-интерфейсе:
Как видим, что-то получилось, и даже показывает какое-то покрытие, но оно не соответствует нашему Jest-отчету.
Давайте разбираться. Посмотрим на проект более детально, кликнем по значению покрытия, и "провалимся" в детализированный отчет по файлам:
Здесь мы видим помимо основного, исследуемого файла HelloWorld.vue, присутствует и файл main.ts, который и портит всю картину покрытия. Но как же так, мы его исключали из расчета покрытия. Да, все правильно, но это было на уровне Jest, но сканер его проиндексировал, поэтому он попал в его расчеты.
Давайте исправим это:
sonar-project.properties:
...
sonar.exclusions=src/main.ts
...
Хочется сделать уточнение: помимо тех папок, которые заданы в данном параметре, также добавляются все папки, перечисленные в параметре sonar.test.inclusions.
После запуска сканера видим уже корректную информацию:
Разберем следующий момент – Quality profiles. Я говорил выше о поддержке Sonar’ом несколько ЯП одновременно. Вот это как раз мы и наблюдаем. Но мы знаем, что проект у нас написан на TS, поэтому зачем напрягать сканер лишними манипуляциями и проверками. Язык для анализа зададим через добавление еще одного параметра в файл конфигурации Sonar’а:
sonar-project.properties:
...
sonar.language=ts
...
Снова запустим сканер и посмотрим результат:
Покрытие пропало вовсе.
Если посмотрим в лог сканера, то можем увидеть следующую строчку:
То есть файлы нашего проекта просто не были проиндексированы.
Ситуация следующая: официально поддержка VueJs есть в плагине SonarJS, который отвечает за Javascript.
Но этой поддержки нет в плагине SonarTS для TS, о чем заведен официальный тикет в баг-трекере Sonar’а:
Вот некоторые ответы одного из представителей со стороны разработчиков SonarQube, подтверждающий этот факт.
Но у нас же все работало, возразите Вы. Да, так и есть, давайте попробуем немного “похакерить”.
Если есть поддержка .vue-файлов Sonar’ом, то давайте попробуем сказать ему чтобы он их рассматривал как Typescript.
Добавим параметр:
sonar-project.properties:
...
sonar.typescript.file.suffixes=.ts,.tsx,.vue
...
Запустим сканер:
И, вуаля, все вернулось на круги своя, и с одним профилем только для Typescript. То есть удалось решить проблему в поддержке VueJs+TS для SonarQube.
Попробуем пойти дальше и немного улучшим информацию о покрытии.
Что же мы сделали на данный момент:
- добавили в проект Sonar-сканер;
- настроили Jest для формирования информации о покрытии;
- сконфигурировали Sonar-сканер;
- решили проблему поддержки .vue-файлов + Typescript.
Кроме покрытия тестами есть другие интересные полезные критерии качества кода, например, дублирование кода и количество строк (участвует в расчете коэффициентов, связанных со сложностью кода) проекта.
В текущей реализации плагина для работы с TS (SonarTS) не будет работать CPD (Copy Paste Detector) и подсчет строк кода .vue-файлов.
Для создания синтетической ситуации по дублированию кода, просто задублируем файл компонента с другим именем, также добавим в код main.ts функцию-пустышку и задублируем его с другим именем. Чтобы проверить дублирование как в .vue, так и в .ts -файлах.
main.ts:
...
function name(params:string): void {
console.log(params);
}
...
Для этого необходимо временно закоментировать строчку конфигурации:
sonar-project.properties:
...
sonar.exclusions=src/main.ts
...
Перезапустим сканер вместе с тестированием:
yarn test && yarn run sonar
У нас конечно упадет покрытие, но сейчас нам это не интересно.
В разрезе дублирования строк кода увидим:
Для проверки воспользуемся CPD-утилитой – jscpd:
npx jscpd src
Для строк кода:
Возможно, это решится в будущих версиях плагинов SonarJS(TS). Хочу заметить, что они постепенно начинают сливать эти два плагина в один SonarJS, что, думаю, правильно.
Теперь хотелось рассмотреть вариант улучшения информации о покрытии.
Пока мы видим покрытие тестами в процентном отношении, по всему проекту, и по файлам в частности. Но есть возможность расширить этот показатель информацией о количестве unit-тестов по проекту, а также в разрезе файлов.
Есть библиотека, которая умеет Jest-репорт конвертировать в формат для Sonar’а:
generic test data —
Установим эту библиотеку к себе в проект:
yarn add jest-sonar-reporter
И добавим его в конфигурацию Jest:
package.json:
…
"testResultsProcessor": "jest-sonar-reporter"
…
Теперь выполним тест:
yarn test
После чего в корне проекта будет создан файл test-report.xml.
Задействуем его в конфигурации Sonar’а:
sonar-project.properties:
…
sonar.testExecutionReportPaths=test-report.xml
…
И перезапустим сканер:
yarn run sonar
Посмотрим, что поменялось в интерфейсе Sonar’а:
И ничего не поменялось. Дело в том, что Sonar не рассматривает файлы, описанные в Jest-репорте, как файлы unit-тестов. Для того, чтобы исправить эту ситуацию задействуем параметр конфигурации Sonar sonar.tests, в котором явно укажем папки с тестами (она у нас пока одна):
sonar-project.properties:
…
sonar.tests=src/components/__tests__
…
Перезапустим сканер:
yarn run sonar
Посмотрим, что поменялось в интерфейсе:
Теперь мы увидели количество наших unit-тестов и, провалившись по клику внутрь, можем посмотреть распределение этого числа по файлам проекта:
Заключение
Итак, мы рассмотрели инструмент для непрерывного анализа SonarQube. Успешно интегрировали в него проект, написанный на VueJs+TS. Решили некоторые проблемы совместимости. Повысили информативность показателя о покрытии тестами. В данной статье мы рассмотрели лишь один из критериев качества кода (возможно, один из основных), но SonarQube поддерживает и другие критерии качества, включая тестирование на безопасность. Но не все эти возможности в полном объеме доступны в community-версии. Одна из интересных и полезных возможностей — это интеграции SonarQube с различными системами управления репозиториями кода, например, такие как GitLab и BitBucket. Чтобы не допустить merge pull(merge) request’а в основную ветку репозитория при деградации покрытия. Но это история уже совершенно другой статьи.
PS: Все, что описано в статье в виде кода доступно в
Только зарегистрированные пользователи могут участвовать в опросе.
Используете ли Вы платформу SonarQube:
-
26,3%Да5
-
15,8%Нет3
-
15,8%Слышал о данной платформе и хочу использовать3
-
10,5%Слышал о данной платформе и не хочу использовать2
-
0,0%Использую другую платформу0
-
31,6%Впервые слышу о ней6
Проголосовали 19 пользователей. Воздержались 3 пользователя.
Источник: habr.com