Шукаємо помилки за допомогою Google Error Prone
Всім привіт. Я Сергій Моренець, розробник, тренер, викладач, спікер та технічний письменник. Хочу поділитися своїм досвідом роботи з технологією Google Error Prone. У статті опишу її можливості та розповім, як ми використовували їх у своїх проєктах. Сподіваюся, ця стаття буде корисною для всіх, хто хоче більше дізнатися про те, як зменшити кількість помилок у своїх проєктах, підвищити якість коду, зробити його більш компактним та сучасним.
Що таке Error Prone
Коли я тільки-но почав писати свій цикл «Розробка Java-застосунків», присвятив один з розділів вивченню підвищення якості коду за допомогою засобів статичного аналізу. Як приклади для використання я обрав технології FindBugs та SonarQube. Десять років тому це були дуже популярні проєкти. SonarQube таким і залишився, тоді як FindBugs припинив свій розвиток у 2017 році. Але, на щастя, на його основі було створено новий продукт — SpotBugs, який зараз активно підтримується. Також дуже популярним інструментом для аналізу коду є Checkstyle.
Але в цій статті я хочу поговорити про інший продукт — Google Error Prone. Інтерес до нього очевидний — він створений Google 10 років тому та є досить стабільною технологією. Тому з ним варто познайомитися всім, хто піклується про підвищення надійності своїх проєктів.
Отже, Error Prone — це засіб аналізу коду під час його збирання. Важливий момент: ця бібліотека створена не для того, щоб впроваджувати best practices, принципи гарного дизайну або шукати код, який цим принципам не відповідає, а лише для пошуку помилок. Error Prone підтримує принцип Continuous Integration, перевіряючи код під час збирання. Таким чином, якщо у вас будуть знайдені помилки, збирання просто не пройде успішно. Які ж помилки може знайти Error Prone? Є спеціальний розділ у документації, де вказані патерни, за якими вони шукаються. Усього їх 702, як випливає з вихідного коду. Особливо сильно автори цієї бібліотеки пишаються тим, що завдяки їхньому витвору було знайдено баг у JDK 10.
Error Prone вимагає JDK 11+ і може інтегруватися з будь-якою сучасною системою збирання (Bazel, Ant, Maven, Gradle). Є плагін під Intellij IDEA, але, на жаль, немає для Eclipse.
Технічні особливості
Як працює Error Prone? Тут є базовий клас BugChecker:
public abstract class BugChecker implements Suppressible, Serializable {
І всі компоненти, які перевіряють ваш код, повинні його успадкувати. Наприклад, цей компонент перевіряє, що ваш JavaDoc дійсно оформлений як JavaDoc:
@BugPattern(
summary =
"This comment contains Javadoc or HTML tags, but isn't started with a double asterisk"
+ " (/**); is it meant to be Javadoc?",
severity = WARNING,
tags = STYLE,
documentSuppression = false)
public final class AlmostJavadoc extends BugChecker implements CompilationUnitTreeMatcher {
Усі компоненти можна поділити на три великі групи:
- Видають помилку
- Видають попередження
- Вимкнені за замовчуванням та не використовуються
Кожен компонент використовує JDK Compiler API для роботи та навігації за вихідним кодом. Таким чином, ви можете теоретично написати свій bug checker для вашого конкретного випадку. Вийде у нас чи ні, поговоримо трохи пізніше, а в їхній репозиторії навіть є посібник для написання своїх компонентів. Але писати свої bug checker не так просто, крім того, їх потрібно підтримувати. Наприклад, є такий компонент UnusedVariable, який перевіряє, що змінна оголошена та використовується. Він довго працював без будь-яких нарікань, поки не вийшла Java 22 з безіменними змінними (unnamed variables). Тепер потрібно перевіряти поточну версію і чи є змінна безіменна.
Впроваджуємо Error Prone
Тепер спробуємо використати нову технологію у нашому проєкті. Додамо його спочатку до Maven-конфігурації:
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<parameters>true</parameters>
<source>${java.version}</source>
<target>${java.version}</target>
<release>22</release>
<compilerArgs>
<arg>-XDcompilePolicy=simple</arg>
<arg>-Xplugin:ErrorProne</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>2.30.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
Запускаємо складання всього проєкту і відразу отримуємо помилку:
Caused by: org.apache.maven.plugin.compiler.CompilationFailureException: Compilation failure
An unknown compilation problem occurred
Ця проблема відома і пов’язана з тим, що в JDK 16 посилилися правила інкапсуляції. Для розв’язання цієї проблеми потрібно в корені проєкту створити теку .mvn, а в ній файл jvm.config наступного змісту:
--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
Цей файл доступний, починаючи з Maven 3.3.1, і дозволяє вказувати JVM-аргументи для кожного окремого проєкту (а не для всіх одразу, як зі змінною оточення MAVEN_OPTS). Знову запускаємо складання та отримуємо дивну помилку:
constructor RandomNumberGenerator in class app.infra.util.generator.RandomNumberGenerator cannot be applied to given types;
[ERROR] required: no arguments
[ERROR] found: int
[ERROR] reason: actual and formal argument lists differ in length
Вихідний код цього класу показує, що тут справді є конструктор, який чомусь більше не виявляється при компіляції:
@RequiredArgsConstructor
public final class RandomNumberGenerator implements NumberGenerator {
private final int limit;
@Override
public int generate() {
return (int) (Math.random() * limit);
}
}
Швидше за все, ця проблема пов’язана з Lombok, і для того, щоб її розвʼязати, додамо Lombok як annotation processor:
<annotationProcessorPaths>
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>2.30.0</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
Розбираємо помилки
Знову запускаємо збирання, і тепер у консолі з’являються десятки попереджень від Error Prone, які не переривають збирання, доки не трапляється перша помилка:
CustomFailsafeExecutorTest.java:[78,67] [ImpossibleNullComparison] This value cannot be null, and comparing it to null may be misleading.
[ERROR] (see https://errorprone.info/bugpattern/ImpossibleNullComparison)
[ERROR] Did you mean 'return Optional.of(Long.parseLong(value));'?
Відкриваємо CustomFailsafeExecutorTest і натикаємось на такий блок коду:
if (clz == Long.class) {
return Optional.ofNullable(Long.parseLong(value));
} else if (clz == Double.class) {
return Optional.ofNullable(Double.parseDouble(value));
} else if (clz == Boolean.class) {
return Optional.ofNullable(Boolean.parseBoolean(value));
} else if (clz == Integer.class) {
return Optional.ofNullable(Integer.parseInt(value));
}
І справді, Long.parseLong, Integer.parseInt і всі схожі методи повертають примітивні типи, які null бути не можуть. Тому використовувати Optional.ofNullable немає сенсу, тільки Optional.of:
if (clz == Long.class) {
return Optional.of(Long.parseLong(value));
} else if (clz == Double.class) {
return Optional.of(Double.parseDouble(value));
} else if (clz == Boolean.class) {
return Optional.of(Boolean.parseBoolean(value));
} else if (clz == Integer.class) {
return Optional.of(Integer.parseInt(value));
}
Продовжуємо збирання, і виникає нова помилка:
EagerExtension.java:[31,128] [ReturnValueIgnored] Return value of 'toString' must be used
(see https://errorprone.info/bugpattern/ReturnValueIgnored)
Did you mean 'var unused = beanManager.getReference(bean, bean.getBeanClass(), beanManager.createCreationalContext(bean)).toString();' or to remove this line?
Відкриваємо підозрілий код:
public void load(@Observes AfterDeploymentValidation event, BeanManager beanManager) {
for (Bean<?> bean : startupBeans) {
beanManager.getReference(bean, bean.getBeanClass(), beanManager.createCreationalContext(bean)).toString();
}
}
Тут ми викликаємо метод toString для примусової ініціалізації бина, тому що значення, яке повертається, нас не цікавить. Але це вважається помилкою в Error Prone. Що тут можна зробити? По-перше, якщо ви переклали великий проєкт на Error Prone, то у вас відразу з’являться десятки та сотні помилок. Ви ж не будете все виправляти? Тут може допомогти Java-анотація @SuppresWarnings, куди потрібно передати назву bug pattern:
@SuppressWarnings("ReturnValueIgnored")
public void load(@Observes AfterDeploymentValidation event, BeanManager beanManager) {
В нашому варіанті можемо виправити код, щоб Error Prone не лаявся. Бібліотека радить наступну заміну:
var unused = beanManager.getReference(bean, bean.getBeanClass(), beanManager.createCreationalContext(bean)).toString();
Але ж у нас є безіменні змінні. Перевіримо, чи підтримує їх Error Prone:
var _ = beanManager.getReference(bean, bean.getBeanClass(), beanManager.createCreationalContext(bean)).toString();
Тепер код не викликає помилку. Перезбираємо проєкт. Більше помилок немає, але Error Prone видає величезну кількість попереджень. Розглянемо деякі з них.
[NotJavadoc] Avoid using `/**` for comments which aren't actually Javadoc.
(see https://errorprone.info/bugpattern/NotJavadoc)
Did you mean '/*'?
Ось анотація, на яку свариться Error Prone:
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
/**
* Should be placed on CDI beans to eagerly loads beans on startup
* @author Morenets
*
*/
public @interface Eager {
}
На перший погляд все виглядає коректно, але тільки такий варіант бібліотека вважає правильним, коли JavaDocs йде перед анотаціями:
/**
* Should be placed on CDI beans to eagerly loads beans on startup
* @author Morenets
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
public @interface Eager {
}
Також багато подібних попереджень:
[EmptyBlockTag] A block tag (@param, @return, @throws, @deprecated) has an empty description. Block tags without descriptions don't add much value for future readers of the code; consider removing the tag entirely or adding a description.
(see https://google.github.io/styleguide/javaguide.html#s7.1.3-javadoc-block-tags)
Did you mean '*'?
Тут в Error Prone претензії до того, що в JavaDoc не вказано описи аргументів:
/**
* Registers new metric in the registry
*
* @param name
* @param metric
* @return created metric instance
*/
public Metric registerMetric(final String name, final Metric metric) {
return metricRegistry.register(name, metric);
}
Це розумне зауваження, але для того, щоб додати описи до всіх класів, знадобиться неймовірно багато часу. А ці десятки попереджень лише засмічують логи. Чи можна вимкнути конкретний bug checker? Так, для цього є аргумент -Xep, який ми додамо до списку аргументів компілятора, додавши назву bug checker (EmptyBlockTag):
<compilerArgs>
<arg>-XDcompilePolicy=simple</arg>
<arg>-Xplugin:ErrorProne -Xep:EmptyBlockTag:OFF</arg>
</compilerArgs>
Це лише частина знайдених проблем у наших проєктах, а про решту я розповім у другій частині.
Висновки
За попередніми підсумками використання можна сказати, що інтеграція з Error Prone не спричиняє труднощів. Завдяки великій (>700) базі bug patterns ця бібліотека дозволяє знайти помилки, які важко визначити під час code review або запуску тестів. Конфігурація дозволяє змінити рівень критичності будь-якого bug pattern (error, warning) чи вимкнути його. Якщо ви не хочете вручну виправляти помилки, можна запустити збірку в режимі patching і буде згенерований файл error-prone.patch з усіма змінами. Про те, що ця бібліотека допомагає виправляти помилки, говорить і той факт, що її творець (Google) збільшує винагороду за знайдені помилки у своїх продуктах. Якщо сума збільшується, отже, помилок стало менше та їх важче знайти.
У другій частині цієї статті я продовжу розповідати про те, які помилки були знайдені, та покажу, як ми написали свій bug pattern (checker).
1 коментар
Додати коментар Підписатись на коментаріВідписатись від коментарів