Привет всем! Недавно я решил поэкспериментировать с SonarQube и создать свой собственный кастомный плагин для проверки кода на соответствие моим правилам разработки. В этой статье я поделюсь с вами своим опытом и покажу, как вы тоже можете создать такой плагин своими руками.
Небольшие вводные
Disclaimer: Данная статья повторяет многое из официального гайда.
Давайте начнем с выдуманного use-case’a:
Нам нужен инструмент для проверки кода во время MR, который сможет определять не соответствия установленному код-стайлу команды.
Тут на самом деле у нас есть 2 варианта.
Использование готовых Lint-инструментов
Примером таких инструментов могут служить ESLint (для JavaScript/TypeScript) или Checkstyle (для Java). Этот вариант, на мой взгляд, является самым простым и удобным. В случаях, когда требуется кастомная проверка, процесс также несложен: разработайте свой чек и добавьте его в директорию plugins или extensions.
Плюсы:
-
Простота и удобство использования.
-
Легкость добавления кастомных проверок.
Минусы:
-
Сложно разделять правила проверки кода по разным командам с различными стандартами разработки. В таких случаях правила одной команды могут конфликтовать с правилами другой.
Использование SonarQube с кастомным плагином
Этот вариант привлекает меня тем, что все проверки, связанные с код-стайлингом и безопасностью кода, находятся в одном месте и объединены в красивом интерфейсе. SonarQube отлично подходит для компаний с большим количеством команд разработки, так как позволяет создавать кастомные профили.
Плюсы:
-
Все проверки кода в одном месте.
-
Удобный и красивый интерфейс.
-
Подходит для крупных компаний с множеством команд разработки благодаря возможности создания кастомных профилей.
Минусы:
-
Требования к инфраструктуре: SonarQube требует отдельного сервера и ресурсов для своей работы, что может быть проблематично для небольших команд или проектов.
Подготавливаем локальную среду
Давайте начнем разработку нашего первого кастомного чека спулив проект с гитхаба: ссылка.
Я немного почистил и изменил проект, чтобы облегчить процесс разработки и избавить вас от необходимости заниматься конфигурацией Maven.
По рекомендации разработчиков SonarQube, мы должны придерживаться подхода TDD (разработка через тестирование) при написании наших кастомных чеков. Лично я не являюсь большим поклонником этого подхода, но в данном случае он показывает себя очень эффективно.
Чтобы наш кастомный чек правильно работал после интеграции в SonarQube, его необходимо протестировать. Для этого мы будем использовать инструмент CheckVerifier. С его помощью можно прогонять наши проверки на реальных кейсах, что, на мой взгляд, гораздо удобнее, чем написание моков в Mockito.
Давайте напишем тесты для чека, который будет проверять, написаны ли URL эндпойнтов в kebab-case.
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.DeleteMapping; public class MyController { @GetMapping("/kebab-case-url") // Compliant public String getCompliant() { return "compliant"; } @GetMapping("/camelCaseUrl") // Noncompliant public String getNonCompliant() { return "noncompliant"; } @PostMapping("/another-kebab-case-url") // Compliant public void postCompliant() { } @PostMapping("/AnotherCamelCaseUrl") // Noncompliant public void postNonCompliant() { } @PutMapping("/yet-another-kebab-case-url") // Compliant public void putCompliant() { } @PutMapping("/YetAnotherCamelCaseUrl") // Noncompliant public void putNonCompliant() { } @DeleteMapping("/final-kebab-case-url") // Compliant public void deleteCompliant() { } @DeleteMapping("/FinalCamelCaseUrl") // Noncompliant public void deleteNonCompliant() { } }
Важные аспекты тест-кейсов
-
Расположение тестов:
-
Все ваши тесты должны находиться в следующей директории:
src/test/files/
.
-
-
Сценарии тестирования:
-
Обязательно расписывайте как негативные, так и позитивные сценарии.
-
Негативные сценарии помечайте как
// Noncompliant
, а позитивные как// Compliant
.
-
-
Импорты:
-
Не забывайте добавлять необходимые импорты. Без них тесты не пройдут успешно.
-
Теперь давайте напишем непосредственно саму проверку на kebab-case.
@Rule(key = "KebabCaseUrlCheck") public class KebabCaseUrlCheck extends BaseTreeVisitor implements JavaFileScanner { private static final Pattern KEBAB_CASE_PATTERN = Pattern.compile("^(/[a-z0-9]+(-[a-z0-9]+)*)*$"); private JavaFileScannerContext context; @Override public void scanFile(JavaFileScannerContext context) { this.context = context; scan(context.getTree()); } @Override public void visitMethod(MethodTree tree) { List<AnnotationTree> annotations = tree.modifiers().annotations(); for (AnnotationTree annotationTree : annotations) { TypeTree annotationType = annotationTree.annotationType(); if (annotationType.is(Tree.Kind.IDENTIFIER)) { IdentifierTree identifier = (IdentifierTree) annotationType; String annotationName = identifier.name(); if (annotationName.equals("GetMapping") || annotationName.equals("PostMapping") || annotationName.equals("PutMapping") || annotationName.equals("DeleteMapping")) { checkUrl(annotationTree); } } } super.visitMethod(tree); } private void checkUrl(AnnotationTree annotationTree) { if (annotationTree.arguments().isEmpty()) { return; } Tree argument = annotationTree.arguments().get(0); if (argument.is(Tree.Kind.ASSIGNMENT)) { argument = ((org.sonar.plugins.java.api.tree.AssignmentExpressionTree) argument).expression(); } if (argument.is(Tree.Kind.STRING_LITERAL)) { LiteralTree literal = (LiteralTree) argument; String url = literal.value().substring(1, literal.value().length() - 1); if (!KEBAB_CASE_PATTERN.matcher(url).matches()) { context.reportIssue(this, literal, "The URL should be in kebab-case."); } } } }
Начнем с аннотации @Rule(key = «KebabCaseUrlCheck»), которая определяет ключ правила, используемого в SonarQube для идентификации этого чека. Класс KebabCaseUrlCheck наследуется от BaseTreeVisitor и реализует интерфейс JavaFileScanner, что позволяет ему сканировать Java-файлы.
Шаблон для проверки соответствия URL формату kebab-case определяется с помощью регулярного выражения:
private static final Pattern KEBAB_CASE_PATTERN = Pattern.compile("^(/[a-z0-9]+(-[a-z0-9]+))$");.
Также создается контекст сканирования, который используется для отчета об ошибках:
private JavaFileScannerContext context;
Основной метод для сканирования файла scanFile сохраняет контекст и начинает процесс сканирования дерева синтаксиса файла:
@Override public void scanFile(JavaFileScannerContext context);
Сканирование начинается вызовом
scan(context.getTree());
Метод visitMethod переопределяется для обработки каждого метода в файле:
public void visitMethod(MethodTree tree);
Он получает все аннотации метода:
List<AnnotationTree> annotations = tree.modifiers().annotations();
В цикле for каждая аннотация проверяется, и если она является одной из @GetMapping
, @PostMapping
, @PutMapping
или @DeleteMapping
, вызывается метод checkUrl.
Метод checkUrl
проверяет URL в аннотации. Он начинается с проверки наличия аргументов у аннотации. Если аргументов нет, метод завершается. Затем извлекается первый аргумент аннотации и проверяется, является ли он строковым литералом. Если это так, значение строки извлекается без кавычек:
String url = literal.value().substring(1, literal.value().length() - 1);
Далее, с помощью регулярного выражения проверяется, соответствует ли URL шаблону kebab-case: if (!KEBAB_CASE_PATTERN.matcher(url).matches())
. Если URL не соответствует шаблону, создается отчет об ошибке с сообщением «The URL should be in kebab-case.»
Запускаем наш первый тест
После того как мы настроили тест-кейсы и сам класс проверки, нам нужно непосредственно данный тест запустить.
Для этого в директории src/test/java/org/sonar/samples/java/checks
создаем новый класс KebabCaseUrlTest
и пишим простую конструкцию для теста:
package org.sonar.samples.java.checks; import org.junit.jupiter.api.Test; import org.sonar.java.checks.verifier.CheckVerifier; public class KebabCaseUrlCheckTest { @Test void test() { CheckVerifier.newVerifier() .onFile("src/test/files/KebabCaseUrlCheck.java") .withCheck(new KebabCaseUrlCheck()) .verifyIssues(); } }
Далее, запускаем тест и радуемся, что наш метод проверки url работает!
Финальные штрихи
Начнем с конфигурации нейминга нашего плагина. Переходим к этому файлу:
src/main/java/org/sonar/samples/java/MyJavaRulesDefinition.java
и меняем поля REPOSITORY_KEY и REPOSITORY_NAME на нужные вам.
public static final String REPOSITORY_KEY = "sonar-plugin-habr"; public static final String REPOSITORY_NAME = "Habr Guide Plugin";
Далее, Перед тем, как компилить наш .jar файл, нам нужно зарегистрировать наш кастомный плагин. Для этого переходим к этому файлу: src/main/java/org/sonar/samples/java/RulesList.java
и в метод getJavaChecks
добавляем наш KebabCaseUrlCheck.
public static List<Class<? extends JavaCheck>> getJavaChecks() { return Collections.unmodifiableList(Arrays.asList( SpringControllerRequestMappingEntityRule.class, AvoidAnnotationRule.class, AvoidBrandInMethodNamesRule.class, AvoidMethodDeclarationRule.class, AvoidSuperClassRule.class, AvoidTreeListRule.class, MyCustomSubscriptionRule.class, KebabCaseUrlCheck.class, SecurityAnnotationMandatoryRule.class)); }
Далее, нам нужно добавить наш файл с проверкой в тест, проверяющий регистрацию правил. По умолчанию, он использует метод containsExactly()
что как по мне делает реализацию проверок более комплексной и не нужной, для этого, по желанию вы можете поменять метод на containsExactlyInAnyOrder()
что в итоге не будет выкидать ошибку, если вы будете регистрировать ваши правила не по порядку.
class MyJavaFileCheckRegistrarTest { @Test void checkRegisteredRulesKeysAndClasses() { TestCheckRegistrarContext context = new TestCheckRegistrarContext(); MyJavaFileCheckRegistrar registrar = new MyJavaFileCheckRegistrar(); registrar.register(context); assertThat(context.mainRuleKeys).extracting(RuleKey::toString).containsExactlyInAnyOrder( "omni-sonar:SpringControllerRequestMappingEntity", "omni-sonar:AvoidAnnotation", "omni-sonar:AvoidBrandInMethodNames", "omni-sonar:AvoidMethodDeclaration", "omni-sonar:AvoidSuperClass", "omni-sonar:AvoidTreeList", "omni-sonar:AvoidMethodWithSameTypeInArgument", "omni-sonar:KebabCaseUrlCheck", "omni-sonar:SecurityAnnotationMandatory"); assertThat(context.mainCheckClasses).extracting(Class::getSimpleName).containsExactlyInAnyOrder( "SpringControllerRequestMappingEntityRule", "AvoidAnnotationRule", "AvoidBrandInMethodNamesRule", "AvoidMethodDeclarationRule", "AvoidSuperClassRule", "AvoidTreeListRule", "MyCustomSubscriptionRule", "KebabCaseUrlCheck", "SecurityAnnotationMandatoryRule"); assertThat(context.testRuleKeys).extracting(RuleKey::toString).containsExactly( "omni-sonar:NoIfStatementInTests"); assertThat(context.testCheckClasses).extracting(Class::getSimpleName).containsExactly( "NoIfStatementInTestsRule"); } }
Как последний пункт, нам нужно добавить документацию к нашему кастомному плагину. Это делается довольно легко, мы создаем 2 файла(html и json) в директории src/main/resources/org/sonar/l10n/java/rules/java
с идентичным названием нашей проверки. В нашем случае, файлы будут называться: KebabCaseUrlCheck.html и KebabCaseUrlCheck.json.
В HTML файле, нам нужно описать нашу проверку и как она работает, код помещайте в тэг <pre>.
<h1>Kebab-Case URL Check</h1> <p>This rule ensures that the URLs in <code>@GetMapping</code>, <code>@PostMapping</code>, <code>@PutMapping</code>, and <code>@DeleteMapping</code> annotations are in kebab-case format.</p> <h2>Noncompliant Code Example</h2> <pre> import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.DeleteMapping; public class MyController { @GetMapping("/camelCaseUrl") // Noncompliant public String getNonCompliant() { return "noncompliant"; } @PostMapping("/AnotherCamelCaseUrl") // Noncompliant public void postNonCompliant() { } @PutMapping("/YetAnotherCamelCaseUrl") // Noncompliant public void putNonCompliant() { } @DeleteMapping("/FinalCamelCaseUrl") // Noncompliant public void deleteNonCompliant() { } @GetMapping("/nested/camelCaseUrl") // Noncompliant public String getNestedNonCompliant() { return "noncompliant"; } } </pre> <h2>Compliant Solution</h2> <pre> import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.DeleteMapping; public class MyController { @GetMapping("/kebab-case-url") // Compliant public String getCompliant() { return "compliant"; } @PostMapping("/another-kebab-case-url") // Compliant public void postCompliant() { } @PutMapping("/yet-another-kebab-case-url") // Compliant public void putCompliant() { } @DeleteMapping("/final-kebab-case-url") // Compliant public void deleteCompliant() { } @GetMapping("/nested/kebab-case-url") // Compliant public String getNestedCompliant() { return "compliant"; } } </pre>
В Json вам нужно предоставить метаданные вашей проверки:
{ "title": "URLs in @GetMapping, @PostMapping, @PutMapping, @DeleteMapping should be in kebab-case", "type": "code_smell", "status": "ready", "tags": [ "convention", "style", "rest" ], "defaultSeverity": "Minor" }
Тут интуитивно все понятно, с type можете ознакомиться по ссылке.
Как последний штрих, давайте запустим mvn clean install и получим .jar файл в директории /target
Интеграция в SonarQube
Для начала, давайте мы sonar развернем локально и добавим наш .jar файл.
Для этого используйте данный Dockerfile:
# Use the official SonarQube image as the base image FROM sonarqube:latest # Set the SonarQube home directory as an environment variable ENV SONAR_HOME=/opt/sonarqube # Copy the custom rules JAR file to the SonarQube plugins directory COPY target/java-custom-rules-example-1.0.0-SNAPSHOT.jar $SONAR_HOME/extensions/plugins/
и запустите его следующей командой:
docker build . -t sonar-habr docker run -d -p 9000:9000 sonar-habr
Активируем Плагин
-
Заходите в Quality Profiles и делаете фильтр на язык Java.
-
Зайдите в профиль SonarWay и скопируйте его назовите habr.
-
В профиле habr нажмите activate more и во вкладке Repository вы увидите наш плагин «Habr Guide Plugin».
-
Найдите нашу проверку и активируйте ее, она будет называться URLs in @GetMapping, @PostMapping, @PutMapping, @DeleteMapping should be in kebab-case.
Весь наш код можете найти по данной ссылке.
ссылка на оригинал статьи https://habr.com/ru/articles/823590/
Добавить комментарий