Вебинар: Стратегия без иллюзий: как превращать цели в результаты - 19.08
Java уже какое-то время витает в облаках. Всё больше приложений и сервисов переходят на облачную архитектуру. Мы решили не отставать и проверить один из главных проектов для разработки производительных облачных приложений на Java — Quarkus.

Мы регулярно проверяем Open Source проекты с помощью нашего анализатора PVS-Studio и публикуем статьи о найденных ошибках. Такие статьи позволяют читателям ближе познакомиться с концепцией статического анализа, запомнить, какие ошибки случаются чаще всего, а также разобраться в нюансах и тонкостях при разработке на определённом языке.
Особенно интересно смотреть на ошибки в крупных проектах, ведь именно они постоянно развиваются: у них огромная кодовая база, а при их разработке применяются разные подходы и технологии. Это значит, что и сами ошибки могут быть совершенно разными.
В этот раз выбор упал на популярный проект Quarkus. Это современный фреймворк, ориентированный на разработку высокопроизводительных облачных приложений на Java с использованием технологий, ставших стандартами в корпоративной разработке (микросервисы, DI-контейнеры и так далее).
Quarkus делает упор на производительности готовых приложений за счёт смещения большого количества операций на время сборки: чтение конфигураций, сканирование classpath, построение контекста приложения. Также приложения на Quarkus удобно компилируются в нативный образ.
Из этого понятно, что перед нами проект непростой и большой. Поэтому мы решили посмотреть, какие ошибки в нём удастся найти. За основу для проверки взяли вот этот коммит. Не будем больше задерживаться, давайте переходить к делу.
Вы подумали, что я буду рассказывать лекции о воспитании? Я всего лишь напомню, какие подводные камни бывают в иерархии наследования Java. Предлагаю посмотреть на такой код.
Фрагмент 1
public class InterceptorInfo extends BeanInfo
implements Comparable<InterceptorInfo> {
private final Set<AnnotationInstance> bindings;
....
@Override
public String toString() {
return "INTERCEPTOR bean [bindings=" + bindings +
", target=" + getTarget() + "]";
}
}
Обратите внимание, что в методе toString используется поле bindings класса InterceptorInfo. А теперь посмотрим на конструктор родительского класса BeanInfo:
BeanInfo(....) {
....
this.identifier = Hashes.sha1_base64(
(identifier != null ? identifier : "") +
toString() + beanDeployment.toString()
);
....
}
И чтобы окончательно сложить весь паззл в одну картину, вот конструктор класса InterceptorInfo:
InterceptorInfo(. . . ., Set<AnnotationInstance> bindings) {
super(. . . .);
this.bindings = bindings;
....
}
Итак, давайте разбираться, что здесь происходит. При инициализации класса InterceptorInfo сначала вызывается конструктор родительского класса BeanInfo. Тот, в свою очередь, вызывает метод toString, который переопределён в классе InterceptorInfo. Метод toString использует не инициализированное поле bindings. В результате полученная строка отображает не до конца сформированное состояние объекта.
Подробнее об этой проблеме и как с ней бороться мы писали в отдельной статье.
Предупреждение PVS-Studio:
V6052 Calling overridden 'toString' method in 'BeanInfo' parent-class constructor may lead to use of uninitialized data. Inspect field: bindings. InterceptorInfo.java 265
Забытые, потерянные или перепутанные фрагменты кода частенько появляются в том или ином проекте, и эта ошибка не обошла стороной Quarkus.
Фрагмент 2
public void build(Path projectDir) {
....
try {
ModelUtils.persistModel(projectDir.resolve("pom.xml"), model);
} catch (IOException e) {
throw new IllegalStateException(); // <=
}
}
Этот метод класса отвечает за построение модели проекта Maven и его зависимостей. В показанном фрагменте видно, что при сохранении файла модели может возникнуть исключение, информацию о котором перетирают исключением типа IllegalStateException. Это не самая лучшая практика, так как в случае возникновения проблем, исключение, брошенное из метода build, не будет содержать информации о реальной ошибке. Никто не узнает, какие скелеты спрятались в вашем шкафу.
Предупреждение PVS-Studio:
V6118 The original exception object 'IOException' was swallowed. Cause of original exception could be lost. MvnProjectBuilder.java 122
Фрагмент 3
Foo.Bar bar1 = new Bar(new ArrayList<>());
event1.fire(bar1);
assertEquals(1, bar1.getNames().size()); // <=
assertEquals("bazinga", bar1.getNames().get(0));
Foo.Bar bar2 = new Bar(new ArrayList<>());
event2.fire(bar2);
assertEquals(1, bar1.getNames().size()); // <=
assertEquals("bazinga", bar2.getNames().get(0));
Это фрагмент из тестов, проверяющих механизм событий. Здесь создают и тестируют два экземпляра Foo.Bar. Нетрудно заметить, что второй блок тестового кода — копипаста первого. В продублированной строке assertEquals(1, bar1.getNames().size()) забыли заменить bar1 на bar2, из-за чего проверяют состояние не того объекта.
Предупреждение PVS-Studio:
V6072 Two similar code fragments were found. Perhaps, this is a typo and 'bar2' variable should be used instead of 'bar1'. MockEventTest.java 40
Кстати, мы добавили точно такую же диагностику в наш новый анализатор для JavaScript и TypeScript.
Фрагмент 4
public class PathTreeBuilder {
private List<String> includes;
private List<String> excludes;
....
List<String> getIncludes() {
return includes;
}
List<String> getExcludes() {
return includes; // <=
}
}
Как можно догадаться из названия класса, PathTreeBuilder создаёт экземпляр PathTree, который нужен для удобной работы с зависимостями проекта в виде дерева. Строитель имеет методы заполнения полей includes и excludes. Однако в методе getExcludes по ошибке возвращают поле includes.
Предупреждение PVS-Studio:
V6091 Suspicious getter implementation. The 'excludes' field should probably be returned instead. PathTreeBuilder.java 58
Фрагмент 5
private static boolean isIgnored(DotName classDotName) {
String className = classDotName.toString();
if (className.startsWith("java.util.")
|| className.startsWith("java.lang.")
|| className.startsWith("org.hibernate.engine.spi.")
|| className.startsWith("jakarta.persistence.")
|| className.startsWith("jakarta.persistence.") // <=
) {
return true;
}
return false;
}
Этот метод используется в классе JpaJandexScavenger для фильтрации классов, необходимых в работе интегрированного Hibernate ORM. Сам же класс заранее определяет, какие для этого типы нужны. Как уже было сказано во введении, Quarkus старается перенести часть логики на момент сборки и JpaJandexScavenger реализует именно эту концепцию.
В теле метода можно увидеть повторение пакета jakarta.persistence. в логическом условии:
className.startsWith("jakarta.persistence.") ||
className.startsWith("jakarta.persistence.")
Учитывая назначение этого метода, не исключено, что здесь забыли добавить ещё какое-то ограничение. На функционал это может и не повлиять, однако размер итогового приложения может вырасти за счёт покрытия большего числа классов метаданными. А может, всё гораздо проще, и это просто лишняя проверка, оставленная по ошибке.
Предупреждение PVS-Studio:
V6001 There are identical sub-expressions 'className.startsWith("jakarta.persistence.")' to the left and to the right of the '||' operator. JpaJandexScavenger.java 621
Работа с числами — ещё одно пространство для ошибок. Давайте разбираться.
Фрагмент 6
@ConsumeEvent("address-4")
CompletionStage<Long> listenAddress4(int i) {
return CompletableFuture.completedFuture((long) (i + 1));
}
Если переменная i будет равна Integer.MAX_VALUE, то при добавлении к ней единицы произойдёт переполнение, и результат этой операции станет равен Integer.MIN_VALUE. Защититься от такой ошибки можно путём приведения одного из операндов к long. Хотя в этом примере приведение есть, его применяют к результату операции, так что ошибки избежать не удастся.
Предупреждение PVS-Studio:
V6117 Possible overflow. The expression will be evaluated before casting. Consider casting one of the operands instead. CodecRegistrationTest.java 195
Фрагмент 7
@Override
public void apply(SocketSettings.Builder builder) {
if (config.connectTimeout().isPresent()) {
builder.connectTimeout((int) config.connectTimeout().get() // <=
.toMillis(), TimeUnit.MILLISECONDS);
}
if (config.readTimeout().isPresent()) {
builder.readTimeout((int) config.readTimeout().get() // <=
.toMillis(), TimeUnit.MILLISECONDS);
}
}
Ещё один пример приведения, который может привести к переполнению. Его цель не ясна, тем более что и connectTimeout, и readTimeout у переменной builder принимают long.
Анализатор PVS-Studio выдаёт следующие предупреждения в этих местах:
V6106 Casting expression to int type before implicitly casting it to other type may be excessive or incorrect. MongoClients.java 289
V6106 Casting expression to int type before implicitly casting it to other type may be excessive or incorrect. MongoClients.java 286
Фрагмент 8
static <T> List<T> takeLast(List<T> list, int n) {
if (n < 1 || n > list.size()) {
throw new IndexOutOfBoundsException(n);
}
if (list.isEmpty()) { // <=
return list;
}
return list.subList(list.size() - n, list.size());
}
Метод берёт последние n элементов из list. Подразумевается, что нельзя взять меньше одного элемента или больше, чем в этом списке есть. Но проверка говорит ещё и о том, что сам список быть пустым не может. Если мы берём 1 элемент из списка, это значит, что он должен иметь как минимум один элемент. Поэтому следующая проверка list.isEmpty() всегда ложная. Если автор кода хотел обработать случай взаимодействия с пустым списком, то тут этого сделать не выйдет, и текущую проверку стоит пересмотреть.
Предупреждение PVS-Studio:
V6007 Expression 'list.isEmpty()' is always false. CollectionTemplateExtensions.java 57
Фрагмент 9
if (errorRate != -1.0) {
list.add("ERROR");
list.add(new BigDecimal(errorRate).toPlainString()); // Prevent E notation
}
С этой ошибкой предлагаю ознакомиться подробнее. Если мы передадим в конструктор BigDecimal число 0.1, то будет сохранено 0.1000000000000000055511151231257827021181583404541015625. Наличие такого хвоста — особенность представления вещественных чисел в компьютере. Для тех, кто интересуется, почему так происходит, оставлю здесь ссылку на теоретическую информацию.
Ну а нас теперь разберёмся, как можно создать объект без "сюрприза" в виде хвоста. Для этого в стандартной библиотеке есть метод BigDecimal#valueOf, внутри которого реализована хитрая логика:
public static BigDecimal valueOf(double val) {
....
var fmt = FormattedFPDecimal.valueForDoubleToString(Math.abs(val));
long s = fmt.getSignificand();
....
}
Если говорить общими словами, то происходит следующее: метод получает двоичное представление вещественного числа и переводит его в десятичное представление так, чтобы в итоге мы получили корректно округлённое, кратчайшее по записи десятичное число. В случае с вышерассмотренным числом таким строковым значением как раз будет 0.1. Поэтому метод BigDecimal#valueOf сохранит наше точное значение.
Впервые этот метод появился в Java 5 и выглядел совсем иначе:
public static BigDecimal valueOf(double val) {
return new BigDecimal(Double.toString(val));
}
И хотя вместо десятичного представления здесь использовалась строка, глобально задача решалась точно так же.
К слову, метод BigDecimal#valueOf до сих пор в своей Javadoc ссылается на Double#toString(), хотя сейчас под капотом использует другие методы. Но суть алгоритма осталась прежней, и она хорошо описана в Javadoc к методу Double#toString().
Ну а что было до Java 5? Все просто: конструктор BigDecimal(String) существовал всегда и рекомендовался к использованию вместо BigDecimal(double).
Подобные тонкости имеют особое значение в системах точных вычислений, где даже небольшие погрешности могут привести к неожиданным последствиям.
Предупреждение PVS-Studio:
V6068 Constructor call can result in imprecise representation of the initialized value. BfInsertArgs.java 95
Если после празднования Нового Года вы оказались в прошлом или будущем, не пугайтесь — это баг.
Фрагмент 10
public class WebSocketNextJsonRPCService implements ConnectionListener {
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("YYYY-MM-dd HH:mm:ss");
....
}
На первый взгляд здесь вообще никакой проблемы нет, но давайте посмотрим внимательно на строку формата даты: YYYY-MM-dd. И снова выглядит всё нормально. Или нет?
Если текущая дата, к примеру, 1.1.2027, то с таким форматом отображаться она будет как 1.1.2026.
Всё из-за того, что такой формат записи с YYYY отображает год на основе номера недели в нём. И для вычисления года есть даже хитрая схема: если большая часть недели принадлежит предыдущему году, то отображаться будет именно он. В нашем примере 1 января — пятница, а значит, большая часть недели принадлежит 2026 году.
Обратная ситуация работает, если меньшая часть недели принадлежит уходящему году. Так, 31.12.2024 по этому формату будет отображаться как 31.12.2025, потому что большая часть недели принадлежит 2025 году. Незаметно улетел целый год...
Это настолько необычная ошибка, что мы посвятили ей отдельную статью.
А исправить её можно заменой YYYY на yyyy, о чём так же предупреждает PVS-Studio:
V6122 Usage of 'Y' (week year) pattern was detected: it was probably intended to use 'y' (year). WebSocketNextJsonRPCService.java 39
Что за теория такая и как она связана с разработкой, мы писали в отдельной статье, а сейчас мы посмотрим наглядно на то, как она себя проявляет.
Фрагмент 11
@Override
public <T> T getClaim(String claimName) {
if (claimName.equals(Claims.groups)) {
return (T) getGroups();
} else if (claimName.equals(Claims.groups)) { // <=
return (T) getAudience();
} else if (claimName.equals(Claims.exp)) {
return (T) Long.valueOf(getExpirationTime());
} else if (claimName.equals(Claims.iat)) {
return (T) Long.valueOf(getIssuedAtTime());
} else if (claimName.equals(Claims.aud)) {
return (T) getAudience();
}
return (T) claims.getClaim(claimName);
}
Предупреждение PVS-Studio:
V6003 The use of 'if (A) {...} else if (A) {...}' pattern was detected. There is a probability of logical error presence. CognitoPrincipal.java 38
Что мы тут видим: во втором условии цепочки if-else допустили опечатку и ещё раз проверяют на равенство с Claims.groups, при том, что возвращают результат вызова метода getAudience. Можно сказать, что ничего страшного, ведь правильное условие описано последним в цепочке if-else, а вторая проверка claimName.equals(Claims.groups) уже не будет истинной, ведь она не прошла в первом условии. Но здесь важно не это. Это срабатывание из пакета amazon-lambda-rest. Теперь предлагаю посмотреть на другое срабатывание рядом, в пакете amazon-lambda-http:
@Override
public <T> T getClaim(String claimName) {
if (claimName.equals(Claims.groups)) {
return (T) getGroups();
} else if (claimName.equals(Claims.groups)) { // <=
return (T) getAudience();
} else if (claimName.equals(Claims.exp)) {
return (T) Long.valueOf(getExpirationTime());
} else if (claimName.equals(Claims.iat)) {
return (T) Long.valueOf(getIssuedAtTime());
} else if (claimName.equals(Claims.aud)) {
return (T) getAudience();
}
return (T) getClaims().getClaims().get(claimName);
}
И снова точно такой же код и точно такая же ошибка. Разница лишь в последней строке. Вот так ошибочный код стал расползаться по проекту через обычный copy-paste.
Срабатывание PVS-Studio во втором пакете: V6003 The use of 'if (A) {...} else if (A) {...}' pattern was detected. There is a probability of logical error presence. CognitoPrincipal.java 41
Фрагмент 12
@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
VertxWebRouterBuildItem initializeRouter(....) {
....
List<RouteBuildItem> redirectRoutes = new ArrayList<>();
....
if (frameworkRouterCreated) {
if (redirectRoutes.size() > 0) { // <=
recorder.setNonApplicationRedirectHandler(
nonApplicationRootPath.getNonApplicationRootPath(),
nonApplicationRootPath.getNormalizedHttpRootPath()
);
redirectRoutes.forEach(route -> recorder.addRoute(
httpRouteRouter,
route.getRouteFunction(),
recorder.getNonApplicationRedirectHandler(),
route.getType()
)
);
}
}
return new VertxWebRouterBuildItem(httpRouteRouter, mainRouter,
frameworkRouter, managementRouter,
mutinyRouter);
}
Предупреждение PVS-Studio:
V6007 Expression 'redirectRoutes.size() > 0' is always false. VertxHttpProcessor.java 392
Тут мы видим, что часть логики никогда не выполняется, потому что коллекция redirectRoutes ничем не заполняется в рамках метода и никуда не передаётся. Мы решили разобраться и посмотреть историю проекта на этом файле. В итоге удалось найти коммит 2021 года с интересными удалёнными строками:
if (httpBuildTimeConfig.redirectToNonApplicationRootPath &&
route.isRequiresLegacyRedirect()
) {
redirectRoutes.add(route);
}
А причём тут разбитые окна? На самом деле всё просто: лишний код, оставленный в одном месте, может привести к появлению такого же кода в других частях проекта. Например, в забытом фрагменте используется метод, который не удаляют, потому что он всё ещё нужен именно там. В итоге забытый код цепляет уже два класса, и эту цепочку можно продолжать долго. Как результат — раздутая кодовая база и трудности в восприятия всего кода.
Вот такой код подсветил анализатор.
Фрагмент 13
static final boolean IS_CYGWIN = OS.WINDOWS.isCurrent()
&& System.getenv("PWD") != null
&& System.getenv("PWD").startsWith("/");
Здесь обращаются к переменной окружения PWD. Обычно так обозначают рабочую директорию процесса. С ней есть свои заморочки: она может быть или не заданной, или переопределённой извне. Если хочется стабильных результатов, то безопаснее использовать системное свойство JVM user.dir, поскольку оно гарантированно выставляется при старте приложения и извне не меняется.
Предупреждения PVS-Studio на этот код:
V6110 Using the 'PWD' environment variable could be unsafe or unreliable. Consider using trusted system property 'user.dir' instead. TerminalUtils.java 27
V6110 Using the 'PWD' environment variable could be unsafe or unreliable. Consider using trusted system property 'user.dir' instead. TerminalUtils.java 28
Кроме того, в проекте есть ещё несколько подобных срабатываний, но уже с другими значениями, которые также рекомендуется получать через вызов System.getProperty:
V6110 Using the 'HOME' environment variable could be unsafe or unreliable. Consider using trusted system property 'user.home' instead. Constants.java 11
V6110 Using the 'USER' environment variable could be unsafe or unreliable. Consider using trusted system property 'user.name' instead. AnalyticsService.java 231
С многопоточным кодом нужно быть особенно внимательным, иначе беды не избежать.
Фрагмент 14
public class KubernetesDevUIProcessor {
static volatile List<Manifest> manifests;
public List<Manifest> getManifests() throws BootstrapException {
if (manifests == null) {
synchronized (Holder.class) {
if (manifests == null) {
manifests = new ArrayList<>();
....
try (CuratedApplication bootstrap = quarkusBootstrap.bootstrap()) {
....
for (var entry : context.entrySet()) {
manifests.add(
new Manifest(entry.getKey(),
new String(entry.getValue()))
);
}
}
}
}
}
return manifests;
}
}
Это пример неправильной реализации паттерна double-checked locking (если интересно, как работает и зачем нужен этот паттерн, можно почитать нашу статью) для поля manifests.
Когда поток заходит в блок синхронизации, он инициализирует поле manifests пустым списком, а потом переходит к процессу его заполнения. То есть изначально предполагалось, что поле станет доступным после его создания и заполнения значениями. Однако публикация происходит уже после инициализации manifests, и в результате другой поток может получить созданный, но ещё не заполненный список.
Исправить такую ошибку довольно просто: достаточно лишь инициализировать поле готовым списком из локальной переменной.
Предупреждение PVS-Studio:
V6082 Unsafe double-checked locking. Object was assigned to the field before it was initialized. KubernetesDevUIProcessor.java 66
Фрагмент 15
public class VertxUdpMetrics implements DatagramSocketMetrics {
private volatile Tags tags;
@Override
public void listening(String localName, SocketAddress localAddress) {
tags = tags.and("address", NetworkMetrics.toString(localAddress)); // <=
}
}
Поле tags отметили ключевым словом volatile. Вспомним, что это значит в Java: если один поток изменяет переменную, другой поток моментально видит это изменение. Однако это работает безошибочно для атомарных операций. Если нам нужно выполнить логику из нескольких действий, тут уже нужны совершенно другие подходы, чтобы избежать проблему состояния гонки.
Чтобы понять, что здесь происходит, представим, что метод listening одновременно используют два потока. Оба зашли в метод, оба создали новый экземпляр через метод and и оба попытались сохранить новое значение в переменную tags.
Эти три операции не представляют собой одну атомарную, так что ключевое слово volatile тут не поможет и придётся использовать другие подходы (например, делать метод синхронизированным).
Предупреждение PVS-Studio:
V6074 Non-atomic modification of volatile variable. Inspect 'tags'. VertxUdpMetrics.java 37
Порой из-за невнимательности рождается ошибочный или лишний код, как произошло здесь.
Фрагмент 16
С этим фрагментом предлагаю разобраться поэтапно.
public class TemplateHtmlBuilder {
private static final String HEADER_TEMPLATE_NO_STACK = "<h1>%1$s</h1>\n" +
"%2$s \n" +
"<div class=\"container content\">\n";
private static final String HTML_TEMPLATE_START_NO_STACK = "" +
"<!doctype html>\n" +
"<html lang=\"en\">\n" +
"<head>\n" +
" <title>%1$s%2$s</title>\n" +
" <meta charset=\"utf-8\">\n" +
"</head>";
....
}
Здесь мы видим два шаблона HTML, записанных в поля. Если мы посмотрим на них внимательно, то заметим, что и в первом, и во втором шаблоне есть маркеры подстановки под две строковые переменные.
А теперь посмотрим на то, как они используются:
public TemplateHtmlBuilder(....,
String title,
String details
) {
....
result = new StringBuilder(String.format(HTML_TEMPLATE_START_NO_STACK,
escapeHtml(title),
subTitle == null || subTitle.isEmpty() ? "" : " - " +
escapeHtml(subTitle), CSS));
result.append(String.format(HEADER_TEMPLATE_NO_STACK,
escapeHtml(title),
escapeHtml(details),
actionLinks.toString()
));
}
В использовании полей HEADER_TEMPLATE_NO_STACK и HTML_TEMPLATE_START_NO_STACK шаблон заполняется тремя аргументами в обоих случаях, хотя в самих строках мест всего два. В этом же методе есть точно такие же строки, которые используют те же самые аргументы, но с шаблонами для трёх аргументов. Так что здесь мы видим пример обычного copy-paste, когда забыли убрать лишние аргументы.
Предупреждение PVS-Studio:
V6046 Incorrect format. A different number of format items is expected. Arguments not used: 3. TemplateHtmlBuilder.java 325
V6046 Incorrect format. A different number of format items is expected. Arguments not used: 3. TemplateHtmlBuilder.java 328
Фрагмент 17
public void boot(...., Optional<FunctionInitializedBuildItem> hasFunctions) {
if (!hasFunctions.isPresent() || hasFunctions.get() == null) // <=
return;
}
В метод приходит параметр hasFunctions типа Optional, с которым выполняют очень странную проверку:
!hasFunctions.isPresent() || hasFunctions.get() == null
Напомним, что происходит внутри Optional::isPresent:
public boolean isPresent() {
return value != null;
}
Конкретно в этом случае нужно было оставить только вызов Optional::isPresent, так как Optional::get выбросит исключение, если значение действительно null.
Предупреждение PVS-Studio:
V6007 Expression 'hasFunctions.get() == null' is always false. FunqyHttpBuildStep.java 77
Фрагмент 18
if (typeInfo == null || (typeInfo != null &&
typeInfo.endsWith(SectionHelperFactory.HINT_METADATA))
) {
continue;
}
Здесь всё предельно просто: проверка typeInfo != null лишняя, так как обратная ситуация проверялась первой.
Предупреждение PVS-Studio:
V6007 Expression 'typeInfo != null' is always true. QuteProcessor.java 920
Фрагмент 19
if (persistenceProviderResolver == null ||
(persistenceProviderResolver != null // <=
&& !(persistenceProviderResolver instanceof
MultiplePersistenceProviderResolver
)
)
) {
....
}
Это срабатывание похоже на предыдущее, но здесь даже проверка на равенство с null лишняя. На самом деле, instanceof вернёт false, если передаваемый объект равен null. Учитывая, что здесь проверяется, что persistenceProviderResolver не имеет тип MultiplePersistenceProviderResolver, столь громоздкое условие if можно сократить до:
!(persistenceProviderResolver instanceof MultiplePersistenceProviderResolver)
Предупреждение PVS-Studio:
V6007 Expression 'persistenceProviderResolver != null' is always true. PersistenceProviderSetup.java 28
Фрагмент 20
for (String name : res.headers().names()) {
if (name.equalsIgnoreCase("Transfer-Encoding")) { // <=
continue; // ignore transfer encoding,
// chunked screws up message and response
}
for (String v : res.headers().getAll(name)) {
if (name.equalsIgnoreCase("Transfer-Encoding") // <=
&& v.contains("chunked")) {
continue;
}
responseBuilder.getMultiValueHeaders().add(name, v);
}
}
Переменную name проверяют на равенство со строкой Transfer-Encoding перед внутренним циклом и на каждой его итерации. Это лишняя операция, которая нагружает код и делает его менее компактным.
Предупреждение PVS-Studio:
V6007 Expression 'name.equalsIgnoreCase("Transfer-Encoding")' is always false. LambdaHttpHandler.java 104
Фрагмент 21
while (
!ResteasyReactiveDotNames.OBJECT.equals(currentClazz.name()) &&
currentClazz != null // <=
) {
....
}
Ну и на десерт просто вишенка на торте: сначала разыменовывают currentClazz и лишь потом проверяют, что она не null.
Предупреждение PVS-Studio:
V6060 The 'currentClazz' reference was utilized before it was verified against null. ResteasyReactiveProcessor.java 1125
Вот мы и рассмотрели интересные ошибки и подозрительные моменты, которые удалось найти в исходном коде Quarkus. Мы проверяли другие крупные проекты и находили там немало интересного:
Кстати, в репозитории Quarkus, как и во многих современных проектах, можно найти .md файлы для ИИ-агентов. Но насколько бы мощным инструментом ни был ИИ, даже он иногда совершает коварные ошибки — как, например, здесь:
А на этом мы эту статью завершаем. Если вам интересно проверить свой проект, предлагаем пробную версию нашего анализатора. Получить её можно здесь.
0